├── .github └── workflows │ └── greetings.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── COVERAGE ├── Dockerfile ├── HACKING ├── LICENSE ├── README ├── README.md ├── TODO ├── com.github.whipper_team.Whipper.metainfo.xml ├── man ├── Makefile ├── README.md ├── accuraterip-checksum.rst ├── whipper-accurip.rst ├── whipper-cd-info.rst ├── whipper-cd-rip.rst ├── whipper-cd.rst ├── whipper-drive-analyze.rst ├── whipper-drive-list.rst ├── whipper-drive.rst ├── whipper-image-verify.rst ├── whipper-image.rst ├── whipper-mblookup.rst ├── whipper-offset-find.rst ├── whipper-offset.rst └── whipper.rst ├── misc ├── header.py └── offsets.py ├── requirements.txt ├── scripts └── accuraterip-checksum ├── setup.py ├── src ├── README.md └── accuraterip-checksum.c └── whipper ├── __init__.py ├── __main__.py ├── command ├── __init__.py ├── accurip.py ├── basecommand.py ├── cd.py ├── drive.py ├── image.py ├── main.py ├── mblookup.py └── offset.py ├── common ├── __init__.py ├── accurip.py ├── checksum.py ├── common.py ├── config.py ├── directory.py ├── drive.py ├── encode.py ├── mbngs.py ├── path.py ├── program.py ├── renamer.py ├── task.py └── yaml.py ├── extern ├── __init__.py ├── asyncsub.py ├── freedb.py └── task │ ├── ChangeLog │ ├── __init__.py │ └── task.py ├── image ├── __init__.py ├── cue.py ├── image.py ├── table.py └── toc.py ├── program ├── __init__.py ├── arc.py ├── cdparanoia.py ├── cdrdao.py ├── flac.py ├── sox.py ├── soxi.py └── utils.py ├── result ├── __init__.py ├── logger.py └── result.py └── test ├── 76df3287-6cda-33eb-8e9a-044b5e15ffdd.jpg ├── __init__.py ├── bloc.cue ├── bloc.toc ├── breeders.cue ├── breeders.toc ├── cache └── result │ └── fe105a11.pickle ├── capital.1.toc ├── capital.2.toc ├── capital.fast.toc ├── cdparanoia.progress ├── cdparanoia.progress.error ├── cdparanoia.progress.strokes ├── cdparanoia ├── MATSHITA.cdparanoia-A.log ├── MATSHITA.cdparanoia-A.stderr ├── PX-L890SA.cdparanoia-A.log └── PX-L890SA.cdparanoia-A.stderr ├── cdrdao.readtoc.progress ├── common.py ├── cure.cue ├── cure.toc ├── dBAR-002-0000f21c-00027ef8-05021002.bin ├── dBAR-011-0010e284-009228a3-9809ff0b.bin ├── dBAR-020-002e5023-029d8e49-040eaa14.bin ├── diorama.cue ├── diorama_noutf8.toc ├── diorama_utf8.toc ├── gentlemen.fast.toc ├── jose.toc ├── kanye.cue ├── kings-separate.cue ├── kings-single.cue ├── ladyhawke.toc ├── release.08397059-86c1-463b-8ed0-cd596dbd174f.xml ├── release.93a6268c-ddf1-4898-bf93-fb862b1c5c5e.xml ├── release.c7d919f4-3ea0-4c4b-a230-b3605f069440.xml ├── silentalarm.result.pickle ├── strokes-someday.eac.cue ├── strokes-someday.toc ├── surferrosa.eac.corrected.cue ├── surferrosa.eac.currentgap.cue ├── surferrosa.eac.leftout.cue ├── surferrosa.eac.noncompliant.cue ├── surferrosa.eac.single.cue ├── surferrosa.toc ├── test_command_mblookup.py ├── test_common_accurip.py ├── test_common_common.py ├── test_common_config.py ├── test_common_directory.py ├── test_common_drive.py ├── test_common_mbngs.py ├── test_common_path.py ├── test_common_program.py ├── test_common_renamer.py ├── test_image_cue.py ├── test_image_table.py ├── test_image_toc.py ├── test_program_cdparanoia.py ├── test_program_cdrdao.py ├── test_program_sox.py ├── test_program_soxi.py ├── test_result_logger.log ├── test_result_logger.py ├── toc_cdtext_string_parsing.py ├── totbl.fast.toc ├── track-separate.cue ├── track-single.cue ├── track.flac ├── whipper.discid.xu338_M8WukSRi0J.KTlDoflB8Y-.pickle ├── whipper.release.410f99f8-a876-3416-bd8e-42233a00a477.json ├── whipper.release.6109ceed-7e21-490b-b5ad-3a66b4e4cfbb.json ├── whipper.release.61c6fd9b-18f8-4a45-963a-ba3c5d990cae.json ├── whipper.release.8478d4da-0cda-4e46-ae8c-1eeacfa5cf37.json ├── whipper.release.8a457e97-ed59-31f1-8b1c-41f24e9a7183.json ├── whipper.release.a76714e0-32b1-4ed4-b28e-f86d99642193.json ├── whipper.release.c56ff16e-1d81-47de-926f-ba22891bd2bd.json ├── whipper.release.d8e6153a-2c47-4804-9d73-0aac1081c3b1.json ├── whipper.release.e32ae79a-336e-4d33-945c-8c5e8206dbd3.json └── whipper.release.f484a9fc-db21-4106-9408-bcd105c90047.json /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: [pull_request_target, issues] 4 | 5 | jobs: 6 | greeting: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/first-interaction@v1 10 | with: 11 | repo-token: ${{ secrets.GITHUB_TOKEN }} 12 | issue-message: | 13 | 👋 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. 14 | 15 | 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). 16 | pr-message: '💖 Thanks for opening your first pull request here! 💖' 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | INSTALL 3 | py-compile 4 | REVISION 5 | *.o 6 | 7 | # For Python development using Eclipse IDE 8 | .project 9 | .pydevproject 10 | 11 | # setup.py install generated files 12 | build/ 13 | dist/ 14 | whipper.egg-info/ 15 | 16 | # From coverage report 17 | .coverage 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | dist: focal 3 | 4 | # This is needed by setuptools_scm to generate a correct version string 5 | git: 6 | depth: false 7 | 8 | language: python 9 | python: 10 | - "3.5" 11 | - "3.6" 12 | - "3.7" 13 | - "3.8" 14 | - "3.9" 15 | - "3.10-dev" 16 | virtualenv: 17 | system_site_packages: false 18 | 19 | cache: pip 20 | 21 | env: 22 | - FLAKE8=false MANPAGES=false 23 | 24 | jobs: 25 | allow_failures: 26 | - python: "3.5" 27 | - python: "3.10-dev" 28 | include: 29 | - python: 3.9 30 | env: FLAKE8=true MANPAGES=false 31 | - python: 3.9 32 | env: FLAKE8=false MANPAGES=true 33 | 34 | install: 35 | # Dependencies 36 | - sudo apt-get -qq update 37 | - sudo apt-get -qq install cd-paranoia cdrdao flac git libcdio-dev libdiscid-dev libgirepository1.0-dev libiso9660-dev libsndfile1-dev sox swig 38 | # Lock pycdio version to the right one for Ubuntu focal 39 | - pip install pycdio==2.1.0 40 | # flake8 and twisted are testing dependencies 41 | - pip install flake8 twisted -r requirements.txt 42 | # Docutils is needed to build the man pages 43 | - sudo apt-get -qq install python3-docutils 44 | # Installing 45 | - python setup.py install 46 | 47 | script: 48 | - if [ ! "$FLAKE8" = true ] && [ ! "$MANPAGES" = true ]; then python -m unittest discover; fi 49 | - if [ "$FLAKE8" = true ]; then flake8 --benchmark --statistics; fi 50 | - if [ "$MANPAGES" = true ]; then cd man && make; fi 51 | -------------------------------------------------------------------------------- /COVERAGE: -------------------------------------------------------------------------------- 1 | Coverage.py 5.5 text report against whipper v0.10.0 2 | 3 | $ coverage run --branch --omit='whipper/test/*' --source=whipper -m unittest discover 4 | $ coverage report -m 5 | 6 | Name Stmts Miss Branch BrPart Cover Missing 7 | ----------------------------------------------------------------------------- 8 | whipper/__init__.py 15 5 4 2 63% 9-12, 16, 18 9 | whipper/__main__.py 6 6 2 0 0% 4-13 10 | whipper/command/__init__.py 0 0 0 0 100% 11 | whipper/command/accurip.py 41 41 18 0 0% 21-90 12 | whipper/command/basecommand.py 68 29 30 8 52% 72, 74, 78, 84-90, 100-104, 109-116, 129, 131, 135, 141, 144-147 13 | whipper/command/cd.py 272 231 88 0 11% 81-89, 94-209, 212, 225, 252-324, 331-366, 369-594 14 | whipper/command/drive.py 57 57 10 0 0% 21-107 15 | whipper/command/image.py 36 36 6 0 0% 21-73 16 | whipper/command/main.py 74 74 24 0 0% 4-133 17 | whipper/command/mblookup.py 39 3 14 2 91% 47-49, 63->72, 65->72 18 | whipper/command/offset.py 115 115 36 0 0% 21-225 19 | whipper/common/__init__.py 0 0 0 0 100% 20 | whipper/common/accurip.py 115 4 56 5 95% 79, 116, 125, 131, 223->229, 233->239 21 | whipper/common/checksum.py 26 14 2 0 43% 41-42, 45-46, 49-64 22 | whipper/common/common.py 150 28 38 6 78% 51-52, 116-117, 128->131, 140-141, 156-163, 176, 185->192, 269-274, 279-284, 321->329, 323-327 23 | whipper/common/config.py 89 6 18 4 91% 107, 117, 119, 121, 147-148 24 | whipper/common/directory.py 12 5 2 0 50% 33-39 25 | whipper/common/drive.py 37 24 8 0 33% 36-41, 45-51, 55-61, 65-72, 95-98 26 | whipper/common/encode.py 80 52 12 0 30% 38-39, 42-43, 46-47, 54-57, 60-61, 64-65, 77-78, 81-82, 85-92, 99-100, 103-104, 117-148, 155-160 27 | whipper/common/mbngs.py 212 52 86 7 76% 40-41, 47, 119-125, 187->186, 245-246, 251-252, 305-306, 313-314, 344-346, 355, 382-392, 412-450 28 | whipper/common/path.py 22 0 12 0 100% 29 | whipper/common/program.py 380 288 134 6 21% 82->85, 91-93, 101-112, 121-137, 145-147, 152-156, 212, 228-229, 231-233, 234->238, 243, 261-278, 299-411, 423-491, 500-508, 521-537, 541-556, 582-622, 635-658, 661-681, 684-694, 697-705 30 | whipper/common/renamer.py 103 2 16 1 97% 58->66, 127, 152 31 | whipper/common/task.py 77 15 14 2 79% 45-50, 84-85, 100, 113-114, 119, 123, 127, 131, 135 32 | whipper/extern/__init__.py 0 0 0 0 100% 33 | whipper/extern/asyncsub.py 112 56 69 16 45% 15-17, 32, 37-38, 47-84, 89-102, 110->113, 115, 122, 125->123, 126->119, 134, 139->141, 141->152, 145-147, 151 34 | whipper/extern/freedb.py 90 72 42 0 17% 48, 56, 75-154, 171-210 35 | whipper/extern/task/__init__.py 0 0 0 0 100% 36 | whipper/extern/task/task.py 273 115 56 11 53% 52, 58, 64->66, 75, 83, 152-154, 166->exit, 174-176, 185-201, 217-220, 230->232, 235->exit, 241-242, 284-285, 288-294, 309-310, 318-320, 329-336, 340-357, 361, 364, 372-389, 402-403, 406-409, 413, 416, 432, 435-437, 455, 469, 502->504, 513-518, 525-530, 539-547, 550-558, 561-562, 570, 575-577 37 | whipper/image/__init__.py 0 0 0 0 100% 38 | whipper/image/cue.py 91 9 20 3 89% 96, 113-114, 129-131, 159, 188, 207 39 | whipper/image/image.py 123 100 20 0 16% 51-59, 68-70, 79-112, 124-167, 170-186, 195-225 40 | whipper/image/table.py 383 19 120 16 93% 195->198, 258, 276, 498, 531->535, 554->557, 577, 584->591, 673-674, 695-696, 703->709, 705-708, 736->740, 740->735, 762, 810-811, 813-814, 859-860, 865-867 41 | whipper/image/toc.py 203 16 60 10 90% 141, 222->230, 271-272, 288-291, 297->302, 333->340, 349-351, 373-375, 382->386, 398, 424, 457 42 | whipper/program/__init__.py 0 0 0 0 100% 43 | whipper/program/arc.py 3 1 0 0 67% 5 44 | whipper/program/cdparanoia.py 312 184 84 2 38% 45-47, 54-55, 119-121, 194-195, 233-247, 250-318, 321-359, 362-366, 369-405, 462-515, 520-567, 601-604, 607, 614, 622-627 45 | whipper/program/cdrdao.py 120 80 34 2 27% 35-64, 84-90, 94-109, 112-141, 144-148, 151-168, 173-176, 184-186, 190-192 46 | whipper/program/flac.py 9 5 0 0 44% 13-20 47 | whipper/program/sox.py 17 4 4 2 71% 18-19, 23-24 48 | whipper/program/soxi.py 28 2 4 1 91% 41, 54 49 | whipper/program/utils.py 23 16 2 0 28% 10-15, 21-27, 39-44 50 | whipper/result/__init__.py 0 0 0 0 100% 51 | whipper/result/logger.py 150 26 44 18 76% 67, 83-91, 111, 122, 127, 129, 133-134, 142, 144, 202, 213->217, 217->222, 222->226, 226->230, 240, 244-245, 250-251, 256-257 52 | whipper/result/result.py 59 13 6 0 71% 118-122, 137, 148-149, 158-165 53 | ----------------------------------------------------------------------------- 54 | TOTAL 4022 1805 1195 124 52% 55 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:buster 2 | ARG optical_gid 3 | ARG uid=1000 4 | 5 | RUN apt-get update && apt-get install --no-install-recommends -y \ 6 | autoconf \ 7 | automake \ 8 | cdrdao \ 9 | bzip2 \ 10 | curl \ 11 | eject \ 12 | flac \ 13 | git \ 14 | libdiscid0 \ 15 | libiso9660-dev \ 16 | libsndfile1-dev \ 17 | libtool \ 18 | locales \ 19 | make \ 20 | pkgconf \ 21 | python3-dev \ 22 | python3-musicbrainzngs \ 23 | python3-mutagen \ 24 | python3-pil \ 25 | python3-pip \ 26 | python3-ruamel.yaml \ 27 | python3-setuptools \ 28 | sox \ 29 | swig \ 30 | && apt-get clean && rm -rf /var/lib/apt/lists/* \ 31 | && pip3 --no-cache-dir install pycdio==2.1.0 discid 32 | 33 | # libcdio-paranoia / libcdio-utils are wrongfully packaged in Debian, thus built manually 34 | # see https://github.com/whipper-team/whipper/pull/237#issuecomment-367985625 35 | ENV LIBCDIO_VERSION 2.1.0 36 | RUN curl -o - "https://ftp.gnu.org/gnu/libcdio/libcdio-${LIBCDIO_VERSION}.tar.bz2" | tar jxf - \ 37 | && cd libcdio-${LIBCDIO_VERSION} \ 38 | && autoreconf -fi \ 39 | && ./configure --disable-dependency-tracking --disable-cxx --disable-example-progs --disable-static \ 40 | && make install \ 41 | && cd .. \ 42 | && rm -rf libcdio-${LIBCDIO_VERSION} 43 | 44 | # Install cd-paranoia from tarball 45 | ENV LIBCDIO_PARANOIA_VERSION 10.2+2.0.1 46 | RUN curl -o - "https://ftp.gnu.org/gnu/libcdio/libcdio-paranoia-${LIBCDIO_PARANOIA_VERSION}.tar.bz2" | tar jxf - \ 47 | && cd libcdio-paranoia-${LIBCDIO_PARANOIA_VERSION} \ 48 | && autoreconf -fi \ 49 | && ./configure --disable-dependency-tracking --disable-example-progs --disable-static \ 50 | && make install \ 51 | && cd .. \ 52 | && rm -rf libcdio-paranoia-${LIBCDIO_PARANOIA_VERSION} 53 | 54 | RUN ldconfig 55 | 56 | # add user (+ group workaround for ArchLinux) 57 | RUN useradd -m worker --uid ${uid} -G cdrom \ 58 | && if [ -n "${optical_gid}" ]; then groupadd -f -g "${optical_gid}" optical \ 59 | && usermod -a -G optical worker; fi \ 60 | && mkdir -p /output /home/worker/.config/whipper \ 61 | && chown worker: /output /home/worker/.config/whipper 62 | VOLUME ["/home/worker/.config/whipper", "/output"] 63 | 64 | # setup locales + cleanup 65 | RUN echo "LC_ALL=en_US.UTF-8" >> /etc/environment \ 66 | && echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen \ 67 | && echo "LANG=en_US.UTF-8" > /etc/locale.conf \ 68 | && locale-gen en_US.UTF-8 69 | 70 | # install whipper 71 | RUN mkdir /whipper 72 | COPY . /whipper/ 73 | RUN cd /whipper && python3 setup.py install \ 74 | && rm -rf /whipper \ 75 | && whipper -v 76 | 77 | ENV LC_ALL=en_US.UTF-8 78 | ENV LANG=en_US 79 | ENV LANGUAGE=en_US.UTF-8 80 | ENV PYTHONIOENCODING=utf-8 81 | 82 | USER worker 83 | WORKDIR /output 84 | ENTRYPOINT ["whipper"] 85 | -------------------------------------------------------------------------------- /HACKING: -------------------------------------------------------------------------------- 1 | style guide 2 | ----------- 3 | 4 | Where possible when writing new code, try to stay as PEP8 compliant as possible. 5 | We understand that large portions of the codebase are not, but part of our 6 | ongoing efforts is to clean up whipper. 7 | 8 | major data structures 9 | --------------------- 10 | 11 | - image.table.Track: A list of track properties, including a list of indexes, 12 | the isrc, cdtext, pre-emphasis flag, track number, and track type (audio 13 | or data) 14 | 15 | - image.table.Index: A list of track index properties, including the track's path, 16 | relative and absolute offsets within the track & disc, respectively, as 17 | well as the track number and counter. 18 | 19 | - image.table.Table: An ordered list of Track objects, with their leadouts, 20 | cdtext, and catalog numbers. 21 | 22 | - image.cue.CueFile: Generates an image.table.Table from a .cue file from an 23 | existing rip. 24 | 25 | - image.toc.TocFile: Generates an image.table.Table from a .toc file generated 26 | by `cdrdao read-toc`. 27 | 28 | notes 29 | ----- 30 | 31 | test: single rip of kings of leon - only by the night 32 | 33 | track 1: frame start 0, 17811 CD frames, 34 | track 2: frame start 17811, 18481 CD frames 35 | ARCue.pl says 2c15499a 36 | track 11: frame start 166858, 25103 CD frames (14760564 audio frames) 37 | 38 | 191961 total CD frames 39 | 40 | unicode 41 | ------- 42 | - All text files should be read and written as unicode. 43 | - All strings that came from the outside should be converted to unicode objects. 44 | - Use asserts liberally to ensure this so we catch problems earlier. 45 | - All gst.parse_launch() pipelines should be passed as utf-8; use 46 | encode('utf-8') 47 | - whipper.extern.log.log is not unicode-safe; don't pass it unicode objects; 48 | for example, always use %r to log paths 49 | - run with RIP_DEBUG=5 once in a while to catch unicode/logging errors. 50 | - Also use unicode prefix/suffix in tempfile.* methods; to force unicode. 51 | - filesystems on Unix do not have an encoding. file names are bytes. 52 | However, most distros default to a utf-8 interpretation 53 | - You can either treat paths as byte strings all the way without interpreting 54 | (even when writing them to other files), or assume utf-8 on in and out. 55 | - also direct output to a file; redirection sets codec to ASCII and brings out 56 | unicode bugs 57 | 58 | CDROMS 59 | ------ 60 | 61 | PLEXTOR CD-R PX-W8432T Read offset of device is: 355. 62 | 63 | test discs 64 | ---------- 65 | Julie Roberts - Julie Roberts: cdparanoia paranoid mode has a false positive 66 | jitter correction and silently rips the incorrect track. ripping with 67 | -Z rips the correct track. 68 | Rush - Test for Echo: has 31 frames of silence in the first track's pregap, 69 | test for HTOA detection regressions. 70 | The Strokes - Someday (promo): has 1 frame silence marked as SILENCE 71 | The Pixies - Surfer Rosa/Come on Pilgrim: has pre-gap, and INDEX 02 on TRACK 11 72 | Florence & The Machine - Lungs: data track 73 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | TODO: 2 | 3 | Please see https://github.com/whipper-team/whipper/milestones for further 4 | TODO items; this file exists only to have contents individually removed 5 | eventually, not to be continually updated. 6 | 7 | EASY 8 | 9 | - self.error() invokes exit() which is bad for a gui 10 | 11 | - change format to be %2d - %performer by default 12 | FIXME: why was this again? 13 | 14 | - at least mention the data track somewhere in the log 15 | 16 | - handle errors on cdrdao spawning (for example, not having cdrecorder, 17 | or not putting the disk in) 18 | 19 | - add rip offset verify, to verify that the current offset looks coorect 20 | 21 | - handle not having wav plugin: 22 | http://sprunge.us/DYjd 23 | DEBUG [13888] "ChecksumTask 0xb5e8b290" ChecksumTask Jul 25 19:09:47 bus_error_cb: bus , message (morituri/extern/task/gstreamer.py:211) 24 | DEBUG [13888] "ChecksumTask 0xb5e8b290" ChecksumTask Jul 25 19:09:47 set exception, 'exception GstException at /home/merlijn/archive/morituri/morituri/extern/task/gstreamer.py:217: bus_error_cb(): (, "/var/tmp/portage/media-libs/gst-plugins-base-0.10.36-r1/work/gst-plugins-base-0.10.36/gst/playback/gstdecodebin.c(1003): close_pad_link (): /GstPipeline:pipeline0/GstDecodeBin:decode:\\nNo decoder to handle media type \'audio/x-wav\'")' (morituri/extern/task/task.py:197) 25 | 26 | MEDIUM 27 | 28 | - after fixing relative, pregaps, and index 02, check when htoa is 0, 29 | and add a setSilence to table to set a counter 0 with no path, and test 30 | that the cue file puts a SILENCE/PREGAP 31 | 32 | - store drive features in a database 33 | 34 | - try http://www.ime.usp.br/~pjssilva/secure-cdparanoia.py and see if it 35 | is better at handling some bad cd's 36 | 37 | - add AccurateRip validation for ripped images to rip command 38 | 39 | - consider basing ripping progress not only on read (reaches 100% before 40 | writes are done) or writes (very bursty in cdparanoia) but a combo of the 41 | two, each counting for half. 42 | 43 | - rip task should abort on task 4 if checksums don't match 44 | 45 | - retry cdrdao a few times when it had to load the tray 46 | 47 | - do some character mangling so trail of dead is not in a hidden dir 48 | 49 | HARD 50 | 51 | - rip the data session 52 | 53 | - add GUI 54 | 55 | - write xbmc/plex plugin 56 | 57 | SPECIFIC RELEASES ISSUES 58 | 59 | - on ana, Goldfrapp tells me I have offset 0! 60 | 61 | - discs we should rip: 62 | LCD soundsystem disc 2 (data track) 63 | 64 | - Zita Swoon anthology cd 1 shows track 8 rip NOT accurate, but checksums match 65 | 66 | NO DECISION YET 67 | 68 | - possibly figure out how to name releases with credited artist; look at gorky and spiritualized electric mainline 69 | 70 | - check if cdda2wav or icedax analyze pregaps correctly 71 | 72 | OLD 73 | 74 | - check pregaps more than once, to see if results are consistent, or with 75 | different methods 76 | - check if it's simple to listen to each track in a multitrack completing 77 | - save trms to a pickle, after finishing each track 78 | - cache results of MusicBrainz lookups 79 | - don't keep short HTOA's if their peak level is low 80 | (see Pixies Planet of Sound single) 81 | - if disk not found in accuraterip, it doesn't mean that it's not accurate 82 | - burn ripped images 83 | - use a temp dir, until the whole rip is good don't move it, so we easily find 84 | half done rips 85 | Compare https://musicbrainz.org/cdtoc/MAj3xXf6QMy7G.BIFOyHyq4MySE- 86 | with https://musicbrainz.org/cdtoc/USC1utCZbTLZy80aHvQzJw4FASk- 87 | Almost same, but second is 2 seconds longer on last track, suggesting it 88 | was calculated wrong (150 frame offset done wrong ?) Can't find it in 89 | edit history though 90 | Write an example document with this cd as an example explaining offsets 91 | and id calculations 92 | - when shortening file name then reripping, it rerips since it doesn't know 93 | the shortened name; see sufjan stevens 94 | - if a disc output dir is already there, see if it was properly finished; 95 | complain if it was, to not overwrite 96 | - if multiple releases with different artist match the disc id, stop and 97 | let user continue by choosing one 98 | - artist-credit-phrase fabricated by musicbrainzngs only looks at name, not at artist-credit->name (see e.g. Gorky) 99 | - fix %r for normal case release name 100 | - decide whether output-dir should be part of the relative filenames of things; 101 | right now it is; maybe split in to base and output ? 102 | -------------------------------------------------------------------------------- /com.github.whipper_team.Whipper.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.github.whipper_team.Whipper 5 | CC0-1.0 6 | 7 | whipper 8 | GPL-3.0-or-later 9 | The Whipper Team 10 | A CD-DA ripper prioritising accuracy over speed 11 | 12 |

13 | whipper is a command-line CD-DA ripper that focuses on making accurate 14 | rips over fast ones. 15 |

16 |
17 | 18 | https://github.com/whipper-team/whipper 19 | https://github.com/whipper-team/whipper/issues 20 | https://github.com/whipper-team/whipper/blob/master/README.md 21 | 22 | 23 | AudioVideo 24 | Audio 25 | Music 26 | ConsoleOnly 27 | 28 | 29 | 30 | whipper 31 | whipper 32 | 33 |
34 | -------------------------------------------------------------------------------- /man/Makefile: -------------------------------------------------------------------------------- 1 | MAKEFLAGS += --silent 2 | 3 | build: 4 | for manpage in *.rst; do rst2man --exit-status=2 --report=1 $${manpage} "$${manpage%%.*}".1 ; done 5 | 6 | clean: 7 | rm *.1 8 | -------------------------------------------------------------------------------- /man/README.md: -------------------------------------------------------------------------------- 1 | The man pages in this directory can be generated using the `rst2man` command 2 | line tool provided by the Python `docutils` project: 3 | 4 | rst2man whipper.rst whipper.1 5 | 6 | Alternatively, you can also build all the man pages in this directory at the 7 | same time by running (requires `make`): 8 | 9 | make 10 | 11 | or this way (without make): 12 | 13 | for manpage in *.rst; do rst2man --exit-status=2 --report=1 --debug ${manpage} "${manpage%%.*}".1 ; done 14 | 15 | The directory can be cleaned of generated man pages by running: 16 | 17 | make clean 18 | 19 | or this way (without make): 20 | 21 | rm *.1 22 | -------------------------------------------------------------------------------- /man/accuraterip-checksum.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | accuraterip-checksum 3 | ==================== 4 | 5 | ---------------------------------------------------------- 6 | Compute the AccurateRip checksum of single track WAV files 7 | ---------------------------------------------------------- 8 | 9 | :Author: Louis-Philippe Véronneau 10 | :Date: 2022 11 | :Manual section: 1 12 | 13 | Synopsis 14 | ======== 15 | 16 | | accuraterip-checksum [**options**] ** ** ** 17 | 18 | Options 19 | ======= 20 | 21 | | **-h** | **--help** 22 | | Show this help message and exit 23 | 24 | | **--version** 25 | | Show version information 26 | 27 | | **--accuraterip-v1** | **--accuraterip-v2** 28 | | Explicitly choose AccurateRip checksum version (v2 is the default) 29 | 30 | See Also 31 | ======== 32 | 33 | whipper(1) 34 | -------------------------------------------------------------------------------- /man/whipper-accurip.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | whipper-accurip 3 | =============== 4 | 5 | ------------------------------ 6 | Handle AccurateRip information 7 | ------------------------------ 8 | 9 | :Author: Louis-Philippe Véronneau 10 | :Date: 2020 11 | :Manual section: 1 12 | 13 | Synopsis 14 | ======== 15 | 16 | | whipper accurip **show** ** 17 | | whipper accurip **-h** 18 | 19 | Arguments 20 | ========= 21 | 22 | | **show** ** Show AccurateRip data for the given URL 23 | 24 | Options 25 | ======= 26 | 27 | | **-h** | **--help** 28 | | Show this help message and exit 29 | 30 | See Also 31 | ======== 32 | 33 | whipper(1) 34 | -------------------------------------------------------------------------------- /man/whipper-cd-info.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | whipper-cd-info 3 | =============== 4 | 5 | ---------------------------------------------------- 6 | Retrieve information about the currently inserted CD 7 | ---------------------------------------------------- 8 | 9 | :Author: Louis-Philippe Véronneau 10 | :Date: 2020 11 | :Manual section: 1 12 | 13 | Synopsis 14 | ======== 15 | 16 | | whipper cd info [**-R** **] [**-p**] [**-c** **] 17 | | whipper cd info **-h** 18 | 19 | Options 20 | ======= 21 | 22 | | **-h** | **--help** 23 | | Show this help message and exit 24 | 25 | | **-R** ** | **--release-id** ** 26 | | MusicBrainz release id to match to (if there are multiple) 27 | 28 | | **-p** | **--prompt** 29 | | Prompt if there are multiple matching releases 30 | 31 | | **-c** ** | **--country** ** 32 | | Filter releases by country 33 | 34 | 35 | See Also 36 | ======== 37 | 38 | whipper(1), whipper-cd(1), whipper-cd-rip(1) 39 | -------------------------------------------------------------------------------- /man/whipper-cd-rip.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | whipper-cd-rip 3 | ============== 4 | 5 | --------- 6 | Rips a CD 7 | --------- 8 | 9 | :Author: Louis-Philippe Véronneau 10 | :Date: 2021 11 | :Manual section: 1 12 | 13 | Synopsis 14 | ======== 15 | 16 | | whipper cd rip [**options**] 17 | | whipper cd rip **-h** 18 | 19 | Options 20 | ======= 21 | 22 | | **-h** | **--help** 23 | | Show this help message and exit 24 | 25 | | **-R** ** | **--release-id** ** 26 | | MusicBrainz release id to match to (if there are multiple) 27 | 28 | | **-p** | **--prompt** 29 | | Prompt if there are multiple matching releases 30 | 31 | | **-c** ** | **--country** ** 32 | | Filter releases by country 33 | 34 | | **-L** ** 35 | | Logger to use 36 | 37 | | **-o** ** | **--offset** ** 38 | | Sample read offset 39 | 40 | | **-x** | **--force-overread** 41 | | Force overreading into the lead-out portion of the disc. Works only if 42 | | the patched cdparanoia package is installed and the drive supports this 43 | | feature 44 | 45 | | **-O** ** | **--output-directory** ** 46 | | Output directory; will be included in file paths in log 47 | 48 | | **-W** ** | **--working-directory** ** 49 | | Working directory; whipper will change to this directory and files will 50 | | be created relative to it when not absolute 51 | 52 | | **--track-template** ** 53 | | Template for track file naming 54 | 55 | | **--disc-template** ** 56 | | Template for disc file naming 57 | 58 | | **-U** | **--unknown** 59 | | whether to continue ripping if the CD is unknown 60 | 61 | | **--cdr** 62 | | whether to continue ripping if the disc is a CD-R 63 | 64 | | **-C** | **--cover-art** *file embed complete* 65 | | Fetch cover art and save it as standalone file, embed into FLAC files or 66 | | perform both actions: file, embed, complete option values respectively 67 | 68 | | **-r** | **--max-retries** ** 69 | | Number of rip attempts before giving up if can't rip a track. This 70 | | defaults to 5; 0 means infinity. 71 | 72 | | **-k** | **--keep-going** 73 | | continue ripping further tracks instead of giving up if a track can't be 74 | | ripped 75 | 76 | Template schemes 77 | ================ 78 | 79 | | Tracks are named according to the track template, filling in the variables 80 | | and adding the file extension. Variables exclusive to the track template are: 81 | 82 | | 83 | 84 | | - %t: track number 85 | | - %a: track artist 86 | | - %n: track title 87 | | - %s: track sort name 88 | 89 | | Disc files (.cue, .log, .m3u) are named according to the disc template, 90 | | filling in the variables and adding the file extension. Variables for both 91 | | disc and track template are: 92 | 93 | | 94 | 95 | | - %A: release artist 96 | | - %S: release sort name 97 | | - %B: release barcode 98 | | - %C: release catalog number 99 | | - %c: release disambiguation comment 100 | | - %d: release title (with disambiguation) 101 | | - %D: disc title (without disambiguation) 102 | | - %I: MusicBrainz Disc ID 103 | | - %M: total number of discs in the chosen release 104 | | - %N: number of current disc 105 | | - %T: medium title 106 | | - %y: release year 107 | | - %r: release type, lowercase 108 | | - %R: release type, normal case 109 | | - %x: audio extension, lowercase 110 | | - %X: audio extension, uppercase 111 | 112 | | Paths to track files referenced in .cue and .m3u files will be made 113 | | relative to the directory of the disc files. 114 | 115 | | All files will be created relative to the given output directory. 116 | | Log files will log the path to tracks relative to this directory 117 | 118 | See Also 119 | ======== 120 | 121 | whipper(1), whipper-cd(1), whipper-cd-info(1) 122 | -------------------------------------------------------------------------------- /man/whipper-cd.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | whipper-cd 3 | ========== 4 | 5 | ---------------------------------- 6 | Display and rip CD-DA and metadata 7 | ---------------------------------- 8 | 9 | :Author: Louis-Philippe Véronneau 10 | :Date: 2020 11 | :Manual section: 1 12 | 13 | Synopsis 14 | ======== 15 | 16 | | whipper cd **-d** ** [**subcommand**] 17 | | whipper cd **-h** 18 | 19 | Subcommands 20 | =========== 21 | 22 | | **info** Retrieve information about the currently inserted CD 23 | | **rip** Rip the CD 24 | 25 | | For more details on these subcommands, see their respective man pages. 26 | 27 | Options 28 | ======= 29 | 30 | | **-h** | **--help** 31 | | Show this help message and exit 32 | 33 | | **-d** ** | **--device** ** 34 | | Path to the CD-DA device 35 | 36 | See Also 37 | ======== 38 | 39 | whipper(1), whipper-cd-info(1), whipper-cd-rip(1) 40 | -------------------------------------------------------------------------------- /man/whipper-drive-analyze.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | whipper-drive-analyze 3 | ===================== 4 | 5 | -------------------------------------------------------------------- 6 | Determine whether cdparanoia can defeat the audio cache of the drive 7 | -------------------------------------------------------------------- 8 | 9 | :Author: Louis-Philippe Véronneau 10 | :Date: 2020 11 | :Manual section: 1 12 | 13 | Synopsis 14 | ======== 15 | 16 | | whipper drive analyze [**-d** **] 17 | | whipper drive analyze **-h** 18 | 19 | Options 20 | ======= 21 | 22 | | **-h** | **--help** 23 | | Show this help message and exit 24 | 25 | | **-d** ** | **--device** ** 26 | | Path to the CD-DA device 27 | 28 | See Also 29 | ======== 30 | 31 | whipper(1), whipper-drive(1), whipper-drive-list(1) 32 | -------------------------------------------------------------------------------- /man/whipper-drive-list.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | whipper-drive-analyze 3 | ===================== 4 | 5 | --------------------------- 6 | List available CD-DA drives 7 | --------------------------- 8 | 9 | :Author: Louis-Philippe Véronneau 10 | :Date: 2020 11 | :Manual section: 1 12 | 13 | Synopsis 14 | ======== 15 | 16 | | whipper drive list [**-h**] 17 | 18 | Options 19 | ======= 20 | 21 | | **-h** | **--help** 22 | | Show this help message and exit 23 | 24 | See Also 25 | ======== 26 | 27 | whipper(1), whipper-drive(1), whipper-drive-analyze(1) 28 | -------------------------------------------------------------------------------- /man/whipper-drive.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | whipper-drive 3 | ============= 4 | 5 | --------------- 6 | Drive utilities 7 | --------------- 8 | 9 | :Author: Louis-Philippe Véronneau 10 | :Date: 2020 11 | :Manual section: 1 12 | 13 | Synopsis 14 | ======== 15 | 16 | | whipper drive [**subcommand**] 17 | | whipper drive **-h** 18 | 19 | Subcommands 20 | =========== 21 | 22 | | **analyze** Analyze caching behaviour of drive 23 | | **list** List drives 24 | 25 | | For more details on these subcommands, see their respective man pages. 26 | 27 | Options 28 | ======= 29 | 30 | | **-h** | **--help** 31 | | Show this help message and exit 32 | 33 | See Also 34 | ======== 35 | 36 | whipper(1), whipper-drive-analyze(1), whipper-drive-list(1) 37 | -------------------------------------------------------------------------------- /man/whipper-image-verify.rst: -------------------------------------------------------------------------------- 1 | ==================== 2 | whipper-image-verify 3 | ==================== 4 | 5 | ----------------------------------------------------------------------------- 6 | Verifies the image from the given .cue files against the AccurateRip database 7 | ----------------------------------------------------------------------------- 8 | 9 | :Author: Louis-Philippe Véronneau 10 | :Date: 2020 11 | :Manual section: 1 12 | 13 | Synopsis 14 | ======== 15 | 16 | | whipper image verify ** 17 | | whipper image verify **-h** 18 | 19 | Options 20 | ======= 21 | 22 | | **-h** | **--help** 23 | | Show this help message and exit 24 | 25 | Arguments 26 | ========= 27 | 28 | | ** CUE file to load rip image from 29 | 30 | See Also 31 | ======== 32 | 33 | whipper(1), whipper-image(1) 34 | -------------------------------------------------------------------------------- /man/whipper-image.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | whipper-image 3 | ============= 4 | 5 | ------------------ 6 | Handle disc images 7 | ------------------ 8 | 9 | :Author: Louis-Philippe Véronneau 10 | :Date: 2020 11 | :Manual section: 1 12 | 13 | Synopsis 14 | ======== 15 | 16 | | whipper image [**subcommand**] 17 | | whipper image **-h** 18 | 19 | Subcommands 20 | =========== 21 | 22 | | **verify** Verify image 23 | 24 | | For more details on these subcommands, see their respective man pages. 25 | 26 | Options 27 | ======= 28 | 29 | | **-h** | **--help** 30 | | Show this help message and exit 31 | 32 | See Also 33 | ======== 34 | 35 | whipper(1), whipper-image-verify(1) 36 | -------------------------------------------------------------------------------- /man/whipper-mblookup.rst: -------------------------------------------------------------------------------- 1 | ================ 2 | whipper-mblookup 3 | ================ 4 | 5 | ------------------------------------------------------------------------- 6 | Look up either a MusicBrainz Disc ID or Release ID and output information 7 | ------------------------------------------------------------------------- 8 | 9 | :Author: Louis-Philippe Véronneau 10 | :Date: 2021 11 | :Manual section: 1 12 | 13 | Synopsis 14 | ======== 15 | 16 | | whipper mblookup ** 17 | | whipper mblookup **-h** 18 | 19 | Arguments 20 | ========= 21 | 22 | | ** MusicBrainz Disc ID or Release ID to look up 23 | 24 | Options 25 | ======= 26 | 27 | | **-h** | **--help** 28 | | Show this help message and exit 29 | 30 | Examples 31 | ======== 32 | 33 | You can lookup a MusicBrainz Disc ID and output its information this way:: 34 | 35 | whipper mblookup KnpGsLhvH.lPrNc1PBL21lb9Bg4- 36 | 37 | See Also 38 | ======== 39 | 40 | whipper(1) 41 | -------------------------------------------------------------------------------- /man/whipper-offset-find.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | whipper-offset-find 3 | =================== 4 | 5 | -------------------------------------------------------------------------------- 6 | Find drive's read offset by ripping tracks from a CD in the AccurateRip database 7 | -------------------------------------------------------------------------------- 8 | 9 | :Author: Louis-Philippe Véronneau 10 | :Date: 2020 11 | :Manual section: 1 12 | 13 | Synopsis 14 | ======== 15 | 16 | | whipper offset find [**-o** **] [**-d** **] 17 | | whipper offset find **-h** 18 | 19 | Options 20 | ======= 21 | 22 | | **-h** | **--help** 23 | | Show this help message and exit 24 | 25 | | **-o** ** | **--offsets** ** 26 | | List of offsets, comma-separated, colon-separated for range 27 | 28 | | **-d** ** | **--device** ** 29 | | Path to the CD-DA device 30 | 31 | See Also 32 | ======== 33 | 34 | whipper(1), whipper-offset(1) 35 | -------------------------------------------------------------------------------- /man/whipper-offset.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | whipper-offset 3 | ============== 4 | 5 | ------------------------------ 6 | Drive offset detection utility 7 | ------------------------------ 8 | 9 | :Author: Louis-Philippe Véronneau 10 | :Date: 2020 11 | :Manual section: 1 12 | 13 | Synopsis 14 | ======== 15 | 16 | | whipper offset [**subcommand**] 17 | | whipper offset **-h** 18 | 19 | Subcommands 20 | =========== 21 | 22 | | **find** Find drive read offset 23 | 24 | | For more details on these subcommands, see their respective man pages. 25 | 26 | Options 27 | ======= 28 | 29 | | **-h** | **--help** 30 | | Show this help message and exit 31 | 32 | See Also 33 | ======== 34 | 35 | whipper(1), whipper-offset-find(1) 36 | -------------------------------------------------------------------------------- /man/whipper.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | whipper 3 | ======= 4 | 5 | ---------------------------------------------------- 6 | A CD ripping utility focusing on accuracy over speed 7 | ---------------------------------------------------- 8 | 9 | :Author: Louis-Philippe Véronneau 10 | :Date: 2020 11 | :Manual section: 1 12 | 13 | Synopsis 14 | ======== 15 | 16 | | whipper [**subcommand**] 17 | | whipper [**-R**] [**-v**] [**-h**] [**-e** *{never failure success always}*] 18 | 19 | Description 20 | =========== 21 | 22 | | **whipper** is a CD ripping utility focusing on accuracy over speed that 23 | | supports multiple features. As such, **whipper**: 24 | 25 | | 26 | 27 | | * Detects correct read offset (in samples) 28 | | * Detects whether ripped media is a CD-R 29 | | * Has ability to defeat cache of drives 30 | | * Performs Test & Copy rips 31 | | * Verifies rip accuracy using the AccurateRip database 32 | | * Uses MusicBrainz for metadata lookup 33 | | * Supports reading the pre-emphasis flag embedded into some CDs (and 34 | | correctly tags the resulting rip) 35 | | * Detects and rips non digitally silent Hidden Track One Audio (HTOA) 36 | | * Provides batch ripping capabilities 37 | | * Provides templates for file and directory naming 38 | | * Supports lossless encoding of ripped audio tracks (FLAC) 39 | | * Allows extensibility through external logger plugins 40 | 41 | Options 42 | ======= 43 | 44 | | **-h** | **--help** 45 | | Show this help message and exit 46 | 47 | | **-e** | **--eject** *never failure success always* 48 | | When to eject disc (default: success) 49 | 50 | | **-c** | **--drive-auto-close** *True False* 51 | | Whether to auto close the drive's tray before reading a CD 52 | | (default: True) 53 | 54 | | **-R** | **--record** 55 | | Record API requests for playback 56 | 57 | | **-v** | **--version** 58 | | Show version information 59 | 60 | Subcommands 61 | =========== 62 | 63 | **whipper** gives you a tree of subcommands to work with, namely: 64 | 65 | | 66 | 67 | | * accurip 68 | | * cd 69 | | * drive 70 | | * image 71 | | * mblookup 72 | | * offset 73 | 74 | | For more details on these subcommands, see their respective man pages. 75 | 76 | Bugs 77 | ==== 78 | 79 | | Bugs can be reported to your distribution's bug tracker or upstream 80 | | at https://github.com/whipper-team/whipper/issues. 81 | 82 | See Also 83 | ======== 84 | 85 | whipper-accurip(1), whipper-cd(1), whipper-drive(1), whipper-image(1), 86 | whipper-mblookup(1), whipper-offset(1) 87 | -------------------------------------------------------------------------------- /misc/header.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; test-case-name: whipper.test.test_header -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | # Copyright (C) 2009 Thomas Vander Stichele 5 | 6 | # This file is part of whipper. 7 | # 8 | # whipper is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # whipper is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with whipper. If not, see . 20 | -------------------------------------------------------------------------------- /misc/offsets.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | # show all possible offsets, in order of popularity, from a download of 5 | # http://www.accuraterip.com/driveoffsets.htm 6 | 7 | import sys 8 | 9 | from bs4 import BeautifulSoup 10 | 11 | if len(sys.argv) < 2: 12 | print("Usage: %s driveoffsets_file" % sys.argv[0], file=sys.stderr) 13 | raise SystemExit(1) 14 | 15 | with open(sys.argv[1]) as f: 16 | doc = f.read() 17 | 18 | soup = BeautifulSoup(doc, features='html.parser') 19 | 20 | offsets = {} # offset -> total count 21 | 22 | # skip first two spurious elements 23 | rows = soup.findAll('tr')[2:] 24 | for row in rows: 25 | columns = row.findAll('td') 26 | if len(columns) == 4: 27 | first, second, third, fourth = columns 28 | name = first.find(text=True) 29 | offset = second.find(text=True) 30 | count = third.find(text=True) 31 | 32 | # only use numeric offsets 33 | try: 34 | int(offset) 35 | except ValueError: 36 | continue 37 | 38 | if offset not in offsets.keys(): 39 | offsets[offset] = 0 40 | offsets[offset] += int(count) 41 | 42 | # now sort offsets by count 43 | counts = [] 44 | for offset, count in offsets.items(): 45 | counts.append((count, offset)) 46 | 47 | counts.sort() 48 | counts.reverse() 49 | 50 | offsets = [] 51 | for count, offset in counts: 52 | offsets.append(offset) 53 | 54 | # now format it for code inclusion 55 | lines = [] 56 | line = 'OFFSETS = ("' 57 | 58 | for offset in offsets: 59 | line += offset + ', ' 60 | if len(line) > 60: 61 | line += '"' 62 | lines.append(line) 63 | line = ' "' 64 | 65 | # get last line too, trimming the comma and adding the quote 66 | if len(line) > 11: 67 | line = line[:-2] + '")' 68 | lines.append(line) 69 | 70 | print('\n'.join(lines)) 71 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | musicbrainzngs 2 | mutagen 3 | pycdio>0.20 4 | ruamel.yaml 5 | setuptools_scm 6 | discid 7 | -------------------------------------------------------------------------------- /scripts/accuraterip-checksum: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import accuraterip 5 | import sys 6 | 7 | 8 | if len(sys.argv) == 2 and sys.argv[1] == '--version': 9 | print('accuraterip-checksum version 2.0') 10 | raise SystemExit() 11 | 12 | use_v1 = None 13 | if len(sys.argv) == 4: 14 | offset = 0 15 | use_v1 = False 16 | elif len(sys.argv) == 5: 17 | offset = 1 18 | if sys.argv[1] == '--accuraterip-v1': 19 | use_v1 = True 20 | elif sys.argv[1] == '--accuraterip-v2': 21 | use_v1 = False 22 | 23 | if use_v1 is None: 24 | print('Syntax: accuraterip-checksum [--version / --accuraterip-v1 / ' 25 | '--accuraterip-v2 (default)] filename track_number total_tracks', 26 | file=sys.stderr) 27 | raise SystemExit(1) 28 | 29 | filename = sys.argv[offset + 1] 30 | track_number = int(sys.argv[offset + 2]) 31 | total_tracks = int(sys.argv[offset + 3]) 32 | 33 | v1, v2 = accuraterip.compute(filename, track_number, total_tracks) 34 | if use_v1: 35 | print('%08X' % v1) 36 | else: 37 | print('%08X' % v2) 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages, Extension 2 | 3 | setup( 4 | name="whipper", 5 | use_scm_version=True, 6 | description="a secure cd ripper preferring accuracy over speed", 7 | author=['Thomas Vander Stichele', 'The Whipper Team'], 8 | maintainer=['The Whipper Team'], 9 | url='https://github.com/whipper-team/whipper', 10 | license='GPL3', 11 | python_requires='>=3.6', 12 | packages=find_packages(), 13 | setup_requires=['setuptools_scm'], 14 | ext_modules=[ 15 | Extension('accuraterip', 16 | libraries=['sndfile'], 17 | sources=['src/accuraterip-checksum.c']) 18 | ], 19 | extras_require={ 20 | 'cover_art': ["pillow"] 21 | }, 22 | entry_points={ 23 | 'console_scripts': [ 24 | 'whipper = whipper.command.main:main' 25 | ] 26 | }, 27 | data_files=[ 28 | ('share/metainfo', ['com.github.whipper_team.Whipper.metainfo.xml']), 29 | ], 30 | scripts=[ 31 | 'scripts/accuraterip-checksum', 32 | ], 33 | ) 34 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # accuraterip-checksum 2 | 3 | ## Description 4 | A C99 command line program to compute the [AccurateRip](http://accuraterip.com/) checksum of single track WAV files, i.e. WAV files which contain only a single track of an audio CD. 5 | Such files can for example be generated by [Exact Audio Copy](http://exactaudiocopy.de/) and various other CD ripping programs, as listed e.g. [here](http://accuraterip.com/software.htm) and [here](https://wiki.hydrogenaud.io/index.php?title=AccurateRip). 6 | 7 | Implemented according to [this thread on HydrogenAudio](http://www.hydrogenaudio.org/forums/index.php?showtopic=97603). 8 | 9 | ## Usage 10 | Calculate AccurateRip v2 checksum of track number ```TRACK``` which is contained in WAV file ```TRACK_FILE```, and which was ripped from a disc with a total track count of ```TOTAL_TRACKS```: 11 | 12 | accuraterip-checksum TRACK_FILE TRACK TOTAL_TRACKS 13 | 14 | Explicitly choose AccurateRip checksum version, where ```VERSION``` is 1 or 2: 15 | 16 | accuraterip-checksum --accuraterip-vVERSION TRACK_FILE TRACK TOTAL_TRACKS 17 | 18 | Show accuraterip-checksum program version (this is **not** the AccurateRip checksum version!): 19 | 20 | accuraterip-checksum --version 21 | 22 | The version of accuraterip-checksum should be added to the tags of audio files which were processed using the output of accuraterip-checksum: 23 | If any severe bugs are ever found in accuraterip-checksum this will allow you to identify files which were tagged using affected version. 24 | 25 | ## Dependencies 26 | libsndfile is used for reading the WAV files. 27 | Therefore, on Ubuntu, make sure you have the following packages installed: 28 | 29 | libsndfile1 30 | 31 | For compiling you need: 32 | 33 | libsndfile1-dev 34 | 35 | ## Author 36 | Leo Bogert (http://leo.bogert.de) 37 | 38 | ## Version 39 | 1.5 40 | 41 | ## Donations 42 | bitcoin:14kPd2QWsri3y2irVFX6wC33vv7FqTaEBh 43 | 44 | ## License 45 | GPLv3 46 | -------------------------------------------------------------------------------- /src/accuraterip-checksum.c: -------------------------------------------------------------------------------- 1 | /* 2 | ============================================================================ 3 | Name : accuraterip-checksum.c 4 | Authors : Leo Bogert (http://leo.bogert.de), Andreas Oberritter 5 | License : GPLv3 6 | Description : A Python C extension to compute the AccurateRip checksum of WAV or FLAC tracks. 7 | Implemented according to http://www.hydrogenaudio.org/forums/index.php?showtopic=97603 8 | ============================================================================ 9 | */ 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | static bool check_fileformat(const SF_INFO *sfinfo) 21 | { 22 | #ifdef DEBUG 23 | printf("Channels: %i\n", sfinfo->channels); 24 | printf("Format: %X\n", sfinfo->format); 25 | printf("Frames: %li\n", sfinfo->frames); 26 | printf("Samplerate: %i\n", sfinfo->samplerate); 27 | printf("Sections: %i\n", sfinfo->sections); 28 | printf("Seekable: %i\n", sfinfo->seekable); 29 | #endif 30 | 31 | switch (sfinfo->format & SF_FORMAT_TYPEMASK) { 32 | case SF_FORMAT_WAV: 33 | case SF_FORMAT_FLAC: 34 | return (sfinfo->channels == 2) && 35 | (sfinfo->samplerate == 44100) && 36 | ((sfinfo->format & SF_FORMAT_SUBMASK) == SF_FORMAT_PCM_16); 37 | } 38 | 39 | return false; 40 | } 41 | 42 | static void *load_full_audiodata(SNDFILE *sndfile, const SF_INFO *sfinfo, size_t size) 43 | { 44 | void *data = malloc(size); 45 | 46 | if(data == NULL) 47 | return NULL; 48 | 49 | if(sf_readf_short(sndfile, data, sfinfo->frames) != sfinfo->frames) { 50 | free(data); 51 | return NULL; 52 | } 53 | 54 | return data; 55 | } 56 | 57 | static void compute_checksums(const uint32_t *audio_data, size_t audio_data_size, size_t track_number, size_t total_tracks, uint32_t *v1, uint32_t *v2) 58 | { 59 | uint32_t csum_hi = 0; 60 | uint32_t csum_lo = 0; 61 | uint32_t AR_CRCPosCheckFrom = 0; 62 | size_t Datauint32_tSize = audio_data_size / sizeof(uint32_t); 63 | uint32_t AR_CRCPosCheckTo = Datauint32_tSize; 64 | const size_t SectorBytes = 2352; // each sector 65 | uint32_t MulBy = 1; 66 | size_t i; 67 | 68 | if (track_number == 1) // first? 69 | AR_CRCPosCheckFrom += ((SectorBytes * 5) / sizeof(uint32_t)); 70 | if (track_number == total_tracks) // last? 71 | AR_CRCPosCheckTo -= ((SectorBytes * 5) / sizeof(uint32_t)); 72 | 73 | for (i = 0; i < Datauint32_tSize; i++) { 74 | if (MulBy >= AR_CRCPosCheckFrom && MulBy <= AR_CRCPosCheckTo) { 75 | uint64_t product = (uint64_t)audio_data[i] * (uint64_t)MulBy; 76 | csum_hi += (uint32_t)(product >> 32); 77 | csum_lo += (uint32_t)(product); 78 | } 79 | MulBy++; 80 | } 81 | 82 | *v1 = csum_lo; 83 | *v2 = csum_lo + csum_hi; 84 | } 85 | 86 | static PyObject *accuraterip_compute(PyObject *self, PyObject *args) 87 | { 88 | const char *filename; 89 | unsigned int track_number; 90 | unsigned int total_tracks; 91 | uint32_t v1, v2; 92 | void *audio_data; 93 | size_t size; 94 | SF_INFO sfinfo; 95 | SNDFILE *sndfile = NULL; 96 | 97 | if (!PyArg_ParseTuple(args, "sII", &filename, &track_number, &total_tracks)) 98 | goto err; 99 | 100 | if (track_number < 1 || track_number > total_tracks) { 101 | fprintf(stderr, "Invalid track_number!\n"); 102 | goto err; 103 | } 104 | 105 | if (total_tracks < 1 || total_tracks > 99) { 106 | fprintf(stderr, "Invalid total_tracks!\n"); 107 | goto err; 108 | } 109 | 110 | #ifdef DEBUG 111 | printf("Reading %s\n", filename); 112 | #endif 113 | 114 | memset(&sfinfo, 0, sizeof(sfinfo)); 115 | sndfile = sf_open(filename, SFM_READ, &sfinfo); 116 | if (sndfile == NULL) { 117 | fprintf(stderr, "sf_open failed! sf_error==%i\n", sf_error(NULL)); 118 | goto err; 119 | } 120 | 121 | if (!check_fileformat(&sfinfo)) { 122 | fprintf(stderr, "check_fileformat failed!\n"); 123 | goto err; 124 | } 125 | 126 | size = sfinfo.frames * sfinfo.channels * sizeof(uint16_t); 127 | audio_data = load_full_audiodata(sndfile, &sfinfo, size); 128 | if (audio_data == NULL) { 129 | fprintf(stderr, "load_full_audiodata failed!\n"); 130 | goto err; 131 | } 132 | 133 | compute_checksums(audio_data, size, track_number, total_tracks, &v1, &v2); 134 | free(audio_data); 135 | sf_close(sndfile); 136 | 137 | return Py_BuildValue("II", v1, v2); 138 | 139 | err: 140 | if (sndfile) 141 | sf_close(sndfile); 142 | return Py_BuildValue("OO", Py_None, Py_None); 143 | } 144 | 145 | static PyMethodDef accuraterip_methods[] = { 146 | { "compute", accuraterip_compute, METH_VARARGS, "Compute AccurateRip v1 and v2 checksums" }, 147 | { NULL, NULL, 0, NULL }, 148 | }; 149 | 150 | static struct PyModuleDef accuraterip_module = { 151 | .m_base = PyModuleDef_HEAD_INIT, 152 | .m_name = "accuraterip", 153 | .m_methods = accuraterip_methods, 154 | }; 155 | 156 | PyMODINIT_FUNC PyInit_accuraterip(void) 157 | { 158 | return PyModule_Create(&accuraterip_module); 159 | } 160 | -------------------------------------------------------------------------------- /whipper/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | 5 | from pkg_resources import (get_distribution, 6 | DistributionNotFound, RequirementParseError) 7 | try: 8 | __version__ = get_distribution(__name__).version 9 | except (DistributionNotFound, RequirementParseError): 10 | # not installed as package or is being run from source/git checkout 11 | from setuptools_scm import get_version 12 | __version__ = get_version() 13 | 14 | level = logging.INFO 15 | if 'WHIPPER_DEBUG' in os.environ: 16 | level = os.environ['WHIPPER_DEBUG'].upper() 17 | if 'WHIPPER_LOGFILE' in os.environ: 18 | logging.basicConfig(filename=os.environ['WHIPPER_LOGFILE'], 19 | filemode='w', level=level) 20 | else: 21 | logging.basicConfig(stream=sys.stderr, level=level) 22 | -------------------------------------------------------------------------------- /whipper/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | import os 5 | 6 | from whipper.command.main import main 7 | 8 | 9 | if __name__ == '__main__': 10 | # Make accuraterip_checksum be found automatically if it was built 11 | local_arb = os.path.join(os.path.dirname(__file__), '..', 'src') 12 | os.environ['PATH'] = ':'.join([os.getenv('PATH'), local_arb]) 13 | raise SystemExit(main()) 14 | -------------------------------------------------------------------------------- /whipper/command/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whipper-team/whipper/bc8b96d95647718d547aa5f8dc512241a7505cac/whipper/command/__init__.py -------------------------------------------------------------------------------- /whipper/command/accurip.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | # Copyright (C) 2009 Thomas Vander Stichele 5 | 6 | # This file is part of whipper. 7 | # 8 | # whipper is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # whipper is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with whipper. If not, see . 20 | 21 | from whipper.command.basecommand import BaseCommand 22 | from whipper.common.accurip import get_db_entry, ACCURATERIP_URL 23 | 24 | import logging 25 | logger = logging.getLogger(__name__) 26 | 27 | 28 | class Show(BaseCommand): 29 | summary = "show accuraterip data" 30 | description = """ 31 | retrieves and display accuraterip data from the given URL 32 | """ 33 | 34 | def add_arguments(self): 35 | self.parser.add_argument('url', action='store', 36 | help="accuraterip URL to load data from") 37 | 38 | def _strip_url_prefix(self, url): 39 | if self.options.url.startswith(ACCURATERIP_URL): 40 | return self.options.url[len(ACCURATERIP_URL):] 41 | return url 42 | 43 | def do(self): 44 | responses = get_db_entry(self._strip_url_prefix(self.options.url)) 45 | 46 | count = responses[0].num_tracks 47 | 48 | logger.info("found %d responses for %d tracks", len(responses), count) 49 | 50 | for (i, r) in enumerate(responses): 51 | if r.num_tracks != count: 52 | logger.warning("response %d has %d tracks instead of %d", 53 | i, r.num_tracks, count) 54 | 55 | # checksum and confidence by track 56 | for track in range(count): 57 | print("Track %d:" % (track + 1)) 58 | checksums = {} 59 | 60 | for (i, r) in enumerate(responses): 61 | if r.num_tracks != count: 62 | continue 63 | 64 | assert len(r.checksums) == r.num_tracks 65 | assert len(r.confidences) == r.num_tracks 66 | 67 | entry = {"confidence": r.confidences[track], "response": i + 1} 68 | checksum = r.checksums[track] 69 | if checksum in checksums: 70 | checksums[checksum].append(entry) 71 | else: 72 | checksums[checksum] = [entry, ] 73 | 74 | # now sort track results in checksum by highest confidence 75 | sortedChecksums = [] 76 | for checksum, entries in list(checksums.items()): 77 | highest = max(d['confidence'] for d in entries) 78 | sortedChecksums.append((highest, checksum)) 79 | 80 | sortedChecksums.sort() 81 | sortedChecksums.reverse() 82 | 83 | for highest, checksum in sortedChecksums: 84 | print(" %d result(s) for checksum %s: %s" % ( 85 | len(checksums[checksum]), 86 | checksum, checksums[checksum])) 87 | 88 | 89 | class AccuRip(BaseCommand): 90 | summary = "handle AccurateRip information" 91 | description = """ 92 | Handle AccurateRip information. Retrieves AccurateRip disc entries and 93 | displays diagnostic information. 94 | """ 95 | subcommands = { 96 | 'show': Show 97 | } 98 | -------------------------------------------------------------------------------- /whipper/command/drive.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | # Copyright (C) 2009 Thomas Vander Stichele 5 | 6 | # This file is part of whipper. 7 | # 8 | # whipper is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # whipper is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with whipper. If not, see . 20 | 21 | from whipper.command.basecommand import BaseCommand 22 | from whipper.common import config, drive 23 | from whipper.extern.task import task 24 | from whipper.program import cdparanoia 25 | 26 | import logging 27 | logger = logging.getLogger(__name__) 28 | 29 | 30 | class Analyze(BaseCommand): 31 | summary = "analyze caching behaviour of drive" 32 | description = """Determine whether cdparanoia can defeat the audio cache of the drive.""" # noqa: E501 33 | device_option = True 34 | 35 | def do(self): 36 | runner = task.SyncRunner() 37 | t = cdparanoia.AnalyzeTask(self.options.device) 38 | runner.run(t) 39 | 40 | if t.defeatsCache is None: 41 | logger.critical('cannot analyze the drive: is there a CD in it?') 42 | return 43 | if not t.defeatsCache: 44 | logger.info('cdparanoia cannot defeat the audio cache ' 45 | 'on this drive') 46 | else: 47 | logger.info('cdparanoia can defeat the audio cache on this drive') 48 | 49 | info = drive.getDeviceInfo(self.options.device) 50 | if not info: 51 | logger.error('drive caching behaviour not saved: ' 52 | 'could not get device info') 53 | return 54 | 55 | logger.info('adding drive cache behaviour to configuration file') 56 | 57 | config.Config().setDefeatsCache( 58 | info[0], info[1], info[2], t.defeatsCache) 59 | 60 | 61 | class List(BaseCommand): 62 | summary = "list drives" 63 | description = """list available CD-DA drives""" 64 | 65 | def do(self): 66 | paths = drive.getAllDevicePaths() 67 | self.config = config.Config() 68 | 69 | if not paths: 70 | logger.critical('no drives found. Create /dev/cdrom ' 71 | 'if you have a CD drive, or install ' 72 | 'pycdio for better detection') 73 | return 74 | 75 | try: 76 | import cdio as _ # noqa: F401 (TODO: fix it in a separate PR?) 77 | except ImportError: 78 | logger.error('install pycdio for vendor/model/release detection') 79 | return 80 | 81 | for path in paths: 82 | vendor, model, release = drive.getDeviceInfo(path) 83 | print("drive: %s, vendor: %s, model: %s, release: %s" % ( 84 | path, vendor, model, release)) 85 | 86 | try: 87 | offset = self.config.getReadOffset( 88 | vendor, model, release) 89 | print(" Configured read offset: %d" % offset) 90 | except KeyError: 91 | # Note spaces at the beginning for pretty terminal output 92 | logger.warning("no read offset found. " 93 | "Run 'whipper offset find'") 94 | 95 | try: 96 | defeats = self.config.getDefeatsCache( 97 | vendor, model, release) 98 | print(" Can defeat audio cache: %s" % defeats) 99 | except KeyError: 100 | logger.warning("unknown whether audio cache can be " 101 | "defeated. Run 'whipper drive analyze'") 102 | 103 | 104 | class Drive(BaseCommand): 105 | summary = "handle drives" 106 | description = """Drive utilities.""" 107 | subcommands = { 108 | 'analyze': Analyze, 109 | 'list': List 110 | } 111 | -------------------------------------------------------------------------------- /whipper/command/image.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | # Copyright (C) 2009 Thomas Vander Stichele 5 | 6 | # This file is part of whipper. 7 | # 8 | # whipper is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # whipper is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with whipper. If not, see . 20 | 21 | from whipper.command.basecommand import BaseCommand 22 | from whipper.common import accurip, config, program 23 | from whipper.extern.task import task 24 | from whipper.image import image 25 | from whipper.result import result 26 | 27 | import logging 28 | logger = logging.getLogger(__name__) 29 | 30 | 31 | class Verify(BaseCommand): 32 | summary = "verify image" 33 | description = """ 34 | Verifies the image from the given .cue files against the AccurateRip database. 35 | """ 36 | 37 | def add_arguments(self): 38 | self.parser.add_argument('cuefile', nargs='+', action='store', 39 | help="cue file to load rip image from") 40 | 41 | def do(self): 42 | prog = program.Program(config.Config()) 43 | runner = task.SyncRunner() 44 | 45 | for arg in self.options.cuefile: 46 | cueImage = image.Image(arg) 47 | cueImage.setup(runner) 48 | 49 | # FIXME: this feels like we're poking at internals. 50 | prog.cuePath = arg 51 | prog.result = result.RipResult() 52 | for track in cueImage.table.tracks: 53 | tr = result.TrackResult() 54 | tr.number = track.number 55 | prog.result.tracks.append(tr) 56 | 57 | verified = False 58 | try: 59 | verified = prog.verifyImage(runner, cueImage.table) 60 | except accurip.EntryNotFound: 61 | print('AccurateRip entry not found') 62 | accurip.print_report(prog.result) 63 | if not verified: 64 | raise SystemExit(1) 65 | 66 | 67 | class Image(BaseCommand): 68 | summary = "handle images" 69 | description = """ 70 | Handle disc images. Disc images are described by a .cue file. 71 | Disc images can be verified. 72 | """ 73 | subcommands = { 74 | 'verify': Verify, 75 | } 76 | -------------------------------------------------------------------------------- /whipper/command/mblookup.py: -------------------------------------------------------------------------------- 1 | from whipper.command.basecommand import BaseCommand 2 | from whipper.common.mbngs import musicbrainz, getReleaseMetadata 3 | 4 | import re 5 | 6 | 7 | class MBLookup(BaseCommand): 8 | summary = "lookup MusicBrainz entry" 9 | description = """Look up either a MusicBrainz Disc ID or Release ID and output information. 10 | 11 | You can get the MusicBrainz Disc ID with whipper cd info. 12 | 13 | Example Disc ID: KnpGsLhvH.lPrNc1PBL21lb9Bg4-""" 14 | 15 | def add_arguments(self): 16 | self.parser.add_argument( 17 | 'mbid', action='store', 18 | help="MusicBrainz Disc ID or Release ID to look up" 19 | ) 20 | 21 | @staticmethod 22 | def _printMetadata(md): 23 | """ 24 | Print out metadata received in a sensible way. 25 | 26 | :param md: MusicBrainz's metadata about the disc 27 | :type md: `DiscMetadata` 28 | """ 29 | print(' Artist: %s' % md.artist.encode('utf-8')) 30 | print(' Title: %s' % md.releaseTitle.encode('utf-8')) 31 | print(' Type: %s' % str(md.releaseType).encode('utf-8')) 32 | print(' URL: %s' % md.url) 33 | print(' Tracks: %d' % len(md.tracks)) 34 | if md.catalogNumber: 35 | print(' Cat no: %s' % md.catalogNumber) 36 | if md.barcode: 37 | print(' Barcode: %s' % md.barcode) 38 | 39 | for j, track in enumerate(md.tracks): 40 | print(' Track %2d: %s - %s' % ( 41 | j + 1, track.artist.encode('utf-8'), 42 | track.title.encode('utf-8') 43 | )) 44 | 45 | def do(self): 46 | try: 47 | mbid = str(self.options.mbid.strip()) 48 | except IndexError: 49 | print('Please specify a MusicBrainz Disc ID or Release ID.') 50 | return 3 51 | 52 | releaseIdMatch = re.match( 53 | r'^[\dA-Fa-f]{8}-(?:[\dA-Fa-f]{4}-){3}[\dA-Fa-f]{12}$', 54 | mbid 55 | ) 56 | discIdMatch = re.match( 57 | r'^[\dA-Za-z._]{27}-$', 58 | mbid 59 | ) 60 | 61 | # see https://musicbrainz.org/doc/MusicBrainz_Identifier 62 | if releaseIdMatch: 63 | md = getReleaseMetadata(releaseIdMatch.group(0)) 64 | if md: 65 | self._printMetadata(md) 66 | elif discIdMatch: 67 | metadatas = musicbrainz(discIdMatch.group(0)) 68 | 69 | print('%d releases' % len(metadatas)) 70 | for i, md in enumerate(metadatas): 71 | print('- Release %d:' % (i + 1, )) 72 | self._printMetadata(md) 73 | return None 74 | -------------------------------------------------------------------------------- /whipper/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whipper-team/whipper/bc8b96d95647718d547aa5f8dc512241a7505cac/whipper/common/__init__.py -------------------------------------------------------------------------------- /whipper/common/checksum.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; test-case-name: whipper.test.test_common_checksum -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | # Copyright (C) 2009 Thomas Vander Stichele 5 | 6 | # This file is part of whipper. 7 | # 8 | # whipper is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # whipper is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with whipper. If not, see . 20 | 21 | import binascii 22 | import wave 23 | import tempfile 24 | import subprocess 25 | import os 26 | 27 | 28 | from whipper.extern.task import task as etask 29 | 30 | import logging 31 | logger = logging.getLogger(__name__) 32 | 33 | # checksums are not CRC's. a CRC is a specific type of checksum. 34 | 35 | 36 | class CRC32Task(etask.Task): 37 | # TODO: Support sampleStart, sampleLength later on (should be trivial, just 38 | # add change the read part in _crc32 to skip some samples and/or not 39 | # read too far) 40 | def __init__(self, path, sampleStart=0, sampleLength=-1, is_wave=True): 41 | self.path = path 42 | self.is_wave = is_wave 43 | 44 | def start(self, runner): 45 | etask.Task.start(self, runner) 46 | self.schedule(0.0, self._crc32) 47 | 48 | def _crc32(self): 49 | if not self.is_wave: 50 | _, tmpf = tempfile.mkstemp() 51 | 52 | try: 53 | subprocess.check_call(['flac', '-d', self.path, '-fo', tmpf]) 54 | 55 | w = wave.open(tmpf) 56 | finally: 57 | os.remove(tmpf) 58 | else: 59 | w = wave.open(self.path) 60 | 61 | d = w._data_chunk.read() 62 | 63 | self.checksum = binascii.crc32(d) & 0xffffffff 64 | self.stop() 65 | -------------------------------------------------------------------------------- /whipper/common/directory.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; test-case-name: whipper.test.test_common_directory -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | # Copyright (C) 2013 Thomas Vander Stichele 5 | 6 | # This file is part of whipper. 7 | # 8 | # whipper is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # whipper is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with whipper. If not, see . 20 | 21 | from os import getenv, makedirs 22 | from os.path import join, expanduser 23 | 24 | 25 | def config_path(): 26 | path = join(getenv('XDG_CONFIG_HOME') or join(expanduser('~'), '.config'), 27 | 'whipper') 28 | makedirs(path, exist_ok=True) 29 | return join(path, 'whipper.conf') 30 | 31 | 32 | def data_path(name=None): 33 | path = join(getenv('XDG_DATA_HOME') or 34 | join(expanduser('~'), '.local/share'), 35 | 'whipper') 36 | if name: 37 | path = join(path, name) 38 | makedirs(path, exist_ok=True) 39 | return path 40 | -------------------------------------------------------------------------------- /whipper/common/drive.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; test-case-name: whipper.test.test_common_drive -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | # Copyright (C) 2009 Thomas Vander Stichele 5 | 6 | # This file is part of whipper. 7 | # 8 | # whipper is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # whipper is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with whipper. If not, see . 20 | 21 | import os 22 | from fcntl import ioctl 23 | 24 | import logging 25 | logger = logging.getLogger(__name__) 26 | 27 | 28 | def _listify(listOrString): 29 | if isinstance(listOrString, str): 30 | return [listOrString, ] 31 | 32 | return listOrString 33 | 34 | 35 | def getAllDevicePaths(): 36 | try: 37 | # see https://savannah.gnu.org/bugs/index.php?38477 38 | return [str(dev) for dev in _getAllDevicePathsPyCdio()] 39 | except ImportError: 40 | logger.info('cannot import pycdio') 41 | return _getAllDevicePathsStatic() 42 | 43 | 44 | def _getAllDevicePathsPyCdio(): 45 | import pycdio 46 | import cdio 47 | 48 | # using FS_AUDIO here only makes it list the drive when an audio cd 49 | # is inserted 50 | # ticket 102: this cdio call returns a list of str, or a single str 51 | return _listify(cdio.get_devices_with_cap(pycdio.FS_MATCH_ALL, False)) 52 | 53 | 54 | def _getAllDevicePathsStatic(): 55 | ret = [] 56 | 57 | for c in ['/dev/cdrom', '/dev/cdrecorder']: 58 | if os.path.exists(c): 59 | ret.append(c) 60 | 61 | return ret 62 | 63 | 64 | def getDeviceInfo(path): 65 | try: 66 | import cdio 67 | except ImportError: 68 | return None 69 | device = cdio.Device(path) 70 | _, vendor, model, release = device.get_hwinfo() 71 | 72 | return vendor, model, release 73 | 74 | 75 | def get_cdrom_drive_status(drive_path): 76 | """ 77 | Get the status of the disc drive. 78 | 79 | Drive status possibilities returned by CDROM_DRIVE_STATUS ioctl: 80 | - CDS_NO_INFO = 0 (if not implemented) 81 | - CDS_NO_DISC = 1 82 | - CDS_TRAY_OPEN = 2 83 | - CDS_DRIVE_NOT_READY = 3 84 | - CDS_DISC_OK = 4 85 | 86 | Documentation here: 87 | - https://www.kernel.org/doc/Documentation/ioctl/cdrom.txt 88 | - https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/cdrom.h # noqa: E501 89 | 90 | :param drive_path: path to the disc drive 91 | :type drive_path: str 92 | :returns: return code of the 'CDROM_DRIVE_STATUS' ioctl 93 | :rtype: int 94 | """ 95 | fd = os.open(drive_path, os.O_RDONLY | os.O_NONBLOCK) 96 | rc = ioctl(fd, 0x5326) # AKA 'CDROM_DRIVE_STATUS' 97 | os.close(fd) 98 | return rc 99 | -------------------------------------------------------------------------------- /whipper/common/encode.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; test-case-name: whipper.test.test_common_encode -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | # Copyright (C) 2009 Thomas Vander Stichele 5 | 6 | # This file is part of whipper. 7 | # 8 | # whipper is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # whipper is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with whipper. If not, see . 20 | 21 | 22 | from mutagen.flac import FLAC, Picture 23 | from mutagen.id3 import PictureType 24 | 25 | from whipper.extern.task import task 26 | 27 | from whipper.program import sox 28 | from whipper.program import flac 29 | 30 | import logging 31 | logger = logging.getLogger(__name__) 32 | 33 | 34 | class SoxPeakTask(task.Task): 35 | description = 'Calculating peak level' 36 | 37 | def __init__(self, track_path): 38 | self.track_path = track_path 39 | self.peak = None 40 | 41 | def start(self, runner): 42 | task.Task.start(self, runner) 43 | self.schedule(0.0, self._sox_peak) 44 | 45 | def _sox_peak(self): 46 | self.peak = sox.peak_level(self.track_path) 47 | self.stop() 48 | 49 | 50 | class FlacEncodeTask(task.Task): 51 | description = 'Encoding to FLAC' 52 | 53 | def __init__(self, track_path, track_out_path, what="track"): 54 | self.track_path = track_path 55 | self.track_out_path = track_out_path 56 | self.new_path = None 57 | self.description = 'Encoding %s to FLAC' % what 58 | 59 | def start(self, runner): 60 | task.Task.start(self, runner) 61 | self.schedule(0.0, self._flac_encode) 62 | 63 | def _flac_encode(self): 64 | flac.encode(self.track_path, self.track_out_path) 65 | self.stop() 66 | 67 | 68 | class TaggingTask(task.Task): 69 | # TODO: Wizzup: Do we really want this as 'Task'...? 70 | # I only made it a task for now because that it's easier to integrate in 71 | # program/cdparanoia.py - where whipper currently does the tagging. 72 | # We should just move the tagging to a more sensible place. 73 | 74 | description = 'Writing tags to FLAC' 75 | 76 | def __init__(self, track_path, tags): 77 | self.track_path = track_path 78 | self.tags = tags 79 | 80 | def start(self, runner): 81 | task.Task.start(self, runner) 82 | self.schedule(0.0, self._tag) 83 | 84 | def _tag(self): 85 | w = FLAC(self.track_path) 86 | 87 | for k, v in list(self.tags.items()): 88 | w[k] = v 89 | 90 | w.save() 91 | 92 | self.stop() 93 | 94 | 95 | class EmbedPictureTask(task.Task): 96 | description = 'Embed picture to FLAC' 97 | 98 | def __init__(self, track_path, cover_art_path): 99 | self.track_path = track_path 100 | self.cover_art_path = cover_art_path 101 | 102 | def start(self, runner): 103 | task.Task.start(self, runner) 104 | self.schedule(0.0, self._embed_picture) 105 | 106 | @staticmethod 107 | def _make_flac_picture(cover_art_filename): 108 | """ 109 | Given a path to a jpg/png file, return a FLAC picture for embedding. 110 | 111 | The embedding will be performed using the mutagen module. 112 | 113 | :param cover_art_filename: path to cover art image file 114 | :type cover_art_filename: str 115 | :returns: a valid FLAC picture for embedding 116 | :rtype: mutagen.flac.Picture or None 117 | """ 118 | if not cover_art_filename: 119 | return 120 | 121 | from PIL import Image 122 | 123 | im = Image.open(cover_art_filename) 124 | # NOTE: the cover art thumbnails we're getting from the Cover Art 125 | # Archive should be always in the JPEG format: this check is currently 126 | # useless but will leave it here to better handle unexpected formats. 127 | if im.format == 'JPEG': 128 | mime = 'image/jpeg' 129 | elif im.format == 'PNG': 130 | mime = 'image/png' 131 | else: 132 | # we only support png and jpeg 133 | logger.warning("no cover art will be added because the fetched " 134 | "image format is unsupported") 135 | return 136 | 137 | pic = Picture() 138 | with open(cover_art_filename, 'rb') as f: 139 | pic.data = f.read() 140 | 141 | pic.type = PictureType.COVER_FRONT 142 | pic.mime = mime 143 | pic.width, pic.height = im.size 144 | if im.mode not in ('P', 'RGB', 'SRGB'): 145 | logger.warning("no cover art will be added because the fetched " 146 | "image mode is unsupported") 147 | return 148 | 149 | return pic 150 | 151 | def _embed_picture(self): 152 | """ 153 | Get flac picture generated from mutagen.flac.Picture then embed 154 | it to given track if the flac picture exists. 155 | """ 156 | flac_pic = self._make_flac_picture(self.cover_art_path) 157 | if flac_pic: 158 | w = FLAC(self.track_path) 159 | w.add_picture(flac_pic) 160 | w.save() 161 | 162 | self.stop() 163 | -------------------------------------------------------------------------------- /whipper/common/path.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; test-case-name: whipper.test.test_common_path -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | # Copyright (C) 2009 Thomas Vander Stichele 5 | 6 | # This file is part of whipper. 7 | # 8 | # whipper is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # whipper is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with whipper. If not, see . 20 | 21 | import re 22 | 23 | 24 | class PathFilter: 25 | """Filter path components for safe storage on file systems.""" 26 | 27 | def __init__(self, dot=True, posix=True, vfat=False, whitespace=False, 28 | printable=False): 29 | """ 30 | Init PathFilter. 31 | 32 | :param dot: whether to strip leading dot 33 | :param posix: whether to strip illegal chars in *nix OSes 34 | :param vfat: whether to strip illegal chars in VFAT filesystems 35 | :param whitespace: whether to strip all whitespace chars 36 | :param printable: whether to strip all non printable ASCII chars 37 | """ 38 | self._dot = dot 39 | self._posix = posix 40 | self._vfat = vfat 41 | self._whitespace = whitespace 42 | self._printable = printable 43 | 44 | def filter(self, path): 45 | R_CH = '_' 46 | if self._dot: 47 | if path[:1] == '.': # Slicing tolerant to empty strings 48 | path = R_CH + path[1:] 49 | if self._posix: 50 | path = re.sub(r'[/\x00]', R_CH, path) 51 | if self._vfat: 52 | path = re.sub(r'[\x00-\x1F\x7F\"*/:<>?\\|]', R_CH, path) 53 | if self._whitespace: 54 | path = re.sub(r'\s', R_CH, path) 55 | if self._printable: 56 | path = re.sub(r'[^\x20-\x7E]', R_CH, path) 57 | return path 58 | -------------------------------------------------------------------------------- /whipper/common/task.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | import os 5 | import signal 6 | import subprocess 7 | 8 | from whipper.extern import asyncsub 9 | from whipper.extern.task import task 10 | 11 | import logging 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class SyncRunner(task.SyncRunner): 16 | pass 17 | 18 | 19 | class LoggableTask(task.Task): 20 | pass 21 | 22 | 23 | class LoggableMultiSeparateTask(task.MultiSeparateTask): 24 | pass 25 | 26 | 27 | class PopenTask(task.Task): 28 | """Task that runs a command using Popen.""" 29 | 30 | logCategory = 'PopenTask' 31 | bufsize = 1024 32 | command = None 33 | cwd = None 34 | 35 | def start(self, runner): 36 | task.Task.start(self, runner) 37 | 38 | try: 39 | self._popen = asyncsub.Popen(self.command, 40 | bufsize=self.bufsize, 41 | stdin=subprocess.PIPE, 42 | stdout=subprocess.PIPE, 43 | stderr=subprocess.PIPE, 44 | close_fds=True, cwd=self.cwd) 45 | except OSError as e: 46 | import errno 47 | if e.errno == errno.ENOENT: 48 | self.commandMissing() 49 | 50 | raise 51 | 52 | logger.debug('started %r with pid %d', self.command, self._popen.pid) 53 | 54 | self.schedule(1.0, self._read, runner) 55 | 56 | def _read(self, runner): 57 | try: 58 | read = False 59 | 60 | ret = self._popen.recv() 61 | 62 | if ret: 63 | logger.debug("read from stdout: %s", ret) 64 | self.readbytesout(ret) 65 | read = True 66 | 67 | ret = self._popen.recv_err() 68 | 69 | if ret: 70 | logger.debug("read from stderr: %s", ret) 71 | self.readbyteserr(ret) 72 | read = True 73 | 74 | # if we read anything, we might have more to read, so 75 | # reschedule immediately 76 | if read and self.runner: 77 | self.schedule(0.0, self._read, runner) 78 | return 79 | 80 | # if we didn't read anything, give the command more time to 81 | # produce output 82 | if self._popen.poll() is None and self.runner: 83 | # not finished yet 84 | self.schedule(1.0, self._read, runner) 85 | return 86 | 87 | self._done() 88 | # FIXME: catching too general exception (Exception) 89 | except Exception as e: 90 | logger.debug('exception during _read(): %s', e) 91 | self.setException(e) 92 | self.stop() 93 | 94 | def _done(self): 95 | assert self._popen.returncode is not None, "No returncode" 96 | 97 | if self._popen.returncode >= 0: 98 | logger.debug('return code was %d', self._popen.returncode) 99 | else: 100 | logger.debug('terminated with signal %d', -self._popen.returncode) 101 | 102 | self.setProgress(1.0) 103 | 104 | if self._popen.returncode != 0: 105 | self.failed() 106 | else: 107 | self.done() 108 | 109 | self.stop() 110 | return 111 | 112 | def abort(self): 113 | logger.debug('aborting, sending SIGTERM to %d', self._popen.pid) 114 | os.kill(self._popen.pid, signal.SIGTERM) 115 | # self.stop() 116 | 117 | def readbytesout(self, bytes_stdout): 118 | """Call when bytes have been read from stdout.""" 119 | pass 120 | 121 | def readbyteserr(self, bytes_stderr): 122 | """Call when bytes have been read from stderr.""" 123 | pass 124 | 125 | def done(self): 126 | """Call when the command completed successfully.""" 127 | pass 128 | 129 | def failed(self): 130 | """Call when the command failed.""" 131 | pass 132 | 133 | def commandMissing(self): 134 | """Call when the command is missing.""" 135 | pass 136 | -------------------------------------------------------------------------------- /whipper/common/yaml.py: -------------------------------------------------------------------------------- 1 | from ruamel.yaml import YAML as ruamel_YAML 2 | from ruamel.yaml.compat import StringIO 3 | 4 | 5 | # https://yaml.readthedocs.io/en/latest/example.html#output-of-dump-as-a-string 6 | class YAML(ruamel_YAML): 7 | def __init__(self, *args, **kwargs): 8 | super().__init__() 9 | self.width = 4000 10 | self.default_flow_style = False 11 | 12 | def dump(self, data, stream=None, **kw): 13 | inefficient = False 14 | if stream is None: 15 | inefficient = True 16 | stream = StringIO() 17 | ruamel_YAML.dump(self, data, stream, **kw) 18 | if inefficient: 19 | return stream.getvalue() 20 | -------------------------------------------------------------------------------- /whipper/extern/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whipper-team/whipper/bc8b96d95647718d547aa5f8dc512241a7505cac/whipper/extern/__init__.py -------------------------------------------------------------------------------- /whipper/extern/asyncsub.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | # from http://code.activestate.com/recipes/440554/ 5 | 6 | import os 7 | import subprocess 8 | import errno 9 | import time 10 | import sys 11 | 12 | PIPE = subprocess.PIPE 13 | 14 | if sys.platform == 'win32': 15 | from win32file import ReadFile, WriteFile 16 | from win32pipe import PeekNamedPipe 17 | import msvcrt 18 | else: 19 | import select 20 | import fcntl 21 | 22 | 23 | class Popen(subprocess.Popen): 24 | 25 | def recv(self, maxsize=None): 26 | return self._recv('stdout', maxsize) 27 | 28 | def recv_err(self, maxsize=None): 29 | return self._recv('stderr', maxsize) 30 | 31 | def send_recv(self, in_put='', maxsize=None): 32 | return self.send(in_put), self.recv(maxsize), self.recv_err(maxsize) 33 | 34 | def get_conn_maxsize(self, which, maxsize): 35 | if maxsize is None: 36 | maxsize = 1024 37 | elif maxsize < 1: 38 | maxsize = 1 39 | return getattr(self, which), maxsize 40 | 41 | def _close(self, which): 42 | getattr(self, which).close() 43 | setattr(self, which, None) 44 | 45 | if sys.platform == 'win32': 46 | 47 | def send(self, in_put): 48 | if not self.stdin: 49 | return None 50 | 51 | try: 52 | x = msvcrt.get_osfhandle(self.stdin.fileno()) 53 | (errCode, written) = WriteFile(x, in_put) 54 | except ValueError: 55 | return self._close('stdin') 56 | except (subprocess.pywintypes.error, Exception) as why: 57 | if why.args[0] in (109, errno.ESHUTDOWN): 58 | return self._close('stdin') 59 | raise 60 | 61 | return written 62 | 63 | def _recv(self, which, maxsize): 64 | conn, maxsize = self.get_conn_maxsize(which, maxsize) 65 | if conn is None: 66 | return None 67 | 68 | try: 69 | x = msvcrt.get_osfhandle(conn.fileno()) 70 | (read, nAvail, nMessage) = PeekNamedPipe(x, 0) 71 | if maxsize < nAvail: 72 | nAvail = maxsize 73 | if nAvail > 0: 74 | (errCode, read) = ReadFile(x, nAvail, None) 75 | except ValueError: 76 | return self._close(which) 77 | except (subprocess.pywintypes.error, Exception) as why: 78 | if why.args[0] in (109, errno.ESHUTDOWN): 79 | return self._close(which) 80 | raise 81 | 82 | if self.universal_newlines: 83 | read = self._translate_newlines(read) 84 | return read 85 | 86 | else: 87 | 88 | def send(self, in_put): 89 | if not self.stdin: 90 | return None 91 | 92 | if not select.select([], [self.stdin], [], 0)[1]: 93 | return 0 94 | 95 | try: 96 | written = os.write(self.stdin.fileno(), in_put) 97 | except OSError as why: 98 | if why.args[0] == errno.EPIPE: # broken pipe 99 | return self._close('stdin') 100 | raise 101 | 102 | return written 103 | 104 | def _recv(self, which, maxsize): 105 | conn, maxsize = self.get_conn_maxsize(which, maxsize) 106 | if conn is None: 107 | return None 108 | 109 | flags = fcntl.fcntl(conn, fcntl.F_GETFL) 110 | if not conn.closed: 111 | fcntl.fcntl(conn, fcntl.F_SETFL, flags | os.O_NONBLOCK) 112 | 113 | try: 114 | if not select.select([conn], [], [], 0)[0]: 115 | return '' 116 | 117 | r = conn.read(maxsize) 118 | if not r: 119 | return self._close(which) 120 | 121 | if self.universal_newlines: 122 | r = self._translate_newlines(r) 123 | return r 124 | finally: 125 | if not conn.closed: 126 | fcntl.fcntl(conn, fcntl.F_SETFL, flags) 127 | 128 | 129 | message = "Other end disconnected!" 130 | 131 | 132 | def recv_some(p, t=.1, e=1, tr=5, stderr=0): 133 | if tr < 1: 134 | tr = 1 135 | x = time.time() + t 136 | y = [] 137 | r = '' 138 | pr = p.recv 139 | if stderr: 140 | pr = p.recv_err 141 | while time.time() < x or r: 142 | r = pr() 143 | if r is None: 144 | if e: 145 | raise Exception(message) 146 | else: 147 | break 148 | elif r: 149 | y.append(r) 150 | else: 151 | time.sleep(max((x - time.time()) / tr, 0)) 152 | return ''.join(x.decode() for x in y).encode() 153 | -------------------------------------------------------------------------------- /whipper/extern/task/ChangeLog: -------------------------------------------------------------------------------- 1 | 2012-11-18 Thomas Vander Stichele 2 | 3 | * gstreamer.py: 4 | Only set an exception once in bus_error_cb. 5 | Was triggered by morituri's checksum test, but only 6 | if multiple tests were run - got the same bus error 7 | twice. 8 | 9 | 2012-07-12 Thomas Vander Stichele 10 | 11 | * task.py: 12 | Add a debug statement. 13 | 14 | 2011-08-15 Thomas Vander Stichele 15 | 16 | * task.py: 17 | Better logging when scheduling. 18 | * gstreamer.py: 19 | If paused() returns True, don't go to playing. 20 | add a method for querying duration in the common case. 21 | 22 | 2011-08-08 Thomas Vander Stichele 23 | 24 | * task.py: 25 | Remove scrubFilename call. 26 | 27 | 2011-08-08 Thomas Vander Stichele 28 | 29 | * task.py: 30 | Pull in getExceptionMessage privately. 31 | 32 | 2011-08-05 Thomas Vander Stichele 33 | 34 | * gstreamer.py: 35 | * task.py: 36 | Don't rely on the log module; users that want to log 37 | should first subclass from a log class that implements 38 | warning/info/debug/log 39 | 40 | 2011-08-05 Thomas Vander Stichele 41 | 42 | * gstreamer.py: 43 | Document bus and pipeline. Make bus public. 44 | 45 | 2011-08-05 Thomas Vander Stichele 46 | 47 | * gstreamer.py: 48 | Add quoteParse() method. 49 | 50 | 2011-08-05 Thomas Vander Stichele 51 | 52 | * gstreamer.py: 53 | Add getPipeline() method. 54 | Base class implementation uses getPipelineDesc(). 55 | 56 | -------------------------------------------------------------------------------- /whipper/extern/task/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whipper-team/whipper/bc8b96d95647718d547aa5f8dc512241a7505cac/whipper/extern/task/__init__.py -------------------------------------------------------------------------------- /whipper/image/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whipper-team/whipper/bc8b96d95647718d547aa5f8dc512241a7505cac/whipper/image/__init__.py -------------------------------------------------------------------------------- /whipper/program/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whipper-team/whipper/bc8b96d95647718d547aa5f8dc512241a7505cac/whipper/program/__init__.py -------------------------------------------------------------------------------- /whipper/program/arc.py: -------------------------------------------------------------------------------- 1 | import accuraterip 2 | 3 | 4 | def accuraterip_checksum(f, track_number, total_tracks): 5 | return accuraterip.compute(f, track_number, total_tracks) 6 | -------------------------------------------------------------------------------- /whipper/program/flac.py: -------------------------------------------------------------------------------- 1 | from subprocess import check_call, CalledProcessError 2 | 3 | import logging 4 | logger = logging.getLogger(__name__) 5 | 6 | 7 | def encode(infile, outfile): 8 | """ 9 | Encode infile to outfile, with flac. 10 | 11 | Uses ``-f`` because whipper already creates the file. 12 | """ 13 | try: 14 | # TODO: Replace with Popen so that we can catch stderr and write it to 15 | # logging 16 | check_call(['flac', '--silent', '--verify', '-o', outfile, 17 | '-f', infile]) 18 | except CalledProcessError: 19 | logger.exception('flac failed') 20 | raise 21 | -------------------------------------------------------------------------------- /whipper/program/sox.py: -------------------------------------------------------------------------------- 1 | import os 2 | from subprocess import Popen, PIPE 3 | 4 | import logging 5 | logger = logging.getLogger(__name__) 6 | 7 | SOX = 'sox' 8 | 9 | 10 | def peak_level(track_path): 11 | """ 12 | Accept a path to a sox-decodable audio file. 13 | 14 | :returns: track peak level from sox ('maximum amplitude') 15 | :rtype: float or None 16 | """ 17 | if not os.path.exists(track_path): 18 | logger.warning("SoX peak detection failed: file not found") 19 | return None 20 | sox = Popen([SOX, track_path, "-n", "stats", "-b", "16"], stderr=PIPE) 21 | _, err = sox.communicate() 22 | if sox.returncode: 23 | logger.warning("SoX peak detection failed: %s", sox.returncode) 24 | return None 25 | # relevant captured lines looks like this: 26 | # Min level -26215 27 | # Max level 26215 28 | min_level = int(err.splitlines()[2].split()[2]) 29 | max_level = int(err.splitlines()[3].split()[2]) 30 | return max(abs(min_level), abs(max_level)) 31 | -------------------------------------------------------------------------------- /whipper/program/soxi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from whipper.common import common 4 | from whipper.common import task as ctask 5 | 6 | import logging 7 | logger = logging.getLogger(__name__) 8 | 9 | SOXI = 'soxi' 10 | 11 | 12 | class AudioLengthTask(ctask.PopenTask): 13 | """ 14 | Calculate the length of a track in audio samples. 15 | 16 | :cvar length: length of the decoded audio file, in audio samples 17 | :vartype length: int 18 | """ 19 | 20 | logCategory = 'AudioLengthTask' 21 | description = 'Getting length of audio track' 22 | length = None 23 | 24 | def __init__(self, path): 25 | """ 26 | Init AudioLengthTask. 27 | 28 | :param path: path to audio track 29 | :type path: str 30 | """ 31 | assert isinstance(path, str), "%r is not str" % path 32 | 33 | self.logName = os.path.basename(path) 34 | 35 | self.command = [SOXI, '-s', path] 36 | 37 | self._error = [] 38 | self._output = [] 39 | 40 | def commandMissing(self): 41 | raise common.MissingDependencyException('soxi') 42 | 43 | def readbytesout(self, bytes_stdout): 44 | self._output.append(bytes_stdout) 45 | 46 | def readbyteserr(self, bytes_stderr): 47 | self._error.append(bytes_stderr) 48 | 49 | def failed(self): 50 | self.setException(Exception("soxi failed: %s" % "".join(self._error))) 51 | 52 | def done(self): 53 | if self._error: 54 | logger.warning("soxi reported on stderr: %s", "".join(self._error)) 55 | self.length = int("".join(o.decode() for o in self._output)) 56 | -------------------------------------------------------------------------------- /whipper/program/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | import logging 5 | logger = logging.getLogger(__name__) 6 | 7 | 8 | def eject_device(device): 9 | """Eject the given device.""" 10 | logger.debug("ejecting device %s", device) 11 | try: 12 | # `eject device` prints nothing to stdout 13 | subprocess.check_output(['eject', device], stderr=subprocess.STDOUT) 14 | except subprocess.CalledProcessError as e: 15 | logger.warning("command '%s' returned with exit code '%d' (%s)", 16 | ' '.join(e.cmd), e.returncode, e.output.rstrip()) 17 | 18 | 19 | def load_device(device): 20 | """Load the given device.""" 21 | logger.debug("loading (eject -t) device %s", device) 22 | try: 23 | # `eject -t device` prints nothing to stdout 24 | subprocess.check_output(['eject', '-t', device], 25 | stderr=subprocess.STDOUT) 26 | except subprocess.CalledProcessError as e: 27 | logger.warning("command '%s' returned with exit code '%d' (%s)", 28 | ' '.join(e.cmd), e.returncode, e.output.rstrip()) 29 | 30 | 31 | def unmount_device(device): 32 | """ 33 | Unmount the given device if it is mounted. 34 | 35 | This usually happens with automounted data tracks. 36 | 37 | If the given device is a symlink, the target will be checked. 38 | """ 39 | device = os.path.realpath(device) 40 | logger.debug('possibly unmount real path %r', device) 41 | proc = open('/proc/mounts').read() 42 | if device in proc: 43 | print('Device %s is mounted, unmounting' % device) 44 | os.system('umount %s' % device) 45 | -------------------------------------------------------------------------------- /whipper/result/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whipper-team/whipper/bc8b96d95647718d547aa5f8dc512241a7505cac/whipper/result/__init__.py -------------------------------------------------------------------------------- /whipper/result/result.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; test-case-name: whipper.test.test_result_result -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | # Copyright (C) 2009 Thomas Vander Stichele 5 | 6 | # This file is part of whipper. 7 | # 8 | # whipper is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # whipper is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with whipper. If not, see . 20 | 21 | import pkg_resources 22 | import time 23 | 24 | 25 | class TrackResult: 26 | number = None 27 | filename = None 28 | pregap = 0 # in frames 29 | pre_emphasis = None 30 | peak = 0 31 | quality = 0.0 32 | testspeed = 0.0 33 | copyspeed = 0.0 34 | testduration = 0.0 35 | copyduration = 0.0 36 | # 4 byte CRCs for the test and copy reads 37 | testcrc = None 38 | copycrc = None 39 | AR = None 40 | classVersion = 3 41 | skipped = False 42 | 43 | def __init__(self): 44 | """ 45 | Init TrackResult. 46 | 47 | * CRC: calculated 4 byte AccurateRip CRC 48 | * DBCRC: 4 byte AccurateRip CRC from the AR database 49 | * DBConfidence: confidence for the matched AccurateRip DB CRC 50 | * DBMaxConfidence: track's maximum confidence in the AccurateRip DB 51 | * DBMaxConfidenceCRC: maximum confidence CRC 52 | """ 53 | self.AR = { 54 | 'v1': { 55 | 'CRC': None, 56 | 'DBCRC': None, 57 | 'DBConfidence': None, 58 | }, 59 | 'v2': { 60 | 'CRC': None, 61 | 'DBCRC': None, 62 | 'DBConfidence': None, 63 | }, 64 | 'DBMaxConfidence': None, 65 | 'DBMaxConfidenceCRC': None, 66 | } 67 | 68 | 69 | class RipResult: 70 | """ 71 | Hold information about the result for rips. 72 | 73 | It can be used to write log files. 74 | 75 | :cvar offset: sample read offset 76 | :cvar table: the full index table 77 | :vartype table: whipper.image.table.Table 78 | :cvar metadata: disc metadata from MusicBrainz (if available) 79 | :vartype metadata: whipper.common.mbngs.DiscMetadata 80 | :cvar vendor: vendor of the CD drive 81 | :cvar model: model of the CD drive 82 | :cvar release: release of the CD drive 83 | :cvar cdrdaoVersion: version of cdrdao used for the rip 84 | :cvar cdparanoiaVersion: version of cdparanoia used for the rip 85 | """ 86 | 87 | offset = 0 88 | overread = None 89 | isCdr = None 90 | logger = None 91 | table = None 92 | artist = None 93 | title = None 94 | metadata = None 95 | 96 | vendor = None 97 | model = None 98 | release = None 99 | 100 | cdrdaoVersion = None 101 | cdparanoiaVersion = None 102 | cdparanoiaDefeatsCache = None 103 | 104 | classVersion = 3 105 | 106 | def __init__(self): 107 | self.tracks = [] 108 | 109 | def getTrackResult(self, number): 110 | """ 111 | Return TrackResult for the given track number. 112 | 113 | :param number: the track number (0 for HTOA) 114 | :type number: int 115 | :returns: TrackResult for the given track number 116 | :rtype: TrackResult 117 | """ 118 | for t in self.tracks: 119 | if t.number == number: 120 | return t 121 | 122 | return None 123 | 124 | 125 | class Logger: 126 | """Log the result of a rip.""" 127 | 128 | def log(self, ripResult, epoch=time.time()): 129 | """ 130 | Create a log from the given ripresult. 131 | 132 | :param epoch: when the log file gets generated 133 | :type epoch: float 134 | :type ripResult: RipResult 135 | :rtype: str 136 | """ 137 | raise NotImplementedError 138 | 139 | 140 | # A setuptools-like entry point 141 | 142 | 143 | class EntryPoint: 144 | name = 'whipper' 145 | 146 | @staticmethod 147 | def load(): 148 | from whipper.result import logger 149 | return logger.WhipperLogger 150 | 151 | 152 | def getLoggers(): 153 | """ 154 | Get all logger plugins with entry point ``whipper.logger``. 155 | 156 | :rtype: dict(str, Logger) 157 | """ 158 | d = {} 159 | 160 | pluggables = list(pkg_resources.iter_entry_points("whipper.logger")) 161 | for entrypoint in [EntryPoint(), ] + pluggables: 162 | plugin_class = entrypoint.load() 163 | d[entrypoint.name] = plugin_class 164 | 165 | return d 166 | -------------------------------------------------------------------------------- /whipper/test/76df3287-6cda-33eb-8e9a-044b5e15ffdd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whipper-team/whipper/bc8b96d95647718d547aa5f8dc512241a7505cac/whipper/test/76df3287-6cda-33eb-8e9a-044b5e15ffdd.jpg -------------------------------------------------------------------------------- /whipper/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whipper-team/whipper/bc8b96d95647718d547aa5f8dc512241a7505cac/whipper/test/__init__.py -------------------------------------------------------------------------------- /whipper/test/bloc.cue: -------------------------------------------------------------------------------- 1 | REM DISCID AD0BE00D 2 | REM COMMENT "whipper 0.5.1" 3 | FILE "data.wav" WAVE 4 | TRACK 01 AUDIO 5 | PREGAP 03:22:70 6 | INDEX 01 00:00:00 7 | TRACK 02 AUDIO 8 | INDEX 01 04:21:74 9 | TRACK 03 AUDIO 10 | INDEX 01 08:02:12 11 | TRACK 04 AUDIO 12 | INDEX 01 11:57:45 13 | TRACK 05 AUDIO 14 | INDEX 00 15:18:00 15 | INDEX 01 15:18:72 16 | TRACK 06 AUDIO 17 | INDEX 00 18:05:40 18 | INDEX 01 18:06:06 19 | TRACK 07 AUDIO 20 | INDEX 00 21:35:15 21 | INDEX 01 21:35:32 22 | TRACK 08 AUDIO 23 | INDEX 00 26:00:74 24 | INDEX 01 26:01:03 25 | TRACK 09 AUDIO 26 | INDEX 00 29:36:14 27 | INDEX 01 29:36:25 28 | TRACK 10 AUDIO 29 | INDEX 01 33:56:02 30 | TRACK 11 AUDIO 31 | INDEX 00 37:48:26 32 | INDEX 01 37:48:69 33 | TRACK 12 AUDIO 34 | INDEX 00 41:44:45 35 | INDEX 01 41:46:11 36 | TRACK 13 AUDIO 37 | INDEX 00 45:56:11 38 | INDEX 01 45:56:33 39 | -------------------------------------------------------------------------------- /whipper/test/bloc.toc: -------------------------------------------------------------------------------- 1 | CD_DA 2 | 3 | 4 | // Track 1 5 | TRACK AUDIO 6 | NO COPY 7 | NO PRE_EMPHASIS 8 | TWO_CHANNEL_AUDIO 9 | SILENCE 03:22:70 10 | FILE "data.wav" 0 04:21:74 11 | START 03:22:70 12 | 13 | 14 | // Track 2 15 | TRACK AUDIO 16 | NO COPY 17 | NO PRE_EMPHASIS 18 | TWO_CHANNEL_AUDIO 19 | FILE "data.wav" 04:21:74 03:40:13 20 | 21 | 22 | // Track 3 23 | TRACK AUDIO 24 | NO COPY 25 | NO PRE_EMPHASIS 26 | TWO_CHANNEL_AUDIO 27 | FILE "data.wav" 08:02:12 03:55:33 28 | 29 | 30 | // Track 4 31 | TRACK AUDIO 32 | NO COPY 33 | NO PRE_EMPHASIS 34 | TWO_CHANNEL_AUDIO 35 | FILE "data.wav" 11:57:45 03:20:30 36 | 37 | 38 | // Track 5 39 | TRACK AUDIO 40 | NO COPY 41 | NO PRE_EMPHASIS 42 | TWO_CHANNEL_AUDIO 43 | FILE "data.wav" 15:18:00 02:47:40 44 | START 00:00:72 45 | 46 | 47 | // Track 6 48 | TRACK AUDIO 49 | NO COPY 50 | NO PRE_EMPHASIS 51 | TWO_CHANNEL_AUDIO 52 | FILE "data.wav" 18:05:40 03:29:50 53 | START 00:00:41 54 | 55 | 56 | // Track 7 57 | TRACK AUDIO 58 | NO COPY 59 | NO PRE_EMPHASIS 60 | TWO_CHANNEL_AUDIO 61 | FILE "data.wav" 21:35:15 04:25:59 62 | START 00:00:17 63 | 64 | 65 | // Track 8 66 | TRACK AUDIO 67 | NO COPY 68 | NO PRE_EMPHASIS 69 | TWO_CHANNEL_AUDIO 70 | FILE "data.wav" 26:00:74 03:35:15 71 | START 00:00:04 72 | 73 | 74 | // Track 9 75 | TRACK AUDIO 76 | NO COPY 77 | NO PRE_EMPHASIS 78 | TWO_CHANNEL_AUDIO 79 | FILE "data.wav" 29:36:14 04:19:63 80 | START 00:00:11 81 | 82 | 83 | // Track 10 84 | TRACK AUDIO 85 | NO COPY 86 | NO PRE_EMPHASIS 87 | TWO_CHANNEL_AUDIO 88 | FILE "data.wav" 33:56:02 03:52:24 89 | 90 | 91 | // Track 11 92 | TRACK AUDIO 93 | NO COPY 94 | NO PRE_EMPHASIS 95 | TWO_CHANNEL_AUDIO 96 | FILE "data.wav" 37:48:26 03:56:19 97 | START 00:00:43 98 | 99 | 100 | // Track 12 101 | TRACK AUDIO 102 | NO COPY 103 | NO PRE_EMPHASIS 104 | TWO_CHANNEL_AUDIO 105 | FILE "data.wav" 41:44:45 04:11:41 106 | START 00:01:41 107 | 108 | 109 | // Track 13 110 | TRACK AUDIO 111 | NO COPY 112 | NO PRE_EMPHASIS 113 | TWO_CHANNEL_AUDIO 114 | FILE "data.wav" 45:56:11 04:43:60 115 | START 00:00:22 116 | 117 | -------------------------------------------------------------------------------- /whipper/test/breeders.cue: -------------------------------------------------------------------------------- 1 | REM DISCID BE08990D 2 | REM COMMENT "whipper 0.5.1" 3 | CATALOG 0652637280326 4 | PERFORMER "THE BREEDERS" 5 | TITLE "MOUNTAIN BATTLES" 6 | FILE "data.wav" WAVE 7 | TRACK 01 AUDIO 8 | TITLE "OVERGLAZED" 9 | ISRC GBAFL0700213 10 | INDEX 01 00:00:00 11 | TRACK 02 AUDIO 12 | TITLE "BANG ON" 13 | ISRC GBAFL0700214 14 | INDEX 00 02:14:51 15 | INDEX 01 02:15:26 16 | TRACK 03 AUDIO 17 | TITLE "NIGHT OF JOY" 18 | ISRC GBAFL0700215 19 | INDEX 00 04:17:74 20 | INDEX 01 04:18:34 21 | TRACK 04 AUDIO 22 | TITLE "WE'RE GONNA RISE" 23 | ISRC GBAFL0700216 24 | INDEX 01 07:44:22 25 | TRACK 05 AUDIO 26 | TITLE "GERMAN STUDIES" 27 | ISRC GBAFL0700217 28 | INDEX 01 11:37:39 29 | TRACK 06 AUDIO 30 | TITLE "SPARK" 31 | ISRC GBAFL0700218 32 | INDEX 00 13:51:54 33 | INDEX 01 13:53:38 34 | TRACK 07 AUDIO 35 | TITLE "INSTANBUL" 36 | ISRC GBAFL0700219 37 | INDEX 00 16:31:20 38 | INDEX 01 16:32:49 39 | TRACK 08 AUDIO 40 | TITLE "WALK IT OFF" 41 | ISRC GBAFL0700220 42 | INDEX 01 19:30:19 43 | TRACK 09 AUDIO 44 | TITLE "REGLAME ESTA NOCHE" 45 | ISRC GBAFL0700221 46 | INDEX 00 22:14:69 47 | INDEX 01 22:16:27 48 | TRACK 10 AUDIO 49 | TITLE "HERE NO MORE" 50 | ISRC GBAFL0700222 51 | INDEX 00 25:06:18 52 | INDEX 01 25:08:01 53 | TRACK 11 AUDIO 54 | TITLE "NO WAY" 55 | ISRC GBAFL0700223 56 | INDEX 01 27:46:64 57 | TRACK 12 AUDIO 58 | TITLE "IT'S THE LOVE" 59 | ISRC GBAFL0700224 60 | INDEX 01 30:19:39 61 | TRACK 13 AUDIO 62 | TITLE "MOUNTAIN BATTLES" 63 | ISRC GBAFL0700225 64 | INDEX 01 32:47:56 65 | -------------------------------------------------------------------------------- /whipper/test/breeders.toc: -------------------------------------------------------------------------------- 1 | CD_DA 2 | 3 | CATALOG "0652637280326" 4 | CD_TEXT { 5 | LANGUAGE_MAP { 6 | 0: 9 7 | } 8 | LANGUAGE 0 { 9 | TITLE "MOUNTAIN BATTLES" 10 | PERFORMER "THE BREEDERS" 11 | DISC_ID "CADD2803CD" 12 | SIZE_INFO { 1, 1, 20, 0, 16, 3, 0, 0, 0, 0, 1, 0, 13 | 0, 0, 0, 0, 0, 0, 0, 3, 22, 0, 0, 0, 14 | 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0} 15 | } 16 | } 17 | 18 | // Track 1 19 | TRACK AUDIO 20 | NO COPY 21 | NO PRE_EMPHASIS 22 | TWO_CHANNEL_AUDIO 23 | ISRC "GBAFL0700213" 24 | CD_TEXT { 25 | LANGUAGE 0 { 26 | TITLE "OVERGLAZED" 27 | PERFORMER "" 28 | } 29 | } 30 | FILE "data.wav" 0 02:14:51 31 | 32 | 33 | // Track 2 34 | TRACK AUDIO 35 | NO COPY 36 | NO PRE_EMPHASIS 37 | TWO_CHANNEL_AUDIO 38 | ISRC "GBAFL0700214" 39 | CD_TEXT { 40 | LANGUAGE 0 { 41 | TITLE "BANG ON" 42 | PERFORMER "" 43 | } 44 | } 45 | FILE "data.wav" 02:14:51 02:03:23 46 | START 00:00:50 47 | 48 | 49 | // Track 3 50 | TRACK AUDIO 51 | NO COPY 52 | NO PRE_EMPHASIS 53 | TWO_CHANNEL_AUDIO 54 | ISRC "GBAFL0700215" 55 | CD_TEXT { 56 | LANGUAGE 0 { 57 | TITLE "NIGHT OF JOY" 58 | PERFORMER "" 59 | } 60 | } 61 | FILE "data.wav" 04:17:74 03:26:23 62 | START 00:00:35 63 | 64 | 65 | // Track 4 66 | TRACK AUDIO 67 | NO COPY 68 | NO PRE_EMPHASIS 69 | TWO_CHANNEL_AUDIO 70 | ISRC "GBAFL0700216" 71 | CD_TEXT { 72 | LANGUAGE 0 { 73 | TITLE "WE'RE GONNA RISE" 74 | PERFORMER "" 75 | } 76 | } 77 | FILE "data.wav" 07:44:22 03:53:17 78 | 79 | 80 | // Track 5 81 | TRACK AUDIO 82 | NO COPY 83 | NO PRE_EMPHASIS 84 | TWO_CHANNEL_AUDIO 85 | ISRC "GBAFL0700217" 86 | CD_TEXT { 87 | LANGUAGE 0 { 88 | TITLE "GERMAN STUDIES" 89 | PERFORMER "" 90 | } 91 | } 92 | FILE "data.wav" 11:37:39 02:14:15 93 | 94 | 95 | // Track 6 96 | TRACK AUDIO 97 | NO COPY 98 | NO PRE_EMPHASIS 99 | TWO_CHANNEL_AUDIO 100 | ISRC "GBAFL0700218" 101 | CD_TEXT { 102 | LANGUAGE 0 { 103 | TITLE "SPARK" 104 | PERFORMER "" 105 | } 106 | } 107 | FILE "data.wav" 13:51:54 02:39:41 108 | START 00:01:59 109 | 110 | 111 | // Track 7 112 | TRACK AUDIO 113 | NO COPY 114 | NO PRE_EMPHASIS 115 | TWO_CHANNEL_AUDIO 116 | ISRC "GBAFL0700219" 117 | CD_TEXT { 118 | LANGUAGE 0 { 119 | TITLE "INSTANBUL" 120 | PERFORMER "" 121 | } 122 | } 123 | FILE "data.wav" 16:31:20 02:58:74 124 | START 00:01:29 125 | 126 | 127 | // Track 8 128 | TRACK AUDIO 129 | NO COPY 130 | NO PRE_EMPHASIS 131 | TWO_CHANNEL_AUDIO 132 | ISRC "GBAFL0700220" 133 | CD_TEXT { 134 | LANGUAGE 0 { 135 | TITLE "WALK IT OFF" 136 | PERFORMER "" 137 | } 138 | } 139 | FILE "data.wav" 19:30:19 02:44:50 140 | 141 | 142 | // Track 9 143 | TRACK AUDIO 144 | NO COPY 145 | NO PRE_EMPHASIS 146 | TWO_CHANNEL_AUDIO 147 | ISRC "GBAFL0700221" 148 | CD_TEXT { 149 | LANGUAGE 0 { 150 | TITLE "REGLAME ESTA NOCHE" 151 | PERFORMER "" 152 | } 153 | } 154 | FILE "data.wav" 22:14:69 02:51:24 155 | START 00:01:33 156 | 157 | 158 | // Track 10 159 | TRACK AUDIO 160 | NO COPY 161 | NO PRE_EMPHASIS 162 | TWO_CHANNEL_AUDIO 163 | ISRC "GBAFL0700222" 164 | CD_TEXT { 165 | LANGUAGE 0 { 166 | TITLE "HERE NO MORE" 167 | PERFORMER "" 168 | } 169 | } 170 | FILE "data.wav" 25:06:18 02:40:46 171 | START 00:01:58 172 | 173 | 174 | // Track 11 175 | TRACK AUDIO 176 | NO COPY 177 | NO PRE_EMPHASIS 178 | TWO_CHANNEL_AUDIO 179 | ISRC "GBAFL0700223" 180 | CD_TEXT { 181 | LANGUAGE 0 { 182 | TITLE "NO WAY" 183 | PERFORMER "" 184 | } 185 | } 186 | FILE "data.wav" 27:46:64 02:32:50 187 | 188 | 189 | // Track 12 190 | TRACK AUDIO 191 | NO COPY 192 | NO PRE_EMPHASIS 193 | TWO_CHANNEL_AUDIO 194 | ISRC "GBAFL0700224" 195 | CD_TEXT { 196 | LANGUAGE 0 { 197 | TITLE "IT'S THE LOVE" 198 | PERFORMER "" 199 | } 200 | } 201 | FILE "data.wav" 30:19:39 02:28:17 202 | 203 | 204 | // Track 13 205 | TRACK AUDIO 206 | NO COPY 207 | NO PRE_EMPHASIS 208 | TWO_CHANNEL_AUDIO 209 | ISRC "GBAFL0700225" 210 | CD_TEXT { 211 | LANGUAGE 0 { 212 | TITLE "MOUNTAIN BATTLES" 213 | PERFORMER "" 214 | } 215 | } 216 | FILE "data.wav" 32:47:56 03:53:66 217 | 218 | -------------------------------------------------------------------------------- /whipper/test/capital.1.toc: -------------------------------------------------------------------------------- 1 | CD_DA 2 | 3 | 4 | // Track 1 5 | TRACK AUDIO 6 | NO COPY 7 | NO PRE_EMPHASIS 8 | TWO_CHANNEL_AUDIO 9 | ISRC "GBAAA0300350" 10 | SILENCE 05:22:20 11 | FILE "data.wav" 0 04:32:55 12 | START 05:22:20 13 | 14 | 15 | // Track 2 16 | TRACK AUDIO 17 | NO COPY 18 | NO PRE_EMPHASIS 19 | TWO_CHANNEL_AUDIO 20 | ISRC "GBAAA0300351" 21 | FILE "data.wav" 04:32:55 04:16:02 22 | START 00:01:05 23 | 24 | 25 | // Track 3 26 | TRACK AUDIO 27 | NO COPY 28 | NO PRE_EMPHASIS 29 | TWO_CHANNEL_AUDIO 30 | ISRC "GBAAA0300352" 31 | FILE "data.wav" 08:48:57 03:03:65 32 | START 00:01:38 33 | 34 | 35 | // Track 4 36 | TRACK AUDIO 37 | NO COPY 38 | NO PRE_EMPHASIS 39 | TWO_CHANNEL_AUDIO 40 | ISRC "GBAAA0300353" 41 | FILE "data.wav" 11:52:47 02:16:03 42 | START 00:01:43 43 | 44 | 45 | // Track 5 46 | TRACK AUDIO 47 | NO COPY 48 | NO PRE_EMPHASIS 49 | TWO_CHANNEL_AUDIO 50 | ISRC "GBAAA0300355" 51 | FILE "data.wav" 14:08:50 03:32:55 52 | START 00:01:50 53 | 54 | 55 | // Track 6 56 | TRACK AUDIO 57 | NO COPY 58 | NO PRE_EMPHASIS 59 | TWO_CHANNEL_AUDIO 60 | ISRC "GBAAA0300356" 61 | FILE "data.wav" 17:41:30 03:09:70 62 | START 00:01:20 63 | 64 | 65 | // Track 7 66 | TRACK AUDIO 67 | NO COPY 68 | NO PRE_EMPHASIS 69 | TWO_CHANNEL_AUDIO 70 | ISRC "GBAAA0300357" 71 | FILE "data.wav" 20:51:25 02:27:25 72 | START 00:01:00 73 | 74 | 75 | // Track 8 76 | TRACK AUDIO 77 | NO COPY 78 | NO PRE_EMPHASIS 79 | TWO_CHANNEL_AUDIO 80 | ISRC "GBAAA0300358" 81 | FILE "data.wav" 23:18:50 02:46:35 82 | START 00:00:35 83 | 84 | 85 | // Track 9 86 | TRACK AUDIO 87 | NO COPY 88 | NO PRE_EMPHASIS 89 | TWO_CHANNEL_AUDIO 90 | ISRC "GBAAA0300359" 91 | FILE "data.wav" 26:05:10 05:02:72 92 | START 00:00:60 93 | 94 | 95 | // Track 10 96 | TRACK AUDIO 97 | NO COPY 98 | NO PRE_EMPHASIS 99 | TWO_CHANNEL_AUDIO 100 | ISRC "GBAAA0300360" 101 | FILE "data.wav" 31:08:07 03:50:38 102 | START 00:00:60 103 | 104 | 105 | // Track 11 106 | TRACK AUDIO 107 | NO COPY 108 | NO PRE_EMPHASIS 109 | TWO_CHANNEL_AUDIO 110 | ISRC "GBAAA0300361" 111 | FILE "data.wav" 34:58:45 03:35:10 112 | START 00:00:65 113 | 114 | -------------------------------------------------------------------------------- /whipper/test/capital.2.toc: -------------------------------------------------------------------------------- 1 | CD_ROM 2 | 3 | 4 | // Track 1 5 | TRACK MODE1 6 | NO COPY 7 | DATAFILE "data_1" 27:30:00 // length in bytes: 253440000 8 | 9 | -------------------------------------------------------------------------------- /whipper/test/capital.fast.toc: -------------------------------------------------------------------------------- 1 | CD_ROM 2 | 3 | 4 | // Track 1 5 | TRACK AUDIO 6 | NO COPY 7 | NO PRE_EMPHASIS 8 | TWO_CHANNEL_AUDIO 9 | ISRC "GBAAA0300350" 10 | FILE "data.wav" 0 04:33:60 11 | 12 | 13 | // Track 2 14 | TRACK AUDIO 15 | NO COPY 16 | NO PRE_EMPHASIS 17 | TWO_CHANNEL_AUDIO 18 | ISRC "GBAAA0300351" 19 | FILE "data.wav" 04:33:60 04:16:35 20 | 21 | 22 | // Track 3 23 | TRACK AUDIO 24 | NO COPY 25 | NO PRE_EMPHASIS 26 | TWO_CHANNEL_AUDIO 27 | ISRC "GBAAA0300352" 28 | FILE "data.wav" 08:50:20 03:03:70 29 | 30 | 31 | // Track 4 32 | TRACK AUDIO 33 | NO COPY 34 | NO PRE_EMPHASIS 35 | TWO_CHANNEL_AUDIO 36 | ISRC "GBAAA0300353" 37 | FILE "data.wav" 11:54:15 02:16:10 38 | 39 | 40 | // Track 5 41 | TRACK AUDIO 42 | NO COPY 43 | NO PRE_EMPHASIS 44 | TWO_CHANNEL_AUDIO 45 | ISRC "GBAAA0300355" 46 | FILE "data.wav" 14:10:25 03:32:25 47 | 48 | 49 | // Track 6 50 | TRACK AUDIO 51 | NO COPY 52 | NO PRE_EMPHASIS 53 | TWO_CHANNEL_AUDIO 54 | ISRC "GBAAA0300356" 55 | FILE "data.wav" 17:42:50 03:09:50 56 | 57 | 58 | // Track 7 59 | TRACK AUDIO 60 | NO COPY 61 | NO PRE_EMPHASIS 62 | TWO_CHANNEL_AUDIO 63 | ISRC "GBAAA0300357" 64 | FILE "data.wav" 20:52:25 02:26:60 65 | 66 | 67 | // Track 8 68 | TRACK AUDIO 69 | NO COPY 70 | NO PRE_EMPHASIS 71 | TWO_CHANNEL_AUDIO 72 | ISRC "GBAAA0300358" 73 | FILE "data.wav" 23:19:10 02:46:60 74 | 75 | 76 | // Track 9 77 | TRACK AUDIO 78 | NO COPY 79 | NO PRE_EMPHASIS 80 | TWO_CHANNEL_AUDIO 81 | ISRC "GBAAA0300359" 82 | FILE "data.wav" 26:05:70 05:02:72 83 | 84 | 85 | // Track 10 86 | TRACK AUDIO 87 | NO COPY 88 | NO PRE_EMPHASIS 89 | TWO_CHANNEL_AUDIO 90 | ISRC "GBAAA0300360" 91 | FILE "data.wav" 31:08:67 03:50:43 92 | 93 | 94 | // Track 11 95 | TRACK AUDIO 96 | NO COPY 97 | NO PRE_EMPHASIS 98 | TWO_CHANNEL_AUDIO 99 | ISRC "GBAAA0300361" 100 | FILE "data.wav" 34:59:35 06:04:20 101 | 102 | 103 | // Track 12 104 | TRACK MODE1 105 | NO COPY 106 | ZERO MODE1 00:02:00 107 | DATAFILE "data_12" 27:30:00 // length in bytes: 253440000 108 | START 00:02:00 109 | 110 | -------------------------------------------------------------------------------- /whipper/test/cdparanoia.progress.strokes: -------------------------------------------------------------------------------- 1 | Sending all callbacks to stderr for wrapper script 2 | cdparanoia III release 10.2 (September 11, 2008) 3 | 4 | Ripping from sector 0 (track 0 [0:00.00]) 5 | to sector 0 (track 0 [0:00.00]) 6 | 7 | outputting to cdda.wav 8 | 9 | ##: 0 [read] @ 24696 10 | ##: 0 [read] @ 56448 11 | ##: 0 [read] @ 88200 12 | ##: 0 [read] @ 119952 13 | ##: 0 [read] @ 151704 14 | ##: 0 [read] @ 183456 15 | ##: 0 [read] @ 215208 16 | ##: 0 [read] @ 246960 17 | ##: 0 [read] @ 278712 18 | ##: 0 [read] @ 310464 19 | ##: 0 [read] @ 342216 20 | ##: 0 [read] @ 373968 21 | ##: 0 [read] @ 405720 22 | ##: 0 [read] @ 437472 23 | ##: 0 [read] @ 469224 24 | ##: 0 [read] @ 500976 25 | ##: 0 [read] @ 532728 26 | ##: 0 [read] @ 564480 27 | ##: 0 [read] @ 596232 28 | ##: 0 [read] @ 627984 29 | ##: 0 [read] @ 659736 30 | ##: 0 [read] @ 691488 31 | ##: 0 [read] @ 723240 32 | ##: 0 [read] @ 754992 33 | ##: 0 [read] @ 786744 34 | ##: 0 [read] @ 818496 35 | ##: 0 [read] @ 850248 36 | ##: 0 [read] @ 882000 37 | ##: 0 [read] @ 913752 38 | ##: 0 [read] @ 945504 39 | ##: 0 [read] @ 977256 40 | ##: 0 [read] @ 1009008 41 | ##: 0 [read] @ 1040760 42 | ##: 0 [read] @ 1072512 43 | ##: 0 [read] @ 1104264 44 | ##: 0 [read] @ 1136016 45 | ##: 0 [read] @ 1167768 46 | ##: 0 [read] @ 1199520 47 | ##: 0 [read] @ 1231272 48 | ##: 0 [read] @ 1263024 49 | ##: 0 [read] @ 1294776 50 | ##: 0 [read] @ 1326528 51 | ##: 0 [read] @ 1358280 52 | ##: 0 [read] @ 1390032 53 | ##: 0 [read] @ 1410024 54 | ##: 0 [read] @ 23520 55 | ##: 0 [read] @ 55272 56 | ##: 0 [read] @ 87024 57 | ##: 0 [read] @ 118776 58 | ##: 0 [read] @ 150528 59 | ##: 0 [read] @ 182280 60 | ##: 0 [read] @ 214032 61 | ##: 0 [read] @ 245784 62 | ##: 0 [read] @ 277536 63 | ##: 0 [read] @ 309288 64 | ##: 0 [read] @ 341040 65 | ##: 0 [read] @ 372792 66 | ##: 0 [read] @ 404544 67 | ##: 0 [read] @ 436296 68 | ##: 0 [read] @ 468048 69 | ##: 0 [read] @ 499800 70 | ##: 0 [read] @ 531552 71 | ##: 0 [read] @ 563304 72 | ##: 0 [read] @ 595056 73 | ##: 0 [read] @ 626808 74 | ##: 0 [read] @ 658560 75 | ##: 0 [read] @ 690312 76 | ##: 0 [read] @ 722064 77 | ##: 0 [read] @ 753816 78 | ##: 0 [read] @ 785568 79 | ##: 0 [read] @ 817320 80 | ##: 0 [read] @ 849072 81 | ##: 0 [read] @ 880824 82 | ##: 0 [read] @ 912576 83 | ##: 0 [read] @ 944328 84 | ##: 0 [read] @ 976080 85 | ##: 0 [read] @ 1007832 86 | ##: 0 [read] @ 1039584 87 | ##: 0 [read] @ 1071336 88 | ##: 0 [read] @ 1103088 89 | ##: 0 [read] @ 1134840 90 | ##: 0 [read] @ 1166592 91 | ##: 0 [read] @ 1198344 92 | ##: 0 [read] @ 1230096 93 | ##: 0 [read] @ 1261848 94 | ##: 0 [read] @ 1293600 95 | ##: 0 [read] @ 1325352 96 | ##: 0 [read] @ 1357104 97 | ##: 0 [read] @ 1388856 98 | ##: 0 [read] @ 1410024 99 | ##: 1 [verify] @ 0 100 | ##: 3 [correction] @ 1005459 101 | ##: 3 [correction] @ 1005480 102 | ##: 1 [verify] @ 1005480 103 | ##: 1 [verify] @ 1005480 104 | ##: -2 [wrote] @ 1175 105 | ##: -2 [wrote] @ 1176 106 | ##: -1 [finished] @ 1175 107 | 108 | 109 | Done. 110 | 111 | 112 | -------------------------------------------------------------------------------- /whipper/test/cdparanoia/PX-L890SA.cdparanoia-A.stderr: -------------------------------------------------------------------------------- 1 | cdparanoia III release 10.2 (September 11, 2008) 2 | 3 | Using cdda library version: 10.2 4 | Using paranoia library version: 10.2 5 | Checking /dev/cdrom for cdrom... 6 | Testing /dev/cdrom for SCSI/MMC interface 7 | SG_IO device: /dev/sr0 8 | 9 | CDROM model sensed sensed: PLEXTOR DVDR PX-L890SA 1.05 10 | 11 | 12 | Checking for SCSI emulation... 13 | Drive is ATAPI (using SG_IO host adaptor emulation) 14 | 15 | Checking for MMC style command set... 16 | Drive is MMC style 17 | DMA scatter/gather table entries: 1 18 | table entry size: 524288 bytes 19 | maximum theoretical transfer: 222 sectors 20 | Setting default read size to 27 sectors (63504 bytes). 21 | 22 | Verifying CDDA command set... 23 | Expected command set reads OK. 24 | 25 | Attempting to set cdrom to full speed... 26 | drive returned OK. 27 | 28 | =================== Checking drive cache/timing behavior =================== 29 | 30 | Seek/read timing: 31 | [39:43.53]: 19ms seek, 0.70ms/sec read [18.9x] spinning up... [39:43.52]: 19ms seek, 1.28ms/sec read [10.4x] spinning up... [39:43.51]: 23ms seek, 0.52ms/sec read [25.7x] spinning up... [39:43.50]: 231ms seek, 0.46ms/sec read [29.0x] spinning up... [39:43.49]: 18ms seek, 0.41ms/sec read [32.7x] spinning up... [39:43.48]: 18ms seek, 1.28ms/sec read [10.4x] spinning up... [39:43.47]: 21ms seek, 0.37ms/sec read [36.3x] spinning up... [39:43.46]: 21ms seek, 0.37ms/sec read [36.1x] spinning up... [39:43.45]: 15ms seek, 0.37ms/sec read [36.0x] spinning up... [39:43.44]: 21ms seek, 0.37ms/sec read [36.0x] spinning up... [39:43.43]: 15ms seek, 0.37ms/sec read [36.0x] spinning up... [39:43.42]: 21ms seek, 0.37ms/sec read [36.0x] spinning up... [39:43.41]: 15ms seek, 0.37ms/sec read [36.0x] spinning up... [39:43.40]: 21ms seek, 0.37ms/sec read [36.0x] spinning up... [39:43.39]: 21ms seek, 0.37ms/sec read [36.0x] spinning up... [39:43.38]: 15ms seek, 0.37ms/sec read [36.0x] spinning up... [39:43.37]: 21ms seek, 0.37ms/sec read [36.0x] spinning up... [39:43.36]: 15ms seek, 0.37ms/sec read [36.0x] 32 | [30:00.00]: 21ms seek, 0.41ms/sec read [32.7x] 33 | [20:00.00]: 22ms seek, 0.44ms/sec read [30.0x] 34 | [10:00.00]: 30ms seek, 0.52ms/sec read [25.7x] 35 | [00:00.00]: 33ms seek, 0.70ms/sec read [18.9x] 36 | 37 | Analyzing cache behavior... 38 | Fast search for approximate cache size... 0 sectors Slow verify for approximate cache size... 0 sectors.......... Drive does not cache nonlinear access 39 | 40 | Drive tests OK with Paranoia. 41 | 42 | -------------------------------------------------------------------------------- /whipper/test/common.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | import re 5 | import os 6 | import sys 7 | 8 | import whipper 9 | 10 | # twisted's unittests have skip support, standard unittest don't 11 | from twisted.trial import unittest 12 | 13 | # lifted from flumotion 14 | 15 | 16 | def _diff(old, new, desc): 17 | import difflib 18 | lines = difflib.unified_diff(old, new) 19 | lines = list(lines) 20 | if not lines: 21 | return 22 | output = '' 23 | for line in lines: 24 | output += '%s: %s\n' % (desc, line[:-1]) 25 | 26 | raise AssertionError( 27 | ("\nError while comparing strings:\n" 28 | "%s") % (output.encode('utf-8'), )) 29 | 30 | 31 | def diffStrings(orig, new, desc='input'): 32 | 33 | assert isinstance(orig, type(new)), 'type %s and %s are different' % ( 34 | type(orig), type(new)) 35 | 36 | def _tolines(s): 37 | return [line + '\n' for line in s.split('\n')] 38 | 39 | return _diff(_tolines(orig), 40 | _tolines(new), 41 | desc=desc) 42 | 43 | 44 | class TestCase(unittest.TestCase): 45 | # unittest.TestCase.failUnlessRaises does not return the exception, 46 | # and we'd like to check for the actual exception under TaskException, 47 | # so override the way twisted.trial.unittest does, without failure 48 | 49 | # XXX: Pylint, method could be a function (no-self-use) 50 | def failUnlessRaises(self, exception, f, *args, **kwargs): 51 | try: 52 | result = f(*args, **kwargs) 53 | except exception as inst: 54 | return inst 55 | except exception as e: 56 | raise Exception('%s raised instead of %s:\n %s' % 57 | (sys.exc_info()[0], exception.__name__, str(e)) 58 | ) 59 | else: 60 | raise Exception('%s not raised (%r returned)' % 61 | (exception.__name__, result) 62 | ) 63 | 64 | assertRaises = failUnlessRaises 65 | 66 | @staticmethod 67 | def readCue(name): 68 | """ 69 | Read a .cue file replacing the version comment with the current value. 70 | 71 | So that it can be used in comparisons. 72 | """ 73 | cuefile = os.path.join(os.path.dirname(__file__), name) 74 | with open(cuefile) as f: 75 | ret = f.read() 76 | ret = re.sub( 77 | 'REM COMMENT "whipper.*', 78 | 'REM COMMENT "whipper %s"' % whipper.__version__, 79 | ret, re.MULTILINE) 80 | 81 | return ret 82 | 83 | 84 | class UnicodeTestMixin: 85 | # A helper mixin to skip tests if we're not in a UTF-8 locale 86 | 87 | try: 88 | os.stat('whipper.test.B\xeate Noire.empty') 89 | except UnicodeEncodeError: 90 | skip = 'No UTF-8 locale' 91 | except OSError: 92 | pass 93 | -------------------------------------------------------------------------------- /whipper/test/cure.cue: -------------------------------------------------------------------------------- 1 | REM DISCID B90C650D 2 | REM COMMENT "whipper 0.5.1" 3 | CATALOG 0602517642256 4 | FILE "data.wav" WAVE 5 | TRACK 01 AUDIO 6 | ISRC USUM70839873 7 | INDEX 01 00:00:00 8 | TRACK 02 AUDIO 9 | ISRC USUM70839874 10 | INDEX 00 06:16:45 11 | INDEX 01 06:17:49 12 | TRACK 03 AUDIO 13 | ISRC USUM70839875 14 | INDEX 00 10:13:02 15 | INDEX 01 10:14:60 16 | TRACK 04 AUDIO 17 | ISRC USUM70839876 18 | INDEX 00 14:50:07 19 | INDEX 01 14:50:17 20 | TRACK 05 AUDIO 21 | ISRC USUM70839877 22 | INDEX 00 17:18:42 23 | INDEX 01 17:20:47 24 | TRACK 06 AUDIO 25 | ISRC USUM70839878 26 | INDEX 00 19:43:06 27 | INDEX 01 19:43:10 28 | TRACK 07 AUDIO 29 | ISRC USUM70839879 30 | INDEX 00 24:25:07 31 | INDEX 01 24:26:41 32 | TRACK 08 AUDIO 33 | ISRC USUM70839880 34 | INDEX 00 28:56:00 35 | INDEX 01 28:56:09 36 | TRACK 09 AUDIO 37 | ISRC USUM70839881 38 | INDEX 00 32:38:11 39 | INDEX 01 32:40:45 40 | TRACK 10 AUDIO 41 | ISRC USUM70839882 42 | INDEX 00 36:01:58 43 | INDEX 01 36:02:04 44 | TRACK 11 AUDIO 45 | ISRC USUM70839883 46 | INDEX 00 40:08:30 47 | INDEX 01 40:08:53 48 | TRACK 12 AUDIO 49 | ISRC USUM70839884 50 | INDEX 00 43:59:51 51 | INDEX 01 44:00:27 52 | TRACK 13 AUDIO 53 | ISRC USUM70839885 54 | INDEX 00 48:35:63 55 | INDEX 01 48:36:71 56 | -------------------------------------------------------------------------------- /whipper/test/cure.toc: -------------------------------------------------------------------------------- 1 | CD_DA 2 | 3 | CATALOG "0602517642256" 4 | 5 | // Track 1 6 | TRACK AUDIO 7 | NO COPY 8 | NO PRE_EMPHASIS 9 | TWO_CHANNEL_AUDIO 10 | ISRC "USUM70839873" 11 | FILE "data.wav" 0 06:16:45 12 | 13 | 14 | // Track 2 15 | TRACK AUDIO 16 | NO COPY 17 | NO PRE_EMPHASIS 18 | TWO_CHANNEL_AUDIO 19 | ISRC "USUM70839874" 20 | FILE "data.wav" 06:16:45 03:56:32 21 | START 00:01:04 22 | 23 | 24 | // Track 3 25 | TRACK AUDIO 26 | NO COPY 27 | NO PRE_EMPHASIS 28 | TWO_CHANNEL_AUDIO 29 | ISRC "USUM70839875" 30 | FILE "data.wav" 10:13:02 04:37:05 31 | START 00:01:58 32 | 33 | 34 | // Track 4 35 | TRACK AUDIO 36 | NO COPY 37 | NO PRE_EMPHASIS 38 | TWO_CHANNEL_AUDIO 39 | ISRC "USUM70839876" 40 | FILE "data.wav" 14:50:07 02:28:35 41 | START 00:00:10 42 | 43 | 44 | // Track 5 45 | TRACK AUDIO 46 | NO COPY 47 | NO PRE_EMPHASIS 48 | TWO_CHANNEL_AUDIO 49 | ISRC "USUM70839877" 50 | FILE "data.wav" 17:18:42 02:24:39 51 | START 00:02:05 52 | 53 | 54 | // Track 6 55 | TRACK AUDIO 56 | NO COPY 57 | NO PRE_EMPHASIS 58 | TWO_CHANNEL_AUDIO 59 | ISRC "USUM70839878" 60 | FILE "data.wav" 19:43:06 04:42:01 61 | START 00:00:04 62 | 63 | 64 | // Track 7 65 | TRACK AUDIO 66 | NO COPY 67 | NO PRE_EMPHASIS 68 | TWO_CHANNEL_AUDIO 69 | ISRC "USUM70839879" 70 | FILE "data.wav" 24:25:07 04:30:68 71 | START 00:01:34 72 | 73 | 74 | // Track 8 75 | TRACK AUDIO 76 | NO COPY 77 | NO PRE_EMPHASIS 78 | TWO_CHANNEL_AUDIO 79 | ISRC "USUM70839880" 80 | FILE "data.wav" 28:56:00 03:42:11 81 | START 00:00:09 82 | 83 | 84 | // Track 9 85 | TRACK AUDIO 86 | NO COPY 87 | NO PRE_EMPHASIS 88 | TWO_CHANNEL_AUDIO 89 | ISRC "USUM70839881" 90 | FILE "data.wav" 32:38:11 03:23:47 91 | START 00:02:34 92 | 93 | 94 | // Track 10 95 | TRACK AUDIO 96 | NO COPY 97 | NO PRE_EMPHASIS 98 | TWO_CHANNEL_AUDIO 99 | ISRC "USUM70839882" 100 | FILE "data.wav" 36:01:58 04:06:47 101 | START 00:00:21 102 | 103 | 104 | // Track 11 105 | TRACK AUDIO 106 | NO COPY 107 | NO PRE_EMPHASIS 108 | TWO_CHANNEL_AUDIO 109 | ISRC "USUM70839883" 110 | FILE "data.wav" 40:08:30 03:51:21 111 | START 00:00:23 112 | 113 | 114 | // Track 12 115 | TRACK AUDIO 116 | NO COPY 117 | NO PRE_EMPHASIS 118 | TWO_CHANNEL_AUDIO 119 | ISRC "USUM70839884" 120 | FILE "data.wav" 43:59:51 04:36:12 121 | START 00:00:51 122 | 123 | 124 | // Track 13 125 | TRACK AUDIO 126 | NO COPY 127 | NO PRE_EMPHASIS 128 | TWO_CHANNEL_AUDIO 129 | ISRC "USUM70839885" 130 | FILE "data.wav" 48:35:63 04:17:71 131 | START 00:01:08 132 | 133 | -------------------------------------------------------------------------------- /whipper/test/dBAR-002-0000f21c-00027ef8-05021002.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whipper-team/whipper/bc8b96d95647718d547aa5f8dc512241a7505cac/whipper/test/dBAR-002-0000f21c-00027ef8-05021002.bin -------------------------------------------------------------------------------- /whipper/test/dBAR-011-0010e284-009228a3-9809ff0b.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whipper-team/whipper/bc8b96d95647718d547aa5f8dc512241a7505cac/whipper/test/dBAR-011-0010e284-009228a3-9809ff0b.bin -------------------------------------------------------------------------------- /whipper/test/dBAR-020-002e5023-029d8e49-040eaa14.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whipper-team/whipper/bc8b96d95647718d547aa5f8dc512241a7505cac/whipper/test/dBAR-020-002e5023-029d8e49-040eaa14.bin -------------------------------------------------------------------------------- /whipper/test/diorama.cue: -------------------------------------------------------------------------------- 1 | REM DISCID 700AC908 2 | REM COMMENT "whipper 0.10.1.dev27+ga4b9742.d20240827" 3 | PERFORMER "MØL" 4 | TITLE "Diorama" 5 | FILE "data.wav" WAVE 6 | TRACK 01 AUDIO 7 | PERFORMER "MØL" 8 | TITLE "Fraktur" 9 | ISRC DED832100085 10 | INDEX 01 00:00:00 11 | TRACK 02 AUDIO 12 | PERFORMER "MØL" 13 | TITLE "Photophobic" 14 | ISRC DED832100086 15 | INDEX 01 04:19:00 16 | TRACK 03 AUDIO 17 | PERFORMER "MØL" 18 | TITLE "Serf" 19 | ISRC DED832100087 20 | INDEX 01 09:37:00 21 | TRACK 04 AUDIO 22 | PERFORMER "MØL" 23 | TITLE "Vestige" 24 | ISRC DED832100088 25 | INDEX 01 14:59:12 26 | TRACK 05 AUDIO 27 | PERFORMER "MØL" 28 | TITLE "Redacted" 29 | ISRC DED832100089 30 | INDEX 01 20:37:68 31 | TRACK 06 AUDIO 32 | PERFORMER "MØL" 33 | TITLE "Itinerari" 34 | ISRC DED832100090 35 | INDEX 01 25:54:18 36 | TRACK 07 AUDIO 37 | PERFORMER "MØL" 38 | TITLE "Tvesind" 39 | ISRC DED832100091 40 | INDEX 01 30:57:58 41 | TRACK 08 AUDIO 42 | PERFORMER "MØL" 43 | TITLE "Diorama" 44 | ISRC DED832100092 45 | INDEX 01 38:46:57 46 | -------------------------------------------------------------------------------- /whipper/test/diorama_noutf8.toc: -------------------------------------------------------------------------------- 1 | CD_DA 2 | 3 | CD_TEXT { 4 | LANGUAGE_MAP { 5 | 0: 9 6 | } 7 | LANGUAGE 0 { 8 | TITLE "Diorama" 9 | PERFORMER "M\330L" 10 | SIZE_INFO { 0, 1, 8, 0, 7, 3, 0, 0, 0, 0, 0, 0, 11 | 0, 0, 0, 0, 0, 0, 0, 3, 12, 0, 0, 0, 12 | 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0} 13 | } 14 | } 15 | 16 | // Track 1 17 | TRACK AUDIO 18 | NO COPY 19 | NO PRE_EMPHASIS 20 | TWO_CHANNEL_AUDIO 21 | ISRC "DED832100085" 22 | CD_TEXT { 23 | LANGUAGE 0 { 24 | TITLE "Fraktur" 25 | PERFORMER "M\330L" 26 | } 27 | } 28 | FILE "data.wav" 0 04:19:00 29 | 30 | 31 | // Track 2 32 | TRACK AUDIO 33 | NO COPY 34 | NO PRE_EMPHASIS 35 | TWO_CHANNEL_AUDIO 36 | ISRC "DED832100086" 37 | CD_TEXT { 38 | LANGUAGE 0 { 39 | TITLE "Photophobic" 40 | PERFORMER "M\330L" 41 | } 42 | } 43 | FILE "data.wav" 04:19:00 05:18:00 44 | 45 | 46 | // Track 3 47 | TRACK AUDIO 48 | NO COPY 49 | NO PRE_EMPHASIS 50 | TWO_CHANNEL_AUDIO 51 | ISRC "DED832100087" 52 | CD_TEXT { 53 | LANGUAGE 0 { 54 | TITLE "Serf" 55 | PERFORMER "M\330L" 56 | } 57 | } 58 | FILE "data.wav" 09:37:00 05:22:12 59 | 60 | 61 | // Track 4 62 | TRACK AUDIO 63 | NO COPY 64 | NO PRE_EMPHASIS 65 | TWO_CHANNEL_AUDIO 66 | ISRC "DED832100088" 67 | CD_TEXT { 68 | LANGUAGE 0 { 69 | TITLE "Vestige" 70 | PERFORMER "M\330L" 71 | } 72 | } 73 | FILE "data.wav" 14:59:12 05:38:56 74 | 75 | 76 | // Track 5 77 | TRACK AUDIO 78 | NO COPY 79 | NO PRE_EMPHASIS 80 | TWO_CHANNEL_AUDIO 81 | ISRC "DED832100089" 82 | CD_TEXT { 83 | LANGUAGE 0 { 84 | TITLE "Redacted" 85 | PERFORMER "M\330L" 86 | } 87 | } 88 | FILE "data.wav" 20:37:68 05:16:25 89 | 90 | 91 | // Track 6 92 | TRACK AUDIO 93 | NO COPY 94 | NO PRE_EMPHASIS 95 | TWO_CHANNEL_AUDIO 96 | ISRC "DED832100090" 97 | CD_TEXT { 98 | LANGUAGE 0 { 99 | TITLE "Itinerari" 100 | PERFORMER "M\330L" 101 | } 102 | } 103 | FILE "data.wav" 25:54:18 05:03:40 104 | 105 | 106 | // Track 7 107 | TRACK AUDIO 108 | NO COPY 109 | NO PRE_EMPHASIS 110 | TWO_CHANNEL_AUDIO 111 | ISRC "DED832100091" 112 | CD_TEXT { 113 | LANGUAGE 0 { 114 | TITLE "Tvesind" 115 | PERFORMER "M\330L" 116 | } 117 | } 118 | FILE "data.wav" 30:57:58 07:48:74 119 | 120 | 121 | // Track 10 122 | TRACK AUDIO 123 | NO COPY 124 | NO PRE_EMPHASIS 125 | TWO_CHANNEL_AUDIO 126 | ISRC "DED832100092" 127 | CD_TEXT { 128 | LANGUAGE 0 { 129 | TITLE "Diorama" 130 | PERFORMER "M\330L" 131 | } 132 | } 133 | FILE "data.wav" 38:46:57 07:14:36 134 | 135 | -------------------------------------------------------------------------------- /whipper/test/diorama_utf8.toc: -------------------------------------------------------------------------------- 1 | CD_DA 2 | 3 | CD_TEXT { 4 | LANGUAGE_MAP { 5 | 0: 9 6 | } 7 | LANGUAGE 0 { 8 | TITLE "Diorama" 9 | PERFORMER "MØL" 10 | SIZE_INFO { 0, 1, 8, 0, 7, 3, 0, 0, 0, 0, 0, 0, 11 | 0, 0, 0, 0, 0, 0, 0, 3, 12, 0, 0, 0, 12 | 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0} 13 | } 14 | } 15 | 16 | // Track 1 17 | TRACK AUDIO 18 | NO COPY 19 | NO PRE_EMPHASIS 20 | TWO_CHANNEL_AUDIO 21 | ISRC "DED832100085" 22 | CD_TEXT { 23 | LANGUAGE 0 { 24 | TITLE "Fraktur" 25 | PERFORMER "MØL" 26 | } 27 | } 28 | FILE "data.wav" 0 04:19:00 29 | 30 | 31 | // Track 2 32 | TRACK AUDIO 33 | NO COPY 34 | NO PRE_EMPHASIS 35 | TWO_CHANNEL_AUDIO 36 | ISRC "DED832100086" 37 | CD_TEXT { 38 | LANGUAGE 0 { 39 | TITLE "Photophobic" 40 | PERFORMER "MØL" 41 | } 42 | } 43 | FILE "data.wav" 04:19:00 05:18:00 44 | 45 | 46 | // Track 3 47 | TRACK AUDIO 48 | NO COPY 49 | NO PRE_EMPHASIS 50 | TWO_CHANNEL_AUDIO 51 | ISRC "DED832100087" 52 | CD_TEXT { 53 | LANGUAGE 0 { 54 | TITLE "Serf" 55 | PERFORMER "MØL" 56 | } 57 | } 58 | FILE "data.wav" 09:37:00 05:22:12 59 | 60 | 61 | // Track 4 62 | TRACK AUDIO 63 | NO COPY 64 | NO PRE_EMPHASIS 65 | TWO_CHANNEL_AUDIO 66 | ISRC "DED832100088" 67 | CD_TEXT { 68 | LANGUAGE 0 { 69 | TITLE "Vestige" 70 | PERFORMER "MØL" 71 | } 72 | } 73 | FILE "data.wav" 14:59:12 05:38:56 74 | 75 | 76 | // Track 5 77 | TRACK AUDIO 78 | NO COPY 79 | NO PRE_EMPHASIS 80 | TWO_CHANNEL_AUDIO 81 | ISRC "DED832100089" 82 | CD_TEXT { 83 | LANGUAGE 0 { 84 | TITLE "Redacted" 85 | PERFORMER "MØL" 86 | } 87 | } 88 | FILE "data.wav" 20:37:68 05:16:25 89 | 90 | 91 | // Track 6 92 | TRACK AUDIO 93 | NO COPY 94 | NO PRE_EMPHASIS 95 | TWO_CHANNEL_AUDIO 96 | ISRC "DED832100090" 97 | CD_TEXT { 98 | LANGUAGE 0 { 99 | TITLE "Itinerari" 100 | PERFORMER "MØL" 101 | } 102 | } 103 | FILE "data.wav" 25:54:18 05:03:40 104 | 105 | 106 | // Track 7 107 | TRACK AUDIO 108 | NO COPY 109 | NO PRE_EMPHASIS 110 | TWO_CHANNEL_AUDIO 111 | ISRC "DED832100091" 112 | CD_TEXT { 113 | LANGUAGE 0 { 114 | TITLE "Tvesind" 115 | PERFORMER "MØL" 116 | } 117 | } 118 | FILE "data.wav" 30:57:58 07:48:74 119 | 120 | 121 | // Track 8 122 | TRACK AUDIO 123 | NO COPY 124 | NO PRE_EMPHASIS 125 | TWO_CHANNEL_AUDIO 126 | ISRC "DED832100092" 127 | CD_TEXT { 128 | LANGUAGE 0 { 129 | TITLE "Diorama" 130 | PERFORMER "MØL" 131 | } 132 | } 133 | FILE "data.wav" 38:46:57 07:14:36 134 | 135 | -------------------------------------------------------------------------------- /whipper/test/gentlemen.fast.toc: -------------------------------------------------------------------------------- 1 | CD_DA 2 | 3 | CATALOG "0075596150125" 4 | 5 | // Track 1 6 | TRACK AUDIO 7 | NO COPY 8 | NO PRE_EMPHASIS 9 | TWO_CHANNEL_AUDIO 10 | FILE "data.wav" 0 03:05:62 11 | 12 | 13 | // Track 2 14 | TRACK AUDIO 15 | NO COPY 16 | NO PRE_EMPHASIS 17 | TWO_CHANNEL_AUDIO 18 | FILE "data.wav" 03:05:62 03:53:53 19 | 20 | 21 | // Track 3 22 | TRACK AUDIO 23 | NO COPY 24 | NO PRE_EMPHASIS 25 | TWO_CHANNEL_AUDIO 26 | FILE "data.wav" 06:59:40 03:36:70 27 | 28 | 29 | // Track 4 30 | TRACK AUDIO 31 | NO COPY 32 | NO PRE_EMPHASIS 33 | TWO_CHANNEL_AUDIO 34 | FILE "data.wav" 10:36:35 04:14:42 35 | 36 | 37 | // Track 5 38 | TRACK AUDIO 39 | NO COPY 40 | NO PRE_EMPHASIS 41 | TWO_CHANNEL_AUDIO 42 | FILE "data.wav" 14:51:02 05:48:05 43 | 44 | 45 | // Track 6 46 | TRACK AUDIO 47 | NO COPY 48 | NO PRE_EMPHASIS 49 | TWO_CHANNEL_AUDIO 50 | FILE "data.wav" 20:39:07 04:21:23 51 | 52 | 53 | // Track 7 54 | TRACK AUDIO 55 | NO COPY 56 | NO PRE_EMPHASIS 57 | TWO_CHANNEL_AUDIO 58 | FILE "data.wav" 25:00:30 03:30:50 59 | 60 | 61 | // Track 8 62 | TRACK AUDIO 63 | NO COPY 64 | NO PRE_EMPHASIS 65 | TWO_CHANNEL_AUDIO 66 | FILE "data.wav" 28:31:05 05:46:00 67 | 68 | 69 | // Track 9 70 | TRACK AUDIO 71 | NO COPY 72 | NO PRE_EMPHASIS 73 | TWO_CHANNEL_AUDIO 74 | FILE "data.wav" 34:17:05 04:10:22 75 | 76 | 77 | // Track 10 78 | TRACK AUDIO 79 | NO COPY 80 | NO PRE_EMPHASIS 81 | TWO_CHANNEL_AUDIO 82 | FILE "data.wav" 38:27:27 04:51:65 83 | 84 | 85 | // Track 11 86 | TRACK AUDIO 87 | NO COPY 88 | NO PRE_EMPHASIS 89 | TWO_CHANNEL_AUDIO 90 | FILE "data.wav" 43:19:17 05:40:03 91 | 92 | -------------------------------------------------------------------------------- /whipper/test/jose.toc: -------------------------------------------------------------------------------- 1 | CD_DA 2 | 3 | CD_TEXT { 4 | LANGUAGE_MAP { 5 | 0: 9 6 | } 7 | LANGUAGE 0 { 8 | TITLE "In Our Nature" 9 | PERFORMER "Jos\351 Gonz\341lez" 10 | GENRE { 0, 0, 0} 11 | SIZE_INFO { 1, 1, 10, 0, 12, 13, 0, 0, 0, 0, 0, 1, 12 | 0, 0, 0, 0, 0, 0, 0, 3, 28, 0, 0, 0, 13 | 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0} 14 | } 15 | } 16 | 17 | // Track 1 18 | TRACK AUDIO 19 | NO COPY 20 | NO PRE_EMPHASIS 21 | TWO_CHANNEL_AUDIO 22 | ISRC "SEVVX0700301" 23 | CD_TEXT { 24 | LANGUAGE 0 { 25 | TITLE "How Low" 26 | PERFORMER "Jos\351 Gonz\341lez" 27 | } 28 | } 29 | FILE "data.wav" 0 02:40:01 30 | 31 | 32 | // Track 2 33 | TRACK AUDIO 34 | NO COPY 35 | NO PRE_EMPHASIS 36 | TWO_CHANNEL_AUDIO 37 | ISRC "SEVVX0700302" 38 | CD_TEXT { 39 | LANGUAGE 0 { 40 | TITLE "Down The Line" 41 | PERFORMER "Jos\351 Gonz\341lez" 42 | } 43 | } 44 | FILE "data.wav" 02:40:01 03:10:62 45 | 46 | 47 | // Track 3 48 | TRACK AUDIO 49 | NO COPY 50 | NO PRE_EMPHASIS 51 | TWO_CHANNEL_AUDIO 52 | ISRC "SEVVX0700303" 53 | CD_TEXT { 54 | LANGUAGE 0 { 55 | TITLE "Killing For Love" 56 | PERFORMER "Jos\351 Gonz\341lez" 57 | } 58 | } 59 | FILE "data.wav" 05:50:63 03:02:67 60 | 61 | 62 | // Track 4 63 | TRACK AUDIO 64 | NO COPY 65 | NO PRE_EMPHASIS 66 | TWO_CHANNEL_AUDIO 67 | ISRC "SEVVX0700304" 68 | CD_TEXT { 69 | LANGUAGE 0 { 70 | TITLE "In Our Nature" 71 | PERFORMER "Jos\351 Gonz\341lez" 72 | } 73 | } 74 | FILE "data.wav" 08:53:55 02:42:51 75 | 76 | 77 | // Track 5 78 | TRACK AUDIO 79 | NO COPY 80 | NO PRE_EMPHASIS 81 | TWO_CHANNEL_AUDIO 82 | ISRC "SEVVX0700305" 83 | CD_TEXT { 84 | LANGUAGE 0 { 85 | TITLE "Teardrop" 86 | PERFORMER "Jos\351 Gonz\341lez" 87 | } 88 | } 89 | FILE "data.wav" 11:36:31 03:20:64 90 | 91 | 92 | // Track 6 93 | TRACK AUDIO 94 | NO COPY 95 | NO PRE_EMPHASIS 96 | TWO_CHANNEL_AUDIO 97 | ISRC "SEVVX0700306" 98 | CD_TEXT { 99 | LANGUAGE 0 { 100 | TITLE "Abram" 101 | PERFORMER "Jos\351 Gonz\341lez" 102 | } 103 | } 104 | FILE "data.wav" 14:57:20 01:58:56 105 | START 00:12:24 106 | 107 | 108 | // Track 7 109 | TRACK AUDIO 110 | NO COPY 111 | NO PRE_EMPHASIS 112 | TWO_CHANNEL_AUDIO 113 | ISRC "SEVVX0700307" 114 | CD_TEXT { 115 | LANGUAGE 0 { 116 | TITLE "Time To Send Someone Away" 117 | PERFORMER "Jos\351 Gonz\341lez" 118 | } 119 | } 120 | FILE "data.wav" 16:56:01 02:49:68 121 | START 00:02:05 122 | 123 | 124 | // Track 8 125 | TRACK AUDIO 126 | NO COPY 127 | NO PRE_EMPHASIS 128 | TWO_CHANNEL_AUDIO 129 | ISRC "SEVVX0700308" 130 | CD_TEXT { 131 | LANGUAGE 0 { 132 | TITLE "The Nest" 133 | PERFORMER "Jos\351 Gonz\341lez" 134 | } 135 | } 136 | FILE "data.wav" 19:45:69 02:23:66 137 | 138 | 139 | // Track 9 140 | TRACK AUDIO 141 | NO COPY 142 | NO PRE_EMPHASIS 143 | TWO_CHANNEL_AUDIO 144 | ISRC "SEVVX0700309" 145 | CD_TEXT { 146 | LANGUAGE 0 { 147 | TITLE "Fold" 148 | PERFORMER "Jos\351 Gonz\341lez" 149 | } 150 | } 151 | FILE "data.wav" 22:09:60 02:54:58 152 | 153 | 154 | // Track 10 155 | TRACK AUDIO 156 | NO COPY 157 | NO PRE_EMPHASIS 158 | TWO_CHANNEL_AUDIO 159 | ISRC "SEVVX0700310" 160 | CD_TEXT { 161 | LANGUAGE 0 { 162 | TITLE "Cycling Trivialities" 163 | PERFORMER "Jos\351 Gonz\341lez" 164 | } 165 | } 166 | FILE "data.wav" 25:04:43 08:09:16 167 | 168 | -------------------------------------------------------------------------------- /whipper/test/kanye.cue: -------------------------------------------------------------------------------- 1 | REM GENRE "Hip Hop" 2 | REM DATE 2008 3 | REM DISCID A90D2E0D 4 | REM COMMENT "ExactAudioCopy v0.99pb4" 5 | CATALOG 0602517931596 6 | PERFORMER "Kanye West" 7 | TITLE "808s & Heartbreak" 8 | FILE "Kanye West - 808s & Heartbreak\Kanye West - Say You Will.wav" WAVE 9 | TRACK 01 AUDIO 10 | TITLE "Say You Will" 11 | PERFORMER "Kanye West" 12 | ISRC USUM70846386 13 | INDEX 01 00:00:00 14 | FILE "Kanye West - 808s & Heartbreak\Kanye West - Welcome To Heartbreak (Feat. Kid Cudi).wav" WAVE 15 | TRACK 02 AUDIO 16 | TITLE "Welcome To Heartbreak (Feat. Kid Cudi)" 17 | PERFORMER "Kanye West" 18 | ISRC USUM70846387 19 | INDEX 01 00:00:00 20 | TRACK 03 AUDIO 21 | TITLE "Heartless" 22 | PERFORMER "Kanye West" 23 | ISRC USUM70840511 24 | INDEX 00 04:22:70 25 | FILE "Kanye West - 808s & Heartbreak\Kanye West - Heartless.wav" WAVE 26 | INDEX 01 00:00:00 27 | FILE "Kanye West - 808s & Heartbreak\Kanye West - Amazing (Feat. Young Jeezy).wav" WAVE 28 | TRACK 04 AUDIO 29 | TITLE "Amazing (Feat. Young Jeezy)" 30 | PERFORMER "Kanye West" 31 | ISRC USUM70846401 32 | INDEX 01 00:00:00 33 | FILE "Kanye West - 808s & Heartbreak\Kanye West - Love Lockdown.wav" WAVE 34 | TRACK 05 AUDIO 35 | TITLE "Love Lockdown" 36 | PERFORMER "Kanye West" 37 | ISRC USUM70837229 38 | INDEX 01 00:00:00 39 | TRACK 06 AUDIO 40 | TITLE "Paranoid (Feat. Mr. Hudson)" 41 | PERFORMER "Kanye West" 42 | ISRC USUM70846402 43 | INDEX 00 04:30:23 44 | FILE "Kanye West - 808s & Heartbreak\Kanye West - Paranoid (Feat. Mr. Hudson).wav" WAVE 45 | INDEX 01 00:00:00 46 | FILE "Kanye West - 808s & Heartbreak\Kanye West - RoboCop.wav" WAVE 47 | TRACK 07 AUDIO 48 | TITLE "RoboCop" 49 | PERFORMER "Kanye West" 50 | ISRC USUM70846388 51 | INDEX 01 00:00:00 52 | TRACK 08 AUDIO 53 | TITLE "Street Lights" 54 | PERFORMER "Kanye West" 55 | ISRC USUM70846403 56 | INDEX 00 04:34:27 57 | FILE "Kanye West - 808s & Heartbreak\Kanye West - Street Lights.wav" WAVE 58 | INDEX 01 00:00:00 59 | FILE "Kanye West - 808s & Heartbreak\Kanye West - Bad News.wav" WAVE 60 | TRACK 09 AUDIO 61 | TITLE "Bad News" 62 | PERFORMER "Kanye West" 63 | ISRC USUM70846389 64 | INDEX 01 00:00:00 65 | FILE "Kanye West - 808s & Heartbreak\Kanye West - See You In My Nightmares (Feat. Lil Wayne).wav" WAVE 66 | TRACK 10 AUDIO 67 | TITLE "See You In My Nightmares (Feat. Lil Wayne)" 68 | PERFORMER "Kanye West" 69 | ISRC USUM70846390 70 | INDEX 01 00:00:00 71 | TRACK 11 AUDIO 72 | TITLE "Coldest Winter" 73 | PERFORMER "Kanye West" 74 | ISRC USUM70846400 75 | INDEX 00 04:18:09 76 | FILE "Kanye West - 808s & Heartbreak\Kanye West - Coldest Winter.wav" WAVE 77 | INDEX 01 00:00:00 78 | TRACK 12 AUDIO 79 | TITLE "Pinocchio Story (Freestyle Live From Singapore)" 80 | PERFORMER "Kanye West" 81 | ISRC USUM70846838 82 | INDEX 00 02:44:25 83 | FILE "Kanye West - 808s & Heartbreak\Kanye West - Pinocchio Story (Freestyle Live From Singapore).wav" WAVE 84 | INDEX 01 00:00:00 85 | TRACK 13 MODEx/2xxx 86 | TITLE "Data Track" 87 | PERFORMER "Kanye West" 88 | INDEX 00 06:01:45 89 | -------------------------------------------------------------------------------- /whipper/test/kings-separate.cue: -------------------------------------------------------------------------------- 1 | REM GENRE Alternative 2 | REM DATE 2008 3 | REM DISCID 9809FF0B 4 | REM COMMENT "ExactAudioCopy v0.99pb4" 5 | PERFORMER "Kings of Leon" 6 | TITLE "Only By the Night" 7 | FILE "Kings of Leon - Only By the Night\Kings of Leon - Closer.wav" WAVE 8 | TRACK 01 AUDIO 9 | TITLE "Closer" 10 | PERFORMER "Kings of Leon" 11 | INDEX 01 00:00:00 12 | FILE "Kings of Leon - Only By the Night\Kings of Leon - Crawl.wav" WAVE 13 | TRACK 02 AUDIO 14 | TITLE "Crawl" 15 | PERFORMER "Kings of Leon" 16 | INDEX 01 00:00:00 17 | FILE "Kings of Leon - Only By the Night\Kings of Leon - Sex On Fire.wav" WAVE 18 | TRACK 03 AUDIO 19 | TITLE "Sex On Fire" 20 | PERFORMER "Kings of Leon" 21 | INDEX 01 00:00:00 22 | FILE "Kings of Leon - Only By the Night\Kings of Leon - Use Somebody.wav" WAVE 23 | TRACK 04 AUDIO 24 | TITLE "Use Somebody" 25 | PERFORMER "Kings of Leon" 26 | INDEX 01 00:00:00 27 | FILE "Kings of Leon - Only By the Night\Kings of Leon - Manhattan.wav" WAVE 28 | TRACK 05 AUDIO 29 | TITLE "Manhattan" 30 | PERFORMER "Kings of Leon" 31 | INDEX 01 00:00:00 32 | FILE "Kings of Leon - Only By the Night\Kings of Leon - Revelry.wav" WAVE 33 | TRACK 06 AUDIO 34 | TITLE "Revelry" 35 | PERFORMER "Kings of Leon" 36 | INDEX 01 00:00:00 37 | FILE "Kings of Leon - Only By the Night\Kings of Leon - 17.wav" WAVE 38 | TRACK 07 AUDIO 39 | TITLE "17" 40 | PERFORMER "Kings of Leon" 41 | INDEX 01 00:00:00 42 | FILE "Kings of Leon - Only By the Night\Kings of Leon - Notion.wav" WAVE 43 | TRACK 08 AUDIO 44 | TITLE "Notion" 45 | PERFORMER "Kings of Leon" 46 | INDEX 01 00:00:00 47 | FILE "Kings of Leon - Only By the Night\Kings of Leon - I Want You.wav" WAVE 48 | TRACK 09 AUDIO 49 | TITLE "I Want You" 50 | PERFORMER "Kings of Leon" 51 | INDEX 01 00:00:00 52 | FILE "Kings of Leon - Only By the Night\Kings of Leon - Be Somebody.wav" WAVE 53 | TRACK 10 AUDIO 54 | TITLE "Be Somebody" 55 | PERFORMER "Kings of Leon" 56 | INDEX 01 00:00:00 57 | FILE "Kings of Leon - Only By the Night\Kings of Leon - Cold Desert.wav" WAVE 58 | TRACK 11 AUDIO 59 | TITLE "Cold Desert" 60 | PERFORMER "Kings of Leon" 61 | INDEX 01 00:00:00 62 | -------------------------------------------------------------------------------- /whipper/test/kings-single.cue: -------------------------------------------------------------------------------- 1 | FILE "dummy.wav" WAVE 2 | TRACK 01 AUDIO 3 | INDEX 01 00:00:00 4 | TRACK 02 AUDIO 5 | INDEX 01 03:57:36 6 | TRACK 03 AUDIO 7 | INDEX 01 08:03:67 8 | TRACK 04 AUDIO 9 | INDEX 01 11:27:18 10 | TRACK 05 AUDIO 11 | INDEX 01 15:18:00 12 | TRACK 06 AUDIO 13 | INDEX 01 18:42:17 14 | TRACK 07 AUDIO 15 | INDEX 01 22:03:72 16 | TRACK 08 AUDIO 17 | INDEX 01 25:09:25 18 | TRACK 09 AUDIO 19 | INDEX 01 28:10:13 20 | TRACK 10 AUDIO 21 | INDEX 01 33:17:47 22 | TRACK 11 AUDIO 23 | INDEX 01 37:04:58 24 | -------------------------------------------------------------------------------- /whipper/test/ladyhawke.toc: -------------------------------------------------------------------------------- 1 | CD_ROM_XA 2 | 3 | CATALOG "0602517818866" 4 | 5 | // Track 1 6 | TRACK AUDIO 7 | NO COPY 8 | NO PRE_EMPHASIS 9 | TWO_CHANNEL_AUDIO 10 | ISRC "GBUM70808708" 11 | FILE "data.wav" 0 03:26:53 12 | 13 | 14 | // Track 2 15 | TRACK AUDIO 16 | NO COPY 17 | NO PRE_EMPHASIS 18 | TWO_CHANNEL_AUDIO 19 | ISRC "GBUM70810780" 20 | FILE "data.wav" 03:26:53 03:35:46 21 | START 00:00:34 22 | 23 | 24 | // Track 3 25 | TRACK AUDIO 26 | NO COPY 27 | NO PRE_EMPHASIS 28 | TWO_CHANNEL_AUDIO 29 | ISRC "GBUM70808705" 30 | FILE "data.wav" 07:02:24 04:15:03 31 | START 00:00:17 32 | 33 | 34 | // Track 4 35 | TRACK AUDIO 36 | NO COPY 37 | NO PRE_EMPHASIS 38 | TWO_CHANNEL_AUDIO 39 | ISRC "GBUM70810804" 40 | FILE "data.wav" 11:17:27 03:26:60 41 | START 00:00:64 42 | 43 | 44 | // Track 5 45 | TRACK AUDIO 46 | NO COPY 47 | NO PRE_EMPHASIS 48 | TWO_CHANNEL_AUDIO 49 | ISRC "GBUM70810795" 50 | FILE "data.wav" 14:44:12 03:17:34 51 | START 00:02:04 52 | 53 | 54 | // Track 6 55 | TRACK AUDIO 56 | NO COPY 57 | NO PRE_EMPHASIS 58 | TWO_CHANNEL_AUDIO 59 | ISRC "GBUM70810805" 60 | FILE "data.wav" 18:01:46 04:03:13 61 | START 00:01:06 62 | 63 | 64 | // Track 7 65 | TRACK AUDIO 66 | NO COPY 67 | NO PRE_EMPHASIS 68 | TWO_CHANNEL_AUDIO 69 | ISRC "AUUM70800167" 70 | FILE "data.wav" 22:04:59 03:40:53 71 | START 00:00:50 72 | 73 | 74 | // Track 8 75 | TRACK AUDIO 76 | NO COPY 77 | NO PRE_EMPHASIS 78 | TWO_CHANNEL_AUDIO 79 | ISRC "GBUM70808467" 80 | FILE "data.wav" 25:45:37 03:47:38 81 | START 00:00:08 82 | 83 | 84 | // Track 9 85 | TRACK AUDIO 86 | NO COPY 87 | NO PRE_EMPHASIS 88 | TWO_CHANNEL_AUDIO 89 | ISRC "GBUM70810807" 90 | FILE "data.wav" 29:33:00 03:44:32 91 | START 00:01:43 92 | 93 | 94 | // Track 10 95 | TRACK AUDIO 96 | NO COPY 97 | NO PRE_EMPHASIS 98 | TWO_CHANNEL_AUDIO 99 | ISRC "GBUM70811315" 100 | FILE "data.wav" 33:17:32 02:36:03 101 | START 00:00:40 102 | 103 | 104 | // Track 11 105 | TRACK AUDIO 106 | NO COPY 107 | NO PRE_EMPHASIS 108 | TWO_CHANNEL_AUDIO 109 | ISRC "GBUM70810809" 110 | FILE "data.wav" 35:53:35 03:34:50 111 | START 00:00:50 112 | 113 | 114 | // Track 12 115 | TRACK AUDIO 116 | NO COPY 117 | NO PRE_EMPHASIS 118 | TWO_CHANNEL_AUDIO 119 | ISRC "GBUM70810814" 120 | FILE "data.wav" 39:28:10 06:31:21 121 | START 00:00:72 122 | 123 | 124 | // Track 13 125 | TRACK MODE2_FORM_MIX 126 | NO COPY 127 | ZERO MODE2_FORM_MIX 00:02:00 128 | DATAFILE "data_13" 00:43:54 // length in bytes: 7659744 129 | START 00:02:00 130 | 131 | -------------------------------------------------------------------------------- /whipper/test/release.08397059-86c1-463b-8ed0-cd596dbd174f.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Das Capital: The Songwriting Genius of Luke Haines and The Auteurs 6 | B00009XG2O 7 | 8 | Luke HainesHaines, Luke 9 | 10 | 11 | 12 | How Could I Be Wrong273800 13 | 14 | 15 | Showgirl256466 16 | 17 | 18 | Baader Meinhof183933 19 | 20 | 21 | Lenny Valentino136133 22 | 23 | 24 | Starstruck212333 25 | 26 | 27 | Satan Wants Me189666 28 | 29 | 30 | Unsolved Child Murder146800 31 | 32 | 33 | Junk Shop Clothes166800 34 | 35 | 36 | The Mitford Sisters302960 37 | 38 | 39 | Bugger Bognor230573 40 | 41 | 42 | Future Generation216266 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /whipper/test/release.93a6268c-ddf1-4898-bf93-fb862b1c5c5e.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Ladyhawke 5 | 6 | LadyhawkeLadyhawke 7 | 8 | 9 | 10 | Magic207000 11 | 12 | 13 | Manipulating Woman215000 14 | 15 | 16 | My Delirium255000 17 | 18 | 19 | Better Than Sunday208000 20 | 21 | 22 | Another Runaway196000 23 | 24 | 25 | Love Don't Live Here242000 26 | 27 | 28 | Back of the Van220000 29 | 30 | 31 | Paris Is Burning229000 32 | 33 | 34 | Professional Suicide223000 35 | 36 | 37 | Dusk Till Dawn156000 38 | 39 | 40 | Crazy World215000 41 | 42 | 43 | Morning Dreams240000 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /whipper/test/release.c7d919f4-3ea0-4c4b-a230-b3605f069440.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | Lamprey 7 | B00000581T 8 | 9 | Bettie ServeertBettie Serveert 10 | 11 | 12 | 13 | Keepsake378693 14 | 15 | 16 | Ray Ray Rain262106 17 | 18 | 19 | D. Feathers332626 20 | 21 | 22 | Re-Feel-It238240 23 | 24 | 25 | 21 Days203826 26 | 27 | 28 | Cybor*D241800 29 | 30 | 31 | Tell Me, Sad318333 32 | 33 | 34 | Crutches292373 35 | 36 | 37 | Something So Wild171466 38 | 39 | 40 | Totally Freaked Out250893 41 | 42 | 43 | Silent Spring272533 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /whipper/test/strokes-someday.eac.cue: -------------------------------------------------------------------------------- 1 | REM GENRE "Alternative Rock" 2 | REM DATE 2001 3 | REM DISCID 0200BA01 4 | REM COMMENT "ExactAudioCopy v0.99pb4" 5 | PERFORMER "The Strokes" 6 | TITLE "Someday" 7 | FILE "The Strokes - Someday\01 - The Strokes - Someday.wav" WAVE 8 | TRACK 01 AUDIO 9 | TITLE "Someday" 10 | PERFORMER "The Strokes" 11 | FLAGS DCP 12 | PREGAP 00:00:01 13 | INDEX 01 00:00:00 14 | -------------------------------------------------------------------------------- /whipper/test/strokes-someday.toc: -------------------------------------------------------------------------------- 1 | CD_DA 2 | 3 | 4 | // Track 1 5 | TRACK AUDIO 6 | COPY 7 | NO PRE_EMPHASIS 8 | TWO_CHANNEL_AUDIO 9 | SILENCE 00:00:01 10 | FILE "data.wav" 0 03:06:59 11 | START 00:00:01 12 | 13 | -------------------------------------------------------------------------------- /whipper/test/surferrosa.eac.corrected.cue: -------------------------------------------------------------------------------- 1 | REM GENRE Alternative 2 | REM DATE 1987 3 | REM DISCID 350CAA15 4 | REM COMMENT "ExactAudioCopy v0.99pb4" 5 | CATALOG 0000000000000 6 | PERFORMER "Pixies" 7 | TITLE "Surfer Rosa & Come on Pilgrim" 8 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\01 - Pixies - Bone Machine.wav" WAVE 9 | TRACK 01 AUDIO 10 | TITLE "Bone Machine" 11 | PERFORMER "Pixies" 12 | ISRC 000000000000 13 | INDEX 00 00:00:00 14 | INDEX 01 00:00:32 15 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\02 - Pixies - Break My Body.wav" WAVE 16 | TRACK 02 AUDIO 17 | TITLE "Break My Body" 18 | PERFORMER "Pixies" 19 | ISRC 000000000000 20 | INDEX 01 00:00:00 21 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\03 - Pixies - Something Against You.wav" WAVE 22 | TRACK 03 AUDIO 23 | TITLE "Something Against You" 24 | PERFORMER "Pixies" 25 | ISRC 000000000000 26 | INDEX 00 00:00:00 27 | INDEX 01 00:00:45 28 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\04 - Pixies - Broken Face.wav" WAVE 29 | TRACK 04 AUDIO 30 | TITLE "Broken Face" 31 | PERFORMER "Pixies" 32 | ISRC 000000000000 33 | INDEX 01 00:00:00 34 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\05 - Pixies - Gigantic.wav" WAVE 35 | TRACK 05 AUDIO 36 | TITLE "Gigantic" 37 | PERFORMER "Pixies" 38 | ISRC 000000000000 39 | INDEX 01 00:00:00 40 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\06 - Pixies - River Euphrates.wav" WAVE 41 | TRACK 06 AUDIO 42 | TITLE "River Euphrates" 43 | PERFORMER "Pixies" 44 | ISRC 000000000000 45 | INDEX 01 00:00:00 46 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\07 - Pixies - Where Is My Mind .wav" WAVE 47 | TRACK 07 AUDIO 48 | TITLE "Where Is My Mind?" 49 | PERFORMER "Pixies" 50 | ISRC 000000000000 51 | INDEX 01 00:00:00 52 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\08 - Pixies - Cactus.wav" WAVE 53 | TRACK 08 AUDIO 54 | TITLE "Cactus" 55 | PERFORMER "Pixies" 56 | ISRC 000000000000 57 | INDEX 01 00:00:00 58 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\09 - Pixies - Tony's Theme.wav" WAVE 59 | TRACK 09 AUDIO 60 | TITLE "Tony's Theme" 61 | PERFORMER "Pixies" 62 | ISRC 000000000000 63 | INDEX 01 00:00:00 64 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\10 - Pixies - Oh My Golly!.wav" WAVE 65 | TRACK 10 AUDIO 66 | TITLE "Oh My Golly!" 67 | PERFORMER "Pixies" 68 | ISRC 000000000000 69 | INDEX 01 00:00:00 70 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\11 - Pixies - Vamos.wav" WAVE 71 | TRACK 11 AUDIO 72 | TITLE "Vamos" 73 | PERFORMER "Pixies" 74 | ISRC 000000000000 75 | INDEX 01 00:00:00 76 | INDEX 02 00:44:70 77 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\12 - Pixies - I'm Amazed.wav" WAVE 78 | TRACK 12 AUDIO 79 | TITLE "I'm Amazed" 80 | PERFORMER "Pixies" 81 | ISRC 000000000000 82 | INDEX 01 00:00:00 83 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\13 - Pixies - Brick is Red.wav" WAVE 84 | TRACK 13 AUDIO 85 | TITLE "Brick is Red" 86 | PERFORMER "Pixies" 87 | ISRC 000000000000 88 | INDEX 01 00:00:00 89 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\14 - Pixies - Caribou.wav" WAVE 90 | TRACK 14 AUDIO 91 | TITLE "Caribou" 92 | PERFORMER "Pixies" 93 | ISRC 000000000000 94 | INDEX 01 00:00:00 95 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\15 - Pixies - Vamos.wav" WAVE 96 | TRACK 15 AUDIO 97 | TITLE "Vamos" 98 | PERFORMER "Pixies" 99 | ISRC 000000000000 100 | INDEX 01 00:00:00 101 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\16 - Pixies - Isla de Encanta.wav" WAVE 102 | TRACK 16 AUDIO 103 | TITLE "Isla de Encanta" 104 | PERFORMER "Pixies" 105 | ISRC 000000000000 106 | INDEX 01 00:00:00 107 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\17 - Pixies - Ed is Dead.wav" WAVE 108 | TRACK 17 AUDIO 109 | TITLE "Ed is Dead" 110 | PERFORMER "Pixies" 111 | ISRC 000000000000 112 | INDEX 01 00:00:00 113 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\18 - Pixies - The Holyday Song.wav" WAVE 114 | TRACK 18 AUDIO 115 | TITLE "The Holyday Song" 116 | PERFORMER "Pixies" 117 | ISRC 000000000000 118 | INDEX 01 00:00:00 119 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\19 - Pixies - Nimrod's Son.wav" WAVE 120 | TRACK 19 AUDIO 121 | TITLE "Nimrod's Son" 122 | PERFORMER "Pixies" 123 | ISRC 000000000000 124 | INDEX 01 00:00:00 125 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\20 - Pixies - I've Been Tired.wav" WAVE 126 | TRACK 20 AUDIO 127 | TITLE "I've Been Tired" 128 | PERFORMER "Pixies" 129 | ISRC 000000000000 130 | INDEX 01 00:00:00 131 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\21 - Pixies - Levitate Me.wav" WAVE 132 | TRACK 21 AUDIO 133 | TITLE "Levitate Me" 134 | PERFORMER "Pixies" 135 | ISRC 000000000000 136 | INDEX 01 00:00:00 137 | -------------------------------------------------------------------------------- /whipper/test/surferrosa.eac.currentgap.cue: -------------------------------------------------------------------------------- 1 | REM GENRE Alternative 2 | REM DATE 1987 3 | REM DISCID 350CAA15 4 | REM COMMENT "ExactAudioCopy v0.99pb4" 5 | CATALOG 0000000000000 6 | PERFORMER "Pixies" 7 | TITLE "Surfer Rosa & Come on Pilgrim" 8 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\01 - Pixies - Bone Machine.wav" WAVE 9 | TRACK 01 AUDIO 10 | TITLE "Bone Machine" 11 | PERFORMER "Pixies" 12 | ISRC 000000000000 13 | PREGAP 00:00:32 14 | INDEX 01 00:00:00 15 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\02 - Pixies - Break My Body.wav" WAVE 16 | TRACK 02 AUDIO 17 | TITLE "Break My Body" 18 | PERFORMER "Pixies" 19 | ISRC 000000000000 20 | INDEX 01 00:00:00 21 | TRACK 03 AUDIO 22 | TITLE "Something Against You" 23 | PERFORMER "Pixies" 24 | ISRC 000000000000 25 | INDEX 00 02:05:00 26 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\03 - Pixies - Something Against You.wav" WAVE 27 | INDEX 01 00:00:00 28 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\04 - Pixies - Broken Face.wav" WAVE 29 | TRACK 04 AUDIO 30 | TITLE "Broken Face" 31 | PERFORMER "Pixies" 32 | ISRC 000000000000 33 | INDEX 01 00:00:00 34 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\05 - Pixies - Gigantic.wav" WAVE 35 | TRACK 05 AUDIO 36 | TITLE "Gigantic" 37 | PERFORMER "Pixies" 38 | ISRC 000000000000 39 | INDEX 01 00:00:00 40 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\06 - Pixies - River Euphrates.wav" WAVE 41 | TRACK 06 AUDIO 42 | TITLE "River Euphrates" 43 | PERFORMER "Pixies" 44 | ISRC 000000000000 45 | INDEX 01 00:00:00 46 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\07 - Pixies - Where Is My Mind .wav" WAVE 47 | TRACK 07 AUDIO 48 | TITLE "Where Is My Mind?" 49 | PERFORMER "Pixies" 50 | ISRC 000000000000 51 | INDEX 01 00:00:00 52 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\08 - Pixies - Cactus.wav" WAVE 53 | TRACK 08 AUDIO 54 | TITLE "Cactus" 55 | PERFORMER "Pixies" 56 | ISRC 000000000000 57 | INDEX 01 00:00:00 58 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\09 - Pixies - Tony's Theme.wav" WAVE 59 | TRACK 09 AUDIO 60 | TITLE "Tony's Theme" 61 | PERFORMER "Pixies" 62 | ISRC 000000000000 63 | INDEX 01 00:00:00 64 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\10 - Pixies - Oh My Golly!.wav" WAVE 65 | TRACK 10 AUDIO 66 | TITLE "Oh My Golly!" 67 | PERFORMER "Pixies" 68 | ISRC 000000000000 69 | INDEX 01 00:00:00 70 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\11 - Pixies - Vamos.wav" WAVE 71 | TRACK 11 AUDIO 72 | TITLE "Vamos" 73 | PERFORMER "Pixies" 74 | ISRC 000000000000 75 | INDEX 01 00:00:00 76 | INDEX 02 00:44:70 77 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\12 - Pixies - I'm Amazed.wav" WAVE 78 | TRACK 12 AUDIO 79 | TITLE "I'm Amazed" 80 | PERFORMER "Pixies" 81 | ISRC 000000000000 82 | INDEX 01 00:00:00 83 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\13 - Pixies - Brick is Red.wav" WAVE 84 | TRACK 13 AUDIO 85 | TITLE "Brick is Red" 86 | PERFORMER "Pixies" 87 | ISRC 000000000000 88 | INDEX 01 00:00:00 89 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\14 - Pixies - Caribou.wav" WAVE 90 | TRACK 14 AUDIO 91 | TITLE "Caribou" 92 | PERFORMER "Pixies" 93 | ISRC 000000000000 94 | INDEX 01 00:00:00 95 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\15 - Pixies - Vamos.wav" WAVE 96 | TRACK 15 AUDIO 97 | TITLE "Vamos" 98 | PERFORMER "Pixies" 99 | ISRC 000000000000 100 | INDEX 01 00:00:00 101 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\16 - Pixies - Isla de Encanta.wav" WAVE 102 | TRACK 16 AUDIO 103 | TITLE "Isla de Encanta" 104 | PERFORMER "Pixies" 105 | ISRC 000000000000 106 | INDEX 01 00:00:00 107 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\17 - Pixies - Ed is Dead.wav" WAVE 108 | TRACK 17 AUDIO 109 | TITLE "Ed is Dead" 110 | PERFORMER "Pixies" 111 | ISRC 000000000000 112 | INDEX 01 00:00:00 113 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\18 - Pixies - The Holyday Song.wav" WAVE 114 | TRACK 18 AUDIO 115 | TITLE "The Holyday Song" 116 | PERFORMER "Pixies" 117 | ISRC 000000000000 118 | INDEX 01 00:00:00 119 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\19 - Pixies - Nimrod's Son.wav" WAVE 120 | TRACK 19 AUDIO 121 | TITLE "Nimrod's Son" 122 | PERFORMER "Pixies" 123 | ISRC 000000000000 124 | INDEX 01 00:00:00 125 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\20 - Pixies - I've Been Tired.wav" WAVE 126 | TRACK 20 AUDIO 127 | TITLE "I've Been Tired" 128 | PERFORMER "Pixies" 129 | ISRC 000000000000 130 | INDEX 01 00:00:00 131 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\21 - Pixies - Levitate Me.wav" WAVE 132 | TRACK 21 AUDIO 133 | TITLE "Levitate Me" 134 | PERFORMER "Pixies" 135 | ISRC 000000000000 136 | INDEX 01 00:00:00 137 | -------------------------------------------------------------------------------- /whipper/test/surferrosa.eac.leftout.cue: -------------------------------------------------------------------------------- 1 | REM GENRE Alternative 2 | REM DATE 1987 3 | REM DISCID 350CAA15 4 | REM COMMENT "ExactAudioCopy v0.99pb4" 5 | CATALOG 0000000000000 6 | PERFORMER "Pixies" 7 | TITLE "Surfer Rosa & Come on Pilgrim" 8 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\01 - Pixies - Bone Machine.wav" WAVE 9 | TRACK 01 AUDIO 10 | TITLE "Bone Machine" 11 | PERFORMER "Pixies" 12 | ISRC 000000000000 13 | PREGAP 00:00:32 14 | INDEX 01 00:00:00 15 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\02 - Pixies - Break My Body.wav" WAVE 16 | TRACK 02 AUDIO 17 | TITLE "Break My Body" 18 | PERFORMER "Pixies" 19 | ISRC 000000000000 20 | INDEX 01 00:00:00 21 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\03 - Pixies - Something Against You.wav" WAVE 22 | TRACK 03 AUDIO 23 | TITLE "Something Against You" 24 | PERFORMER "Pixies" 25 | ISRC 000000000000 26 | PREGAP 00:00:45 27 | INDEX 01 00:00:00 28 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\04 - Pixies - Broken Face.wav" WAVE 29 | TRACK 04 AUDIO 30 | TITLE "Broken Face" 31 | PERFORMER "Pixies" 32 | ISRC 000000000000 33 | INDEX 01 00:00:00 34 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\05 - Pixies - Gigantic.wav" WAVE 35 | TRACK 05 AUDIO 36 | TITLE "Gigantic" 37 | PERFORMER "Pixies" 38 | ISRC 000000000000 39 | INDEX 01 00:00:00 40 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\06 - Pixies - River Euphrates.wav" WAVE 41 | TRACK 06 AUDIO 42 | TITLE "River Euphrates" 43 | PERFORMER "Pixies" 44 | ISRC 000000000000 45 | INDEX 01 00:00:00 46 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\07 - Pixies - Where Is My Mind .wav" WAVE 47 | TRACK 07 AUDIO 48 | TITLE "Where Is My Mind?" 49 | PERFORMER "Pixies" 50 | ISRC 000000000000 51 | INDEX 01 00:00:00 52 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\08 - Pixies - Cactus.wav" WAVE 53 | TRACK 08 AUDIO 54 | TITLE "Cactus" 55 | PERFORMER "Pixies" 56 | ISRC 000000000000 57 | INDEX 01 00:00:00 58 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\09 - Pixies - Tony's Theme.wav" WAVE 59 | TRACK 09 AUDIO 60 | TITLE "Tony's Theme" 61 | PERFORMER "Pixies" 62 | ISRC 000000000000 63 | INDEX 01 00:00:00 64 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\10 - Pixies - Oh My Golly!.wav" WAVE 65 | TRACK 10 AUDIO 66 | TITLE "Oh My Golly!" 67 | PERFORMER "Pixies" 68 | ISRC 000000000000 69 | INDEX 01 00:00:00 70 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\11 - Pixies - Vamos.wav" WAVE 71 | TRACK 11 AUDIO 72 | TITLE "Vamos" 73 | PERFORMER "Pixies" 74 | ISRC 000000000000 75 | INDEX 01 00:00:00 76 | INDEX 02 00:44:70 77 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\12 - Pixies - I'm Amazed.wav" WAVE 78 | TRACK 12 AUDIO 79 | TITLE "I'm Amazed" 80 | PERFORMER "Pixies" 81 | ISRC 000000000000 82 | INDEX 01 00:00:00 83 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\13 - Pixies - Brick is Red.wav" WAVE 84 | TRACK 13 AUDIO 85 | TITLE "Brick is Red" 86 | PERFORMER "Pixies" 87 | ISRC 000000000000 88 | INDEX 01 00:00:00 89 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\14 - Pixies - Caribou.wav" WAVE 90 | TRACK 14 AUDIO 91 | TITLE "Caribou" 92 | PERFORMER "Pixies" 93 | ISRC 000000000000 94 | INDEX 01 00:00:00 95 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\15 - Pixies - Vamos.wav" WAVE 96 | TRACK 15 AUDIO 97 | TITLE "Vamos" 98 | PERFORMER "Pixies" 99 | ISRC 000000000000 100 | INDEX 01 00:00:00 101 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\16 - Pixies - Isla de Encanta.wav" WAVE 102 | TRACK 16 AUDIO 103 | TITLE "Isla de Encanta" 104 | PERFORMER "Pixies" 105 | ISRC 000000000000 106 | INDEX 01 00:00:00 107 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\17 - Pixies - Ed is Dead.wav" WAVE 108 | TRACK 17 AUDIO 109 | TITLE "Ed is Dead" 110 | PERFORMER "Pixies" 111 | ISRC 000000000000 112 | INDEX 01 00:00:00 113 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\18 - Pixies - The Holyday Song.wav" WAVE 114 | TRACK 18 AUDIO 115 | TITLE "The Holyday Song" 116 | PERFORMER "Pixies" 117 | ISRC 000000000000 118 | INDEX 01 00:00:00 119 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\19 - Pixies - Nimrod's Son.wav" WAVE 120 | TRACK 19 AUDIO 121 | TITLE "Nimrod's Son" 122 | PERFORMER "Pixies" 123 | ISRC 000000000000 124 | INDEX 01 00:00:00 125 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\20 - Pixies - I've Been Tired.wav" WAVE 126 | TRACK 20 AUDIO 127 | TITLE "I've Been Tired" 128 | PERFORMER "Pixies" 129 | ISRC 000000000000 130 | INDEX 01 00:00:00 131 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\21 - Pixies - Levitate Me.wav" WAVE 132 | TRACK 21 AUDIO 133 | TITLE "Levitate Me" 134 | PERFORMER "Pixies" 135 | ISRC 000000000000 136 | INDEX 01 00:00:00 137 | -------------------------------------------------------------------------------- /whipper/test/surferrosa.eac.noncompliant.cue: -------------------------------------------------------------------------------- 1 | REM GENRE Alternative 2 | REM DATE 1987 3 | REM DISCID 350CAA15 4 | REM COMMENT "ExactAudioCopy v0.99pb4" 5 | CATALOG 0000000000000 6 | PERFORMER "Pixies" 7 | TITLE "Surfer Rosa & Come on Pilgrim" 8 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\01 - Pixies - Bone Machine.wav" WAVE 9 | TRACK 01 AUDIO 10 | TITLE "Bone Machine" 11 | PERFORMER "Pixies" 12 | ISRC 000000000000 13 | PREGAP 00:00:32 14 | INDEX 01 00:00:00 15 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\02 - Pixies - Break My Body.wav" WAVE 16 | TRACK 02 AUDIO 17 | TITLE "Break My Body" 18 | PERFORMER "Pixies" 19 | ISRC 000000000000 20 | INDEX 01 00:00:00 21 | TRACK 03 AUDIO 22 | TITLE "Something Against You" 23 | PERFORMER "Pixies" 24 | ISRC 000000000000 25 | INDEX 00 02:05:00 26 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\03 - Pixies - Something Against You.wav" WAVE 27 | INDEX 01 00:00:00 28 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\04 - Pixies - Broken Face.wav" WAVE 29 | TRACK 04 AUDIO 30 | TITLE "Broken Face" 31 | PERFORMER "Pixies" 32 | ISRC 000000000000 33 | INDEX 01 00:00:00 34 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\05 - Pixies - Gigantic.wav" WAVE 35 | TRACK 05 AUDIO 36 | TITLE "Gigantic" 37 | PERFORMER "Pixies" 38 | ISRC 000000000000 39 | INDEX 01 00:00:00 40 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\06 - Pixies - River Euphrates.wav" WAVE 41 | TRACK 06 AUDIO 42 | TITLE "River Euphrates" 43 | PERFORMER "Pixies" 44 | ISRC 000000000000 45 | INDEX 01 00:00:00 46 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\07 - Pixies - Where Is My Mind .wav" WAVE 47 | TRACK 07 AUDIO 48 | TITLE "Where Is My Mind?" 49 | PERFORMER "Pixies" 50 | ISRC 000000000000 51 | INDEX 01 00:00:00 52 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\08 - Pixies - Cactus.wav" WAVE 53 | TRACK 08 AUDIO 54 | TITLE "Cactus" 55 | PERFORMER "Pixies" 56 | ISRC 000000000000 57 | INDEX 01 00:00:00 58 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\09 - Pixies - Tony's Theme.wav" WAVE 59 | TRACK 09 AUDIO 60 | TITLE "Tony's Theme" 61 | PERFORMER "Pixies" 62 | ISRC 000000000000 63 | INDEX 01 00:00:00 64 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\10 - Pixies - Oh My Golly!.wav" WAVE 65 | TRACK 10 AUDIO 66 | TITLE "Oh My Golly!" 67 | PERFORMER "Pixies" 68 | ISRC 000000000000 69 | INDEX 01 00:00:00 70 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\11 - Pixies - Vamos.wav" WAVE 71 | TRACK 11 AUDIO 72 | TITLE "Vamos" 73 | PERFORMER "Pixies" 74 | ISRC 000000000000 75 | INDEX 01 00:00:00 76 | INDEX 02 00:44:70 77 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\12 - Pixies - I'm Amazed.wav" WAVE 78 | TRACK 12 AUDIO 79 | TITLE "I'm Amazed" 80 | PERFORMER "Pixies" 81 | ISRC 000000000000 82 | INDEX 01 00:00:00 83 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\13 - Pixies - Brick is Red.wav" WAVE 84 | TRACK 13 AUDIO 85 | TITLE "Brick is Red" 86 | PERFORMER "Pixies" 87 | ISRC 000000000000 88 | INDEX 01 00:00:00 89 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\14 - Pixies - Caribou.wav" WAVE 90 | TRACK 14 AUDIO 91 | TITLE "Caribou" 92 | PERFORMER "Pixies" 93 | ISRC 000000000000 94 | INDEX 01 00:00:00 95 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\15 - Pixies - Vamos.wav" WAVE 96 | TRACK 15 AUDIO 97 | TITLE "Vamos" 98 | PERFORMER "Pixies" 99 | ISRC 000000000000 100 | INDEX 01 00:00:00 101 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\16 - Pixies - Isla de Encanta.wav" WAVE 102 | TRACK 16 AUDIO 103 | TITLE "Isla de Encanta" 104 | PERFORMER "Pixies" 105 | ISRC 000000000000 106 | INDEX 01 00:00:00 107 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\17 - Pixies - Ed is Dead.wav" WAVE 108 | TRACK 17 AUDIO 109 | TITLE "Ed is Dead" 110 | PERFORMER "Pixies" 111 | ISRC 000000000000 112 | INDEX 01 00:00:00 113 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\18 - Pixies - The Holyday Song.wav" WAVE 114 | TRACK 18 AUDIO 115 | TITLE "The Holyday Song" 116 | PERFORMER "Pixies" 117 | ISRC 000000000000 118 | INDEX 01 00:00:00 119 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\19 - Pixies - Nimrod's Son.wav" WAVE 120 | TRACK 19 AUDIO 121 | TITLE "Nimrod's Son" 122 | PERFORMER "Pixies" 123 | ISRC 000000000000 124 | INDEX 01 00:00:00 125 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\20 - Pixies - I've Been Tired.wav" WAVE 126 | TRACK 20 AUDIO 127 | TITLE "I've Been Tired" 128 | PERFORMER "Pixies" 129 | ISRC 000000000000 130 | INDEX 01 00:00:00 131 | FILE "Pixies - Surfer Rosa & Come on Pilgrim\21 - Pixies - Levitate Me.wav" WAVE 132 | TRACK 21 AUDIO 133 | TITLE "Levitate Me" 134 | PERFORMER "Pixies" 135 | ISRC 000000000000 136 | INDEX 01 00:00:00 137 | -------------------------------------------------------------------------------- /whipper/test/surferrosa.eac.single.cue: -------------------------------------------------------------------------------- 1 | REM GENRE Alternative 2 | REM DATE 1987 3 | REM DISCID 350CAA15 4 | REM COMMENT "ExactAudioCopy v0.99pb4" 5 | CATALOG 0000000000000 6 | PERFORMER "Pixies" 7 | TITLE "Surfer Rosa & Come on Pilgrim" 8 | FILE "Range.wav" WAVE 9 | TRACK 01 AUDIO 10 | TITLE "Bone Machine" 11 | PERFORMER "Pixies" 12 | ISRC 000000000000 13 | INDEX 00 00:00:00 14 | INDEX 01 00:00:32 15 | TRACK 02 AUDIO 16 | TITLE "Break My Body" 17 | PERFORMER "Pixies" 18 | ISRC 000000000000 19 | INDEX 01 03:03:42 20 | TRACK 03 AUDIO 21 | TITLE "Something Against You" 22 | PERFORMER "Pixies" 23 | ISRC 000000000000 24 | INDEX 00 05:08:42 25 | INDEX 01 05:09:12 26 | TRACK 04 AUDIO 27 | TITLE "Broken Face" 28 | PERFORMER "Pixies" 29 | ISRC 000000000000 30 | INDEX 01 06:56:67 31 | TRACK 05 AUDIO 32 | TITLE "Gigantic" 33 | PERFORMER "Pixies" 34 | ISRC 000000000000 35 | INDEX 01 08:27:00 36 | TRACK 06 AUDIO 37 | TITLE "River Euphrates" 38 | PERFORMER "Pixies" 39 | ISRC 000000000000 40 | INDEX 01 12:21:70 41 | TRACK 07 AUDIO 42 | TITLE "Where Is My Mind?" 43 | PERFORMER "Pixies" 44 | ISRC 000000000000 45 | INDEX 01 14:53:60 46 | TRACK 08 AUDIO 47 | TITLE "Cactus" 48 | PERFORMER "Pixies" 49 | ISRC 000000000000 50 | INDEX 01 18:47:15 51 | TRACK 09 AUDIO 52 | TITLE "Tony's Theme" 53 | PERFORMER "Pixies" 54 | ISRC 000000000000 55 | INDEX 01 21:03:70 56 | TRACK 10 AUDIO 57 | TITLE "Oh My Golly!" 58 | PERFORMER "Pixies" 59 | ISRC 000000000000 60 | INDEX 01 22:56:15 61 | TRACK 11 AUDIO 62 | TITLE "Vamos" 63 | PERFORMER "Pixies" 64 | ISRC 000000000000 65 | INDEX 01 24:43:32 66 | INDEX 02 25:28:27 67 | TRACK 12 AUDIO 68 | TITLE "I'm Amazed" 69 | PERFORMER "Pixies" 70 | ISRC 000000000000 71 | INDEX 01 29:49:20 72 | TRACK 13 AUDIO 73 | TITLE "Brick is Red" 74 | PERFORMER "Pixies" 75 | ISRC 000000000000 76 | INDEX 01 31:31:27 77 | TRACK 14 AUDIO 78 | TITLE "Caribou" 79 | PERFORMER "Pixies" 80 | ISRC 000000000000 81 | INDEX 01 33:32:20 82 | TRACK 15 AUDIO 83 | TITLE "Vamos" 84 | PERFORMER "Pixies" 85 | ISRC 000000000000 86 | INDEX 01 36:46:45 87 | TRACK 16 AUDIO 88 | TITLE "Isla de Encanta" 89 | PERFORMER "Pixies" 90 | ISRC 000000000000 91 | INDEX 01 39:40:22 92 | TRACK 17 AUDIO 93 | TITLE "Ed is Dead" 94 | PERFORMER "Pixies" 95 | ISRC 000000000000 96 | INDEX 01 41:21:47 97 | TRACK 18 AUDIO 98 | TITLE "The Holyday Song" 99 | PERFORMER "Pixies" 100 | ISRC 000000000000 101 | INDEX 01 43:51:47 102 | TRACK 19 AUDIO 103 | TITLE "Nimrod's Son" 104 | PERFORMER "Pixies" 105 | ISRC 000000000000 106 | INDEX 01 46:06:10 107 | TRACK 20 AUDIO 108 | TITLE "I've Been Tired" 109 | PERFORMER "Pixies" 110 | ISRC 000000000000 111 | INDEX 01 48:23:25 112 | TRACK 21 AUDIO 113 | TITLE "Levitate Me" 114 | PERFORMER "Pixies" 115 | ISRC 000000000000 116 | INDEX 01 51:24:07 117 | -------------------------------------------------------------------------------- /whipper/test/surferrosa.toc: -------------------------------------------------------------------------------- 1 | CD_DA 2 | 3 | CATALOG "0000000000000" 4 | 5 | // Track 1 6 | TRACK AUDIO 7 | NO COPY 8 | NO PRE_EMPHASIS 9 | TWO_CHANNEL_AUDIO 10 | ISRC "000000000000" 11 | SILENCE 00:00:32 12 | FILE "data.wav" 0 03:03:10 13 | START 00:00:32 14 | 15 | 16 | // Track 2 17 | TRACK AUDIO 18 | NO COPY 19 | NO PRE_EMPHASIS 20 | TWO_CHANNEL_AUDIO 21 | ISRC "000000000000" 22 | FILE "data.wav" 03:03:10 02:05:00 23 | 24 | 25 | // Track 3 26 | TRACK AUDIO 27 | NO COPY 28 | NO PRE_EMPHASIS 29 | TWO_CHANNEL_AUDIO 30 | ISRC "000000000000" 31 | FILE "data.wav" 05:08:10 01:48:25 32 | START 00:00:45 33 | 34 | 35 | // Track 4 36 | TRACK AUDIO 37 | NO COPY 38 | NO PRE_EMPHASIS 39 | TWO_CHANNEL_AUDIO 40 | ISRC "000000000000" 41 | FILE "data.wav" 06:56:35 01:30:08 42 | 43 | 44 | // Track 5 45 | TRACK AUDIO 46 | NO COPY 47 | NO PRE_EMPHASIS 48 | TWO_CHANNEL_AUDIO 49 | ISRC "000000000000" 50 | FILE "data.wav" 08:26:43 03:54:70 51 | 52 | 53 | // Track 6 54 | TRACK AUDIO 55 | NO COPY 56 | NO PRE_EMPHASIS 57 | TWO_CHANNEL_AUDIO 58 | ISRC "000000000000" 59 | FILE "data.wav" 12:21:38 02:31:65 60 | 61 | 62 | // Track 7 63 | TRACK AUDIO 64 | NO COPY 65 | NO PRE_EMPHASIS 66 | TWO_CHANNEL_AUDIO 67 | ISRC "000000000000" 68 | FILE "data.wav" 14:53:28 03:53:30 69 | 70 | 71 | // Track 8 72 | TRACK AUDIO 73 | NO COPY 74 | NO PRE_EMPHASIS 75 | TWO_CHANNEL_AUDIO 76 | ISRC "000000000000" 77 | FILE "data.wav" 18:46:58 02:16:55 78 | 79 | 80 | // Track 9 81 | TRACK AUDIO 82 | NO COPY 83 | NO PRE_EMPHASIS 84 | TWO_CHANNEL_AUDIO 85 | ISRC "000000000000" 86 | FILE "data.wav" 21:03:38 01:52:20 87 | 88 | 89 | // Track 10 90 | TRACK AUDIO 91 | NO COPY 92 | NO PRE_EMPHASIS 93 | TWO_CHANNEL_AUDIO 94 | ISRC "000000000000" 95 | FILE "data.wav" 22:55:58 01:47:17 96 | 97 | 98 | // Track 11 99 | TRACK AUDIO 100 | NO COPY 101 | NO PRE_EMPHASIS 102 | TWO_CHANNEL_AUDIO 103 | ISRC "000000000000" 104 | FILE "data.wav" 24:43:00 05:05:63 105 | INDEX 00:44:70 106 | 107 | 108 | // Track 12 109 | TRACK AUDIO 110 | NO COPY 111 | NO PRE_EMPHASIS 112 | TWO_CHANNEL_AUDIO 113 | ISRC "000000000000" 114 | FILE "data.wav" 29:48:63 01:42:07 115 | 116 | 117 | // Track 13 118 | TRACK AUDIO 119 | NO COPY 120 | NO PRE_EMPHASIS 121 | TWO_CHANNEL_AUDIO 122 | ISRC "000000000000" 123 | FILE "data.wav" 31:30:70 02:00:68 124 | 125 | 126 | // Track 14 127 | TRACK AUDIO 128 | NO COPY 129 | NO PRE_EMPHASIS 130 | TWO_CHANNEL_AUDIO 131 | ISRC "000000000000" 132 | FILE "data.wav" 33:31:63 03:14:25 133 | 134 | 135 | // Track 15 136 | TRACK AUDIO 137 | NO COPY 138 | NO PRE_EMPHASIS 139 | TWO_CHANNEL_AUDIO 140 | ISRC "000000000000" 141 | FILE "data.wav" 36:46:13 02:53:52 142 | 143 | 144 | // Track 16 145 | TRACK AUDIO 146 | NO COPY 147 | NO PRE_EMPHASIS 148 | TWO_CHANNEL_AUDIO 149 | ISRC "000000000000" 150 | FILE "data.wav" 39:39:65 01:41:25 151 | 152 | 153 | // Track 17 154 | TRACK AUDIO 155 | NO COPY 156 | NO PRE_EMPHASIS 157 | TWO_CHANNEL_AUDIO 158 | ISRC "000000000000" 159 | FILE "data.wav" 41:21:15 02:30:00 160 | 161 | 162 | // Track 18 163 | TRACK AUDIO 164 | NO COPY 165 | NO PRE_EMPHASIS 166 | TWO_CHANNEL_AUDIO 167 | ISRC "000000000000" 168 | FILE "data.wav" 43:51:15 02:14:38 169 | 170 | 171 | // Track 19 172 | TRACK AUDIO 173 | NO COPY 174 | NO PRE_EMPHASIS 175 | TWO_CHANNEL_AUDIO 176 | ISRC "000000000000" 177 | FILE "data.wav" 46:05:53 02:17:15 178 | 179 | 180 | // Track 20 181 | TRACK AUDIO 182 | NO COPY 183 | NO PRE_EMPHASIS 184 | TWO_CHANNEL_AUDIO 185 | ISRC "000000000000" 186 | FILE "data.wav" 48:22:68 03:00:57 187 | 188 | 189 | // Track 21 190 | TRACK AUDIO 191 | NO COPY 192 | NO PRE_EMPHASIS 193 | TWO_CHANNEL_AUDIO 194 | ISRC "000000000000" 195 | FILE "data.wav" 51:23:50 02:38:38 196 | 197 | -------------------------------------------------------------------------------- /whipper/test/test_command_mblookup.py: -------------------------------------------------------------------------------- 1 | # vi:si:et:sw=4:sts=4:ts=4:set fileencoding=utf-8 2 | """Tests for whipper.command.mblookup""" 3 | 4 | import os 5 | import pickle 6 | import unittest 7 | import json 8 | 9 | from whipper.command import mblookup 10 | from whipper.common.mbngs import _getMetadata 11 | 12 | 13 | class MBLookupTestCase(unittest.TestCase): 14 | """Test cases for whipper.command.mblookup.MBLookup""" 15 | 16 | @staticmethod 17 | def _mock_musicbrainz(discid, country=None, record=False): 18 | """Mock function for whipper.common.mbngs.musicbrainz function.""" 19 | filename = "whipper.discid.{}.pickle".format(discid) 20 | path = os.path.join(os.path.dirname(__file__), filename) 21 | with open(path, "rb") as p: 22 | return pickle.load(p) 23 | 24 | @staticmethod 25 | def _mock_getReleaseMetadata(release_id): 26 | """ 27 | Mock function for whipper.common.mbngs.getReleaseMetadata. 28 | 29 | :param release_id: MusicBrainz Release ID 30 | :type release_id: str 31 | :returns: a DiscMetadata object based on the given release_id 32 | :rtype: `DiscMetadata` 33 | """ 34 | filename = 'whipper.release.{}.json'.format(release_id) 35 | path = os.path.join(os.path.dirname(__file__), filename) 36 | with open(path, "rb") as handle: 37 | response = json.loads(handle.read().decode('utf-8')) 38 | return _getMetadata(response['release']) 39 | 40 | def testMissingReleaseType(self): 41 | """Test that lookup for release without a type set doesn't fail.""" 42 | # Using: Gustafsson, Österberg & Cowle - What's Up? 8 (disc 4) 43 | # https://musicbrainz.org/release/d8e6153a-2c47-4804-9d73-0aac1081c3b1 44 | mblookup.musicbrainz = self._mock_musicbrainz 45 | discid = "xu338_M8WukSRi0J.KTlDoflB8Y-" 46 | # https://musicbrainz.org/cdtoc/xu338_M8WukSRi0J.KTlDoflB8Y- 47 | lookup = mblookup.MBLookup([discid], 'whipper mblookup', None) 48 | lookup.do() 49 | 50 | def testGetDataFromReleaseId(self): 51 | """Test that lookup for a release with a specified id.""" 52 | # Using: The KLF - Space & Chill Out 53 | # https://musicbrainz.org/release/c56ff16e-1d81-47de-926f-ba22891bd2bd 54 | mblookup.getReleaseMetadata = self._mock_getReleaseMetadata 55 | releaseid = 'c56ff16e-1d81-47de-926f-ba22891bd2bd' 56 | lookup = mblookup.MBLookup([releaseid], 'whipper mblookup', None) 57 | lookup.do() 58 | -------------------------------------------------------------------------------- /whipper/test/test_common_common.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; test-case-name: whipper.test.test_common_common -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | import os 5 | import tempfile 6 | 7 | from whipper.common import common 8 | 9 | from whipper.test import common as tcommon 10 | 11 | 12 | class ShrinkTestCase(tcommon.TestCase): 13 | 14 | def testSufjan(self): 15 | path = ('whipper/Sufjan Stevens - Illinois/02. Sufjan Stevens - ' 16 | 'The Black Hawk War, or, How to Demolish an Entire ' 17 | 'Civilization and Still Feel Good About Yourself in the ' 18 | 'Morning, or, We Apologize for the Inconvenience but ' 19 | 'You\'re Going to Have to Leave Now, or, "I Have Fought ' 20 | 'the Big Knives and Will Continue to Fight Them Until They ' 21 | 'Are Off Our Lands!".flac') 22 | 23 | shorter = common.shrinkPath(path) 24 | self.assertTrue(os.path.splitext(path)[0].startswith( 25 | os.path.splitext(shorter)[0])) 26 | self.failIfEquals(path, shorter) 27 | 28 | 29 | class FramesTestCase(tcommon.TestCase): 30 | 31 | def testFrames(self): 32 | self.assertEqual(common.framesToHMSF(123456), '00:27:26.06') 33 | 34 | 35 | class FormatTimeTestCase(tcommon.TestCase): 36 | 37 | def testFormatTime(self): 38 | self.assertEqual(common.formatTime(7202), '02:00:02.000') 39 | 40 | 41 | class GetRelativePathTestCase(tcommon.TestCase): 42 | 43 | def testRelativeOutputDirectory(self): 44 | directory = '.Placebo - Black Market Music (2000)' 45 | cue = './' + directory + '/Placebo - Black Market Music (2000)' 46 | track = './' + directory + '/01. Placebo - Taste in Men.flac' 47 | 48 | self.assertEqual(common.getRelativePath(track, cue), 49 | '01. Placebo - Taste in Men.flac') 50 | 51 | 52 | class GetRealPathTestCase(tcommon.TestCase): 53 | 54 | def testRealWithBackslash(self): 55 | fd, path = tempfile.mkstemp(suffix='back\\slash.flac') 56 | refPath = os.path.join(os.path.dirname(path), 'fake.cue') 57 | 58 | self.assertEqual(common.getRealPath(refPath, path), path) 59 | 60 | # same path, but with wav extension, will point to flac file 61 | wavPath = path[:-4] + 'wav' 62 | self.assertEqual(common.getRealPath(refPath, wavPath), path) 63 | 64 | os.close(fd) 65 | os.unlink(path) 66 | -------------------------------------------------------------------------------- /whipper/test/test_common_config.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; test-case-name: whipper.test.test_common_config -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | import os 5 | import tempfile 6 | 7 | from whipper.common import config 8 | 9 | from whipper.test import common as tcommon 10 | 11 | 12 | class ConfigTestCase(tcommon.TestCase): 13 | 14 | def setUp(self): 15 | fd, self._path = tempfile.mkstemp(suffix='.whipper.test.config') 16 | os.close(fd) 17 | self._config = config.Config(self._path) 18 | 19 | def tearDown(self): 20 | os.unlink(self._path) 21 | 22 | def testAddReadOffset(self): 23 | self.assertRaises(KeyError, self._config.getReadOffset, 24 | 'PLEXTOR ', 'DVDR PX-L890SA', '1.05') 25 | self._config.setReadOffset('PLEXTOR ', 'DVDR PX-L890SA', '1.05', 6) 26 | 27 | # getting it from memory should work 28 | offset = self._config.getReadOffset( 29 | 'PLEXTOR ', 'DVDR PX-L890SA', '1.05') 30 | self.assertEqual(offset, 6) 31 | 32 | # and so should getting it after reading it again 33 | self._config.open() 34 | offset = self._config.getReadOffset( 35 | 'PLEXTOR ', 'DVDR PX-L890SA', '1.05') 36 | self.assertEqual(offset, 6) 37 | 38 | def testAddReadOffsetSpaced(self): 39 | self.assertRaises(KeyError, self._config.getReadOffset, 40 | 'Slimtype', 'eSAU208 2 ', 'ML03') 41 | self._config.setReadOffset('Slimtype', 'eSAU208 2 ', 'ML03', 6) 42 | 43 | # getting it from memory should work 44 | offset = self._config.getReadOffset( 45 | 'Slimtype', 'eSAU208 2 ', 'ML03') 46 | self.assertEqual(offset, 6) 47 | 48 | # and so should getting it after reading it again 49 | self._config.open() 50 | offset = self._config.getReadOffset( 51 | 'Slimtype', 'eSAU208 2 ', 'ML03') 52 | self.assertEqual(offset, 6) 53 | 54 | def testDefeatsCache(self): 55 | self.assertRaises(KeyError, self._config.getDefeatsCache, 56 | 'PLEXTOR ', 'DVDR PX-L890SA', '1.05') 57 | 58 | self._config.setDefeatsCache( 59 | 'PLEXTOR ', 'DVDR PX-L890SA', '1.05', False) 60 | defeats = self._config.getDefeatsCache( 61 | 'PLEXTOR ', 'DVDR PX-L890SA', '1.05') 62 | self.assertEqual(defeats, False) 63 | 64 | self._config.setDefeatsCache( 65 | 'PLEXTOR ', 'DVDR PX-L890SA', '1.05', True) 66 | defeats = self._config.getDefeatsCache( 67 | 'PLEXTOR ', 'DVDR PX-L890SA', '1.05') 68 | self.assertEqual(defeats, True) 69 | 70 | def test_get_musicbrainz_server(self): 71 | self.assertEqual(self._config.get_musicbrainz_server(), 72 | {'scheme': 'https', 'netloc': 'musicbrainz.org'}, 73 | msg='Default value is correct') 74 | 75 | self._config._parser.add_section('musicbrainz') 76 | 77 | self._config._parser.set('musicbrainz', 'server', 78 | 'http://192.168.2.141:5000') 79 | self._config.write() 80 | self.assertEqual(self._config.get_musicbrainz_server(), 81 | {'scheme': 'http', 'netloc': '192.168.2.141:5000'}, 82 | msg='Correctly returns user-set value') 83 | 84 | # Test for unsupported scheme 85 | self._config._parser.set('musicbrainz', 'server', 'ftp://example.com') 86 | self._config.write() 87 | self.assertRaises(KeyError, self._config.get_musicbrainz_server) 88 | 89 | # Test for absent scheme 90 | self._config._parser.set('musicbrainz', 'server', 'example.com') 91 | self._config.write() 92 | self.assertRaises(KeyError, self._config.get_musicbrainz_server) 93 | 94 | self._config._parser.remove_section('musicbrainz') 95 | -------------------------------------------------------------------------------- /whipper/test/test_common_directory.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; test-case-name: whipper.test.test_common_directory -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | from os.path import dirname, expanduser 5 | from whipper.common import directory 6 | from whipper.test import common 7 | 8 | 9 | class DirectoryTestCase(common.TestCase): 10 | HOME = expanduser('~') 11 | HOME_PARENT = dirname(HOME) 12 | 13 | def testAll(self): 14 | path = directory.config_path() 15 | self.assertTrue(path.startswith(DirectoryTestCase.HOME_PARENT)) 16 | -------------------------------------------------------------------------------- /whipper/test/test_common_drive.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; test-case-name: whipper.test.test_common_drive -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | from whipper.test import common 5 | from whipper.common import drive 6 | 7 | 8 | class ListifyTestCase(common.TestCase): 9 | 10 | def testString(self): 11 | string = '/dev/sr0' 12 | self.assertEqual(drive._listify(string), [string, ]) 13 | 14 | def testList(self): 15 | lst = ['/dev/scd0', '/dev/sr0'] 16 | self.assertEqual(drive._listify(lst), lst) 17 | -------------------------------------------------------------------------------- /whipper/test/test_common_path.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; test-case-name: whipper.test.test_common_path -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | from whipper.common import path 5 | from whipper.test import common 6 | 7 | 8 | # TODO: Right now you're testing different strings for different functions. 9 | # I think it'd make more sense to come up with a selection of strings to test 10 | # and then test that set of strings for the entire matrix to make sure that 11 | # they all behave correctly in all instances. 12 | # 13 | class FilterTestCase(common.TestCase): 14 | def setUp(self): 15 | self._filter_none = path.PathFilter(dot=False, posix=False, 16 | vfat=False, whitespace=False, 17 | printable=False) 18 | self._filter_dot = path.PathFilter(dot=True, posix=False, 19 | vfat=False, whitespace=False, 20 | printable=False) 21 | self._filter_posix = path.PathFilter(dot=False, posix=True, 22 | vfat=False, whitespace=False, 23 | printable=False) 24 | self._filter_vfat = path.PathFilter(dot=False, posix=False, 25 | vfat=True, whitespace=False, 26 | printable=False) 27 | self._filter_whitespace = path.PathFilter(dot=False, posix=False, 28 | vfat=False, whitespace=True, 29 | printable=False) 30 | self._filter_printable = path.PathFilter(dot=False, posix=False, 31 | vfat=False, whitespace=False, 32 | printable=True) 33 | self._filter_all = path.PathFilter(dot=True, posix=True, vfat=True, 34 | whitespace=True, printable=True) 35 | 36 | def testNone(self): 37 | part = '<<< $&*!\' "()`{}[]spaceship>>>' 38 | self.assertEqual(self._filter_posix.filter(part), part) 39 | 40 | def testDot(self): 41 | part = '.弐' 42 | self.assertEqual(self._filter_dot.filter(part), '_弐') 43 | 44 | def testPosix(self): 45 | part = 'A Charm/A \x00Blade' 46 | self.assertEqual(self._filter_posix.filter(part), 'A Charm_A _Blade') 47 | 48 | def testVfat(self): 49 | part = 'A Word: F**k you?' 50 | self.assertEqual(self._filter_vfat.filter(part), 'A Word_ F__k you_') 51 | 52 | def testWhitespace(self): 53 | part = 'This is just a test!' 54 | self.assertEqual(self._filter_whitespace.filter(part), 55 | 'This_is_just_a_test!') 56 | 57 | def testPrintable(self): 58 | part = 'Supper’s Ready† 😽' 59 | self.assertEqual(self._filter_printable.filter(part), 60 | 'Supper_s Ready_ _') 61 | 62 | def testAll(self): 63 | part = 'Greatest Ever! Soul: The Definitive Collection' 64 | self.assertEqual(self._filter_all.filter(part), 65 | 'Greatest_Ever!_Soul__The_Definitive_Collection') 66 | -------------------------------------------------------------------------------- /whipper/test/test_common_program.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; test-case-name: whipper.test.test_common_program -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | 5 | import os 6 | import shutil 7 | import unittest 8 | 9 | from tempfile import NamedTemporaryFile 10 | from whipper.common import program, mbngs, config 11 | from whipper.command.cd import DEFAULT_DISC_TEMPLATE 12 | 13 | 14 | class PathTestCase(unittest.TestCase): 15 | 16 | def testStandardTemplateEmpty(self): 17 | prog = program.Program(config.Config()) 18 | 19 | path = prog.getPath('/tmp', DEFAULT_DISC_TEMPLATE, 20 | 'mbdiscid', None) 21 | self.assertEqual(path, ('/tmp/unknown/Unknown Artist - mbdiscid/' 22 | 'Unknown Artist - mbdiscid')) 23 | 24 | def testStandardTemplateFilled(self): 25 | prog = program.Program(config.Config()) 26 | md = mbngs.DiscMetadata() 27 | md.artist = md.sortName = 'Jeff Buckley' 28 | md.releaseTitle = 'Grace' 29 | 30 | path = prog.getPath('/tmp', DEFAULT_DISC_TEMPLATE, 31 | 'mbdiscid', md, 0) 32 | self.assertEqual(path, ('/tmp/unknown/Jeff Buckley - Grace/' 33 | 'Jeff Buckley - Grace')) 34 | 35 | def testIssue66TemplateFilled(self): 36 | prog = program.Program(config.Config()) 37 | md = mbngs.DiscMetadata() 38 | md.artist = md.sortName = 'Jeff Buckley' 39 | md.releaseTitle = 'Grace' 40 | 41 | path = prog.getPath('/tmp', '%A/%d', 'mbdiscid', md, 0) 42 | self.assertEqual(path, 43 | '/tmp/Jeff Buckley/Grace') 44 | 45 | 46 | # TODO: Test cover art embedding too. 47 | class CoverArtTestCase(unittest.TestCase): 48 | 49 | @staticmethod 50 | def _mock_get_front_image(release_id): 51 | """ 52 | Mock `musicbrainzngs.get_front_image` function. 53 | 54 | Reads a local cover art image and returns its binary data. 55 | 56 | :param release_id: a release id (self.program.metadata.mbid) 57 | :type release_id: str 58 | :returns: the binary content of the local cover art image 59 | :rtype: bytes 60 | """ 61 | filename = '%s.jpg' % release_id 62 | path = os.path.join(os.path.dirname(__file__), filename) 63 | with open(path, 'rb') as f: 64 | return f.read() 65 | 66 | def _mock_getCoverArt(self, path, release_id): 67 | """ 68 | Mock `common.program.getCoverArt` function. 69 | 70 | :param path: where to store the fetched image 71 | :type path: str 72 | :param release_id: a release id (self.program.metadata.mbid) 73 | :type release_id: str 74 | :returns: path to the downloaded cover art 75 | :rtype: str 76 | """ 77 | cover_art_path = os.path.join(path, 'cover.jpg') 78 | 79 | data = self._mock_get_front_image(release_id) 80 | 81 | with NamedTemporaryFile(suffix='.cover.jpg', delete=False) as f: 82 | f.write(data) 83 | os.chmod(f.name, 0o644) 84 | shutil.move(f.name, cover_art_path) 85 | return cover_art_path 86 | 87 | def testCoverArtPath(self): 88 | """Test whether a fetched cover art is saved properly.""" 89 | # Using: Dummy by Portishead 90 | # https://musicbrainz.org/release/76df3287-6cda-33eb-8e9a-044b5e15ffdd 91 | path = os.path.dirname(__file__) 92 | release_id = "76df3287-6cda-33eb-8e9a-044b5e15ffdd" 93 | coverArtPath = self._mock_getCoverArt(path, release_id) 94 | self.assertTrue(os.path.isfile(coverArtPath)) 95 | -------------------------------------------------------------------------------- /whipper/test/test_image_cue.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; test-case-name: whipper.test.test_image_cue -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | import os 5 | import tempfile 6 | import unittest 7 | 8 | import whipper 9 | 10 | from whipper.image import table, cue 11 | 12 | from whipper.test import common 13 | 14 | 15 | class KingsSingleTestCase(unittest.TestCase): 16 | 17 | def setUp(self): 18 | self.cue = cue.CueFile(os.path.join(os.path.dirname(__file__), 19 | 'kings-single.cue')) 20 | self.cue.parse() 21 | self.assertEqual(len(self.cue.table.tracks), 11) 22 | 23 | def testGetTrackLength(self): 24 | t = self.cue.table.tracks[0] 25 | self.assertEqual(self.cue.getTrackLength(t), 17811) 26 | # last track has unknown length 27 | t = self.cue.table.tracks[-1] 28 | self.assertEqual(self.cue.getTrackLength(t), -1) 29 | 30 | 31 | class KingsSeparateTestCase(unittest.TestCase): 32 | 33 | def setUp(self): 34 | self.cue = cue.CueFile(os.path.join(os.path.dirname(__file__), 35 | 'kings-separate.cue')) 36 | self.cue.parse() 37 | self.assertEqual(len(self.cue.table.tracks), 11) 38 | 39 | def testGetTrackLength(self): 40 | # all tracks have unknown length 41 | t = self.cue.table.tracks[0] 42 | self.assertEqual(self.cue.getTrackLength(t), -1) 43 | t = self.cue.table.tracks[-1] 44 | self.assertEqual(self.cue.getTrackLength(t), -1) 45 | 46 | 47 | class KanyeMixedTestCase(unittest.TestCase): 48 | 49 | def setUp(self): 50 | self.cue = cue.CueFile(os.path.join(os.path.dirname(__file__), 51 | 'kanye.cue')) 52 | self.cue.parse() 53 | self.assertEqual(len(self.cue.table.tracks), 13) 54 | 55 | def testGetTrackLength(self): 56 | t = self.cue.table.tracks[0] 57 | self.assertEqual(self.cue.getTrackLength(t), -1) 58 | 59 | 60 | class WriteCueFileTestCase(unittest.TestCase): 61 | 62 | @staticmethod 63 | def testWrite(): 64 | fd, path = tempfile.mkstemp(suffix='.whipper.test.cue') 65 | os.close(fd) 66 | 67 | it = table.Table() 68 | 69 | t = table.Track(1) 70 | t.index(1, absolute=0, path='track01.wav', relative=0, counter=1) 71 | it.tracks.append(t) 72 | 73 | t = table.Track(2) 74 | t.index(0, absolute=1000, path='track01.wav', 75 | relative=1000, counter=1) 76 | t.index(1, absolute=2000, path='track02.wav', relative=0, counter=2) 77 | it.tracks.append(t) 78 | it.absolutize() 79 | it.leadout = 3000 80 | 81 | common.diffStrings("""REM DISCID 0C002802 82 | REM COMMENT "whipper %s" 83 | FILE "track01.wav" WAVE 84 | TRACK 01 AUDIO 85 | INDEX 01 00:00:00 86 | TRACK 02 AUDIO 87 | INDEX 00 00:13:25 88 | FILE "track02.wav" WAVE 89 | INDEX 01 00:00:00 90 | """ % whipper.__version__, it.cue()) 91 | os.unlink(path) 92 | -------------------------------------------------------------------------------- /whipper/test/test_program_cdparanoia.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; test-case-name: whipper.test.test_program_cdparanoia -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | import os 5 | 6 | from whipper.extern.task import task 7 | 8 | from whipper.program import cdparanoia 9 | 10 | from whipper.test import common 11 | 12 | 13 | class ParseTestCase(common.TestCase): 14 | 15 | def setUp(self): 16 | # report from Afghan Whigs - Sweet Son Of A Bitch 17 | path = os.path.join(os.path.dirname(__file__), 18 | 'cdparanoia.progress') 19 | self._parser = cdparanoia.ProgressParser(start=45990, stop=47719) 20 | 21 | self._handle = open(path) 22 | 23 | def testParse(self): 24 | for line in self._handle.readlines(): 25 | self._parser.parse(line) 26 | 27 | q = '%.01f %%' % (self._parser.getTrackQuality() * 100.0, ) 28 | self.assertEqual(q, '99.6 %') 29 | 30 | 31 | class Parse1FrameTestCase(common.TestCase): 32 | 33 | def setUp(self): 34 | path = os.path.join(os.path.dirname(__file__), 35 | 'cdparanoia.progress.strokes') 36 | self._parser = cdparanoia.ProgressParser(start=0, stop=0) 37 | 38 | self._handle = open(path) 39 | 40 | def testParse(self): 41 | for line in self._handle.readlines(): 42 | self._parser.parse(line) 43 | 44 | q = '%.01f %%' % (self._parser.getTrackQuality() * 100.0, ) 45 | self.assertEqual(q, '100.0 %') 46 | 47 | 48 | class ErrorTestCase(common.TestCase): 49 | 50 | def setUp(self): 51 | # report from a rip with offset -1164 causing scsi errors 52 | path = os.path.join(os.path.dirname(__file__), 53 | 'cdparanoia.progress.error') 54 | self._parser = cdparanoia.ProgressParser(start=0, stop=10800) 55 | 56 | self._handle = open(path) 57 | 58 | def testParse(self): 59 | for line in self._handle.readlines(): 60 | self._parser.parse(line) 61 | 62 | q = '%.01f %%' % (self._parser.getTrackQuality() * 100.0, ) 63 | self.assertEqual(q, '79.6 %') 64 | 65 | 66 | class VersionTestCase(common.TestCase): 67 | 68 | def testGetVersion(self): 69 | v = cdparanoia.getCdParanoiaVersion() 70 | self.assertTrue(v) 71 | 72 | 73 | class AnalyzeFileTask(cdparanoia.AnalyzeTask): 74 | 75 | def __init__(self, path): 76 | self.command = ['cat', path] 77 | 78 | def readbytesout(self, bytes_stdout): 79 | self.readbyteserr(bytes_stdout) 80 | 81 | 82 | class CacheTestCase(common.TestCase): 83 | 84 | def testDefeatsCache(self): 85 | self.runner = task.SyncRunner(verbose=False) 86 | 87 | path = os.path.join(os.path.dirname(__file__), 88 | 'cdparanoia', 'PX-L890SA.cdparanoia-A.stderr') 89 | t = AnalyzeFileTask(path) 90 | self.runner.run(t) 91 | self.assertTrue(t.defeatsCache) 92 | -------------------------------------------------------------------------------- /whipper/test/test_program_cdrdao.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; test-case-name: whipper.test.test_program_cdparanoia -*- 2 | # vi:si:et:sw=4:sts=4:ts=4 3 | 4 | from whipper.program import cdrdao 5 | from whipper.test import common 6 | 7 | # TODO: Current test architecture makes testing cdrdao difficult. Revisit. 8 | 9 | 10 | class VersionTestCase(common.TestCase): 11 | def testGetVersion(self): 12 | v = cdrdao.version() 13 | self.assertTrue(v) 14 | # make sure it starts with a digit 15 | self.assertTrue(int(v[0])) 16 | -------------------------------------------------------------------------------- /whipper/test/test_program_sox.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; test-case-name: whipper.test.test_program_sox -*- 2 | 3 | import os 4 | 5 | from whipper.program import sox 6 | from whipper.test import common 7 | 8 | 9 | class PeakLevelTestCase(common.TestCase): 10 | def setUp(self): 11 | self.path = os.path.join(os.path.dirname(__file__), 'track.flac') 12 | 13 | def testParse(self): 14 | self.assertEqual(26215, sox.peak_level(self.path)) 15 | -------------------------------------------------------------------------------- /whipper/test/test_program_soxi.py: -------------------------------------------------------------------------------- 1 | # -*- Mode: Python; test-case-name: whipper.test.test_program_sox -*- 2 | 3 | import os 4 | import tempfile 5 | 6 | from whipper.common import common 7 | from whipper.extern.task import task 8 | from whipper.program.soxi import AudioLengthTask 9 | from whipper.test import common as tcommon 10 | 11 | base_track_file = os.path.join(os.path.dirname(__file__), 'track.flac') 12 | base_track_length = 10 * common.SAMPLES_PER_FRAME 13 | 14 | 15 | class AudioLengthTestCase(tcommon.TestCase): 16 | 17 | def testLength(self): 18 | path = base_track_file 19 | t = AudioLengthTask(path) 20 | runner = task.SyncRunner() 21 | runner.run(t, verbose=False) 22 | self.assertEqual(t.length, base_track_length) 23 | 24 | 25 | class AudioLengthPathTestCase(tcommon.TestCase): 26 | 27 | def _testSuffix(self, suffix): 28 | fd, path = tempfile.mkstemp(suffix=suffix) 29 | with os.fdopen(fd, "wb") as temptrack: 30 | with open(base_track_file, "rb") as f: 31 | temptrack.write(f.read()) 32 | 33 | t = AudioLengthTask(path) 34 | runner = task.SyncRunner() 35 | runner.run(t, verbose=False) 36 | self.assertEqual(t.length, base_track_length) 37 | os.unlink(path) 38 | 39 | 40 | class NormalAudioLengthPathTestCase(AudioLengthPathTestCase): 41 | 42 | def testSingleQuote(self): 43 | self._testSuffix("whipper.test.Guns 'N Roses.flac") 44 | 45 | def testDoubleQuote(self): 46 | # This test makes sure we can checksum files with double quote in 47 | # their name 48 | self._testSuffix('whipper.test.12" edit.flac') 49 | 50 | 51 | class AbsentFileAudioLengthPathTestCase(AudioLengthPathTestCase): 52 | def testAbsentFile(self): 53 | tempdir = tempfile.mkdtemp() 54 | path = os.path.join(tempdir, "nonexistent.flac") 55 | 56 | t = AudioLengthTask(path) 57 | runner = task.SyncRunner() 58 | self.assertRaises(task.TaskException, runner.run, 59 | t, verbose=False) 60 | 61 | os.rmdir(tempdir) 62 | -------------------------------------------------------------------------------- /whipper/test/test_result_logger.log: -------------------------------------------------------------------------------- 1 | Log created by: whipper 0.7.4.dev87+gb71ec9f.d20191026 (internal logger) 2 | Log creation date: 2019-10-26T14:25:02Z 3 | 4 | Ripping phase information: 5 | Drive: HL-DT-STBD-RE WH14NS40 (revision 1.03) 6 | Extraction engine: cdparanoia cdparanoia III 10.2 libcdio 2.0.0 x86_64-pc-linux-gnu 7 | Defeat audio cache: true 8 | Read offset correction: 6 9 | Overread into lead-out: false 10 | Gap detection: cdrdao 1.2.4 11 | CD-R detected: false 12 | 13 | CD metadata: 14 | Release: 15 | Artist: Example - Symbol - Artist 16 | Title: 'Album With: - Dashes' 17 | CDDB Disc ID: c30bde0d 18 | MusicBrainz Disc ID: eyjySLXGdKigAjY3_C0nbBmNUHc- 19 | MusicBrainz lookup URL: https://musicbrainz.org/cdtoc/attach?toc=1+13+228039+150+16414+33638+51378+69369+88891+104871+121645+138672+160748+178096+194680+212628&tracks=13&id=eyjySLXGdKigAjY3_C0nbBmNUHc- 20 | 21 | TOC: 22 | 1: 23 | Start: 00:00:00 24 | Length: 03:36:64 25 | Start sector: 0 26 | End sector: 16263 27 | 28 | 2: 29 | Start: 03:36:64 30 | Length: 03:49:49 31 | Start sector: 16264 32 | End sector: 33487 33 | 34 | Tracks: 35 | 1: 36 | Filename: ./soundtrack/Various Artists - Shark Tale - Motion Picture Soundtrack/01. Sean Paul & Ziggy Marley - Three Little Birds.flac 37 | Peak level: 0.90036 38 | Pre-emphasis: 39 | Extraction speed: 7.0 X 40 | Extraction quality: 100.00 % 41 | Test CRC: 0025D726 42 | Copy CRC: 0025D726 43 | AccurateRip v1: 44 | Result: Found, exact match 45 | Confidence: 14 46 | Local CRC: 95E6A189 47 | Remote CRC: 95E6A189 48 | AccurateRip v2: 49 | Result: Found, exact match 50 | Confidence: 11 51 | Local CRC: 113FA733 52 | Remote CRC: 113FA733 53 | Status: Copy OK 54 | 55 | 2: 56 | Filename: ./soundtrack/Various Artists - Shark Tale - Motion Picture Soundtrack/02. Christina Aguilera feat. Missy Elliott - Car Wash (Shark Tale mix).flac 57 | Peak level: 0.972351 58 | Pre-emphasis: 59 | Extraction speed: 7.7 X 60 | Extraction quality: 100.00 % 61 | Test CRC: F77C14CB 62 | Copy CRC: F77C14CB 63 | AccurateRip v1: 64 | Result: Found, exact match 65 | Confidence: 14 66 | Local CRC: 0B3316DB 67 | Remote CRC: 0B3316DB 68 | AccurateRip v2: 69 | Result: Found, exact match 70 | Confidence: 10 71 | Local CRC: A0AE0E57 72 | Remote CRC: A0AE0E57 73 | Status: Copy OK 74 | 75 | Conclusive status report: 76 | AccurateRip summary: All tracks accurately ripped 77 | Health status: No errors occurred 78 | EOF: End of status report 79 | 80 | SHA-256 hash: 2B176D8C722989B25459160E335E5CC0C1A6813C9DA69F869B625FBF737C475E 81 | -------------------------------------------------------------------------------- /whipper/test/toc_cdtext_string_parsing.py: -------------------------------------------------------------------------------- 1 | from whipper.image.toc import _CDTEXT_CANDIDATE_RE, parse_toc_string 2 | 3 | from whipper.test import common 4 | 5 | class TestTOCStringParsing(common.TestCase): 6 | def check_string(self, str_with_quotes: str, str_parsed_expected: str): 7 | text = f"PERFORMER {str_with_quotes}" 8 | match = _CDTEXT_CANDIDATE_RE.match(text) 9 | if not match: 10 | self.fail(f"String wasn't matched: {text}") 11 | self.assertEquals(match.start(), 0) 12 | self.assertEquals(match.end(), len(text)) 13 | 14 | str_parsed_actual = parse_toc_string(match.group("value")) 15 | self.assertEquals(str_parsed_actual, str_parsed_expected) 16 | 17 | def test_simple(self): 18 | self.check_string('"foo bar"', 'foo bar') 19 | 20 | def test_escaped_quotes(self): 21 | self.check_string(r'"the \"foos\""', r'the "foos"') 22 | 23 | def test_escaped_backslash(self): 24 | self.check_string(r'"foo\\bar"', r'foo\bar') 25 | 26 | def test_escaped_latin1(self): 27 | self.check_string(r'"M\330L"', r'MØL') 28 | 29 | def test_incomplete_escape(self): 30 | self.check_string(r'"M\33a"', r'M\33a') 31 | 32 | def test_trailing_backslash(self): 33 | self.check_string(r'"foo\\"', 'foo\\') 34 | 35 | def test_unicode(self): 36 | self.check_string(r'"MØL"', 'MØL') -------------------------------------------------------------------------------- /whipper/test/totbl.fast.toc: -------------------------------------------------------------------------------- 1 | CD_DA 2 | 3 | 4 | // Track 1 5 | TRACK AUDIO 6 | NO COPY 7 | NO PRE_EMPHASIS 8 | TWO_CHANNEL_AUDIO 9 | FILE "data.wav" 0 03:56:50 10 | 11 | 12 | // Track 2 13 | TRACK AUDIO 14 | NO COPY 15 | NO PRE_EMPHASIS 16 | TWO_CHANNEL_AUDIO 17 | FILE "data.wav" 03:56:50 04:11:41 18 | 19 | 20 | // Track 3 21 | TRACK AUDIO 22 | NO COPY 23 | NO PRE_EMPHASIS 24 | TWO_CHANNEL_AUDIO 25 | FILE "data.wav" 08:08:16 04:19:28 26 | 27 | 28 | // Track 4 29 | TRACK AUDIO 30 | NO COPY 31 | NO PRE_EMPHASIS 32 | TWO_CHANNEL_AUDIO 33 | FILE "data.wav" 12:27:44 05:00:04 34 | 35 | 36 | // Track 5 37 | TRACK AUDIO 38 | NO COPY 39 | NO PRE_EMPHASIS 40 | TWO_CHANNEL_AUDIO 41 | FILE "data.wav" 17:27:48 04:28:34 42 | 43 | 44 | // Track 6 45 | TRACK AUDIO 46 | NO COPY 47 | NO PRE_EMPHASIS 48 | TWO_CHANNEL_AUDIO 49 | FILE "data.wav" 21:56:07 03:05:47 50 | 51 | 52 | // Track 7 53 | TRACK AUDIO 54 | NO COPY 55 | NO PRE_EMPHASIS 56 | TWO_CHANNEL_AUDIO 57 | FILE "data.wav" 25:01:54 03:47:06 58 | 59 | 60 | // Track 8 61 | TRACK AUDIO 62 | NO COPY 63 | NO PRE_EMPHASIS 64 | TWO_CHANNEL_AUDIO 65 | FILE "data.wav" 28:48:60 06:28:05 66 | 67 | 68 | // Track 9 69 | TRACK AUDIO 70 | NO COPY 71 | NO PRE_EMPHASIS 72 | TWO_CHANNEL_AUDIO 73 | FILE "data.wav" 35:16:65 03:35:39 74 | 75 | 76 | // Track 10 77 | TRACK AUDIO 78 | NO COPY 79 | NO PRE_EMPHASIS 80 | TWO_CHANNEL_AUDIO 81 | FILE "data.wav" 38:52:29 06:07:27 82 | 83 | 84 | // Track 11 85 | TRACK AUDIO 86 | NO COPY 87 | NO PRE_EMPHASIS 88 | TWO_CHANNEL_AUDIO 89 | FILE "data.wav" 44:59:56 04:00:00 90 | 91 | -------------------------------------------------------------------------------- /whipper/test/track-separate.cue: -------------------------------------------------------------------------------- 1 | FILE "track.flac" WAVE 2 | TRACK 01 AUDIO 3 | INDEX 01 00:00:00 4 | FILE "track.flac" WAVE 5 | TRACK 02 AUDIO 6 | INDEX 01 00:00:00 7 | FILE "track.flac" WAVE 8 | TRACK 03 AUDIO 9 | INDEX 01 00:00:00 10 | FILE "track.flac" WAVE 11 | TRACK 04 AUDIO 12 | INDEX 01 00:00:00 13 | -------------------------------------------------------------------------------- /whipper/test/track-single.cue: -------------------------------------------------------------------------------- 1 | FILE "track.flac" WAVE 2 | TRACK 01 AUDIO 3 | INDEX 01 00:00:00 4 | TRACK 02 AUDIO 5 | INDEX 01 00:00:02 6 | TRACK 03 AUDIO 7 | INDEX 01 00:00:04 8 | TRACK 04 AUDIO 9 | INDEX 01 00:00:06 10 | -------------------------------------------------------------------------------- /whipper/test/track.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whipper-team/whipper/bc8b96d95647718d547aa5f8dc512241a7505cac/whipper/test/track.flac -------------------------------------------------------------------------------- /whipper/test/whipper.discid.xu338_M8WukSRi0J.KTlDoflB8Y-.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whipper-team/whipper/bc8b96d95647718d547aa5f8dc512241a7505cac/whipper/test/whipper.discid.xu338_M8WukSRi0J.KTlDoflB8Y-.pickle --------------------------------------------------------------------------------