From 56c6773c6b5f29fefe21bd934b2d06109888838a Mon Sep 17 00:00:00 2001 From: Labrys of Knossos Date: Mon, 28 Nov 2022 18:02:40 -0500 Subject: [PATCH] Update vendored beets to 1.6.0 Updates colorama to 0.4.6 Adds confuse version 1.7.0 Updates jellyfish to 0.9.0 Adds mediafile 0.10.1 Updates munkres to 1.1.4 Updates musicbrainzngs to 0.7.1 Updates mutagen to 1.46.0 Updates pyyaml to 6.0 Updates unidecode to 1.3.6 --- libs/common/_yaml/__init__.py | 33 + libs/common/beets/__init__.py | 26 +- libs/common/beets/__main__.py | 2 - libs/common/beets/art.py | 72 +- libs/common/beets/autotag/__init__.py | 111 +- libs/common/beets/autotag/hooks.py | 209 +- libs/common/beets/autotag/match.py | 55 +- libs/common/beets/autotag/mb.py | 186 +- libs/common/beets/config_default.yaml | 15 + libs/common/beets/dbcore/__init__.py | 2 - libs/common/beets/dbcore/db.py | 372 ++- libs/common/beets/dbcore/query.py | 108 +- libs/common/beets/dbcore/queryparse.py | 70 +- libs/common/beets/dbcore/types.py | 70 +- libs/common/beets/importer.py | 204 +- libs/common/beets/library.py | 574 ++-- libs/common/beets/logging.py | 16 +- libs/common/beets/mediafile.py | 2098 +------------- libs/common/beets/plugins.py | 271 +- libs/common/beets/random.py | 113 + libs/common/beets/ui/__init__.py | 351 +-- libs/common/beets/ui/commands.py | 727 ++--- libs/common/beets/util/__init__.py | 292 +- libs/common/beets/util/artresizer.py | 361 ++- libs/common/beets/util/bluelet.py | 24 +- libs/common/beets/util/confit.py | 1507 +---------- libs/common/beets/util/enumeration.py | 2 - libs/common/beets/util/functemplate.py | 169 +- libs/common/beets/util/hidden.py | 2 - libs/common/beets/util/pipeline.py | 50 +- libs/common/beets/vfs.py | 2 - libs/common/beetsplug/__init__.py | 2 - libs/common/beetsplug/absubmit.py | 77 +- libs/common/beetsplug/acousticbrainz.py | 87 +- libs/common/beetsplug/albumtypes.py | 65 + libs/common/beetsplug/aura.py | 984 +++++++ libs/common/beetsplug/badfiles.py | 181 +- libs/common/beetsplug/bareasc.py | 82 + libs/common/beetsplug/beatport.py | 171 +- libs/common/beetsplug/bench.py | 2 - libs/common/beetsplug/bpd/__init__.py | 833 ++++-- libs/common/beetsplug/bpd/gstplayer.py | 73 +- libs/common/beetsplug/bpm.py | 23 +- libs/common/beetsplug/bpsync.py | 186 ++ libs/common/beetsplug/bucket.py | 27 +- libs/common/beetsplug/chroma.py | 105 +- libs/common/beetsplug/convert.py | 254 +- libs/common/beetsplug/cue.py | 57 - libs/common/beetsplug/deezer.py | 230 ++ libs/common/beetsplug/discogs.py | 254 +- libs/common/beetsplug/duplicates.py | 100 +- libs/common/beetsplug/edit.py | 65 +- libs/common/beetsplug/embedart.py | 60 +- libs/common/beetsplug/embyupdate.py | 29 +- libs/common/beetsplug/export.py | 194 +- libs/common/beetsplug/fetchart.py | 664 +++-- libs/common/beetsplug/filefilter.py | 8 +- libs/common/beetsplug/fish.py | 285 ++ libs/common/beetsplug/freedesktop.py | 12 +- libs/common/beetsplug/fromfilename.py | 7 +- libs/common/beetsplug/ftintitle.py | 20 +- libs/common/beetsplug/fuzzy.py | 4 +- libs/common/beetsplug/gmusic.py | 81 +- libs/common/beetsplug/hook.py | 57 +- libs/common/beetsplug/ihate.py | 18 +- libs/common/beetsplug/importadded.py | 39 +- libs/common/beetsplug/importfeeds.py | 10 +- libs/common/beetsplug/info.py | 117 +- libs/common/beetsplug/inline.py | 29 +- libs/common/beetsplug/ipfs.py | 49 +- libs/common/beetsplug/keyfinder.py | 42 +- libs/common/beetsplug/kodiupdate.py | 25 +- libs/common/beetsplug/lastgenre/__init__.py | 120 +- .../beetsplug/lastgenre/genres-tree.yaml | 25 +- libs/common/beetsplug/lastgenre/genres.txt | 8 + libs/common/beetsplug/lastimport.py | 54 +- libs/common/beetsplug/loadext.py | 44 + libs/common/beetsplug/lyrics.py | 566 ++-- libs/common/beetsplug/mbcollection.py | 28 +- libs/common/beetsplug/mbsubmit.py | 11 +- libs/common/beetsplug/mbsync.py | 65 +- libs/common/beetsplug/metasync/__init__.py | 19 +- libs/common/beetsplug/metasync/amarok.py | 8 +- libs/common/beetsplug/metasync/itunes.py | 38 +- libs/common/beetsplug/missing.py | 30 +- libs/common/beetsplug/mpdstats.py | 127 +- libs/common/beetsplug/mpdupdate.py | 27 +- libs/common/beetsplug/parentwork.py | 211 ++ libs/common/beetsplug/permissions.py | 64 +- libs/common/beetsplug/play.py | 37 +- libs/common/beetsplug/playlist.py | 185 ++ libs/common/beetsplug/plexupdate.py | 64 +- libs/common/beetsplug/random.py | 108 +- libs/common/beetsplug/replaygain.py | 1040 ++++--- libs/common/beetsplug/rewrite.py | 10 +- libs/common/beetsplug/scrub.py | 30 +- libs/common/beetsplug/smartplaylist.py | 67 +- libs/common/beetsplug/sonosupdate.py | 10 +- libs/common/beetsplug/spotify.py | 540 +++- libs/common/beetsplug/subsonicplaylist.py | 171 ++ libs/common/beetsplug/subsonicupdate.py | 144 + libs/common/beetsplug/the.py | 26 +- libs/common/beetsplug/thumbnails.py | 69 +- libs/common/beetsplug/types.py | 6 +- libs/common/beetsplug/unimported.py | 68 + libs/common/beetsplug/web/__init__.py | 167 +- libs/common/beetsplug/web/static/beets.js | 2 +- libs/common/beetsplug/zero.py | 29 +- libs/common/bin/beet.exe | Bin 93012 -> 108369 bytes libs/common/bin/chardetect.exe | Bin 93026 -> 0 bytes libs/common/bin/easy_install-3.7.exe | Bin 93035 -> 0 bytes libs/common/bin/easy_install.exe | Bin 93035 -> 0 bytes libs/common/bin/guessit.exe | Bin 93020 -> 0 bytes libs/common/bin/mid3cp | 16 - libs/common/bin/mid3cp.exe | Bin 0 -> 108396 bytes libs/common/bin/mid3iconv | 16 - libs/common/bin/mid3iconv.exe | Bin 0 -> 108399 bytes libs/common/bin/mid3v2 | 16 - libs/common/bin/mid3v2.exe | Bin 0 -> 108396 bytes libs/common/bin/moggsplit | 16 - libs/common/bin/moggsplit.exe | Bin 0 -> 108399 bytes libs/common/bin/mutagen-inspect | 16 - libs/common/bin/mutagen-inspect.exe | Bin 0 -> 108405 bytes libs/common/bin/mutagen-pony | 16 - libs/common/bin/mutagen-pony.exe | Bin 0 -> 108402 bytes libs/common/bin/pbr.exe | Bin 93016 -> 0 bytes libs/common/bin/srt.exe | Bin 93018 -> 0 bytes libs/common/bin/subliminal.exe | Bin 93030 -> 0 bytes libs/common/bin/unidecode.exe | Bin 93018 -> 108375 bytes libs/common/colorama/__init__.py | 5 +- libs/common/colorama/ansi.py | 2 +- libs/common/colorama/ansitowin32.py | 46 +- libs/common/colorama/initialise.py | 51 +- libs/common/colorama/tests/__init__.py | 1 + libs/common/colorama/tests/ansi_test.py | 76 + .../common/colorama/tests/ansitowin32_test.py | 294 ++ libs/common/colorama/tests/initialise_test.py | 189 ++ libs/common/colorama/tests/isatty_test.py | 57 + libs/common/colorama/tests/utils.py | 49 + libs/common/colorama/tests/winterm_test.py | 131 + libs/common/colorama/win32.py | 28 + libs/common/colorama/winterm.py | 28 +- libs/common/confuse/__init__.py | 13 + libs/common/confuse/core.py | 724 +++++ libs/common/confuse/exceptions.py | 56 + libs/common/confuse/sources.py | 184 ++ libs/common/confuse/templates.py | 741 +++++ libs/common/confuse/util.py | 186 ++ libs/common/confuse/yaml_util.py | 228 ++ libs/common/jellyfish/__init__.py | 26 +- libs/common/jellyfish/__init__.pyi | 11 + libs/common/jellyfish/_jellyfish.py | 437 +-- .../jellyfish/cjellyfish.cp37-win_amd64.pyd | Bin 0 -> 31232 bytes libs/common/jellyfish/compat.py | 11 - libs/common/jellyfish/porter.py | 183 +- libs/common/jellyfish/py.typed | 0 libs/common/jellyfish/test.py | 176 +- libs/common/mediafile.py | 2398 +++++++++++++++++ libs/common/munkres.py | 469 +--- libs/common/musicbrainzngs/caa.py | 30 +- libs/common/musicbrainzngs/compat.py | 7 +- libs/common/musicbrainzngs/mbxml.py | 42 +- libs/common/musicbrainzngs/musicbrainz.py | 145 +- libs/common/mutagen/__init__.py | 3 +- libs/common/mutagen/_compat.py | 92 - libs/common/mutagen/_constants.py | 1 - libs/common/mutagen/_file.py | 20 +- libs/common/mutagen/_iff.py | 386 +++ libs/common/mutagen/_riff.py | 69 + libs/common/mutagen/_senf/__init__.py | 91 - libs/common/mutagen/_senf/_argv.py | 117 - libs/common/mutagen/_senf/_compat.py | 58 - libs/common/mutagen/_senf/_environ.py | 267 -- libs/common/mutagen/_senf/_fsnative.py | 666 ----- libs/common/mutagen/_senf/_print.py | 424 --- libs/common/mutagen/_senf/_stdlib.py | 154 -- libs/common/mutagen/_senf/_temp.py | 96 - libs/common/mutagen/_senf/_winansi.py | 319 --- libs/common/mutagen/_senf/_winapi.py | 222 -- libs/common/mutagen/_tags.py | 5 +- libs/common/mutagen/_tools/__init__.py | 1 - libs/common/mutagen/_tools/_util.py | 12 +- libs/common/mutagen/_tools/mid3cp.py | 30 +- libs/common/mutagen/_tools/mid3iconv.py | 17 +- libs/common/mutagen/_tools/mid3v2.py | 100 +- libs/common/mutagen/_tools/moggsplit.py | 5 +- libs/common/mutagen/_tools/mutagen_inspect.py | 18 +- libs/common/mutagen/_tools/mutagen_pony.py | 11 +- libs/common/mutagen/_util.py | 173 +- libs/common/mutagen/_vorbis.py | 46 +- libs/common/mutagen/aac.py | 17 +- libs/common/mutagen/ac3.py | 329 +++ libs/common/mutagen/aiff.py | 279 +- libs/common/mutagen/apev2.py | 89 +- libs/common/mutagen/asf/__init__.py | 21 +- libs/common/mutagen/asf/_attrs.py | 43 +- libs/common/mutagen/asf/_objects.py | 35 +- libs/common/mutagen/asf/_util.py | 1 - libs/common/mutagen/dsdiff.py | 266 ++ libs/common/mutagen/dsf.py | 13 +- libs/common/mutagen/easyid3.py | 34 +- libs/common/mutagen/easymp4.py | 32 +- libs/common/mutagen/flac.py | 77 +- libs/common/mutagen/id3/__init__.py | 3 +- libs/common/mutagen/id3/_file.py | 42 +- libs/common/mutagen/id3/_frames.py | 61 +- libs/common/mutagen/id3/_id3v1.py | 85 +- libs/common/mutagen/id3/_specs.py | 62 +- libs/common/mutagen/id3/_tags.py | 87 +- libs/common/mutagen/id3/_util.py | 17 +- libs/common/mutagen/m4a.py | 1 - libs/common/mutagen/monkeysaudio.py | 4 +- libs/common/mutagen/mp3/__init__.py | 36 +- libs/common/mutagen/mp3/_util.py | 14 +- libs/common/mutagen/mp4/__init__.py | 208 +- libs/common/mutagen/mp4/_as_entry.py | 29 +- libs/common/mutagen/mp4/_atom.py | 10 +- libs/common/mutagen/mp4/_util.py | 1 - libs/common/mutagen/musepack.py | 18 +- libs/common/mutagen/ogg.py | 50 +- libs/common/mutagen/oggflac.py | 8 +- libs/common/mutagen/oggopus.py | 3 +- libs/common/mutagen/oggspeex.py | 1 - libs/common/mutagen/oggtheora.py | 17 +- libs/common/mutagen/oggvorbis.py | 12 +- libs/common/mutagen/optimfrog.py | 31 +- libs/common/mutagen/py.typed | 0 libs/common/mutagen/smf.py | 6 +- libs/common/mutagen/tak.py | 237 ++ libs/common/mutagen/trueaudio.py | 6 +- libs/common/mutagen/wave.py | 209 ++ libs/common/mutagen/wavpack.py | 21 +- libs/common/share/man/man1/mid3cp.1 | 66 + libs/common/share/man/man1/mid3iconv.1 | 68 + libs/common/share/man/man1/mid3v2.1 | 151 ++ libs/common/share/man/man1/moggsplit.1 | 64 + libs/common/share/man/man1/mutagen-inspect.1 | 47 + libs/common/share/man/man1/mutagen-pony.1 | 46 + libs/common/unidecode/__init__.py | 145 +- libs/common/unidecode/__init__.pyi | 11 + libs/common/unidecode/__main__.py | 3 + libs/common/unidecode/py.typed | 0 libs/common/unidecode/util.py | 51 +- libs/common/unidecode/x000.py | 6 +- libs/common/unidecode/x002.py | 36 +- libs/common/unidecode/x003.py | 100 +- libs/common/unidecode/x004.py | 34 +- libs/common/unidecode/x005.py | 184 +- libs/common/unidecode/x006.py | 98 +- libs/common/unidecode/x007.py | 270 +- libs/common/unidecode/x009.py | 124 +- libs/common/unidecode/x00a.py | 204 +- libs/common/unidecode/x00b.py | 226 +- libs/common/unidecode/x00c.py | 190 +- libs/common/unidecode/x00d.py | 192 +- libs/common/unidecode/x00e.py | 206 +- libs/common/unidecode/x00f.py | 126 +- libs/common/unidecode/x010.py | 198 +- libs/common/unidecode/x011.py | 30 +- libs/common/unidecode/x012.py | 50 +- libs/common/unidecode/x013.py | 114 +- libs/common/unidecode/x014.py | 4 +- libs/common/unidecode/x016.py | 52 +- libs/common/unidecode/x017.py | 304 +-- libs/common/unidecode/x018.py | 202 +- libs/common/unidecode/x01e.py | 16 +- libs/common/unidecode/x01f.py | 44 +- libs/common/unidecode/x020.py | 144 +- libs/common/unidecode/x021.py | 68 +- libs/common/unidecode/x022.py | 488 ++-- libs/common/unidecode/x023.py | 504 ++-- libs/common/unidecode/x024.py | 92 +- libs/common/unidecode/x025.py | 34 +- libs/common/unidecode/x026.py | 292 +- libs/common/unidecode/x027.py | 136 +- libs/common/unidecode/x029.py | 506 ++-- libs/common/unidecode/x02a.py | 504 ++-- libs/common/unidecode/x02c.py | 480 ++-- libs/common/unidecode/x02e.py | 510 ++-- libs/common/unidecode/x02f.py | 510 ++-- libs/common/unidecode/x030.py | 20 +- libs/common/unidecode/x031.py | 162 +- libs/common/unidecode/x032.py | 38 +- libs/common/unidecode/x04d.py | 510 ++-- libs/common/unidecode/x04e.py | 16 +- libs/common/unidecode/x04f.py | 10 +- libs/common/unidecode/x050.py | 6 +- libs/common/unidecode/x051.py | 4 +- libs/common/unidecode/x053.py | 8 +- libs/common/unidecode/x054.py | 6 +- libs/common/unidecode/x055.py | 4 +- libs/common/unidecode/x056.py | 8 +- libs/common/unidecode/x057.py | 4 +- libs/common/unidecode/x058.py | 14 +- libs/common/unidecode/x059.py | 4 +- libs/common/unidecode/x05a.py | 4 +- libs/common/unidecode/x05b.py | 2 +- libs/common/unidecode/x05c.py | 10 +- libs/common/unidecode/x05d.py | 10 +- libs/common/unidecode/x05e.py | 6 +- libs/common/unidecode/x05f.py | 4 +- libs/common/unidecode/x060.py | 2 +- libs/common/unidecode/x061.py | 6 +- libs/common/unidecode/x062.py | 4 +- libs/common/unidecode/x063.py | 8 +- libs/common/unidecode/x064.py | 4 +- libs/common/unidecode/x065.py | 4 +- libs/common/unidecode/x066.py | 2 +- libs/common/unidecode/x067.py | 8 +- libs/common/unidecode/x068.py | 4 +- libs/common/unidecode/x069.py | 8 +- libs/common/unidecode/x06a.py | 12 +- libs/common/unidecode/x06b.py | 8 +- libs/common/unidecode/x06c.py | 6 +- libs/common/unidecode/x06d.py | 4 +- libs/common/unidecode/x06e.py | 10 +- libs/common/unidecode/x06f.py | 12 +- libs/common/unidecode/x070.py | 16 +- libs/common/unidecode/x071.py | 26 +- libs/common/unidecode/x072.py | 14 +- libs/common/unidecode/x073.py | 4 +- libs/common/unidecode/x074.py | 10 +- libs/common/unidecode/x076.py | 4 +- libs/common/unidecode/x077.py | 2 +- libs/common/unidecode/x078.py | 18 +- libs/common/unidecode/x079.py | 12 +- libs/common/unidecode/x07a.py | 10 +- libs/common/unidecode/x07b.py | 2 +- libs/common/unidecode/x07c.py | 16 +- libs/common/unidecode/x07e.py | 2 +- libs/common/unidecode/x07f.py | 6 +- libs/common/unidecode/x080.py | 8 +- libs/common/unidecode/x081.py | 2 +- libs/common/unidecode/x082.py | 8 +- libs/common/unidecode/x083.py | 8 +- libs/common/unidecode/x084.py | 16 +- libs/common/unidecode/x085.py | 8 +- libs/common/unidecode/x086.py | 6 +- libs/common/unidecode/x087.py | 8 +- libs/common/unidecode/x088.py | 6 +- libs/common/unidecode/x089.py | 4 +- libs/common/unidecode/x08b.py | 4 +- libs/common/unidecode/x08c.py | 2 +- libs/common/unidecode/x08d.py | 6 +- libs/common/unidecode/x08e.py | 4 +- libs/common/unidecode/x08f.py | 2 +- libs/common/unidecode/x090.py | 4 +- libs/common/unidecode/x091.py | 2 +- libs/common/unidecode/x092.py | 4 +- libs/common/unidecode/x093.py | 20 +- libs/common/unidecode/x094.py | 8 +- libs/common/unidecode/x095.py | 4 +- libs/common/unidecode/x096.py | 2 +- libs/common/unidecode/x097.py | 10 +- libs/common/unidecode/x098.py | 2 +- libs/common/unidecode/x099.py | 2 +- libs/common/unidecode/x09b.py | 2 +- libs/common/unidecode/x09d.py | 2 +- libs/common/unidecode/x09e.py | 2 +- libs/common/unidecode/x09f.py | 178 +- libs/common/unidecode/x0a4.py | 128 +- libs/common/unidecode/x0d7.py | 182 +- libs/common/unidecode/x0fa.py | 444 +-- libs/common/unidecode/x0fb.py | 154 +- libs/common/unidecode/x0fd.py | 122 +- libs/common/unidecode/x0fe.py | 112 +- libs/common/unidecode/x0ff.py | 52 +- libs/common/unidecode/x1d4.py | 24 +- libs/common/unidecode/x1d5.py | 24 +- libs/common/unidecode/x1d6.py | 78 +- libs/common/unidecode/x1d7.py | 412 +-- libs/common/unidecode/x1f1.py | 438 +-- libs/common/unidecode/x1f6.py | 258 ++ libs/common/yaml/__init__.py | 114 +- libs/common/yaml/_yaml.cp37-win_amd64.pyd | Bin 0 -> 264192 bytes libs/common/yaml/composer.py | 4 +- libs/common/yaml/constructor.py | 182 +- libs/common/yaml/cyaml.py | 40 +- libs/common/yaml/dumper.py | 18 +- libs/common/yaml/emitter.py | 16 +- libs/common/yaml/loader.py | 25 +- libs/common/yaml/reader.py | 9 +- libs/common/yaml/representer.py | 20 +- libs/common/yaml/resolver.py | 6 +- libs/common/yaml/scanner.py | 45 +- 385 files changed, 25143 insertions(+), 18080 deletions(-) create mode 100644 libs/common/_yaml/__init__.py create mode 100644 libs/common/beets/random.py create mode 100644 libs/common/beetsplug/albumtypes.py create mode 100644 libs/common/beetsplug/aura.py create mode 100644 libs/common/beetsplug/bareasc.py create mode 100644 libs/common/beetsplug/bpsync.py delete mode 100644 libs/common/beetsplug/cue.py create mode 100644 libs/common/beetsplug/deezer.py create mode 100644 libs/common/beetsplug/fish.py create mode 100644 libs/common/beetsplug/loadext.py create mode 100644 libs/common/beetsplug/parentwork.py create mode 100644 libs/common/beetsplug/playlist.py create mode 100644 libs/common/beetsplug/subsonicplaylist.py create mode 100644 libs/common/beetsplug/subsonicupdate.py create mode 100644 libs/common/beetsplug/unimported.py delete mode 100644 libs/common/bin/chardetect.exe delete mode 100644 libs/common/bin/easy_install-3.7.exe delete mode 100644 libs/common/bin/easy_install.exe delete mode 100644 libs/common/bin/guessit.exe delete mode 100644 libs/common/bin/mid3cp create mode 100644 libs/common/bin/mid3cp.exe delete mode 100644 libs/common/bin/mid3iconv create mode 100644 libs/common/bin/mid3iconv.exe delete mode 100644 libs/common/bin/mid3v2 create mode 100644 libs/common/bin/mid3v2.exe delete mode 100644 libs/common/bin/moggsplit create mode 100644 libs/common/bin/moggsplit.exe delete mode 100644 libs/common/bin/mutagen-inspect create mode 100644 libs/common/bin/mutagen-inspect.exe delete mode 100644 libs/common/bin/mutagen-pony create mode 100644 libs/common/bin/mutagen-pony.exe delete mode 100644 libs/common/bin/pbr.exe delete mode 100644 libs/common/bin/srt.exe delete mode 100644 libs/common/bin/subliminal.exe create mode 100644 libs/common/colorama/tests/__init__.py create mode 100644 libs/common/colorama/tests/ansi_test.py create mode 100644 libs/common/colorama/tests/ansitowin32_test.py create mode 100644 libs/common/colorama/tests/initialise_test.py create mode 100644 libs/common/colorama/tests/isatty_test.py create mode 100644 libs/common/colorama/tests/utils.py create mode 100644 libs/common/colorama/tests/winterm_test.py create mode 100644 libs/common/confuse/__init__.py create mode 100644 libs/common/confuse/core.py create mode 100644 libs/common/confuse/exceptions.py create mode 100644 libs/common/confuse/sources.py create mode 100644 libs/common/confuse/templates.py create mode 100644 libs/common/confuse/util.py create mode 100644 libs/common/confuse/yaml_util.py create mode 100644 libs/common/jellyfish/__init__.pyi create mode 100644 libs/common/jellyfish/cjellyfish.cp37-win_amd64.pyd delete mode 100644 libs/common/jellyfish/compat.py create mode 100644 libs/common/jellyfish/py.typed create mode 100644 libs/common/mediafile.py delete mode 100644 libs/common/mutagen/_compat.py create mode 100644 libs/common/mutagen/_iff.py create mode 100644 libs/common/mutagen/_riff.py delete mode 100644 libs/common/mutagen/_senf/__init__.py delete mode 100644 libs/common/mutagen/_senf/_argv.py delete mode 100644 libs/common/mutagen/_senf/_compat.py delete mode 100644 libs/common/mutagen/_senf/_environ.py delete mode 100644 libs/common/mutagen/_senf/_fsnative.py delete mode 100644 libs/common/mutagen/_senf/_print.py delete mode 100644 libs/common/mutagen/_senf/_stdlib.py delete mode 100644 libs/common/mutagen/_senf/_temp.py delete mode 100644 libs/common/mutagen/_senf/_winansi.py delete mode 100644 libs/common/mutagen/_senf/_winapi.py create mode 100644 libs/common/mutagen/ac3.py create mode 100644 libs/common/mutagen/dsdiff.py create mode 100644 libs/common/mutagen/py.typed create mode 100644 libs/common/mutagen/tak.py create mode 100644 libs/common/mutagen/wave.py create mode 100644 libs/common/share/man/man1/mid3cp.1 create mode 100644 libs/common/share/man/man1/mid3iconv.1 create mode 100644 libs/common/share/man/man1/mid3v2.1 create mode 100644 libs/common/share/man/man1/moggsplit.1 create mode 100644 libs/common/share/man/man1/mutagen-inspect.1 create mode 100644 libs/common/share/man/man1/mutagen-pony.1 create mode 100644 libs/common/unidecode/__init__.pyi create mode 100644 libs/common/unidecode/__main__.py create mode 100644 libs/common/unidecode/py.typed create mode 100644 libs/common/unidecode/x1f6.py create mode 100644 libs/common/yaml/_yaml.cp37-win_amd64.pyd diff --git a/libs/common/_yaml/__init__.py b/libs/common/_yaml/__init__.py new file mode 100644 index 00000000..7baa8c4b --- /dev/null +++ b/libs/common/_yaml/__init__.py @@ -0,0 +1,33 @@ +# This is a stub package designed to roughly emulate the _yaml +# extension module, which previously existed as a standalone module +# and has been moved into the `yaml` package namespace. +# It does not perfectly mimic its old counterpart, but should get +# close enough for anyone who's relying on it even when they shouldn't. +import yaml + +# in some circumstances, the yaml module we imoprted may be from a different version, so we need +# to tread carefully when poking at it here (it may not have the attributes we expect) +if not getattr(yaml, '__with_libyaml__', False): + from sys import version_info + + exc = ModuleNotFoundError if version_info >= (3, 6) else ImportError + raise exc("No module named '_yaml'") +else: + from yaml._yaml import * + import warnings + warnings.warn( + 'The _yaml extension module is now located at yaml._yaml' + ' and its location is subject to change. To use the' + ' LibYAML-based parser and emitter, import from `yaml`:' + ' `from yaml import CLoader as Loader, CDumper as Dumper`.', + DeprecationWarning + ) + del warnings + # Don't `del yaml` here because yaml is actually an existing + # namespace member of _yaml. + +__name__ = '_yaml' +# If the module is top-level (i.e. not a part of any specific package) +# then the attribute should be set to ''. +# https://docs.python.org/3.8/library/types.html +__package__ = '' diff --git a/libs/common/beets/__init__.py b/libs/common/beets/__init__.py index b8fe2a84..9642a6f3 100644 --- a/libs/common/beets/__init__.py +++ b/libs/common/beets/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -13,30 +12,29 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function -import os +import confuse +from sys import stderr -from beets.util import confit - -__version__ = u'1.4.7' -__author__ = u'Adrian Sampson ' +__version__ = '1.6.0' +__author__ = 'Adrian Sampson ' -class IncludeLazyConfig(confit.LazyConfig): - """A version of Confit's LazyConfig that also merges in data from +class IncludeLazyConfig(confuse.LazyConfig): + """A version of Confuse's LazyConfig that also merges in data from YAML files specified in an `include` setting. """ def read(self, user=True, defaults=True): - super(IncludeLazyConfig, self).read(user, defaults) + super().read(user, defaults) try: for view in self['include']: - filename = view.as_filename() - if os.path.isfile(filename): - self.set_file(filename) - except confit.NotFoundError: + self.set_file(view.as_filename()) + except confuse.NotFoundError: pass + except confuse.ConfigReadError as err: + stderr.write("configuration `import` failed: {}" + .format(err.reason)) config = IncludeLazyConfig('beets', __name__) diff --git a/libs/common/beets/__main__.py b/libs/common/beets/__main__.py index 8010ca0d..ac829de9 100644 --- a/libs/common/beets/__main__.py +++ b/libs/common/beets/__main__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2017, Adrian Sampson. # @@ -17,7 +16,6 @@ `python -m beets`. """ -from __future__ import division, absolute_import, print_function import sys from .ui import main diff --git a/libs/common/beets/art.py b/libs/common/beets/art.py index 979a6f72..13d5dfbd 100644 --- a/libs/common/beets/art.py +++ b/libs/common/beets/art.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -17,7 +16,6 @@ music and items' embedded album art. """ -from __future__ import division, absolute_import, print_function import subprocess import platform @@ -26,7 +24,7 @@ import os from beets.util import displayable_path, syspath, bytestring_path from beets.util.artresizer import ArtResizer -from beets import mediafile +import mediafile def mediafile_image(image_path, maxwidth=None): @@ -43,7 +41,7 @@ def get_art(log, item): try: mf = mediafile.MediaFile(syspath(item.path)) except mediafile.UnreadableFileError as exc: - log.warning(u'Could not extract art from {0}: {1}', + log.warning('Could not extract art from {0}: {1}', displayable_path(item.path), exc) return @@ -51,26 +49,27 @@ def get_art(log, item): def embed_item(log, item, imagepath, maxwidth=None, itempath=None, - compare_threshold=0, ifempty=False, as_album=False): + compare_threshold=0, ifempty=False, as_album=False, id3v23=None, + quality=0): """Embed an image into the item's media file. """ # Conditions and filters. if compare_threshold: if not check_art_similarity(log, item, imagepath, compare_threshold): - log.info(u'Image not similar; skipping.') + log.info('Image not similar; skipping.') return if ifempty and get_art(log, item): - log.info(u'media file already contained art') - return + log.info('media file already contained art') + return if maxwidth and not as_album: - imagepath = resize_image(log, imagepath, maxwidth) + imagepath = resize_image(log, imagepath, maxwidth, quality) # Get the `Image` object from the file. try: - log.debug(u'embedding {0}', displayable_path(imagepath)) + log.debug('embedding {0}', displayable_path(imagepath)) image = mediafile_image(imagepath, maxwidth) - except IOError as exc: - log.warning(u'could not read image file: {0}', exc) + except OSError as exc: + log.warning('could not read image file: {0}', exc) return # Make sure the image kind is safe (some formats only support PNG @@ -80,36 +79,39 @@ def embed_item(log, item, imagepath, maxwidth=None, itempath=None, image.mime_type) return - item.try_write(path=itempath, tags={'images': [image]}) + item.try_write(path=itempath, tags={'images': [image]}, id3v23=id3v23) -def embed_album(log, album, maxwidth=None, quiet=False, - compare_threshold=0, ifempty=False): +def embed_album(log, album, maxwidth=None, quiet=False, compare_threshold=0, + ifempty=False, quality=0): """Embed album art into all of the album's items. """ imagepath = album.artpath if not imagepath: - log.info(u'No album art present for {0}', album) + log.info('No album art present for {0}', album) return if not os.path.isfile(syspath(imagepath)): - log.info(u'Album art not found at {0} for {1}', + log.info('Album art not found at {0} for {1}', displayable_path(imagepath), album) return if maxwidth: - imagepath = resize_image(log, imagepath, maxwidth) + imagepath = resize_image(log, imagepath, maxwidth, quality) - log.info(u'Embedding album art into {0}', album) + log.info('Embedding album art into {0}', album) for item in album.items(): - embed_item(log, item, imagepath, maxwidth, None, - compare_threshold, ifempty, as_album=True) + embed_item(log, item, imagepath, maxwidth, None, compare_threshold, + ifempty, as_album=True, quality=quality) -def resize_image(log, imagepath, maxwidth): - """Returns path to an image resized to maxwidth. +def resize_image(log, imagepath, maxwidth, quality): + """Returns path to an image resized to maxwidth and encoded with the + specified quality level. """ - log.debug(u'Resizing album art to {0} pixels wide', maxwidth) - imagepath = ArtResizer.shared.resize(maxwidth, syspath(imagepath)) + log.debug('Resizing album art to {0} pixels wide and encoding at quality \ + level {1}', maxwidth, quality) + imagepath = ArtResizer.shared.resize(maxwidth, syspath(imagepath), + quality=quality) return imagepath @@ -131,7 +133,7 @@ def check_art_similarity(log, item, imagepath, compare_threshold): syspath(art, prefix=False), '-colorspace', 'gray', 'MIFF:-'] compare_cmd = ['compare', '-metric', 'PHASH', '-', 'null:'] - log.debug(u'comparing images with pipeline {} | {}', + log.debug('comparing images with pipeline {} | {}', convert_cmd, compare_cmd) convert_proc = subprocess.Popen( convert_cmd, @@ -155,7 +157,7 @@ def check_art_similarity(log, item, imagepath, compare_threshold): convert_proc.wait() if convert_proc.returncode: log.debug( - u'ImageMagick convert failed with status {}: {!r}', + 'ImageMagick convert failed with status {}: {!r}', convert_proc.returncode, convert_stderr, ) @@ -165,7 +167,7 @@ def check_art_similarity(log, item, imagepath, compare_threshold): stdout, stderr = compare_proc.communicate() if compare_proc.returncode: if compare_proc.returncode != 1: - log.debug(u'ImageMagick compare failed: {0}, {1}', + log.debug('ImageMagick compare failed: {0}, {1}', displayable_path(imagepath), displayable_path(art)) return @@ -176,10 +178,10 @@ def check_art_similarity(log, item, imagepath, compare_threshold): try: phash_diff = float(out_str) except ValueError: - log.debug(u'IM output is not a number: {0!r}', out_str) + log.debug('IM output is not a number: {0!r}', out_str) return - log.debug(u'ImageMagick compare score: {0}', phash_diff) + log.debug('ImageMagick compare score: {0}', phash_diff) return phash_diff <= compare_threshold return True @@ -189,18 +191,18 @@ def extract(log, outpath, item): art = get_art(log, item) outpath = bytestring_path(outpath) if not art: - log.info(u'No album art present in {0}, skipping.', item) + log.info('No album art present in {0}, skipping.', item) return # Add an extension to the filename. ext = mediafile.image_extension(art) if not ext: - log.warning(u'Unknown image type in {0}.', + log.warning('Unknown image type in {0}.', displayable_path(item.path)) return outpath += bytestring_path('.' + ext) - log.info(u'Extracting album art from: {0} to: {1}', + log.info('Extracting album art from: {0} to: {1}', item, displayable_path(outpath)) with open(syspath(outpath), 'wb') as f: f.write(art) @@ -216,7 +218,7 @@ def extract_first(log, outpath, items): def clear(log, lib, query): items = lib.items(query) - log.info(u'Clearing album art from {0} items', len(items)) + log.info('Clearing album art from {0} items', len(items)) for item in items: - log.debug(u'Clearing art for {0}', item) + log.debug('Clearing art for {0}', item) item.try_write(tags={'images': None}) diff --git a/libs/common/beets/autotag/__init__.py b/libs/common/beets/autotag/__init__.py index c4ee1300..e62f492c 100644 --- a/libs/common/beets/autotag/__init__.py +++ b/libs/common/beets/autotag/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -16,19 +15,59 @@ """Facilities for automatically determining files' correct metadata. """ -from __future__ import division, absolute_import, print_function from beets import logging from beets import config # Parts of external interface. -from .hooks import AlbumInfo, TrackInfo, AlbumMatch, TrackMatch # noqa +from .hooks import ( # noqa + AlbumInfo, + TrackInfo, + AlbumMatch, + TrackMatch, + Distance, +) from .match import tag_item, tag_album, Proposal # noqa from .match import Recommendation # noqa # Global logger. log = logging.getLogger('beets') +# Metadata fields that are already hardcoded, or where the tag name changes. +SPECIAL_FIELDS = { + 'album': ( + 'va', + 'releasegroup_id', + 'artist_id', + 'album_id', + 'mediums', + 'tracks', + 'year', + 'month', + 'day', + 'artist', + 'artist_credit', + 'artist_sort', + 'data_url' + ), + 'track': ( + 'track_alt', + 'artist_id', + 'release_track_id', + 'medium', + 'index', + 'medium_index', + 'title', + 'artist_credit', + 'artist_sort', + 'artist', + 'track_id', + 'medium_total', + 'data_url', + 'length' + ) +} + # Additional utilities for the main interface. @@ -43,17 +82,14 @@ def apply_item_metadata(item, track_info): item.mb_releasetrackid = track_info.release_track_id if track_info.artist_id: item.mb_artistid = track_info.artist_id - if track_info.data_source: - item.data_source = track_info.data_source - if track_info.lyricist is not None: - item.lyricist = track_info.lyricist - if track_info.composer is not None: - item.composer = track_info.composer - if track_info.composer_sort is not None: - item.composer_sort = track_info.composer_sort - if track_info.arranger is not None: - item.arranger = track_info.arranger + for field, value in track_info.items(): + # We only overwrite fields that are not already hardcoded. + if field in SPECIAL_FIELDS['track']: + continue + if value is None: + continue + item[field] = value # At the moment, the other metadata is left intact (including album # and track number). Perhaps these should be emptied? @@ -142,33 +178,24 @@ def apply_metadata(album_info, mapping): # Compilation flag. item.comp = album_info.va - # Miscellaneous metadata. - for field in ('albumtype', - 'label', - 'asin', - 'catalognum', - 'script', - 'language', - 'country', - 'albumstatus', - 'albumdisambig', - 'data_source',): - value = getattr(album_info, field) - if value is not None: - item[field] = value - if track_info.disctitle is not None: - item.disctitle = track_info.disctitle - - if track_info.media is not None: - item.media = track_info.media - - if track_info.lyricist is not None: - item.lyricist = track_info.lyricist - if track_info.composer is not None: - item.composer = track_info.composer - if track_info.composer_sort is not None: - item.composer_sort = track_info.composer_sort - if track_info.arranger is not None: - item.arranger = track_info.arranger - + # Track alt. item.track_alt = track_info.track_alt + + # Don't overwrite fields with empty values unless the + # field is explicitly allowed to be overwritten + for field, value in album_info.items(): + if field in SPECIAL_FIELDS['album']: + continue + clobber = field in config['overwrite_null']['album'].as_str_seq() + if value is None and not clobber: + continue + item[field] = value + + for field, value in track_info.items(): + if field in SPECIAL_FIELDS['track']: + continue + clobber = field in config['overwrite_null']['track'].as_str_seq() + value = getattr(track_info, field) + if value is None and not clobber: + continue + item[field] = value diff --git a/libs/common/beets/autotag/hooks.py b/libs/common/beets/autotag/hooks.py index 3615a933..9cd6f2cd 100644 --- a/libs/common/beets/autotag/hooks.py +++ b/libs/common/beets/autotag/hooks.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -14,7 +13,6 @@ # included in all copies or substantial portions of the Software. """Glue between metadata sources and the matching logic.""" -from __future__ import division, absolute_import, print_function from collections import namedtuple from functools import total_ordering @@ -27,14 +25,36 @@ from beets.util import as_string from beets.autotag import mb from jellyfish import levenshtein_distance from unidecode import unidecode -import six log = logging.getLogger('beets') +# The name of the type for patterns in re changed in Python 3.7. +try: + Pattern = re._pattern_type +except AttributeError: + Pattern = re.Pattern + # Classes used to represent candidate options. +class AttrDict(dict): + """A dictionary that supports attribute ("dot") access, so `d.field` + is equivalent to `d['field']`. + """ -class AlbumInfo(object): + def __getattr__(self, attr): + if attr in self: + return self.get(attr) + else: + raise AttributeError + + def __setattr__(self, key, value): + self.__setitem__(key, value) + + def __hash__(self): + return id(self) + + +class AlbumInfo(AttrDict): """Describes a canonical release that may be used to match a release in the library. Consists of these data members: @@ -43,38 +63,22 @@ class AlbumInfo(object): - ``artist``: name of the release's primary artist - ``artist_id`` - ``tracks``: list of TrackInfo objects making up the release - - ``asin``: Amazon ASIN - - ``albumtype``: string describing the kind of release - - ``va``: boolean: whether the release has "various artists" - - ``year``: release year - - ``month``: release month - - ``day``: release day - - ``label``: music label responsible for the release - - ``mediums``: the number of discs in this release - - ``artist_sort``: name of the release's artist for sorting - - ``releasegroup_id``: MBID for the album's release group - - ``catalognum``: the label's catalog number for the release - - ``script``: character set used for metadata - - ``language``: human language of the metadata - - ``country``: the release country - - ``albumstatus``: MusicBrainz release status (Official, etc.) - - ``media``: delivery mechanism (Vinyl, etc.) - - ``albumdisambig``: MusicBrainz release disambiguation comment - - ``artist_credit``: Release-specific artist name - - ``data_source``: The original data source (MusicBrainz, Discogs, etc.) - - ``data_url``: The data source release URL. - The fields up through ``tracks`` are required. The others are - optional and may be None. + ``mediums`` along with the fields up through ``tracks`` are required. + The others are optional and may be None. """ - def __init__(self, album, album_id, artist, artist_id, tracks, asin=None, - albumtype=None, va=False, year=None, month=None, day=None, - label=None, mediums=None, artist_sort=None, - releasegroup_id=None, catalognum=None, script=None, - language=None, country=None, albumstatus=None, media=None, - albumdisambig=None, artist_credit=None, original_year=None, - original_month=None, original_day=None, data_source=None, - data_url=None): + + def __init__(self, tracks, album=None, album_id=None, artist=None, + artist_id=None, asin=None, albumtype=None, va=False, + year=None, month=None, day=None, label=None, mediums=None, + artist_sort=None, releasegroup_id=None, catalognum=None, + script=None, language=None, country=None, style=None, + genre=None, albumstatus=None, media=None, albumdisambig=None, + releasegroupdisambig=None, artist_credit=None, + original_year=None, original_month=None, + original_day=None, data_source=None, data_url=None, + discogs_albumid=None, discogs_labelid=None, + discogs_artistid=None, **kwargs): self.album = album self.album_id = album_id self.artist = artist @@ -94,15 +98,22 @@ class AlbumInfo(object): self.script = script self.language = language self.country = country + self.style = style + self.genre = genre self.albumstatus = albumstatus self.media = media self.albumdisambig = albumdisambig + self.releasegroupdisambig = releasegroupdisambig self.artist_credit = artist_credit self.original_year = original_year self.original_month = original_month self.original_day = original_day self.data_source = data_source self.data_url = data_url + self.discogs_albumid = discogs_albumid + self.discogs_labelid = discogs_labelid + self.discogs_artistid = discogs_artistid + self.update(kwargs) # Work around a bug in python-musicbrainz-ngs that causes some # strings to be bytes rather than Unicode. @@ -112,54 +123,46 @@ class AlbumInfo(object): constituent `TrackInfo` objects, are decoded to Unicode. """ for fld in ['album', 'artist', 'albumtype', 'label', 'artist_sort', - 'catalognum', 'script', 'language', 'country', - 'albumstatus', 'albumdisambig', 'artist_credit', 'media']: + 'catalognum', 'script', 'language', 'country', 'style', + 'genre', 'albumstatus', 'albumdisambig', + 'releasegroupdisambig', 'artist_credit', + 'media', 'discogs_albumid', 'discogs_labelid', + 'discogs_artistid']: value = getattr(self, fld) if isinstance(value, bytes): setattr(self, fld, value.decode(codec, 'ignore')) - if self.tracks: - for track in self.tracks: - track.decode(codec) + for track in self.tracks: + track.decode(codec) + + def copy(self): + dupe = AlbumInfo([]) + dupe.update(self) + dupe.tracks = [track.copy() for track in self.tracks] + return dupe -class TrackInfo(object): +class TrackInfo(AttrDict): """Describes a canonical track present on a release. Appears as part of an AlbumInfo's ``tracks`` list. Consists of these data members: - ``title``: name of the track - ``track_id``: MusicBrainz ID; UUID fragment only - - ``release_track_id``: MusicBrainz ID respective to a track on a - particular release; UUID fragment only - - ``artist``: individual track artist name - - ``artist_id`` - - ``length``: float: duration of the track in seconds - - ``index``: position on the entire release - - ``media``: delivery mechanism (Vinyl, etc.) - - ``medium``: the disc number this track appears on in the album - - ``medium_index``: the track's position on the disc - - ``medium_total``: the number of tracks on the item's disc - - ``artist_sort``: name of the track artist for sorting - - ``disctitle``: name of the individual medium (subtitle) - - ``artist_credit``: Recording-specific artist name - - ``data_source``: The original data source (MusicBrainz, Discogs, etc.) - - ``data_url``: The data source release URL. - - ``lyricist``: individual track lyricist name - - ``composer``: individual track composer name - - ``composer_sort``: individual track composer sort name - - ``arranger`: individual track arranger name - - ``track_alt``: alternative track number (tape, vinyl, etc.) Only ``title`` and ``track_id`` are required. The rest of the fields may be None. The indices ``index``, ``medium``, and ``medium_index`` are all 1-based. """ - def __init__(self, title, track_id, release_track_id=None, artist=None, - artist_id=None, length=None, index=None, medium=None, - medium_index=None, medium_total=None, artist_sort=None, - disctitle=None, artist_credit=None, data_source=None, - data_url=None, media=None, lyricist=None, composer=None, - composer_sort=None, arranger=None, track_alt=None): + + def __init__(self, title=None, track_id=None, release_track_id=None, + artist=None, artist_id=None, length=None, index=None, + medium=None, medium_index=None, medium_total=None, + artist_sort=None, disctitle=None, artist_credit=None, + data_source=None, data_url=None, media=None, lyricist=None, + composer=None, composer_sort=None, arranger=None, + track_alt=None, work=None, mb_workid=None, + work_disambig=None, bpm=None, initial_key=None, genre=None, + **kwargs): self.title = title self.track_id = track_id self.release_track_id = release_track_id @@ -181,6 +184,13 @@ class TrackInfo(object): self.composer_sort = composer_sort self.arranger = arranger self.track_alt = track_alt + self.work = work + self.mb_workid = mb_workid + self.work_disambig = work_disambig + self.bpm = bpm + self.initial_key = initial_key + self.genre = genre + self.update(kwargs) # As above, work around a bug in python-musicbrainz-ngs. def decode(self, codec='utf-8'): @@ -193,6 +203,11 @@ class TrackInfo(object): if isinstance(value, bytes): setattr(self, fld, value.decode(codec, 'ignore')) + def copy(self): + dupe = TrackInfo() + dupe.update(self) + return dupe + # Candidate distance scoring. @@ -220,8 +235,8 @@ def _string_dist_basic(str1, str2): transliteration/lowering to ASCII characters. Normalized by string length. """ - assert isinstance(str1, six.text_type) - assert isinstance(str2, six.text_type) + assert isinstance(str1, str) + assert isinstance(str2, str) str1 = as_string(unidecode(str1)) str2 = as_string(unidecode(str2)) str1 = re.sub(r'[^a-z0-9]', '', str1.lower()) @@ -249,9 +264,9 @@ def string_dist(str1, str2): # "something, the". for word in SD_END_WORDS: if str1.endswith(', %s' % word): - str1 = '%s %s' % (word, str1[:-len(word) - 2]) + str1 = '{} {}'.format(word, str1[:-len(word) - 2]) if str2.endswith(', %s' % word): - str2 = '%s %s' % (word, str2[:-len(word) - 2]) + str2 = '{} {}'.format(word, str2[:-len(word) - 2]) # Perform a couple of basic normalizing substitutions. for pat, repl in SD_REPLACE: @@ -289,11 +304,12 @@ def string_dist(str1, str2): return base_dist + penalty -class LazyClassProperty(object): +class LazyClassProperty: """A decorator implementing a read-only property that is *lazy* in the sense that the getter is only invoked once. Subsequent accesses through *any* instance use the cached result. """ + def __init__(self, getter): self.getter = getter self.computed = False @@ -306,17 +322,17 @@ class LazyClassProperty(object): @total_ordering -@six.python_2_unicode_compatible -class Distance(object): +class Distance: """Keeps track of multiple distance penalties. Provides a single weighted distance for all penalties as well as a weighted distance for each individual penalty. """ + def __init__(self): self._penalties = {} @LazyClassProperty - def _weights(cls): # noqa + def _weights(cls): # noqa: N805 """A dictionary from keys to floating-point weights. """ weights_view = config['match']['distance_weights'] @@ -394,7 +410,7 @@ class Distance(object): return other - self.distance def __str__(self): - return "{0:.2f}".format(self.distance) + return f"{self.distance:.2f}" # Behave like a dict. @@ -421,7 +437,7 @@ class Distance(object): """ if not isinstance(dist, Distance): raise ValueError( - u'`dist` must be a Distance object, not {0}'.format(type(dist)) + '`dist` must be a Distance object, not {}'.format(type(dist)) ) for key, penalties in dist._penalties.items(): self._penalties.setdefault(key, []).extend(penalties) @@ -433,7 +449,7 @@ class Distance(object): be a compiled regular expression, in which case it will be matched against `value2`. """ - if isinstance(value1, re._pattern_type): + if isinstance(value1, Pattern): return bool(value1.match(value2)) return value1 == value2 @@ -445,7 +461,7 @@ class Distance(object): """ if not 0.0 <= dist <= 1.0: raise ValueError( - u'`dist` must be between 0.0 and 1.0, not {0}'.format(dist) + f'`dist` must be between 0.0 and 1.0, not {dist}' ) self._penalties.setdefault(key, []).append(dist) @@ -541,7 +557,7 @@ def album_for_mbid(release_id): try: album = mb.album_for_id(release_id) if album: - plugins.send(u'albuminfo_received', info=album) + plugins.send('albuminfo_received', info=album) return album except mb.MusicBrainzAPIError as exc: exc.log(log) @@ -554,7 +570,7 @@ def track_for_mbid(recording_id): try: track = mb.track_for_id(recording_id) if track: - plugins.send(u'trackinfo_received', info=track) + plugins.send('trackinfo_received', info=track) return track except mb.MusicBrainzAPIError as exc: exc.log(log) @@ -567,7 +583,7 @@ def albums_for_id(album_id): yield a for a in plugins.album_for_id(album_id): if a: - plugins.send(u'albuminfo_received', info=a) + plugins.send('albuminfo_received', info=a) yield a @@ -578,40 +594,43 @@ def tracks_for_id(track_id): yield t for t in plugins.track_for_id(track_id): if t: - plugins.send(u'trackinfo_received', info=t) + plugins.send('trackinfo_received', info=t) yield t -@plugins.notify_info_yielded(u'albuminfo_received') -def album_candidates(items, artist, album, va_likely): +@plugins.notify_info_yielded('albuminfo_received') +def album_candidates(items, artist, album, va_likely, extra_tags): """Search for album matches. ``items`` is a list of Item objects that make up the album. ``artist`` and ``album`` are the respective names (strings), which may be derived from the item list or may be entered by the user. ``va_likely`` is a boolean indicating whether - the album is likely to be a "various artists" release. + the album is likely to be a "various artists" release. ``extra_tags`` + is an optional dictionary of additional tags used to further + constrain the search. """ + # Base candidates if we have album and artist to match. if artist and album: try: - for candidate in mb.match_album(artist, album, len(items)): - yield candidate + yield from mb.match_album(artist, album, len(items), + extra_tags) except mb.MusicBrainzAPIError as exc: exc.log(log) # Also add VA matches from MusicBrainz where appropriate. if va_likely and album: try: - for candidate in mb.match_album(None, album, len(items)): - yield candidate + yield from mb.match_album(None, album, len(items), + extra_tags) except mb.MusicBrainzAPIError as exc: exc.log(log) # Candidates from plugins. - for candidate in plugins.candidates(items, artist, album, va_likely): - yield candidate + yield from plugins.candidates(items, artist, album, va_likely, + extra_tags) -@plugins.notify_info_yielded(u'trackinfo_received') +@plugins.notify_info_yielded('trackinfo_received') def item_candidates(item, artist, title): """Search for item matches. ``item`` is the Item to be matched. ``artist`` and ``title`` are strings and either reflect the item or @@ -621,11 +640,9 @@ def item_candidates(item, artist, title): # MusicBrainz candidates. if artist and title: try: - for candidate in mb.match_track(artist, title): - yield candidate + yield from mb.match_track(artist, title) except mb.MusicBrainzAPIError as exc: exc.log(log) # Plugin candidates. - for candidate in plugins.item_candidates(item, artist, title): - yield candidate + yield from plugins.item_candidates(item, artist, title) diff --git a/libs/common/beets/autotag/match.py b/libs/common/beets/autotag/match.py index 71b62adb..d352a013 100644 --- a/libs/common/beets/autotag/match.py +++ b/libs/common/beets/autotag/match.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -17,7 +16,6 @@ releases and tracks. """ -from __future__ import division, absolute_import, print_function import datetime import re @@ -35,7 +33,7 @@ from beets.util.enumeration import OrderedEnum # album level to determine whether a given release is likely a VA # release and also on the track level to to remove the penalty for # differing artists. -VA_ARTISTS = (u'', u'various artists', u'various', u'va', u'unknown') +VA_ARTISTS = ('', 'various artists', 'various', 'va', 'unknown') # Global logger. log = logging.getLogger('beets') @@ -108,7 +106,7 @@ def assign_items(items, tracks): log.debug('...done.') # Produce the output matching. - mapping = dict((items[i], tracks[j]) for (i, j) in matching) + mapping = {items[i]: tracks[j] for (i, j) in matching} extra_items = list(set(items) - set(mapping.keys())) extra_items.sort(key=lambda i: (i.disc, i.track, i.title)) extra_tracks = list(set(tracks) - set(mapping.values())) @@ -276,16 +274,16 @@ def match_by_id(items): try: first = next(albumids) except StopIteration: - log.debug(u'No album ID found.') + log.debug('No album ID found.') return None # Is there a consensus on the MB album ID? for other in albumids: if other != first: - log.debug(u'No album ID consensus.') + log.debug('No album ID consensus.') return None # If all album IDs are equal, look up the album. - log.debug(u'Searching for discovered album ID: {0}', first) + log.debug('Searching for discovered album ID: {0}', first) return hooks.album_for_mbid(first) @@ -351,23 +349,23 @@ def _add_candidate(items, results, info): checking the track count, ordering the items, checking for duplicates, and calculating the distance. """ - log.debug(u'Candidate: {0} - {1} ({2})', + log.debug('Candidate: {0} - {1} ({2})', info.artist, info.album, info.album_id) # Discard albums with zero tracks. if not info.tracks: - log.debug(u'No tracks.') + log.debug('No tracks.') return # Don't duplicate. if info.album_id in results: - log.debug(u'Duplicate.') + log.debug('Duplicate.') return # Discard matches without required tags. for req_tag in config['match']['required'].as_str_seq(): if getattr(info, req_tag) is None: - log.debug(u'Ignored. Missing required tag: {0}', req_tag) + log.debug('Ignored. Missing required tag: {0}', req_tag) return # Find mapping between the items and the track info. @@ -380,10 +378,10 @@ def _add_candidate(items, results, info): penalties = [key for key, _ in dist] for penalty in config['match']['ignored'].as_str_seq(): if penalty in penalties: - log.debug(u'Ignored. Penalty: {0}', penalty) + log.debug('Ignored. Penalty: {0}', penalty) return - log.debug(u'Success. Distance: {0}', dist) + log.debug('Success. Distance: {0}', dist) results[info.album_id] = hooks.AlbumMatch(dist, info, mapping, extra_items, extra_tracks) @@ -411,7 +409,7 @@ def tag_album(items, search_artist=None, search_album=None, likelies, consensus = current_metadata(items) cur_artist = likelies['artist'] cur_album = likelies['album'] - log.debug(u'Tagging {0} - {1}', cur_artist, cur_album) + log.debug('Tagging {0} - {1}', cur_artist, cur_album) # The output result (distance, AlbumInfo) tuples (keyed by MB album # ID). @@ -420,7 +418,7 @@ def tag_album(items, search_artist=None, search_album=None, # Search by explicit ID. if search_ids: for search_id in search_ids: - log.debug(u'Searching for album ID: {0}', search_id) + log.debug('Searching for album ID: {0}', search_id) for id_candidate in hooks.albums_for_id(search_id): _add_candidate(items, candidates, id_candidate) @@ -431,13 +429,13 @@ def tag_album(items, search_artist=None, search_album=None, if id_info: _add_candidate(items, candidates, id_info) rec = _recommendation(list(candidates.values())) - log.debug(u'Album ID match recommendation is {0}', rec) + log.debug('Album ID match recommendation is {0}', rec) if candidates and not config['import']['timid']: # If we have a very good MBID match, return immediately. # Otherwise, this match will compete against metadata-based # matches. if rec == Recommendation.strong: - log.debug(u'ID match.') + log.debug('ID match.') return cur_artist, cur_album, \ Proposal(list(candidates.values()), rec) @@ -445,22 +443,29 @@ def tag_album(items, search_artist=None, search_album=None, if not (search_artist and search_album): # No explicit search terms -- use current metadata. search_artist, search_album = cur_artist, cur_album - log.debug(u'Search terms: {0} - {1}', search_artist, search_album) + log.debug('Search terms: {0} - {1}', search_artist, search_album) + + extra_tags = None + if config['musicbrainz']['extra_tags']: + tag_list = config['musicbrainz']['extra_tags'].get() + extra_tags = {k: v for (k, v) in likelies.items() if k in tag_list} + log.debug('Additional search terms: {0}', extra_tags) # Is this album likely to be a "various artist" release? va_likely = ((not consensus['artist']) or (search_artist.lower() in VA_ARTISTS) or any(item.comp for item in items)) - log.debug(u'Album might be VA: {0}', va_likely) + log.debug('Album might be VA: {0}', va_likely) # Get the results from the data sources. for matched_candidate in hooks.album_candidates(items, search_artist, search_album, - va_likely): + va_likely, + extra_tags): _add_candidate(items, candidates, matched_candidate) - log.debug(u'Evaluating {0} candidates.', len(candidates)) + log.debug('Evaluating {0} candidates.', len(candidates)) # Sort and get the recommendation. candidates = _sort_candidates(candidates.values()) rec = _recommendation(candidates) @@ -485,7 +490,7 @@ def tag_item(item, search_artist=None, search_title=None, trackids = search_ids or [t for t in [item.mb_trackid] if t] if trackids: for trackid in trackids: - log.debug(u'Searching for track ID: {0}', trackid) + log.debug('Searching for track ID: {0}', trackid) for track_info in hooks.tracks_for_id(trackid): dist = track_distance(item, track_info, incl_artist=True) candidates[track_info.track_id] = \ @@ -494,7 +499,7 @@ def tag_item(item, search_artist=None, search_title=None, rec = _recommendation(_sort_candidates(candidates.values())) if rec == Recommendation.strong and \ not config['import']['timid']: - log.debug(u'Track ID match.') + log.debug('Track ID match.') return Proposal(_sort_candidates(candidates.values()), rec) # If we're searching by ID, don't proceed. @@ -507,7 +512,7 @@ def tag_item(item, search_artist=None, search_title=None, # Search terms. if not (search_artist and search_title): search_artist, search_title = item.artist, item.title - log.debug(u'Item search terms: {0} - {1}', search_artist, search_title) + log.debug('Item search terms: {0} - {1}', search_artist, search_title) # Get and evaluate candidate metadata. for track_info in hooks.item_candidates(item, search_artist, search_title): @@ -515,7 +520,7 @@ def tag_item(item, search_artist=None, search_title=None, candidates[track_info.track_id] = hooks.TrackMatch(dist, track_info) # Sort by distance and return with recommendation. - log.debug(u'Found {0} candidates.', len(candidates)) + log.debug('Found {0} candidates.', len(candidates)) candidates = _sort_candidates(candidates.values()) rec = _recommendation(candidates) return Proposal(candidates, rec) diff --git a/libs/common/beets/autotag/mb.py b/libs/common/beets/autotag/mb.py index 2b28a5cc..e6a2e277 100644 --- a/libs/common/beets/autotag/mb.py +++ b/libs/common/beets/autotag/mb.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,57 +14,72 @@ """Searches for albums in the MusicBrainz database. """ -from __future__ import division, absolute_import, print_function import musicbrainzngs import re import traceback -from six.moves.urllib.parse import urljoin from beets import logging +from beets import plugins import beets.autotag.hooks import beets from beets import util from beets import config -import six +from collections import Counter +from urllib.parse import urljoin VARIOUS_ARTISTS_ID = '89ad4ac3-39f7-470e-963a-56509c546377' -if util.SNI_SUPPORTED: - BASE_URL = 'https://musicbrainz.org/' -else: - BASE_URL = 'http://musicbrainz.org/' +BASE_URL = 'https://musicbrainz.org/' SKIPPED_TRACKS = ['[data track]'] +FIELDS_TO_MB_KEYS = { + 'catalognum': 'catno', + 'country': 'country', + 'label': 'label', + 'media': 'format', + 'year': 'date', +} + musicbrainzngs.set_useragent('beets', beets.__version__, - 'http://beets.io/') + 'https://beets.io/') class MusicBrainzAPIError(util.HumanReadableException): """An error while talking to MusicBrainz. The `query` field is the parameter to the action and may have any type. """ + def __init__(self, reason, verb, query, tb=None): self.query = query if isinstance(reason, musicbrainzngs.WebServiceError): - reason = u'MusicBrainz not reachable' - super(MusicBrainzAPIError, self).__init__(reason, verb, tb) + reason = 'MusicBrainz not reachable' + super().__init__(reason, verb, tb) def get_message(self): - return u'{0} in {1} with query {2}'.format( + return '{} in {} with query {}'.format( self._reasonstr(), self.verb, repr(self.query) ) + log = logging.getLogger('beets') RELEASE_INCLUDES = ['artists', 'media', 'recordings', 'release-groups', 'labels', 'artist-credits', 'aliases', 'recording-level-rels', 'work-rels', - 'work-level-rels', 'artist-rels'] -TRACK_INCLUDES = ['artists', 'aliases'] + 'work-level-rels', 'artist-rels', 'isrcs'] +BROWSE_INCLUDES = ['artist-credits', 'work-rels', + 'artist-rels', 'recording-rels', 'release-rels'] +if "work-level-rels" in musicbrainzngs.VALID_BROWSE_INCLUDES['recording']: + BROWSE_INCLUDES.append("work-level-rels") +BROWSE_CHUNKSIZE = 100 +BROWSE_MAXTRACKS = 500 +TRACK_INCLUDES = ['artists', 'aliases', 'isrcs'] if 'work-level-rels' in musicbrainzngs.VALID_INCLUDES['recording']: TRACK_INCLUDES += ['work-level-rels', 'artist-rels'] +if 'genres' in musicbrainzngs.VALID_INCLUDES['recording']: + RELEASE_INCLUDES += ['genres'] def track_url(trackid): @@ -81,7 +95,11 @@ def configure(): from the beets configuration. This should be called at startup. """ hostname = config['musicbrainz']['host'].as_str() - musicbrainzngs.set_hostname(hostname) + https = config['musicbrainz']['https'].get(bool) + # Only call set_hostname when a custom server is configured. Since + # musicbrainz-ngs connects to musicbrainz.org with HTTPS by default + if hostname != "musicbrainz.org": + musicbrainzngs.set_hostname(hostname, https) musicbrainzngs.set_rate_limit( config['musicbrainz']['ratelimit_interval'].as_number(), config['musicbrainz']['ratelimit'].get(int), @@ -138,7 +156,7 @@ def _flatten_artist_credit(credit): artist_sort_parts = [] artist_credit_parts = [] for el in credit: - if isinstance(el, six.string_types): + if isinstance(el, str): # Join phrase. artist_parts.append(el) artist_credit_parts.append(el) @@ -185,13 +203,13 @@ def track_info(recording, index=None, medium=None, medium_index=None, the number of tracks on the medium. Each number is a 1-based index. """ info = beets.autotag.hooks.TrackInfo( - recording['title'], - recording['id'], + title=recording['title'], + track_id=recording['id'], index=index, medium=medium, medium_index=medium_index, medium_total=medium_total, - data_source=u'MusicBrainz', + data_source='MusicBrainz', data_url=track_url(recording['id']), ) @@ -207,12 +225,22 @@ def track_info(recording, index=None, medium=None, medium_index=None, if recording.get('length'): info.length = int(recording['length']) / (1000.0) + info.trackdisambig = recording.get('disambiguation') + + if recording.get('isrc-list'): + info.isrc = ';'.join(recording['isrc-list']) + lyricist = [] composer = [] composer_sort = [] for work_relation in recording.get('work-relation-list', ()): if work_relation['type'] != 'performance': continue + info.work = work_relation['work']['title'] + info.mb_workid = work_relation['work']['id'] + if 'disambiguation' in work_relation['work']: + info.work_disambig = work_relation['work']['disambiguation'] + for artist_relation in work_relation['work'].get( 'artist-relation-list', ()): if 'type' in artist_relation: @@ -224,10 +252,10 @@ def track_info(recording, index=None, medium=None, medium_index=None, composer_sort.append( artist_relation['artist']['sort-name']) if lyricist: - info.lyricist = u', '.join(lyricist) + info.lyricist = ', '.join(lyricist) if composer: - info.composer = u', '.join(composer) - info.composer_sort = u', '.join(composer_sort) + info.composer = ', '.join(composer) + info.composer_sort = ', '.join(composer_sort) arranger = [] for artist_relation in recording.get('artist-relation-list', ()): @@ -236,7 +264,12 @@ def track_info(recording, index=None, medium=None, medium_index=None, if type == 'arranger': arranger.append(artist_relation['artist']['name']) if arranger: - info.arranger = u', '.join(arranger) + info.arranger = ', '.join(arranger) + + # Supplementary fields provided by plugins + extra_trackdatas = plugins.send('mb_track_extract', data=recording) + for extra_trackdata in extra_trackdatas: + info.update(extra_trackdata) info.decode() return info @@ -270,6 +303,26 @@ def album_info(release): artist_name, artist_sort_name, artist_credit_name = \ _flatten_artist_credit(release['artist-credit']) + ntracks = sum(len(m['track-list']) for m in release['medium-list']) + + # The MusicBrainz API omits 'artist-relation-list' and 'work-relation-list' + # when the release has more than 500 tracks. So we use browse_recordings + # on chunks of tracks to recover the same information in this case. + if ntracks > BROWSE_MAXTRACKS: + log.debug('Album {} has too many tracks', release['id']) + recording_list = [] + for i in range(0, ntracks, BROWSE_CHUNKSIZE): + log.debug('Retrieving tracks starting at {}', i) + recording_list.extend(musicbrainzngs.browse_recordings( + release=release['id'], limit=BROWSE_CHUNKSIZE, + includes=BROWSE_INCLUDES, + offset=i)['recording-list']) + track_map = {r['id']: r for r in recording_list} + for medium in release['medium-list']: + for recording in medium['track-list']: + recording_info = track_map[recording['recording']['id']] + recording['recording'] = recording_info + # Basic info. track_infos = [] index = 0 @@ -281,7 +334,8 @@ def album_info(release): continue all_tracks = medium['track-list'] - if 'data-track-list' in medium: + if ('data-track-list' in medium + and not config['match']['ignore_data_tracks']): all_tracks += medium['data-track-list'] track_count = len(all_tracks) @@ -327,15 +381,15 @@ def album_info(release): track_infos.append(ti) info = beets.autotag.hooks.AlbumInfo( - release['title'], - release['id'], - artist_name, - release['artist-credit'][0]['artist']['id'], - track_infos, + album=release['title'], + album_id=release['id'], + artist=artist_name, + artist_id=release['artist-credit'][0]['artist']['id'], + tracks=track_infos, mediums=len(release['medium-list']), artist_sort=artist_sort_name, artist_credit=artist_credit_name, - data_source=u'MusicBrainz', + data_source='MusicBrainz', data_url=album_url(release['id']), ) info.va = info.artist_id == VARIOUS_ARTISTS_ID @@ -345,13 +399,12 @@ def album_info(release): info.releasegroup_id = release['release-group']['id'] info.albumstatus = release.get('status') - # Build up the disambiguation string from the release group and release. - disambig = [] + # Get the disambiguation strings at the release and release group level. if release['release-group'].get('disambiguation'): - disambig.append(release['release-group'].get('disambiguation')) + info.releasegroupdisambig = \ + release['release-group'].get('disambiguation') if release.get('disambiguation'): - disambig.append(release.get('disambiguation')) - info.albumdisambig = u', '.join(disambig) + info.albumdisambig = release.get('disambiguation') # Get the "classic" Release type. This data comes from a legacy API # feature before MusicBrainz supported multiple release types. @@ -360,18 +413,17 @@ def album_info(release): if reltype: info.albumtype = reltype.lower() - # Log the new-style "primary" and "secondary" release types. - # Eventually, we'd like to actually store this data, but we just log - # it for now to help understand the differences. + # Set the new-style "primary" and "secondary" release types. + albumtypes = [] if 'primary-type' in release['release-group']: rel_primarytype = release['release-group']['primary-type'] if rel_primarytype: - log.debug('primary MB release type: ' + rel_primarytype.lower()) + albumtypes.append(rel_primarytype.lower()) if 'secondary-type-list' in release['release-group']: if release['release-group']['secondary-type-list']: - log.debug('secondary MB release type(s): ' + ', '.join( - [secondarytype.lower() for secondarytype in - release['release-group']['secondary-type-list']])) + for sec_type in release['release-group']['secondary-type-list']: + albumtypes.append(sec_type.lower()) + info.albumtypes = '; '.join(albumtypes) # Release events. info.country, release_date = _preferred_release_event(release) @@ -402,17 +454,33 @@ def album_info(release): first_medium = release['medium-list'][0] info.media = first_medium.get('format') + if config['musicbrainz']['genres']: + sources = [ + release['release-group'].get('genre-list', []), + release.get('genre-list', []), + ] + genres = Counter() + for source in sources: + for genreitem in source: + genres[genreitem['name']] += int(genreitem['count']) + info.genre = '; '.join(g[0] for g in sorted(genres.items(), + key=lambda g: -g[1])) + + extra_albumdatas = plugins.send('mb_album_extract', data=release) + for extra_albumdata in extra_albumdatas: + info.update(extra_albumdata) + info.decode() return info -def match_album(artist, album, tracks=None): +def match_album(artist, album, tracks=None, extra_tags=None): """Searches for a single album ("release" in MusicBrainz parlance) and returns an iterator over AlbumInfo objects. May raise a MusicBrainzAPIError. The query consists of an artist name, an album name, and, - optionally, a number of tracks on the album. + optionally, a number of tracks on the album and any other extra tags. """ # Build search criteria. criteria = {'release': album.lower().strip()} @@ -422,14 +490,24 @@ def match_album(artist, album, tracks=None): # Various Artists search. criteria['arid'] = VARIOUS_ARTISTS_ID if tracks is not None: - criteria['tracks'] = six.text_type(tracks) + criteria['tracks'] = str(tracks) + + # Additional search cues from existing metadata. + if extra_tags: + for tag in extra_tags: + key = FIELDS_TO_MB_KEYS[tag] + value = str(extra_tags.get(tag, '')).lower().strip() + if key == 'catno': + value = value.replace(' ', '') + if value: + criteria[key] = value # Abort if we have no search terms. if not any(criteria.values()): return try: - log.debug(u'Searching for MusicBrainz releases with: {!r}', criteria) + log.debug('Searching for MusicBrainz releases with: {!r}', criteria) res = musicbrainzngs.search_releases( limit=config['musicbrainz']['searchlimit'].get(int), **criteria) except musicbrainzngs.MusicBrainzError as exc: @@ -470,7 +548,7 @@ def _parse_id(s): no ID can be found, return None. """ # Find the first thing that looks like a UUID/MBID. - match = re.search(u'[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}', s) + match = re.search('[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}', s) if match: return match.group() @@ -480,19 +558,19 @@ def album_for_id(releaseid): object or None if the album is not found. May raise a MusicBrainzAPIError. """ - log.debug(u'Requesting MusicBrainz release {}', releaseid) + log.debug('Requesting MusicBrainz release {}', releaseid) albumid = _parse_id(releaseid) if not albumid: - log.debug(u'Invalid MBID ({0}).', releaseid) + log.debug('Invalid MBID ({0}).', releaseid) return try: res = musicbrainzngs.get_release_by_id(albumid, RELEASE_INCLUDES) except musicbrainzngs.ResponseError: - log.debug(u'Album ID match failed.') + log.debug('Album ID match failed.') return None except musicbrainzngs.MusicBrainzError as exc: - raise MusicBrainzAPIError(exc, u'get release by ID', albumid, + raise MusicBrainzAPIError(exc, 'get release by ID', albumid, traceback.format_exc()) return album_info(res['release']) @@ -503,14 +581,14 @@ def track_for_id(releaseid): """ trackid = _parse_id(releaseid) if not trackid: - log.debug(u'Invalid MBID ({0}).', releaseid) + log.debug('Invalid MBID ({0}).', releaseid) return try: res = musicbrainzngs.get_recording_by_id(trackid, TRACK_INCLUDES) except musicbrainzngs.ResponseError: - log.debug(u'Track ID match failed.') + log.debug('Track ID match failed.') return None except musicbrainzngs.MusicBrainzError as exc: - raise MusicBrainzAPIError(exc, u'get recording by ID', trackid, + raise MusicBrainzAPIError(exc, 'get recording by ID', trackid, traceback.format_exc()) return track_info(res['recording']) diff --git a/libs/common/beets/config_default.yaml b/libs/common/beets/config_default.yaml index 273f9423..74540891 100644 --- a/libs/common/beets/config_default.yaml +++ b/libs/common/beets/config_default.yaml @@ -7,6 +7,7 @@ import: move: no link: no hardlink: no + reflink: no delete: no resume: ask incremental: no @@ -44,10 +45,20 @@ replace: '^\s+': '' '^-': _ path_sep_replace: _ +drive_sep_replace: _ asciify_paths: false art_filename: cover max_filename_length: 0 +aunique: + keys: albumartist album + disambiguators: albumtype year label catalognum albumdisambig releasegroupdisambig + bracket: '[]' + +overwrite_null: + album: [] + track: [] + plugins: [] pluginpath: [] threaded: yes @@ -91,9 +102,12 @@ statefile: state.pickle musicbrainz: host: musicbrainz.org + https: no ratelimit: 1 ratelimit_interval: 1.0 searchlimit: 5 + extra_tags: [] + genres: no match: strong_rec_thresh: 0.04 @@ -129,6 +143,7 @@ match: ignored: [] required: [] ignored_media: [] + ignore_data_tracks: yes ignore_video_tracks: yes track_length_grace: 10 track_length_max: 30 diff --git a/libs/common/beets/dbcore/__init__.py b/libs/common/beets/dbcore/__init__.py index 689e7202..923c34ca 100644 --- a/libs/common/beets/dbcore/__init__.py +++ b/libs/common/beets/dbcore/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -16,7 +15,6 @@ """DBCore is an abstract database package that forms the basis for beets' Library. """ -from __future__ import division, absolute_import, print_function from .db import Model, Database from .query import Query, FieldQuery, MatchQuery, AndQuery, OrQuery diff --git a/libs/common/beets/dbcore/db.py b/libs/common/beets/dbcore/db.py index 0f4dc151..acd131be 100644 --- a/libs/common/beets/dbcore/db.py +++ b/libs/common/beets/dbcore/db.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,22 +14,21 @@ """The central Model and Database constructs for DBCore. """ -from __future__ import division, absolute_import, print_function import time import os +import re from collections import defaultdict import threading import sqlite3 import contextlib -import collections import beets -from beets.util.functemplate import Template +from beets.util import functemplate from beets.util import py3_path from beets.dbcore import types from .query import MatchQuery, NullSort, TrueQuery -import six +from collections.abc import Mapping class DBAccessError(Exception): @@ -42,20 +40,30 @@ class DBAccessError(Exception): """ -class FormattedMapping(collections.Mapping): +class FormattedMapping(Mapping): """A `dict`-like formatted view of a model. The accessor `mapping[key]` returns the formatted version of `model[key]` as a unicode string. + The `included_keys` parameter allows filtering the fields that are + returned. By default all fields are returned. Limiting to specific keys can + avoid expensive per-item database queries. + If `for_path` is true, all path separators in the formatted values are replaced. """ - def __init__(self, model, for_path=False): + ALL_KEYS = '*' + + def __init__(self, model, included_keys=ALL_KEYS, for_path=False): self.for_path = for_path self.model = model - self.model_keys = model.keys(True) + if included_keys == self.ALL_KEYS: + # Performance note: this triggers a database query. + self.model_keys = self.model.keys(True) + else: + self.model_keys = included_keys def __getitem__(self, key): if key in self.model_keys: @@ -72,7 +80,7 @@ class FormattedMapping(collections.Mapping): def get(self, key, default=None): if default is None: default = self.model._type(key).format(None) - return super(FormattedMapping, self).get(key, default) + return super().get(key, default) def _get_formatted(self, model, key): value = model._type(key).format(model.get(key)) @@ -81,6 +89,11 @@ class FormattedMapping(collections.Mapping): if self.for_path: sep_repl = beets.config['path_sep_replace'].as_str() + sep_drive = beets.config['drive_sep_replace'].as_str() + + if re.match(r'^\w:', value): + value = re.sub(r'(?<=^\w):', sep_drive, value) + for sep in (os.path.sep, os.path.altsep): if sep: value = value.replace(sep, sep_repl) @@ -88,11 +101,105 @@ class FormattedMapping(collections.Mapping): return value +class LazyConvertDict: + """Lazily convert types for attributes fetched from the database + """ + + def __init__(self, model_cls): + """Initialize the object empty + """ + self.data = {} + self.model_cls = model_cls + self._converted = {} + + def init(self, data): + """Set the base data that should be lazily converted + """ + self.data = data + + def _convert(self, key, value): + """Convert the attribute type according the the SQL type + """ + return self.model_cls._type(key).from_sql(value) + + def __setitem__(self, key, value): + """Set an attribute value, assume it's already converted + """ + self._converted[key] = value + + def __getitem__(self, key): + """Get an attribute value, converting the type on demand + if needed + """ + if key in self._converted: + return self._converted[key] + elif key in self.data: + value = self._convert(key, self.data[key]) + self._converted[key] = value + return value + + def __delitem__(self, key): + """Delete both converted and base data + """ + if key in self._converted: + del self._converted[key] + if key in self.data: + del self.data[key] + + def keys(self): + """Get a list of available field names for this object. + """ + return list(self._converted.keys()) + list(self.data.keys()) + + def copy(self): + """Create a copy of the object. + """ + new = self.__class__(self.model_cls) + new.data = self.data.copy() + new._converted = self._converted.copy() + return new + + # Act like a dictionary. + + def update(self, values): + """Assign all values in the given dict. + """ + for key, value in values.items(): + self[key] = value + + def items(self): + """Iterate over (key, value) pairs that this object contains. + Computed fields are not included. + """ + for key in self: + yield key, self[key] + + def get(self, key, default=None): + """Get the value for a given key or `default` if it does not + exist. + """ + if key in self: + return self[key] + else: + return default + + def __contains__(self, key): + """Determine whether `key` is an attribute on this object. + """ + return key in self.keys() + + def __iter__(self): + """Iterate over the available field names (excluding computed + fields). + """ + return iter(self.keys()) + + # Abstract base for model classes. -class Model(object): +class Model: """An abstract object representing an object in the database. Model - objects act like dictionaries (i.e., the allow subscript access like + objects act like dictionaries (i.e., they allow subscript access like ``obj['field']``). The same field set is available via attribute access as a shortcut (i.e., ``obj.field``). Three kinds of attributes are available: @@ -143,12 +250,22 @@ class Model(object): are subclasses of `Sort`. """ + _queries = {} + """Named queries that use a field-like `name:value` syntax but which + do not relate to any specific field. + """ + _always_dirty = False """By default, fields only become "dirty" when their value actually changes. Enabling this flag marks fields as dirty even when the new value is the same as the old value (e.g., `o.f = o.f`). """ + _revision = -1 + """A revision number from when the model was loaded from or written + to the database. + """ + @classmethod def _getters(cls): """Return a mapping from field names to getter functions. @@ -172,8 +289,8 @@ class Model(object): """ self._db = db self._dirty = set() - self._values_fixed = {} - self._values_flex = {} + self._values_fixed = LazyConvertDict(self) + self._values_flex = LazyConvertDict(self) # Initial contents. self.update(values) @@ -187,23 +304,25 @@ class Model(object): ordinary construction are bypassed. """ obj = cls(db) - for key, value in fixed_values.items(): - obj._values_fixed[key] = cls._type(key).from_sql(value) - for key, value in flex_values.items(): - obj._values_flex[key] = cls._type(key).from_sql(value) + + obj._values_fixed.init(fixed_values) + obj._values_flex.init(flex_values) + return obj def __repr__(self): - return '{0}({1})'.format( + return '{}({})'.format( type(self).__name__, - ', '.join('{0}={1!r}'.format(k, v) for k, v in dict(self).items()), + ', '.join(f'{k}={v!r}' for k, v in dict(self).items()), ) def clear_dirty(self): """Mark all fields as *clean* (i.e., not needing to be stored to - the database). + the database). Also update the revision. """ self._dirty = set() + if self._db: + self._revision = self._db.revision def _check_db(self, need_id=True): """Ensure that this object is associated with a database row: it @@ -212,10 +331,10 @@ class Model(object): """ if not self._db: raise ValueError( - u'{0} has no database'.format(type(self).__name__) + '{} has no database'.format(type(self).__name__) ) if need_id and not self.id: - raise ValueError(u'{0} has no id'.format(type(self).__name__)) + raise ValueError('{} has no id'.format(type(self).__name__)) def copy(self): """Create a copy of the model object. @@ -243,19 +362,32 @@ class Model(object): """ return cls._fields.get(key) or cls._types.get(key) or types.DEFAULT - def __getitem__(self, key): - """Get the value for a field. Raise a KeyError if the field is - not available. + def _get(self, key, default=None, raise_=False): + """Get the value for a field, or `default`. Alternatively, + raise a KeyError if the field is not available. """ getters = self._getters() if key in getters: # Computed. return getters[key](self) elif key in self._fields: # Fixed. - return self._values_fixed.get(key, self._type(key).null) + if key in self._values_fixed: + return self._values_fixed[key] + else: + return self._type(key).null elif key in self._values_flex: # Flexible. return self._values_flex[key] - else: + elif raise_: raise KeyError(key) + else: + return default + + get = _get + + def __getitem__(self, key): + """Get the value for a field. Raise a KeyError if the field is + not available. + """ + return self._get(key, raise_=True) def _setitem(self, key, value): """Assign the value for a field, return whether new and old value @@ -290,12 +422,12 @@ class Model(object): if key in self._values_flex: # Flexible. del self._values_flex[key] self._dirty.add(key) # Mark for dropping on store. + elif key in self._fields: # Fixed + setattr(self, key, self._type(key).null) elif key in self._getters(): # Computed. - raise KeyError(u'computed field {0} cannot be deleted'.format(key)) - elif key in self._fields: # Fixed. - raise KeyError(u'fixed field {0} cannot be deleted'.format(key)) + raise KeyError(f'computed field {key} cannot be deleted') else: - raise KeyError(u'no such field {0}'.format(key)) + raise KeyError(f'no such field {key}') def keys(self, computed=False): """Get a list of available field names for this object. The @@ -330,19 +462,10 @@ class Model(object): for key in self: yield key, self[key] - def get(self, key, default=None): - """Get the value for a given key or `default` if it does not - exist. - """ - if key in self: - return self[key] - else: - return default - def __contains__(self, key): """Determine whether `key` is an attribute on this object. """ - return key in self.keys(True) + return key in self.keys(computed=True) def __iter__(self): """Iterate over the available field names (excluding computed @@ -354,22 +477,22 @@ class Model(object): def __getattr__(self, key): if key.startswith('_'): - raise AttributeError(u'model has no attribute {0!r}'.format(key)) + raise AttributeError(f'model has no attribute {key!r}') else: try: return self[key] except KeyError: - raise AttributeError(u'no such field {0!r}'.format(key)) + raise AttributeError(f'no such field {key!r}') def __setattr__(self, key, value): if key.startswith('_'): - super(Model, self).__setattr__(key, value) + super().__setattr__(key, value) else: self[key] = value def __delattr__(self, key): if key.startswith('_'): - super(Model, self).__delattr__(key) + super().__delattr__(key) else: del self[key] @@ -398,7 +521,7 @@ class Model(object): with self._db.transaction() as tx: # Main table update. if assignments: - query = 'UPDATE {0} SET {1} WHERE id=?'.format( + query = 'UPDATE {} SET {} WHERE id=?'.format( self._table, assignments ) subvars.append(self.id) @@ -409,7 +532,7 @@ class Model(object): if key in self._dirty: self._dirty.remove(key) tx.mutate( - 'INSERT INTO {0} ' + 'INSERT INTO {} ' '(entity_id, key, value) ' 'VALUES (?, ?, ?);'.format(self._flex_table), (self.id, key, value), @@ -418,7 +541,7 @@ class Model(object): # Deleted flexible attributes. for key in self._dirty: tx.mutate( - 'DELETE FROM {0} ' + 'DELETE FROM {} ' 'WHERE entity_id=? AND key=?'.format(self._flex_table), (self.id, key) ) @@ -427,12 +550,18 @@ class Model(object): def load(self): """Refresh the object's metadata from the library database. + + If check_revision is true, the database is only queried loaded when a + transaction has been committed since the item was last loaded. """ self._check_db() + if not self._dirty and self._db.revision == self._revision: + # Exit early + return stored_obj = self._db._get(type(self), self.id) - assert stored_obj is not None, u"object {0} not in DB".format(self.id) - self._values_fixed = {} - self._values_flex = {} + assert stored_obj is not None, f"object {self.id} not in DB" + self._values_fixed = LazyConvertDict(self) + self._values_flex = LazyConvertDict(self) self.update(dict(stored_obj)) self.clear_dirty() @@ -442,11 +571,11 @@ class Model(object): self._check_db() with self._db.transaction() as tx: tx.mutate( - 'DELETE FROM {0} WHERE id=?'.format(self._table), + f'DELETE FROM {self._table} WHERE id=?', (self.id,) ) tx.mutate( - 'DELETE FROM {0} WHERE entity_id=?'.format(self._flex_table), + f'DELETE FROM {self._flex_table} WHERE entity_id=?', (self.id,) ) @@ -464,7 +593,7 @@ class Model(object): with self._db.transaction() as tx: new_id = tx.mutate( - 'INSERT INTO {0} DEFAULT VALUES'.format(self._table) + f'INSERT INTO {self._table} DEFAULT VALUES' ) self.id = new_id self.added = time.time() @@ -479,11 +608,11 @@ class Model(object): _formatter = FormattedMapping - def formatted(self, for_path=False): + def formatted(self, included_keys=_formatter.ALL_KEYS, for_path=False): """Get a mapping containing all values on this object formatted as human-readable unicode strings. """ - return self._formatter(self, for_path) + return self._formatter(self, included_keys, for_path) def evaluate_template(self, template, for_path=False): """Evaluate a template (a string or a `Template` object) using @@ -491,9 +620,9 @@ class Model(object): separators will be added to the template. """ # Perform substitution. - if isinstance(template, six.string_types): - template = Template(template) - return template.substitute(self.formatted(for_path), + if isinstance(template, str): + template = functemplate.template(template) + return template.substitute(self.formatted(for_path=for_path), self._template_funcs()) # Parsing. @@ -502,8 +631,8 @@ class Model(object): def _parse(cls, key, string): """Parse a string as a value for the given key. """ - if not isinstance(string, six.string_types): - raise TypeError(u"_parse() argument must be a string") + if not isinstance(string, str): + raise TypeError("_parse() argument must be a string") return cls._type(key).parse(string) @@ -515,11 +644,13 @@ class Model(object): # Database controller and supporting interfaces. -class Results(object): +class Results: """An item query result set. Iterating over the collection lazily constructs LibModel objects that reflect database rows. """ - def __init__(self, model_class, rows, db, query=None, sort=None): + + def __init__(self, model_class, rows, db, flex_rows, + query=None, sort=None): """Create a result set that will construct objects of type `model_class`. @@ -539,6 +670,7 @@ class Results(object): self.db = db self.query = query self.sort = sort + self.flex_rows = flex_rows # We keep a queue of rows we haven't yet consumed for # materialization. We preserve the original total number of @@ -560,6 +692,10 @@ class Results(object): a `Results` object a second time should be much faster than the first. """ + + # Index flexible attributes by the item ID, so we have easier access + flex_attrs = self._get_indexed_flex_attrs() + index = 0 # Position in the materialized objects. while index < len(self._objects) or self._rows: # Are there previously-materialized objects to produce? @@ -572,7 +708,7 @@ class Results(object): else: while self._rows: row = self._rows.pop(0) - obj = self._make_model(row) + obj = self._make_model(row, flex_attrs.get(row['id'], {})) # If there is a slow-query predicate, ensurer that the # object passes it. if not self.query or self.query.match(obj): @@ -594,20 +730,24 @@ class Results(object): # Objects are pre-sorted (i.e., by the database). return self._get_objects() - def _make_model(self, row): - # Get the flexible attributes for the object. - with self.db.transaction() as tx: - flex_rows = tx.query( - 'SELECT * FROM {0} WHERE entity_id=?'.format( - self.model_class._flex_table - ), - (row['id'],) - ) + def _get_indexed_flex_attrs(self): + """ Index flexible attributes by the entity id they belong to + """ + flex_values = {} + for row in self.flex_rows: + if row['entity_id'] not in flex_values: + flex_values[row['entity_id']] = {} + flex_values[row['entity_id']][row['key']] = row['value'] + + return flex_values + + def _make_model(self, row, flex_values={}): + """ Create a Model object for the given row + """ cols = dict(row) - values = dict((k, v) for (k, v) in cols.items() - if not k[:4] == 'flex') - flex_values = dict((row['key'], row['value']) for row in flex_rows) + values = {k: v for (k, v) in cols.items() + if not k[:4] == 'flex'} # Construct the Python object obj = self.model_class._awaken(self.db, values, flex_values) @@ -656,7 +796,7 @@ class Results(object): next(it) return next(it) except StopIteration: - raise IndexError(u'result index {0} out of range'.format(n)) + raise IndexError(f'result index {n} out of range') def get(self): """Return the first matching object, or None if no objects @@ -669,10 +809,16 @@ class Results(object): return None -class Transaction(object): +class Transaction: """A context manager for safe, concurrent access to the database. All SQL commands should be executed through a transaction. """ + + _mutated = False + """A flag storing whether a mutation has been executed in the + current transaction. + """ + def __init__(self, db): self.db = db @@ -694,12 +840,15 @@ class Transaction(object): entered but not yet exited transaction. If it is the last active transaction, the database updates are committed. """ + # Beware of races; currently secured by db._db_lock + self.db.revision += self._mutated with self.db._tx_stack() as stack: assert stack.pop() is self empty = not stack if empty: # Ending a "root" transaction. End the SQLite transaction. self.db._connection().commit() + self._mutated = False self.db._db_lock.release() def query(self, statement, subvals=()): @@ -715,7 +864,6 @@ class Transaction(object): """ try: cursor = self.db._connection().execute(statement, subvals) - return cursor.lastrowid except sqlite3.OperationalError as e: # In two specific cases, SQLite reports an error while accessing # the underlying database file. We surface these exceptions as @@ -725,26 +873,41 @@ class Transaction(object): raise DBAccessError(e.args[0]) else: raise + else: + self._mutated = True + return cursor.lastrowid def script(self, statements): """Execute a string containing multiple SQL statements.""" + # We don't know whether this mutates, but quite likely it does. + self._mutated = True self.db._connection().executescript(statements) -class Database(object): +class Database: """A container for Model objects that wraps an SQLite database as the backend. """ + _models = () """The Model subclasses representing tables in this database. """ + supports_extensions = hasattr(sqlite3.Connection, 'enable_load_extension') + """Whether or not the current version of SQLite supports extensions""" + + revision = 0 + """The current revision of the database. To be increased whenever + data is written in a transaction. + """ + def __init__(self, path, timeout=5.0): self.path = path self.timeout = timeout self._connections = {} self._tx_stacks = defaultdict(list) + self._extensions = [] # A lock to protect the _connections and _tx_stacks maps, which # both map thread IDs to private resources. @@ -794,6 +957,13 @@ class Database(object): py3_path(self.path), timeout=self.timeout ) + if self.supports_extensions: + conn.enable_load_extension(True) + + # Load any extension that are already loaded for other connections. + for path in self._extensions: + conn.load_extension(path) + # Access SELECT results like dictionaries. conn.row_factory = sqlite3.Row return conn @@ -822,6 +992,18 @@ class Database(object): """ return Transaction(self) + def load_extension(self, path): + """Load an SQLite extension into all open connections.""" + if not self.supports_extensions: + raise ValueError( + 'this sqlite3 installation does not support extensions') + + self._extensions.append(path) + + # Load the extension into every open connection. + for conn in self._connections.values(): + conn.load_extension(path) + # Schema setup and migration. def _make_table(self, table, fields): @@ -831,7 +1013,7 @@ class Database(object): # Get current schema. with self.transaction() as tx: rows = tx.query('PRAGMA table_info(%s)' % table) - current_fields = set([row[1] for row in rows]) + current_fields = {row[1] for row in rows} field_names = set(fields.keys()) if current_fields.issuperset(field_names): @@ -842,9 +1024,9 @@ class Database(object): # No table exists. columns = [] for name, typ in fields.items(): - columns.append('{0} {1}'.format(name, typ.sql)) - setup_sql = 'CREATE TABLE {0} ({1});\n'.format(table, - ', '.join(columns)) + columns.append(f'{name} {typ.sql}') + setup_sql = 'CREATE TABLE {} ({});\n'.format(table, + ', '.join(columns)) else: # Table exists does not match the field set. @@ -852,7 +1034,7 @@ class Database(object): for name, typ in fields.items(): if name in current_fields: continue - setup_sql += 'ALTER TABLE {0} ADD COLUMN {1} {2};\n'.format( + setup_sql += 'ALTER TABLE {} ADD COLUMN {} {};\n'.format( table, name, typ.sql ) @@ -888,17 +1070,31 @@ class Database(object): where, subvals = query.clause() order_by = sort.order_clause() - sql = ("SELECT * FROM {0} WHERE {1} {2}").format( + sql = ("SELECT * FROM {} WHERE {} {}").format( model_cls._table, where or '1', - "ORDER BY {0}".format(order_by) if order_by else '', + f"ORDER BY {order_by}" if order_by else '', + ) + + # Fetch flexible attributes for items matching the main query. + # Doing the per-item filtering in python is faster than issuing + # one query per item to sqlite. + flex_sql = (""" + SELECT * FROM {} WHERE entity_id IN + (SELECT id FROM {} WHERE {}); + """.format( + model_cls._flex_table, + model_cls._table, + where or '1', + ) ) with self.transaction() as tx: rows = tx.query(sql, subvals) + flex_rows = tx.query(flex_sql, subvals) return Results( - model_cls, rows, self, + model_cls, rows, self, flex_rows, None if where else query, # Slow query component. sort if sort.is_slow() else None, # Slow sort component. ) diff --git a/libs/common/beets/dbcore/query.py b/libs/common/beets/dbcore/query.py index 8fb64e20..96476a5b 100644 --- a/libs/common/beets/dbcore/query.py +++ b/libs/common/beets/dbcore/query.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,7 +14,6 @@ """The Query type hierarchy for DBCore. """ -from __future__ import division, absolute_import, print_function import re from operator import mul @@ -23,10 +21,6 @@ from beets import util from datetime import datetime, timedelta import unicodedata from functools import reduce -import six - -if not six.PY2: - buffer = memoryview # sqlite won't accept memoryview in python 2 class ParsingError(ValueError): @@ -44,8 +38,8 @@ class InvalidQueryError(ParsingError): def __init__(self, query, explanation): if isinstance(query, list): query = " ".join(query) - message = u"'{0}': {1}".format(query, explanation) - super(InvalidQueryError, self).__init__(message) + message = f"'{query}': {explanation}" + super().__init__(message) class InvalidQueryArgumentValueError(ParsingError): @@ -56,13 +50,13 @@ class InvalidQueryArgumentValueError(ParsingError): """ def __init__(self, what, expected, detail=None): - message = u"'{0}' is not {1}".format(what, expected) + message = f"'{what}' is not {expected}" if detail: - message = u"{0}: {1}".format(message, detail) - super(InvalidQueryArgumentValueError, self).__init__(message) + message = f"{message}: {detail}" + super().__init__(message) -class Query(object): +class Query: """An abstract class representing a query into the item database. """ @@ -82,7 +76,7 @@ class Query(object): raise NotImplementedError def __repr__(self): - return "{0.__class__.__name__}()".format(self) + return f"{self.__class__.__name__}()" def __eq__(self, other): return type(self) == type(other) @@ -129,7 +123,7 @@ class FieldQuery(Query): "{0.fast})".format(self)) def __eq__(self, other): - return super(FieldQuery, self).__eq__(other) and \ + return super().__eq__(other) and \ self.field == other.field and self.pattern == other.pattern def __hash__(self): @@ -151,17 +145,13 @@ class NoneQuery(FieldQuery): """A query that checks whether a field is null.""" def __init__(self, field, fast=True): - super(NoneQuery, self).__init__(field, None, fast) + super().__init__(field, None, fast) def col_clause(self): return self.field + " IS NULL", () - @classmethod - def match(cls, item): - try: - return item[cls.field] is None - except KeyError: - return True + def match(self, item): + return item.get(self.field) is None def __repr__(self): return "{0.__class__.__name__}({0.field!r}, {0.fast})".format(self) @@ -214,14 +204,14 @@ class RegexpQuery(StringFieldQuery): """ def __init__(self, field, pattern, fast=True): - super(RegexpQuery, self).__init__(field, pattern, fast) + super().__init__(field, pattern, fast) pattern = self._normalize(pattern) try: self.pattern = re.compile(self.pattern) except re.error as exc: # Invalid regular expression. raise InvalidQueryArgumentValueError(pattern, - u"a regular expression", + "a regular expression", format(exc)) @staticmethod @@ -242,8 +232,8 @@ class BooleanQuery(MatchQuery): """ def __init__(self, field, pattern, fast=True): - super(BooleanQuery, self).__init__(field, pattern, fast) - if isinstance(pattern, six.string_types): + super().__init__(field, pattern, fast) + if isinstance(pattern, str): self.pattern = util.str2bool(pattern) self.pattern = int(self.pattern) @@ -256,16 +246,16 @@ class BytesQuery(MatchQuery): """ def __init__(self, field, pattern): - super(BytesQuery, self).__init__(field, pattern) + super().__init__(field, pattern) # Use a buffer/memoryview representation of the pattern for SQLite # matching. This instructs SQLite to treat the blob as binary # rather than encoded Unicode. - if isinstance(self.pattern, (six.text_type, bytes)): - if isinstance(self.pattern, six.text_type): + if isinstance(self.pattern, (str, bytes)): + if isinstance(self.pattern, str): self.pattern = self.pattern.encode('utf-8') - self.buf_pattern = buffer(self.pattern) - elif isinstance(self.pattern, buffer): + self.buf_pattern = memoryview(self.pattern) + elif isinstance(self.pattern, memoryview): self.buf_pattern = self.pattern self.pattern = bytes(self.pattern) @@ -297,10 +287,10 @@ class NumericQuery(FieldQuery): try: return float(s) except ValueError: - raise InvalidQueryArgumentValueError(s, u"an int or a float") + raise InvalidQueryArgumentValueError(s, "an int or a float") def __init__(self, field, pattern, fast=True): - super(NumericQuery, self).__init__(field, pattern, fast) + super().__init__(field, pattern, fast) parts = pattern.split('..', 1) if len(parts) == 1: @@ -318,7 +308,7 @@ class NumericQuery(FieldQuery): if self.field not in item: return False value = item[self.field] - if isinstance(value, six.string_types): + if isinstance(value, str): value = self._convert(value) if self.point is not None: @@ -335,14 +325,14 @@ class NumericQuery(FieldQuery): return self.field + '=?', (self.point,) else: if self.rangemin is not None and self.rangemax is not None: - return (u'{0} >= ? AND {0} <= ?'.format(self.field), + return ('{0} >= ? AND {0} <= ?'.format(self.field), (self.rangemin, self.rangemax)) elif self.rangemin is not None: - return u'{0} >= ?'.format(self.field), (self.rangemin,) + return f'{self.field} >= ?', (self.rangemin,) elif self.rangemax is not None: - return u'{0} <= ?'.format(self.field), (self.rangemax,) + return f'{self.field} <= ?', (self.rangemax,) else: - return u'1', () + return '1', () class CollectionQuery(Query): @@ -387,7 +377,7 @@ class CollectionQuery(Query): return "{0.__class__.__name__}({0.subqueries!r})".format(self) def __eq__(self, other): - return super(CollectionQuery, self).__eq__(other) and \ + return super().__eq__(other) and \ self.subqueries == other.subqueries def __hash__(self): @@ -411,7 +401,7 @@ class AnyFieldQuery(CollectionQuery): subqueries = [] for field in self.fields: subqueries.append(cls(field, pattern, True)) - super(AnyFieldQuery, self).__init__(subqueries) + super().__init__(subqueries) def clause(self): return self.clause_with_joiner('or') @@ -427,7 +417,7 @@ class AnyFieldQuery(CollectionQuery): "{0.query_class.__name__})".format(self)) def __eq__(self, other): - return super(AnyFieldQuery, self).__eq__(other) and \ + return super().__eq__(other) and \ self.query_class == other.query_class def __hash__(self): @@ -453,7 +443,7 @@ class AndQuery(MutableCollectionQuery): return self.clause_with_joiner('and') def match(self, item): - return all([q.match(item) for q in self.subqueries]) + return all(q.match(item) for q in self.subqueries) class OrQuery(MutableCollectionQuery): @@ -463,7 +453,7 @@ class OrQuery(MutableCollectionQuery): return self.clause_with_joiner('or') def match(self, item): - return any([q.match(item) for q in self.subqueries]) + return any(q.match(item) for q in self.subqueries) class NotQuery(Query): @@ -477,7 +467,7 @@ class NotQuery(Query): def clause(self): clause, subvals = self.subquery.clause() if clause: - return 'not ({0})'.format(clause), subvals + return f'not ({clause})', subvals else: # If there is no clause, there is nothing to negate. All the logic # is handled by match() for slow queries. @@ -490,7 +480,7 @@ class NotQuery(Query): return "{0.__class__.__name__}({0.subquery!r})".format(self) def __eq__(self, other): - return super(NotQuery, self).__eq__(other) and \ + return super().__eq__(other) and \ self.subquery == other.subquery def __hash__(self): @@ -546,7 +536,7 @@ def _parse_periods(pattern): return (start, end) -class Period(object): +class Period: """A period of time given by a date, time and precision. Example: 2014-01-01 10:50:30 with precision 'month' represents all @@ -572,7 +562,7 @@ class Period(object): or "second"). """ if precision not in Period.precisions: - raise ValueError(u'Invalid precision {0}'.format(precision)) + raise ValueError(f'Invalid precision {precision}') self.date = date self.precision = precision @@ -653,10 +643,10 @@ class Period(object): elif 'second' == precision: return date + timedelta(seconds=1) else: - raise ValueError(u'unhandled precision {0}'.format(precision)) + raise ValueError(f'unhandled precision {precision}') -class DateInterval(object): +class DateInterval: """A closed-open interval of dates. A left endpoint of None means since the beginning of time. @@ -665,7 +655,7 @@ class DateInterval(object): def __init__(self, start, end): if start is not None and end is not None and not start < end: - raise ValueError(u"start date {0} is not before end date {1}" + raise ValueError("start date {} is not before end date {}" .format(start, end)) self.start = start self.end = end @@ -686,7 +676,7 @@ class DateInterval(object): return True def __str__(self): - return '[{0}, {1})'.format(self.start, self.end) + return f'[{self.start}, {self.end})' class DateQuery(FieldQuery): @@ -700,7 +690,7 @@ class DateQuery(FieldQuery): """ def __init__(self, field, pattern, fast=True): - super(DateQuery, self).__init__(field, pattern, fast) + super().__init__(field, pattern, fast) start, end = _parse_periods(pattern) self.interval = DateInterval.from_periods(start, end) @@ -759,12 +749,12 @@ class DurationQuery(NumericQuery): except ValueError: raise InvalidQueryArgumentValueError( s, - u"a M:SS string or a float") + "a M:SS string or a float") # Sorting. -class Sort(object): +class Sort: """An abstract class representing a sort operation for a query into the item database. """ @@ -851,13 +841,13 @@ class MultipleSort(Sort): return items def __repr__(self): - return 'MultipleSort({!r})'.format(self.sorts) + return f'MultipleSort({self.sorts!r})' def __hash__(self): return hash(tuple(self.sorts)) def __eq__(self, other): - return super(MultipleSort, self).__eq__(other) and \ + return super().__eq__(other) and \ self.sorts == other.sorts @@ -878,14 +868,14 @@ class FieldSort(Sort): def key(item): field_val = item.get(self.field, '') - if self.case_insensitive and isinstance(field_val, six.text_type): + if self.case_insensitive and isinstance(field_val, str): field_val = field_val.lower() return field_val return sorted(objs, key=key, reverse=not self.ascending) def __repr__(self): - return '<{0}: {1}{2}>'.format( + return '<{}: {}{}>'.format( type(self).__name__, self.field, '+' if self.ascending else '-', @@ -895,7 +885,7 @@ class FieldSort(Sort): return hash((self.field, self.ascending)) def __eq__(self, other): - return super(FieldSort, self).__eq__(other) and \ + return super().__eq__(other) and \ self.field == other.field and \ self.ascending == other.ascending @@ -913,7 +903,7 @@ class FixedFieldSort(FieldSort): 'ELSE {0} END)'.format(self.field) else: field = self.field - return "{0} {1}".format(field, order) + return f"{field} {order}" class SlowFieldSort(FieldSort): diff --git a/libs/common/beets/dbcore/queryparse.py b/libs/common/beets/dbcore/queryparse.py index bc9cc77e..3bf02e4d 100644 --- a/libs/common/beets/dbcore/queryparse.py +++ b/libs/common/beets/dbcore/queryparse.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,12 +14,10 @@ """Parsing of strings into DBCore queries. """ -from __future__ import division, absolute_import, print_function import re import itertools from . import query -import beets PARSE_QUERY_PART_REGEX = re.compile( # Non-capturing optional segment for the keyword. @@ -89,7 +86,7 @@ def parse_query_part(part, query_classes={}, prefixes={}, assert match # Regex should always match negate = bool(match.group(1)) key = match.group(2) - term = match.group(3).replace('\:', ':') + term = match.group(3).replace('\\:', ':') # Check whether there's a prefix in the query and use the # corresponding query type. @@ -119,12 +116,13 @@ def construct_query_part(model_cls, prefixes, query_part): if not query_part: return query.TrueQuery() - # Use `model_cls` to build up a map from field names to `Query` - # classes. + # Use `model_cls` to build up a map from field (or query) names to + # `Query` classes. query_classes = {} for k, t in itertools.chain(model_cls._fields.items(), model_cls._types.items()): query_classes[k] = t.query + query_classes.update(model_cls._queries) # Non-field queries. # Parse the string. key, pattern, query_class, negate = \ @@ -137,26 +135,27 @@ def construct_query_part(model_cls, prefixes, query_part): # The query type matches a specific field, but none was # specified. So we use a version of the query that matches # any field. - q = query.AnyFieldQuery(pattern, model_cls._search_fields, - query_class) - if negate: - return query.NotQuery(q) - else: - return q + out_query = query.AnyFieldQuery(pattern, model_cls._search_fields, + query_class) else: # Non-field query type. - if negate: - return query.NotQuery(query_class(pattern)) - else: - return query_class(pattern) + out_query = query_class(pattern) - # Otherwise, this must be a `FieldQuery`. Use the field name to - # construct the query object. - key = key.lower() - q = query_class(key.lower(), pattern, key in model_cls._fields) + # Field queries get constructed according to the name of the field + # they are querying. + elif issubclass(query_class, query.FieldQuery): + key = key.lower() + out_query = query_class(key.lower(), pattern, key in model_cls._fields) + + # Non-field (named) query. + else: + out_query = query_class(pattern) + + # Apply negation. if negate: - return query.NotQuery(q) - return q + return query.NotQuery(out_query) + else: + return out_query def query_from_strings(query_cls, model_cls, prefixes, query_parts): @@ -172,11 +171,13 @@ def query_from_strings(query_cls, model_cls, prefixes, query_parts): return query_cls(subqueries) -def construct_sort_part(model_cls, part): +def construct_sort_part(model_cls, part, case_insensitive=True): """Create a `Sort` from a single string criterion. `model_cls` is the `Model` being queried. `part` is a single string - ending in ``+`` or ``-`` indicating the sort. + ending in ``+`` or ``-`` indicating the sort. `case_insensitive` + indicates whether or not the sort should be performed in a case + sensitive manner. """ assert part, "part must be a field name and + or -" field = part[:-1] @@ -185,7 +186,6 @@ def construct_sort_part(model_cls, part): assert direction in ('+', '-'), "part must end with + or -" is_ascending = direction == '+' - case_insensitive = beets.config['sort_case_insensitive'].get(bool) if field in model_cls._sorts: sort = model_cls._sorts[field](model_cls, is_ascending, case_insensitive) @@ -197,21 +197,23 @@ def construct_sort_part(model_cls, part): return sort -def sort_from_strings(model_cls, sort_parts): +def sort_from_strings(model_cls, sort_parts, case_insensitive=True): """Create a `Sort` from a list of sort criteria (strings). """ if not sort_parts: sort = query.NullSort() elif len(sort_parts) == 1: - sort = construct_sort_part(model_cls, sort_parts[0]) + sort = construct_sort_part(model_cls, sort_parts[0], case_insensitive) else: sort = query.MultipleSort() for part in sort_parts: - sort.add_sort(construct_sort_part(model_cls, part)) + sort.add_sort(construct_sort_part(model_cls, part, + case_insensitive)) return sort -def parse_sorted_query(model_cls, parts, prefixes={}): +def parse_sorted_query(model_cls, parts, prefixes={}, + case_insensitive=True): """Given a list of strings, create the `Query` and `Sort` that they represent. """ @@ -222,8 +224,8 @@ def parse_sorted_query(model_cls, parts, prefixes={}): # Split up query in to comma-separated subqueries, each representing # an AndQuery, which need to be joined together in one OrQuery subquery_parts = [] - for part in parts + [u',']: - if part.endswith(u','): + for part in parts + [',']: + if part.endswith(','): # Ensure we can catch "foo, bar" as well as "foo , bar" last_subquery_part = part[:-1] if last_subquery_part: @@ -237,8 +239,8 @@ def parse_sorted_query(model_cls, parts, prefixes={}): else: # Sort parts (1) end in + or -, (2) don't have a field, and # (3) consist of more than just the + or -. - if part.endswith((u'+', u'-')) \ - and u':' not in part \ + if part.endswith(('+', '-')) \ + and ':' not in part \ and len(part) > 1: sort_parts.append(part) else: @@ -246,5 +248,5 @@ def parse_sorted_query(model_cls, parts, prefixes={}): # Avoid needlessly wrapping single statements in an OR q = query.OrQuery(query_parts) if len(query_parts) > 1 else query_parts[0] - s = sort_from_strings(model_cls, sort_parts) + s = sort_from_strings(model_cls, sort_parts, case_insensitive) return q, s diff --git a/libs/common/beets/dbcore/types.py b/libs/common/beets/dbcore/types.py index b909904b..40f6a080 100644 --- a/libs/common/beets/dbcore/types.py +++ b/libs/common/beets/dbcore/types.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,25 +14,20 @@ """Representation of type information for DBCore model fields. """ -from __future__ import division, absolute_import, print_function from . import query from beets.util import str2bool -import six - -if not six.PY2: - buffer = memoryview # sqlite won't accept memoryview in python 2 # Abstract base. -class Type(object): +class Type: """An object encapsulating the type of a model field. Includes information about how to store, query, format, and parse a given field. """ - sql = u'TEXT' + sql = 'TEXT' """The SQLite column type for the value. """ @@ -41,7 +35,7 @@ class Type(object): """The `Query` subclass to be used when querying the field. """ - model_type = six.text_type + model_type = str """The Python type that is used to represent the value in the model. The model is guaranteed to return a value of this type if the field @@ -63,11 +57,11 @@ class Type(object): value = self.null # `self.null` might be `None` if value is None: - value = u'' + value = '' if isinstance(value, bytes): value = value.decode('utf-8', 'ignore') - return six.text_type(value) + return str(value) def parse(self, string): """Parse a (possibly human-written) string and return the @@ -97,16 +91,16 @@ class Type(object): For fixed fields the type of `value` is determined by the column type affinity given in the `sql` property and the SQL to Python mapping of the database adapter. For more information see: - http://www.sqlite.org/datatype3.html + https://www.sqlite.org/datatype3.html https://docs.python.org/2/library/sqlite3.html#sqlite-and-python-types Flexible fields have the type affinity `TEXT`. This means the - `sql_value` is either a `buffer`/`memoryview` or a `unicode` object` + `sql_value` is either a `memoryview` or a `unicode` object` and the method must handle these in addition. """ - if isinstance(sql_value, buffer): + if isinstance(sql_value, memoryview): sql_value = bytes(sql_value).decode('utf-8', 'ignore') - if isinstance(sql_value, six.text_type): + if isinstance(sql_value, str): return self.parse(sql_value) else: return self.normalize(sql_value) @@ -127,10 +121,18 @@ class Default(Type): class Integer(Type): """A basic integer type. """ - sql = u'INTEGER' + sql = 'INTEGER' query = query.NumericQuery model_type = int + def normalize(self, value): + try: + return self.model_type(round(float(value))) + except ValueError: + return self.null + except TypeError: + return self.null + class PaddedInt(Integer): """An integer field that is formatted with a given number of digits, @@ -140,19 +142,25 @@ class PaddedInt(Integer): self.digits = digits def format(self, value): - return u'{0:0{1}d}'.format(value or 0, self.digits) + return '{0:0{1}d}'.format(value or 0, self.digits) + + +class NullPaddedInt(PaddedInt): + """Same as `PaddedInt`, but does not normalize `None` to `0.0`. + """ + null = None class ScaledInt(Integer): """An integer whose formatting operation scales the number by a constant and adds a suffix. Good for units with large magnitudes. """ - def __init__(self, unit, suffix=u''): + def __init__(self, unit, suffix=''): self.unit = unit self.suffix = suffix def format(self, value): - return u'{0}{1}'.format((value or 0) // self.unit, self.suffix) + return '{}{}'.format((value or 0) // self.unit, self.suffix) class Id(Integer): @@ -163,18 +171,22 @@ class Id(Integer): def __init__(self, primary=True): if primary: - self.sql = u'INTEGER PRIMARY KEY' + self.sql = 'INTEGER PRIMARY KEY' class Float(Type): - """A basic floating-point type. + """A basic floating-point type. The `digits` parameter specifies how + many decimal places to use in the human-readable representation. """ - sql = u'REAL' + sql = 'REAL' query = query.NumericQuery model_type = float + def __init__(self, digits=1): + self.digits = digits + def format(self, value): - return u'{0:.1f}'.format(value or 0.0) + return '{0:.{1}f}'.format(value or 0, self.digits) class NullFloat(Float): @@ -186,19 +198,25 @@ class NullFloat(Float): class String(Type): """A Unicode string type. """ - sql = u'TEXT' + sql = 'TEXT' query = query.SubstringQuery + def normalize(self, value): + if value is None: + return self.null + else: + return self.model_type(value) + class Boolean(Type): """A boolean type. """ - sql = u'INTEGER' + sql = 'INTEGER' query = query.BooleanQuery model_type = bool def format(self, value): - return six.text_type(bool(value)) + return str(bool(value)) def parse(self, string): return str2bool(string) diff --git a/libs/common/beets/importer.py b/libs/common/beets/importer.py index 4e4084ee..561cedd2 100644 --- a/libs/common/beets/importer.py +++ b/libs/common/beets/importer.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function """Provides the basic, interface-agnostic workflow for importing and autotagging music files. @@ -40,7 +38,7 @@ from beets import config from beets.util import pipeline, sorted_walk, ancestry, MoveOperation from beets.util import syspath, normpath, displayable_path from enum import Enum -from beets import mediafile +import mediafile action = Enum('action', ['SKIP', 'ASIS', 'TRACKS', 'APPLY', 'ALBUMS', 'RETAG']) @@ -75,7 +73,7 @@ def _open_state(): # unpickling, including ImportError. We use a catch-all # exception to avoid enumerating them all (the docs don't even have a # full list!). - log.debug(u'state file could not be read: {0}', exc) + log.debug('state file could not be read: {0}', exc) return {} @@ -84,8 +82,8 @@ def _save_state(state): try: with open(config['statefile'].as_filename(), 'wb') as f: pickle.dump(state, f) - except IOError as exc: - log.error(u'state file could not be written: {0}', exc) + except OSError as exc: + log.error('state file could not be written: {0}', exc) # Utilities for reading and writing the beets progress file, which @@ -174,10 +172,11 @@ def history_get(): # Abstract session class. -class ImportSession(object): +class ImportSession: """Controls an import action. Subclasses should implement methods to communicate with the user or otherwise make decisions. """ + def __init__(self, lib, loghandler, paths, query): """Create a session. `lib` is a Library object. `loghandler` is a logging.Handler. Either `paths` or `query` is non-null and indicates @@ -187,7 +186,7 @@ class ImportSession(object): self.logger = self._setup_logging(loghandler) self.paths = paths self.query = query - self._is_resuming = dict() + self._is_resuming = {} self._merged_items = set() self._merged_dirs = set() @@ -222,19 +221,31 @@ class ImportSession(object): iconfig['resume'] = False iconfig['incremental'] = False - # Copy, move, link, and hardlink are mutually exclusive. + if iconfig['reflink']: + iconfig['reflink'] = iconfig['reflink'] \ + .as_choice(['auto', True, False]) + + # Copy, move, reflink, link, and hardlink are mutually exclusive. if iconfig['move']: iconfig['copy'] = False iconfig['link'] = False iconfig['hardlink'] = False + iconfig['reflink'] = False elif iconfig['link']: iconfig['copy'] = False iconfig['move'] = False iconfig['hardlink'] = False + iconfig['reflink'] = False elif iconfig['hardlink']: iconfig['copy'] = False iconfig['move'] = False iconfig['link'] = False + iconfig['reflink'] = False + elif iconfig['reflink']: + iconfig['copy'] = False + iconfig['move'] = False + iconfig['link'] = False + iconfig['hardlink'] = False # Only delete when copying. if not iconfig['copy']: @@ -246,7 +257,7 @@ class ImportSession(object): """Log a message about a given album to the importer log. The status should reflect the reason the album couldn't be tagged. """ - self.logger.info(u'{0} {1}', status, displayable_path(paths)) + self.logger.info('{0} {1}', status, displayable_path(paths)) def log_choice(self, task, duplicate=False): """Logs the task's current choice if it should be logged. If @@ -257,17 +268,17 @@ class ImportSession(object): if duplicate: # Duplicate: log all three choices (skip, keep both, and trump). if task.should_remove_duplicates: - self.tag_log(u'duplicate-replace', paths) + self.tag_log('duplicate-replace', paths) elif task.choice_flag in (action.ASIS, action.APPLY): - self.tag_log(u'duplicate-keep', paths) + self.tag_log('duplicate-keep', paths) elif task.choice_flag is (action.SKIP): - self.tag_log(u'duplicate-skip', paths) + self.tag_log('duplicate-skip', paths) else: # Non-duplicate: log "skip" and "asis" choices. if task.choice_flag is action.ASIS: - self.tag_log(u'asis', paths) + self.tag_log('asis', paths) elif task.choice_flag is action.SKIP: - self.tag_log(u'skip', paths) + self.tag_log('skip', paths) def should_resume(self, path): raise NotImplementedError @@ -284,7 +295,7 @@ class ImportSession(object): def run(self): """Run the import task. """ - self.logger.info(u'import started {0}', time.asctime()) + self.logger.info('import started {0}', time.asctime()) self.set_config(config['import']) # Set up the pipeline. @@ -368,8 +379,8 @@ class ImportSession(object): """Mark paths and directories as merged for future reimport tasks. """ self._merged_items.update(paths) - dirs = set([os.path.dirname(path) if os.path.isfile(path) else path - for path in paths]) + dirs = {os.path.dirname(path) if os.path.isfile(path) else path + for path in paths} self._merged_dirs.update(dirs) def is_resuming(self, toppath): @@ -389,7 +400,7 @@ class ImportSession(object): # Either accept immediately or prompt for input to decide. if self.want_resume is True or \ self.should_resume(toppath): - log.warning(u'Resuming interrupted import of {0}', + log.warning('Resuming interrupted import of {0}', util.displayable_path(toppath)) self._is_resuming[toppath] = True else: @@ -399,11 +410,12 @@ class ImportSession(object): # The importer task class. -class BaseImportTask(object): +class BaseImportTask: """An abstract base class for importer tasks. Tasks flow through the importer pipeline. Each stage can update them. """ + def __init__(self, toppath, paths, items): """Create a task. The primary fields that define a task are: @@ -457,8 +469,9 @@ class ImportTask(BaseImportTask): * `finalize()` Update the import progress and cleanup the file system. """ + def __init__(self, toppath, paths, items): - super(ImportTask, self).__init__(toppath, paths, items) + super().__init__(toppath, paths, items) self.choice_flag = None self.cur_album = None self.cur_artist = None @@ -550,28 +563,34 @@ class ImportTask(BaseImportTask): def remove_duplicates(self, lib): duplicate_items = self.duplicate_items(lib) - log.debug(u'removing {0} old duplicated items', len(duplicate_items)) + log.debug('removing {0} old duplicated items', len(duplicate_items)) for item in duplicate_items: item.remove() if lib.directory in util.ancestry(item.path): - log.debug(u'deleting duplicate {0}', + log.debug('deleting duplicate {0}', util.displayable_path(item.path)) util.remove(item.path) util.prune_dirs(os.path.dirname(item.path), lib.directory) - def set_fields(self): + def set_fields(self, lib): """Sets the fields given at CLI or configuration to the specified - values. + values, for both the album and all its items. """ + items = self.imported_items() for field, view in config['import']['set_fields'].items(): value = view.get() - log.debug(u'Set field {1}={2} for {0}', + log.debug('Set field {1}={2} for {0}', displayable_path(self.paths), field, value) self.album[field] = value - self.album.store() + for item in items: + item[field] = value + with lib.transaction(): + for item in items: + item.store() + self.album.store() def finalize(self, session): """Save progress, clean up files, and emit plugin event. @@ -655,7 +674,7 @@ class ImportTask(BaseImportTask): return [] duplicates = [] - task_paths = set(i.path for i in self.items if i) + task_paths = {i.path for i in self.items if i} duplicate_query = dbcore.AndQuery(( dbcore.MatchQuery('albumartist', artist), dbcore.MatchQuery('album', album), @@ -665,7 +684,7 @@ class ImportTask(BaseImportTask): # Check whether the album paths are all present in the task # i.e. album is being completely re-imported by the task, # in which case it is not a duplicate (will be replaced). - album_paths = set(i.path for i in album.items()) + album_paths = {i.path for i in album.items()} if not (album_paths <= task_paths): duplicates.append(album) return duplicates @@ -707,7 +726,7 @@ class ImportTask(BaseImportTask): item.update(changes) def manipulate_files(self, operation=None, write=False, session=None): - """ Copy, move, link or hardlink (depending on `operation`) the files + """ Copy, move, link, hardlink or reflink (depending on `operation`) the files as well as write metadata. `operation` should be an instance of `util.MoveOperation`. @@ -754,6 +773,8 @@ class ImportTask(BaseImportTask): self.record_replaced(lib) self.remove_replaced(lib) self.album = lib.add_album(self.imported_items()) + if 'data_source' in self.imported_items()[0]: + self.album.data_source = self.imported_items()[0].data_source self.reimport_metadata(lib) def record_replaced(self, lib): @@ -772,7 +793,7 @@ class ImportTask(BaseImportTask): if (not dup_item.album_id or dup_item.album_id in replaced_album_ids): continue - replaced_album = dup_item.get_album() + replaced_album = dup_item._cached_album if replaced_album: replaced_album_ids.add(dup_item.album_id) self.replaced_albums[replaced_album.path] = replaced_album @@ -789,8 +810,8 @@ class ImportTask(BaseImportTask): self.album.artpath = replaced_album.artpath self.album.store() log.debug( - u'Reimported album: added {0}, flexible ' - u'attributes {1} from album {2} for {3}', + 'Reimported album: added {0}, flexible ' + 'attributes {1} from album {2} for {3}', self.album.added, replaced_album._values_flex.keys(), replaced_album.id, @@ -803,16 +824,16 @@ class ImportTask(BaseImportTask): if dup_item.added and dup_item.added != item.added: item.added = dup_item.added log.debug( - u'Reimported item added {0} ' - u'from item {1} for {2}', + 'Reimported item added {0} ' + 'from item {1} for {2}', item.added, dup_item.id, displayable_path(item.path) ) item.update(dup_item._values_flex) log.debug( - u'Reimported item flexible attributes {0} ' - u'from item {1} for {2}', + 'Reimported item flexible attributes {0} ' + 'from item {1} for {2}', dup_item._values_flex.keys(), dup_item.id, displayable_path(item.path) @@ -825,10 +846,10 @@ class ImportTask(BaseImportTask): """ for item in self.imported_items(): for dup_item in self.replaced_items[item]: - log.debug(u'Replacing item {0}: {1}', + log.debug('Replacing item {0}: {1}', dup_item.id, displayable_path(item.path)) dup_item.remove() - log.debug(u'{0} of {1} items replaced', + log.debug('{0} of {1} items replaced', sum(bool(l) for l in self.replaced_items.values()), len(self.imported_items())) @@ -866,7 +887,7 @@ class SingletonImportTask(ImportTask): """ def __init__(self, toppath, item): - super(SingletonImportTask, self).__init__(toppath, [item.path], [item]) + super().__init__(toppath, [item.path], [item]) self.item = item self.is_album = False self.paths = [item.path] @@ -932,13 +953,13 @@ class SingletonImportTask(ImportTask): def reload(self): self.item.load() - def set_fields(self): + def set_fields(self, lib): """Sets the fields given at CLI or configuration to the specified - values. + values, for the singleton item. """ for field, view in config['import']['set_fields'].items(): value = view.get() - log.debug(u'Set field {1}={2} for {0}', + log.debug('Set field {1}={2} for {0}', displayable_path(self.paths), field, value) @@ -959,7 +980,7 @@ class SentinelImportTask(ImportTask): """ def __init__(self, toppath, paths): - super(SentinelImportTask, self).__init__(toppath, paths, ()) + super().__init__(toppath, paths, ()) # TODO Remove the remaining attributes eventually self.should_remove_duplicates = False self.is_album = True @@ -1003,7 +1024,7 @@ class ArchiveImportTask(SentinelImportTask): """ def __init__(self, toppath): - super(ArchiveImportTask, self).__init__(toppath, ()) + super().__init__(toppath, ()) self.extracted = False @classmethod @@ -1032,14 +1053,20 @@ class ArchiveImportTask(SentinelImportTask): cls._handlers = [] from zipfile import is_zipfile, ZipFile cls._handlers.append((is_zipfile, ZipFile)) - from tarfile import is_tarfile, TarFile - cls._handlers.append((is_tarfile, TarFile)) + import tarfile + cls._handlers.append((tarfile.is_tarfile, tarfile.open)) try: from rarfile import is_rarfile, RarFile except ImportError: pass else: cls._handlers.append((is_rarfile, RarFile)) + try: + from py7zr import is_7zfile, SevenZipFile + except ImportError: + pass + else: + cls._handlers.append((is_7zfile, SevenZipFile)) return cls._handlers @@ -1047,7 +1074,7 @@ class ArchiveImportTask(SentinelImportTask): """Removes the temporary directory the archive was extracted to. """ if self.extracted: - log.debug(u'Removing extracted directory: {0}', + log.debug('Removing extracted directory: {0}', displayable_path(self.toppath)) shutil.rmtree(self.toppath) @@ -1059,9 +1086,9 @@ class ArchiveImportTask(SentinelImportTask): if path_test(util.py3_path(self.toppath)): break + extract_to = mkdtemp() + archive = handler_class(util.py3_path(self.toppath), mode='r') try: - extract_to = mkdtemp() - archive = handler_class(util.py3_path(self.toppath), mode='r') archive.extractall(extract_to) finally: archive.close() @@ -1069,10 +1096,11 @@ class ArchiveImportTask(SentinelImportTask): self.toppath = extract_to -class ImportTaskFactory(object): +class ImportTaskFactory: """Generate album and singleton import tasks for all media files indicated by a path. """ + def __init__(self, toppath, session): """Create a new task factory. @@ -1110,14 +1138,12 @@ class ImportTaskFactory(object): if self.session.config['singletons']: for path in paths: tasks = self._create(self.singleton(path)) - for task in tasks: - yield task + yield from tasks yield self.sentinel(dirs) else: tasks = self._create(self.album(paths, dirs)) - for task in tasks: - yield task + yield from tasks # Produce the final sentinel for this toppath to indicate that # it is finished. This is usually just a SentinelImportTask, but @@ -1165,7 +1191,7 @@ class ImportTaskFactory(object): """Return a `SingletonImportTask` for the music file. """ if self.session.already_imported(self.toppath, [path]): - log.debug(u'Skipping previously-imported path: {0}', + log.debug('Skipping previously-imported path: {0}', displayable_path(path)) self.skipped += 1 return None @@ -1186,10 +1212,10 @@ class ImportTaskFactory(object): return None if dirs is None: - dirs = list(set(os.path.dirname(p) for p in paths)) + dirs = list({os.path.dirname(p) for p in paths}) if self.session.already_imported(self.toppath, dirs): - log.debug(u'Skipping previously-imported path: {0}', + log.debug('Skipping previously-imported path: {0}', displayable_path(dirs)) self.skipped += 1 return None @@ -1219,22 +1245,22 @@ class ImportTaskFactory(object): if not (self.session.config['move'] or self.session.config['copy']): - log.warning(u"Archive importing requires either " - u"'copy' or 'move' to be enabled.") + log.warning("Archive importing requires either " + "'copy' or 'move' to be enabled.") return - log.debug(u'Extracting archive: {0}', + log.debug('Extracting archive: {0}', displayable_path(self.toppath)) archive_task = ArchiveImportTask(self.toppath) try: archive_task.extract() except Exception as exc: - log.error(u'extraction failed: {0}', exc) + log.error('extraction failed: {0}', exc) return # Now read albums from the extracted directory. self.toppath = archive_task.toppath - log.debug(u'Archive extracted to: {0}', self.toppath) + log.debug('Archive extracted to: {0}', self.toppath) return archive_task def read_item(self, path): @@ -1250,9 +1276,9 @@ class ImportTaskFactory(object): # Silently ignore non-music files. pass elif isinstance(exc.reason, mediafile.UnreadableFileError): - log.warning(u'unreadable file: {0}', displayable_path(path)) + log.warning('unreadable file: {0}', displayable_path(path)) else: - log.error(u'error reading {0}: {1}', + log.error('error reading {0}: {1}', displayable_path(path), exc) @@ -1291,17 +1317,16 @@ def read_tasks(session): # Generate tasks. task_factory = ImportTaskFactory(toppath, session) - for t in task_factory.tasks(): - yield t + yield from task_factory.tasks() skipped += task_factory.skipped if not task_factory.imported: - log.warning(u'No files imported from {0}', + log.warning('No files imported from {0}', displayable_path(toppath)) # Show skipped directories (due to incremental/resume). if skipped: - log.info(u'Skipped {0} paths.', skipped) + log.info('Skipped {0} paths.', skipped) def query_tasks(session): @@ -1319,7 +1344,7 @@ def query_tasks(session): else: # Search for albums. for album in session.lib.albums(session.query): - log.debug(u'yielding album {0}: {1} - {2}', + log.debug('yielding album {0}: {1} - {2}', album.id, album.albumartist, album.album) items = list(album.items()) _freshen_items(items) @@ -1342,7 +1367,7 @@ def lookup_candidates(session, task): return plugins.send('import_task_start', session=session, task=task) - log.debug(u'Looking up: {0}', displayable_path(task.paths)) + log.debug('Looking up: {0}', displayable_path(task.paths)) # Restrict the initial lookup to IDs specified by the user via the -m # option. Currently all the IDs are passed onto the tasks directly. @@ -1381,8 +1406,7 @@ def user_query(session, task): def emitter(task): for item in task.items: task = SingletonImportTask(task.toppath, item) - for new_task in task.handle_created(session): - yield new_task + yield from task.handle_created(session) yield SentinelImportTask(task.toppath, task.paths) return _extend_pipeline(emitter(task), @@ -1428,30 +1452,30 @@ def resolve_duplicates(session, task): if task.choice_flag in (action.ASIS, action.APPLY, action.RETAG): found_duplicates = task.find_duplicates(session.lib) if found_duplicates: - log.debug(u'found duplicates: {}'.format( + log.debug('found duplicates: {}'.format( [o.id for o in found_duplicates] )) # Get the default action to follow from config. duplicate_action = config['import']['duplicate_action'].as_choice({ - u'skip': u's', - u'keep': u'k', - u'remove': u'r', - u'merge': u'm', - u'ask': u'a', + 'skip': 's', + 'keep': 'k', + 'remove': 'r', + 'merge': 'm', + 'ask': 'a', }) - log.debug(u'default action for duplicates: {0}', duplicate_action) + log.debug('default action for duplicates: {0}', duplicate_action) - if duplicate_action == u's': + if duplicate_action == 's': # Skip new. task.set_choice(action.SKIP) - elif duplicate_action == u'k': + elif duplicate_action == 'k': # Keep both. Do nothing; leave the choice intact. pass - elif duplicate_action == u'r': + elif duplicate_action == 'r': # Remove old. task.should_remove_duplicates = True - elif duplicate_action == u'm': + elif duplicate_action == 'm': # Merge duplicates together task.should_merge_duplicates = True else: @@ -1471,7 +1495,7 @@ def import_asis(session, task): if task.skip: return - log.info(u'{}', displayable_path(task.paths)) + log.info('{}', displayable_path(task.paths)) task.set_choice(action.ASIS) apply_choice(session, task) @@ -1496,7 +1520,7 @@ def apply_choice(session, task): # because then the ``ImportTask`` won't have an `album` for which # it can set the fields. if config['import']['set_fields']: - task.set_fields() + task.set_fields(session.lib) @pipeline.mutator_stage @@ -1534,6 +1558,8 @@ def manipulate_files(session, task): operation = MoveOperation.LINK elif session.config['hardlink']: operation = MoveOperation.HARDLINK + elif session.config['reflink']: + operation = MoveOperation.REFLINK else: operation = None @@ -1552,11 +1578,11 @@ def log_files(session, task): """A coroutine (pipeline stage) to log each file to be imported. """ if isinstance(task, SingletonImportTask): - log.info(u'Singleton: {0}', displayable_path(task.item['path'])) + log.info('Singleton: {0}', displayable_path(task.item['path'])) elif task.items: - log.info(u'Album: {0}', displayable_path(task.paths[0])) + log.info('Album: {0}', displayable_path(task.paths[0])) for item in task.items: - log.info(u' {0}', displayable_path(item['path'])) + log.info(' {0}', displayable_path(item['path'])) def group_albums(session): diff --git a/libs/common/beets/library.py b/libs/common/beets/library.py index ba57407d..888836cd 100644 --- a/libs/common/beets/library.py +++ b/libs/common/beets/library.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,34 +14,30 @@ """The core data store and collection logic for beets. """ -from __future__ import division, absolute_import, print_function import os import sys import unicodedata import time import re -import six +import string +import shlex from beets import logging -from beets.mediafile import MediaFile, UnreadableFileError +from mediafile import MediaFile, UnreadableFileError from beets import plugins from beets import util from beets.util import bytestring_path, syspath, normpath, samefile, \ - MoveOperation -from beets.util.functemplate import Template + MoveOperation, lazy_property +from beets.util.functemplate import template, Template from beets import dbcore from beets.dbcore import types import beets # To use the SQLite "blob" type, it doesn't suffice to provide a byte -# string; SQLite treats that as encoded text. Wrapping it in a `buffer` or a -# `memoryview`, depending on the Python version, tells it that we -# actually mean non-text data. -if six.PY2: - BLOB_TYPE = buffer # noqa: F821 -else: - BLOB_TYPE = memoryview +# string; SQLite treats that as encoded text. Wrapping it in a +# `memoryview` tells it that we actually mean non-text data. +BLOB_TYPE = memoryview log = logging.getLogger('beets') @@ -64,7 +59,7 @@ class PathQuery(dbcore.FieldQuery): `case_sensitive` can be a bool or `None`, indicating that the behavior should depend on the filesystem. """ - super(PathQuery, self).__init__(field, pattern, fast) + super().__init__(field, pattern, fast) # By default, the case sensitivity depends on the filesystem # that the query path is located on. @@ -149,7 +144,7 @@ class PathType(types.Type): `bytes` objects, in keeping with the Unix filesystem abstraction. """ - sql = u'BLOB' + sql = 'BLOB' query = PathQuery model_type = bytes @@ -173,7 +168,7 @@ class PathType(types.Type): return normpath(bytestring_path(string)) def normalize(self, value): - if isinstance(value, six.text_type): + if isinstance(value, str): # Paths stored internally as encoded bytes. return bytestring_path(value) @@ -251,6 +246,7 @@ class SmartArtistSort(dbcore.query.Sort): """Sort by artist (either album artist or track artist), prioritizing the sort field over the raw field. """ + def __init__(self, model_cls, ascending=True, case_insensitive=True): self.album = model_cls is Album self.ascending = ascending @@ -266,12 +262,15 @@ class SmartArtistSort(dbcore.query.Sort): def sort(self, objs): if self.album: - field = lambda a: a.albumartist_sort or a.albumartist + def field(a): + return a.albumartist_sort or a.albumartist else: - field = lambda i: i.artist_sort or i.artist + def field(i): + return i.artist_sort or i.artist if self.case_insensitive: - key = lambda x: field(x).lower() + def key(x): + return field(x).lower() else: key = field return sorted(objs, key=key, reverse=not self.ascending) @@ -282,17 +281,17 @@ PF_KEY_DEFAULT = 'default' # Exceptions. -@six.python_2_unicode_compatible class FileOperationError(Exception): """Indicates an error when interacting with a file on disk. Possibilities include an unsupported media type, a permissions error, and an unhandled Mutagen exception. """ + def __init__(self, path, reason): """Create an exception describing an operation on the file at `path` with the underlying (chained) exception `reason`. """ - super(FileOperationError, self).__init__(path, reason) + super().__init__(path, reason) self.path = path self.reason = reason @@ -300,9 +299,9 @@ class FileOperationError(Exception): """Get a string representing the error. Describes both the underlying reason and the file path in question. """ - return u'{0}: {1}'.format( + return '{}: {}'.format( util.displayable_path(self.path), - six.text_type(self.reason) + str(self.reason) ) # define __str__ as text to avoid infinite loop on super() calls @@ -310,25 +309,24 @@ class FileOperationError(Exception): __str__ = text -@six.python_2_unicode_compatible class ReadError(FileOperationError): """An error while reading a file (i.e. in `Item.read`). """ + def __str__(self): - return u'error reading ' + super(ReadError, self).text() + return 'error reading ' + super().text() -@six.python_2_unicode_compatible class WriteError(FileOperationError): """An error while writing a file (i.e. in `Item.write`). """ + def __str__(self): - return u'error writing ' + super(WriteError, self).text() + return 'error writing ' + super().text() # Item and Album model classes. -@six.python_2_unicode_compatible class LibModel(dbcore.Model): """Shared concrete functionality for Items and Albums. """ @@ -343,21 +341,21 @@ class LibModel(dbcore.Model): return funcs def store(self, fields=None): - super(LibModel, self).store(fields) + super().store(fields) plugins.send('database_change', lib=self._db, model=self) def remove(self): - super(LibModel, self).remove() + super().remove() plugins.send('database_change', lib=self._db, model=self) def add(self, lib=None): - super(LibModel, self).add(lib) + super().add(lib) plugins.send('database_change', lib=self._db, model=self) def __format__(self, spec): if not spec: spec = beets.config[self._format_config_key].as_str() - assert isinstance(spec, six.text_type) + assert isinstance(spec, str) return self.evaluate_template(spec) def __str__(self): @@ -373,15 +371,42 @@ class FormattedItemMapping(dbcore.db.FormattedMapping): Album-level fields take precedence if `for_path` is true. """ - def __init__(self, item, for_path=False): - super(FormattedItemMapping, self).__init__(item, for_path) - self.album = item.get_album() - self.album_keys = [] + ALL_KEYS = '*' + + def __init__(self, item, included_keys=ALL_KEYS, for_path=False): + # We treat album and item keys specially here, + # so exclude transitive album keys from the model's keys. + super().__init__(item, included_keys=[], + for_path=for_path) + self.included_keys = included_keys + if included_keys == self.ALL_KEYS: + # Performance note: this triggers a database query. + self.model_keys = item.keys(computed=True, with_album=False) + else: + self.model_keys = included_keys + self.item = item + + @lazy_property + def all_keys(self): + return set(self.model_keys).union(self.album_keys) + + @lazy_property + def album_keys(self): + album_keys = [] if self.album: - for key in self.album.keys(True): - if key in Album.item_keys or key not in item._fields.keys(): - self.album_keys.append(key) - self.all_keys = set(self.model_keys).union(self.album_keys) + if self.included_keys == self.ALL_KEYS: + # Performance note: this triggers a database query. + for key in self.album.keys(computed=True): + if key in Album.item_keys \ + or key not in self.item._fields.keys(): + album_keys.append(key) + else: + album_keys = self.included_keys + return album_keys + + @property + def album(self): + return self.item._cached_album def _get(self, key): """Get the value for a key, either from the album or the item. @@ -397,19 +422,23 @@ class FormattedItemMapping(dbcore.db.FormattedMapping): raise KeyError(key) def __getitem__(self, key): - """Get the value for a key. Certain unset values are remapped. + """Get the value for a key. `artist` and `albumartist` + are fallback values for each other when not set. """ value = self._get(key) # `artist` and `albumartist` fields fall back to one another. # This is helpful in path formats when the album artist is unset # on as-is imports. - if key == 'artist' and not value: - return self._get('albumartist') - elif key == 'albumartist' and not value: - return self._get('artist') - else: - return value + try: + if key == 'artist' and not value: + return self._get('albumartist') + elif key == 'albumartist' and not value: + return self._get('artist') + except KeyError: + pass + + return value def __iter__(self): return iter(self.all_keys) @@ -422,74 +451,85 @@ class Item(LibModel): _table = 'items' _flex_table = 'item_attributes' _fields = { - 'id': types.PRIMARY_ID, - 'path': PathType(), + 'id': types.PRIMARY_ID, + 'path': PathType(), 'album_id': types.FOREIGN_ID, - 'title': types.STRING, - 'artist': types.STRING, - 'artist_sort': types.STRING, - 'artist_credit': types.STRING, - 'album': types.STRING, - 'albumartist': types.STRING, - 'albumartist_sort': types.STRING, - 'albumartist_credit': types.STRING, - 'genre': types.STRING, - 'lyricist': types.STRING, - 'composer': types.STRING, - 'composer_sort': types.STRING, - 'arranger': types.STRING, - 'grouping': types.STRING, - 'year': types.PaddedInt(4), - 'month': types.PaddedInt(2), - 'day': types.PaddedInt(2), - 'track': types.PaddedInt(2), - 'tracktotal': types.PaddedInt(2), - 'disc': types.PaddedInt(2), - 'disctotal': types.PaddedInt(2), - 'lyrics': types.STRING, - 'comments': types.STRING, - 'bpm': types.INTEGER, - 'comp': types.BOOLEAN, - 'mb_trackid': types.STRING, - 'mb_albumid': types.STRING, - 'mb_artistid': types.STRING, - 'mb_albumartistid': types.STRING, - 'mb_releasetrackid': types.STRING, - 'albumtype': types.STRING, - 'label': types.STRING, + 'title': types.STRING, + 'artist': types.STRING, + 'artist_sort': types.STRING, + 'artist_credit': types.STRING, + 'album': types.STRING, + 'albumartist': types.STRING, + 'albumartist_sort': types.STRING, + 'albumartist_credit': types.STRING, + 'genre': types.STRING, + 'style': types.STRING, + 'discogs_albumid': types.INTEGER, + 'discogs_artistid': types.INTEGER, + 'discogs_labelid': types.INTEGER, + 'lyricist': types.STRING, + 'composer': types.STRING, + 'composer_sort': types.STRING, + 'work': types.STRING, + 'mb_workid': types.STRING, + 'work_disambig': types.STRING, + 'arranger': types.STRING, + 'grouping': types.STRING, + 'year': types.PaddedInt(4), + 'month': types.PaddedInt(2), + 'day': types.PaddedInt(2), + 'track': types.PaddedInt(2), + 'tracktotal': types.PaddedInt(2), + 'disc': types.PaddedInt(2), + 'disctotal': types.PaddedInt(2), + 'lyrics': types.STRING, + 'comments': types.STRING, + 'bpm': types.INTEGER, + 'comp': types.BOOLEAN, + 'mb_trackid': types.STRING, + 'mb_albumid': types.STRING, + 'mb_artistid': types.STRING, + 'mb_albumartistid': types.STRING, + 'mb_releasetrackid': types.STRING, + 'trackdisambig': types.STRING, + 'albumtype': types.STRING, + 'albumtypes': types.STRING, + 'label': types.STRING, 'acoustid_fingerprint': types.STRING, - 'acoustid_id': types.STRING, - 'mb_releasegroupid': types.STRING, - 'asin': types.STRING, - 'catalognum': types.STRING, - 'script': types.STRING, - 'language': types.STRING, - 'country': types.STRING, - 'albumstatus': types.STRING, - 'media': types.STRING, - 'albumdisambig': types.STRING, - 'disctitle': types.STRING, - 'encoder': types.STRING, - 'rg_track_gain': types.NULL_FLOAT, - 'rg_track_peak': types.NULL_FLOAT, - 'rg_album_gain': types.NULL_FLOAT, - 'rg_album_peak': types.NULL_FLOAT, - 'r128_track_gain': types.PaddedInt(6), - 'r128_album_gain': types.PaddedInt(6), - 'original_year': types.PaddedInt(4), - 'original_month': types.PaddedInt(2), - 'original_day': types.PaddedInt(2), - 'initial_key': MusicalKey(), + 'acoustid_id': types.STRING, + 'mb_releasegroupid': types.STRING, + 'asin': types.STRING, + 'isrc': types.STRING, + 'catalognum': types.STRING, + 'script': types.STRING, + 'language': types.STRING, + 'country': types.STRING, + 'albumstatus': types.STRING, + 'media': types.STRING, + 'albumdisambig': types.STRING, + 'releasegroupdisambig': types.STRING, + 'disctitle': types.STRING, + 'encoder': types.STRING, + 'rg_track_gain': types.NULL_FLOAT, + 'rg_track_peak': types.NULL_FLOAT, + 'rg_album_gain': types.NULL_FLOAT, + 'rg_album_peak': types.NULL_FLOAT, + 'r128_track_gain': types.NullPaddedInt(6), + 'r128_album_gain': types.NullPaddedInt(6), + 'original_year': types.PaddedInt(4), + 'original_month': types.PaddedInt(2), + 'original_day': types.PaddedInt(2), + 'initial_key': MusicalKey(), - 'length': DurationType(), - 'bitrate': types.ScaledInt(1000, u'kbps'), - 'format': types.STRING, - 'samplerate': types.ScaledInt(1000, u'kHz'), - 'bitdepth': types.INTEGER, - 'channels': types.INTEGER, - 'mtime': DateType(), - 'added': DateType(), + 'length': DurationType(), + 'bitrate': types.ScaledInt(1000, 'kbps'), + 'format': types.STRING, + 'samplerate': types.ScaledInt(1000, 'kHz'), + 'bitdepth': types.INTEGER, + 'channels': types.INTEGER, + 'mtime': DateType(), + 'added': DateType(), } _search_fields = ('artist', 'title', 'comments', @@ -522,6 +562,29 @@ class Item(LibModel): _format_config_key = 'format_item' + __album = None + """Cached album object. Read-only.""" + + @property + def _cached_album(self): + """The Album object that this item belongs to, if any, or + None if the item is a singleton or is not associated with a + library. + The instance is cached and refreshed on access. + + DO NOT MODIFY! + If you want a copy to modify, use :meth:`get_album`. + """ + if not self.__album and self._db: + self.__album = self._db.get_album(self) + elif self.__album: + self.__album.load() + return self.__album + + @_cached_album.setter + def _cached_album(self, album): + self.__album = album + @classmethod def _getters(cls): getters = plugins.item_field_getters() @@ -544,27 +607,72 @@ class Item(LibModel): """ # Encode unicode paths and read buffers. if key == 'path': - if isinstance(value, six.text_type): + if isinstance(value, str): value = bytestring_path(value) elif isinstance(value, BLOB_TYPE): value = bytes(value) + elif key == 'album_id': + self._cached_album = None - changed = super(Item, self)._setitem(key, value) + changed = super()._setitem(key, value) if changed and key in MediaFile.fields(): self.mtime = 0 # Reset mtime on dirty. + def __getitem__(self, key): + """Get the value for a field, falling back to the album if + necessary. Raise a KeyError if the field is not available. + """ + try: + return super().__getitem__(key) + except KeyError: + if self._cached_album: + return self._cached_album[key] + raise + + def __repr__(self): + # This must not use `with_album=True`, because that might access + # the database. When debugging, that is not guaranteed to succeed, and + # can even deadlock due to the database lock. + return '{}({})'.format( + type(self).__name__, + ', '.join('{}={!r}'.format(k, self[k]) + for k in self.keys(with_album=False)), + ) + + def keys(self, computed=False, with_album=True): + """Get a list of available field names. `with_album` + controls whether the album's fields are included. + """ + keys = super().keys(computed=computed) + if with_album and self._cached_album: + keys = set(keys) + keys.update(self._cached_album.keys(computed=computed)) + keys = list(keys) + return keys + + def get(self, key, default=None, with_album=True): + """Get the value for a given key or `default` if it does not + exist. Set `with_album` to false to skip album fallback. + """ + try: + return self._get(key, default, raise_=with_album) + except KeyError: + if self._cached_album: + return self._cached_album.get(key, default) + return default + def update(self, values): """Set all key/value pairs in the mapping. If mtime is specified, it is not reset (as it might otherwise be). """ - super(Item, self).update(values) + super().update(values) if self.mtime == 0 and 'mtime' in values: self.mtime = values['mtime'] def clear(self): """Set all key/value pairs to None.""" - for key in self._media_fields: + for key in self._media_tag_fields: setattr(self, key, None) def get_album(self): @@ -598,7 +706,7 @@ class Item(LibModel): for key in self._media_fields: value = getattr(mediafile, key) - if isinstance(value, six.integer_types): + if isinstance(value, int): if value.bit_length() > 63: value = 0 self[key] = value @@ -609,7 +717,7 @@ class Item(LibModel): self.path = read_path - def write(self, path=None, tags=None): + def write(self, path=None, tags=None, id3v23=None): """Write the item's metadata to a media file. All fields in `_media_fields` are written to disk according to @@ -621,6 +729,9 @@ class Item(LibModel): `tags` is a dictionary of additional metadata the should be written to the file. (These tags need not be in `_media_fields`.) + `id3v23` will override the global `id3v23` config option if it is + set to something other than `None`. + Can raise either a `ReadError` or a `WriteError`. """ if path is None: @@ -628,6 +739,9 @@ class Item(LibModel): else: path = normpath(path) + if id3v23 is None: + id3v23 = beets.config['id3v23'].get(bool) + # Get the data to write to the file. item_tags = dict(self) item_tags = {k: v for k, v in item_tags.items() @@ -638,8 +752,7 @@ class Item(LibModel): # Open the file. try: - mediafile = MediaFile(syspath(path), - id3v23=beets.config['id3v23'].get(bool)) + mediafile = MediaFile(syspath(path), id3v23=id3v23) except UnreadableFileError as exc: raise ReadError(path, exc) @@ -655,17 +768,17 @@ class Item(LibModel): self.mtime = self.current_mtime() plugins.send('after_write', item=self, path=path) - def try_write(self, path=None, tags=None): + def try_write(self, *args, **kwargs): """Calls `write()` but catches and logs `FileOperationError` exceptions. Returns `False` an exception was caught and `True` otherwise. """ try: - self.write(path, tags) + self.write(*args, **kwargs) return True except FileOperationError as exc: - log.error(u"{0}", exc) + log.error("{0}", exc) return False def try_sync(self, write, move, with_album=True): @@ -685,7 +798,7 @@ class Item(LibModel): if move: # Check whether this file is inside the library directory. if self._db and self._db.directory in util.ancestry(self.path): - log.debug(u'moving {0} to synchronize path', + log.debug('moving {0} to synchronize path', util.displayable_path(self.path)) self.move(with_album=with_album) self.store() @@ -720,6 +833,16 @@ class Item(LibModel): util.hardlink(self.path, dest) plugins.send("item_hardlinked", item=self, source=self.path, destination=dest) + elif operation == MoveOperation.REFLINK: + util.reflink(self.path, dest, fallback=False) + plugins.send("item_reflinked", item=self, source=self.path, + destination=dest) + elif operation == MoveOperation.REFLINK_AUTO: + util.reflink(self.path, dest, fallback=True) + plugins.send("item_reflinked", item=self, source=self.path, + destination=dest) + else: + assert False, 'unknown MoveOperation' # Either copying or moving succeeded, so update the stored path. self.path = dest @@ -738,7 +861,7 @@ class Item(LibModel): try: return os.path.getsize(syspath(self.path)) except (OSError, Exception) as exc: - log.warning(u'could not get filesize: {0}', exc) + log.warning('could not get filesize: {0}', exc) return 0 # Model methods. @@ -748,7 +871,7 @@ class Item(LibModel): removed from disk. If `with_album`, then the item's album (if any) is removed if it the item was the last in the album. """ - super(Item, self).remove() + super().remove() # Remove the album if it is empty. if with_album: @@ -815,7 +938,7 @@ class Item(LibModel): # Templating. def destination(self, fragment=False, basedir=None, platform=None, - path_formats=None): + path_formats=None, replacements=None): """Returns the path in the library directory designated for the item (i.e., where the file ought to be). fragment makes this method return just the path fragment underneath the root library @@ -827,6 +950,8 @@ class Item(LibModel): platform = platform or sys.platform basedir = basedir or self._db.directory path_formats = path_formats or self._db.path_formats + if replacements is None: + replacements = self._db.replacements # Use a path format based on a query, falling back on the # default. @@ -844,11 +969,11 @@ class Item(LibModel): if query == PF_KEY_DEFAULT: break else: - assert False, u"no default path format" + assert False, "no default path format" if isinstance(path_format, Template): subpath_tmpl = path_format else: - subpath_tmpl = Template(path_format) + subpath_tmpl = template(path_format) # Evaluate the selected template. subpath = self.evaluate_template(subpath_tmpl, True) @@ -871,16 +996,16 @@ class Item(LibModel): maxlen = util.max_filename_length(self._db.directory) subpath, fellback = util.legalize_path( - subpath, self._db.replacements, maxlen, + subpath, replacements, maxlen, os.path.splitext(self.path)[1], fragment ) if fellback: # Print an error message if legalization fell back to # default replacements because of the maximum length. log.warning( - u'Fell back to default replacements when naming ' - u'file {}. Configure replacements to avoid lengthening ' - u'the filename.', + 'Fell back to default replacements when naming ' + 'file {}. Configure replacements to avoid lengthening ' + 'the filename.', subpath ) @@ -899,44 +1024,50 @@ class Album(LibModel): _flex_table = 'album_attributes' _always_dirty = True _fields = { - 'id': types.PRIMARY_ID, + 'id': types.PRIMARY_ID, 'artpath': PathType(True), - 'added': DateType(), + 'added': DateType(), - 'albumartist': types.STRING, - 'albumartist_sort': types.STRING, + 'albumartist': types.STRING, + 'albumartist_sort': types.STRING, 'albumartist_credit': types.STRING, - 'album': types.STRING, - 'genre': types.STRING, - 'year': types.PaddedInt(4), - 'month': types.PaddedInt(2), - 'day': types.PaddedInt(2), - 'disctotal': types.PaddedInt(2), - 'comp': types.BOOLEAN, - 'mb_albumid': types.STRING, - 'mb_albumartistid': types.STRING, - 'albumtype': types.STRING, - 'label': types.STRING, - 'mb_releasegroupid': types.STRING, - 'asin': types.STRING, - 'catalognum': types.STRING, - 'script': types.STRING, - 'language': types.STRING, - 'country': types.STRING, - 'albumstatus': types.STRING, - 'albumdisambig': types.STRING, - 'rg_album_gain': types.NULL_FLOAT, - 'rg_album_peak': types.NULL_FLOAT, - 'r128_album_gain': types.PaddedInt(6), - 'original_year': types.PaddedInt(4), - 'original_month': types.PaddedInt(2), - 'original_day': types.PaddedInt(2), + 'album': types.STRING, + 'genre': types.STRING, + 'style': types.STRING, + 'discogs_albumid': types.INTEGER, + 'discogs_artistid': types.INTEGER, + 'discogs_labelid': types.INTEGER, + 'year': types.PaddedInt(4), + 'month': types.PaddedInt(2), + 'day': types.PaddedInt(2), + 'disctotal': types.PaddedInt(2), + 'comp': types.BOOLEAN, + 'mb_albumid': types.STRING, + 'mb_albumartistid': types.STRING, + 'albumtype': types.STRING, + 'albumtypes': types.STRING, + 'label': types.STRING, + 'mb_releasegroupid': types.STRING, + 'asin': types.STRING, + 'catalognum': types.STRING, + 'script': types.STRING, + 'language': types.STRING, + 'country': types.STRING, + 'albumstatus': types.STRING, + 'albumdisambig': types.STRING, + 'releasegroupdisambig': types.STRING, + 'rg_album_gain': types.NULL_FLOAT, + 'rg_album_peak': types.NULL_FLOAT, + 'r128_album_gain': types.NullPaddedInt(6), + 'original_year': types.PaddedInt(4), + 'original_month': types.PaddedInt(2), + 'original_day': types.PaddedInt(2), } _search_fields = ('album', 'albumartist', 'genre') _types = { - 'path': PathType(), + 'path': PathType(), 'data_source': types.STRING, } @@ -952,6 +1083,10 @@ class Album(LibModel): 'albumartist_credit', 'album', 'genre', + 'style', + 'discogs_albumid', + 'discogs_artistid', + 'discogs_labelid', 'year', 'month', 'day', @@ -960,6 +1095,7 @@ class Album(LibModel): 'mb_albumid', 'mb_albumartistid', 'albumtype', + 'albumtypes', 'label', 'mb_releasegroupid', 'asin', @@ -969,6 +1105,7 @@ class Album(LibModel): 'country', 'albumstatus', 'albumdisambig', + 'releasegroupdisambig', 'rg_album_gain', 'rg_album_peak', 'r128_album_gain', @@ -1003,7 +1140,10 @@ class Album(LibModel): containing the album are also removed (recursively) if empty. Set with_items to False to avoid removing the album's items. """ - super(Album, self).remove() + super().remove() + + # Send a 'album_removed' signal to plugins + plugins.send('album_removed', album=self) # Delete art file. if delete: @@ -1027,12 +1167,18 @@ class Album(LibModel): if not old_art: return + if not os.path.exists(old_art): + log.error('removing reference to missing album art file {}', + util.displayable_path(old_art)) + self.artpath = None + return + new_art = self.art_destination(old_art) if new_art == old_art: return new_art = util.unique_path(new_art) - log.debug(u'moving album art {0} to {1}', + log.debug('moving album art {0} to {1}', util.displayable_path(old_art), util.displayable_path(new_art)) if operation == MoveOperation.MOVE: @@ -1044,6 +1190,12 @@ class Album(LibModel): util.link(old_art, new_art) elif operation == MoveOperation.HARDLINK: util.hardlink(old_art, new_art) + elif operation == MoveOperation.REFLINK: + util.reflink(old_art, new_art, fallback=False) + elif operation == MoveOperation.REFLINK_AUTO: + util.reflink(old_art, new_art, fallback=True) + else: + assert False, 'unknown MoveOperation' self.artpath = new_art def move(self, operation=MoveOperation.MOVE, basedir=None, store=True): @@ -1083,7 +1235,7 @@ class Album(LibModel): """ item = self.items().get() if not item: - raise ValueError(u'empty album') + raise ValueError('empty album for album id %d' % self.id) return os.path.dirname(item.path) def _albumtotal(self): @@ -1119,7 +1271,7 @@ class Album(LibModel): image = bytestring_path(image) item_dir = item_dir or self.item_dir() - filename_tmpl = Template( + filename_tmpl = template( beets.config['art_filename'].as_str()) subpath = self.evaluate_template(filename_tmpl, True) if beets.config['asciify_paths']: @@ -1180,7 +1332,7 @@ class Album(LibModel): track_updates[key] = self[key] with self._db.transaction(): - super(Album, self).store(fields) + super().store(fields) if track_updates: for item in self.items(): for key, value in track_updates.items(): @@ -1224,8 +1376,10 @@ def parse_query_parts(parts, model_cls): else: non_path_parts.append(s) + case_insensitive = beets.config['sort_case_insensitive'].get(bool) + query, sort = dbcore.parse_sorted_query( - model_cls, non_path_parts, prefixes + model_cls, non_path_parts, prefixes, case_insensitive ) # Add path queries to aggregate query. @@ -1243,10 +1397,10 @@ def parse_query_string(s, model_cls): The string is split into components using shell-like syntax. """ - message = u"Query is not unicode: {0!r}".format(s) - assert isinstance(s, six.text_type), message + message = f"Query is not unicode: {s!r}" + assert isinstance(s, str), message try: - parts = util.shlex_split(s) + parts = shlex.split(s) except ValueError as exc: raise dbcore.InvalidQueryError(s, exc) return parse_query_parts(parts, model_cls) @@ -1259,10 +1413,7 @@ def _sqlite_bytelower(bytestring): ``-DSQLITE_LIKE_DOESNT_MATCH_BLOBS``. See ``https://github.com/beetbox/beets/issues/2172`` for details. """ - if not six.PY2: - return bytestring.lower() - - return buffer(bytes(bytestring).lower()) # noqa: F821 + return bytestring.lower() # The Library: interface to the database. @@ -1278,7 +1429,7 @@ class Library(dbcore.Database): '$artist/$album/$track $title'),), replacements=None): timeout = beets.config['timeout'].as_number() - super(Library, self).__init__(path, timeout=timeout) + super().__init__(path, timeout=timeout) self.directory = bytestring_path(normpath(directory)) self.path_formats = path_formats @@ -1287,7 +1438,7 @@ class Library(dbcore.Database): self._memotable = {} # Used for template substitution performance. def _create_connection(self): - conn = super(Library, self)._create_connection() + conn = super()._create_connection() conn.create_function('bytelower', 1, _sqlite_bytelower) return conn @@ -1309,10 +1460,10 @@ class Library(dbcore.Database): be empty. """ if not items: - raise ValueError(u'need at least one item') + raise ValueError('need at least one item') # Create the album structure using metadata from the first item. - values = dict((key, items[0][key]) for key in Album.item_keys) + values = {key: items[0][key] for key in Album.item_keys} album = Album(self, **values) # Add the album structure and set the items' album_id fields. @@ -1337,7 +1488,7 @@ class Library(dbcore.Database): # Parse the query, if necessary. try: parsed_sort = None - if isinstance(query, six.string_types): + if isinstance(query, str): query, parsed_sort = parse_query_string(query, model_cls) elif isinstance(query, (list, tuple)): query, parsed_sort = parse_query_parts(query, model_cls) @@ -1349,7 +1500,7 @@ class Library(dbcore.Database): if parsed_sort and not isinstance(parsed_sort, dbcore.query.NullSort): sort = parsed_sort - return super(Library, self)._fetch( + return super()._fetch( model_cls, query, sort ) @@ -1408,7 +1559,7 @@ def _int_arg(s): return int(s.strip()) -class DefaultTemplateFunctions(object): +class DefaultTemplateFunctions: """A container class for the default functions provided to path templates. These functions are contained in an object to provide additional context to the functions -- specifically, the Item being @@ -1447,7 +1598,7 @@ class DefaultTemplateFunctions(object): @staticmethod def tmpl_title(s): """Convert a string to title case.""" - return s.title() + return string.capwords(s) @staticmethod def tmpl_left(s, chars): @@ -1460,7 +1611,7 @@ class DefaultTemplateFunctions(object): return s[-_int_arg(chars):] @staticmethod - def tmpl_if(condition, trueval, falseval=u''): + def tmpl_if(condition, trueval, falseval=''): """If ``condition`` is nonempty and nonzero, emit ``trueval``; otherwise, emit ``falseval`` (if provided). """ @@ -1502,18 +1653,25 @@ class DefaultTemplateFunctions(object): """ # Fast paths: no album, no item or library, or memoized value. if not self.item or not self.lib: - return u'' - if self.item.album_id is None: - return u'' - memokey = ('aunique', keys, disam, self.item.album_id) + return '' + + if isinstance(self.item, Item): + album_id = self.item.album_id + elif isinstance(self.item, Album): + album_id = self.item.id + + if album_id is None: + return '' + + memokey = ('aunique', keys, disam, album_id) memoval = self.lib._memotable.get(memokey) if memoval is not None: return memoval - keys = keys or 'albumartist album' - disam = disam or 'albumtype year label catalognum albumdisambig' + keys = keys or beets.config['aunique']['keys'].as_str() + disam = disam or beets.config['aunique']['disambiguators'].as_str() if bracket is None: - bracket = '[]' + bracket = beets.config['aunique']['bracket'].as_str() keys = keys.split() disam = disam.split() @@ -1522,32 +1680,34 @@ class DefaultTemplateFunctions(object): bracket_l = bracket[0] bracket_r = bracket[1] else: - bracket_l = u'' - bracket_r = u'' + bracket_l = '' + bracket_r = '' - album = self.lib.get_album(self.item) + album = self.lib.get_album(album_id) if not album: # Do nothing for singletons. - self.lib._memotable[memokey] = u'' - return u'' + self.lib._memotable[memokey] = '' + return '' # Find matching albums to disambiguate with. subqueries = [] for key in keys: value = album.get(key, '') - subqueries.append(dbcore.MatchQuery(key, value)) + # Use slow queries for flexible attributes. + fast = key in album.item_keys + subqueries.append(dbcore.MatchQuery(key, value, fast)) albums = self.lib.albums(dbcore.AndQuery(subqueries)) # If there's only one album to matching these details, then do # nothing. if len(albums) == 1: - self.lib._memotable[memokey] = u'' - return u'' + self.lib._memotable[memokey] = '' + return '' # Find the first disambiguator that distinguishes the albums. for disambiguator in disam: # Get the value for each album for the current field. - disam_values = set([a.get(disambiguator, '') for a in albums]) + disam_values = {a.get(disambiguator, '') for a in albums} # If the set of unique values is equal to the number of # albums in the disambiguation set, we're done -- this is @@ -1557,24 +1717,24 @@ class DefaultTemplateFunctions(object): else: # No disambiguator distinguished all fields. - res = u' {1}{0}{2}'.format(album.id, bracket_l, bracket_r) + res = f' {bracket_l}{album.id}{bracket_r}' self.lib._memotable[memokey] = res return res # Flatten disambiguation value into a string. - disam_value = album.formatted(True).get(disambiguator) + disam_value = album.formatted(for_path=True).get(disambiguator) # Return empty string if disambiguator is empty. if disam_value: - res = u' {1}{0}{2}'.format(disam_value, bracket_l, bracket_r) + res = f' {bracket_l}{disam_value}{bracket_r}' else: - res = u'' + res = '' self.lib._memotable[memokey] = res return res @staticmethod - def tmpl_first(s, count=1, skip=0, sep=u'; ', join_str=u'; '): + def tmpl_first(s, count=1, skip=0, sep='; ', join_str='; '): """ Gets the item(s) from x to y in a string separated by something and join then with something @@ -1588,7 +1748,7 @@ class DefaultTemplateFunctions(object): count = skip + int(count) return join_str.join(s.split(sep)[skip:count]) - def tmpl_ifdef(self, field, trueval=u'', falseval=u''): + def tmpl_ifdef(self, field, trueval='', falseval=''): """ If field exists return trueval or the field (default) otherwise, emit return falseval (if provided). @@ -1597,7 +1757,7 @@ class DefaultTemplateFunctions(object): :param falseval: The string if the condition is false :return: The string, based on condition """ - if self.item.formatted().get(field): + if field in self.item: return trueval if trueval else self.item.formatted().get(field) else: return falseval diff --git a/libs/common/beets/logging.py b/libs/common/beets/logging.py index d5ec7b73..4f004f8d 100644 --- a/libs/common/beets/logging.py +++ b/libs/common/beets/logging.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -21,13 +20,11 @@ that when getLogger(name) instantiates a logger that logger uses {}-style formatting. """ -from __future__ import division, absolute_import, print_function from copy import copy from logging import * # noqa import subprocess import threading -import six def logsafe(val): @@ -43,7 +40,7 @@ def logsafe(val): example. """ # Already Unicode. - if isinstance(val, six.text_type): + if isinstance(val, str): return val # Bytestring: needs decoding. @@ -57,7 +54,7 @@ def logsafe(val): # A "problem" object: needs a workaround. elif isinstance(val, subprocess.CalledProcessError): try: - return six.text_type(val) + return str(val) except UnicodeDecodeError: # An object with a broken __unicode__ formatter. Use __str__ # instead. @@ -74,7 +71,7 @@ class StrFormatLogger(Logger): instead of %-style formatting. """ - class _LogMessage(object): + class _LogMessage: def __init__(self, msg, args, kwargs): self.msg = msg self.args = args @@ -82,22 +79,23 @@ class StrFormatLogger(Logger): def __str__(self): args = [logsafe(a) for a in self.args] - kwargs = dict((k, logsafe(v)) for (k, v) in self.kwargs.items()) + kwargs = {k: logsafe(v) for (k, v) in self.kwargs.items()} return self.msg.format(*args, **kwargs) def _log(self, level, msg, args, exc_info=None, extra=None, **kwargs): """Log msg.format(*args, **kwargs)""" m = self._LogMessage(msg, args, kwargs) - return super(StrFormatLogger, self)._log(level, m, (), exc_info, extra) + return super()._log(level, m, (), exc_info, extra) class ThreadLocalLevelLogger(Logger): """A version of `Logger` whose level is thread-local instead of shared. """ + def __init__(self, name, level=NOTSET): self._thread_level = threading.local() self.default_level = NOTSET - super(ThreadLocalLevelLogger, self).__init__(name, level) + super().__init__(name, level) @property def level(self): diff --git a/libs/common/beets/mediafile.py b/libs/common/beets/mediafile.py index 32a32fe1..82bcc973 100644 --- a/libs/common/beets/mediafile.py +++ b/libs/common/beets/mediafile.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -13,2096 +12,15 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -"""Handles low-level interfacing for files' tags. Wraps Mutagen to -automatically detect file types and provide a unified interface for a -useful subset of music files' tags. -Usage: +import mediafile - >>> f = MediaFile('Lucy.mp3') - >>> f.title - u'Lucy in the Sky with Diamonds' - >>> f.artist = 'The Beatles' - >>> f.save() +import warnings +warnings.warn("beets.mediafile is deprecated; use mediafile instead") -A field will always return a reasonable value of the correct type, even -if no tag is present. If no value is available, the value will be false -(e.g., zero or the empty string). +# Import everything from the mediafile module into this module. +for key, value in mediafile.__dict__.items(): + if key not in ['__name__']: + globals()[key] = value -Internally ``MediaFile`` uses ``MediaField`` descriptors to access the -data from the tags. In turn ``MediaField`` uses a number of -``StorageStyle`` strategies to handle format specific logic. -""" -from __future__ import division, absolute_import, print_function - -import mutagen -import mutagen.id3 -import mutagen.mp4 -import mutagen.flac -import mutagen.asf - -import codecs -import datetime -import re -import base64 -import binascii -import math -import struct -import imghdr -import os -import traceback -import enum -import logging -import six - - -__all__ = ['UnreadableFileError', 'FileTypeError', 'MediaFile'] - -log = logging.getLogger(__name__) - -# Human-readable type names. -TYPES = { - 'mp3': 'MP3', - 'aac': 'AAC', - 'alac': 'ALAC', - 'ogg': 'OGG', - 'opus': 'Opus', - 'flac': 'FLAC', - 'ape': 'APE', - 'wv': 'WavPack', - 'mpc': 'Musepack', - 'asf': 'Windows Media', - 'aiff': 'AIFF', - 'dsf': 'DSD Stream File', -} - -PREFERRED_IMAGE_EXTENSIONS = {'jpeg': 'jpg'} - - -# Exceptions. - -class UnreadableFileError(Exception): - """Mutagen is not able to extract information from the file. - """ - def __init__(self, path, msg): - Exception.__init__(self, msg if msg else repr(path)) - - -class FileTypeError(UnreadableFileError): - """Reading this type of file is not supported. - - If passed the `mutagen_type` argument this indicates that the - mutagen type is not supported by `Mediafile`. - """ - def __init__(self, path, mutagen_type=None): - if mutagen_type is None: - msg = u'{0!r}: not in a recognized format'.format(path) - else: - msg = u'{0}: of mutagen type {1}'.format(repr(path), mutagen_type) - Exception.__init__(self, msg) - - -class MutagenError(UnreadableFileError): - """Raised when Mutagen fails unexpectedly---probably due to a bug. - """ - def __init__(self, path, mutagen_exc): - msg = u'{0}: {1}'.format(repr(path), mutagen_exc) - Exception.__init__(self, msg) - - -# Interacting with Mutagen. - -def mutagen_call(action, path, func, *args, **kwargs): - """Call a Mutagen function with appropriate error handling. - - `action` is a string describing what the function is trying to do, - and `path` is the relevant filename. The rest of the arguments - describe the callable to invoke. - - We require at least Mutagen 1.33, where `IOError` is *never* used, - neither for internal parsing errors *nor* for ordinary IO error - conditions such as a bad filename. Mutagen-specific parsing errors and IO - errors are reraised as `UnreadableFileError`. Other exceptions - raised inside Mutagen---i.e., bugs---are reraised as `MutagenError`. - """ - try: - return func(*args, **kwargs) - except mutagen.MutagenError as exc: - log.debug(u'%s failed: %s', action, six.text_type(exc)) - raise UnreadableFileError(path, six.text_type(exc)) - except Exception as exc: - # Isolate bugs in Mutagen. - log.debug(u'%s', traceback.format_exc()) - log.error(u'uncaught Mutagen exception in %s: %s', action, exc) - raise MutagenError(path, exc) - - -# Utility. - -def _safe_cast(out_type, val): - """Try to covert val to out_type but never raise an exception. If - the value can't be converted, then a sensible default value is - returned. out_type should be bool, int, or unicode; otherwise, the - value is just passed through. - """ - if val is None: - return None - - if out_type == int: - if isinstance(val, int) or isinstance(val, float): - # Just a number. - return int(val) - else: - # Process any other type as a string. - if isinstance(val, bytes): - val = val.decode('utf-8', 'ignore') - elif not isinstance(val, six.string_types): - val = six.text_type(val) - # Get a number from the front of the string. - match = re.match(r'[\+-]?[0-9]+', val.strip()) - return int(match.group(0)) if match else 0 - - elif out_type == bool: - try: - # Should work for strings, bools, ints: - return bool(int(val)) - except ValueError: - return False - - elif out_type == six.text_type: - if isinstance(val, bytes): - return val.decode('utf-8', 'ignore') - elif isinstance(val, six.text_type): - return val - else: - return six.text_type(val) - - elif out_type == float: - if isinstance(val, int) or isinstance(val, float): - return float(val) - else: - if isinstance(val, bytes): - val = val.decode('utf-8', 'ignore') - else: - val = six.text_type(val) - match = re.match(r'[\+-]?([0-9]+\.?[0-9]*|[0-9]*\.[0-9]+)', - val.strip()) - if match: - val = match.group(0) - if val: - return float(val) - return 0.0 - - else: - return val - - -# Image coding for ASF/WMA. - -def _unpack_asf_image(data): - """Unpack image data from a WM/Picture tag. Return a tuple - containing the MIME type, the raw image data, a type indicator, and - the image's description. - - This function is treated as "untrusted" and could throw all manner - of exceptions (out-of-bounds, etc.). We should clean this up - sometime so that the failure modes are well-defined. - """ - type, size = struct.unpack_from(' 0: - gain = math.log10(maxgain / 1000.0) * -10 - else: - # Invalid gain value found. - gain = 0.0 - - # SoundCheck stores peak values as the actual value of the sample, - # and again separately for the left and right channels. We need to - # convert this to a percentage of full scale, which is 32768 for a - # 16 bit sample. Once again, we play it safe by using the larger of - # the two values. - peak = max(soundcheck[6:8]) / 32768.0 - - return round(gain, 2), round(peak, 6) - - -def _sc_encode(gain, peak): - """Encode ReplayGain gain/peak values as a Sound Check string. - """ - # SoundCheck stores the peak value as the actual value of the - # sample, rather than the percentage of full scale that RG uses, so - # we do a simple conversion assuming 16 bit samples. - peak *= 32768.0 - - # SoundCheck stores absolute RMS values in some unknown units rather - # than the dB values RG uses. We can calculate these absolute values - # from the gain ratio using a reference value of 1000 units. We also - # enforce the maximum value here, which is equivalent to about - # -18.2dB. - g1 = int(min(round((10 ** (gain / -10)) * 1000), 65534)) - # Same as above, except our reference level is 2500 units. - g2 = int(min(round((10 ** (gain / -10)) * 2500), 65534)) - - # The purpose of these values are unknown, but they also seem to be - # unused so we just use zero. - uk = 0 - values = (g1, g1, g2, g2, uk, uk, int(peak), int(peak), uk, uk) - return (u' %08X' * 10) % values - - -# Cover art and other images. -def _imghdr_what_wrapper(data): - """A wrapper around imghdr.what to account for jpeg files that can only be - identified as such using their magic bytes - See #1545 - See https://github.com/file/file/blob/master/magic/Magdir/jpeg#L12 - """ - # imghdr.what returns none for jpegs with only the magic bytes, so - # _wider_test_jpeg is run in that case. It still returns None if it didn't - # match such a jpeg file. - return imghdr.what(None, h=data) or _wider_test_jpeg(data) - - -def _wider_test_jpeg(data): - """Test for a jpeg file following the UNIX file implementation which - uses the magic bytes rather than just looking for the bytes that - represent 'JFIF' or 'EXIF' at a fixed position. - """ - if data[:2] == b'\xff\xd8': - return 'jpeg' - - -def image_mime_type(data): - """Return the MIME type of the image data (a bytestring). - """ - # This checks for a jpeg file with only the magic bytes (unrecognized by - # imghdr.what). imghdr.what returns none for that type of file, so - # _wider_test_jpeg is run in that case. It still returns None if it didn't - # match such a jpeg file. - kind = _imghdr_what_wrapper(data) - if kind in ['gif', 'jpeg', 'png', 'tiff', 'bmp']: - return 'image/{0}'.format(kind) - elif kind == 'pgm': - return 'image/x-portable-graymap' - elif kind == 'pbm': - return 'image/x-portable-bitmap' - elif kind == 'ppm': - return 'image/x-portable-pixmap' - elif kind == 'xbm': - return 'image/x-xbitmap' - else: - return 'image/x-{0}'.format(kind) - - -def image_extension(data): - ext = _imghdr_what_wrapper(data) - return PREFERRED_IMAGE_EXTENSIONS.get(ext, ext) - - -class ImageType(enum.Enum): - """Indicates the kind of an `Image` stored in a file's tag. - """ - other = 0 - icon = 1 - other_icon = 2 - front = 3 - back = 4 - leaflet = 5 - media = 6 - lead_artist = 7 - artist = 8 - conductor = 9 - group = 10 - composer = 11 - lyricist = 12 - recording_location = 13 - recording_session = 14 - performance = 15 - screen_capture = 16 - fish = 17 - illustration = 18 - artist_logo = 19 - publisher_logo = 20 - - -class Image(object): - """Structure representing image data and metadata that can be - stored and retrieved from tags. - - The structure has four properties. - * ``data`` The binary data of the image - * ``desc`` An optional description of the image - * ``type`` An instance of `ImageType` indicating the kind of image - * ``mime_type`` Read-only property that contains the mime type of - the binary data - """ - def __init__(self, data, desc=None, type=None): - assert isinstance(data, bytes) - if desc is not None: - assert isinstance(desc, six.text_type) - self.data = data - self.desc = desc - if isinstance(type, int): - try: - type = list(ImageType)[type] - except IndexError: - log.debug(u"ignoring unknown image type index %s", type) - type = ImageType.other - self.type = type - - @property - def mime_type(self): - if self.data: - return image_mime_type(self.data) - - @property - def type_index(self): - if self.type is None: - # This method is used when a tag format requires the type - # index to be set, so we return "other" as the default value. - return 0 - return self.type.value - - -# StorageStyle classes describe strategies for accessing values in -# Mutagen file objects. - -class StorageStyle(object): - """A strategy for storing a value for a certain tag format (or set - of tag formats). This basic StorageStyle describes simple 1:1 - mapping from raw values to keys in a Mutagen file object; subclasses - describe more sophisticated translations or format-specific access - strategies. - - MediaFile uses a StorageStyle via three methods: ``get()``, - ``set()``, and ``delete()``. It passes a Mutagen file object to - each. - - Internally, the StorageStyle implements ``get()`` and ``set()`` - using two steps that may be overridden by subtypes. To get a value, - the StorageStyle first calls ``fetch()`` to retrieve the value - corresponding to a key and then ``deserialize()`` to convert the raw - Mutagen value to a consumable Python value. Similarly, to set a - field, we call ``serialize()`` to encode the value and then - ``store()`` to assign the result into the Mutagen object. - - Each StorageStyle type has a class-level `formats` attribute that is - a list of strings indicating the formats that the style applies to. - MediaFile only uses StorageStyles that apply to the correct type for - a given audio file. - """ - - formats = ['FLAC', 'OggOpus', 'OggTheora', 'OggSpeex', 'OggVorbis', - 'OggFlac', 'APEv2File', 'WavPack', 'Musepack', 'MonkeysAudio'] - """List of mutagen classes the StorageStyle can handle. - """ - - def __init__(self, key, as_type=six.text_type, suffix=None, - float_places=2): - """Create a basic storage strategy. Parameters: - - - `key`: The key on the Mutagen file object used to access the - field's data. - - `as_type`: The Python type that the value is stored as - internally (`unicode`, `int`, `bool`, or `bytes`). - - `suffix`: When `as_type` is a string type, append this before - storing the value. - - `float_places`: When the value is a floating-point number and - encoded as a string, the number of digits to store after the - decimal point. - """ - self.key = key - self.as_type = as_type - self.suffix = suffix - self.float_places = float_places - - # Convert suffix to correct string type. - if self.suffix and self.as_type is six.text_type \ - and not isinstance(self.suffix, six.text_type): - self.suffix = self.suffix.decode('utf-8') - - # Getter. - - def get(self, mutagen_file): - """Get the value for the field using this style. - """ - return self.deserialize(self.fetch(mutagen_file)) - - def fetch(self, mutagen_file): - """Retrieve the raw value of for this tag from the Mutagen file - object. - """ - try: - return mutagen_file[self.key][0] - except (KeyError, IndexError): - return None - - def deserialize(self, mutagen_value): - """Given a raw value stored on a Mutagen object, decode and - return the represented value. - """ - if self.suffix and isinstance(mutagen_value, six.text_type) \ - and mutagen_value.endswith(self.suffix): - return mutagen_value[:-len(self.suffix)] - else: - return mutagen_value - - # Setter. - - def set(self, mutagen_file, value): - """Assign the value for the field using this style. - """ - self.store(mutagen_file, self.serialize(value)) - - def store(self, mutagen_file, value): - """Store a serialized value in the Mutagen file object. - """ - mutagen_file[self.key] = [value] - - def serialize(self, value): - """Convert the external Python value to a type that is suitable for - storing in a Mutagen file object. - """ - if isinstance(value, float) and self.as_type is six.text_type: - value = u'{0:.{1}f}'.format(value, self.float_places) - value = self.as_type(value) - elif self.as_type is six.text_type: - if isinstance(value, bool): - # Store bools as 1/0 instead of True/False. - value = six.text_type(int(bool(value))) - elif isinstance(value, bytes): - value = value.decode('utf-8', 'ignore') - else: - value = six.text_type(value) - else: - value = self.as_type(value) - - if self.suffix: - value += self.suffix - - return value - - def delete(self, mutagen_file): - """Remove the tag from the file. - """ - if self.key in mutagen_file: - del mutagen_file[self.key] - - -class ListStorageStyle(StorageStyle): - """Abstract storage style that provides access to lists. - - The ListMediaField descriptor uses a ListStorageStyle via two - methods: ``get_list()`` and ``set_list()``. It passes a Mutagen file - object to each. - - Subclasses may overwrite ``fetch`` and ``store``. ``fetch`` must - return a (possibly empty) list and ``store`` receives a serialized - list of values as the second argument. - - The `serialize` and `deserialize` methods (from the base - `StorageStyle`) are still called with individual values. This class - handles packing and unpacking the values into lists. - """ - def get(self, mutagen_file): - """Get the first value in the field's value list. - """ - try: - return self.get_list(mutagen_file)[0] - except IndexError: - return None - - def get_list(self, mutagen_file): - """Get a list of all values for the field using this style. - """ - return [self.deserialize(item) for item in self.fetch(mutagen_file)] - - def fetch(self, mutagen_file): - """Get the list of raw (serialized) values. - """ - try: - return mutagen_file[self.key] - except KeyError: - return [] - - def set(self, mutagen_file, value): - """Set an individual value as the only value for the field using - this style. - """ - self.set_list(mutagen_file, [value]) - - def set_list(self, mutagen_file, values): - """Set all values for the field using this style. `values` - should be an iterable. - """ - self.store(mutagen_file, [self.serialize(value) for value in values]) - - def store(self, mutagen_file, values): - """Set the list of all raw (serialized) values for this field. - """ - mutagen_file[self.key] = values - - -class SoundCheckStorageStyleMixin(object): - """A mixin for storage styles that read and write iTunes SoundCheck - analysis values. The object must have an `index` field that - indicates which half of the gain/peak pair---0 or 1---the field - represents. - """ - def get(self, mutagen_file): - data = self.fetch(mutagen_file) - if data is not None: - return _sc_decode(data)[self.index] - - def set(self, mutagen_file, value): - data = self.fetch(mutagen_file) - if data is None: - gain_peak = [0, 0] - else: - gain_peak = list(_sc_decode(data)) - gain_peak[self.index] = value or 0 - data = self.serialize(_sc_encode(*gain_peak)) - self.store(mutagen_file, data) - - -class ASFStorageStyle(ListStorageStyle): - """A general storage style for Windows Media/ASF files. - """ - formats = ['ASF'] - - def deserialize(self, data): - if isinstance(data, mutagen.asf.ASFBaseAttribute): - data = data.value - return data - - -class MP4StorageStyle(StorageStyle): - """A general storage style for MPEG-4 tags. - """ - formats = ['MP4'] - - def serialize(self, value): - value = super(MP4StorageStyle, self).serialize(value) - if self.key.startswith('----:') and isinstance(value, six.text_type): - value = value.encode('utf-8') - return value - - -class MP4TupleStorageStyle(MP4StorageStyle): - """A style for storing values as part of a pair of numbers in an - MPEG-4 file. - """ - def __init__(self, key, index=0, **kwargs): - super(MP4TupleStorageStyle, self).__init__(key, **kwargs) - self.index = index - - def deserialize(self, mutagen_value): - items = mutagen_value or [] - packing_length = 2 - return list(items) + [0] * (packing_length - len(items)) - - def get(self, mutagen_file): - value = super(MP4TupleStorageStyle, self).get(mutagen_file)[self.index] - if value == 0: - # The values are always present and saved as integers. So we - # assume that "0" indicates it is not set. - return None - else: - return value - - def set(self, mutagen_file, value): - if value is None: - value = 0 - items = self.deserialize(self.fetch(mutagen_file)) - items[self.index] = int(value) - self.store(mutagen_file, items) - - def delete(self, mutagen_file): - if self.index == 0: - super(MP4TupleStorageStyle, self).delete(mutagen_file) - else: - self.set(mutagen_file, None) - - -class MP4ListStorageStyle(ListStorageStyle, MP4StorageStyle): - pass - - -class MP4SoundCheckStorageStyle(SoundCheckStorageStyleMixin, MP4StorageStyle): - def __init__(self, key, index=0, **kwargs): - super(MP4SoundCheckStorageStyle, self).__init__(key, **kwargs) - self.index = index - - -class MP4BoolStorageStyle(MP4StorageStyle): - """A style for booleans in MPEG-4 files. (MPEG-4 has an atom type - specifically for representing booleans.) - """ - def get(self, mutagen_file): - try: - return mutagen_file[self.key] - except KeyError: - return None - - def get_list(self, mutagen_file): - raise NotImplementedError(u'MP4 bool storage does not support lists') - - def set(self, mutagen_file, value): - mutagen_file[self.key] = value - - def set_list(self, mutagen_file, values): - raise NotImplementedError(u'MP4 bool storage does not support lists') - - -class MP4ImageStorageStyle(MP4ListStorageStyle): - """Store images as MPEG-4 image atoms. Values are `Image` objects. - """ - def __init__(self, **kwargs): - super(MP4ImageStorageStyle, self).__init__(key='covr', **kwargs) - - def deserialize(self, data): - return Image(data) - - def serialize(self, image): - if image.mime_type == 'image/png': - kind = mutagen.mp4.MP4Cover.FORMAT_PNG - elif image.mime_type == 'image/jpeg': - kind = mutagen.mp4.MP4Cover.FORMAT_JPEG - else: - raise ValueError(u'MP4 files only supports PNG and JPEG images') - return mutagen.mp4.MP4Cover(image.data, kind) - - -class MP3StorageStyle(StorageStyle): - """Store data in ID3 frames. - """ - formats = ['MP3', 'AIFF', 'DSF'] - - def __init__(self, key, id3_lang=None, **kwargs): - """Create a new ID3 storage style. `id3_lang` is the value for - the language field of newly created frames. - """ - self.id3_lang = id3_lang - super(MP3StorageStyle, self).__init__(key, **kwargs) - - def fetch(self, mutagen_file): - try: - return mutagen_file[self.key].text[0] - except (KeyError, IndexError): - return None - - def store(self, mutagen_file, value): - frame = mutagen.id3.Frames[self.key](encoding=3, text=[value]) - mutagen_file.tags.setall(self.key, [frame]) - - -class MP3PeopleStorageStyle(MP3StorageStyle): - """Store list of people in ID3 frames. - """ - def __init__(self, key, involvement='', **kwargs): - self.involvement = involvement - super(MP3PeopleStorageStyle, self).__init__(key, **kwargs) - - def store(self, mutagen_file, value): - frames = mutagen_file.tags.getall(self.key) - - # Try modifying in place. - found = False - for frame in frames: - if frame.encoding == mutagen.id3.Encoding.UTF8: - for pair in frame.people: - if pair[0].lower() == self.involvement.lower(): - pair[1] = value - found = True - - # Try creating a new frame. - if not found: - frame = mutagen.id3.Frames[self.key]( - encoding=mutagen.id3.Encoding.UTF8, - people=[[self.involvement, value]] - ) - mutagen_file.tags.add(frame) - - def fetch(self, mutagen_file): - for frame in mutagen_file.tags.getall(self.key): - for pair in frame.people: - if pair[0].lower() == self.involvement.lower(): - try: - return pair[1] - except IndexError: - return None - - -class MP3ListStorageStyle(ListStorageStyle, MP3StorageStyle): - """Store lists of data in multiple ID3 frames. - """ - def fetch(self, mutagen_file): - try: - return mutagen_file[self.key].text - except KeyError: - return [] - - def store(self, mutagen_file, values): - frame = mutagen.id3.Frames[self.key](encoding=3, text=values) - mutagen_file.tags.setall(self.key, [frame]) - - -class MP3UFIDStorageStyle(MP3StorageStyle): - """Store string data in a UFID ID3 frame with a particular owner. - """ - def __init__(self, owner, **kwargs): - self.owner = owner - super(MP3UFIDStorageStyle, self).__init__('UFID:' + owner, **kwargs) - - def fetch(self, mutagen_file): - try: - return mutagen_file[self.key].data - except KeyError: - return None - - def store(self, mutagen_file, value): - # This field type stores text data as encoded data. - assert isinstance(value, six.text_type) - value = value.encode('utf-8') - - frames = mutagen_file.tags.getall(self.key) - for frame in frames: - # Replace existing frame data. - if frame.owner == self.owner: - frame.data = value - else: - # New frame. - frame = mutagen.id3.UFID(owner=self.owner, data=value) - mutagen_file.tags.setall(self.key, [frame]) - - -class MP3DescStorageStyle(MP3StorageStyle): - """Store data in a TXXX (or similar) ID3 frame. The frame is - selected based its ``desc`` field. - """ - def __init__(self, desc=u'', key='TXXX', **kwargs): - assert isinstance(desc, six.text_type) - self.description = desc - super(MP3DescStorageStyle, self).__init__(key=key, **kwargs) - - def store(self, mutagen_file, value): - frames = mutagen_file.tags.getall(self.key) - if self.key != 'USLT': - value = [value] - - # Try modifying in place. - found = False - for frame in frames: - if frame.desc.lower() == self.description.lower(): - frame.text = value - frame.encoding = mutagen.id3.Encoding.UTF8 - found = True - - # Try creating a new frame. - if not found: - frame = mutagen.id3.Frames[self.key]( - desc=self.description, - text=value, - encoding=mutagen.id3.Encoding.UTF8, - ) - if self.id3_lang: - frame.lang = self.id3_lang - mutagen_file.tags.add(frame) - - def fetch(self, mutagen_file): - for frame in mutagen_file.tags.getall(self.key): - if frame.desc.lower() == self.description.lower(): - if self.key == 'USLT': - return frame.text - try: - return frame.text[0] - except IndexError: - return None - - def delete(self, mutagen_file): - found_frame = None - for frame in mutagen_file.tags.getall(self.key): - if frame.desc.lower() == self.description.lower(): - found_frame = frame - break - if found_frame is not None: - del mutagen_file[frame.HashKey] - - -class MP3SlashPackStorageStyle(MP3StorageStyle): - """Store value as part of pair that is serialized as a slash- - separated string. - """ - def __init__(self, key, pack_pos=0, **kwargs): - super(MP3SlashPackStorageStyle, self).__init__(key, **kwargs) - self.pack_pos = pack_pos - - def _fetch_unpacked(self, mutagen_file): - data = self.fetch(mutagen_file) - if data: - items = six.text_type(data).split('/') - else: - items = [] - packing_length = 2 - return list(items) + [None] * (packing_length - len(items)) - - def get(self, mutagen_file): - return self._fetch_unpacked(mutagen_file)[self.pack_pos] - - def set(self, mutagen_file, value): - items = self._fetch_unpacked(mutagen_file) - items[self.pack_pos] = value - if items[0] is None: - items[0] = '' - if items[1] is None: - items.pop() # Do not store last value - self.store(mutagen_file, '/'.join(map(six.text_type, items))) - - def delete(self, mutagen_file): - if self.pack_pos == 0: - super(MP3SlashPackStorageStyle, self).delete(mutagen_file) - else: - self.set(mutagen_file, None) - - -class MP3ImageStorageStyle(ListStorageStyle, MP3StorageStyle): - """Converts between APIC frames and ``Image`` instances. - - The `get_list` method inherited from ``ListStorageStyle`` returns a - list of ``Image``s. Similarly, the `set_list` method accepts a - list of ``Image``s as its ``values`` argument. - """ - def __init__(self): - super(MP3ImageStorageStyle, self).__init__(key='APIC') - self.as_type = bytes - - def deserialize(self, apic_frame): - """Convert APIC frame into Image.""" - return Image(data=apic_frame.data, desc=apic_frame.desc, - type=apic_frame.type) - - def fetch(self, mutagen_file): - return mutagen_file.tags.getall(self.key) - - def store(self, mutagen_file, frames): - mutagen_file.tags.setall(self.key, frames) - - def delete(self, mutagen_file): - mutagen_file.tags.delall(self.key) - - def serialize(self, image): - """Return an APIC frame populated with data from ``image``. - """ - assert isinstance(image, Image) - frame = mutagen.id3.Frames[self.key]() - frame.data = image.data - frame.mime = image.mime_type - frame.desc = image.desc or u'' - - # For compatibility with OS X/iTunes prefer latin-1 if possible. - # See issue #899 - try: - frame.desc.encode("latin-1") - except UnicodeEncodeError: - frame.encoding = mutagen.id3.Encoding.UTF16 - else: - frame.encoding = mutagen.id3.Encoding.LATIN1 - - frame.type = image.type_index - return frame - - -class MP3SoundCheckStorageStyle(SoundCheckStorageStyleMixin, - MP3DescStorageStyle): - def __init__(self, index=0, **kwargs): - super(MP3SoundCheckStorageStyle, self).__init__(**kwargs) - self.index = index - - -class ASFImageStorageStyle(ListStorageStyle): - """Store images packed into Windows Media/ASF byte array attributes. - Values are `Image` objects. - """ - formats = ['ASF'] - - def __init__(self): - super(ASFImageStorageStyle, self).__init__(key='WM/Picture') - - def deserialize(self, asf_picture): - mime, data, type, desc = _unpack_asf_image(asf_picture.value) - return Image(data, desc=desc, type=type) - - def serialize(self, image): - pic = mutagen.asf.ASFByteArrayAttribute() - pic.value = _pack_asf_image(image.mime_type, image.data, - type=image.type_index, - description=image.desc or u'') - return pic - - -class VorbisImageStorageStyle(ListStorageStyle): - """Store images in Vorbis comments. Both legacy COVERART fields and - modern METADATA_BLOCK_PICTURE tags are supported. Data is - base64-encoded. Values are `Image` objects. - """ - formats = ['OggOpus', 'OggTheora', 'OggSpeex', 'OggVorbis', - 'OggFlac'] - - def __init__(self): - super(VorbisImageStorageStyle, self).__init__( - key='metadata_block_picture' - ) - self.as_type = bytes - - def fetch(self, mutagen_file): - images = [] - if 'metadata_block_picture' not in mutagen_file: - # Try legacy COVERART tags. - if 'coverart' in mutagen_file: - for data in mutagen_file['coverart']: - images.append(Image(base64.b64decode(data))) - return images - for data in mutagen_file["metadata_block_picture"]: - try: - pic = mutagen.flac.Picture(base64.b64decode(data)) - except (TypeError, AttributeError): - continue - images.append(Image(data=pic.data, desc=pic.desc, - type=pic.type)) - return images - - def store(self, mutagen_file, image_data): - # Strip all art, including legacy COVERART. - if 'coverart' in mutagen_file: - del mutagen_file['coverart'] - if 'coverartmime' in mutagen_file: - del mutagen_file['coverartmime'] - super(VorbisImageStorageStyle, self).store(mutagen_file, image_data) - - def serialize(self, image): - """Turn a Image into a base64 encoded FLAC picture block. - """ - pic = mutagen.flac.Picture() - pic.data = image.data - pic.type = image.type_index - pic.mime = image.mime_type - pic.desc = image.desc or u'' - - # Encoding with base64 returns bytes on both Python 2 and 3. - # Mutagen requires the data to be a Unicode string, so we decode - # it before passing it along. - return base64.b64encode(pic.write()).decode('ascii') - - -class FlacImageStorageStyle(ListStorageStyle): - """Converts between ``mutagen.flac.Picture`` and ``Image`` instances. - """ - formats = ['FLAC'] - - def __init__(self): - super(FlacImageStorageStyle, self).__init__(key='') - - def fetch(self, mutagen_file): - return mutagen_file.pictures - - def deserialize(self, flac_picture): - return Image(data=flac_picture.data, desc=flac_picture.desc, - type=flac_picture.type) - - def store(self, mutagen_file, pictures): - """``pictures`` is a list of mutagen.flac.Picture instances. - """ - mutagen_file.clear_pictures() - for pic in pictures: - mutagen_file.add_picture(pic) - - def serialize(self, image): - """Turn a Image into a mutagen.flac.Picture. - """ - pic = mutagen.flac.Picture() - pic.data = image.data - pic.type = image.type_index - pic.mime = image.mime_type - pic.desc = image.desc or u'' - return pic - - def delete(self, mutagen_file): - """Remove all images from the file. - """ - mutagen_file.clear_pictures() - - -class APEv2ImageStorageStyle(ListStorageStyle): - """Store images in APEv2 tags. Values are `Image` objects. - """ - formats = ['APEv2File', 'WavPack', 'Musepack', 'MonkeysAudio', 'OptimFROG'] - - TAG_NAMES = { - ImageType.other: 'Cover Art (other)', - ImageType.icon: 'Cover Art (icon)', - ImageType.other_icon: 'Cover Art (other icon)', - ImageType.front: 'Cover Art (front)', - ImageType.back: 'Cover Art (back)', - ImageType.leaflet: 'Cover Art (leaflet)', - ImageType.media: 'Cover Art (media)', - ImageType.lead_artist: 'Cover Art (lead)', - ImageType.artist: 'Cover Art (artist)', - ImageType.conductor: 'Cover Art (conductor)', - ImageType.group: 'Cover Art (band)', - ImageType.composer: 'Cover Art (composer)', - ImageType.lyricist: 'Cover Art (lyricist)', - ImageType.recording_location: 'Cover Art (studio)', - ImageType.recording_session: 'Cover Art (recording)', - ImageType.performance: 'Cover Art (performance)', - ImageType.screen_capture: 'Cover Art (movie scene)', - ImageType.fish: 'Cover Art (colored fish)', - ImageType.illustration: 'Cover Art (illustration)', - ImageType.artist_logo: 'Cover Art (band logo)', - ImageType.publisher_logo: 'Cover Art (publisher logo)', - } - - def __init__(self): - super(APEv2ImageStorageStyle, self).__init__(key='') - - def fetch(self, mutagen_file): - images = [] - for cover_type, cover_tag in self.TAG_NAMES.items(): - try: - frame = mutagen_file[cover_tag] - text_delimiter_index = frame.value.find(b'\x00') - if text_delimiter_index > 0: - comment = frame.value[0:text_delimiter_index] - comment = comment.decode('utf-8', 'replace') - else: - comment = None - image_data = frame.value[text_delimiter_index + 1:] - images.append(Image(data=image_data, type=cover_type, - desc=comment)) - except KeyError: - pass - - return images - - def set_list(self, mutagen_file, values): - self.delete(mutagen_file) - - for image in values: - image_type = image.type or ImageType.other - comment = image.desc or '' - image_data = comment.encode('utf-8') + b'\x00' + image.data - cover_tag = self.TAG_NAMES[image_type] - mutagen_file[cover_tag] = image_data - - def delete(self, mutagen_file): - """Remove all images from the file. - """ - for cover_tag in self.TAG_NAMES.values(): - try: - del mutagen_file[cover_tag] - except KeyError: - pass - - -# MediaField is a descriptor that represents a single logical field. It -# aggregates several StorageStyles describing how to access the data for -# each file type. - -class MediaField(object): - """A descriptor providing access to a particular (abstract) metadata - field. - """ - def __init__(self, *styles, **kwargs): - """Creates a new MediaField. - - :param styles: `StorageStyle` instances that describe the strategy - for reading and writing the field in particular - formats. There must be at least one style for - each possible file format. - - :param out_type: the type of the value that should be returned when - getting this property. - - """ - self.out_type = kwargs.get('out_type', six.text_type) - self._styles = styles - - def styles(self, mutagen_file): - """Yields the list of storage styles of this field that can - handle the MediaFile's format. - """ - for style in self._styles: - if mutagen_file.__class__.__name__ in style.formats: - yield style - - def __get__(self, mediafile, owner=None): - out = None - for style in self.styles(mediafile.mgfile): - out = style.get(mediafile.mgfile) - if out: - break - return _safe_cast(self.out_type, out) - - def __set__(self, mediafile, value): - if value is None: - value = self._none_value() - for style in self.styles(mediafile.mgfile): - style.set(mediafile.mgfile, value) - - def __delete__(self, mediafile): - for style in self.styles(mediafile.mgfile): - style.delete(mediafile.mgfile) - - def _none_value(self): - """Get an appropriate "null" value for this field's type. This - is used internally when setting the field to None. - """ - if self.out_type == int: - return 0 - elif self.out_type == float: - return 0.0 - elif self.out_type == bool: - return False - elif self.out_type == six.text_type: - return u'' - - -class ListMediaField(MediaField): - """Property descriptor that retrieves a list of multiple values from - a tag. - - Uses ``get_list`` and set_list`` methods of its ``StorageStyle`` - strategies to do the actual work. - """ - def __get__(self, mediafile, _): - values = [] - for style in self.styles(mediafile.mgfile): - values.extend(style.get_list(mediafile.mgfile)) - return [_safe_cast(self.out_type, value) for value in values] - - def __set__(self, mediafile, values): - for style in self.styles(mediafile.mgfile): - style.set_list(mediafile.mgfile, values) - - def single_field(self): - """Returns a ``MediaField`` descriptor that gets and sets the - first item. - """ - options = {'out_type': self.out_type} - return MediaField(*self._styles, **options) - - -class DateField(MediaField): - """Descriptor that handles serializing and deserializing dates - - The getter parses value from tags into a ``datetime.date`` instance - and setter serializes such an instance into a string. - - For granular access to year, month, and day, use the ``*_field`` - methods to create corresponding `DateItemField`s. - """ - def __init__(self, *date_styles, **kwargs): - """``date_styles`` is a list of ``StorageStyle``s to store and - retrieve the whole date from. The ``year`` option is an - additional list of fallback styles for the year. The year is - always set on this style, but is only retrieved if the main - storage styles do not return a value. - """ - super(DateField, self).__init__(*date_styles) - year_style = kwargs.get('year', None) - if year_style: - self._year_field = MediaField(*year_style) - - def __get__(self, mediafile, owner=None): - year, month, day = self._get_date_tuple(mediafile) - if not year: - return None - try: - return datetime.date( - year, - month or 1, - day or 1 - ) - except ValueError: # Out of range values. - return None - - def __set__(self, mediafile, date): - if date is None: - self._set_date_tuple(mediafile, None, None, None) - else: - self._set_date_tuple(mediafile, date.year, date.month, date.day) - - def __delete__(self, mediafile): - super(DateField, self).__delete__(mediafile) - if hasattr(self, '_year_field'): - self._year_field.__delete__(mediafile) - - def _get_date_tuple(self, mediafile): - """Get a 3-item sequence representing the date consisting of a - year, month, and day number. Each number is either an integer or - None. - """ - # Get the underlying data and split on hyphens and slashes. - datestring = super(DateField, self).__get__(mediafile, None) - if isinstance(datestring, six.string_types): - datestring = re.sub(r'[Tt ].*$', '', six.text_type(datestring)) - items = re.split('[-/]', six.text_type(datestring)) - else: - items = [] - - # Ensure that we have exactly 3 components, possibly by - # truncating or padding. - items = items[:3] - if len(items) < 3: - items += [None] * (3 - len(items)) - - # Use year field if year is missing. - if not items[0] and hasattr(self, '_year_field'): - items[0] = self._year_field.__get__(mediafile) - - # Convert each component to an integer if possible. - items_ = [] - for item in items: - try: - items_.append(int(item)) - except (TypeError, ValueError): - items_.append(None) - return items_ - - def _set_date_tuple(self, mediafile, year, month=None, day=None): - """Set the value of the field given a year, month, and day - number. Each number can be an integer or None to indicate an - unset component. - """ - if year is None: - self.__delete__(mediafile) - return - - date = [u'{0:04d}'.format(int(year))] - if month: - date.append(u'{0:02d}'.format(int(month))) - if month and day: - date.append(u'{0:02d}'.format(int(day))) - date = map(six.text_type, date) - super(DateField, self).__set__(mediafile, u'-'.join(date)) - - if hasattr(self, '_year_field'): - self._year_field.__set__(mediafile, year) - - def year_field(self): - return DateItemField(self, 0) - - def month_field(self): - return DateItemField(self, 1) - - def day_field(self): - return DateItemField(self, 2) - - -class DateItemField(MediaField): - """Descriptor that gets and sets constituent parts of a `DateField`: - the month, day, or year. - """ - def __init__(self, date_field, item_pos): - self.date_field = date_field - self.item_pos = item_pos - - def __get__(self, mediafile, _): - return self.date_field._get_date_tuple(mediafile)[self.item_pos] - - def __set__(self, mediafile, value): - items = self.date_field._get_date_tuple(mediafile) - items[self.item_pos] = value - self.date_field._set_date_tuple(mediafile, *items) - - def __delete__(self, mediafile): - self.__set__(mediafile, None) - - -class CoverArtField(MediaField): - """A descriptor that provides access to the *raw image data* for the - cover image on a file. This is used for backwards compatibility: the - full `ImageListField` provides richer `Image` objects. - - When there are multiple images we try to pick the most likely to be a front - cover. - """ - def __init__(self): - pass - - def __get__(self, mediafile, _): - candidates = mediafile.images - if candidates: - return self.guess_cover_image(candidates).data - else: - return None - - @staticmethod - def guess_cover_image(candidates): - if len(candidates) == 1: - return candidates[0] - try: - return next(c for c in candidates if c.type == ImageType.front) - except StopIteration: - return candidates[0] - - def __set__(self, mediafile, data): - if data: - mediafile.images = [Image(data=data)] - else: - mediafile.images = [] - - def __delete__(self, mediafile): - delattr(mediafile, 'images') - - -class ImageListField(ListMediaField): - """Descriptor to access the list of images embedded in tags. - - The getter returns a list of `Image` instances obtained from - the tags. The setter accepts a list of `Image` instances to be - written to the tags. - """ - def __init__(self): - # The storage styles used here must implement the - # `ListStorageStyle` interface and get and set lists of - # `Image`s. - super(ImageListField, self).__init__( - MP3ImageStorageStyle(), - MP4ImageStorageStyle(), - ASFImageStorageStyle(), - VorbisImageStorageStyle(), - FlacImageStorageStyle(), - APEv2ImageStorageStyle(), - out_type=Image, - ) - - -# MediaFile is a collection of fields. - -class MediaFile(object): - """Represents a multimedia file on disk and provides access to its - metadata. - """ - def __init__(self, path, id3v23=False): - """Constructs a new `MediaFile` reflecting the file at path. May - throw `UnreadableFileError`. - - By default, MP3 files are saved with ID3v2.4 tags. You can use - the older ID3v2.3 standard by specifying the `id3v23` option. - """ - self.path = path - - self.mgfile = mutagen_call('open', path, mutagen.File, path) - - if self.mgfile is None: - # Mutagen couldn't guess the type - raise FileTypeError(path) - elif (type(self.mgfile).__name__ == 'M4A' or - type(self.mgfile).__name__ == 'MP4'): - info = self.mgfile.info - if info.codec and info.codec.startswith('alac'): - self.type = 'alac' - else: - self.type = 'aac' - elif (type(self.mgfile).__name__ == 'ID3' or - type(self.mgfile).__name__ == 'MP3'): - self.type = 'mp3' - elif type(self.mgfile).__name__ == 'FLAC': - self.type = 'flac' - elif type(self.mgfile).__name__ == 'OggOpus': - self.type = 'opus' - elif type(self.mgfile).__name__ == 'OggVorbis': - self.type = 'ogg' - elif type(self.mgfile).__name__ == 'MonkeysAudio': - self.type = 'ape' - elif type(self.mgfile).__name__ == 'WavPack': - self.type = 'wv' - elif type(self.mgfile).__name__ == 'Musepack': - self.type = 'mpc' - elif type(self.mgfile).__name__ == 'ASF': - self.type = 'asf' - elif type(self.mgfile).__name__ == 'AIFF': - self.type = 'aiff' - elif type(self.mgfile).__name__ == 'DSF': - self.type = 'dsf' - else: - raise FileTypeError(path, type(self.mgfile).__name__) - - # Add a set of tags if it's missing. - if self.mgfile.tags is None: - self.mgfile.add_tags() - - # Set the ID3v2.3 flag only for MP3s. - self.id3v23 = id3v23 and self.type == 'mp3' - - def save(self): - """Write the object's tags back to the file. May - throw `UnreadableFileError`. - """ - # Possibly save the tags to ID3v2.3. - kwargs = {} - if self.id3v23: - id3 = self.mgfile - if hasattr(id3, 'tags'): - # In case this is an MP3 object, not an ID3 object. - id3 = id3.tags - id3.update_to_v23() - kwargs['v2_version'] = 3 - - mutagen_call('save', self.path, self.mgfile.save, **kwargs) - - def delete(self): - """Remove the current metadata tag from the file. May - throw `UnreadableFileError`. - """ - mutagen_call('delete', self.path, self.mgfile.delete) - - # Convenient access to the set of available fields. - - @classmethod - def fields(cls): - """Get the names of all writable properties that reflect - metadata tags (i.e., those that are instances of - :class:`MediaField`). - """ - for property, descriptor in cls.__dict__.items(): - if isinstance(descriptor, MediaField): - if isinstance(property, bytes): - # On Python 2, class field names are bytes. This method - # produces text strings. - yield property.decode('utf8', 'ignore') - else: - yield property - - @classmethod - def _field_sort_name(cls, name): - """Get a sort key for a field name that determines the order - fields should be written in. - - Fields names are kept unchanged, unless they are instances of - :class:`DateItemField`, in which case `year`, `month`, and `day` - are replaced by `date0`, `date1`, and `date2`, respectively, to - make them appear in that order. - """ - if isinstance(cls.__dict__[name], DateItemField): - name = re.sub('year', 'date0', name) - name = re.sub('month', 'date1', name) - name = re.sub('day', 'date2', name) - return name - - @classmethod - def sorted_fields(cls): - """Get the names of all writable metadata fields, sorted in the - order that they should be written. - - This is a lexicographic order, except for instances of - :class:`DateItemField`, which are sorted in year-month-day - order. - """ - for property in sorted(cls.fields(), key=cls._field_sort_name): - yield property - - @classmethod - def readable_fields(cls): - """Get all metadata fields: the writable ones from - :meth:`fields` and also other audio properties. - """ - for property in cls.fields(): - yield property - for property in ('length', 'samplerate', 'bitdepth', 'bitrate', - 'channels', 'format'): - yield property - - @classmethod - def add_field(cls, name, descriptor): - """Add a field to store custom tags. - - :param name: the name of the property the field is accessed - through. It must not already exist on this class. - - :param descriptor: an instance of :class:`MediaField`. - """ - if not isinstance(descriptor, MediaField): - raise ValueError( - u'{0} must be an instance of MediaField'.format(descriptor)) - if name in cls.__dict__: - raise ValueError( - u'property "{0}" already exists on MediaField'.format(name)) - setattr(cls, name, descriptor) - - def update(self, dict): - """Set all field values from a dictionary. - - For any key in `dict` that is also a field to store tags the - method retrieves the corresponding value from `dict` and updates - the `MediaFile`. If a key has the value `None`, the - corresponding property is deleted from the `MediaFile`. - """ - for field in self.sorted_fields(): - if field in dict: - if dict[field] is None: - delattr(self, field) - else: - setattr(self, field, dict[field]) - - # Field definitions. - - title = MediaField( - MP3StorageStyle('TIT2'), - MP4StorageStyle('\xa9nam'), - StorageStyle('TITLE'), - ASFStorageStyle('Title'), - ) - artist = MediaField( - MP3StorageStyle('TPE1'), - MP4StorageStyle('\xa9ART'), - StorageStyle('ARTIST'), - ASFStorageStyle('Author'), - ) - album = MediaField( - MP3StorageStyle('TALB'), - MP4StorageStyle('\xa9alb'), - StorageStyle('ALBUM'), - ASFStorageStyle('WM/AlbumTitle'), - ) - genres = ListMediaField( - MP3ListStorageStyle('TCON'), - MP4ListStorageStyle('\xa9gen'), - ListStorageStyle('GENRE'), - ASFStorageStyle('WM/Genre'), - ) - genre = genres.single_field() - - lyricist = MediaField( - MP3StorageStyle('TEXT'), - MP4StorageStyle('----:com.apple.iTunes:LYRICIST'), - StorageStyle('LYRICIST'), - ASFStorageStyle('WM/Writer'), - ) - composer = MediaField( - MP3StorageStyle('TCOM'), - MP4StorageStyle('\xa9wrt'), - StorageStyle('COMPOSER'), - ASFStorageStyle('WM/Composer'), - ) - composer_sort = MediaField( - MP3StorageStyle('TSOC'), - MP4StorageStyle('soco'), - StorageStyle('COMPOSERSORT'), - ASFStorageStyle('WM/Composersortorder'), - ) - arranger = MediaField( - MP3PeopleStorageStyle('TIPL', involvement='arranger'), - MP4StorageStyle('----:com.apple.iTunes:Arranger'), - StorageStyle('ARRANGER'), - ASFStorageStyle('beets/Arranger'), - ) - - grouping = MediaField( - MP3StorageStyle('TIT1'), - MP4StorageStyle('\xa9grp'), - StorageStyle('GROUPING'), - ASFStorageStyle('WM/ContentGroupDescription'), - ) - track = MediaField( - MP3SlashPackStorageStyle('TRCK', pack_pos=0), - MP4TupleStorageStyle('trkn', index=0), - StorageStyle('TRACK'), - StorageStyle('TRACKNUMBER'), - ASFStorageStyle('WM/TrackNumber'), - out_type=int, - ) - tracktotal = MediaField( - MP3SlashPackStorageStyle('TRCK', pack_pos=1), - MP4TupleStorageStyle('trkn', index=1), - StorageStyle('TRACKTOTAL'), - StorageStyle('TRACKC'), - StorageStyle('TOTALTRACKS'), - ASFStorageStyle('TotalTracks'), - out_type=int, - ) - disc = MediaField( - MP3SlashPackStorageStyle('TPOS', pack_pos=0), - MP4TupleStorageStyle('disk', index=0), - StorageStyle('DISC'), - StorageStyle('DISCNUMBER'), - ASFStorageStyle('WM/PartOfSet'), - out_type=int, - ) - disctotal = MediaField( - MP3SlashPackStorageStyle('TPOS', pack_pos=1), - MP4TupleStorageStyle('disk', index=1), - StorageStyle('DISCTOTAL'), - StorageStyle('DISCC'), - StorageStyle('TOTALDISCS'), - ASFStorageStyle('TotalDiscs'), - out_type=int, - ) - lyrics = MediaField( - MP3DescStorageStyle(key='USLT'), - MP4StorageStyle('\xa9lyr'), - StorageStyle('LYRICS'), - ASFStorageStyle('WM/Lyrics'), - ) - comments = MediaField( - MP3DescStorageStyle(key='COMM'), - MP4StorageStyle('\xa9cmt'), - StorageStyle('DESCRIPTION'), - StorageStyle('COMMENT'), - ASFStorageStyle('WM/Comments'), - ASFStorageStyle('Description') - ) - bpm = MediaField( - MP3StorageStyle('TBPM'), - MP4StorageStyle('tmpo', as_type=int), - StorageStyle('BPM'), - ASFStorageStyle('WM/BeatsPerMinute'), - out_type=int, - ) - comp = MediaField( - MP3StorageStyle('TCMP'), - MP4BoolStorageStyle('cpil'), - StorageStyle('COMPILATION'), - ASFStorageStyle('WM/IsCompilation', as_type=bool), - out_type=bool, - ) - albumartist = MediaField( - MP3StorageStyle('TPE2'), - MP4StorageStyle('aART'), - StorageStyle('ALBUM ARTIST'), - StorageStyle('ALBUMARTIST'), - ASFStorageStyle('WM/AlbumArtist'), - ) - albumtype = MediaField( - MP3DescStorageStyle(u'MusicBrainz Album Type'), - MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Album Type'), - StorageStyle('MUSICBRAINZ_ALBUMTYPE'), - ASFStorageStyle('MusicBrainz/Album Type'), - ) - label = MediaField( - MP3StorageStyle('TPUB'), - MP4StorageStyle('----:com.apple.iTunes:Label'), - MP4StorageStyle('----:com.apple.iTunes:publisher'), - StorageStyle('LABEL'), - StorageStyle('PUBLISHER'), # Traktor - ASFStorageStyle('WM/Publisher'), - ) - artist_sort = MediaField( - MP3StorageStyle('TSOP'), - MP4StorageStyle('soar'), - StorageStyle('ARTISTSORT'), - ASFStorageStyle('WM/ArtistSortOrder'), - ) - albumartist_sort = MediaField( - MP3DescStorageStyle(u'ALBUMARTISTSORT'), - MP4StorageStyle('soaa'), - StorageStyle('ALBUMARTISTSORT'), - ASFStorageStyle('WM/AlbumArtistSortOrder'), - ) - asin = MediaField( - MP3DescStorageStyle(u'ASIN'), - MP4StorageStyle('----:com.apple.iTunes:ASIN'), - StorageStyle('ASIN'), - ASFStorageStyle('MusicBrainz/ASIN'), - ) - catalognum = MediaField( - MP3DescStorageStyle(u'CATALOGNUMBER'), - MP4StorageStyle('----:com.apple.iTunes:CATALOGNUMBER'), - StorageStyle('CATALOGNUMBER'), - ASFStorageStyle('WM/CatalogNo'), - ) - disctitle = MediaField( - MP3StorageStyle('TSST'), - MP4StorageStyle('----:com.apple.iTunes:DISCSUBTITLE'), - StorageStyle('DISCSUBTITLE'), - ASFStorageStyle('WM/SetSubTitle'), - ) - encoder = MediaField( - MP3StorageStyle('TENC'), - MP4StorageStyle('\xa9too'), - StorageStyle('ENCODEDBY'), - StorageStyle('ENCODER'), - ASFStorageStyle('WM/EncodedBy'), - ) - script = MediaField( - MP3DescStorageStyle(u'Script'), - MP4StorageStyle('----:com.apple.iTunes:SCRIPT'), - StorageStyle('SCRIPT'), - ASFStorageStyle('WM/Script'), - ) - language = MediaField( - MP3StorageStyle('TLAN'), - MP4StorageStyle('----:com.apple.iTunes:LANGUAGE'), - StorageStyle('LANGUAGE'), - ASFStorageStyle('WM/Language'), - ) - country = MediaField( - MP3DescStorageStyle(u'MusicBrainz Album Release Country'), - MP4StorageStyle('----:com.apple.iTunes:MusicBrainz ' - 'Album Release Country'), - StorageStyle('RELEASECOUNTRY'), - ASFStorageStyle('MusicBrainz/Album Release Country'), - ) - albumstatus = MediaField( - MP3DescStorageStyle(u'MusicBrainz Album Status'), - MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Album Status'), - StorageStyle('MUSICBRAINZ_ALBUMSTATUS'), - ASFStorageStyle('MusicBrainz/Album Status'), - ) - media = MediaField( - MP3StorageStyle('TMED'), - MP4StorageStyle('----:com.apple.iTunes:MEDIA'), - StorageStyle('MEDIA'), - ASFStorageStyle('WM/Media'), - ) - albumdisambig = MediaField( - # This tag mapping was invented for beets (not used by Picard, etc). - MP3DescStorageStyle(u'MusicBrainz Album Comment'), - MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Album Comment'), - StorageStyle('MUSICBRAINZ_ALBUMCOMMENT'), - ASFStorageStyle('MusicBrainz/Album Comment'), - ) - - # Release date. - date = DateField( - MP3StorageStyle('TDRC'), - MP4StorageStyle('\xa9day'), - StorageStyle('DATE'), - ASFStorageStyle('WM/Year'), - year=(StorageStyle('YEAR'),)) - - year = date.year_field() - month = date.month_field() - day = date.day_field() - - # *Original* release date. - original_date = DateField( - MP3StorageStyle('TDOR'), - MP4StorageStyle('----:com.apple.iTunes:ORIGINAL YEAR'), - StorageStyle('ORIGINALDATE'), - ASFStorageStyle('WM/OriginalReleaseYear')) - - original_year = original_date.year_field() - original_month = original_date.month_field() - original_day = original_date.day_field() - - # Nonstandard metadata. - artist_credit = MediaField( - MP3DescStorageStyle(u'Artist Credit'), - MP4StorageStyle('----:com.apple.iTunes:Artist Credit'), - StorageStyle('ARTIST_CREDIT'), - ASFStorageStyle('beets/Artist Credit'), - ) - albumartist_credit = MediaField( - MP3DescStorageStyle(u'Album Artist Credit'), - MP4StorageStyle('----:com.apple.iTunes:Album Artist Credit'), - StorageStyle('ALBUMARTIST_CREDIT'), - ASFStorageStyle('beets/Album Artist Credit'), - ) - - # Legacy album art field - art = CoverArtField() - - # Image list - images = ImageListField() - - # MusicBrainz IDs. - mb_trackid = MediaField( - MP3UFIDStorageStyle(owner='http://musicbrainz.org'), - MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Track Id'), - StorageStyle('MUSICBRAINZ_TRACKID'), - ASFStorageStyle('MusicBrainz/Track Id'), - ) - mb_releasetrackid = MediaField( - MP3DescStorageStyle(u'MusicBrainz Release Track Id'), - MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Release Track Id'), - StorageStyle('MUSICBRAINZ_RELEASETRACKID'), - ASFStorageStyle('MusicBrainz/Release Track Id'), - ) - mb_albumid = MediaField( - MP3DescStorageStyle(u'MusicBrainz Album Id'), - MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Album Id'), - StorageStyle('MUSICBRAINZ_ALBUMID'), - ASFStorageStyle('MusicBrainz/Album Id'), - ) - mb_artistid = MediaField( - MP3DescStorageStyle(u'MusicBrainz Artist Id'), - MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Artist Id'), - StorageStyle('MUSICBRAINZ_ARTISTID'), - ASFStorageStyle('MusicBrainz/Artist Id'), - ) - mb_albumartistid = MediaField( - MP3DescStorageStyle(u'MusicBrainz Album Artist Id'), - MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Album Artist Id'), - StorageStyle('MUSICBRAINZ_ALBUMARTISTID'), - ASFStorageStyle('MusicBrainz/Album Artist Id'), - ) - mb_releasegroupid = MediaField( - MP3DescStorageStyle(u'MusicBrainz Release Group Id'), - MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Release Group Id'), - StorageStyle('MUSICBRAINZ_RELEASEGROUPID'), - ASFStorageStyle('MusicBrainz/Release Group Id'), - ) - - # Acoustid fields. - acoustid_fingerprint = MediaField( - MP3DescStorageStyle(u'Acoustid Fingerprint'), - MP4StorageStyle('----:com.apple.iTunes:Acoustid Fingerprint'), - StorageStyle('ACOUSTID_FINGERPRINT'), - ASFStorageStyle('Acoustid/Fingerprint'), - ) - acoustid_id = MediaField( - MP3DescStorageStyle(u'Acoustid Id'), - MP4StorageStyle('----:com.apple.iTunes:Acoustid Id'), - StorageStyle('ACOUSTID_ID'), - ASFStorageStyle('Acoustid/Id'), - ) - - # ReplayGain fields. - rg_track_gain = MediaField( - MP3DescStorageStyle( - u'REPLAYGAIN_TRACK_GAIN', - float_places=2, suffix=u' dB' - ), - MP3DescStorageStyle( - u'replaygain_track_gain', - float_places=2, suffix=u' dB' - ), - MP3SoundCheckStorageStyle( - key='COMM', - index=0, desc=u'iTunNORM', - id3_lang='eng' - ), - MP4StorageStyle( - '----:com.apple.iTunes:replaygain_track_gain', - float_places=2, suffix=' dB' - ), - MP4SoundCheckStorageStyle( - '----:com.apple.iTunes:iTunNORM', - index=0 - ), - StorageStyle( - u'REPLAYGAIN_TRACK_GAIN', - float_places=2, suffix=u' dB' - ), - ASFStorageStyle( - u'replaygain_track_gain', - float_places=2, suffix=u' dB' - ), - out_type=float - ) - rg_album_gain = MediaField( - MP3DescStorageStyle( - u'REPLAYGAIN_ALBUM_GAIN', - float_places=2, suffix=u' dB' - ), - MP3DescStorageStyle( - u'replaygain_album_gain', - float_places=2, suffix=u' dB' - ), - MP4StorageStyle( - '----:com.apple.iTunes:replaygain_album_gain', - float_places=2, suffix=' dB' - ), - StorageStyle( - u'REPLAYGAIN_ALBUM_GAIN', - float_places=2, suffix=u' dB' - ), - ASFStorageStyle( - u'replaygain_album_gain', - float_places=2, suffix=u' dB' - ), - out_type=float - ) - rg_track_peak = MediaField( - MP3DescStorageStyle( - u'REPLAYGAIN_TRACK_PEAK', - float_places=6 - ), - MP3DescStorageStyle( - u'replaygain_track_peak', - float_places=6 - ), - MP3SoundCheckStorageStyle( - key=u'COMM', - index=1, desc=u'iTunNORM', - id3_lang='eng' - ), - MP4StorageStyle( - '----:com.apple.iTunes:replaygain_track_peak', - float_places=6 - ), - MP4SoundCheckStorageStyle( - '----:com.apple.iTunes:iTunNORM', - index=1 - ), - StorageStyle(u'REPLAYGAIN_TRACK_PEAK', float_places=6), - ASFStorageStyle(u'replaygain_track_peak', float_places=6), - out_type=float, - ) - rg_album_peak = MediaField( - MP3DescStorageStyle( - u'REPLAYGAIN_ALBUM_PEAK', - float_places=6 - ), - MP3DescStorageStyle( - u'replaygain_album_peak', - float_places=6 - ), - MP4StorageStyle( - '----:com.apple.iTunes:replaygain_album_peak', - float_places=6 - ), - StorageStyle(u'REPLAYGAIN_ALBUM_PEAK', float_places=6), - ASFStorageStyle(u'replaygain_album_peak', float_places=6), - out_type=float, - ) - - # EBU R128 fields. - r128_track_gain = MediaField( - MP3DescStorageStyle( - u'R128_TRACK_GAIN' - ), - MP4StorageStyle( - '----:com.apple.iTunes:R128_TRACK_GAIN' - ), - StorageStyle( - u'R128_TRACK_GAIN' - ), - ASFStorageStyle( - u'R128_TRACK_GAIN' - ), - out_type=int, - ) - r128_album_gain = MediaField( - MP3DescStorageStyle( - u'R128_ALBUM_GAIN' - ), - MP4StorageStyle( - '----:com.apple.iTunes:R128_ALBUM_GAIN' - ), - StorageStyle( - u'R128_ALBUM_GAIN' - ), - ASFStorageStyle( - u'R128_ALBUM_GAIN' - ), - out_type=int, - ) - - initial_key = MediaField( - MP3StorageStyle('TKEY'), - MP4StorageStyle('----:com.apple.iTunes:initialkey'), - StorageStyle('INITIALKEY'), - ASFStorageStyle('INITIALKEY'), - ) - - @property - def length(self): - """The duration of the audio in seconds (a float).""" - return self.mgfile.info.length - - @property - def samplerate(self): - """The audio's sample rate (an int).""" - if hasattr(self.mgfile.info, 'sample_rate'): - return self.mgfile.info.sample_rate - elif self.type == 'opus': - # Opus is always 48kHz internally. - return 48000 - return 0 - - @property - def bitdepth(self): - """The number of bits per sample in the audio encoding (an int). - Only available for certain file formats (zero where - unavailable). - """ - if hasattr(self.mgfile.info, 'bits_per_sample'): - return self.mgfile.info.bits_per_sample - return 0 - - @property - def channels(self): - """The number of channels in the audio (an int).""" - if hasattr(self.mgfile.info, 'channels'): - return self.mgfile.info.channels - return 0 - - @property - def bitrate(self): - """The number of bits per seconds used in the audio coding (an - int). If this is provided explicitly by the compressed file - format, this is a precise reflection of the encoding. Otherwise, - it is estimated from the on-disk file size. In this case, some - imprecision is possible because the file header is incorporated - in the file size. - """ - if hasattr(self.mgfile.info, 'bitrate') and self.mgfile.info.bitrate: - # Many formats provide it explicitly. - return self.mgfile.info.bitrate - else: - # Otherwise, we calculate bitrate from the file size. (This - # is the case for all of the lossless formats.) - if not self.length: - # Avoid division by zero if length is not available. - return 0 - size = os.path.getsize(self.path) - return int(size * 8 / self.length) - - @property - def format(self): - """A string describing the file format/codec.""" - return TYPES[self.type] +del key, value, warnings, mediafile diff --git a/libs/common/beets/plugins.py b/libs/common/beets/plugins.py index 1bd2cacd..ed1f82d8 100644 --- a/libs/common/beets/plugins.py +++ b/libs/common/beets/plugins.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,19 +14,19 @@ """Support for beets plugins.""" -from __future__ import division, absolute_import, print_function -import inspect import traceback import re +import inspect +import abc from collections import defaultdict from functools import wraps import beets from beets import logging -from beets import mediafile -import six +import mediafile + PLUGIN_NAMESPACE = 'beetsplug' @@ -50,26 +49,28 @@ class PluginLogFilter(logging.Filter): """A logging filter that identifies the plugin that emitted a log message. """ + def __init__(self, plugin): - self.prefix = u'{0}: '.format(plugin.name) + self.prefix = f'{plugin.name}: ' def filter(self, record): if hasattr(record.msg, 'msg') and isinstance(record.msg.msg, - six.string_types): + str): # A _LogMessage from our hacked-up Logging replacement. record.msg.msg = self.prefix + record.msg.msg - elif isinstance(record.msg, six.string_types): + elif isinstance(record.msg, str): record.msg = self.prefix + record.msg return True # Managing the plugins themselves. -class BeetsPlugin(object): +class BeetsPlugin: """The base class for all beets plugins. Plugins provide functionality by defining a subclass of BeetsPlugin and overriding the abstract methods defined here. """ + def __init__(self, name=None): """Perform one-time plugin setup. """ @@ -127,27 +128,24 @@ class BeetsPlugin(object): value after the function returns). Also determines which params may not be sent for backwards-compatibility. """ - argspec = inspect.getargspec(func) + argspec = inspect.getfullargspec(func) @wraps(func) def wrapper(*args, **kwargs): assert self._log.level == logging.NOTSET + verbosity = beets.config['verbose'].get(int) log_level = max(logging.DEBUG, base_log_level - 10 * verbosity) self._log.setLevel(log_level) + if argspec.varkw is None: + kwargs = {k: v for k, v in kwargs.items() + if k in argspec.args} + try: - try: - return func(*args, **kwargs) - except TypeError as exc: - if exc.args[0].startswith(func.__name__): - # caused by 'func' and not stuff internal to 'func' - kwargs = dict((arg, val) for arg, val in kwargs.items() - if arg in argspec.args) - return func(*args, **kwargs) - else: - raise + return func(*args, **kwargs) finally: self._log.setLevel(logging.NOTSET) + return wrapper def queries(self): @@ -167,7 +165,7 @@ class BeetsPlugin(object): """ return beets.autotag.hooks.Distance() - def candidates(self, items, artist, album, va_likely): + def candidates(self, items, artist, album, va_likely, extra_tags=None): """Should return a sequence of AlbumInfo objects that match the album whose items are provided. """ @@ -201,7 +199,7 @@ class BeetsPlugin(object): ``descriptor`` must be an instance of ``mediafile.MediaField``. """ - # Defer impor to prevent circular dependency + # Defer import to prevent circular dependency from beets import library mediafile.MediaFile.add_field(name, descriptor) library.Item._media_fields.add(name) @@ -264,14 +262,14 @@ def load_plugins(names=()): BeetsPlugin subclasses desired. """ for name in names: - modname = '{0}.{1}'.format(PLUGIN_NAMESPACE, name) + modname = f'{PLUGIN_NAMESPACE}.{name}' try: try: namespace = __import__(modname, None, None) except ImportError as exc: # Again, this is hacky: if exc.args[0].endswith(' ' + name): - log.warning(u'** plugin {0} not found', name) + log.warning('** plugin {0} not found', name) else: raise else: @@ -282,7 +280,7 @@ def load_plugins(names=()): except Exception: log.warning( - u'** error loading plugin {}:\n{}', + '** error loading plugin {}:\n{}', name, traceback.format_exc(), ) @@ -296,6 +294,11 @@ def find_plugins(): currently loaded beets plugins. Loads the default plugin set first. """ + if _instances: + # After the first call, use cached instances for performance reasons. + # See https://github.com/beetbox/beets/pull/3810 + return list(_instances.values()) + load_plugins() plugins = [] for cls in _classes: @@ -329,21 +332,31 @@ def queries(): def types(model_cls): # Gives us `item_types` and `album_types` - attr_name = '{0}_types'.format(model_cls.__name__.lower()) + attr_name = f'{model_cls.__name__.lower()}_types' types = {} for plugin in find_plugins(): plugin_types = getattr(plugin, attr_name, {}) for field in plugin_types: if field in types and plugin_types[field] != types[field]: raise PluginConflictException( - u'Plugin {0} defines flexible field {1} ' - u'which has already been defined with ' - u'another type.'.format(plugin.name, field) + 'Plugin {} defines flexible field {} ' + 'which has already been defined with ' + 'another type.'.format(plugin.name, field) ) types.update(plugin_types) return types +def named_queries(model_cls): + # Gather `item_queries` and `album_queries` from the plugins. + attr_name = f'{model_cls.__name__.lower()}_queries' + queries = {} + for plugin in find_plugins(): + plugin_queries = getattr(plugin, attr_name, {}) + queries.update(plugin_queries) + return queries + + def track_distance(item, info): """Gets the track distance calculated by all loaded plugins. Returns a Distance object. @@ -364,20 +377,19 @@ def album_distance(items, album_info, mapping): return dist -def candidates(items, artist, album, va_likely): +def candidates(items, artist, album, va_likely, extra_tags=None): """Gets MusicBrainz candidates for an album from each plugin. """ for plugin in find_plugins(): - for candidate in plugin.candidates(items, artist, album, va_likely): - yield candidate + yield from plugin.candidates(items, artist, album, va_likely, + extra_tags) def item_candidates(item, artist, title): """Gets MusicBrainz candidates for an item from the plugins. """ for plugin in find_plugins(): - for item_candidate in plugin.item_candidates(item, artist, title): - yield item_candidate + yield from plugin.item_candidates(item, artist, title) def album_for_id(album_id): @@ -470,7 +482,7 @@ def send(event, **arguments): Return a list of non-None values returned from the handlers. """ - log.debug(u'Sending event: {0}', event) + log.debug('Sending event: {0}', event) results = [] for handler in event_handlers()[event]: result = handler(**arguments) @@ -488,7 +500,7 @@ def feat_tokens(for_artist=True): feat_words = ['ft', 'featuring', 'feat', 'feat.', 'ft.'] if for_artist: feat_words += ['with', 'vs', 'and', 'con', '&'] - return '(?<=\s)(?:{0})(?=\s)'.format( + return r'(?<=\s)(?:{})(?=\s)'.format( '|'.join(re.escape(x) for x in feat_words) ) @@ -513,7 +525,7 @@ def sanitize_choices(choices, choices_all): def sanitize_pairs(pairs, pairs_all): """Clean up a single-element mapping configuration attribute as returned - by `confit`'s `Pairs` template: keep only two-element tuples present in + by Confuse's `Pairs` template: keep only two-element tuples present in pairs_all, remove duplicate elements, expand ('str', '*') and ('*', '*') wildcards while keeping the original order. Note that ('*', '*') and ('*', 'whatever') have the same effect. @@ -563,3 +575,188 @@ def notify_info_yielded(event): yield v return decorated return decorator + + +def get_distance(config, data_source, info): + """Returns the ``data_source`` weight and the maximum source weight + for albums or individual tracks. + """ + dist = beets.autotag.Distance() + if info.data_source == data_source: + dist.add('source', config['source_weight'].as_number()) + return dist + + +def apply_item_changes(lib, item, move, pretend, write): + """Store, move, and write the item according to the arguments. + + :param lib: beets library. + :type lib: beets.library.Library + :param item: Item whose changes to apply. + :type item: beets.library.Item + :param move: Move the item if it's in the library. + :type move: bool + :param pretend: Return without moving, writing, or storing the item's + metadata. + :type pretend: bool + :param write: Write the item's metadata to its media file. + :type write: bool + """ + if pretend: + return + + from beets import util + + # Move the item if it's in the library. + if move and lib.directory in util.ancestry(item.path): + item.move(with_album=False) + + if write: + item.try_write() + + item.store() + + +class MetadataSourcePlugin(metaclass=abc.ABCMeta): + def __init__(self): + super().__init__() + self.config.add({'source_weight': 0.5}) + + @abc.abstractproperty + def id_regex(self): + raise NotImplementedError + + @abc.abstractproperty + def data_source(self): + raise NotImplementedError + + @abc.abstractproperty + def search_url(self): + raise NotImplementedError + + @abc.abstractproperty + def album_url(self): + raise NotImplementedError + + @abc.abstractproperty + def track_url(self): + raise NotImplementedError + + @abc.abstractmethod + def _search_api(self, query_type, filters, keywords=''): + raise NotImplementedError + + @abc.abstractmethod + def album_for_id(self, album_id): + raise NotImplementedError + + @abc.abstractmethod + def track_for_id(self, track_id=None, track_data=None): + raise NotImplementedError + + @staticmethod + def get_artist(artists, id_key='id', name_key='name'): + """Returns an artist string (all artists) and an artist_id (the main + artist) for a list of artist object dicts. + + For each artist, this function moves articles (such as 'a', 'an', + and 'the') to the front and strips trailing disambiguation numbers. It + returns a tuple containing the comma-separated string of all + normalized artists and the ``id`` of the main/first artist. + + :param artists: Iterable of artist dicts or lists returned by API. + :type artists: list[dict] or list[list] + :param id_key: Key or index corresponding to the value of ``id`` for + the main/first artist. Defaults to 'id'. + :type id_key: str or int + :param name_key: Key or index corresponding to values of names + to concatenate for the artist string (containing all artists). + Defaults to 'name'. + :type name_key: str or int + :return: Normalized artist string. + :rtype: str + """ + artist_id = None + artist_names = [] + for artist in artists: + if not artist_id: + artist_id = artist[id_key] + name = artist[name_key] + # Strip disambiguation number. + name = re.sub(r' \(\d+\)$', '', name) + # Move articles to the front. + name = re.sub(r'^(.*?), (a|an|the)$', r'\2 \1', name, flags=re.I) + artist_names.append(name) + artist = ', '.join(artist_names).replace(' ,', ',') or None + return artist, artist_id + + def _get_id(self, url_type, id_): + """Parse an ID from its URL if necessary. + + :param url_type: Type of URL. Either 'album' or 'track'. + :type url_type: str + :param id_: Album/track ID or URL. + :type id_: str + :return: Album/track ID. + :rtype: str + """ + self._log.debug( + "Searching {} for {} '{}'", self.data_source, url_type, id_ + ) + match = re.search(self.id_regex['pattern'].format(url_type), str(id_)) + if match: + id_ = match.group(self.id_regex['match_group']) + if id_: + return id_ + return None + + def candidates(self, items, artist, album, va_likely, extra_tags=None): + """Returns a list of AlbumInfo objects for Search API results + matching an ``album`` and ``artist`` (if not various). + + :param items: List of items comprised by an album to be matched. + :type items: list[beets.library.Item] + :param artist: The artist of the album to be matched. + :type artist: str + :param album: The name of the album to be matched. + :type album: str + :param va_likely: True if the album to be matched likely has + Various Artists. + :type va_likely: bool + :return: Candidate AlbumInfo objects. + :rtype: list[beets.autotag.hooks.AlbumInfo] + """ + query_filters = {'album': album} + if not va_likely: + query_filters['artist'] = artist + results = self._search_api(query_type='album', filters=query_filters) + albums = [self.album_for_id(album_id=r['id']) for r in results] + return [a for a in albums if a is not None] + + def item_candidates(self, item, artist, title): + """Returns a list of TrackInfo objects for Search API results + matching ``title`` and ``artist``. + + :param item: Singleton item to be matched. + :type item: beets.library.Item + :param artist: The artist of the track to be matched. + :type artist: str + :param title: The title of the track to be matched. + :type title: str + :return: Candidate TrackInfo objects. + :rtype: list[beets.autotag.hooks.TrackInfo] + """ + tracks = self._search_api( + query_type='track', keywords=title, filters={'artist': artist} + ) + return [self.track_for_id(track_data=track) for track in tracks] + + def album_distance(self, items, album_info, mapping): + return get_distance( + data_source=self.data_source, info=album_info, config=self.config + ) + + def track_distance(self, item, track_info): + return get_distance( + data_source=self.data_source, info=track_info, config=self.config + ) diff --git a/libs/common/beets/random.py b/libs/common/beets/random.py new file mode 100644 index 00000000..eb4f55af --- /dev/null +++ b/libs/common/beets/random.py @@ -0,0 +1,113 @@ +# This file is part of beets. +# Copyright 2016, Philippe Mongeau. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Get a random song or album from the library. +""" + +import random +from operator import attrgetter +from itertools import groupby + + +def _length(obj, album): + """Get the duration of an item or album. + """ + if album: + return sum(i.length for i in obj.items()) + else: + return obj.length + + +def _equal_chance_permutation(objs, field='albumartist', random_gen=None): + """Generate (lazily) a permutation of the objects where every group + with equal values for `field` have an equal chance of appearing in + any given position. + """ + rand = random_gen or random + + # Group the objects by artist so we can sample from them. + key = attrgetter(field) + objs.sort(key=key) + objs_by_artists = {} + for artist, v in groupby(objs, key): + objs_by_artists[artist] = list(v) + + # While we still have artists with music to choose from, pick one + # randomly and pick a track from that artist. + while objs_by_artists: + # Choose an artist and an object for that artist, removing + # this choice from the pool. + artist = rand.choice(list(objs_by_artists.keys())) + objs_from_artist = objs_by_artists[artist] + i = rand.randint(0, len(objs_from_artist) - 1) + yield objs_from_artist.pop(i) + + # Remove the artist if we've used up all of its objects. + if not objs_from_artist: + del objs_by_artists[artist] + + +def _take(iter, num): + """Return a list containing the first `num` values in `iter` (or + fewer, if the iterable ends early). + """ + out = [] + for val in iter: + out.append(val) + num -= 1 + if num <= 0: + break + return out + + +def _take_time(iter, secs, album): + """Return a list containing the first values in `iter`, which should + be Item or Album objects, that add up to the given amount of time in + seconds. + """ + out = [] + total_time = 0.0 + for obj in iter: + length = _length(obj, album) + if total_time + length <= secs: + out.append(obj) + total_time += length + return out + + +def random_objs(objs, album, number=1, time=None, equal_chance=False, + random_gen=None): + """Get a random subset of the provided `objs`. + + If `number` is provided, produce that many matches. Otherwise, if + `time` is provided, instead select a list whose total time is close + to that number of minutes. If `equal_chance` is true, give each + artist an equal chance of being included so that artists with more + songs are not represented disproportionately. + """ + rand = random_gen or random + + # Permute the objects either in a straightforward way or an + # artist-balanced way. + if equal_chance: + perm = _equal_chance_permutation(objs) + else: + perm = objs + rand.shuffle(perm) # N.B. This shuffles the original list. + + # Select objects by time our count. + if time: + return _take_time(perm, time * 60, album) + else: + return _take(perm, number) diff --git a/libs/common/beets/ui/__init__.py b/libs/common/beets/ui/__init__.py index af2b79a1..121cb5dc 100644 --- a/libs/common/beets/ui/__init__.py +++ b/libs/common/beets/ui/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -18,7 +17,6 @@ interface. To invoke the CLI, just call beets.ui.main(). The actual CLI commands are implemented in the ui.commands module. """ -from __future__ import division, absolute_import, print_function import optparse import textwrap @@ -30,19 +28,18 @@ import re import struct import traceback import os.path -from six.moves import input from beets import logging from beets import library from beets import plugins from beets import util -from beets.util.functemplate import Template +from beets.util.functemplate import template from beets import config -from beets.util import confit, as_string +from beets.util import as_string from beets.autotag import mb from beets.dbcore import query as db_query from beets.dbcore import db -import six +import confuse # On Windows platforms, use colorama to support "ANSI" terminal colors. if sys.platform == 'win32': @@ -61,8 +58,8 @@ log.propagate = False # Don't propagate to root handler. PF_KEY_QUERIES = { - 'comp': u'comp:true', - 'singleton': u'singleton:true', + 'comp': 'comp:true', + 'singleton': 'singleton:true', } @@ -112,10 +109,7 @@ def decargs(arglist): """Given a list of command-line argument bytestrings, attempts to decode them to Unicode strings when running under Python 2. """ - if six.PY2: - return [s.decode(util.arg_encoding()) for s in arglist] - else: - return arglist + return arglist def print_(*strings, **kwargs): @@ -130,30 +124,25 @@ def print_(*strings, **kwargs): (it defaults to a newline). """ if not strings: - strings = [u''] - assert isinstance(strings[0], six.text_type) + strings = [''] + assert isinstance(strings[0], str) - txt = u' '.join(strings) - txt += kwargs.get('end', u'\n') + txt = ' '.join(strings) + txt += kwargs.get('end', '\n') # Encode the string and write it to stdout. - if six.PY2: - # On Python 2, sys.stdout expects bytes. + # On Python 3, sys.stdout expects text strings and uses the + # exception-throwing encoding error policy. To avoid throwing + # errors and use our configurable encoding override, we use the + # underlying bytes buffer instead. + if hasattr(sys.stdout, 'buffer'): out = txt.encode(_out_encoding(), 'replace') - sys.stdout.write(out) + sys.stdout.buffer.write(out) + sys.stdout.buffer.flush() else: - # On Python 3, sys.stdout expects text strings and uses the - # exception-throwing encoding error policy. To avoid throwing - # errors and use our configurable encoding override, we use the - # underlying bytes buffer instead. - if hasattr(sys.stdout, 'buffer'): - out = txt.encode(_out_encoding(), 'replace') - sys.stdout.buffer.write(out) - sys.stdout.buffer.flush() - else: - # In our test harnesses (e.g., DummyOut), sys.stdout.buffer - # does not exist. We instead just record the text string. - sys.stdout.write(txt) + # In our test harnesses (e.g., DummyOut), sys.stdout.buffer + # does not exist. We instead just record the text string. + sys.stdout.write(txt) # Configuration wrappers. @@ -203,19 +192,16 @@ def input_(prompt=None): """ # raw_input incorrectly sends prompts to stderr, not stdout, so we # use print_() explicitly to display prompts. - # http://bugs.python.org/issue1927 + # https://bugs.python.org/issue1927 if prompt: - print_(prompt, end=u' ') + print_(prompt, end=' ') try: resp = input() except EOFError: - raise UserError(u'stdin stream ended while input required') + raise UserError('stdin stream ended while input required') - if six.PY2: - return resp.decode(_in_encoding(), 'ignore') - else: - return resp + return resp def input_options(options, require=False, prompt=None, fallback_prompt=None, @@ -259,7 +245,7 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None, found_letter = letter break else: - raise ValueError(u'no unambiguous lettering found') + raise ValueError('no unambiguous lettering found') letters[found_letter.lower()] = option index = option.index(found_letter) @@ -267,7 +253,7 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None, # Mark the option's shortcut letter for display. if not require and ( (default is None and not numrange and first) or - (isinstance(default, six.string_types) and + (isinstance(default, str) and found_letter.lower() == default.lower())): # The first option is the default; mark it. show_letter = '[%s]' % found_letter.upper() @@ -303,11 +289,11 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None, prompt_part_lengths = [] if numrange: if isinstance(default, int): - default_name = six.text_type(default) + default_name = str(default) default_name = colorize('action_default', default_name) tmpl = '# selection (default %s)' prompt_parts.append(tmpl % default_name) - prompt_part_lengths.append(len(tmpl % six.text_type(default))) + prompt_part_lengths.append(len(tmpl % str(default))) else: prompt_parts.append('# selection') prompt_part_lengths.append(len(prompt_parts[-1])) @@ -342,9 +328,9 @@ def input_options(options, require=False, prompt=None, fallback_prompt=None, # Make a fallback prompt too. This is displayed if the user enters # something that is not recognized. if not fallback_prompt: - fallback_prompt = u'Enter one of ' + fallback_prompt = 'Enter one of ' if numrange: - fallback_prompt += u'%i-%i, ' % numrange + fallback_prompt += '%i-%i, ' % numrange fallback_prompt += ', '.join(display_letters) + ':' resp = input_(prompt) @@ -383,34 +369,41 @@ def input_yn(prompt, require=False): "yes" unless `require` is `True`, in which case there is no default. """ sel = input_options( - ('y', 'n'), require, prompt, u'Enter Y or N:' + ('y', 'n'), require, prompt, 'Enter Y or N:' ) - return sel == u'y' + return sel == 'y' -def input_select_objects(prompt, objs, rep): +def input_select_objects(prompt, objs, rep, prompt_all=None): """Prompt to user to choose all, none, or some of the given objects. Return the list of selected objects. `prompt` is the prompt string to use for each question (it should be - phrased as an imperative verb). `rep` is a function to call on each - object to print it out when confirming objects individually. + phrased as an imperative verb). If `prompt_all` is given, it is used + instead of `prompt` for the first (yes(/no/select) question. + `rep` is a function to call on each object to print it out when confirming + objects individually. """ choice = input_options( - (u'y', u'n', u's'), False, - u'%s? (Yes/no/select)' % prompt) + ('y', 'n', 's'), False, + '%s? (Yes/no/select)' % (prompt_all or prompt)) print() # Blank line. - if choice == u'y': # Yes. + if choice == 'y': # Yes. return objs - elif choice == u's': # Select. + elif choice == 's': # Select. out = [] for obj in objs: rep(obj) - if input_yn(u'%s? (yes/no)' % prompt, True): + answer = input_options( + ('y', 'n', 'q'), True, '%s? (yes/no/quit)' % prompt, + 'Enter Y or N:' + ) + if answer == 'y': out.append(obj) - print() # go to a new line + elif answer == 'q': + return out return out else: # No. @@ -421,14 +414,14 @@ def input_select_objects(prompt, objs, rep): def human_bytes(size): """Formats size, a number of bytes, in a human-readable way.""" - powers = [u'', u'K', u'M', u'G', u'T', u'P', u'E', u'Z', u'Y', u'H'] + powers = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'H'] unit = 'B' for power in powers: if size < 1024: - return u"%3.1f %s%s" % (size, power, unit) + return f"{size:3.1f} {power}{unit}" size /= 1024.0 - unit = u'iB' - return u"big" + unit = 'iB' + return "big" def human_seconds(interval): @@ -436,13 +429,13 @@ def human_seconds(interval): interval using English words. """ units = [ - (1, u'second'), - (60, u'minute'), - (60, u'hour'), - (24, u'day'), - (7, u'week'), - (52, u'year'), - (10, u'decade'), + (1, 'second'), + (60, 'minute'), + (60, 'hour'), + (24, 'day'), + (7, 'week'), + (52, 'year'), + (10, 'decade'), ] for i in range(len(units) - 1): increment, suffix = units[i] @@ -455,7 +448,7 @@ def human_seconds(interval): increment, suffix = units[-1] interval /= float(increment) - return u"%3.1f %ss" % (interval, suffix) + return f"{interval:3.1f} {suffix}s" def human_seconds_short(interval): @@ -463,13 +456,13 @@ def human_seconds_short(interval): string. """ interval = int(interval) - return u'%i:%02i' % (interval // 60, interval % 60) + return '%i:%02i' % (interval // 60, interval % 60) # Colorization. # ANSI terminal colorization code heavily inspired by pygments: -# http://dev.pocoo.org/hg/pygments-main/file/b2deea5b5030/pygments/console.py +# https://bitbucket.org/birkenfeld/pygments-main/src/default/pygments/console.py # (pygments is by Tim Hatch, Armin Ronacher, et al.) COLOR_ESCAPE = "\x1b[" DARK_COLORS = { @@ -516,7 +509,7 @@ def _colorize(color, text): elif color in LIGHT_COLORS: escape = COLOR_ESCAPE + "%i;01m" % (LIGHT_COLORS[color] + 30) else: - raise ValueError(u'no such color %s', color) + raise ValueError('no such color %s', color) return escape + text + RESET_COLOR @@ -524,22 +517,22 @@ def colorize(color_name, text): """Colorize text if colored output is enabled. (Like _colorize but conditional.) """ - if config['ui']['color']: - global COLORS - if not COLORS: - COLORS = dict((name, - config['ui']['colors'][name].as_str()) - for name in COLOR_NAMES) - # In case a 3rd party plugin is still passing the actual color ('red') - # instead of the abstract color name ('text_error') - color = COLORS.get(color_name) - if not color: - log.debug(u'Invalid color_name: {0}', color_name) - color = color_name - return _colorize(color, text) - else: + if not config['ui']['color'] or 'NO_COLOR' in os.environ.keys(): return text + global COLORS + if not COLORS: + COLORS = {name: + config['ui']['colors'][name].as_str() + for name in COLOR_NAMES} + # In case a 3rd party plugin is still passing the actual color ('red') + # instead of the abstract color name ('text_error') + color = COLORS.get(color_name) + if not color: + log.debug('Invalid color_name: {0}', color_name) + color = color_name + return _colorize(color, text) + def _colordiff(a, b, highlight='text_highlight', minor_highlight='text_highlight_minor'): @@ -548,11 +541,11 @@ def _colordiff(a, b, highlight='text_highlight', highlighted intelligently to show differences; other values are stringified and highlighted in their entirety. """ - if not isinstance(a, six.string_types) \ - or not isinstance(b, six.string_types): + if not isinstance(a, str) \ + or not isinstance(b, str): # Non-strings: use ordinary equality. - a = six.text_type(a) - b = six.text_type(b) + a = str(a) + b = str(b) if a == b: return a, b else: @@ -590,7 +583,7 @@ def _colordiff(a, b, highlight='text_highlight', else: assert(False) - return u''.join(a_out), u''.join(b_out) + return ''.join(a_out), ''.join(b_out) def colordiff(a, b, highlight='text_highlight'): @@ -600,7 +593,7 @@ def colordiff(a, b, highlight='text_highlight'): if config['ui']['color']: return _colordiff(a, b, highlight) else: - return six.text_type(a), six.text_type(b) + return str(a), str(b) def get_path_formats(subview=None): @@ -611,12 +604,12 @@ def get_path_formats(subview=None): subview = subview or config['paths'] for query, view in subview.items(): query = PF_KEY_QUERIES.get(query, query) # Expand common queries. - path_formats.append((query, Template(view.as_str()))) + path_formats.append((query, template(view.as_str()))) return path_formats def get_replacements(): - """Confit validation function that reads regex/string pairs. + """Confuse validation function that reads regex/string pairs. """ replacements = [] for pattern, repl in config['replace'].get(dict).items(): @@ -625,7 +618,7 @@ def get_replacements(): replacements.append((re.compile(pattern), repl)) except re.error: raise UserError( - u'malformed regular expression in replace: {0}'.format( + 'malformed regular expression in replace: {}'.format( pattern ) ) @@ -646,7 +639,7 @@ def term_width(): try: buf = fcntl.ioctl(0, termios.TIOCGWINSZ, ' ' * 4) - except IOError: + except OSError: return fallback try: height, width = struct.unpack('hh', buf) @@ -658,10 +651,10 @@ def term_width(): FLOAT_EPSILON = 0.01 -def _field_diff(field, old, new): - """Given two Model objects, format their values for `field` and - highlight changes among them. Return a human-readable string. If the - value has not changed, return None instead. +def _field_diff(field, old, old_fmt, new, new_fmt): + """Given two Model objects and their formatted views, format their values + for `field` and highlight changes among them. Return a human-readable + string. If the value has not changed, return None instead. """ oldval = old.get(field) newval = new.get(field) @@ -674,18 +667,18 @@ def _field_diff(field, old, new): return None # Get formatted values for output. - oldstr = old.formatted().get(field, u'') - newstr = new.formatted().get(field, u'') + oldstr = old_fmt.get(field, '') + newstr = new_fmt.get(field, '') # For strings, highlight changes. For others, colorize the whole # thing. - if isinstance(oldval, six.string_types): + if isinstance(oldval, str): oldstr, newstr = colordiff(oldval, newstr) else: oldstr = colorize('text_error', oldstr) newstr = colorize('text_error', newstr) - return u'{0} -> {1}'.format(oldstr, newstr) + return f'{oldstr} -> {newstr}' def show_model_changes(new, old=None, fields=None, always=False): @@ -700,6 +693,11 @@ def show_model_changes(new, old=None, fields=None, always=False): """ old = old or new._db._get(type(new), new.id) + # Keep the formatted views around instead of re-creating them in each + # iteration step + old_fmt = old.formatted() + new_fmt = new.formatted() + # Build up lines showing changed fields. changes = [] for field in old: @@ -708,25 +706,25 @@ def show_model_changes(new, old=None, fields=None, always=False): continue # Detect and show difference for this field. - line = _field_diff(field, old, new) + line = _field_diff(field, old, old_fmt, new, new_fmt) if line: - changes.append(u' {0}: {1}'.format(field, line)) + changes.append(f' {field}: {line}') # New fields. for field in set(new) - set(old): if fields and field not in fields: continue - changes.append(u' {0}: {1}'.format( + changes.append(' {}: {}'.format( field, - colorize('text_highlight', new.formatted()[field]) + colorize('text_highlight', new_fmt[field]) )) # Print changes. if changes or always: print_(format(old)) if changes: - print_(u'\n'.join(changes)) + print_('\n'.join(changes)) return bool(changes) @@ -759,15 +757,21 @@ def show_path_changes(path_changes): if max_width > col_width: # Print every change over two lines for source, dest in zip(sources, destinations): - log.info(u'{0} \n -> {1}', source, dest) + color_source, color_dest = colordiff(source, dest) + print_('{0} \n -> {1}'.format(color_source, color_dest)) else: # Print every change on a single line, and add a header title_pad = max_width - len('Source ') + len(' -> ') - log.info(u'Source {0} Destination', ' ' * title_pad) + print_('Source {0} Destination'.format(' ' * title_pad)) for source, dest in zip(sources, destinations): pad = max_width - len(source) - log.info(u'{0} {1} -> {2}', source, ' ' * pad, dest) + color_source, color_dest = colordiff(source, dest) + print_('{0} {1} -> {2}'.format( + color_source, + ' ' * pad, + color_dest, + )) # Helper functions for option parsing. @@ -783,22 +787,25 @@ def _store_dict(option, opt_str, value, parser): if option_values is None: # This is the first supplied ``key=value`` pair of option. # Initialize empty dictionary and get a reference to it. - setattr(parser.values, dest, dict()) + setattr(parser.values, dest, {}) option_values = getattr(parser.values, dest) + # Decode the argument using the platform's argument encoding. + value = util.text_string(value, util.arg_encoding()) + try: - key, value = map(lambda s: util.text_string(s), value.split('=')) + key, value = value.split('=', 1) if not (key and value): raise ValueError except ValueError: raise UserError( - "supplied argument `{0}' is not of the form `key=value'" + "supplied argument `{}' is not of the form `key=value'" .format(value)) option_values[key] = value -class CommonOptionsParser(optparse.OptionParser, object): +class CommonOptionsParser(optparse.OptionParser): """Offers a simple way to add common formatting options. Options available include: @@ -813,8 +820,9 @@ class CommonOptionsParser(optparse.OptionParser, object): Each method is fully documented in the related method. """ + def __init__(self, *args, **kwargs): - super(CommonOptionsParser, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._album_flags = False # this serves both as an indicator that we offer the feature AND allows # us to check whether it has been specified on the CLI - bypassing the @@ -828,7 +836,7 @@ class CommonOptionsParser(optparse.OptionParser, object): Sets the album property on the options extracted from the CLI. """ album = optparse.Option(*flags, action='store_true', - help=u'match albums instead of tracks') + help='match albums instead of tracks') self.add_option(album) self._album_flags = set(flags) @@ -846,7 +854,7 @@ class CommonOptionsParser(optparse.OptionParser, object): elif value: value, = decargs([value]) else: - value = u'' + value = '' parser.values.format = value if target: @@ -873,14 +881,14 @@ class CommonOptionsParser(optparse.OptionParser, object): By default this affects both items and albums. If add_album_option() is used then the target will be autodetected. - Sets the format property to u'$path' on the options extracted from the + Sets the format property to '$path' on the options extracted from the CLI. """ path = optparse.Option(*flags, nargs=0, action='callback', callback=self._set_format, - callback_kwargs={'fmt': u'$path', + callback_kwargs={'fmt': '$path', 'store_true': True}, - help=u'print paths for matched items or albums') + help='print paths for matched items or albums') self.add_option(path) def add_format_option(self, flags=('-f', '--format'), target=None): @@ -900,7 +908,7 @@ class CommonOptionsParser(optparse.OptionParser, object): """ kwargs = {} if target: - if isinstance(target, six.string_types): + if isinstance(target, str): target = {'item': library.Item, 'album': library.Album}[target] kwargs['target'] = target @@ -908,7 +916,7 @@ class CommonOptionsParser(optparse.OptionParser, object): opt = optparse.Option(*flags, action='callback', callback=self._set_format, callback_kwargs=kwargs, - help=u'print with custom format') + help='print with custom format') self.add_option(opt) def add_all_common_options(self): @@ -923,14 +931,15 @@ class CommonOptionsParser(optparse.OptionParser, object): # # This is a fairly generic subcommand parser for optparse. It is # maintained externally here: -# http://gist.github.com/462717 +# https://gist.github.com/462717 # There you will also find a better description of the code and a more # succinct example program. -class Subcommand(object): +class Subcommand: """A subcommand of a root command-line application that may be invoked by a SubcommandOptionParser. """ + def __init__(self, name, parser=None, help='', aliases=(), hide=False): """Creates a new subcommand. name is the primary way to invoke the subcommand; aliases are alternate names. parser is an @@ -958,7 +967,7 @@ class Subcommand(object): @root_parser.setter def root_parser(self, root_parser): self._root_parser = root_parser - self.parser.prog = '{0} {1}'.format( + self.parser.prog = '{} {}'.format( as_string(root_parser.get_prog_name()), self.name) @@ -974,13 +983,13 @@ class SubcommandsOptionParser(CommonOptionsParser): """ # A more helpful default usage. if 'usage' not in kwargs: - kwargs['usage'] = u""" + kwargs['usage'] = """ %prog COMMAND [ARGS...] %prog help COMMAND""" kwargs['add_help_option'] = False # Super constructor. - super(SubcommandsOptionParser, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) # Our root parser needs to stop on the first unrecognized argument. self.disable_interspersed_args() @@ -997,7 +1006,7 @@ class SubcommandsOptionParser(CommonOptionsParser): # Add the list of subcommands to the help message. def format_help(self, formatter=None): # Get the original help message, to which we will append. - out = super(SubcommandsOptionParser, self).format_help(formatter) + out = super().format_help(formatter) if formatter is None: formatter = self.formatter @@ -1083,7 +1092,7 @@ class SubcommandsOptionParser(CommonOptionsParser): cmdname = args.pop(0) subcommand = self._subcommand_for_name(cmdname) if not subcommand: - raise UserError(u"unknown command '{0}'".format(cmdname)) + raise UserError(f"unknown command '{cmdname}'") suboptions, subargs = subcommand.parse_args(args) return subcommand, suboptions, subargs @@ -1094,26 +1103,32 @@ optparse.Option.ALWAYS_TYPED_ACTIONS += ('callback',) # The main entry point and bootstrapping. -def _load_plugins(config): - """Load the plugins specified in the configuration. +def _load_plugins(options, config): + """Load the plugins specified on the command line or in the configuration. """ paths = config['pluginpath'].as_str_seq(split=False) paths = [util.normpath(p) for p in paths] - log.debug(u'plugin paths: {0}', util.displayable_path(paths)) + log.debug('plugin paths: {0}', util.displayable_path(paths)) # On Python 3, the search paths need to be unicode. paths = [util.py3_path(p) for p in paths] # Extend the `beetsplug` package to include the plugin paths. import beetsplug - beetsplug.__path__ = paths + beetsplug.__path__ + beetsplug.__path__ = paths + list(beetsplug.__path__) # For backwards compatibility, also support plugin paths that # *contain* a `beetsplug` package. sys.path += paths - plugins.load_plugins(config['plugins'].as_str_seq()) - plugins.send("pluginload") + # If we were given any plugins on the command line, use those. + if options.plugins is not None: + plugin_list = (options.plugins.split(',') + if len(options.plugins) > 0 else []) + else: + plugin_list = config['plugins'].as_str_seq() + + plugins.load_plugins(plugin_list) return plugins @@ -1127,7 +1142,20 @@ def _setup(options, lib=None): config = _configure(options) - plugins = _load_plugins(config) + plugins = _load_plugins(options, config) + + # Add types and queries defined by plugins. + plugin_types_album = plugins.types(library.Album) + library.Album._types.update(plugin_types_album) + item_types = plugin_types_album.copy() + item_types.update(library.Item._types) + item_types.update(plugins.types(library.Item)) + library.Item._types = item_types + + library.Item._queries.update(plugins.named_queries(library.Item)) + library.Album._queries.update(plugins.named_queries(library.Album)) + + plugins.send("pluginload") # Get the default subcommands. from beets.ui.commands import default_commands @@ -1138,8 +1166,6 @@ def _setup(options, lib=None): if lib is None: lib = _open_library(config) plugins.send("library_opened", lib=lib) - library.Item._types.update(plugins.types(library.Item)) - library.Album._types.update(plugins.types(library.Album)) return subcommands, plugins, lib @@ -1165,18 +1191,18 @@ def _configure(options): log.set_global_level(logging.INFO) if overlay_path: - log.debug(u'overlaying configuration: {0}', + log.debug('overlaying configuration: {0}', util.displayable_path(overlay_path)) config_path = config.user_config_path() if os.path.isfile(config_path): - log.debug(u'user configuration: {0}', + log.debug('user configuration: {0}', util.displayable_path(config_path)) else: - log.debug(u'no user configuration found at {0}', + log.debug('no user configuration found at {0}', util.displayable_path(config_path)) - log.debug(u'data directory: {0}', + log.debug('data directory: {0}', util.displayable_path(config.config_dir())) return config @@ -1193,13 +1219,14 @@ def _open_library(config): get_replacements(), ) lib.get_item(0) # Test database connection. - except (sqlite3.OperationalError, sqlite3.DatabaseError): - log.debug(u'{}', traceback.format_exc()) - raise UserError(u"database file {0} could not be opened".format( - util.displayable_path(dbpath) + except (sqlite3.OperationalError, sqlite3.DatabaseError) as db_error: + log.debug('{}', traceback.format_exc()) + raise UserError("database file {} cannot not be opened: {}".format( + util.displayable_path(dbpath), + db_error )) - log.debug(u'library database: {0}\n' - u'library directory: {1}', + log.debug('library database: {0}\n' + 'library directory: {1}', util.displayable_path(lib.path), util.displayable_path(lib.directory)) return lib @@ -1213,15 +1240,17 @@ def _raw_main(args, lib=None): parser.add_format_option(flags=('--format-item',), target=library.Item) parser.add_format_option(flags=('--format-album',), target=library.Album) parser.add_option('-l', '--library', dest='library', - help=u'library database file to use') + help='library database file to use') parser.add_option('-d', '--directory', dest='directory', - help=u"destination music directory") + help="destination music directory") parser.add_option('-v', '--verbose', dest='verbose', action='count', - help=u'log more details (use twice for even more)') + help='log more details (use twice for even more)') parser.add_option('-c', '--config', dest='config', - help=u'path to configuration file') + help='path to configuration file') + parser.add_option('-p', '--plugins', dest='plugins', + help='a comma-separated list of plugins to load') parser.add_option('-h', '--help', dest='help', action='store_true', - help=u'show this help message and exit') + help='show this help message and exit') parser.add_option('--version', dest='version', action='store_true', help=optparse.SUPPRESS_HELP) @@ -1256,7 +1285,7 @@ def main(args=None): _raw_main(args) except UserError as exc: message = exc.args[0] if exc.args else None - log.error(u'error: {0}', message) + log.error('error: {0}', message) sys.exit(1) except util.HumanReadableException as exc: exc.log(log) @@ -1267,13 +1296,13 @@ def main(args=None): log.debug('{}', traceback.format_exc()) log.error('{}', exc) sys.exit(1) - except confit.ConfigError as exc: - log.error(u'configuration error: {0}', exc) + except confuse.ConfigError as exc: + log.error('configuration error: {0}', exc) sys.exit(1) except db_query.InvalidQueryError as exc: - log.error(u'invalid query: {0}', exc) + log.error('invalid query: {0}', exc) sys.exit(1) - except IOError as exc: + except OSError as exc: if exc.errno == errno.EPIPE: # "Broken pipe". End silently. sys.stderr.close() @@ -1281,11 +1310,11 @@ def main(args=None): raise except KeyboardInterrupt: # Silently ignore ^C except in verbose mode. - log.debug(u'{}', traceback.format_exc()) + log.debug('{}', traceback.format_exc()) except db.DBAccessError as exc: log.error( - u'database access error: {0}\n' - u'the library file might have a permissions problem', + 'database access error: {0}\n' + 'the library file might have a permissions problem', exc ) sys.exit(1) diff --git a/libs/common/beets/ui/commands.py b/libs/common/beets/ui/commands.py index 46ae1d93..3a337401 100644 --- a/libs/common/beets/ui/commands.py +++ b/libs/common/beets/ui/commands.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -17,7 +16,6 @@ interface. """ -from __future__ import division, absolute_import, print_function import os import re @@ -39,11 +37,10 @@ from beets.util import syspath, normpath, ancestry, displayable_path, \ from beets import library from beets import config from beets import logging -from beets.util.confit import _package_path -import six + from . import _store_dict -VARIOUS_ARTISTS = u'Various Artists' +VARIOUS_ARTISTS = 'Various Artists' PromptChoice = namedtuple('PromptChoice', ['short', 'long', 'callback']) # Global logger. @@ -75,9 +72,9 @@ def _do_query(lib, query, album, also_items=True): items = list(lib.items(query)) if album and not albums: - raise ui.UserError(u'No matching albums found.') + raise ui.UserError('No matching albums found.') elif not album and not items: - raise ui.UserError(u'No matching items found.') + raise ui.UserError('No matching items found.') return items, albums @@ -89,33 +86,34 @@ def _print_keys(query): returned row, with indentation of 2 spaces. """ for row in query: - print_(u' ' * 2 + row['key']) + print_(' ' * 2 + row['key']) def fields_func(lib, opts, args): def _print_rows(names): names.sort() - print_(u' ' + u'\n '.join(names)) + print_(' ' + '\n '.join(names)) - print_(u"Item fields:") + print_("Item fields:") _print_rows(library.Item.all_keys()) - print_(u"Album fields:") + print_("Album fields:") _print_rows(library.Album.all_keys()) with lib.transaction() as tx: # The SQL uses the DISTINCT to get unique values from the query unique_fields = 'SELECT DISTINCT key FROM (%s)' - print_(u"Item flexible attributes:") + print_("Item flexible attributes:") _print_keys(tx.query(unique_fields % library.Item._flex_table)) - print_(u"Album flexible attributes:") + print_("Album flexible attributes:") _print_keys(tx.query(unique_fields % library.Album._flex_table)) + fields_cmd = ui.Subcommand( 'fields', - help=u'show fields available for queries and format strings' + help='show fields available for queries and format strings' ) fields_cmd.func = fields_func default_commands.append(fields_cmd) @@ -126,9 +124,9 @@ default_commands.append(fields_cmd) class HelpCommand(ui.Subcommand): def __init__(self): - super(HelpCommand, self).__init__( + super().__init__( 'help', aliases=('?',), - help=u'give detailed help on a specific sub-command', + help='give detailed help on a specific sub-command', ) def func(self, lib, opts, args): @@ -136,7 +134,7 @@ class HelpCommand(ui.Subcommand): cmdname = args[0] helpcommand = self.root_parser._subcommand_for_name(cmdname) if not helpcommand: - raise ui.UserError(u"unknown command '{0}'".format(cmdname)) + raise ui.UserError(f"unknown command '{cmdname}'") helpcommand.print_help() else: self.root_parser.print_help() @@ -161,29 +159,31 @@ def disambig_string(info): if isinstance(info, hooks.AlbumInfo): if info.media: if info.mediums and info.mediums > 1: - disambig.append(u'{0}x{1}'.format( + disambig.append('{}x{}'.format( info.mediums, info.media )) else: disambig.append(info.media) if info.year: - disambig.append(six.text_type(info.year)) + disambig.append(str(info.year)) if info.country: disambig.append(info.country) if info.label: disambig.append(info.label) + if info.catalognum: + disambig.append(info.catalognum) if info.albumdisambig: disambig.append(info.albumdisambig) if disambig: - return u', '.join(disambig) + return ', '.join(disambig) def dist_string(dist): """Formats a distance (a float) as a colorized similarity percentage string. """ - out = u'%.1f%%' % ((1 - dist) * 100) + out = '%.1f%%' % ((1 - dist) * 100) if dist <= config['match']['strong_rec_thresh'].as_number(): out = ui.colorize('text_success', out) elif dist <= config['match']['medium_rec_thresh'].as_number(): @@ -206,7 +206,7 @@ def penalty_string(distance, limit=None): if penalties: if limit and len(penalties) > limit: penalties = penalties[:limit] + ['...'] - return ui.colorize('text_warning', u'(%s)' % ', '.join(penalties)) + return ui.colorize('text_warning', '(%s)' % ', '.join(penalties)) def show_change(cur_artist, cur_album, match): @@ -216,11 +216,11 @@ def show_change(cur_artist, cur_album, match): """ def show_album(artist, album): if artist: - album_description = u' %s - %s' % (artist, album) + album_description = f' {artist} - {album}' elif album: - album_description = u' %s' % album + album_description = ' %s' % album else: - album_description = u' (unknown album)' + album_description = ' (unknown album)' print_(album_description) def format_index(track_info): @@ -238,40 +238,44 @@ def show_change(cur_artist, cur_album, match): mediums = track_info.disctotal if config['per_disc_numbering']: if mediums and mediums > 1: - return u'{0}-{1}'.format(medium, medium_index) + return f'{medium}-{medium_index}' else: - return six.text_type(medium_index or index) + return str(medium_index if medium_index is not None + else index) else: - return six.text_type(index) + return str(index) # Identify the album in question. if cur_artist != match.info.artist or \ (cur_album != match.info.album and match.info.album != VARIOUS_ARTISTS): artist_l, artist_r = cur_artist or '', match.info.artist - album_l, album_r = cur_album or '', match.info.album + album_l, album_r = cur_album or '', match.info.album if artist_r == VARIOUS_ARTISTS: # Hide artists for VA releases. - artist_l, artist_r = u'', u'' + artist_l, artist_r = '', '' + + if config['artist_credit']: + artist_r = match.info.artist_credit artist_l, artist_r = ui.colordiff(artist_l, artist_r) album_l, album_r = ui.colordiff(album_l, album_r) - print_(u"Correcting tags from:") + print_("Correcting tags from:") show_album(artist_l, album_l) - print_(u"To:") + print_("To:") show_album(artist_r, album_r) else: - print_(u"Tagging:\n {0.artist} - {0.album}".format(match.info)) + print_("Tagging:\n {0.artist} - {0.album}".format(match.info)) # Data URL. if match.info.data_url: - print_(u'URL:\n %s' % match.info.data_url) + print_('URL:\n %s' % match.info.data_url) # Info line. info = [] # Similarity. - info.append(u'(Similarity: %s)' % dist_string(match.distance)) + info.append('(Similarity: %s)' % dist_string(match.distance)) # Penalties. penalties = penalty_string(match.distance) if penalties: @@ -279,7 +283,7 @@ def show_change(cur_artist, cur_album, match): # Disambiguation. disambig = disambig_string(match.info) if disambig: - info.append(ui.colorize('text_highlight_minor', u'(%s)' % disambig)) + info.append(ui.colorize('text_highlight_minor', '(%s)' % disambig)) print_(' '.join(info)) # Tracks. @@ -297,16 +301,16 @@ def show_change(cur_artist, cur_album, match): if medium != track_info.medium or disctitle != track_info.disctitle: media = match.info.media or 'Media' if match.info.mediums > 1 and track_info.disctitle: - lhs = u'%s %s: %s' % (media, track_info.medium, - track_info.disctitle) + lhs = '{} {}: {}'.format(media, track_info.medium, + track_info.disctitle) elif match.info.mediums > 1: - lhs = u'%s %s' % (media, track_info.medium) + lhs = f'{media} {track_info.medium}' elif track_info.disctitle: - lhs = u'%s: %s' % (media, track_info.disctitle) + lhs = f'{media}: {track_info.disctitle}' else: lhs = None if lhs: - lines.append((lhs, u'', 0)) + lines.append((lhs, '', 0)) medium, disctitle = track_info.medium, track_info.disctitle # Titles. @@ -327,7 +331,7 @@ def show_change(cur_artist, cur_album, match): color = 'text_highlight_minor' else: color = 'text_highlight' - templ = ui.colorize(color, u' (#{0})') + templ = ui.colorize(color, ' (#{0})') lhs += templ.format(cur_track) rhs += templ.format(new_track) lhs_width += len(cur_track) + 4 @@ -338,7 +342,7 @@ def show_change(cur_artist, cur_album, match): config['ui']['length_diff_thresh'].as_number(): cur_length = ui.human_seconds_short(item.length) new_length = ui.human_seconds_short(track_info.length) - templ = ui.colorize('text_highlight', u' ({0})') + templ = ui.colorize('text_highlight', ' ({0})') lhs += templ.format(cur_length) rhs += templ.format(new_length) lhs_width += len(cur_length) + 3 @@ -349,9 +353,9 @@ def show_change(cur_artist, cur_album, match): rhs += ' %s' % penalties if lhs != rhs: - lines.append((u' * %s' % lhs, rhs, lhs_width)) + lines.append((' * %s' % lhs, rhs, lhs_width)) elif config['import']['detail']: - lines.append((u' * %s' % lhs, '', lhs_width)) + lines.append((' * %s' % lhs, '', lhs_width)) # Print each track in two columns, or across two lines. col_width = (ui.term_width() - len(''.join([' * ', ' -> ']))) // 2 @@ -361,29 +365,36 @@ def show_change(cur_artist, cur_album, match): if not rhs: print_(lhs) elif max_width > col_width: - print_(u'%s ->\n %s' % (lhs, rhs)) + print_(f'{lhs} ->\n {rhs}') else: pad = max_width - lhs_width - print_(u'%s%s -> %s' % (lhs, ' ' * pad, rhs)) + print_('{}{} -> {}'.format(lhs, ' ' * pad, rhs)) # Missing and unmatched tracks. if match.extra_tracks: - print_(u'Missing tracks ({0}/{1} - {2:.1%}):'.format( + print_('Missing tracks ({}/{} - {:.1%}):'.format( len(match.extra_tracks), len(match.info.tracks), len(match.extra_tracks) / len(match.info.tracks) )) + pad_width = max(len(track_info.title) for track_info in + match.extra_tracks) for track_info in match.extra_tracks: - line = u' ! %s (#%s)' % (track_info.title, format_index(track_info)) + line = ' ! {0: <{width}} (#{1: >2})'.format(track_info.title, + format_index(track_info), + width=pad_width) if track_info.length: - line += u' (%s)' % ui.human_seconds_short(track_info.length) + line += ' (%s)' % ui.human_seconds_short(track_info.length) print_(ui.colorize('text_warning', line)) if match.extra_items: - print_(u'Unmatched tracks ({0}):'.format(len(match.extra_items))) + print_('Unmatched tracks ({}):'.format(len(match.extra_items))) + pad_width = max(len(item.title) for item in match.extra_items) for item in match.extra_items: - line = u' ! %s (#%s)' % (item.title, format_index(item)) + line = ' ! {0: <{width}} (#{1: >2})'.format(item.title, + format_index(item), + width=pad_width) if item.length: - line += u' (%s)' % ui.human_seconds_short(item.length) + line += ' (%s)' % ui.human_seconds_short(item.length) print_(ui.colorize('text_warning', line)) @@ -398,22 +409,22 @@ def show_item_change(item, match): cur_artist, new_artist = ui.colordiff(cur_artist, new_artist) cur_title, new_title = ui.colordiff(cur_title, new_title) - print_(u"Correcting track tags from:") - print_(u" %s - %s" % (cur_artist, cur_title)) - print_(u"To:") - print_(u" %s - %s" % (new_artist, new_title)) + print_("Correcting track tags from:") + print_(f" {cur_artist} - {cur_title}") + print_("To:") + print_(f" {new_artist} - {new_title}") else: - print_(u"Tagging track: %s - %s" % (cur_artist, cur_title)) + print_(f"Tagging track: {cur_artist} - {cur_title}") # Data URL. if match.info.data_url: - print_(u'URL:\n %s' % match.info.data_url) + print_('URL:\n %s' % match.info.data_url) # Info line. info = [] # Similarity. - info.append(u'(Similarity: %s)' % dist_string(match.distance)) + info.append('(Similarity: %s)' % dist_string(match.distance)) # Penalties. penalties = penalty_string(match.distance) if penalties: @@ -421,7 +432,7 @@ def show_item_change(item, match): # Disambiguation. disambig = disambig_string(match.info) if disambig: - info.append(ui.colorize('text_highlight_minor', u'(%s)' % disambig)) + info.append(ui.colorize('text_highlight_minor', '(%s)' % disambig)) print_(' '.join(info)) @@ -435,7 +446,7 @@ def summarize_items(items, singleton): """ summary_parts = [] if not singleton: - summary_parts.append(u"{0} items".format(len(items))) + summary_parts.append("{} items".format(len(items))) format_counts = {} for item in items: @@ -449,26 +460,31 @@ def summarize_items(items, singleton): format_counts.items(), key=lambda fmt_and_count: (-fmt_and_count[1], fmt_and_count[0]) ): - summary_parts.append('{0} {1}'.format(fmt, count)) + summary_parts.append(f'{fmt} {count}') if items: average_bitrate = sum([item.bitrate for item in items]) / len(items) total_duration = sum([item.length for item in items]) total_filesize = sum([item.filesize for item in items]) - summary_parts.append(u'{0}kbps'.format(int(average_bitrate / 1000))) + summary_parts.append('{}kbps'.format(int(average_bitrate / 1000))) + if items[0].format == "FLAC": + sample_bits = '{}kHz/{} bit'.format( + round(int(items[0].samplerate) / 1000, 1), items[0].bitdepth) + summary_parts.append(sample_bits) summary_parts.append(ui.human_seconds_short(total_duration)) summary_parts.append(ui.human_bytes(total_filesize)) - return u', '.join(summary_parts) + return ', '.join(summary_parts) def _summary_judgment(rec): """Determines whether a decision should be made without even asking the user. This occurs in quiet mode and when an action is chosen for - NONE recommendations. Return an action or None if the user should be - queried. May also print to the console if a summary judgment is - made. + NONE recommendations. Return None if the user should be queried. + Otherwise, returns an action. May also print to the console if a + summary judgment is made. """ + if config['import']['quiet']: if rec == Recommendation.strong: return importer.action.APPLY @@ -477,21 +493,21 @@ def _summary_judgment(rec): 'skip': importer.action.SKIP, 'asis': importer.action.ASIS, }) - + elif config['import']['timid']: + return None elif rec == Recommendation.none: action = config['import']['none_rec_action'].as_choice({ 'skip': importer.action.SKIP, 'asis': importer.action.ASIS, 'ask': None, }) - else: return None if action == importer.action.SKIP: - print_(u'Skipping.') + print_('Skipping.') elif action == importer.action.ASIS: - print_(u'Importing as-is.') + print_('Importing as-is.') return action @@ -526,12 +542,12 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None, # Zero candidates. if not candidates: if singleton: - print_(u"No matching recordings found.") + print_("No matching recordings found.") else: - print_(u"No matching release found for {0} tracks." + print_("No matching release found for {} tracks." .format(itemcount)) - print_(u'For help, see: ' - u'http://beets.readthedocs.org/en/latest/faq.html#nomatch') + print_('For help, see: ' + 'https://beets.readthedocs.org/en/latest/faq.html#nomatch') sel = ui.input_options(choice_opts) if sel in choice_actions: return choice_actions[sel] @@ -550,22 +566,22 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None, if not bypass_candidates: # Display list of candidates. - print_(u'Finding tags for {0} "{1} - {2}".'.format( - u'track' if singleton else u'album', + print_('Finding tags for {} "{} - {}".'.format( + 'track' if singleton else 'album', item.artist if singleton else cur_artist, item.title if singleton else cur_album, )) - print_(u'Candidates:') + print_('Candidates:') for i, match in enumerate(candidates): # Index, metadata, and distance. line = [ - u'{0}.'.format(i + 1), - u'{0} - {1}'.format( + '{}.'.format(i + 1), + '{} - {}'.format( match.info.artist, match.info.title if singleton else match.info.album, ), - u'({0})'.format(dist_string(match.distance)), + '({})'.format(dist_string(match.distance)), ] # Penalties. @@ -577,14 +593,14 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None, disambig = disambig_string(match.info) if disambig: line.append(ui.colorize('text_highlight_minor', - u'(%s)' % disambig)) + '(%s)' % disambig)) - print_(u' '.join(line)) + print_(' '.join(line)) # Ask the user for a choice. sel = ui.input_options(choice_opts, numrange=(1, len(candidates))) - if sel == u'm': + if sel == 'm': pass elif sel in choice_actions: return choice_actions[sel] @@ -608,19 +624,19 @@ def choose_candidate(candidates, singleton, rec, cur_artist=None, # Ask for confirmation. default = config['import']['default_action'].as_choice({ - u'apply': u'a', - u'skip': u's', - u'asis': u'u', - u'none': None, + 'apply': 'a', + 'skip': 's', + 'asis': 'u', + 'none': None, }) if default is None: require = True # Bell ring when user interaction is needed. if config['import']['bell']: - ui.print_(u'\a', end=u'') - sel = ui.input_options((u'Apply', u'More candidates') + choice_opts, + ui.print_('\a', end='') + sel = ui.input_options(('Apply', 'More candidates') + choice_opts, require=require, default=default) - if sel == u'a': + if sel == 'a': return match elif sel in choice_actions: return choice_actions[sel] @@ -632,8 +648,8 @@ def manual_search(session, task): Input either an artist and album (for full albums) or artist and track name (for singletons) for manual search. """ - artist = input_(u'Artist:').strip() - name = input_(u'Album:' if task.is_album else u'Track:').strip() + artist = input_('Artist:').strip() + name = input_('Album:' if task.is_album else 'Track:').strip() if task.is_album: _, _, prop = autotag.tag_album( @@ -649,8 +665,8 @@ def manual_id(session, task): Input an ID, either for an album ("release") or a track ("recording"). """ - prompt = u'Enter {0} ID:'.format(u'release' if task.is_album - else u'recording') + prompt = 'Enter {} ID:'.format('release' if task.is_album + else 'recording') search_id = input_(prompt).strip() if task.is_album: @@ -671,6 +687,7 @@ def abort_action(session, task): class TerminalImportSession(importer.ImportSession): """An import session that runs in a terminal. """ + def choose_match(self, task): """Given an initial autotagging of items, go through an interactive dance with the user to ask for a choice of metadata. Returns an @@ -678,8 +695,21 @@ class TerminalImportSession(importer.ImportSession): """ # Show what we're tagging. print_() - print_(displayable_path(task.paths, u'\n') + - u' ({0} items)'.format(len(task.items))) + print_(displayable_path(task.paths, '\n') + + ' ({} items)'.format(len(task.items))) + + # Let plugins display info or prompt the user before we go through the + # process of selecting candidate. + results = plugins.send('import_task_before_choice', + session=self, task=task) + actions = [action for action in results if action] + + if len(actions) == 1: + return actions[0] + elif len(actions) > 1: + raise plugins.PluginConflictException( + 'Only one handler for `import_task_before_choice` may return ' + 'an action.') # Take immediate action if appropriate. action = _summary_judgment(task.rec) @@ -768,48 +798,48 @@ class TerminalImportSession(importer.ImportSession): """Decide what to do when a new album or item seems similar to one that's already in the library. """ - log.warning(u"This {0} is already in the library!", - (u"album" if task.is_album else u"item")) + log.warning("This {0} is already in the library!", + ("album" if task.is_album else "item")) if config['import']['quiet']: # In quiet mode, don't prompt -- just skip. - log.info(u'Skipping.') - sel = u's' + log.info('Skipping.') + sel = 's' else: # Print some detail about the existing and new items so the # user can make an informed decision. for duplicate in found_duplicates: - print_(u"Old: " + summarize_items( + print_("Old: " + summarize_items( list(duplicate.items()) if task.is_album else [duplicate], not task.is_album, )) - print_(u"New: " + summarize_items( + print_("New: " + summarize_items( task.imported_items(), not task.is_album, )) sel = ui.input_options( - (u'Skip new', u'Keep both', u'Remove old', u'Merge all') + ('Skip new', 'Keep all', 'Remove old', 'Merge all') ) - if sel == u's': + if sel == 's': # Skip new. task.set_choice(importer.action.SKIP) - elif sel == u'k': + elif sel == 'k': # Keep both. Do nothing; leave the choice intact. pass - elif sel == u'r': + elif sel == 'r': # Remove old. task.should_remove_duplicates = True - elif sel == u'm': + elif sel == 'm': task.should_merge_duplicates = True else: assert False def should_resume(self, path): - return ui.input_yn(u"Import of the directory:\n{0}\n" - u"was interrupted. Resume (Y/n)?" + return ui.input_yn("Import of the directory:\n{}\n" + "was interrupted. Resume (Y/n)?" .format(displayable_path(path))) def _get_choices(self, task): @@ -830,22 +860,22 @@ class TerminalImportSession(importer.ImportSession): """ # Standard, built-in choices. choices = [ - PromptChoice(u's', u'Skip', + PromptChoice('s', 'Skip', lambda s, t: importer.action.SKIP), - PromptChoice(u'u', u'Use as-is', + PromptChoice('u', 'Use as-is', lambda s, t: importer.action.ASIS) ] if task.is_album: choices += [ - PromptChoice(u't', u'as Tracks', + PromptChoice('t', 'as Tracks', lambda s, t: importer.action.TRACKS), - PromptChoice(u'g', u'Group albums', + PromptChoice('g', 'Group albums', lambda s, t: importer.action.ALBUMS), ] choices += [ - PromptChoice(u'e', u'Enter search', manual_search), - PromptChoice(u'i', u'enter Id', manual_id), - PromptChoice(u'b', u'aBort', abort_action), + PromptChoice('e', 'Enter search', manual_search), + PromptChoice('i', 'enter Id', manual_id), + PromptChoice('b', 'aBort', abort_action), ] # Send the before_choose_candidate event and flatten list. @@ -855,7 +885,7 @@ class TerminalImportSession(importer.ImportSession): # Add a "dummy" choice for the other baked-in option, for # duplicate checking. all_choices = [ - PromptChoice(u'a', u'Apply', None), + PromptChoice('a', 'Apply', None), ] + choices + extra_choices # Check for conflicts. @@ -868,8 +898,8 @@ class TerminalImportSession(importer.ImportSession): # Keep the first of the choices, removing the rest. dup_choices = [c for c in all_choices if c.short == short] for c in dup_choices[1:]: - log.warning(u"Prompt choice '{0}' removed due to conflict " - u"with '{1}' (short letter: '{2}')", + log.warning("Prompt choice '{0}' removed due to conflict " + "with '{1}' (short letter: '{2}')", c.long, dup_choices[0].long, c.short) extra_choices.remove(c) @@ -886,21 +916,21 @@ def import_files(lib, paths, query): # Check the user-specified directories. for path in paths: if not os.path.exists(syspath(normpath(path))): - raise ui.UserError(u'no such file or directory: {0}'.format( + raise ui.UserError('no such file or directory: {}'.format( displayable_path(path))) # Check parameter consistency. if config['import']['quiet'] and config['import']['timid']: - raise ui.UserError(u"can't be both quiet and timid") + raise ui.UserError("can't be both quiet and timid") # Open the log. if config['import']['log'].get() is not None: logpath = syspath(config['import']['log'].as_filename()) try: loghandler = logging.FileHandler(logpath) - except IOError: - raise ui.UserError(u"could not open log file for writing: " - u"{0}".format(displayable_path(logpath))) + except OSError: + raise ui.UserError("could not open log file for writing: " + "{}".format(displayable_path(logpath))) else: loghandler = None @@ -931,111 +961,111 @@ def import_func(lib, opts, args): query = None paths = args if not paths: - raise ui.UserError(u'no path specified') + raise ui.UserError('no path specified') - # On Python 2, we get filenames as raw bytes, which is what we - # need. On Python 3, we need to undo the "helpful" conversion to - # Unicode strings to get the real bytestring filename. - if not six.PY2: - paths = [p.encode(util.arg_encoding(), 'surrogateescape') - for p in paths] + # On Python 2, we used to get filenames as raw bytes, which is + # what we need. On Python 3, we need to undo the "helpful" + # conversion to Unicode strings to get the real bytestring + # filename. + paths = [p.encode(util.arg_encoding(), 'surrogateescape') + for p in paths] import_files(lib, paths, query) import_cmd = ui.Subcommand( - u'import', help=u'import new music', aliases=(u'imp', u'im') + 'import', help='import new music', aliases=('imp', 'im') ) import_cmd.parser.add_option( - u'-c', u'--copy', action='store_true', default=None, - help=u"copy tracks into library directory (default)" + '-c', '--copy', action='store_true', default=None, + help="copy tracks into library directory (default)" ) import_cmd.parser.add_option( - u'-C', u'--nocopy', action='store_false', dest='copy', - help=u"don't copy tracks (opposite of -c)" + '-C', '--nocopy', action='store_false', dest='copy', + help="don't copy tracks (opposite of -c)" ) import_cmd.parser.add_option( - u'-m', u'--move', action='store_true', dest='move', - help=u"move tracks into the library (overrides -c)" + '-m', '--move', action='store_true', dest='move', + help="move tracks into the library (overrides -c)" ) import_cmd.parser.add_option( - u'-w', u'--write', action='store_true', default=None, - help=u"write new metadata to files' tags (default)" + '-w', '--write', action='store_true', default=None, + help="write new metadata to files' tags (default)" ) import_cmd.parser.add_option( - u'-W', u'--nowrite', action='store_false', dest='write', - help=u"don't write metadata (opposite of -w)" + '-W', '--nowrite', action='store_false', dest='write', + help="don't write metadata (opposite of -w)" ) import_cmd.parser.add_option( - u'-a', u'--autotag', action='store_true', dest='autotag', - help=u"infer tags for imported files (default)" + '-a', '--autotag', action='store_true', dest='autotag', + help="infer tags for imported files (default)" ) import_cmd.parser.add_option( - u'-A', u'--noautotag', action='store_false', dest='autotag', - help=u"don't infer tags for imported files (opposite of -a)" + '-A', '--noautotag', action='store_false', dest='autotag', + help="don't infer tags for imported files (opposite of -a)" ) import_cmd.parser.add_option( - u'-p', u'--resume', action='store_true', default=None, - help=u"resume importing if interrupted" + '-p', '--resume', action='store_true', default=None, + help="resume importing if interrupted" ) import_cmd.parser.add_option( - u'-P', u'--noresume', action='store_false', dest='resume', - help=u"do not try to resume importing" + '-P', '--noresume', action='store_false', dest='resume', + help="do not try to resume importing" ) import_cmd.parser.add_option( - u'-q', u'--quiet', action='store_true', dest='quiet', - help=u"never prompt for input: skip albums instead" + '-q', '--quiet', action='store_true', dest='quiet', + help="never prompt for input: skip albums instead" ) import_cmd.parser.add_option( - u'-l', u'--log', dest='log', - help=u'file to log untaggable albums for later review' + '-l', '--log', dest='log', + help='file to log untaggable albums for later review' ) import_cmd.parser.add_option( - u'-s', u'--singletons', action='store_true', - help=u'import individual tracks instead of full albums' + '-s', '--singletons', action='store_true', + help='import individual tracks instead of full albums' ) import_cmd.parser.add_option( - u'-t', u'--timid', dest='timid', action='store_true', - help=u'always confirm all actions' + '-t', '--timid', dest='timid', action='store_true', + help='always confirm all actions' ) import_cmd.parser.add_option( - u'-L', u'--library', dest='library', action='store_true', - help=u'retag items matching a query' + '-L', '--library', dest='library', action='store_true', + help='retag items matching a query' ) import_cmd.parser.add_option( - u'-i', u'--incremental', dest='incremental', action='store_true', - help=u'skip already-imported directories' + '-i', '--incremental', dest='incremental', action='store_true', + help='skip already-imported directories' ) import_cmd.parser.add_option( - u'-I', u'--noincremental', dest='incremental', action='store_false', - help=u'do not skip already-imported directories' + '-I', '--noincremental', dest='incremental', action='store_false', + help='do not skip already-imported directories' ) import_cmd.parser.add_option( - u'--from-scratch', dest='from_scratch', action='store_true', - help=u'erase existing metadata before applying new metadata' + '--from-scratch', dest='from_scratch', action='store_true', + help='erase existing metadata before applying new metadata' ) import_cmd.parser.add_option( - u'--flat', dest='flat', action='store_true', - help=u'import an entire tree as a single album' + '--flat', dest='flat', action='store_true', + help='import an entire tree as a single album' ) import_cmd.parser.add_option( - u'-g', u'--group-albums', dest='group_albums', action='store_true', - help=u'group tracks in a folder into separate albums' + '-g', '--group-albums', dest='group_albums', action='store_true', + help='group tracks in a folder into separate albums' ) import_cmd.parser.add_option( - u'--pretend', dest='pretend', action='store_true', - help=u'just print the files to import' + '--pretend', dest='pretend', action='store_true', + help='just print the files to import' ) import_cmd.parser.add_option( - u'-S', u'--search-id', dest='search_ids', action='append', + '-S', '--search-id', dest='search_ids', action='append', metavar='ID', - help=u'restrict matching to a specific metadata backend ID' + help='restrict matching to a specific metadata backend ID' ) import_cmd.parser.add_option( - u'--set', dest='set_fields', action='callback', + '--set', dest='set_fields', action='callback', callback=_store_dict, metavar='FIELD=VALUE', - help=u'set the given fields to the supplied values' + help='set the given fields to the supplied values' ) import_cmd.func = import_func default_commands.append(import_cmd) @@ -1043,7 +1073,7 @@ default_commands.append(import_cmd) # list: Query and show library contents. -def list_items(lib, query, album, fmt=u''): +def list_items(lib, query, album, fmt=''): """Print out items in lib matching query. If album, then search for albums instead of single items. """ @@ -1059,9 +1089,9 @@ def list_func(lib, opts, args): list_items(lib, decargs(args), opts.album) -list_cmd = ui.Subcommand(u'list', help=u'query the library', aliases=(u'ls',)) -list_cmd.parser.usage += u"\n" \ - u'Example: %prog -f \'$album: $title\' artist:beatles' +list_cmd = ui.Subcommand('list', help='query the library', aliases=('ls',)) +list_cmd.parser.usage += "\n" \ + 'Example: %prog -f \'$album: $title\' artist:beatles' list_cmd.parser.add_all_common_options() list_cmd.func = list_func default_commands.append(list_cmd) @@ -1089,7 +1119,7 @@ def update_items(lib, query, album, move, pretend, fields): # Item deleted? if not os.path.exists(syspath(item.path)): ui.print_(format(item)) - ui.print_(ui.colorize('text_error', u' deleted')) + ui.print_(ui.colorize('text_error', ' deleted')) if not pretend: item.remove(True) affected_albums.add(item.album_id) @@ -1097,7 +1127,7 @@ def update_items(lib, query, album, move, pretend, fields): # Did the item change since last checked? if item.current_mtime() <= item.mtime: - log.debug(u'skipping {0} because mtime is up to date ({1})', + log.debug('skipping {0} because mtime is up to date ({1})', displayable_path(item.path), item.mtime) continue @@ -1105,7 +1135,7 @@ def update_items(lib, query, album, move, pretend, fields): try: item.read() except library.ReadError as exc: - log.error(u'error reading {0}: {1}', + log.error('error reading {0}: {1}', displayable_path(item.path), exc) continue @@ -1116,7 +1146,7 @@ def update_items(lib, query, album, move, pretend, fields): old_item = lib.get_item(item.id) if old_item.albumartist == old_item.artist == item.artist: item.albumartist = old_item.albumartist - item._dirty.discard(u'albumartist') + item._dirty.discard('albumartist') # Check for and display changes. changed = ui.show_model_changes( @@ -1149,7 +1179,7 @@ def update_items(lib, query, album, move, pretend, fields): continue album = lib.get_album(album_id) if not album: # Empty albums have already been removed. - log.debug(u'emptied album {0}', album_id) + log.debug('emptied album {0}', album_id) continue first_item = album.items().get() @@ -1160,42 +1190,48 @@ def update_items(lib, query, album, move, pretend, fields): # Move album art (and any inconsistent items). if move and lib.directory in ancestry(first_item.path): - log.debug(u'moving album {0}', album_id) + log.debug('moving album {0}', album_id) # Manually moving and storing the album. items = list(album.items()) for item in items: - item.move(store=False) + item.move(store=False, with_album=False) item.store(fields=fields) album.move(store=False) album.store(fields=fields) def update_func(lib, opts, args): + # Verify that the library folder exists to prevent accidental wipes. + if not os.path.isdir(lib.directory): + ui.print_("Library path is unavailable or does not exist.") + ui.print_(lib.directory) + if not ui.input_yn("Are you sure you want to continue (y/n)?", True): + return update_items(lib, decargs(args), opts.album, ui.should_move(opts.move), opts.pretend, opts.fields) update_cmd = ui.Subcommand( - u'update', help=u'update the library', aliases=(u'upd', u'up',) + 'update', help='update the library', aliases=('upd', 'up',) ) update_cmd.parser.add_album_option() update_cmd.parser.add_format_option() update_cmd.parser.add_option( - u'-m', u'--move', action='store_true', dest='move', - help=u"move files in the library directory" + '-m', '--move', action='store_true', dest='move', + help="move files in the library directory" ) update_cmd.parser.add_option( - u'-M', u'--nomove', action='store_false', dest='move', - help=u"don't move files in library" + '-M', '--nomove', action='store_false', dest='move', + help="don't move files in library" ) update_cmd.parser.add_option( - u'-p', u'--pretend', action='store_true', - help=u"show all changes but do nothing" + '-p', '--pretend', action='store_true', + help="show all changes but do nothing" ) update_cmd.parser.add_option( - u'-F', u'--field', default=None, action='append', dest='fields', - help=u'list of fields to update' + '-F', '--field', default=None, action='append', dest='fields', + help='list of fields to update' ) update_cmd.func = update_func default_commands.append(update_cmd) @@ -1209,31 +1245,53 @@ def remove_items(lib, query, album, delete, force): """ # Get the matching items. items, albums = _do_query(lib, query, album) + objs = albums if album else items # Confirm file removal if not forcing removal. if not force: # Prepare confirmation with user. - print_() + album_str = " in {} album{}".format( + len(albums), 's' if len(albums) > 1 else '' + ) if album else "" + if delete: - fmt = u'$path - $title' - prompt = u'Really DELETE %i file%s (y/n)?' % \ - (len(items), 's' if len(items) > 1 else '') + fmt = '$path - $title' + prompt = 'Really DELETE' + prompt_all = 'Really DELETE {} file{}{}'.format( + len(items), 's' if len(items) > 1 else '', album_str + ) else: - fmt = u'' - prompt = u'Really remove %i item%s from the library (y/n)?' % \ - (len(items), 's' if len(items) > 1 else '') + fmt = '' + prompt = 'Really remove from the library?' + prompt_all = 'Really remove {} item{}{} from the library?'.format( + len(items), 's' if len(items) > 1 else '', album_str + ) + + # Helpers for printing affected items + def fmt_track(t): + ui.print_(format(t, fmt)) + + def fmt_album(a): + ui.print_() + for i in a.items(): + fmt_track(i) + + fmt_obj = fmt_album if album else fmt_track # Show all the items. - for item in items: - ui.print_(format(item, fmt)) + for o in objs: + fmt_obj(o) # Confirm with user. - if not ui.input_yn(prompt, True): - return + objs = ui.input_select_objects(prompt, objs, fmt_obj, + prompt_all=prompt_all) + + if not objs: + return # Remove (and possibly delete) items. with lib.transaction(): - for obj in (albums if album else items): + for obj in objs: obj.remove(delete) @@ -1242,15 +1300,15 @@ def remove_func(lib, opts, args): remove_cmd = ui.Subcommand( - u'remove', help=u'remove matching items from the library', aliases=(u'rm',) + 'remove', help='remove matching items from the library', aliases=('rm',) ) remove_cmd.parser.add_option( - u"-d", u"--delete", action="store_true", - help=u"also remove files from disk" + "-d", "--delete", action="store_true", + help="also remove files from disk" ) remove_cmd.parser.add_option( - u"-f", u"--force", action="store_true", - help=u"do not ask when removing items" + "-f", "--force", action="store_true", + help="do not ask when removing items" ) remove_cmd.parser.add_album_option() remove_cmd.func = remove_func @@ -1275,7 +1333,7 @@ def show_stats(lib, query, exact): try: total_size += os.path.getsize(syspath(item.path)) except OSError as exc: - log.info(u'could not get size of {}: {}', item.path, exc) + log.info('could not get size of {}: {}', item.path, exc) else: total_size += int(item.length * item.bitrate / 8) total_time += item.length @@ -1285,20 +1343,20 @@ def show_stats(lib, query, exact): if item.album_id: albums.add(item.album_id) - size_str = u'' + ui.human_bytes(total_size) + size_str = '' + ui.human_bytes(total_size) if exact: - size_str += u' ({0} bytes)'.format(total_size) + size_str += f' ({total_size} bytes)' - print_(u"""Tracks: {0} -Total time: {1}{2} -{3}: {4} -Artists: {5} -Albums: {6} -Album artists: {7}""".format( + print_("""Tracks: {} +Total time: {}{} +{}: {} +Artists: {} +Albums: {} +Album artists: {}""".format( total_items, ui.human_seconds(total_time), - u' ({0:.2f} seconds)'.format(total_time) if exact else '', - u'Total size' if exact else u'Approximate total size', + f' ({total_time:.2f} seconds)' if exact else '', + 'Total size' if exact else 'Approximate total size', size_str, len(artists), len(albums), @@ -1311,11 +1369,11 @@ def stats_func(lib, opts, args): stats_cmd = ui.Subcommand( - u'stats', help=u'show statistics about the library or a query' + 'stats', help='show statistics about the library or a query' ) stats_cmd.parser.add_option( - u'-e', u'--exact', action='store_true', - help=u'exact size and time' + '-e', '--exact', action='store_true', + help='exact size and time' ) stats_cmd.func = stats_func default_commands.append(stats_cmd) @@ -1324,18 +1382,18 @@ default_commands.append(stats_cmd) # version: Show current beets version. def show_version(lib, opts, args): - print_(u'beets version %s' % beets.__version__) - print_(u'Python version {}'.format(python_version())) + print_('beets version %s' % beets.__version__) + print_(f'Python version {python_version()}') # Show plugins. names = sorted(p.name for p in plugins.find_plugins()) if names: - print_(u'plugins:', ', '.join(names)) + print_('plugins:', ', '.join(names)) else: - print_(u'no plugins loaded') + print_('no plugins loaded') version_cmd = ui.Subcommand( - u'version', help=u'output version information' + 'version', help='output version information' ) version_cmd.func = show_version default_commands.append(version_cmd) @@ -1362,31 +1420,31 @@ def modify_items(lib, mods, dels, query, write, move, album, confirm): # Apply changes *temporarily*, preview them, and collect modified # objects. - print_(u'Modifying {0} {1}s.' - .format(len(objs), u'album' if album else u'item')) - changed = set() + print_('Modifying {} {}s.' + .format(len(objs), 'album' if album else 'item')) + changed = [] for obj in objs: - if print_and_modify(obj, mods, dels): - changed.add(obj) + if print_and_modify(obj, mods, dels) and obj not in changed: + changed.append(obj) # Still something to do? if not changed: - print_(u'No changes to make.') + print_('No changes to make.') return # Confirm action. if confirm: if write and move: - extra = u', move and write tags' + extra = ', move and write tags' elif write: - extra = u' and write tags' + extra = ' and write tags' elif move: - extra = u' and move' + extra = ' and move' else: - extra = u'' + extra = '' changed = ui.input_select_objects( - u'Really modify%s' % extra, changed, + 'Really modify%s' % extra, changed, lambda o: print_and_modify(o, mods, dels) ) @@ -1434,35 +1492,35 @@ def modify_parse_args(args): def modify_func(lib, opts, args): query, mods, dels = modify_parse_args(decargs(args)) if not mods and not dels: - raise ui.UserError(u'no modifications specified') + raise ui.UserError('no modifications specified') modify_items(lib, mods, dels, query, ui.should_write(opts.write), ui.should_move(opts.move), opts.album, not opts.yes) modify_cmd = ui.Subcommand( - u'modify', help=u'change metadata fields', aliases=(u'mod',) + 'modify', help='change metadata fields', aliases=('mod',) ) modify_cmd.parser.add_option( - u'-m', u'--move', action='store_true', dest='move', - help=u"move files in the library directory" + '-m', '--move', action='store_true', dest='move', + help="move files in the library directory" ) modify_cmd.parser.add_option( - u'-M', u'--nomove', action='store_false', dest='move', - help=u"don't move files in library" + '-M', '--nomove', action='store_false', dest='move', + help="don't move files in library" ) modify_cmd.parser.add_option( - u'-w', u'--write', action='store_true', default=None, - help=u"write new metadata to files' tags (default)" + '-w', '--write', action='store_true', default=None, + help="write new metadata to files' tags (default)" ) modify_cmd.parser.add_option( - u'-W', u'--nowrite', action='store_false', dest='write', - help=u"don't write metadata (opposite of -w)" + '-W', '--nowrite', action='store_false', dest='write', + help="don't write metadata (opposite of -w)" ) modify_cmd.parser.add_album_option() modify_cmd.parser.add_format_option(target='item') modify_cmd.parser.add_option( - u'-y', u'--yes', action='store_true', - help=u'skip confirmation' + '-y', '--yes', action='store_true', + help='skip confirmation' ) modify_cmd.func = modify_func default_commands.append(modify_cmd) @@ -1478,18 +1536,28 @@ def move_items(lib, dest, query, copy, album, pretend, confirm=False, """ items, albums = _do_query(lib, query, album, False) objs = albums if album else items + num_objs = len(objs) # Filter out files that don't need to be moved. - isitemmoved = lambda item: item.path != item.destination(basedir=dest) - isalbummoved = lambda album: any(isitemmoved(i) for i in album.items()) + def isitemmoved(item): + return item.path != item.destination(basedir=dest) + + def isalbummoved(album): + return any(isitemmoved(i) for i in album.items()) + objs = [o for o in objs if (isalbummoved if album else isitemmoved)(o)] + num_unmoved = num_objs - len(objs) + # Report unmoved files that match the query. + unmoved_msg = '' + if num_unmoved > 0: + unmoved_msg = f' ({num_unmoved} already in place)' copy = copy or export # Exporting always copies. - action = u'Copying' if copy else u'Moving' - act = u'copy' if copy else u'move' - entity = u'album' if album else u'item' - log.info(u'{0} {1} {2}{3}.', action, len(objs), entity, - u's' if len(objs) != 1 else u'') + action = 'Copying' if copy else 'Moving' + act = 'copy' if copy else 'move' + entity = 'album' if album else 'item' + log.info('{0} {1} {2}{3}{4}.', action, len(objs), entity, + 's' if len(objs) != 1 else '', unmoved_msg) if not objs: return @@ -1503,12 +1571,12 @@ def move_items(lib, dest, query, copy, album, pretend, confirm=False, else: if confirm: objs = ui.input_select_objects( - u'Really %s' % act, objs, + 'Really %s' % act, objs, lambda o: show_path_changes( [(o.path, o.destination(basedir=dest))])) for obj in objs: - log.debug(u'moving: {0}', util.displayable_path(obj.path)) + log.debug('moving: {0}', util.displayable_path(obj.path)) if export: # Copy without affecting the database. @@ -1527,34 +1595,34 @@ def move_func(lib, opts, args): if dest is not None: dest = normpath(dest) if not os.path.isdir(dest): - raise ui.UserError(u'no such directory: %s' % dest) + raise ui.UserError('no such directory: %s' % dest) move_items(lib, dest, decargs(args), opts.copy, opts.album, opts.pretend, opts.timid, opts.export) move_cmd = ui.Subcommand( - u'move', help=u'move or copy items', aliases=(u'mv',) + 'move', help='move or copy items', aliases=('mv',) ) move_cmd.parser.add_option( - u'-d', u'--dest', metavar='DIR', dest='dest', - help=u'destination directory' + '-d', '--dest', metavar='DIR', dest='dest', + help='destination directory' ) move_cmd.parser.add_option( - u'-c', u'--copy', default=False, action='store_true', - help=u'copy instead of moving' + '-c', '--copy', default=False, action='store_true', + help='copy instead of moving' ) move_cmd.parser.add_option( - u'-p', u'--pretend', default=False, action='store_true', - help=u'show how files would be moved, but don\'t touch anything' + '-p', '--pretend', default=False, action='store_true', + help='show how files would be moved, but don\'t touch anything' ) move_cmd.parser.add_option( - u'-t', u'--timid', dest='timid', action='store_true', - help=u'always confirm all actions' + '-t', '--timid', dest='timid', action='store_true', + help='always confirm all actions' ) move_cmd.parser.add_option( - u'-e', u'--export', default=False, action='store_true', - help=u'copy without changing the database path' + '-e', '--export', default=False, action='store_true', + help='copy without changing the database path' ) move_cmd.parser.add_album_option() move_cmd.func = move_func @@ -1572,14 +1640,14 @@ def write_items(lib, query, pretend, force): for item in items: # Item deleted? if not os.path.exists(syspath(item.path)): - log.info(u'missing file: {0}', util.displayable_path(item.path)) + log.info('missing file: {0}', util.displayable_path(item.path)) continue # Get an Item object reflecting the "clean" (on-disk) state. try: clean_item = library.Item.from_path(item.path) except library.ReadError as exc: - log.error(u'error reading {0}: {1}', + log.error('error reading {0}: {1}', displayable_path(item.path), exc) continue @@ -1596,14 +1664,14 @@ def write_func(lib, opts, args): write_items(lib, decargs(args), opts.pretend, opts.force) -write_cmd = ui.Subcommand(u'write', help=u'write tag information to files') +write_cmd = ui.Subcommand('write', help='write tag information to files') write_cmd.parser.add_option( - u'-p', u'--pretend', action='store_true', - help=u"show all changes but do nothing" + '-p', '--pretend', action='store_true', + help="show all changes but do nothing" ) write_cmd.parser.add_option( - u'-f', u'--force', action='store_true', - help=u"write tags even if the existing tags match the database" + '-f', '--force', action='store_true', + help="write tags even if the existing tags match the database" ) write_cmd.func = write_func default_commands.append(write_cmd) @@ -1640,7 +1708,10 @@ def config_func(lib, opts, args): # Dump configuration. else: config_out = config.dump(full=opts.defaults, redact=opts.redact) - print_(util.text_string(config_out)) + if config_out.strip() != '{}': + print_(util.text_string(config_out)) + else: + print("Empty configuration") def config_edit(): @@ -1654,29 +1725,30 @@ def config_edit(): open(path, 'w+').close() util.interactive_open([path], editor) except OSError as exc: - message = u"Could not edit configuration: {0}".format(exc) + message = f"Could not edit configuration: {exc}" if not editor: - message += u". Please set the EDITOR environment variable" + message += ". Please set the EDITOR environment variable" raise ui.UserError(message) -config_cmd = ui.Subcommand(u'config', - help=u'show or edit the user configuration') + +config_cmd = ui.Subcommand('config', + help='show or edit the user configuration') config_cmd.parser.add_option( - u'-p', u'--paths', action='store_true', - help=u'show files that configuration was loaded from' + '-p', '--paths', action='store_true', + help='show files that configuration was loaded from' ) config_cmd.parser.add_option( - u'-e', u'--edit', action='store_true', - help=u'edit user configuration with $EDITOR' + '-e', '--edit', action='store_true', + help='edit user configuration with $EDITOR' ) config_cmd.parser.add_option( - u'-d', u'--defaults', action='store_true', - help=u'include the default configuration' + '-d', '--defaults', action='store_true', + help='include the default configuration' ) config_cmd.parser.add_option( - u'-c', u'--clear', action='store_false', + '-c', '--clear', action='store_false', dest='redact', default=True, - help=u'do not redact sensitive fields' + help='do not redact sensitive fields' ) config_cmd.func = config_func default_commands.append(config_cmd) @@ -1686,19 +1758,20 @@ default_commands.append(config_cmd) def print_completion(*args): for line in completion_script(default_commands + plugins.commands()): - print_(line, end=u'') + print_(line, end='') if not any(map(os.path.isfile, BASH_COMPLETION_PATHS)): - log.warning(u'Warning: Unable to find the bash-completion package. ' - u'Command line completion might not work.') + log.warning('Warning: Unable to find the bash-completion package. ' + 'Command line completion might not work.') + BASH_COMPLETION_PATHS = map(syspath, [ - u'/etc/bash_completion', - u'/usr/share/bash-completion/bash_completion', - u'/usr/local/share/bash-completion/bash_completion', + '/etc/bash_completion', + '/usr/share/bash-completion/bash_completion', + '/usr/local/share/bash-completion/bash_completion', # SmartOS - u'/opt/local/share/bash-completion/bash_completion', + '/opt/local/share/bash-completion/bash_completion', # Homebrew (before bash-completion2) - u'/usr/local/etc/bash_completion', + '/usr/local/etc/bash_completion', ]) @@ -1708,8 +1781,8 @@ def completion_script(commands): ``commands`` is alist of ``ui.Subcommand`` instances to generate completion data for. """ - base_script = os.path.join(_package_path('beets.ui'), 'completion_base.sh') - with open(base_script, 'r') as base_script: + base_script = os.path.join(os.path.dirname(__file__), 'completion_base.sh') + with open(base_script) as base_script: yield util.text_string(base_script.read()) options = {} @@ -1725,12 +1798,12 @@ def completion_script(commands): if re.match(r'^\w+$', alias): aliases[alias] = name - options[name] = {u'flags': [], u'opts': []} + options[name] = {'flags': [], 'opts': []} for opts in cmd.parser._get_all_options()[1:]: if opts.action in ('store_true', 'store_false'): - option_type = u'flags' + option_type = 'flags' else: - option_type = u'opts' + option_type = 'opts' options[name][option_type].extend( opts._short_opts + opts._long_opts @@ -1738,31 +1811,31 @@ def completion_script(commands): # Add global options options['_global'] = { - u'flags': [u'-v', u'--verbose'], - u'opts': - u'-l --library -c --config -d --directory -h --help'.split(u' ') + 'flags': ['-v', '--verbose'], + 'opts': + '-l --library -c --config -d --directory -h --help'.split(' ') } # Add flags common to all commands options['_common'] = { - u'flags': [u'-h', u'--help'] + 'flags': ['-h', '--help'] } # Start generating the script - yield u"_beet() {\n" + yield "_beet() {\n" # Command names - yield u" local commands='%s'\n" % ' '.join(command_names) - yield u"\n" + yield " local commands='%s'\n" % ' '.join(command_names) + yield "\n" # Command aliases - yield u" local aliases='%s'\n" % ' '.join(aliases.keys()) + yield " local aliases='%s'\n" % ' '.join(aliases.keys()) for alias, cmd in aliases.items(): - yield u" local alias__%s=%s\n" % (alias.replace('-', '_'), cmd) - yield u'\n' + yield " local alias__{}={}\n".format(alias.replace('-', '_'), cmd) + yield '\n' # Fields - yield u" fields='%s'\n" % ' '.join( + yield " fields='%s'\n" % ' '.join( set( list(library.Item._fields.keys()) + list(library.Album._fields.keys()) @@ -1773,17 +1846,17 @@ def completion_script(commands): for cmd, opts in options.items(): for option_type, option_list in opts.items(): if option_list: - option_list = u' '.join(option_list) - yield u" local %s__%s='%s'\n" % ( + option_list = ' '.join(option_list) + yield " local {}__{}='{}'\n".format( option_type, cmd.replace('-', '_'), option_list) - yield u' _beet_dispatch\n' - yield u'}\n' + yield ' _beet_dispatch\n' + yield '}\n' completion_cmd = ui.Subcommand( 'completion', - help=u'print shell script that provides command line completion' + help='print shell script that provides command line completion' ) completion_cmd.func = print_completion completion_cmd.hide = True diff --git a/libs/common/beets/util/__init__.py b/libs/common/beets/util/__init__.py index 69870edf..d58bb28e 100644 --- a/libs/common/beets/util/__init__.py +++ b/libs/common/beets/util/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,28 +14,28 @@ """Miscellaneous utility functions.""" -from __future__ import division, absolute_import, print_function import os import sys import errno import locale import re +import tempfile import shutil import fnmatch -from collections import Counter +import functools +from collections import Counter, namedtuple +from multiprocessing.pool import ThreadPool import traceback import subprocess import platform import shlex from beets.util import hidden -import six from unidecode import unidecode from enum import Enum MAX_FILENAME_LENGTH = 200 -WINDOWS_MAGIC_PREFIX = u'\\\\?\\' -SNI_SUPPORTED = sys.version_info >= (2, 7, 9) +WINDOWS_MAGIC_PREFIX = '\\\\?\\' class HumanReadableException(Exception): @@ -58,27 +57,27 @@ class HumanReadableException(Exception): self.reason = reason self.verb = verb self.tb = tb - super(HumanReadableException, self).__init__(self.get_message()) + super().__init__(self.get_message()) def _gerund(self): """Generate a (likely) gerund form of the English verb. """ - if u' ' in self.verb: + if ' ' in self.verb: return self.verb - gerund = self.verb[:-1] if self.verb.endswith(u'e') else self.verb - gerund += u'ing' + gerund = self.verb[:-1] if self.verb.endswith('e') else self.verb + gerund += 'ing' return gerund def _reasonstr(self): """Get the reason as a string.""" - if isinstance(self.reason, six.text_type): + if isinstance(self.reason, str): return self.reason elif isinstance(self.reason, bytes): return self.reason.decode('utf-8', 'ignore') elif hasattr(self.reason, 'strerror'): # i.e., EnvironmentError return self.reason.strerror else: - return u'"{0}"'.format(six.text_type(self.reason)) + return '"{}"'.format(str(self.reason)) def get_message(self): """Create the human-readable description of the error, sans @@ -92,7 +91,7 @@ class HumanReadableException(Exception): """ if self.tb: logger.debug(self.tb) - logger.error(u'{0}: {1}', self.error_kind, self.args[0]) + logger.error('{0}: {1}', self.error_kind, self.args[0]) class FilesystemError(HumanReadableException): @@ -100,29 +99,30 @@ class FilesystemError(HumanReadableException): via a function in this module. The `paths` field is a sequence of pathnames involved in the operation. """ + def __init__(self, reason, verb, paths, tb=None): self.paths = paths - super(FilesystemError, self).__init__(reason, verb, tb) + super().__init__(reason, verb, tb) def get_message(self): # Use a nicer English phrasing for some specific verbs. if self.verb in ('move', 'copy', 'rename'): - clause = u'while {0} {1} to {2}'.format( + clause = 'while {} {} to {}'.format( self._gerund(), displayable_path(self.paths[0]), displayable_path(self.paths[1]) ) elif self.verb in ('delete', 'write', 'create', 'read'): - clause = u'while {0} {1}'.format( + clause = 'while {} {}'.format( self._gerund(), displayable_path(self.paths[0]) ) else: - clause = u'during {0} of paths {1}'.format( - self.verb, u', '.join(displayable_path(p) for p in self.paths) + clause = 'during {} of paths {}'.format( + self.verb, ', '.join(displayable_path(p) for p in self.paths) ) - return u'{0} {1}'.format(self._reasonstr(), clause) + return f'{self._reasonstr()} {clause}' class MoveOperation(Enum): @@ -132,6 +132,8 @@ class MoveOperation(Enum): COPY = 1 LINK = 2 HARDLINK = 3 + REFLINK = 4 + REFLINK_AUTO = 5 def normpath(path): @@ -182,7 +184,7 @@ def sorted_walk(path, ignore=(), ignore_hidden=False, logger=None): contents = os.listdir(syspath(path)) except OSError as exc: if logger: - logger.warning(u'could not list directory {0}: {1}'.format( + logger.warning('could not list directory {}: {}'.format( displayable_path(path), exc.strerror )) return @@ -195,6 +197,10 @@ def sorted_walk(path, ignore=(), ignore_hidden=False, logger=None): skip = False for pat in ignore: if fnmatch.fnmatch(base, pat): + if logger: + logger.debug('ignoring {} due to ignore rule {}'.format( + base, pat + )) skip = True break if skip: @@ -217,8 +223,14 @@ def sorted_walk(path, ignore=(), ignore_hidden=False, logger=None): for base in dirs: cur = os.path.join(path, base) # yield from sorted_walk(...) - for res in sorted_walk(cur, ignore, ignore_hidden, logger): - yield res + yield from sorted_walk(cur, ignore, ignore_hidden, logger) + + +def path_as_posix(path): + """Return the string representation of the path with forward (/) + slashes. + """ + return path.replace(b'\\', b'/') def mkdirall(path): @@ -229,7 +241,7 @@ def mkdirall(path): if not os.path.isdir(syspath(ancestor)): try: os.mkdir(syspath(ancestor)) - except (OSError, IOError) as exc: + except OSError as exc: raise FilesystemError(exc, 'create', (ancestor,), traceback.format_exc()) @@ -282,13 +294,13 @@ def prune_dirs(path, root=None, clutter=('.DS_Store', 'Thumbs.db')): continue clutter = [bytestring_path(c) for c in clutter] match_paths = [bytestring_path(d) for d in os.listdir(directory)] - if fnmatch_all(match_paths, clutter): - # Directory contains only clutter (or nothing). - try: + try: + if fnmatch_all(match_paths, clutter): + # Directory contains only clutter (or nothing). shutil.rmtree(directory) - except OSError: + else: break - else: + except OSError: break @@ -367,18 +379,18 @@ def bytestring_path(path): PATH_SEP = bytestring_path(os.sep) -def displayable_path(path, separator=u'; '): +def displayable_path(path, separator='; '): """Attempts to decode a bytestring path to a unicode object for the purpose of displaying it to the user. If the `path` argument is a list or a tuple, the elements are joined with `separator`. """ if isinstance(path, (list, tuple)): return separator.join(displayable_path(p) for p in path) - elif isinstance(path, six.text_type): + elif isinstance(path, str): return path elif not isinstance(path, bytes): # A non-string object: just get its unicode representation. - return six.text_type(path) + return str(path) try: return path.decode(_fsencoding(), 'ignore') @@ -397,7 +409,7 @@ def syspath(path, prefix=True): if os.path.__name__ != 'ntpath': return path - if not isinstance(path, six.text_type): + if not isinstance(path, str): # Beets currently represents Windows paths internally with UTF-8 # arbitrarily. But earlier versions used MBCS because it is # reported as the FS encoding by Windows. Try both. @@ -410,11 +422,11 @@ def syspath(path, prefix=True): path = path.decode(encoding, 'replace') # Add the magic prefix if it isn't already there. - # http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247.aspx + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247.aspx if prefix and not path.startswith(WINDOWS_MAGIC_PREFIX): - if path.startswith(u'\\\\'): + if path.startswith('\\\\'): # UNC path. Final path should look like \\?\UNC\... - path = u'UNC' + path[1:] + path = 'UNC' + path[1:] path = WINDOWS_MAGIC_PREFIX + path return path @@ -436,7 +448,7 @@ def remove(path, soft=True): return try: os.remove(path) - except (OSError, IOError) as exc: + except OSError as exc: raise FilesystemError(exc, 'delete', (path,), traceback.format_exc()) @@ -451,10 +463,10 @@ def copy(path, dest, replace=False): path = syspath(path) dest = syspath(dest) if not replace and os.path.exists(dest): - raise FilesystemError(u'file exists', 'copy', (path, dest)) + raise FilesystemError('file exists', 'copy', (path, dest)) try: shutil.copyfile(path, dest) - except (OSError, IOError) as exc: + except OSError as exc: raise FilesystemError(exc, 'copy', (path, dest), traceback.format_exc()) @@ -467,24 +479,37 @@ def move(path, dest, replace=False): instead, in which case metadata will *not* be preserved. Paths are translated to system paths. """ + if os.path.isdir(path): + raise FilesystemError(u'source is directory', 'move', (path, dest)) + if os.path.isdir(dest): + raise FilesystemError(u'destination is directory', 'move', + (path, dest)) if samefile(path, dest): return path = syspath(path) dest = syspath(dest) if os.path.exists(dest) and not replace: - raise FilesystemError(u'file exists', 'rename', (path, dest)) + raise FilesystemError('file exists', 'rename', (path, dest)) # First, try renaming the file. try: - os.rename(path, dest) + os.replace(path, dest) except OSError: - # Otherwise, copy and delete the original. + tmp = tempfile.mktemp(suffix='.beets', + prefix=py3_path(b'.' + os.path.basename(dest)), + dir=py3_path(os.path.dirname(dest))) + tmp = syspath(tmp) try: - shutil.copyfile(path, dest) + shutil.copyfile(path, tmp) + os.replace(tmp, dest) + tmp = None os.remove(path) - except (OSError, IOError) as exc: + except OSError as exc: raise FilesystemError(exc, 'move', (path, dest), traceback.format_exc()) + finally: + if tmp is not None: + os.remove(tmp) def link(path, dest, replace=False): @@ -496,18 +521,18 @@ def link(path, dest, replace=False): return if os.path.exists(syspath(dest)) and not replace: - raise FilesystemError(u'file exists', 'rename', (path, dest)) + raise FilesystemError('file exists', 'rename', (path, dest)) try: os.symlink(syspath(path), syspath(dest)) except NotImplementedError: # raised on python >= 3.2 and Windows versions before Vista - raise FilesystemError(u'OS does not support symbolic links.' + raise FilesystemError('OS does not support symbolic links.' 'link', (path, dest), traceback.format_exc()) except OSError as exc: # TODO: Windows version checks can be removed for python 3 if hasattr('sys', 'getwindowsversion'): if sys.getwindowsversion()[0] < 6: # is before Vista - exc = u'OS does not support symbolic links.' + exc = 'OS does not support symbolic links.' raise FilesystemError(exc, 'link', (path, dest), traceback.format_exc()) @@ -521,21 +546,50 @@ def hardlink(path, dest, replace=False): return if os.path.exists(syspath(dest)) and not replace: - raise FilesystemError(u'file exists', 'rename', (path, dest)) + raise FilesystemError('file exists', 'rename', (path, dest)) try: os.link(syspath(path), syspath(dest)) except NotImplementedError: - raise FilesystemError(u'OS does not support hard links.' + raise FilesystemError('OS does not support hard links.' 'link', (path, dest), traceback.format_exc()) except OSError as exc: if exc.errno == errno.EXDEV: - raise FilesystemError(u'Cannot hard link across devices.' + raise FilesystemError('Cannot hard link across devices.' 'link', (path, dest), traceback.format_exc()) else: raise FilesystemError(exc, 'link', (path, dest), traceback.format_exc()) +def reflink(path, dest, replace=False, fallback=False): + """Create a reflink from `dest` to `path`. + + Raise an `OSError` if `dest` already exists, unless `replace` is + True. If `path` == `dest`, then do nothing. + + If reflinking fails and `fallback` is enabled, try copying the file + instead. Otherwise, raise an error without trying a plain copy. + + May raise an `ImportError` if the `reflink` module is not available. + """ + import reflink as pyreflink + + if samefile(path, dest): + return + + if os.path.exists(syspath(dest)) and not replace: + raise FilesystemError('file exists', 'rename', (path, dest)) + + try: + pyreflink.reflink(path, dest) + except (NotImplementedError, pyreflink.ReflinkImpossibleError): + if fallback: + copy(path, dest, replace) + else: + raise FilesystemError('OS/filesystem does not support reflinks.', + 'link', (path, dest), traceback.format_exc()) + + def unique_path(path): """Returns a version of ``path`` that does not exist on the filesystem. Specifically, if ``path` itself already exists, then @@ -553,22 +607,23 @@ def unique_path(path): num = 0 while True: num += 1 - suffix = u'.{}'.format(num).encode() + ext + suffix = f'.{num}'.encode() + ext new_path = base + suffix if not os.path.exists(new_path): return new_path + # Note: The Windows "reserved characters" are, of course, allowed on # Unix. They are forbidden here because they cause problems on Samba # shares, which are sufficiently common as to cause frequent problems. -# http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247.aspx +# https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247.aspx CHAR_REPLACE = [ - (re.compile(r'[\\/]'), u'_'), # / and \ -- forbidden everywhere. - (re.compile(r'^\.'), u'_'), # Leading dot (hidden files on Unix). - (re.compile(r'[\x00-\x1f]'), u''), # Control characters. - (re.compile(r'[<>:"\?\*\|]'), u'_'), # Windows "reserved characters". - (re.compile(r'\.$'), u'_'), # Trailing dots. - (re.compile(r'\s+$'), u''), # Trailing whitespace. + (re.compile(r'[\\/]'), '_'), # / and \ -- forbidden everywhere. + (re.compile(r'^\.'), '_'), # Leading dot (hidden files on Unix). + (re.compile(r'[\x00-\x1f]'), ''), # Control characters. + (re.compile(r'[<>:"\?\*\|]'), '_'), # Windows "reserved characters". + (re.compile(r'\.$'), '_'), # Trailing dots. + (re.compile(r'\s+$'), ''), # Trailing whitespace. ] @@ -692,36 +747,29 @@ def py3_path(path): it is. So this function helps us "smuggle" the true bytes data through APIs that took Python 3's Unicode mandate too seriously. """ - if isinstance(path, six.text_type): + if isinstance(path, str): return path assert isinstance(path, bytes) - if six.PY2: - return path return os.fsdecode(path) def str2bool(value): """Returns a boolean reflecting a human-entered string.""" - return value.lower() in (u'yes', u'1', u'true', u't', u'y') + return value.lower() in ('yes', '1', 'true', 't', 'y') def as_string(value): """Convert a value to a Unicode object for matching with a query. None becomes the empty string. Bytestrings are silently decoded. """ - if six.PY2: - buffer_types = buffer, memoryview # noqa: F821 - else: - buffer_types = memoryview - if value is None: - return u'' - elif isinstance(value, buffer_types): + return '' + elif isinstance(value, memoryview): return bytes(value).decode('utf-8', 'ignore') elif isinstance(value, bytes): return value.decode('utf-8', 'ignore') else: - return six.text_type(value) + return str(value) def text_string(value, encoding='utf-8'): @@ -744,7 +792,7 @@ def plurality(objs): """ c = Counter(objs) if not c: - raise ValueError(u'sequence must be non-empty') + raise ValueError('sequence must be non-empty') return c.most_common(1)[0] @@ -761,7 +809,11 @@ def cpu_count(): num = 0 elif sys.platform == 'darwin': try: - num = int(command_output(['/usr/sbin/sysctl', '-n', 'hw.ncpu'])) + num = int(command_output([ + '/usr/sbin/sysctl', + '-n', + 'hw.ncpu', + ]).stdout) except (ValueError, OSError, subprocess.CalledProcessError): num = 0 else: @@ -781,20 +833,23 @@ def convert_command_args(args): assert isinstance(args, list) def convert(arg): - if six.PY2: - if isinstance(arg, six.text_type): - arg = arg.encode(arg_encoding()) - else: - if isinstance(arg, bytes): - arg = arg.decode(arg_encoding(), 'surrogateescape') + if isinstance(arg, bytes): + arg = arg.decode(arg_encoding(), 'surrogateescape') return arg return [convert(a) for a in args] +# stdout and stderr as bytes +CommandOutput = namedtuple("CommandOutput", ("stdout", "stderr")) + + def command_output(cmd, shell=False): """Runs the command and returns its output after it has exited. + Returns a CommandOutput. The attributes ``stdout`` and ``stderr`` contain + byte strings of the respective output streams. + ``cmd`` is a list of arguments starting with the command names. The arguments are bytes on Unix and strings on Windows. If ``shell`` is true, ``cmd`` is assumed to be a string and passed to a @@ -829,7 +884,7 @@ def command_output(cmd, shell=False): cmd=' '.join(cmd), output=stdout + stderr, ) - return stdout + return CommandOutput(stdout, stderr) def max_filename_length(path, limit=MAX_FILENAME_LENGTH): @@ -876,25 +931,6 @@ def editor_command(): return open_anything() -def shlex_split(s): - """Split a Unicode or bytes string according to shell lexing rules. - - Raise `ValueError` if the string is not a well-formed shell string. - This is a workaround for a bug in some versions of Python. - """ - if not six.PY2 or isinstance(s, bytes): # Shlex works fine. - return shlex.split(s) - - elif isinstance(s, six.text_type): - # Work around a Python bug. - # http://bugs.python.org/issue6988 - bs = s.encode('utf-8') - return [c.decode('utf-8') for c in shlex.split(bs)] - - else: - raise TypeError(u'shlex_split called with non-string') - - def interactive_open(targets, command): """Open the files in `targets` by `exec`ing a new `command`, given as a Unicode string. (The new program takes over, and Python @@ -906,7 +942,7 @@ def interactive_open(targets, command): # Split the command string into its arguments. try: - args = shlex_split(command) + args = shlex.split(command) except ValueError: # Malformed shell tokens. args = [command] @@ -921,7 +957,7 @@ def _windows_long_path_name(short_path): """Use Windows' `GetLongPathNameW` via ctypes to get the canonical, long path given a short filename. """ - if not isinstance(short_path, six.text_type): + if not isinstance(short_path, str): short_path = short_path.decode(_fsencoding()) import ctypes @@ -982,7 +1018,7 @@ def raw_seconds_short(string): """ match = re.match(r'^(\d+):([0-5]\d)$', string) if not match: - raise ValueError(u'String not in M:SS format') + raise ValueError('String not in M:SS format') minutes, seconds = map(int, match.groups()) return float(minutes * 60 + seconds) @@ -1009,3 +1045,59 @@ def asciify_path(path, sep_replace): sep_replace ) return os.sep.join(path_components) + + +def par_map(transform, items): + """Apply the function `transform` to all the elements in the + iterable `items`, like `map(transform, items)` but with no return + value. The map *might* happen in parallel: it's parallel on Python 3 + and sequential on Python 2. + + The parallelism uses threads (not processes), so this is only useful + for IO-bound `transform`s. + """ + pool = ThreadPool() + pool.map(transform, items) + pool.close() + pool.join() + + +def lazy_property(func): + """A decorator that creates a lazily evaluated property. On first access, + the property is assigned the return value of `func`. This first value is + stored, so that future accesses do not have to evaluate `func` again. + + This behaviour is useful when `func` is expensive to evaluate, and it is + not certain that the result will be needed. + """ + field_name = '_' + func.__name__ + + @property + @functools.wraps(func) + def wrapper(self): + if hasattr(self, field_name): + return getattr(self, field_name) + + value = func(self) + setattr(self, field_name, value) + return value + + return wrapper + + +def decode_commandline_path(path): + """Prepare a path for substitution into commandline template. + + On Python 3, we need to construct the subprocess commands to invoke as a + Unicode string. On Unix, this is a little unfortunate---the OS is + expecting bytes---so we use surrogate escaping and decode with the + argument encoding, which is the same encoding that will then be + *reversed* to recover the same bytes before invoking the OS. On + Windows, we want to preserve the Unicode filename "as is." + """ + # On Python 3, the template is a Unicode string, which only supports + # substitution of Unicode variables. + if platform.system() == 'Windows': + return path.decode(_fsencoding()) + else: + return path.decode(arg_encoding(), 'surrogateescape') diff --git a/libs/common/beets/util/artresizer.py b/libs/common/beets/util/artresizer.py index e5117a6a..8683e228 100644 --- a/libs/common/beets/util/artresizer.py +++ b/libs/common/beets/util/artresizer.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Fabrice Laporte # @@ -16,38 +15,39 @@ """Abstraction layer to resize images using PIL, ImageMagick, or a public resizing proxy if neither is available. """ -from __future__ import division, absolute_import, print_function import subprocess import os +import os.path import re from tempfile import NamedTemporaryFile -from six.moves.urllib.parse import urlencode +from urllib.parse import urlencode from beets import logging from beets import util -import six # Resizing methods PIL = 1 IMAGEMAGICK = 2 WEBPROXY = 3 -if util.SNI_SUPPORTED: - PROXY_URL = 'https://images.weserv.nl/' -else: - PROXY_URL = 'http://images.weserv.nl/' +PROXY_URL = 'https://images.weserv.nl/' log = logging.getLogger('beets') -def resize_url(url, maxwidth): +def resize_url(url, maxwidth, quality=0): """Return a proxied image URL that resizes the original image to maxwidth (preserving aspect ratio). """ - return '{0}?{1}'.format(PROXY_URL, urlencode({ + params = { 'url': url.replace('http://', ''), 'w': maxwidth, - })) + } + + if quality > 0: + params['q'] = quality + + return '{}?{}'.format(PROXY_URL, urlencode(params)) def temp_file_for(path): @@ -59,48 +59,102 @@ def temp_file_for(path): return util.bytestring_path(f.name) -def pil_resize(maxwidth, path_in, path_out=None): +def pil_resize(maxwidth, path_in, path_out=None, quality=0, max_filesize=0): """Resize using Python Imaging Library (PIL). Return the output path of resized image. """ path_out = path_out or temp_file_for(path_in) from PIL import Image - log.debug(u'artresizer: PIL resizing {0} to {1}', + + log.debug('artresizer: PIL resizing {0} to {1}', util.displayable_path(path_in), util.displayable_path(path_out)) try: im = Image.open(util.syspath(path_in)) size = maxwidth, maxwidth im.thumbnail(size, Image.ANTIALIAS) - im.save(path_out) - return path_out - except IOError: - log.error(u"PIL cannot create thumbnail for '{0}'", + + if quality == 0: + # Use PIL's default quality. + quality = -1 + + # progressive=False only affects JPEGs and is the default, + # but we include it here for explicitness. + im.save(util.py3_path(path_out), quality=quality, progressive=False) + + if max_filesize > 0: + # If maximum filesize is set, we attempt to lower the quality of + # jpeg conversion by a proportional amount, up to 3 attempts + # First, set the maximum quality to either provided, or 95 + if quality > 0: + lower_qual = quality + else: + lower_qual = 95 + for i in range(5): + # 5 attempts is an abitrary choice + filesize = os.stat(util.syspath(path_out)).st_size + log.debug("PIL Pass {0} : Output size: {1}B", i, filesize) + if filesize <= max_filesize: + return path_out + # The relationship between filesize & quality will be + # image dependent. + lower_qual -= 10 + # Restrict quality dropping below 10 + if lower_qual < 10: + lower_qual = 10 + # Use optimize flag to improve filesize decrease + im.save(util.py3_path(path_out), quality=lower_qual, + optimize=True, progressive=False) + log.warning("PIL Failed to resize file to below {0}B", + max_filesize) + return path_out + + else: + return path_out + except OSError: + log.error("PIL cannot create thumbnail for '{0}'", util.displayable_path(path_in)) return path_in -def im_resize(maxwidth, path_in, path_out=None): - """Resize using ImageMagick's ``convert`` tool. - Return the output path of resized image. +def im_resize(maxwidth, path_in, path_out=None, quality=0, max_filesize=0): + """Resize using ImageMagick. + + Use the ``magick`` program or ``convert`` on older versions. Return + the output path of resized image. """ path_out = path_out or temp_file_for(path_in) - log.debug(u'artresizer: ImageMagick resizing {0} to {1}', + log.debug('artresizer: ImageMagick resizing {0} to {1}', util.displayable_path(path_in), util.displayable_path(path_out)) # "-resize WIDTHx>" shrinks images with the width larger # than the given width while maintaining the aspect ratio # with regards to the height. + # ImageMagick already seems to default to no interlace, but we include it + # here for the sake of explicitness. + cmd = ArtResizer.shared.im_convert_cmd + [ + util.syspath(path_in, prefix=False), + '-resize', f'{maxwidth}x>', + '-interlace', 'none', + ] + + if quality > 0: + cmd += ['-quality', f'{quality}'] + + # "-define jpeg:extent=SIZEb" sets the target filesize for imagemagick to + # SIZE in bytes. + if max_filesize > 0: + cmd += ['-define', f'jpeg:extent={max_filesize}b'] + + cmd.append(util.syspath(path_out, prefix=False)) + try: - util.command_output([ - 'convert', util.syspath(path_in, prefix=False), - '-resize', '{0}x>'.format(maxwidth), - util.syspath(path_out, prefix=False), - ]) + util.command_output(cmd) except subprocess.CalledProcessError: - log.warning(u'artresizer: IM convert failed for {0}', + log.warning('artresizer: IM convert failed for {0}', util.displayable_path(path_in)) return path_in + return path_out @@ -112,31 +166,33 @@ BACKEND_FUNCS = { def pil_getsize(path_in): from PIL import Image + try: im = Image.open(util.syspath(path_in)) return im.size - except IOError as exc: - log.error(u"PIL could not read file {}: {}", + except OSError as exc: + log.error("PIL could not read file {}: {}", util.displayable_path(path_in), exc) def im_getsize(path_in): - cmd = ['identify', '-format', '%w %h', - util.syspath(path_in, prefix=False)] + cmd = ArtResizer.shared.im_identify_cmd + \ + ['-format', '%w %h', util.syspath(path_in, prefix=False)] + try: - out = util.command_output(cmd) + out = util.command_output(cmd).stdout except subprocess.CalledProcessError as exc: - log.warning(u'ImageMagick size query failed') + log.warning('ImageMagick size query failed') log.debug( - u'`convert` exited with (status {}) when ' - u'getting size with command {}:\n{}', + '`convert` exited with (status {}) when ' + 'getting size with command {}:\n{}', exc.returncode, cmd, exc.output.strip() ) return try: return tuple(map(int, out.split(b' '))) except IndexError: - log.warning(u'Could not understand IM output: {0!r}', out) + log.warning('Could not understand IM output: {0!r}', out) BACKEND_GET_SIZE = { @@ -145,14 +201,115 @@ BACKEND_GET_SIZE = { } +def pil_deinterlace(path_in, path_out=None): + path_out = path_out or temp_file_for(path_in) + from PIL import Image + + try: + im = Image.open(util.syspath(path_in)) + im.save(util.py3_path(path_out), progressive=False) + return path_out + except IOError: + return path_in + + +def im_deinterlace(path_in, path_out=None): + path_out = path_out or temp_file_for(path_in) + + cmd = ArtResizer.shared.im_convert_cmd + [ + util.syspath(path_in, prefix=False), + '-interlace', 'none', + util.syspath(path_out, prefix=False), + ] + + try: + util.command_output(cmd) + return path_out + except subprocess.CalledProcessError: + return path_in + + +DEINTERLACE_FUNCS = { + PIL: pil_deinterlace, + IMAGEMAGICK: im_deinterlace, +} + + +def im_get_format(filepath): + cmd = ArtResizer.shared.im_identify_cmd + [ + '-format', '%[magick]', + util.syspath(filepath) + ] + + try: + return util.command_output(cmd).stdout + except subprocess.CalledProcessError: + return None + + +def pil_get_format(filepath): + from PIL import Image, UnidentifiedImageError + + try: + with Image.open(util.syspath(filepath)) as im: + return im.format + except (ValueError, TypeError, UnidentifiedImageError, FileNotFoundError): + log.exception("failed to detect image format for {}", filepath) + return None + + +BACKEND_GET_FORMAT = { + PIL: pil_get_format, + IMAGEMAGICK: im_get_format, +} + + +def im_convert_format(source, target, deinterlaced): + cmd = ArtResizer.shared.im_convert_cmd + [ + util.syspath(source), + *(["-interlace", "none"] if deinterlaced else []), + util.syspath(target), + ] + + try: + subprocess.check_call( + cmd, + stderr=subprocess.DEVNULL, + stdout=subprocess.DEVNULL + ) + return target + except subprocess.CalledProcessError: + return source + + +def pil_convert_format(source, target, deinterlaced): + from PIL import Image, UnidentifiedImageError + + try: + with Image.open(util.syspath(source)) as im: + im.save(util.py3_path(target), progressive=not deinterlaced) + return target + except (ValueError, TypeError, UnidentifiedImageError, FileNotFoundError, + OSError): + log.exception("failed to convert image {} -> {}", source, target) + return source + + +BACKEND_CONVERT_IMAGE_FORMAT = { + PIL: pil_convert_format, + IMAGEMAGICK: im_convert_format, +} + + class Shareable(type): """A pseudo-singleton metaclass that allows both shared and non-shared instances. The ``MyClass.shared`` property holds a lazily-created shared instance of ``MyClass`` while calling ``MyClass()`` to construct a new object works as usual. """ + def __init__(cls, name, bases, dict): - super(Shareable, cls).__init__(name, bases, dict) + super().__init__(name, bases, dict) cls._instance = None @property @@ -162,7 +319,7 @@ class Shareable(type): return cls._instance -class ArtResizer(six.with_metaclass(Shareable, object)): +class ArtResizer(metaclass=Shareable): """A singleton class that performs image resizes. """ @@ -170,21 +327,44 @@ class ArtResizer(six.with_metaclass(Shareable, object)): """Create a resizer object with an inferred method. """ self.method = self._check_method() - log.debug(u"artresizer: method is {0}", self.method) + log.debug("artresizer: method is {0}", self.method) self.can_compare = self._can_compare() - def resize(self, maxwidth, path_in, path_out=None): + # Use ImageMagick's magick binary when it's available. If it's + # not, fall back to the older, separate convert and identify + # commands. + if self.method[0] == IMAGEMAGICK: + self.im_legacy = self.method[2] + if self.im_legacy: + self.im_convert_cmd = ['convert'] + self.im_identify_cmd = ['identify'] + else: + self.im_convert_cmd = ['magick'] + self.im_identify_cmd = ['magick', 'identify'] + + def resize( + self, maxwidth, path_in, path_out=None, quality=0, max_filesize=0 + ): """Manipulate an image file according to the method, returning a new path. For PIL or IMAGEMAGIC methods, resizes the image to a - temporary file. For WEBPROXY, returns `path_in` unmodified. + temporary file and encodes with the specified quality level. + For WEBPROXY, returns `path_in` unmodified. """ if self.local: func = BACKEND_FUNCS[self.method[0]] - return func(maxwidth, path_in, path_out) + return func(maxwidth, path_in, path_out, + quality=quality, max_filesize=max_filesize) else: return path_in - def proxy_url(self, maxwidth, url): + def deinterlace(self, path_in, path_out=None): + if self.local: + func = DEINTERLACE_FUNCS[self.method[0]] + return func(path_in, path_out) + else: + return path_in + + def proxy_url(self, maxwidth, url, quality=0): """Modifies an image URL according the method, returning a new URL. For WEBPROXY, a URL on the proxy server is returned. Otherwise, the URL is returned unmodified. @@ -192,7 +372,7 @@ class ArtResizer(six.with_metaclass(Shareable, object)): if self.local: return url else: - return resize_url(url, maxwidth) + return resize_url(url, maxwidth, quality) @property def local(self): @@ -205,12 +385,50 @@ class ArtResizer(six.with_metaclass(Shareable, object)): """Return the size of an image file as an int couple (width, height) in pixels. - Only available locally + Only available locally. """ if self.local: func = BACKEND_GET_SIZE[self.method[0]] return func(path_in) + def get_format(self, path_in): + """Returns the format of the image as a string. + + Only available locally. + """ + if self.local: + func = BACKEND_GET_FORMAT[self.method[0]] + return func(path_in) + + def reformat(self, path_in, new_format, deinterlaced=True): + """Converts image to desired format, updating its extension, but + keeping the same filename. + + Only available locally. + """ + if not self.local: + return path_in + + new_format = new_format.lower() + # A nonexhaustive map of image "types" to extensions overrides + new_format = { + 'jpeg': 'jpg', + }.get(new_format, new_format) + + fname, ext = os.path.splitext(path_in) + path_new = fname + b'.' + new_format.encode('utf8') + func = BACKEND_CONVERT_IMAGE_FORMAT[self.method[0]] + + # allows the exception to propagate, while still making sure a changed + # file path was removed + result_path = path_in + try: + result_path = func(path_in, path_new, deinterlaced) + finally: + if result_path != path_in: + os.unlink(path_in) + return result_path + def _can_compare(self): """A boolean indicating whether image comparison is available""" @@ -218,10 +436,20 @@ class ArtResizer(six.with_metaclass(Shareable, object)): @staticmethod def _check_method(): - """Return a tuple indicating an available method and its version.""" + """Return a tuple indicating an available method and its version. + + The result has at least two elements: + - The method, eitehr WEBPROXY, PIL, or IMAGEMAGICK. + - The version. + + If the method is IMAGEMAGICK, there is also a third element: a + bool flag indicating whether to use the `magick` binary or + legacy single-purpose executables (`convert`, `identify`, etc.) + """ version = get_im_version() if version: - return IMAGEMAGICK, version + version, legacy = version + return IMAGEMAGICK, version, legacy version = get_pil_version() if version: @@ -231,31 +459,34 @@ class ArtResizer(six.with_metaclass(Shareable, object)): def get_im_version(): - """Return Image Magick version or None if it is unavailable - Try invoking ImageMagick's "convert". + """Get the ImageMagick version and legacy flag as a pair. Or return + None if ImageMagick is not available. """ - try: - out = util.command_output(['convert', '--version']) + for cmd_name, legacy in ((['magick'], False), (['convert'], True)): + cmd = cmd_name + ['--version'] - if b'imagemagick' in out.lower(): - pattern = br".+ (\d+)\.(\d+)\.(\d+).*" - match = re.search(pattern, out) - if match: - return (int(match.group(1)), - int(match.group(2)), - int(match.group(3))) - return (0,) + try: + out = util.command_output(cmd).stdout + except (subprocess.CalledProcessError, OSError) as exc: + log.debug('ImageMagick version check failed: {}', exc) + else: + if b'imagemagick' in out.lower(): + pattern = br".+ (\d+)\.(\d+)\.(\d+).*" + match = re.search(pattern, out) + if match: + version = (int(match.group(1)), + int(match.group(2)), + int(match.group(3))) + return version, legacy - except (subprocess.CalledProcessError, OSError) as exc: - log.debug(u'ImageMagick check `convert --version` failed: {}', exc) - return None + return None def get_pil_version(): - """Return Image Magick version or None if it is unavailable - Try importing PIL.""" + """Get the PIL/Pillow version, or None if it is unavailable. + """ try: - __import__('PIL', fromlist=[str('Image')]) + __import__('PIL', fromlist=['Image']) return (0,) except ImportError: return None diff --git a/libs/common/beets/util/bluelet.py b/libs/common/beets/util/bluelet.py index 0da17559..a40f3b2f 100644 --- a/libs/common/beets/util/bluelet.py +++ b/libs/common/beets/util/bluelet.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Extremely simple pure-Python implementation of coroutine-style asynchronous socket I/O. Inspired by, but inferior to, Eventlet. Bluelet can also be thought of as a less-terrible replacement for @@ -7,9 +5,7 @@ asyncore. Bluelet: easy concurrency without all the messy parallelism. """ -from __future__ import division, absolute_import, print_function -import six import socket import select import sys @@ -22,7 +18,7 @@ import collections # Basic events used for thread scheduling. -class Event(object): +class Event: """Just a base class identifying Bluelet events. An event is an object yielded from a Bluelet thread coroutine to suspend operation and communicate with the scheduler. @@ -201,7 +197,7 @@ class ThreadException(Exception): self.exc_info = exc_info def reraise(self): - six.reraise(self.exc_info[0], self.exc_info[1], self.exc_info[2]) + raise self.exc_info[1].with_traceback(self.exc_info[2]) SUSPENDED = Event() # Special sentinel placeholder for suspended threads. @@ -336,16 +332,20 @@ def run(root_coro): break # Wait and fire. - event2coro = dict((v, k) for k, v in threads.items()) + event2coro = {v: k for k, v in threads.items()} for event in _event_select(threads.values()): # Run the IO operation, but catch socket errors. try: value = event.fire() - except socket.error as exc: + except OSError as exc: if isinstance(exc.args, tuple) and \ exc.args[0] == errno.EPIPE: # Broken pipe. Remote host disconnected. pass + elif isinstance(exc.args, tuple) and \ + exc.args[0] == errno.ECONNRESET: + # Connection was reset by peer. + pass else: traceback.print_exc() # Abort the coroutine. @@ -386,7 +386,7 @@ class SocketClosedError(Exception): pass -class Listener(object): +class Listener: """A socket wrapper object for listening sockets. """ def __init__(self, host, port): @@ -416,7 +416,7 @@ class Listener(object): self.sock.close() -class Connection(object): +class Connection: """A socket wrapper object for connected sockets. """ def __init__(self, sock, addr): @@ -541,7 +541,7 @@ def spawn(coro): and child coroutines run concurrently. """ if not isinstance(coro, types.GeneratorType): - raise ValueError(u'%s is not a coroutine' % coro) + raise ValueError('%s is not a coroutine' % coro) return SpawnEvent(coro) @@ -551,7 +551,7 @@ def call(coro): returns a value using end(), then this event returns that value. """ if not isinstance(coro, types.GeneratorType): - raise ValueError(u'%s is not a coroutine' % coro) + raise ValueError('%s is not a coroutine' % coro) return DelegationEvent(coro) diff --git a/libs/common/beets/util/confit.py b/libs/common/beets/util/confit.py index b5513f48..dd912c44 100644 --- a/libs/common/beets/util/confit.py +++ b/libs/common/beets/util/confit.py @@ -1,6 +1,5 @@ -# -*- coding: utf-8 -*- -# This file is part of Confuse. -# Copyright 2016, Adrian Sampson. +# This file is part of beets. +# Copyright 2016-2019, Adrian Sampson. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -13,1501 +12,17 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -"""Worry-free YAML configuration files. -""" -from __future__ import division, absolute_import, print_function -import platform -import os -import pkgutil -import sys -import yaml -import collections -import re -from collections import OrderedDict +import confuse -UNIX_DIR_VAR = 'XDG_CONFIG_HOME' -UNIX_DIR_FALLBACK = '~/.config' -WINDOWS_DIR_VAR = 'APPDATA' -WINDOWS_DIR_FALLBACK = '~\\AppData\\Roaming' -MAC_DIR = '~/Library/Application Support' +import warnings +warnings.warn("beets.util.confit is deprecated; use confuse instead") -CONFIG_FILENAME = 'config.yaml' -DEFAULT_FILENAME = 'config_default.yaml' -ROOT_NAME = 'root' +# Import everything from the confuse module into this module. +for key, value in confuse.__dict__.items(): + if key not in ['__name__']: + globals()[key] = value -YAML_TAB_PROBLEM = "found character '\\t' that cannot start any token" -REDACTED_TOMBSTONE = 'REDACTED' - - -# Utilities. - -PY3 = sys.version_info[0] == 3 -STRING = str if PY3 else unicode # noqa: F821 -BASESTRING = str if PY3 else basestring # noqa: F821 -NUMERIC_TYPES = (int, float) if PY3 else (int, float, long) # noqa: F821 - - -def iter_first(sequence): - """Get the first element from an iterable or raise a ValueError if - the iterator generates no values. - """ - it = iter(sequence) - try: - return next(it) - except StopIteration: - raise ValueError() - - -# Exceptions. - -class ConfigError(Exception): - """Base class for exceptions raised when querying a configuration. - """ - - -class NotFoundError(ConfigError): - """A requested value could not be found in the configuration trees. - """ - - -class ConfigValueError(ConfigError): - """The value in the configuration is illegal.""" - - -class ConfigTypeError(ConfigValueError): - """The value in the configuration did not match the expected type. - """ - - -class ConfigTemplateError(ConfigError): - """Base class for exceptions raised because of an invalid template. - """ - - -class ConfigReadError(ConfigError): - """A configuration file could not be read.""" - def __init__(self, filename, reason=None): - self.filename = filename - self.reason = reason - - message = u'file {0} could not be read'.format(filename) - if isinstance(reason, yaml.scanner.ScannerError) and \ - reason.problem == YAML_TAB_PROBLEM: - # Special-case error message for tab indentation in YAML markup. - message += u': found tab character at line {0}, column {1}'.format( - reason.problem_mark.line + 1, - reason.problem_mark.column + 1, - ) - elif reason: - # Generic error message uses exception's message. - message += u': {0}'.format(reason) - - super(ConfigReadError, self).__init__(message) - - -# Views and sources. - -class ConfigSource(dict): - """A dictionary augmented with metadata about the source of the - configuration. - """ - def __init__(self, value, filename=None, default=False): - super(ConfigSource, self).__init__(value) - if filename is not None and not isinstance(filename, BASESTRING): - raise TypeError(u'filename must be a string or None') - self.filename = filename - self.default = default - - def __repr__(self): - return 'ConfigSource({0!r}, {1!r}, {2!r})'.format( - super(ConfigSource, self), - self.filename, - self.default, - ) - - @classmethod - def of(cls, value): - """Given either a dictionary or a `ConfigSource` object, return - a `ConfigSource` object. This lets a function accept either type - of object as an argument. - """ - if isinstance(value, ConfigSource): - return value - elif isinstance(value, dict): - return ConfigSource(value) - else: - raise TypeError(u'source value must be a dict') - - -class ConfigView(object): - """A configuration "view" is a query into a program's configuration - data. A view represents a hypothetical location in the configuration - tree; to extract the data from the location, a client typically - calls the ``view.get()`` method. The client can access children in - the tree (subviews) by subscripting the parent view (i.e., - ``view[key]``). - """ - - name = None - """The name of the view, depicting the path taken through the - configuration in Python-like syntax (e.g., ``foo['bar'][42]``). - """ - - def resolve(self): - """The core (internal) data retrieval method. Generates (value, - source) pairs for each source that contains a value for this - view. May raise ConfigTypeError if a type error occurs while - traversing a source. - """ - raise NotImplementedError - - def first(self): - """Return a (value, source) pair for the first object found for - this view. This amounts to the first element returned by - `resolve`. If no values are available, a NotFoundError is - raised. - """ - pairs = self.resolve() - try: - return iter_first(pairs) - except ValueError: - raise NotFoundError(u"{0} not found".format(self.name)) - - def exists(self): - """Determine whether the view has a setting in any source. - """ - try: - self.first() - except NotFoundError: - return False - return True - - def add(self, value): - """Set the *default* value for this configuration view. The - specified value is added as the lowest-priority configuration - data source. - """ - raise NotImplementedError - - def set(self, value): - """*Override* the value for this configuration view. The - specified value is added as the highest-priority configuration - data source. - """ - raise NotImplementedError - - def root(self): - """The RootView object from which this view is descended. - """ - raise NotImplementedError - - def __repr__(self): - return '<{}: {}>'.format(self.__class__.__name__, self.name) - - def __iter__(self): - """Iterate over the keys of a dictionary view or the *subviews* - of a list view. - """ - # Try getting the keys, if this is a dictionary view. - try: - keys = self.keys() - for key in keys: - yield key - - except ConfigTypeError: - # Otherwise, try iterating over a list. - collection = self.get() - if not isinstance(collection, (list, tuple)): - raise ConfigTypeError( - u'{0} must be a dictionary or a list, not {1}'.format( - self.name, type(collection).__name__ - ) - ) - - # Yield all the indices in the list. - for index in range(len(collection)): - yield self[index] - - def __getitem__(self, key): - """Get a subview of this view.""" - return Subview(self, key) - - def __setitem__(self, key, value): - """Create an overlay source to assign a given key under this - view. - """ - self.set({key: value}) - - def __contains__(self, key): - return self[key].exists() - - def set_args(self, namespace): - """Overlay parsed command-line arguments, generated by a library - like argparse or optparse, onto this view's value. ``namespace`` - can be a ``dict`` or namespace object. - """ - args = {} - if isinstance(namespace, dict): - items = namespace.items() - else: - items = namespace.__dict__.items() - for key, value in items: - if value is not None: # Avoid unset options. - args[key] = value - self.set(args) - - # Magical conversions. These special methods make it possible to use - # View objects somewhat transparently in certain circumstances. For - # example, rather than using ``view.get(bool)``, it's possible to - # just say ``bool(view)`` or use ``view`` in a conditional. - - def __str__(self): - """Get the value for this view as a bytestring. - """ - if PY3: - return self.__unicode__() - else: - return bytes(self.get()) - - def __unicode__(self): - """Get the value for this view as a Unicode string. - """ - return STRING(self.get()) - - def __nonzero__(self): - """Gets the value for this view as a boolean. (Python 2 only.) - """ - return self.__bool__() - - def __bool__(self): - """Gets the value for this view as a boolean. (Python 3 only.) - """ - return bool(self.get()) - - # Dictionary emulation methods. - - def keys(self): - """Returns a list containing all the keys available as subviews - of the current views. This enumerates all the keys in *all* - dictionaries matching the current view, in contrast to - ``view.get(dict).keys()``, which gets all the keys for the - *first* dict matching the view. If the object for this view in - any source is not a dict, then a ConfigTypeError is raised. The - keys are ordered according to how they appear in each source. - """ - keys = [] - - for dic, _ in self.resolve(): - try: - cur_keys = dic.keys() - except AttributeError: - raise ConfigTypeError( - u'{0} must be a dict, not {1}'.format( - self.name, type(dic).__name__ - ) - ) - - for key in cur_keys: - if key not in keys: - keys.append(key) - - return keys - - def items(self): - """Iterates over (key, subview) pairs contained in dictionaries - from *all* sources at this view. If the object for this view in - any source is not a dict, then a ConfigTypeError is raised. - """ - for key in self.keys(): - yield key, self[key] - - def values(self): - """Iterates over all the subviews contained in dictionaries from - *all* sources at this view. If the object for this view in any - source is not a dict, then a ConfigTypeError is raised. - """ - for key in self.keys(): - yield self[key] - - # List/sequence emulation. - - def all_contents(self): - """Iterates over all subviews from collections at this view from - *all* sources. If the object for this view in any source is not - iterable, then a ConfigTypeError is raised. This method is - intended to be used when the view indicates a list; this method - will concatenate the contents of the list from all sources. - """ - for collection, _ in self.resolve(): - try: - it = iter(collection) - except TypeError: - raise ConfigTypeError( - u'{0} must be an iterable, not {1}'.format( - self.name, type(collection).__name__ - ) - ) - for value in it: - yield value - - # Validation and conversion. - - def flatten(self, redact=False): - """Create a hierarchy of OrderedDicts containing the data from - this view, recursively reifying all views to get their - represented values. - - If `redact` is set, then sensitive values are replaced with - the string "REDACTED". - """ - od = OrderedDict() - for key, view in self.items(): - if redact and view.redact: - od[key] = REDACTED_TOMBSTONE - else: - try: - od[key] = view.flatten(redact=redact) - except ConfigTypeError: - od[key] = view.get() - return od - - def get(self, template=None): - """Retrieve the value for this view according to the template. - - The `template` against which the values are checked can be - anything convertible to a `Template` using `as_template`. This - means you can pass in a default integer or string value, for - example, or a type to just check that something matches the type - you expect. - - May raise a `ConfigValueError` (or its subclass, - `ConfigTypeError`) or a `NotFoundError` when the configuration - doesn't satisfy the template. - """ - return as_template(template).value(self, template) - - # Shortcuts for common templates. - - def as_filename(self): - """Get the value as a path. Equivalent to `get(Filename())`. - """ - return self.get(Filename()) - - def as_choice(self, choices): - """Get the value from a list of choices. Equivalent to - `get(Choice(choices))`. - """ - return self.get(Choice(choices)) - - def as_number(self): - """Get the value as any number type: int or float. Equivalent to - `get(Number())`. - """ - return self.get(Number()) - - def as_str_seq(self, split=True): - """Get the value as a sequence of strings. Equivalent to - `get(StrSeq())`. - """ - return self.get(StrSeq(split=split)) - - def as_pairs(self, default_value=None): - """Get the value as a sequence of pairs of two strings. Equivalent to - `get(Pairs())`. - """ - return self.get(Pairs(default_value=default_value)) - - def as_str(self): - """Get the value as a (Unicode) string. Equivalent to - `get(unicode)` on Python 2 and `get(str)` on Python 3. - """ - return self.get(String()) - - # Redaction. - - @property - def redact(self): - """Whether the view contains sensitive information and should be - redacted from output. - """ - return () in self.get_redactions() - - @redact.setter - def redact(self, flag): - self.set_redaction((), flag) - - def set_redaction(self, path, flag): - """Add or remove a redaction for a key path, which should be an - iterable of keys. - """ - raise NotImplementedError() - - def get_redactions(self): - """Get the set of currently-redacted sub-key-paths at this view. - """ - raise NotImplementedError() - - -class RootView(ConfigView): - """The base of a view hierarchy. This view keeps track of the - sources that may be accessed by subviews. - """ - def __init__(self, sources): - """Create a configuration hierarchy for a list of sources. At - least one source must be provided. The first source in the list - has the highest priority. - """ - self.sources = list(sources) - self.name = ROOT_NAME - self.redactions = set() - - def add(self, obj): - self.sources.append(ConfigSource.of(obj)) - - def set(self, value): - self.sources.insert(0, ConfigSource.of(value)) - - def resolve(self): - return ((dict(s), s) for s in self.sources) - - def clear(self): - """Remove all sources (and redactions) from this - configuration. - """ - del self.sources[:] - self.redactions.clear() - - def root(self): - return self - - def set_redaction(self, path, flag): - if flag: - self.redactions.add(path) - elif path in self.redactions: - self.redactions.remove(path) - - def get_redactions(self): - return self.redactions - - -class Subview(ConfigView): - """A subview accessed via a subscript of a parent view.""" - def __init__(self, parent, key): - """Make a subview of a parent view for a given subscript key. - """ - self.parent = parent - self.key = key - - # Choose a human-readable name for this view. - if isinstance(self.parent, RootView): - self.name = '' - else: - self.name = self.parent.name - if not isinstance(self.key, int): - self.name += '.' - if isinstance(self.key, int): - self.name += u'#{0}'.format(self.key) - elif isinstance(self.key, bytes): - self.name += self.key.decode('utf-8') - elif isinstance(self.key, STRING): - self.name += self.key - else: - self.name += repr(self.key) - - def resolve(self): - for collection, source in self.parent.resolve(): - try: - value = collection[self.key] - except IndexError: - # List index out of bounds. - continue - except KeyError: - # Dict key does not exist. - continue - except TypeError: - # Not subscriptable. - raise ConfigTypeError( - u"{0} must be a collection, not {1}".format( - self.parent.name, type(collection).__name__ - ) - ) - yield value, source - - def set(self, value): - self.parent.set({self.key: value}) - - def add(self, value): - self.parent.add({self.key: value}) - - def root(self): - return self.parent.root() - - def set_redaction(self, path, flag): - self.parent.set_redaction((self.key,) + path, flag) - - def get_redactions(self): - return (kp[1:] for kp in self.parent.get_redactions() - if kp and kp[0] == self.key) - - -# Config file paths, including platform-specific paths and in-package -# defaults. - -# Based on get_root_path from Flask by Armin Ronacher. -def _package_path(name): - """Returns the path to the package containing the named module or - None if the path could not be identified (e.g., if - ``name == "__main__"``). - """ - loader = pkgutil.get_loader(name) - if loader is None or name == '__main__': - return None - - if hasattr(loader, 'get_filename'): - filepath = loader.get_filename(name) - else: - # Fall back to importing the specified module. - __import__(name) - filepath = sys.modules[name].__file__ - - return os.path.dirname(os.path.abspath(filepath)) - - -def config_dirs(): - """Return a platform-specific list of candidates for user - configuration directories on the system. - - The candidates are in order of priority, from highest to lowest. The - last element is the "fallback" location to be used when no - higher-priority config file exists. - """ - paths = [] - - if platform.system() == 'Darwin': - paths.append(MAC_DIR) - paths.append(UNIX_DIR_FALLBACK) - if UNIX_DIR_VAR in os.environ: - paths.append(os.environ[UNIX_DIR_VAR]) - - elif platform.system() == 'Windows': - paths.append(WINDOWS_DIR_FALLBACK) - if WINDOWS_DIR_VAR in os.environ: - paths.append(os.environ[WINDOWS_DIR_VAR]) - - else: - # Assume Unix. - paths.append(UNIX_DIR_FALLBACK) - if UNIX_DIR_VAR in os.environ: - paths.append(os.environ[UNIX_DIR_VAR]) - - # Expand and deduplicate paths. - out = [] - for path in paths: - path = os.path.abspath(os.path.expanduser(path)) - if path not in out: - out.append(path) - return out - - -# YAML loading. - -class Loader(yaml.SafeLoader): - """A customized YAML loader. This loader deviates from the official - YAML spec in a few convenient ways: - - - All strings as are Unicode objects. - - All maps are OrderedDicts. - - Strings can begin with % without quotation. - """ - # All strings should be Unicode objects, regardless of contents. - def _construct_unicode(self, node): - return self.construct_scalar(node) - - # Use ordered dictionaries for every YAML map. - # From https://gist.github.com/844388 - def construct_yaml_map(self, node): - data = OrderedDict() - yield data - value = self.construct_mapping(node) - data.update(value) - - def construct_mapping(self, node, deep=False): - if isinstance(node, yaml.MappingNode): - self.flatten_mapping(node) - else: - raise yaml.constructor.ConstructorError( - None, None, - u'expected a mapping node, but found %s' % node.id, - node.start_mark - ) - - mapping = OrderedDict() - for key_node, value_node in node.value: - key = self.construct_object(key_node, deep=deep) - try: - hash(key) - except TypeError as exc: - raise yaml.constructor.ConstructorError( - u'while constructing a mapping', - node.start_mark, 'found unacceptable key (%s)' % exc, - key_node.start_mark - ) - value = self.construct_object(value_node, deep=deep) - mapping[key] = value - return mapping - - # Allow bare strings to begin with %. Directives are still detected. - def check_plain(self): - plain = super(Loader, self).check_plain() - return plain or self.peek() == '%' - - -Loader.add_constructor('tag:yaml.org,2002:str', Loader._construct_unicode) -Loader.add_constructor('tag:yaml.org,2002:map', Loader.construct_yaml_map) -Loader.add_constructor('tag:yaml.org,2002:omap', Loader.construct_yaml_map) - - -def load_yaml(filename): - """Read a YAML document from a file. If the file cannot be read or - parsed, a ConfigReadError is raised. - """ - try: - with open(filename, 'rb') as f: - return yaml.load(f, Loader=Loader) - except (IOError, yaml.error.YAMLError) as exc: - raise ConfigReadError(filename, exc) - - -# YAML dumping. - -class Dumper(yaml.SafeDumper): - """A PyYAML Dumper that represents OrderedDicts as ordinary mappings - (in order, of course). - """ - # From http://pyyaml.org/attachment/ticket/161/use_ordered_dict.py - def represent_mapping(self, tag, mapping, flow_style=None): - value = [] - node = yaml.MappingNode(tag, value, flow_style=flow_style) - if self.alias_key is not None: - self.represented_objects[self.alias_key] = node - best_style = False - if hasattr(mapping, 'items'): - mapping = list(mapping.items()) - for item_key, item_value in mapping: - node_key = self.represent_data(item_key) - node_value = self.represent_data(item_value) - if not (isinstance(node_key, yaml.ScalarNode) and - not node_key.style): - best_style = False - if not (isinstance(node_value, yaml.ScalarNode) and - not node_value.style): - best_style = False - value.append((node_key, node_value)) - if flow_style is None: - if self.default_flow_style is not None: - node.flow_style = self.default_flow_style - else: - node.flow_style = best_style - return node - - def represent_list(self, data): - """If a list has less than 4 items, represent it in inline style - (i.e. comma separated, within square brackets). - """ - node = super(Dumper, self).represent_list(data) - length = len(data) - if self.default_flow_style is None and length < 4: - node.flow_style = True - elif self.default_flow_style is None: - node.flow_style = False - return node - - def represent_bool(self, data): - """Represent bool as 'yes' or 'no' instead of 'true' or 'false'. - """ - if data: - value = u'yes' - else: - value = u'no' - return self.represent_scalar('tag:yaml.org,2002:bool', value) - - def represent_none(self, data): - """Represent a None value with nothing instead of 'none'. - """ - return self.represent_scalar('tag:yaml.org,2002:null', '') - - -Dumper.add_representer(OrderedDict, Dumper.represent_dict) -Dumper.add_representer(bool, Dumper.represent_bool) -Dumper.add_representer(type(None), Dumper.represent_none) -Dumper.add_representer(list, Dumper.represent_list) - - -def restore_yaml_comments(data, default_data): - """Scan default_data for comments (we include empty lines in our - definition of comments) and place them before the same keys in data. - Only works with comments that are on one or more own lines, i.e. - not next to a yaml mapping. - """ - comment_map = dict() - default_lines = iter(default_data.splitlines()) - for line in default_lines: - if not line: - comment = "\n" - elif line.startswith("#"): - comment = "{0}\n".format(line) - else: - continue - while True: - line = next(default_lines) - if line and not line.startswith("#"): - break - comment += "{0}\n".format(line) - key = line.split(':')[0].strip() - comment_map[key] = comment - out_lines = iter(data.splitlines()) - out_data = "" - for line in out_lines: - key = line.split(':')[0].strip() - if key in comment_map: - out_data += comment_map[key] - out_data += "{0}\n".format(line) - return out_data - - -# Main interface. - -class Configuration(RootView): - def __init__(self, appname, modname=None, read=True): - """Create a configuration object by reading the - automatically-discovered config files for the application for a - given name. If `modname` is specified, it should be the import - name of a module whose package will be searched for a default - config file. (Otherwise, no defaults are used.) Pass `False` for - `read` to disable automatic reading of all discovered - configuration files. Use this when creating a configuration - object at module load time and then call the `read` method - later. - """ - super(Configuration, self).__init__([]) - self.appname = appname - self.modname = modname - - self._env_var = '{0}DIR'.format(self.appname.upper()) - - if read: - self.read() - - def user_config_path(self): - """Points to the location of the user configuration. - - The file may not exist. - """ - return os.path.join(self.config_dir(), CONFIG_FILENAME) - - def _add_user_source(self): - """Add the configuration options from the YAML file in the - user's configuration directory (given by `config_dir`) if it - exists. - """ - filename = self.user_config_path() - if os.path.isfile(filename): - self.add(ConfigSource(load_yaml(filename) or {}, filename)) - - def _add_default_source(self): - """Add the package's default configuration settings. This looks - for a YAML file located inside the package for the module - `modname` if it was given. - """ - if self.modname: - pkg_path = _package_path(self.modname) - if pkg_path: - filename = os.path.join(pkg_path, DEFAULT_FILENAME) - if os.path.isfile(filename): - self.add(ConfigSource(load_yaml(filename), filename, True)) - - def read(self, user=True, defaults=True): - """Find and read the files for this configuration and set them - as the sources for this configuration. To disable either - discovered user configuration files or the in-package defaults, - set `user` or `defaults` to `False`. - """ - if user: - self._add_user_source() - if defaults: - self._add_default_source() - - def config_dir(self): - """Get the path to the user configuration directory. The - directory is guaranteed to exist as a postcondition (one may be - created if none exist). - - If the application's ``...DIR`` environment variable is set, it - is used as the configuration directory. Otherwise, - platform-specific standard configuration locations are searched - for a ``config.yaml`` file. If no configuration file is found, a - fallback path is used. - """ - # If environment variable is set, use it. - if self._env_var in os.environ: - appdir = os.environ[self._env_var] - appdir = os.path.abspath(os.path.expanduser(appdir)) - if os.path.isfile(appdir): - raise ConfigError(u'{0} must be a directory'.format( - self._env_var - )) - - else: - # Search platform-specific locations. If no config file is - # found, fall back to the final directory in the list. - for confdir in config_dirs(): - appdir = os.path.join(confdir, self.appname) - if os.path.isfile(os.path.join(appdir, CONFIG_FILENAME)): - break - - # Ensure that the directory exists. - if not os.path.isdir(appdir): - os.makedirs(appdir) - return appdir - - def set_file(self, filename): - """Parses the file as YAML and inserts it into the configuration - sources with highest priority. - """ - filename = os.path.abspath(filename) - self.set(ConfigSource(load_yaml(filename), filename)) - - def dump(self, full=True, redact=False): - """Dump the Configuration object to a YAML file. - - The order of the keys is determined from the default - configuration file. All keys not in the default configuration - will be appended to the end of the file. - - :param filename: The file to dump the configuration to, or None - if the YAML string should be returned instead - :type filename: unicode - :param full: Dump settings that don't differ from the defaults - as well - :param redact: Remove sensitive information (views with the `redact` - flag set) from the output - """ - if full: - out_dict = self.flatten(redact=redact) - else: - # Exclude defaults when flattening. - sources = [s for s in self.sources if not s.default] - temp_root = RootView(sources) - temp_root.redactions = self.redactions - out_dict = temp_root.flatten(redact=redact) - - yaml_out = yaml.dump(out_dict, Dumper=Dumper, - default_flow_style=None, indent=4, - width=1000) - - # Restore comments to the YAML text. - default_source = None - for source in self.sources: - if source.default: - default_source = source - break - if default_source and default_source.filename: - with open(default_source.filename, 'rb') as fp: - default_data = fp.read() - yaml_out = restore_yaml_comments(yaml_out, - default_data.decode('utf8')) - - return yaml_out - - -class LazyConfig(Configuration): - """A Configuration at reads files on demand when it is first - accessed. This is appropriate for using as a global config object at - the module level. - """ - def __init__(self, appname, modname=None): - super(LazyConfig, self).__init__(appname, modname, False) - self._materialized = False # Have we read the files yet? - self._lazy_prefix = [] # Pre-materialization calls to set(). - self._lazy_suffix = [] # Calls to add(). - - def read(self, user=True, defaults=True): - self._materialized = True - super(LazyConfig, self).read(user, defaults) - - def resolve(self): - if not self._materialized: - # Read files and unspool buffers. - self.read() - self.sources += self._lazy_suffix - self.sources[:0] = self._lazy_prefix - return super(LazyConfig, self).resolve() - - def add(self, value): - super(LazyConfig, self).add(value) - if not self._materialized: - # Buffer additions to end. - self._lazy_suffix += self.sources - del self.sources[:] - - def set(self, value): - super(LazyConfig, self).set(value) - if not self._materialized: - # Buffer additions to beginning. - self._lazy_prefix[:0] = self.sources - del self.sources[:] - - def clear(self): - """Remove all sources from this configuration.""" - super(LazyConfig, self).clear() - self._lazy_suffix = [] - self._lazy_prefix = [] - - -# "Validated" configuration views: experimental! - - -REQUIRED = object() -"""A sentinel indicating that there is no default value and an exception -should be raised when the value is missing. -""" - - -class Template(object): - """A value template for configuration fields. - - The template works like a type and instructs Confuse about how to - interpret a deserialized YAML value. This includes type conversions, - providing a default value, and validating for errors. For example, a - filepath type might expand tildes and check that the file exists. - """ - def __init__(self, default=REQUIRED): - """Create a template with a given default value. - - If `default` is the sentinel `REQUIRED` (as it is by default), - then an error will be raised when a value is missing. Otherwise, - missing values will instead return `default`. - """ - self.default = default - - def __call__(self, view): - """Invoking a template on a view gets the view's value according - to the template. - """ - return self.value(view, self) - - def value(self, view, template=None): - """Get the value for a `ConfigView`. - - May raise a `NotFoundError` if the value is missing (and the - template requires it) or a `ConfigValueError` for invalid values. - """ - if view.exists(): - value, _ = view.first() - return self.convert(value, view) - elif self.default is REQUIRED: - # Missing required value. This is an error. - raise NotFoundError(u"{0} not found".format(view.name)) - else: - # Missing value, but not required. - return self.default - - def convert(self, value, view): - """Convert the YAML-deserialized value to a value of the desired - type. - - Subclasses should override this to provide useful conversions. - May raise a `ConfigValueError` when the configuration is wrong. - """ - # Default implementation does no conversion. - return value - - def fail(self, message, view, type_error=False): - """Raise an exception indicating that a value cannot be - accepted. - - `type_error` indicates whether the error is due to a type - mismatch rather than a malformed value. In this case, a more - specific exception is raised. - """ - exc_class = ConfigTypeError if type_error else ConfigValueError - raise exc_class( - u'{0}: {1}'.format(view.name, message) - ) - - def __repr__(self): - return '{0}({1})'.format( - type(self).__name__, - '' if self.default is REQUIRED else repr(self.default), - ) - - -class Integer(Template): - """An integer configuration value template. - """ - def convert(self, value, view): - """Check that the value is an integer. Floats are rounded. - """ - if isinstance(value, int): - return value - elif isinstance(value, float): - return int(value) - else: - self.fail(u'must be a number', view, True) - - -class Number(Template): - """A numeric type: either an integer or a floating-point number. - """ - def convert(self, value, view): - """Check that the value is an int or a float. - """ - if isinstance(value, NUMERIC_TYPES): - return value - else: - self.fail( - u'must be numeric, not {0}'.format(type(value).__name__), - view, - True - ) - - -class MappingTemplate(Template): - """A template that uses a dictionary to specify other types for the - values for a set of keys and produce a validated `AttrDict`. - """ - def __init__(self, mapping): - """Create a template according to a dict (mapping). The - mapping's values should themselves either be Types or - convertible to Types. - """ - subtemplates = {} - for key, typ in mapping.items(): - subtemplates[key] = as_template(typ) - self.subtemplates = subtemplates - - def value(self, view, template=None): - """Get a dict with the same keys as the template and values - validated according to the value types. - """ - out = AttrDict() - for key, typ in self.subtemplates.items(): - out[key] = typ.value(view[key], self) - return out - - def __repr__(self): - return 'MappingTemplate({0})'.format(repr(self.subtemplates)) - - -class String(Template): - """A string configuration value template. - """ - def __init__(self, default=REQUIRED, pattern=None): - """Create a template with the added optional `pattern` argument, - a regular expression string that the value should match. - """ - super(String, self).__init__(default) - self.pattern = pattern - if pattern: - self.regex = re.compile(pattern) - - def __repr__(self): - args = [] - - if self.default is not REQUIRED: - args.append(repr(self.default)) - - if self.pattern is not None: - args.append('pattern=' + repr(self.pattern)) - - return 'String({0})'.format(', '.join(args)) - - def convert(self, value, view): - """Check that the value is a string and matches the pattern. - """ - if isinstance(value, BASESTRING): - if self.pattern and not self.regex.match(value): - self.fail( - u"must match the pattern {0}".format(self.pattern), - view - ) - return value - else: - self.fail(u'must be a string', view, True) - - -class Choice(Template): - """A template that permits values from a sequence of choices. - """ - def __init__(self, choices): - """Create a template that validates any of the values from the - iterable `choices`. - - If `choices` is a map, then the corresponding value is emitted. - Otherwise, the value itself is emitted. - """ - self.choices = choices - - def convert(self, value, view): - """Ensure that the value is among the choices (and remap if the - choices are a mapping). - """ - if value not in self.choices: - self.fail( - u'must be one of {0}, not {1}'.format( - repr(list(self.choices)), repr(value) - ), - view - ) - - if isinstance(self.choices, collections.Mapping): - return self.choices[value] - else: - return value - - def __repr__(self): - return 'Choice({0!r})'.format(self.choices) - - -class OneOf(Template): - """A template that permits values complying to one of the given templates. - """ - def __init__(self, allowed, default=REQUIRED): - super(OneOf, self).__init__(default) - self.allowed = list(allowed) - - def __repr__(self): - args = [] - - if self.allowed is not None: - args.append('allowed=' + repr(self.allowed)) - - if self.default is not REQUIRED: - args.append(repr(self.default)) - - return 'OneOf({0})'.format(', '.join(args)) - - def value(self, view, template): - self.template = template - return super(OneOf, self).value(view, template) - - def convert(self, value, view): - """Ensure that the value follows at least one template. - """ - is_mapping = isinstance(self.template, MappingTemplate) - - for candidate in self.allowed: - try: - if is_mapping: - if isinstance(candidate, Filename) and \ - candidate.relative_to: - next_template = candidate.template_with_relatives( - view, - self.template - ) - - next_template.subtemplates[view.key] = as_template( - candidate - ) - else: - next_template = MappingTemplate({view.key: candidate}) - - return view.parent.get(next_template)[view.key] - else: - return view.get(candidate) - except ConfigTemplateError: - raise - except ConfigError: - pass - except ValueError as exc: - raise ConfigTemplateError(exc) - - self.fail( - u'must be one of {0}, not {1}'.format( - repr(self.allowed), repr(value) - ), - view - ) - - -class StrSeq(Template): - """A template for values that are lists of strings. - - Validates both actual YAML string lists and single strings. Strings - can optionally be split on whitespace. - """ - def __init__(self, split=True): - """Create a new template. - - `split` indicates whether, when the underlying value is a single - string, it should be split on whitespace. Otherwise, the - resulting value is a list containing a single string. - """ - super(StrSeq, self).__init__() - self.split = split - - def _convert_value(self, x, view): - if isinstance(x, STRING): - return x - elif isinstance(x, bytes): - return x.decode('utf-8', 'ignore') - else: - self.fail(u'must be a list of strings', view, True) - - def convert(self, value, view): - if isinstance(value, bytes): - value = value.decode('utf-8', 'ignore') - - if isinstance(value, STRING): - if self.split: - value = value.split() - else: - value = [value] - else: - try: - value = list(value) - except TypeError: - self.fail(u'must be a whitespace-separated string or a list', - view, True) - - return [self._convert_value(v, view) for v in value] - - -class Pairs(StrSeq): - """A template for ordered key-value pairs. - - This can either be given with the same syntax as for `StrSeq` (i.e. without - values), or as a list of strings and/or single-element mappings such as:: - - - key: value - - [key, value] - - key - - The result is a list of two-element tuples. If no value is provided, the - `default_value` will be returned as the second element. - """ - - def __init__(self, default_value=None): - """Create a new template. - - `default` is the dictionary value returned for items that are not - a mapping, but a single string. - """ - super(Pairs, self).__init__(split=True) - self.default_value = default_value - - def _convert_value(self, x, view): - try: - return (super(Pairs, self)._convert_value(x, view), - self.default_value) - except ConfigTypeError: - if isinstance(x, collections.Mapping): - if len(x) != 1: - self.fail(u'must be a single-element mapping', view, True) - k, v = iter_first(x.items()) - elif isinstance(x, collections.Sequence): - if len(x) != 2: - self.fail(u'must be a two-element list', view, True) - k, v = x - else: - # Is this even possible? -> Likely, if some !directive cause - # YAML to parse this to some custom type. - self.fail(u'must be a single string, mapping, or a list' - u'' + str(x), - view, True) - return (super(Pairs, self)._convert_value(k, view), - super(Pairs, self)._convert_value(v, view)) - - -class Filename(Template): - """A template that validates strings as filenames. - - Filenames are returned as absolute, tilde-free paths. - - Relative paths are relative to the template's `cwd` argument - when it is specified, then the configuration directory (see - the `config_dir` method) if they come from a file. Otherwise, - they are relative to the current working directory. This helps - attain the expected behavior when using command-line options. - """ - def __init__(self, default=REQUIRED, cwd=None, relative_to=None, - in_app_dir=False): - """`relative_to` is the name of a sibling value that is - being validated at the same time. - - `in_app_dir` indicates whether the path should be resolved - inside the application's config directory (even when the setting - does not come from a file). - """ - super(Filename, self).__init__(default) - self.cwd = cwd - self.relative_to = relative_to - self.in_app_dir = in_app_dir - - def __repr__(self): - args = [] - - if self.default is not REQUIRED: - args.append(repr(self.default)) - - if self.cwd is not None: - args.append('cwd=' + repr(self.cwd)) - - if self.relative_to is not None: - args.append('relative_to=' + repr(self.relative_to)) - - if self.in_app_dir: - args.append('in_app_dir=True') - - return 'Filename({0})'.format(', '.join(args)) - - def resolve_relative_to(self, view, template): - if not isinstance(template, (collections.Mapping, MappingTemplate)): - # disallow config.get(Filename(relative_to='foo')) - raise ConfigTemplateError( - u'relative_to may only be used when getting multiple values.' - ) - - elif self.relative_to == view.key: - raise ConfigTemplateError( - u'{0} is relative to itself'.format(view.name) - ) - - elif self.relative_to not in view.parent.keys(): - # self.relative_to is not in the config - self.fail( - ( - u'needs sibling value "{0}" to expand relative path' - ).format(self.relative_to), - view - ) - - old_template = {} - old_template.update(template.subtemplates) - - # save time by skipping MappingTemplate's init loop - next_template = MappingTemplate({}) - next_relative = self.relative_to - - # gather all the needed templates and nothing else - while next_relative is not None: - try: - # pop to avoid infinite loop because of recursive - # relative paths - rel_to_template = old_template.pop(next_relative) - except KeyError: - if next_relative in template.subtemplates: - # we encountered this config key previously - raise ConfigTemplateError(( - u'{0} and {1} are recursively relative' - ).format(view.name, self.relative_to)) - else: - raise ConfigTemplateError(( - u'missing template for {0}, needed to expand {1}\'s' + - u'relative path' - ).format(self.relative_to, view.name)) - - next_template.subtemplates[next_relative] = rel_to_template - next_relative = rel_to_template.relative_to - - return view.parent.get(next_template)[self.relative_to] - - def value(self, view, template=None): - path, source = view.first() - if not isinstance(path, BASESTRING): - self.fail( - u'must be a filename, not {0}'.format(type(path).__name__), - view, - True - ) - path = os.path.expanduser(STRING(path)) - - if not os.path.isabs(path): - if self.cwd is not None: - # relative to the template's argument - path = os.path.join(self.cwd, path) - - elif self.relative_to is not None: - path = os.path.join( - self.resolve_relative_to(view, template), - path, - ) - - elif source.filename or self.in_app_dir: - # From defaults: relative to the app's directory. - path = os.path.join(view.root().config_dir(), path) - - return os.path.abspath(path) - - -class TypeTemplate(Template): - """A simple template that checks that a value is an instance of a - desired Python type. - """ - def __init__(self, typ, default=REQUIRED): - """Create a template that checks that the value is an instance - of `typ`. - """ - super(TypeTemplate, self).__init__(default) - self.typ = typ - - def convert(self, value, view): - if not isinstance(value, self.typ): - self.fail( - u'must be a {0}, not {1}'.format( - self.typ.__name__, - type(value).__name__, - ), - view, - True - ) - return value - - -class AttrDict(dict): - """A `dict` subclass that can be accessed via attributes (dot - notation) for convenience. - """ - def __getattr__(self, key): - if key in self: - return self[key] - else: - raise AttributeError(key) - - -def as_template(value): - """Convert a simple "shorthand" Python value to a `Template`. - """ - if isinstance(value, Template): - # If it's already a Template, pass it through. - return value - elif isinstance(value, collections.Mapping): - # Dictionaries work as templates. - return MappingTemplate(value) - elif value is int: - return Integer() - elif isinstance(value, int): - return Integer(value) - elif isinstance(value, type) and issubclass(value, BASESTRING): - return String() - elif isinstance(value, BASESTRING): - return String(value) - elif isinstance(value, set): - # convert to list to avoid hash related problems - return Choice(list(value)) - elif isinstance(value, list): - return OneOf(value) - elif value is float: - return Number() - elif value is None: - return Template() - elif value is dict: - return TypeTemplate(collections.Mapping) - elif value is list: - return TypeTemplate(collections.Sequence) - elif isinstance(value, type): - return TypeTemplate(value) - else: - raise ValueError(u'cannot convert to template: {0!r}'.format(value)) +# Cleanup namespace. +del key, value, warnings, confuse diff --git a/libs/common/beets/util/enumeration.py b/libs/common/beets/util/enumeration.py index 3e946718..e49f6fdd 100644 --- a/libs/common/beets/util/enumeration.py +++ b/libs/common/beets/util/enumeration.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function from enum import Enum diff --git a/libs/common/beets/util/functemplate.py b/libs/common/beets/util/functemplate.py index 0e13db4a..289a436d 100644 --- a/libs/common/beets/util/functemplate.py +++ b/libs/common/beets/util/functemplate.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -27,30 +26,30 @@ This is sort of like a tiny, horrible degeneration of a real templating engine like Jinja2 or Mustache. """ -from __future__ import division, absolute_import, print_function import re import ast import dis import types import sys -import six +import functools -SYMBOL_DELIM = u'$' -FUNC_DELIM = u'%' -GROUP_OPEN = u'{' -GROUP_CLOSE = u'}' -ARG_SEP = u',' -ESCAPE_CHAR = u'$' +SYMBOL_DELIM = '$' +FUNC_DELIM = '%' +GROUP_OPEN = '{' +GROUP_CLOSE = '}' +ARG_SEP = ',' +ESCAPE_CHAR = '$' VARIABLE_PREFIX = '__var_' FUNCTION_PREFIX = '__func_' -class Environment(object): +class Environment: """Contains the values and functions to be substituted into a template. """ + def __init__(self, values, functions): self.values = values self.functions = functions @@ -72,15 +71,7 @@ def ex_literal(val): """An int, float, long, bool, string, or None literal with the given value. """ - if val is None: - return ast.Name('None', ast.Load()) - elif isinstance(val, six.integer_types): - return ast.Num(val) - elif isinstance(val, bool): - return ast.Name(bytes(val), ast.Load()) - elif isinstance(val, six.string_types): - return ast.Str(val) - raise TypeError(u'no literal for {0}'.format(type(val))) + return ast.Constant(val) def ex_varassign(name, expr): @@ -97,7 +88,7 @@ def ex_call(func, args): function may be an expression or the name of a function. Each argument may be an expression or a value to be used as a literal. """ - if isinstance(func, six.string_types): + if isinstance(func, str): func = ex_rvalue(func) args = list(args) @@ -105,10 +96,7 @@ def ex_call(func, args): if not isinstance(args[i], ast.expr): args[i] = ex_literal(args[i]) - if sys.version_info[:2] < (3, 5): - return ast.Call(func, args, [], None, None) - else: - return ast.Call(func, args, []) + return ast.Call(func, args, []) def compile_func(arg_names, statements, name='_the_func', debug=False): @@ -116,32 +104,30 @@ def compile_func(arg_names, statements, name='_the_func', debug=False): the resulting Python function. If `debug`, then print out the bytecode of the compiled function. """ - if six.PY2: - func_def = ast.FunctionDef( - name=name.encode('utf-8'), - args=ast.arguments( - args=[ast.Name(n, ast.Param()) for n in arg_names], - vararg=None, - kwarg=None, - defaults=[ex_literal(None) for _ in arg_names], - ), - body=statements, - decorator_list=[], - ) - else: - func_def = ast.FunctionDef( - name=name, - args=ast.arguments( - args=[ast.arg(arg=n, annotation=None) for n in arg_names], - kwonlyargs=[], - kw_defaults=[], - defaults=[ex_literal(None) for _ in arg_names], - ), - body=statements, - decorator_list=[], - ) + args_fields = { + 'args': [ast.arg(arg=n, annotation=None) for n in arg_names], + 'kwonlyargs': [], + 'kw_defaults': [], + 'defaults': [ex_literal(None) for _ in arg_names], + } + if 'posonlyargs' in ast.arguments._fields: # Added in Python 3.8. + args_fields['posonlyargs'] = [] + args = ast.arguments(**args_fields) + + func_def = ast.FunctionDef( + name=name, + args=args, + body=statements, + decorator_list=[], + ) + + # The ast.Module signature changed in 3.8 to accept a list of types to + # ignore. + if sys.version_info >= (3, 8): + mod = ast.Module([func_def], []) + else: + mod = ast.Module([func_def]) - mod = ast.Module([func_def]) ast.fix_missing_locations(mod) prog = compile(mod, '', 'exec') @@ -160,14 +146,15 @@ def compile_func(arg_names, statements, name='_the_func', debug=False): # AST nodes for the template language. -class Symbol(object): +class Symbol: """A variable-substitution symbol in a template.""" + def __init__(self, ident, original): self.ident = ident self.original = original def __repr__(self): - return u'Symbol(%s)' % repr(self.ident) + return 'Symbol(%s)' % repr(self.ident) def evaluate(self, env): """Evaluate the symbol in the environment, returning a Unicode @@ -182,24 +169,22 @@ class Symbol(object): def translate(self): """Compile the variable lookup.""" - if six.PY2: - ident = self.ident.encode('utf-8') - else: - ident = self.ident + ident = self.ident expr = ex_rvalue(VARIABLE_PREFIX + ident) - return [expr], set([ident]), set() + return [expr], {ident}, set() -class Call(object): +class Call: """A function call in a template.""" + def __init__(self, ident, args, original): self.ident = ident self.args = args self.original = original def __repr__(self): - return u'Call(%s, %s, %s)' % (repr(self.ident), repr(self.args), - repr(self.original)) + return 'Call({}, {}, {})'.format(repr(self.ident), repr(self.args), + repr(self.original)) def evaluate(self, env): """Evaluate the function call in the environment, returning a @@ -212,19 +197,15 @@ class Call(object): except Exception as exc: # Function raised exception! Maybe inlining the name of # the exception will help debug. - return u'<%s>' % six.text_type(exc) - return six.text_type(out) + return '<%s>' % str(exc) + return str(out) else: return self.original def translate(self): """Compile the function call.""" varnames = set() - if six.PY2: - ident = self.ident.encode('utf-8') - else: - ident = self.ident - funcnames = set([ident]) + funcnames = {self.ident} arg_exprs = [] for arg in self.args: @@ -235,32 +216,33 @@ class Call(object): # Create a subexpression that joins the result components of # the arguments. arg_exprs.append(ex_call( - ast.Attribute(ex_literal(u''), 'join', ast.Load()), + ast.Attribute(ex_literal(''), 'join', ast.Load()), [ex_call( 'map', [ - ex_rvalue(six.text_type.__name__), + ex_rvalue(str.__name__), ast.List(subexprs, ast.Load()), ] )], )) subexpr_call = ex_call( - FUNCTION_PREFIX + ident, + FUNCTION_PREFIX + self.ident, arg_exprs ) return [subexpr_call], varnames, funcnames -class Expression(object): +class Expression: """Top-level template construct: contains a list of text blobs, Symbols, and Calls. """ + def __init__(self, parts): self.parts = parts def __repr__(self): - return u'Expression(%s)' % (repr(self.parts)) + return 'Expression(%s)' % (repr(self.parts)) def evaluate(self, env): """Evaluate the entire expression in the environment, returning @@ -268,11 +250,11 @@ class Expression(object): """ out = [] for part in self.parts: - if isinstance(part, six.string_types): + if isinstance(part, str): out.append(part) else: out.append(part.evaluate(env)) - return u''.join(map(six.text_type, out)) + return ''.join(map(str, out)) def translate(self): """Compile the expression to a list of Python AST expressions, a @@ -282,7 +264,7 @@ class Expression(object): varnames = set() funcnames = set() for part in self.parts: - if isinstance(part, six.string_types): + if isinstance(part, str): expressions.append(ex_literal(part)) else: e, v, f = part.translate() @@ -298,7 +280,7 @@ class ParseError(Exception): pass -class Parser(object): +class Parser: """Parses a template expression string. Instantiate the class with the template source and call ``parse_expression``. The ``pos`` field will indicate the character after the expression finished and @@ -311,6 +293,7 @@ class Parser(object): replaced with a real, accepted parsing technique (PEG, parser generator, etc.). """ + def __init__(self, string, in_argument=False): """ Create a new parser. :param in_arguments: boolean that indicates the parser is to be @@ -326,7 +309,7 @@ class Parser(object): special_chars = (SYMBOL_DELIM, FUNC_DELIM, GROUP_OPEN, GROUP_CLOSE, ESCAPE_CHAR) special_char_re = re.compile(r'[%s]|\Z' % - u''.join(re.escape(c) for c in special_chars)) + ''.join(re.escape(c) for c in special_chars)) escapable_chars = (SYMBOL_DELIM, FUNC_DELIM, GROUP_CLOSE, ARG_SEP) terminator_chars = (GROUP_CLOSE,) @@ -343,7 +326,7 @@ class Parser(object): if self.in_argument: extra_special_chars = (ARG_SEP,) special_char_re = re.compile( - r'[%s]|\Z' % u''.join( + r'[%s]|\Z' % ''.join( re.escape(c) for c in self.special_chars + extra_special_chars ) @@ -387,7 +370,7 @@ class Parser(object): # Shift all characters collected so far into a single string. if text_parts: - self.parts.append(u''.join(text_parts)) + self.parts.append(''.join(text_parts)) text_parts = [] if char == SYMBOL_DELIM: @@ -409,7 +392,7 @@ class Parser(object): # If any parsed characters remain, shift them into a string. if text_parts: - self.parts.append(u''.join(text_parts)) + self.parts.append(''.join(text_parts)) def parse_symbol(self): """Parse a variable reference (like ``$foo`` or ``${foo}``) @@ -547,11 +530,27 @@ def _parse(template): return Expression(parts) -# External interface. +def cached(func): + """Like the `functools.lru_cache` decorator, but works (as a no-op) + on Python < 3.2. + """ + if hasattr(functools, 'lru_cache'): + return functools.lru_cache(maxsize=128)(func) + else: + # Do nothing when lru_cache is not available. + return func -class Template(object): + +@cached +def template(fmt): + return Template(fmt) + + +# External interface. +class Template: """A string template, including text, Symbols, and Calls. """ + def __init__(self, template): self.expr = _parse(template) self.original = template @@ -600,7 +599,7 @@ class Template(object): for funcname in funcnames: args[FUNCTION_PREFIX + funcname] = functions[funcname] parts = func(**args) - return u''.join(parts) + return ''.join(parts) return wrapper_func @@ -609,9 +608,9 @@ class Template(object): if __name__ == '__main__': import timeit - _tmpl = Template(u'foo $bar %baz{foozle $bar barzle} $bar') + _tmpl = Template('foo $bar %baz{foozle $bar barzle} $bar') _vars = {'bar': 'qux'} - _funcs = {'baz': six.text_type.upper} + _funcs = {'baz': str.upper} interp_time = timeit.timeit('_tmpl.interpret(_vars, _funcs)', 'from __main__ import _tmpl, _vars, _funcs', number=10000) @@ -620,4 +619,4 @@ if __name__ == '__main__': 'from __main__ import _tmpl, _vars, _funcs', number=10000) print(comp_time) - print(u'Speedup:', interp_time / comp_time) + print('Speedup:', interp_time / comp_time) diff --git a/libs/common/beets/util/hidden.py b/libs/common/beets/util/hidden.py index ed97f2bf..881de1ac 100644 --- a/libs/common/beets/util/hidden.py +++ b/libs/common/beets/util/hidden.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -14,7 +13,6 @@ # included in all copies or substantial portions of the Software. """Simple library to work out if a file is hidden on different platforms.""" -from __future__ import division, absolute_import, print_function import os import stat diff --git a/libs/common/beets/util/pipeline.py b/libs/common/beets/util/pipeline.py index 39bc7152..d338cb51 100644 --- a/libs/common/beets/util/pipeline.py +++ b/libs/common/beets/util/pipeline.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -32,12 +31,10 @@ To do so, pass an iterable of coroutines to the Pipeline constructor in place of any single coroutine. """ -from __future__ import division, absolute_import, print_function -from six.moves import queue +import queue from threading import Thread, Lock import sys -import six BUBBLE = '__PIPELINE_BUBBLE__' POISON = '__PIPELINE_POISON__' @@ -91,6 +88,7 @@ class CountedQueue(queue.Queue): still feeding into it. The queue is poisoned when all threads are finished with the queue. """ + def __init__(self, maxsize=0): queue.Queue.__init__(self, maxsize) self.nthreads = 0 @@ -135,10 +133,11 @@ class CountedQueue(queue.Queue): _invalidate_queue(self, POISON, False) -class MultiMessage(object): +class MultiMessage: """A message yielded by a pipeline stage encapsulating multiple values to be sent to the next stage. """ + def __init__(self, messages): self.messages = messages @@ -210,8 +209,9 @@ def _allmsgs(obj): class PipelineThread(Thread): """Abstract base class for pipeline-stage threads.""" + def __init__(self, all_threads): - super(PipelineThread, self).__init__() + super().__init__() self.abort_lock = Lock() self.abort_flag = False self.all_threads = all_threads @@ -241,15 +241,13 @@ class FirstPipelineThread(PipelineThread): """The thread running the first stage in a parallel pipeline setup. The coroutine should just be a generator. """ + def __init__(self, coro, out_queue, all_threads): - super(FirstPipelineThread, self).__init__(all_threads) + super().__init__(all_threads) self.coro = coro self.out_queue = out_queue self.out_queue.acquire() - self.abort_lock = Lock() - self.abort_flag = False - def run(self): try: while True: @@ -282,8 +280,9 @@ class MiddlePipelineThread(PipelineThread): """A thread running any stage in the pipeline except the first or last. """ + def __init__(self, coro, in_queue, out_queue, all_threads): - super(MiddlePipelineThread, self).__init__(all_threads) + super().__init__(all_threads) self.coro = coro self.in_queue = in_queue self.out_queue = out_queue @@ -330,8 +329,9 @@ class LastPipelineThread(PipelineThread): """A thread running the last stage in a pipeline. The coroutine should yield nothing. """ + def __init__(self, coro, in_queue, all_threads): - super(LastPipelineThread, self).__init__(all_threads) + super().__init__(all_threads) self.coro = coro self.in_queue = in_queue @@ -362,17 +362,18 @@ class LastPipelineThread(PipelineThread): return -class Pipeline(object): +class Pipeline: """Represents a staged pattern of work. Each stage in the pipeline is a coroutine that receives messages from the previous stage and yields messages to be sent to the next stage. """ + def __init__(self, stages): """Makes a new pipeline from a list of coroutines. There must be at least two stages. """ if len(stages) < 2: - raise ValueError(u'pipeline must have at least two stages') + raise ValueError('pipeline must have at least two stages') self.stages = [] for stage in stages: if isinstance(stage, (list, tuple)): @@ -442,7 +443,7 @@ class Pipeline(object): exc_info = thread.exc_info if exc_info: # Make the exception appear as it was raised originally. - six.reraise(exc_info[0], exc_info[1], exc_info[2]) + raise exc_info[1].with_traceback(exc_info[2]) def pull(self): """Yield elements from the end of the pipeline. Runs the stages @@ -469,6 +470,7 @@ class Pipeline(object): for msg in msgs: yield msg + # Smoke test. if __name__ == '__main__': import time @@ -477,14 +479,14 @@ if __name__ == '__main__': # in parallel. def produce(): for i in range(5): - print(u'generating %i' % i) + print('generating %i' % i) time.sleep(1) yield i def work(): num = yield while True: - print(u'processing %i' % num) + print('processing %i' % num) time.sleep(2) num = yield num * 2 @@ -492,7 +494,7 @@ if __name__ == '__main__': while True: num = yield time.sleep(1) - print(u'received %i' % num) + print('received %i' % num) ts_start = time.time() Pipeline([produce(), work(), consume()]).run_sequential() @@ -501,22 +503,22 @@ if __name__ == '__main__': ts_par = time.time() Pipeline([produce(), (work(), work()), consume()]).run_parallel() ts_end = time.time() - print(u'Sequential time:', ts_seq - ts_start) - print(u'Parallel time:', ts_par - ts_seq) - print(u'Multiply-parallel time:', ts_end - ts_par) + print('Sequential time:', ts_seq - ts_start) + print('Parallel time:', ts_par - ts_seq) + print('Multiply-parallel time:', ts_end - ts_par) print() # Test a pipeline that raises an exception. def exc_produce(): for i in range(10): - print(u'generating %i' % i) + print('generating %i' % i) time.sleep(1) yield i def exc_work(): num = yield while True: - print(u'processing %i' % num) + print('processing %i' % num) time.sleep(3) if num == 3: raise Exception() @@ -525,6 +527,6 @@ if __name__ == '__main__': def exc_consume(): while True: num = yield - print(u'received %i' % num) + print('received %i' % num) Pipeline([exc_produce(), exc_work(), exc_consume()]).run_parallel(1) diff --git a/libs/common/beets/vfs.py b/libs/common/beets/vfs.py index 7f9a049e..aef69650 100644 --- a/libs/common/beets/vfs.py +++ b/libs/common/beets/vfs.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -16,7 +15,6 @@ """A simple utility for constructing filesystem-like trees from beets libraries. """ -from __future__ import division, absolute_import, print_function from collections import namedtuple from beets import util diff --git a/libs/common/beetsplug/__init__.py b/libs/common/beetsplug/__init__.py index febeb66f..da248491 100644 --- a/libs/common/beetsplug/__init__.py +++ b/libs/common/beetsplug/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,7 +14,6 @@ """A namespace package for beets plugins.""" -from __future__ import division, absolute_import, print_function # Make this a namespace package. from pkgutil import extend_path diff --git a/libs/common/beetsplug/absubmit.py b/libs/common/beetsplug/absubmit.py index 0c288b9d..d1ea692f 100644 --- a/libs/common/beetsplug/absubmit.py +++ b/libs/common/beetsplug/absubmit.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Pieter Mulder. # @@ -16,7 +15,6 @@ """Calculate acoustic information and submit to AcousticBrainz. """ -from __future__ import division, absolute_import, print_function import errno import hashlib @@ -32,6 +30,9 @@ from beets import plugins from beets import util from beets import ui +# We use this field to check whether AcousticBrainz info is present. +PROBE_FIELD = 'mood_acoustic' + class ABSubmitError(Exception): """Raised when failing to analyse file with extractor.""" @@ -43,19 +44,23 @@ def call(args): Raise a AnalysisABSubmitError on failure. """ try: - return util.command_output(args) + return util.command_output(args).stdout except subprocess.CalledProcessError as e: raise ABSubmitError( - u'{0} exited with status {1}'.format(args[0], e.returncode) + '{} exited with status {}'.format(args[0], e.returncode) ) class AcousticBrainzSubmitPlugin(plugins.BeetsPlugin): def __init__(self): - super(AcousticBrainzSubmitPlugin, self).__init__() + super().__init__() - self.config.add({'extractor': u''}) + self.config.add({ + 'extractor': '', + 'force': False, + 'pretend': False + }) self.extractor = self.config['extractor'].as_str() if self.extractor: @@ -63,7 +68,7 @@ class AcousticBrainzSubmitPlugin(plugins.BeetsPlugin): # Expicit path to extractor if not os.path.isfile(self.extractor): raise ui.UserError( - u'Extractor command does not exist: {0}.'. + 'Extractor command does not exist: {0}.'. format(self.extractor) ) else: @@ -73,8 +78,8 @@ class AcousticBrainzSubmitPlugin(plugins.BeetsPlugin): call([self.extractor]) except OSError: raise ui.UserError( - u'No extractor command found: please install the ' - u'extractor binary from http://acousticbrainz.org/download' + 'No extractor command found: please install the extractor' + ' binary from https://acousticbrainz.org/download' ) except ABSubmitError: # Extractor found, will exit with an error if not called with @@ -96,7 +101,18 @@ class AcousticBrainzSubmitPlugin(plugins.BeetsPlugin): def commands(self): cmd = ui.Subcommand( 'absubmit', - help=u'calculate and submit AcousticBrainz analysis' + help='calculate and submit AcousticBrainz analysis' + ) + cmd.parser.add_option( + '-f', '--force', dest='force_refetch', + action='store_true', default=False, + help='re-download data when already present' + ) + cmd.parser.add_option( + '-p', '--pretend', dest='pretend_fetch', + action='store_true', default=False, + help='pretend to perform action, but show \ +only files which would be processed' ) cmd.func = self.command return [cmd] @@ -104,17 +120,30 @@ class AcousticBrainzSubmitPlugin(plugins.BeetsPlugin): def command(self, lib, opts, args): # Get items from arguments items = lib.items(ui.decargs(args)) - for item in items: - analysis = self._get_analysis(item) - if analysis: - self._submit_data(item, analysis) + self.opts = opts + util.par_map(self.analyze_submit, items) + + def analyze_submit(self, item): + analysis = self._get_analysis(item) + if analysis: + self._submit_data(item, analysis) def _get_analysis(self, item): mbid = item['mb_trackid'] - # If file has no mbid skip it. + + # Avoid re-analyzing files that already have AB data. + if not self.opts.force_refetch and not self.config['force']: + if item.get(PROBE_FIELD): + return None + + # If file has no MBID, skip it. if not mbid: - self._log.info(u'Not analysing {}, missing ' - u'musicbrainz track id.', item) + self._log.info('Not analysing {}, missing ' + 'musicbrainz track id.', item) + return None + + if self.opts.pretend_fetch or self.config['pretend']: + self._log.info('pretend action - extract item: {}', item) return None # Temporary file to save extractor output to, extractor only works @@ -129,11 +158,11 @@ class AcousticBrainzSubmitPlugin(plugins.BeetsPlugin): call([self.extractor, util.syspath(item.path), filename]) except ABSubmitError as e: self._log.warning( - u'Failed to analyse {item} for AcousticBrainz: {error}', + 'Failed to analyse {item} for AcousticBrainz: {error}', item=item, error=e ) return None - with open(filename, 'rb') as tmp_file: + with open(filename) as tmp_file: analysis = json.load(tmp_file) # Add the hash to the output. analysis['metadata']['version']['essentia_build_sha'] = \ @@ -157,11 +186,11 @@ class AcousticBrainzSubmitPlugin(plugins.BeetsPlugin): try: message = response.json()['message'] except (ValueError, KeyError) as e: - message = u'unable to get error message: {}'.format(e) + message = f'unable to get error message: {e}' self._log.error( - u'Failed to submit AcousticBrainz analysis of {item}: ' - u'{message}).', item=item, message=message + 'Failed to submit AcousticBrainz analysis of {item}: ' + '{message}).', item=item, message=message ) else: - self._log.debug(u'Successfully submitted AcousticBrainz analysis ' - u'for {}.', item) + self._log.debug('Successfully submitted AcousticBrainz analysis ' + 'for {}.', item) diff --git a/libs/common/beetsplug/acousticbrainz.py b/libs/common/beetsplug/acousticbrainz.py index f4960c30..eabc5849 100644 --- a/libs/common/beetsplug/acousticbrainz.py +++ b/libs/common/beetsplug/acousticbrainz.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2015-2016, Ohm Patel. # @@ -15,12 +14,13 @@ """Fetch various AcousticBrainz metadata using MBID. """ -from __future__ import division, absolute_import, print_function + +from collections import defaultdict import requests -from collections import defaultdict from beets import plugins, ui +from beets.dbcore import types ACOUSTIC_BASE = "https://acousticbrainz.org/" LEVELS = ["/low-level", "/high-level"] @@ -72,6 +72,9 @@ ABSCHEME = { 'sad': 'mood_sad' } }, + 'moods_mirex': { + 'value': 'moods_mirex' + }, 'ismir04_rhythm': { 'value': 'rhythm' }, @@ -80,6 +83,9 @@ ABSCHEME = { 'tonal': 'tonal' } }, + 'timbre': { + 'value': 'timbre' + }, 'voice_instrumental': { 'value': 'voice_instrumental' }, @@ -104,8 +110,33 @@ ABSCHEME = { class AcousticPlugin(plugins.BeetsPlugin): + item_types = { + 'average_loudness': types.Float(6), + 'chords_changes_rate': types.Float(6), + 'chords_key': types.STRING, + 'chords_number_rate': types.Float(6), + 'chords_scale': types.STRING, + 'danceable': types.Float(6), + 'gender': types.STRING, + 'genre_rosamerica': types.STRING, + 'initial_key': types.STRING, + 'key_strength': types.Float(6), + 'mood_acoustic': types.Float(6), + 'mood_aggressive': types.Float(6), + 'mood_electronic': types.Float(6), + 'mood_happy': types.Float(6), + 'mood_party': types.Float(6), + 'mood_relaxed': types.Float(6), + 'mood_sad': types.Float(6), + 'moods_mirex': types.STRING, + 'rhythm': types.Float(6), + 'timbre': types.STRING, + 'tonal': types.Float(6), + 'voice_instrumental': types.STRING, + } + def __init__(self): - super(AcousticPlugin, self).__init__() + super().__init__() self.config.add({ 'auto': True, @@ -119,11 +150,11 @@ class AcousticPlugin(plugins.BeetsPlugin): def commands(self): cmd = ui.Subcommand('acousticbrainz', - help=u"fetch metadata from AcousticBrainz") + help="fetch metadata from AcousticBrainz") cmd.parser.add_option( - u'-f', u'--force', dest='force_refetch', + '-f', '--force', dest='force_refetch', action='store_true', default=False, - help=u're-download data when already present' + help='re-download data when already present' ) def func(lib, opts, args): @@ -142,22 +173,22 @@ class AcousticPlugin(plugins.BeetsPlugin): def _get_data(self, mbid): data = {} for url in _generate_urls(mbid): - self._log.debug(u'fetching URL: {}', url) + self._log.debug('fetching URL: {}', url) try: res = requests.get(url) except requests.RequestException as exc: - self._log.info(u'request error: {}', exc) + self._log.info('request error: {}', exc) return {} if res.status_code == 404: - self._log.info(u'recording ID {} not found', mbid) + self._log.info('recording ID {} not found', mbid) return {} try: data.update(res.json()) except ValueError: - self._log.debug(u'Invalid Response: {}', res.text) + self._log.debug('Invalid Response: {}', res.text) return {} return data @@ -172,28 +203,28 @@ class AcousticPlugin(plugins.BeetsPlugin): # representative field name to check for previously fetched # data. if not force: - mood_str = item.get('mood_acoustic', u'') + mood_str = item.get('mood_acoustic', '') if mood_str: - self._log.info(u'data already present for: {}', item) + self._log.info('data already present for: {}', item) continue # We can only fetch data for tracks with MBIDs. if not item.mb_trackid: continue - self._log.info(u'getting data for: {}', item) + self._log.info('getting data for: {}', item) data = self._get_data(item.mb_trackid) if data: for attr, val in self._map_data_to_scheme(data, ABSCHEME): if not tags or attr in tags: - self._log.debug(u'attribute {} of {} set to {}', + self._log.debug('attribute {} of {} set to {}', attr, item, val) setattr(item, attr, val) else: - self._log.debug(u'skipping attribute {} of {}' - u' (value {}) due to config', + self._log.debug('skipping attribute {} of {}' + ' (value {}) due to config', attr, item, val) @@ -255,10 +286,9 @@ class AcousticPlugin(plugins.BeetsPlugin): # The recursive traversal. composites = defaultdict(list) - for attr, val in self._data_to_scheme_child(data, - scheme, - composites): - yield attr, val + yield from self._data_to_scheme_child(data, + scheme, + composites) # When composites has been populated, yield the composite attributes # by joining their parts. @@ -278,10 +308,9 @@ class AcousticPlugin(plugins.BeetsPlugin): for k, v in subscheme.items(): if k in subdata: if type(v) == dict: - for attr, val in self._data_to_scheme_child(subdata[k], - v, - composites): - yield attr, val + yield from self._data_to_scheme_child(subdata[k], + v, + composites) elif type(v) == tuple: composite_attribute, part_number = v attribute_parts = composites[composite_attribute] @@ -292,10 +321,10 @@ class AcousticPlugin(plugins.BeetsPlugin): else: yield v, subdata[k] else: - self._log.warning(u'Acousticbrainz did not provide info' - u'about {}', k) - self._log.debug(u'Data {} could not be mapped to scheme {} ' - u'because key {} was not found', subdata, v, k) + self._log.warning('Acousticbrainz did not provide info' + 'about {}', k) + self._log.debug('Data {} could not be mapped to scheme {} ' + 'because key {} was not found', subdata, v, k) def _generate_urls(mbid): diff --git a/libs/common/beetsplug/albumtypes.py b/libs/common/beetsplug/albumtypes.py new file mode 100644 index 00000000..47f8dc64 --- /dev/null +++ b/libs/common/beetsplug/albumtypes.py @@ -0,0 +1,65 @@ +# This file is part of beets. +# Copyright 2021, Edgars Supe. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Adds an album template field for formatted album types.""" + + +from beets.autotag.mb import VARIOUS_ARTISTS_ID +from beets.library import Album +from beets.plugins import BeetsPlugin + + +class AlbumTypesPlugin(BeetsPlugin): + """Adds an album template field for formatted album types.""" + + def __init__(self): + """Init AlbumTypesPlugin.""" + super().__init__() + self.album_template_fields['atypes'] = self._atypes + self.config.add({ + 'types': [ + ('ep', 'EP'), + ('single', 'Single'), + ('soundtrack', 'OST'), + ('live', 'Live'), + ('compilation', 'Anthology'), + ('remix', 'Remix') + ], + 'ignore_va': ['compilation'], + 'bracket': '[]' + }) + + def _atypes(self, item: Album): + """Returns a formatted string based on album's types.""" + types = self.config['types'].as_pairs() + ignore_va = self.config['ignore_va'].as_str_seq() + bracket = self.config['bracket'].as_str() + + # Assign a left and right bracket or leave blank if argument is empty. + if len(bracket) == 2: + bracket_l = bracket[0] + bracket_r = bracket[1] + else: + bracket_l = '' + bracket_r = '' + + res = '' + albumtypes = item.albumtypes.split('; ') + is_va = item.mb_albumartistid == VARIOUS_ARTISTS_ID + for type in types: + if type[0] in albumtypes and type[1]: + if not is_va or (type[0] not in ignore_va and is_va): + res += f'{bracket_l}{type[1]}{bracket_r}' + + return res diff --git a/libs/common/beetsplug/aura.py b/libs/common/beetsplug/aura.py new file mode 100644 index 00000000..f4ae5527 --- /dev/null +++ b/libs/common/beetsplug/aura.py @@ -0,0 +1,984 @@ +# This file is part of beets. +# Copyright 2020, Callum Brown. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""An AURA server using Flask.""" + + +from mimetypes import guess_type +import re +import os.path +from os.path import isfile, getsize + +from beets.plugins import BeetsPlugin +from beets.ui import Subcommand, _open_library +from beets import config +from beets.util import py3_path +from beets.library import Item, Album +from beets.dbcore.query import ( + MatchQuery, + NotQuery, + RegexpQuery, + AndQuery, + FixedFieldSort, + SlowFieldSort, + MultipleSort, +) + +from flask import ( + Blueprint, + Flask, + current_app, + send_file, + make_response, + request, +) + + +# Constants + +# AURA server information +# TODO: Add version information +SERVER_INFO = { + "aura-version": "0", + "server": "beets-aura", + "server-version": "0.1", + "auth-required": False, + "features": ["albums", "artists", "images"], +} + +# Maps AURA Track attribute to beets Item attribute +TRACK_ATTR_MAP = { + # Required + "title": "title", + "artist": "artist", + # Optional + "album": "album", + "track": "track", # Track number on album + "tracktotal": "tracktotal", + "disc": "disc", + "disctotal": "disctotal", + "year": "year", + "month": "month", + "day": "day", + "bpm": "bpm", + "genre": "genre", + "recording-mbid": "mb_trackid", # beets trackid is MB recording + "track-mbid": "mb_releasetrackid", + "composer": "composer", + "albumartist": "albumartist", + "comments": "comments", + # Optional for Audio Metadata + # TODO: Support the mimetype attribute, format != mime type + # "mimetype": track.format, + "duration": "length", + "framerate": "samplerate", + # I don't think beets has a framecount field + # "framecount": ???, + "channels": "channels", + "bitrate": "bitrate", + "bitdepth": "bitdepth", + "size": "filesize", +} + +# Maps AURA Album attribute to beets Album attribute +ALBUM_ATTR_MAP = { + # Required + "title": "album", + "artist": "albumartist", + # Optional + "tracktotal": "albumtotal", + "disctotal": "disctotal", + "year": "year", + "month": "month", + "day": "day", + "genre": "genre", + "release-mbid": "mb_albumid", + "release-group-mbid": "mb_releasegroupid", +} + +# Maps AURA Artist attribute to beets Item field +# Artists are not first-class in beets, so information is extracted from +# beets Items. +ARTIST_ATTR_MAP = { + # Required + "name": "artist", + # Optional + "artist-mbid": "mb_artistid", +} + + +class AURADocument: + """Base class for building AURA documents.""" + + @staticmethod + def error(status, title, detail): + """Make a response for an error following the JSON:API spec. + + Args: + status: An HTTP status code string, e.g. "404 Not Found". + title: A short, human-readable summary of the problem. + detail: A human-readable explanation specific to this + occurrence of the problem. + """ + document = { + "errors": [{"status": status, "title": title, "detail": detail}] + } + return make_response(document, status) + + def translate_filters(self): + """Translate filters from request arguments to a beets Query.""" + # The format of each filter key in the request parameter is: + # filter[]. This regex extracts . + pattern = re.compile(r"filter\[(?P[a-zA-Z0-9_-]+)\]") + queries = [] + for key, value in request.args.items(): + match = pattern.match(key) + if match: + # Extract attribute name from key + aura_attr = match.group("attribute") + # Get the beets version of the attribute name + beets_attr = self.attribute_map.get(aura_attr, aura_attr) + converter = self.get_attribute_converter(beets_attr) + value = converter(value) + # Add exact match query to list + # Use a slow query so it works with all fields + queries.append(MatchQuery(beets_attr, value, fast=False)) + # NOTE: AURA doesn't officially support multiple queries + return AndQuery(queries) + + def translate_sorts(self, sort_arg): + """Translate an AURA sort parameter into a beets Sort. + + Args: + sort_arg: The value of the 'sort' query parameter; a comma + separated list of fields to sort by, in order. + E.g. "-year,title". + """ + # Change HTTP query parameter to a list + aura_sorts = sort_arg.strip(",").split(",") + sorts = [] + for aura_attr in aura_sorts: + if aura_attr[0] == "-": + ascending = False + # Remove leading "-" + aura_attr = aura_attr[1:] + else: + # JSON:API default + ascending = True + # Get the beets version of the attribute name + beets_attr = self.attribute_map.get(aura_attr, aura_attr) + # Use slow sort so it works with all fields (inc. computed) + sorts.append(SlowFieldSort(beets_attr, ascending=ascending)) + return MultipleSort(sorts) + + def paginate(self, collection): + """Get a page of the collection and the URL to the next page. + + Args: + collection: The raw data from which resource objects can be + built. Could be an sqlite3.Cursor object (tracks and + albums) or a list of strings (artists). + """ + # Pages start from zero + page = request.args.get("page", 0, int) + # Use page limit defined in config by default. + default_limit = config["aura"]["page_limit"].get(int) + limit = request.args.get("limit", default_limit, int) + # start = offset of first item to return + start = page * limit + # end = offset of last item + 1 + end = start + limit + if end > len(collection): + end = len(collection) + next_url = None + else: + # Not the last page so work out links.next url + if not request.args: + # No existing arguments, so current page is 0 + next_url = request.url + "?page=1" + elif not request.args.get("page", None): + # No existing page argument, so add one to the end + next_url = request.url + "&page=1" + else: + # Increment page token by 1 + next_url = request.url.replace( + f"page={page}", "page={}".format(page + 1) + ) + # Get only the items in the page range + data = [self.resource_object(collection[i]) for i in range(start, end)] + return data, next_url + + def get_included(self, data, include_str): + """Build a list of resource objects for inclusion. + + Args: + data: An array of dicts in the form of resource objects. + include_str: A comma separated list of resource types to + include. E.g. "tracks,images". + """ + # Change HTTP query parameter to a list + to_include = include_str.strip(",").split(",") + # Build a list of unique type and id combinations + # For each resource object in the primary data, iterate over it's + # relationships. If a relationship matches one of the types + # requested for inclusion (e.g. "albums") then add each type-id pair + # under the "data" key to unique_identifiers, checking first that + # it has not already been added. This ensures that no resources are + # included more than once. + unique_identifiers = [] + for res_obj in data: + for rel_name, rel_obj in res_obj["relationships"].items(): + if rel_name in to_include: + # NOTE: Assumes relationship is to-many + for identifier in rel_obj["data"]: + if identifier not in unique_identifiers: + unique_identifiers.append(identifier) + # TODO: I think this could be improved + included = [] + for identifier in unique_identifiers: + res_type = identifier["type"] + if res_type == "track": + track_id = int(identifier["id"]) + track = current_app.config["lib"].get_item(track_id) + included.append(TrackDocument.resource_object(track)) + elif res_type == "album": + album_id = int(identifier["id"]) + album = current_app.config["lib"].get_album(album_id) + included.append(AlbumDocument.resource_object(album)) + elif res_type == "artist": + artist_id = identifier["id"] + included.append(ArtistDocument.resource_object(artist_id)) + elif res_type == "image": + image_id = identifier["id"] + included.append(ImageDocument.resource_object(image_id)) + else: + raise ValueError(f"Invalid resource type: {res_type}") + return included + + def all_resources(self): + """Build document for /tracks, /albums or /artists.""" + query = self.translate_filters() + sort_arg = request.args.get("sort", None) + if sort_arg: + sort = self.translate_sorts(sort_arg) + # For each sort field add a query which ensures all results + # have a non-empty, non-zero value for that field. + for s in sort.sorts: + query.subqueries.append( + NotQuery( + # Match empty fields (^$) or zero fields, (^0$) + RegexpQuery(s.field, "(^$|^0$)", fast=False) + ) + ) + else: + sort = None + # Get information from the library + collection = self.get_collection(query=query, sort=sort) + # Convert info to AURA form and paginate it + data, next_url = self.paginate(collection) + document = {"data": data} + # If there are more pages then provide a way to access them + if next_url: + document["links"] = {"next": next_url} + # Include related resources for each element in "data" + include_str = request.args.get("include", None) + if include_str: + document["included"] = self.get_included(data, include_str) + return document + + def single_resource_document(self, resource_object): + """Build document for a specific requested resource. + + Args: + resource_object: A dictionary in the form of a JSON:API + resource object. + """ + document = {"data": resource_object} + include_str = request.args.get("include", None) + if include_str: + # [document["data"]] is because arg needs to be list + document["included"] = self.get_included( + [document["data"]], include_str + ) + return document + + +class TrackDocument(AURADocument): + """Class for building documents for /tracks endpoints.""" + + attribute_map = TRACK_ATTR_MAP + + def get_collection(self, query=None, sort=None): + """Get Item objects from the library. + + Args: + query: A beets Query object or a beets query string. + sort: A beets Sort object. + """ + return current_app.config["lib"].items(query, sort) + + def get_attribute_converter(self, beets_attr): + """Work out what data type an attribute should be for beets. + + Args: + beets_attr: The name of the beets attribute, e.g. "title". + """ + # filesize is a special field (read from disk not db?) + if beets_attr == "filesize": + converter = int + else: + try: + # Look for field in list of Item fields + # and get python type of database type. + # See beets.library.Item and beets.dbcore.types + converter = Item._fields[beets_attr].model_type + except KeyError: + # Fall back to string (NOTE: probably not good) + converter = str + return converter + + @staticmethod + def resource_object(track): + """Construct a JSON:API resource object from a beets Item. + + Args: + track: A beets Item object. + """ + attributes = {} + # Use aura => beets attribute map, e.g. size => filesize + for aura_attr, beets_attr in TRACK_ATTR_MAP.items(): + a = getattr(track, beets_attr) + # Only set attribute if it's not None, 0, "", etc. + # NOTE: This could result in required attributes not being set + if a: + attributes[aura_attr] = a + + # JSON:API one-to-many relationship to parent album + relationships = { + "artists": {"data": [{"type": "artist", "id": track.artist}]} + } + # Only add album relationship if not singleton + if not track.singleton: + relationships["albums"] = { + "data": [{"type": "album", "id": str(track.album_id)}] + } + + return { + "type": "track", + "id": str(track.id), + "attributes": attributes, + "relationships": relationships, + } + + def single_resource(self, track_id): + """Get track from the library and build a document. + + Args: + track_id: The beets id of the track (integer). + """ + track = current_app.config["lib"].get_item(track_id) + if not track: + return self.error( + "404 Not Found", + "No track with the requested id.", + "There is no track with an id of {} in the library.".format( + track_id + ), + ) + return self.single_resource_document(self.resource_object(track)) + + +class AlbumDocument(AURADocument): + """Class for building documents for /albums endpoints.""" + + attribute_map = ALBUM_ATTR_MAP + + def get_collection(self, query=None, sort=None): + """Get Album objects from the library. + + Args: + query: A beets Query object or a beets query string. + sort: A beets Sort object. + """ + return current_app.config["lib"].albums(query, sort) + + def get_attribute_converter(self, beets_attr): + """Work out what data type an attribute should be for beets. + + Args: + beets_attr: The name of the beets attribute, e.g. "title". + """ + try: + # Look for field in list of Album fields + # and get python type of database type. + # See beets.library.Album and beets.dbcore.types + converter = Album._fields[beets_attr].model_type + except KeyError: + # Fall back to string (NOTE: probably not good) + converter = str + return converter + + @staticmethod + def resource_object(album): + """Construct a JSON:API resource object from a beets Album. + + Args: + album: A beets Album object. + """ + attributes = {} + # Use aura => beets attribute name map + for aura_attr, beets_attr in ALBUM_ATTR_MAP.items(): + a = getattr(album, beets_attr) + # Only set attribute if it's not None, 0, "", etc. + # NOTE: This could mean required attributes are not set + if a: + attributes[aura_attr] = a + + # Get beets Item objects for all tracks in the album sorted by + # track number. Sorting is not required but it's nice. + query = MatchQuery("album_id", album.id) + sort = FixedFieldSort("track", ascending=True) + tracks = current_app.config["lib"].items(query, sort) + # JSON:API one-to-many relationship to tracks on the album + relationships = { + "tracks": { + "data": [{"type": "track", "id": str(t.id)} for t in tracks] + } + } + # Add images relationship if album has associated images + if album.artpath: + path = py3_path(album.artpath) + filename = path.split("/")[-1] + image_id = f"album-{album.id}-{filename}" + relationships["images"] = { + "data": [{"type": "image", "id": image_id}] + } + # Add artist relationship if artist name is same on tracks + # Tracks are used to define artists so don't albumartist + # Check for all tracks in case some have featured artists + if album.albumartist in [t.artist for t in tracks]: + relationships["artists"] = { + "data": [{"type": "artist", "id": album.albumartist}] + } + + return { + "type": "album", + "id": str(album.id), + "attributes": attributes, + "relationships": relationships, + } + + def single_resource(self, album_id): + """Get album from the library and build a document. + + Args: + album_id: The beets id of the album (integer). + """ + album = current_app.config["lib"].get_album(album_id) + if not album: + return self.error( + "404 Not Found", + "No album with the requested id.", + "There is no album with an id of {} in the library.".format( + album_id + ), + ) + return self.single_resource_document(self.resource_object(album)) + + +class ArtistDocument(AURADocument): + """Class for building documents for /artists endpoints.""" + + attribute_map = ARTIST_ATTR_MAP + + def get_collection(self, query=None, sort=None): + """Get a list of artist names from the library. + + Args: + query: A beets Query object or a beets query string. + sort: A beets Sort object. + """ + # Gets only tracks with matching artist information + tracks = current_app.config["lib"].items(query, sort) + collection = [] + for track in tracks: + # Do not add duplicates + if track.artist not in collection: + collection.append(track.artist) + return collection + + def get_attribute_converter(self, beets_attr): + """Work out what data type an attribute should be for beets. + + Args: + beets_attr: The name of the beets attribute, e.g. "artist". + """ + try: + # Look for field in list of Item fields + # and get python type of database type. + # See beets.library.Item and beets.dbcore.types + converter = Item._fields[beets_attr].model_type + except KeyError: + # Fall back to string (NOTE: probably not good) + converter = str + return converter + + @staticmethod + def resource_object(artist_id): + """Construct a JSON:API resource object for the given artist. + + Args: + artist_id: A string which is the artist's name. + """ + # Get tracks where artist field exactly matches artist_id + query = MatchQuery("artist", artist_id) + tracks = current_app.config["lib"].items(query) + if not tracks: + return None + + # Get artist information from the first track + # NOTE: It could be that the first track doesn't have a + # MusicBrainz id but later tracks do, which isn't ideal. + attributes = {} + # Use aura => beets attribute map, e.g. artist => name + for aura_attr, beets_attr in ARTIST_ATTR_MAP.items(): + a = getattr(tracks[0], beets_attr) + # Only set attribute if it's not None, 0, "", etc. + # NOTE: This could mean required attributes are not set + if a: + attributes[aura_attr] = a + + relationships = { + "tracks": { + "data": [{"type": "track", "id": str(t.id)} for t in tracks] + } + } + album_query = MatchQuery("albumartist", artist_id) + albums = current_app.config["lib"].albums(query=album_query) + if len(albums) != 0: + relationships["albums"] = { + "data": [{"type": "album", "id": str(a.id)} for a in albums] + } + + return { + "type": "artist", + "id": artist_id, + "attributes": attributes, + "relationships": relationships, + } + + def single_resource(self, artist_id): + """Get info for the requested artist and build a document. + + Args: + artist_id: A string which is the artist's name. + """ + artist_resource = self.resource_object(artist_id) + if not artist_resource: + return self.error( + "404 Not Found", + "No artist with the requested id.", + "There is no artist with an id of {} in the library.".format( + artist_id + ), + ) + return self.single_resource_document(artist_resource) + + +def safe_filename(fn): + """Check whether a string is a simple (non-path) filename. + + For example, `foo.txt` is safe because it is a "plain" filename. But + `foo/bar.txt` and `../foo.txt` and `.` are all non-safe because they + can traverse to other directories other than the current one. + """ + # Rule out any directories. + if os.path.basename(fn) != fn: + return False + + # In single names, rule out Unix directory traversal names. + if fn in ('.', '..'): + return False + + return True + + +class ImageDocument(AURADocument): + """Class for building documents for /images/(id) endpoints.""" + + @staticmethod + def get_image_path(image_id): + """Works out the full path to the image with the given id. + + Returns None if there is no such image. + + Args: + image_id: A string in the form + "--". + """ + # Split image_id into its constituent parts + id_split = image_id.split("-") + if len(id_split) < 3: + # image_id is not in the required format + return None + parent_type = id_split[0] + parent_id = id_split[1] + img_filename = "-".join(id_split[2:]) + if not safe_filename(img_filename): + return None + + # Get the path to the directory parent's images are in + if parent_type == "album": + album = current_app.config["lib"].get_album(int(parent_id)) + if not album or not album.artpath: + return None + # Cut the filename off of artpath + # This is in preparation for supporting images in the same + # directory that are not tracked by beets. + artpath = py3_path(album.artpath) + dir_path = "/".join(artpath.split("/")[:-1]) + else: + # Images for other resource types are not supported + return None + + img_path = os.path.join(dir_path, img_filename) + # Check the image actually exists + if isfile(img_path): + return img_path + else: + return None + + @staticmethod + def resource_object(image_id): + """Construct a JSON:API resource object for the given image. + + Args: + image_id: A string in the form + "--". + """ + # Could be called as a static method, so can't use + # self.get_image_path() + image_path = ImageDocument.get_image_path(image_id) + if not image_path: + return None + + attributes = { + "role": "cover", + "mimetype": guess_type(image_path)[0], + "size": getsize(image_path), + } + try: + from PIL import Image + except ImportError: + pass + else: + im = Image.open(image_path) + attributes["width"] = im.width + attributes["height"] = im.height + + relationships = {} + # Split id into [parent_type, parent_id, filename] + id_split = image_id.split("-") + relationships[id_split[0] + "s"] = { + "data": [{"type": id_split[0], "id": id_split[1]}] + } + + return { + "id": image_id, + "type": "image", + # Remove attributes that are None, 0, "", etc. + "attributes": {k: v for k, v in attributes.items() if v}, + "relationships": relationships, + } + + def single_resource(self, image_id): + """Get info for the requested image and build a document. + + Args: + image_id: A string in the form + "--". + """ + image_resource = self.resource_object(image_id) + if not image_resource: + return self.error( + "404 Not Found", + "No image with the requested id.", + "There is no image with an id of {} in the library.".format( + image_id + ), + ) + return self.single_resource_document(image_resource) + + +# Initialise flask blueprint +aura_bp = Blueprint("aura_bp", __name__) + + +@aura_bp.route("/server") +def server_info(): + """Respond with info about the server.""" + return {"data": {"type": "server", "id": "0", "attributes": SERVER_INFO}} + + +# Track endpoints + + +@aura_bp.route("/tracks") +def all_tracks(): + """Respond with a list of all tracks and related information.""" + doc = TrackDocument() + return doc.all_resources() + + +@aura_bp.route("/tracks/") +def single_track(track_id): + """Respond with info about the specified track. + + Args: + track_id: The id of the track provided in the URL (integer). + """ + doc = TrackDocument() + return doc.single_resource(track_id) + + +@aura_bp.route("/tracks//audio") +def audio_file(track_id): + """Supply an audio file for the specified track. + + Args: + track_id: The id of the track provided in the URL (integer). + """ + track = current_app.config["lib"].get_item(track_id) + if not track: + return AURADocument.error( + "404 Not Found", + "No track with the requested id.", + "There is no track with an id of {} in the library.".format( + track_id + ), + ) + + path = py3_path(track.path) + if not isfile(path): + return AURADocument.error( + "404 Not Found", + "No audio file for the requested track.", + ( + "There is no audio file for track {} at the expected location" + ).format(track_id), + ) + + file_mimetype = guess_type(path)[0] + if not file_mimetype: + return AURADocument.error( + "500 Internal Server Error", + "Requested audio file has an unknown mimetype.", + ( + "The audio file for track {} has an unknown mimetype. " + "Its file extension is {}." + ).format(track_id, path.split(".")[-1]), + ) + + # Check that the Accept header contains the file's mimetype + # Takes into account */* and audio/* + # Adding support for the bitrate parameter would require some effort so I + # left it out. This means the client could be sent an error even if the + # audio doesn't need transcoding. + if not request.accept_mimetypes.best_match([file_mimetype]): + return AURADocument.error( + "406 Not Acceptable", + "Unsupported MIME type or bitrate parameter in Accept header.", + ( + "The audio file for track {} is only available as {} and " + "bitrate parameters are not supported." + ).format(track_id, file_mimetype), + ) + + return send_file( + path, + mimetype=file_mimetype, + # Handles filename in Content-Disposition header + as_attachment=True, + # Tries to upgrade the stream to support range requests + conditional=True, + ) + + +# Album endpoints + + +@aura_bp.route("/albums") +def all_albums(): + """Respond with a list of all albums and related information.""" + doc = AlbumDocument() + return doc.all_resources() + + +@aura_bp.route("/albums/") +def single_album(album_id): + """Respond with info about the specified album. + + Args: + album_id: The id of the album provided in the URL (integer). + """ + doc = AlbumDocument() + return doc.single_resource(album_id) + + +# Artist endpoints +# Artist ids are their names + + +@aura_bp.route("/artists") +def all_artists(): + """Respond with a list of all artists and related information.""" + doc = ArtistDocument() + return doc.all_resources() + + +# Using the path converter allows slashes in artist_id +@aura_bp.route("/artists/") +def single_artist(artist_id): + """Respond with info about the specified artist. + + Args: + artist_id: The id of the artist provided in the URL. A string + which is the artist's name. + """ + doc = ArtistDocument() + return doc.single_resource(artist_id) + + +# Image endpoints +# Image ids are in the form -- +# For example: album-13-cover.jpg + + +@aura_bp.route("/images/") +def single_image(image_id): + """Respond with info about the specified image. + + Args: + image_id: The id of the image provided in the URL. A string in + the form "--". + """ + doc = ImageDocument() + return doc.single_resource(image_id) + + +@aura_bp.route("/images//file") +def image_file(image_id): + """Supply an image file for the specified image. + + Args: + image_id: The id of the image provided in the URL. A string in + the form "--". + """ + img_path = ImageDocument.get_image_path(image_id) + if not img_path: + return AURADocument.error( + "404 Not Found", + "No image with the requested id.", + "There is no image with an id of {} in the library".format( + image_id + ), + ) + return send_file(img_path) + + +# WSGI app + + +def create_app(): + """An application factory for use by a WSGI server.""" + config["aura"].add( + { + "host": "127.0.0.1", + "port": 8337, + "cors": [], + "cors_supports_credentials": False, + "page_limit": 500, + } + ) + + app = Flask(__name__) + # Register AURA blueprint view functions under a URL prefix + app.register_blueprint(aura_bp, url_prefix="/aura") + # AURA specifies mimetype MUST be this + app.config["JSONIFY_MIMETYPE"] = "application/vnd.api+json" + # Disable auto-sorting of JSON keys + app.config["JSON_SORT_KEYS"] = False + # Provide a way to access the beets library + # The normal method of using the Library and config provided in the + # command function is not used because create_app() could be called + # by an external WSGI server. + # NOTE: this uses a 'private' function from beets.ui.__init__ + app.config["lib"] = _open_library(config) + + # Enable CORS if required + cors = config["aura"]["cors"].as_str_seq(list) + if cors: + from flask_cors import CORS + + # "Accept" is the only header clients use + app.config["CORS_ALLOW_HEADERS"] = "Accept" + app.config["CORS_RESOURCES"] = {r"/aura/*": {"origins": cors}} + app.config["CORS_SUPPORTS_CREDENTIALS"] = config["aura"][ + "cors_supports_credentials" + ].get(bool) + CORS(app) + + return app + + +# Beets Plugin Hook + + +class AURAPlugin(BeetsPlugin): + """The BeetsPlugin subclass for the AURA server plugin.""" + + def __init__(self): + """Add configuration options for the AURA plugin.""" + super().__init__() + + def commands(self): + """Add subcommand used to run the AURA server.""" + + def run_aura(lib, opts, args): + """Run the application using Flask's built in-server. + + Args: + lib: A beets Library object (not used). + opts: Command line options. An optparse.Values object. + args: The list of arguments to process (not used). + """ + app = create_app() + # Start the built-in server (not intended for production) + app.run( + host=self.config["host"].get(str), + port=self.config["port"].get(int), + debug=opts.debug, + threaded=True, + ) + + run_aura_cmd = Subcommand("aura", help="run an AURA server") + run_aura_cmd.parser.add_option( + "-d", + "--debug", + action="store_true", + default=False, + help="use Flask debug mode", + ) + run_aura_cmd.func = run_aura + return [run_aura_cmd] diff --git a/libs/common/beetsplug/badfiles.py b/libs/common/beetsplug/badfiles.py index 62c6d8af..ec465895 100644 --- a/libs/common/beetsplug/badfiles.py +++ b/libs/common/beetsplug/badfiles.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, François-Xavier Thomas. # @@ -16,18 +15,19 @@ """Use command-line tools to check for audio file corruption. """ -from __future__ import division, absolute_import, print_function -from beets.plugins import BeetsPlugin -from beets.ui import Subcommand -from beets.util import displayable_path, confit -from beets import ui from subprocess import check_output, CalledProcessError, list2cmdline, STDOUT + import shlex import os import errno import sys -import six +import confuse +from beets.plugins import BeetsPlugin +from beets.ui import Subcommand +from beets.util import displayable_path, par_map +from beets import ui +from beets import importer class CheckerCommandException(Exception): @@ -48,8 +48,17 @@ class CheckerCommandException(Exception): class BadFiles(BeetsPlugin): + def __init__(self): + super().__init__() + self.verbose = False + + self.register_listener('import_task_start', + self.on_import_task_start) + self.register_listener('import_task_before_choice', + self.on_import_task_before_choice) + def run_command(self, cmd): - self._log.debug(u"running command: {}", + self._log.debug("running command: {}", displayable_path(list2cmdline(cmd))) try: output = check_output(cmd, stderr=STDOUT) @@ -61,7 +70,7 @@ class BadFiles(BeetsPlugin): status = e.returncode except OSError as e: raise CheckerCommandException(cmd, e) - output = output.decode(sys.getfilesystemencoding()) + output = output.decode(sys.getdefaultencoding(), 'replace') return status, errors, [line for line in output.split("\n") if line] def check_mp3val(self, path): @@ -85,68 +94,122 @@ class BadFiles(BeetsPlugin): ext = ext.lower() try: command = self.config['commands'].get(dict).get(ext) - except confit.NotFoundError: + except confuse.NotFoundError: command = None if command: return self.check_custom(command) - elif ext == "mp3": + if ext == "mp3": return self.check_mp3val - elif ext == "flac": + if ext == "flac": return self.check_flac - def check_bad(self, lib, opts, args): - for item in lib.items(ui.decargs(args)): + def check_item(self, item): + # First, check whether the path exists. If not, the user + # should probably run `beet update` to cleanup your library. + dpath = displayable_path(item.path) + self._log.debug("checking path: {}", dpath) + if not os.path.exists(item.path): + ui.print_("{}: file does not exist".format( + ui.colorize('text_error', dpath))) - # First, check whether the path exists. If not, the user - # should probably run `beet update` to cleanup your library. - dpath = displayable_path(item.path) - self._log.debug(u"checking path: {}", dpath) - if not os.path.exists(item.path): - ui.print_(u"{}: file does not exist".format( - ui.colorize('text_error', dpath))) + # Run the checker against the file if one is found + ext = os.path.splitext(item.path)[1][1:].decode('utf8', 'ignore') + checker = self.get_checker(ext) + if not checker: + self._log.error("no checker specified in the config for {}", + ext) + return [] + path = item.path + if not isinstance(path, str): + path = item.path.decode(sys.getfilesystemencoding()) + try: + status, errors, output = checker(path) + except CheckerCommandException as e: + if e.errno == errno.ENOENT: + self._log.error( + "command not found: {} when validating file: {}", + e.checker, + e.path + ) + else: + self._log.error("error invoking {}: {}", e.checker, e.msg) + return [] - # Run the checker against the file if one is found - ext = os.path.splitext(item.path)[1][1:].decode('utf8', 'ignore') - checker = self.get_checker(ext) - if not checker: - self._log.error(u"no checker specified in the config for {}", - ext) - continue - path = item.path - if not isinstance(path, six.text_type): - path = item.path.decode(sys.getfilesystemencoding()) - try: - status, errors, output = checker(path) - except CheckerCommandException as e: - if e.errno == errno.ENOENT: - self._log.error( - u"command not found: {} when validating file: {}", - e.checker, - e.path - ) - else: - self._log.error(u"error invoking {}: {}", e.checker, e.msg) - continue - if status > 0: - ui.print_(u"{}: checker exited with status {}" - .format(ui.colorize('text_error', dpath), status)) - for line in output: - ui.print_(u" {}".format(displayable_path(line))) - elif errors > 0: - ui.print_(u"{}: checker found {} errors or warnings" - .format(ui.colorize('text_warning', dpath), errors)) - for line in output: - ui.print_(u" {}".format(displayable_path(line))) - elif opts.verbose: - ui.print_(u"{}: ok".format(ui.colorize('text_success', dpath))) + error_lines = [] + + if status > 0: + error_lines.append( + "{}: checker exited with status {}" + .format(ui.colorize('text_error', dpath), status)) + for line in output: + error_lines.append(f" {line}") + + elif errors > 0: + error_lines.append( + "{}: checker found {} errors or warnings" + .format(ui.colorize('text_warning', dpath), errors)) + for line in output: + error_lines.append(f" {line}") + elif self.verbose: + error_lines.append( + "{}: ok".format(ui.colorize('text_success', dpath))) + + return error_lines + + def on_import_task_start(self, task, session): + if not self.config['check_on_import'].get(False): + return + + checks_failed = [] + + for item in task.items: + error_lines = self.check_item(item) + if error_lines: + checks_failed.append(error_lines) + + if checks_failed: + task._badfiles_checks_failed = checks_failed + + def on_import_task_before_choice(self, task, session): + if hasattr(task, '_badfiles_checks_failed'): + ui.print_('{} one or more files failed checks:' + .format(ui.colorize('text_warning', 'BAD'))) + for error in task._badfiles_checks_failed: + for error_line in error: + ui.print_(error_line) + + ui.print_() + ui.print_('What would you like to do?') + + sel = ui.input_options(['aBort', 'skip', 'continue']) + + if sel == 's': + return importer.action.SKIP + elif sel == 'c': + return None + elif sel == 'b': + raise importer.ImportAbort() + else: + raise Exception(f'Unexpected selection: {sel}') + + def command(self, lib, opts, args): + # Get items from arguments + items = lib.items(ui.decargs(args)) + self.verbose = opts.verbose + + def check_and_print(item): + for error_line in self.check_item(item): + ui.print_(error_line) + + par_map(check_and_print, items) def commands(self): bad_command = Subcommand('bad', - help=u'check for corrupt or missing files') + help='check for corrupt or missing files') bad_command.parser.add_option( - u'-v', u'--verbose', + '-v', '--verbose', action='store_true', default=False, dest='verbose', - help=u'view results for both the bad and uncorrupted files' + help='view results for both the bad and uncorrupted files' ) - bad_command.func = self.check_bad + bad_command.func = self.command return [bad_command] diff --git a/libs/common/beetsplug/bareasc.py b/libs/common/beetsplug/bareasc.py new file mode 100644 index 00000000..21836936 --- /dev/null +++ b/libs/common/beetsplug/bareasc.py @@ -0,0 +1,82 @@ +# This file is part of beets. +# Copyright 2016, Philippe Mongeau. +# Copyright 2021, Graham R. Cobb. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and ascociated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# This module is adapted from Fuzzy in accordance to the licence of +# that module + +"""Provides a bare-ASCII matching query.""" + + +from beets import ui +from beets.ui import print_, decargs +from beets.plugins import BeetsPlugin +from beets.dbcore.query import StringFieldQuery +from unidecode import unidecode + + +class BareascQuery(StringFieldQuery): + """Compare items using bare ASCII, without accents etc.""" + @classmethod + def string_match(cls, pattern, val): + """Convert both pattern and string to plain ASCII before matching. + + If pattern is all lower case, also convert string to lower case so + match is also case insensitive + """ + # smartcase + if pattern.islower(): + val = val.lower() + pattern = unidecode(pattern) + val = unidecode(val) + return pattern in val + + +class BareascPlugin(BeetsPlugin): + """Plugin to provide bare-ASCII option for beets matching.""" + def __init__(self): + """Default prefix for selecting bare-ASCII matching is #.""" + super().__init__() + self.config.add({ + 'prefix': '#', + }) + + def queries(self): + """Register bare-ASCII matching.""" + prefix = self.config['prefix'].as_str() + return {prefix: BareascQuery} + + def commands(self): + """Add bareasc command as unidecode version of 'list'.""" + cmd = ui.Subcommand('bareasc', + help='unidecode version of beet list command') + cmd.parser.usage += "\n" \ + 'Example: %prog -f \'$album: $title\' artist:beatles' + cmd.parser.add_all_common_options() + cmd.func = self.unidecode_list + return [cmd] + + def unidecode_list(self, lib, opts, args): + """Emulate normal 'list' command but with unidecode output.""" + query = decargs(args) + album = opts.album + # Copied from commands.py - list_items + if album: + for album in lib.albums(query): + bare = unidecode(str(album)) + print_(bare) + else: + for item in lib.items(query): + bare = unidecode(str(item)) + print_(bare) diff --git a/libs/common/beetsplug/beatport.py b/libs/common/beetsplug/beatport.py index fc412d99..133441d7 100644 --- a/libs/common/beetsplug/beatport.py +++ b/libs/common/beetsplug/beatport.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,11 +14,9 @@ """Adds Beatport release and track search support to the autotagger """ -from __future__ import division, absolute_import, print_function import json import re -import six from datetime import datetime, timedelta from requests_oauthlib import OAuth1Session @@ -28,35 +25,35 @@ from requests_oauthlib.oauth1_session import (TokenRequestDenied, TokenMissing, import beets import beets.ui -from beets.autotag.hooks import AlbumInfo, TrackInfo, Distance -from beets.plugins import BeetsPlugin -from beets.util import confit +from beets.autotag.hooks import AlbumInfo, TrackInfo +from beets.plugins import BeetsPlugin, MetadataSourcePlugin, get_distance +import confuse AUTH_ERRORS = (TokenRequestDenied, TokenMissing, VerifierMissing) -USER_AGENT = u'beets/{0} +http://beets.io/'.format(beets.__version__) +USER_AGENT = f'beets/{beets.__version__} +https://beets.io/' class BeatportAPIError(Exception): pass -class BeatportObject(object): +class BeatportObject: def __init__(self, data): self.beatport_id = data['id'] - self.name = six.text_type(data['name']) + self.name = str(data['name']) if 'releaseDate' in data: self.release_date = datetime.strptime(data['releaseDate'], '%Y-%m-%d') if 'artists' in data: - self.artists = [(x['id'], six.text_type(x['name'])) + self.artists = [(x['id'], str(x['name'])) for x in data['artists']] if 'genres' in data: - self.genres = [six.text_type(x['name']) + self.genres = [str(x['name']) for x in data['genres']] -class BeatportClient(object): +class BeatportClient: _api_base = 'https://oauth-api.beatport.com' def __init__(self, c_key, c_secret, auth_key=None, auth_secret=None): @@ -109,7 +106,7 @@ class BeatportClient(object): :rtype: (unicode, unicode) tuple """ self.api.parse_authorization_response( - "http://beets.io/auth?" + auth_data) + "https://beets.io/auth?" + auth_data) access_data = self.api.fetch_access_token( self._make_url('/identity/1/oauth/access-token')) return access_data['oauth_token'], access_data['oauth_token_secret'] @@ -131,7 +128,7 @@ class BeatportClient(object): """ response = self._get('catalog/3/search', query=query, perPage=5, - facets=['fieldType:{0}'.format(release_type)]) + facets=[f'fieldType:{release_type}']) for item in response: if release_type == 'release': if details: @@ -150,9 +147,11 @@ class BeatportClient(object): :rtype: :py:class:`BeatportRelease` """ response = self._get('/catalog/3/releases', id=beatport_id) - release = BeatportRelease(response[0]) - release.tracks = self.get_release_tracks(beatport_id) - return release + if response: + release = BeatportRelease(response[0]) + release.tracks = self.get_release_tracks(beatport_id) + return release + return None def get_release_tracks(self, beatport_id): """ Get all tracks for a given release. @@ -191,7 +190,7 @@ class BeatportClient(object): response = self.api.get(self._make_url(endpoint), params=kwargs) except Exception as e: raise BeatportAPIError("Error connecting to Beatport API: {}" - .format(e.message)) + .format(e)) if not response: raise BeatportAPIError( "Error {0.status_code} for '{0.request.path_url}" @@ -199,21 +198,20 @@ class BeatportClient(object): return response.json()['results'] -@six.python_2_unicode_compatible class BeatportRelease(BeatportObject): def __str__(self): if len(self.artists) < 4: artist_str = ", ".join(x[1] for x in self.artists) else: artist_str = "Various Artists" - return u"".format( + return "".format( artist_str, self.name, self.catalog_number, ) def __repr__(self): - return six.text_type(self).encode('utf-8') + return str(self).encode('utf-8') def __init__(self, data): BeatportObject.__init__(self, data) @@ -224,26 +222,26 @@ class BeatportRelease(BeatportObject): if 'category' in data: self.category = data['category'] if 'slug' in data: - self.url = "http://beatport.com/release/{0}/{1}".format( + self.url = "https://beatport.com/release/{}/{}".format( data['slug'], data['id']) + self.genre = data.get('genre') -@six.python_2_unicode_compatible class BeatportTrack(BeatportObject): def __str__(self): artist_str = ", ".join(x[1] for x in self.artists) - return (u"" + return ("" .format(artist_str, self.name, self.mix_name)) def __repr__(self): - return six.text_type(self).encode('utf-8') + return str(self).encode('utf-8') def __init__(self, data): BeatportObject.__init__(self, data) if 'title' in data: - self.title = six.text_type(data['title']) + self.title = str(data['title']) if 'mixName' in data: - self.mix_name = six.text_type(data['mixName']) + self.mix_name = str(data['mixName']) self.length = timedelta(milliseconds=data.get('lengthMs', 0) or 0) if not self.length: try: @@ -252,14 +250,26 @@ class BeatportTrack(BeatportObject): except ValueError: pass if 'slug' in data: - self.url = "http://beatport.com/track/{0}/{1}".format(data['slug'], - data['id']) + self.url = "https://beatport.com/track/{}/{}" \ + .format(data['slug'], data['id']) self.track_number = data.get('trackNumber') + self.bpm = data.get('bpm') + self.initial_key = str( + (data.get('key') or {}).get('shortName') + ) + + # Use 'subgenre' and if not present, 'genre' as a fallback. + if data.get('subGenres'): + self.genre = str(data['subGenres'][0].get('name')) + elif data.get('genres'): + self.genre = str(data['genres'][0].get('name')) class BeatportPlugin(BeetsPlugin): + data_source = 'Beatport' + def __init__(self): - super(BeatportPlugin, self).__init__() + super().__init__() self.config.add({ 'apikey': '57713c3906af6f5def151b33601389176b37b429', 'apisecret': 'b3fe08c93c80aefd749fe871a16cd2bb32e2b954', @@ -279,7 +289,7 @@ class BeatportPlugin(BeetsPlugin): try: with open(self._tokenfile()) as f: tokendata = json.load(f) - except IOError: + except OSError: # No token yet. Generate one. token, secret = self.authenticate(c_key, c_secret) else: @@ -294,22 +304,22 @@ class BeatportPlugin(BeetsPlugin): try: url = auth_client.get_authorize_url() except AUTH_ERRORS as e: - self._log.debug(u'authentication error: {0}', e) - raise beets.ui.UserError(u'communication with Beatport failed') + self._log.debug('authentication error: {0}', e) + raise beets.ui.UserError('communication with Beatport failed') - beets.ui.print_(u"To authenticate with Beatport, visit:") + beets.ui.print_("To authenticate with Beatport, visit:") beets.ui.print_(url) # Ask for the verifier data and validate it. - data = beets.ui.input_(u"Enter the string displayed in your browser:") + data = beets.ui.input_("Enter the string displayed in your browser:") try: token, secret = auth_client.get_access_token(data) except AUTH_ERRORS as e: - self._log.debug(u'authentication error: {0}', e) - raise beets.ui.UserError(u'Beatport token request failed') + self._log.debug('authentication error: {0}', e) + raise beets.ui.UserError('Beatport token request failed') # Save the token for later use. - self._log.debug(u'Beatport token {0}, secret {1}', token, secret) + self._log.debug('Beatport token {0}, secret {1}', token, secret) with open(self._tokenfile(), 'w') as f: json.dump({'token': token, 'secret': secret}, f) @@ -318,74 +328,80 @@ class BeatportPlugin(BeetsPlugin): def _tokenfile(self): """Get the path to the JSON file for storing the OAuth token. """ - return self.config['tokenfile'].get(confit.Filename(in_app_dir=True)) + return self.config['tokenfile'].get(confuse.Filename(in_app_dir=True)) def album_distance(self, items, album_info, mapping): - """Returns the beatport source weight and the maximum source weight + """Returns the Beatport source weight and the maximum source weight for albums. """ - dist = Distance() - if album_info.data_source == 'Beatport': - dist.add('source', self.config['source_weight'].as_number()) - return dist + return get_distance( + data_source=self.data_source, + info=album_info, + config=self.config + ) def track_distance(self, item, track_info): - """Returns the beatport source weight and the maximum source weight + """Returns the Beatport source weight and the maximum source weight for individual tracks. """ - dist = Distance() - if track_info.data_source == 'Beatport': - dist.add('source', self.config['source_weight'].as_number()) - return dist + return get_distance( + data_source=self.data_source, + info=track_info, + config=self.config + ) - def candidates(self, items, artist, release, va_likely): + def candidates(self, items, artist, release, va_likely, extra_tags=None): """Returns a list of AlbumInfo objects for beatport search results matching release and artist (if not various). """ if va_likely: query = release else: - query = '%s %s' % (artist, release) + query = f'{artist} {release}' try: return self._get_releases(query) except BeatportAPIError as e: - self._log.debug(u'API Error: {0} (query: {1})', e, query) + self._log.debug('API Error: {0} (query: {1})', e, query) return [] def item_candidates(self, item, artist, title): """Returns a list of TrackInfo objects for beatport search results matching title and artist. """ - query = '%s %s' % (artist, title) + query = f'{artist} {title}' try: return self._get_tracks(query) except BeatportAPIError as e: - self._log.debug(u'API Error: {0} (query: {1})', e, query) + self._log.debug('API Error: {0} (query: {1})', e, query) return [] def album_for_id(self, release_id): """Fetches a release by its Beatport ID and returns an AlbumInfo object - or None if the release is not found. + or None if the query is not a valid ID or release is not found. """ - self._log.debug(u'Searching for release {0}', release_id) + self._log.debug('Searching for release {0}', release_id) match = re.search(r'(^|beatport\.com/release/.+/)(\d+)$', release_id) if not match: + self._log.debug('Not a valid Beatport release ID.') return None release = self.client.get_release(match.group(2)) - album = self._get_album_info(release) - return album + if release: + return self._get_album_info(release) + return None def track_for_id(self, track_id): """Fetches a track by its Beatport ID and returns a TrackInfo object - or None if the track is not found. + or None if the track is not a valid Beatport ID or track is not found. """ - self._log.debug(u'Searching for track {0}', track_id) + self._log.debug('Searching for track {0}', track_id) match = re.search(r'(^|beatport\.com/track/.+/)(\d+)$', track_id) if not match: + self._log.debug('Not a valid Beatport track ID.') return None bp_track = self.client.get_track(match.group(2)) - track = self._get_track_info(bp_track) - return track + if bp_track is not None: + return self._get_track_info(bp_track) + return None def _get_releases(self, query): """Returns a list of AlbumInfo objects for a beatport search query. @@ -408,7 +424,7 @@ class BeatportPlugin(BeetsPlugin): va = len(release.artists) > 3 artist, artist_id = self._get_artist(release.artists) if va: - artist = u"Various Artists" + artist = "Various Artists" tracks = [self._get_track_info(x) for x in release.tracks] return AlbumInfo(album=release.name, album_id=release.beatport_id, @@ -418,40 +434,33 @@ class BeatportPlugin(BeetsPlugin): month=release.release_date.month, day=release.release_date.day, label=release.label_name, - catalognum=release.catalog_number, media=u'Digital', - data_source=u'Beatport', data_url=release.url) + catalognum=release.catalog_number, media='Digital', + data_source=self.data_source, data_url=release.url, + genre=release.genre) def _get_track_info(self, track): """Returns a TrackInfo object for a Beatport Track object. """ title = track.name - if track.mix_name != u"Original Mix": - title += u" ({0})".format(track.mix_name) + if track.mix_name != "Original Mix": + title += f" ({track.mix_name})" artist, artist_id = self._get_artist(track.artists) length = track.length.total_seconds() return TrackInfo(title=title, track_id=track.beatport_id, artist=artist, artist_id=artist_id, length=length, index=track.track_number, medium_index=track.track_number, - data_source=u'Beatport', data_url=track.url) + data_source=self.data_source, data_url=track.url, + bpm=track.bpm, initial_key=track.initial_key, + genre=track.genre) def _get_artist(self, artists): """Returns an artist string (all artists) and an artist_id (the main artist) for a list of Beatport release or track artists. """ - artist_id = None - bits = [] - for artist in artists: - if not artist_id: - artist_id = artist[0] - name = artist[1] - # Strip disambiguation number. - name = re.sub(r' \(\d+\)$', '', name) - # Move articles to the front. - name = re.sub(r'^(.*?), (a|an|the)$', r'\2 \1', name, flags=re.I) - bits.append(name) - artist = ', '.join(bits).replace(' ,', ',') or None - return artist, artist_id + return MetadataSourcePlugin.get_artist( + artists=artists, id_key=0, name_key=1 + ) def _get_tracks(self, query): """Returns a list of TrackInfo objects for a Beatport query. diff --git a/libs/common/beetsplug/bench.py b/libs/common/beetsplug/bench.py index 41f575cd..6dffbdda 100644 --- a/libs/common/beetsplug/bench.py +++ b/libs/common/beetsplug/bench.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -16,7 +15,6 @@ """Some simple performance benchmarks for beets. """ -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets import ui diff --git a/libs/common/beetsplug/bpd/__init__.py b/libs/common/beetsplug/bpd/__init__.py index 1049f0c7..07198b1b 100644 --- a/libs/common/beetsplug/bpd/__init__.py +++ b/libs/common/beetsplug/bpd/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -18,37 +17,38 @@ Beets library. Attempts to implement a compatible protocol to allow use of the wide range of MPD clients. """ -from __future__ import division, absolute_import, print_function import re +import sys from string import Template import traceback import random import time +import math +import inspect +import socket import beets from beets.plugins import BeetsPlugin import beets.ui -from beets import logging from beets import vfs from beets.util import bluelet from beets.library import Item from beets import dbcore -from beets.mediafile import MediaFile -import six +from mediafile import MediaFile -PROTOCOL_VERSION = '0.13.0' +PROTOCOL_VERSION = '0.16.0' BUFSIZE = 1024 -HELLO = u'OK MPD %s' % PROTOCOL_VERSION -CLIST_BEGIN = u'command_list_begin' -CLIST_VERBOSE_BEGIN = u'command_list_ok_begin' -CLIST_END = u'command_list_end' -RESP_OK = u'OK' -RESP_CLIST_VERBOSE = u'list_OK' -RESP_ERR = u'ACK' +HELLO = 'OK MPD %s' % PROTOCOL_VERSION +CLIST_BEGIN = 'command_list_begin' +CLIST_VERBOSE_BEGIN = 'command_list_ok_begin' +CLIST_END = 'command_list_end' +RESP_OK = 'OK' +RESP_CLIST_VERBOSE = 'list_OK' +RESP_ERR = 'ACK' -NEWLINE = u"\n" +NEWLINE = "\n" ERROR_NOT_LIST = 1 ERROR_ARG = 2 @@ -68,14 +68,18 @@ VOLUME_MAX = 100 SAFE_COMMANDS = ( # Commands that are available when unauthenticated. - u'close', u'commands', u'notcommands', u'password', u'ping', + 'close', 'commands', 'notcommands', 'password', 'ping', ) -ITEM_KEYS_WRITABLE = set(MediaFile.fields()).intersection(Item._fields.keys()) +# List of subsystems/events used by the `idle` command. +SUBSYSTEMS = [ + 'update', 'player', 'mixer', 'options', 'playlist', 'database', + # Related to unsupported commands: + 'stored_playlist', 'output', 'subscription', 'sticker', 'message', + 'partition', +] -# Loggers. -log = logging.getLogger('beets.bpd') -global_log = logging.getLogger('beets') +ITEM_KEYS_WRITABLE = set(MediaFile.fields()).intersection(Item._fields.keys()) # Gstreamer import error. @@ -95,7 +99,7 @@ class BPDError(Exception): self.cmd_name = cmd_name self.index = index - template = Template(u'$resp [$code@$index] {$cmd_name} $message') + template = Template('$resp [$code@$index] {$cmd_name} $message') def response(self): """Returns a string to be used as the response code for the @@ -124,9 +128,9 @@ def make_bpd_error(s_code, s_message): pass return NewBPDError -ArgumentTypeError = make_bpd_error(ERROR_ARG, u'invalid type for argument') -ArgumentIndexError = make_bpd_error(ERROR_ARG, u'argument out of range') -ArgumentNotFoundError = make_bpd_error(ERROR_NO_EXIST, u'argument not found') +ArgumentTypeError = make_bpd_error(ERROR_ARG, 'invalid type for argument') +ArgumentIndexError = make_bpd_error(ERROR_ARG, 'argument out of range') +ArgumentNotFoundError = make_bpd_error(ERROR_NO_EXIST, 'argument not found') def cast_arg(t, val): @@ -150,10 +154,20 @@ class BPDClose(Exception): should be closed. """ + +class BPDIdle(Exception): + """Raised by a command to indicate the client wants to enter the idle state + and should be notified when a relevant event happens. + """ + def __init__(self, subsystems): + super().__init__() + self.subsystems = set(subsystems) + + # Generic server infrastructure, implementing the basic protocol. -class BaseServer(object): +class BaseServer: """A MPD-compatible music player server. The functions with the `cmd_` prefix are invoked in response to @@ -166,34 +180,87 @@ class BaseServer(object): This is a generic superclass and doesn't support many commands. """ - def __init__(self, host, port, password): + def __init__(self, host, port, password, ctrl_port, log, ctrl_host=None): """Create a new server bound to address `host` and listening on port `port`. If `password` is given, it is required to do anything significant on the server. + A separate control socket is established listening to `ctrl_host` on + port `ctrl_port` which is used to forward notifications from the player + and can be sent debug commands (e.g. using netcat). """ self.host, self.port, self.password = host, port, password + self.ctrl_host, self.ctrl_port = ctrl_host or host, ctrl_port + self.ctrl_sock = None + self._log = log # Default server values. self.random = False self.repeat = False + self.consume = False + self.single = False self.volume = VOLUME_MAX self.crossfade = 0 + self.mixrampdb = 0.0 + self.mixrampdelay = float('nan') + self.replay_gain_mode = 'off' self.playlist = [] self.playlist_version = 0 self.current_index = -1 self.paused = False self.error = None + # Current connections + self.connections = set() + # Object for random numbers generation self.random_obj = random.Random() + def connect(self, conn): + """A new client has connected. + """ + self.connections.add(conn) + + def disconnect(self, conn): + """Client has disconnected; clean up residual state. + """ + self.connections.remove(conn) + def run(self): """Block and start listening for connections from clients. An interrupt (^C) closes the server. """ self.startup_time = time.time() - bluelet.run(bluelet.server(self.host, self.port, - Connection.handler(self))) + + def start(): + yield bluelet.spawn( + bluelet.server(self.ctrl_host, self.ctrl_port, + ControlConnection.handler(self))) + yield bluelet.server(self.host, self.port, + MPDConnection.handler(self)) + bluelet.run(start()) + + def dispatch_events(self): + """If any clients have idle events ready, send them. + """ + # We need a copy of `self.connections` here since clients might + # disconnect once we try and send to them, changing `self.connections`. + for conn in list(self.connections): + yield bluelet.spawn(conn.send_notifications()) + + def _ctrl_send(self, message): + """Send some data over the control socket. + If it's our first time, open the socket. The message should be a + string without a terminal newline. + """ + if not self.ctrl_sock: + self.ctrl_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.ctrl_sock.connect((self.ctrl_host, self.ctrl_port)) + self.ctrl_sock.sendall((message + '\n').encode('utf-8')) + + def _send_event(self, event): + """Notify subscribed connections of an event.""" + for conn in self.connections: + conn.notify(event) def _item_info(self, item): """An abstract method that should response lines containing a @@ -231,10 +298,10 @@ class BaseServer(object): def _succ_idx(self): """Returns the index for the next song to play. - It also considers random and repeat flags. + It also considers random, single and repeat flags. No boundaries are checked. """ - if self.repeat: + if self.repeat and self.single: return self.current_index if self.random: return self._random_idx() @@ -245,7 +312,7 @@ class BaseServer(object): It also considers random and repeat flags. No boundaries are checked. """ - if self.repeat: + if self.repeat and self.single: return self.current_index if self.random: return self._random_idx() @@ -255,9 +322,17 @@ class BaseServer(object): """Succeeds.""" pass + def cmd_idle(self, conn, *subsystems): + subsystems = subsystems or SUBSYSTEMS + for system in subsystems: + if system not in SUBSYSTEMS: + raise BPDError(ERROR_ARG, + f'Unrecognised idle event: {system}') + raise BPDIdle(subsystems) # put the connection into idle mode + def cmd_kill(self, conn): """Exits the server process.""" - exit(0) + sys.exit(0) def cmd_close(self, conn): """Closes the connection.""" @@ -269,20 +344,20 @@ class BaseServer(object): conn.authenticated = True else: conn.authenticated = False - raise BPDError(ERROR_PASSWORD, u'incorrect password') + raise BPDError(ERROR_PASSWORD, 'incorrect password') def cmd_commands(self, conn): """Lists the commands available to the user.""" if self.password and not conn.authenticated: # Not authenticated. Show limited list of commands. for cmd in SAFE_COMMANDS: - yield u'command: ' + cmd + yield 'command: ' + cmd else: # Authenticated. Show all commands. for func in dir(self): if func.startswith('cmd_'): - yield u'command: ' + func[4:] + yield 'command: ' + func[4:] def cmd_notcommands(self, conn): """Lists all unavailable commands.""" @@ -292,7 +367,7 @@ class BaseServer(object): if func.startswith('cmd_'): cmd = func[4:] if cmd not in SAFE_COMMANDS: - yield u'command: ' + cmd + yield 'command: ' + cmd else: # Authenticated. No commands are unavailable. @@ -306,29 +381,43 @@ class BaseServer(object): playlist, playlistlength, and xfade. """ yield ( - u'volume: ' + six.text_type(self.volume), - u'repeat: ' + six.text_type(int(self.repeat)), - u'random: ' + six.text_type(int(self.random)), - u'playlist: ' + six.text_type(self.playlist_version), - u'playlistlength: ' + six.text_type(len(self.playlist)), - u'xfade: ' + six.text_type(self.crossfade), + 'repeat: ' + str(int(self.repeat)), + 'random: ' + str(int(self.random)), + 'consume: ' + str(int(self.consume)), + 'single: ' + str(int(self.single)), + 'playlist: ' + str(self.playlist_version), + 'playlistlength: ' + str(len(self.playlist)), + 'mixrampdb: ' + str(self.mixrampdb), ) + if self.volume > 0: + yield 'volume: ' + str(self.volume) + + if not math.isnan(self.mixrampdelay): + yield 'mixrampdelay: ' + str(self.mixrampdelay) + if self.crossfade > 0: + yield 'xfade: ' + str(self.crossfade) + if self.current_index == -1: - state = u'stop' + state = 'stop' elif self.paused: - state = u'pause' + state = 'pause' else: - state = u'play' - yield u'state: ' + state + state = 'play' + yield 'state: ' + state if self.current_index != -1: # i.e., paused or playing current_id = self._item_id(self.playlist[self.current_index]) - yield u'song: ' + six.text_type(self.current_index) - yield u'songid: ' + six.text_type(current_id) + yield 'song: ' + str(self.current_index) + yield 'songid: ' + str(current_id) + if len(self.playlist) > self.current_index + 1: + # If there's a next song, report its index too. + next_id = self._item_id(self.playlist[self.current_index + 1]) + yield 'nextsong: ' + str(self.current_index + 1) + yield 'nextsongid: ' + str(next_id) if self.error: - yield u'error: ' + self.error + yield 'error: ' + self.error def cmd_clearerror(self, conn): """Removes the persistent error state of the server. This @@ -340,29 +429,82 @@ class BaseServer(object): def cmd_random(self, conn, state): """Set or unset random (shuffle) mode.""" self.random = cast_arg('intbool', state) + self._send_event('options') def cmd_repeat(self, conn, state): """Set or unset repeat mode.""" self.repeat = cast_arg('intbool', state) + self._send_event('options') + + def cmd_consume(self, conn, state): + """Set or unset consume mode.""" + self.consume = cast_arg('intbool', state) + self._send_event('options') + + def cmd_single(self, conn, state): + """Set or unset single mode.""" + # TODO support oneshot in addition to 0 and 1 [MPD 0.20] + self.single = cast_arg('intbool', state) + self._send_event('options') def cmd_setvol(self, conn, vol): """Set the player's volume level (0-100).""" vol = cast_arg(int, vol) if vol < VOLUME_MIN or vol > VOLUME_MAX: - raise BPDError(ERROR_ARG, u'volume out of range') + raise BPDError(ERROR_ARG, 'volume out of range') self.volume = vol + self._send_event('mixer') + + def cmd_volume(self, conn, vol_delta): + """Deprecated command to change the volume by a relative amount.""" + vol_delta = cast_arg(int, vol_delta) + return self.cmd_setvol(conn, self.volume + vol_delta) def cmd_crossfade(self, conn, crossfade): """Set the number of seconds of crossfading.""" crossfade = cast_arg(int, crossfade) if crossfade < 0: - raise BPDError(ERROR_ARG, u'crossfade time must be nonnegative') + raise BPDError(ERROR_ARG, 'crossfade time must be nonnegative') + self._log.warning('crossfade is not implemented in bpd') + self.crossfade = crossfade + self._send_event('options') + + def cmd_mixrampdb(self, conn, db): + """Set the mixramp normalised max volume in dB.""" + db = cast_arg(float, db) + if db > 0: + raise BPDError(ERROR_ARG, 'mixrampdb time must be negative') + self._log.warning('mixramp is not implemented in bpd') + self.mixrampdb = db + self._send_event('options') + + def cmd_mixrampdelay(self, conn, delay): + """Set the mixramp delay in seconds.""" + delay = cast_arg(float, delay) + if delay < 0: + raise BPDError(ERROR_ARG, 'mixrampdelay time must be nonnegative') + self._log.warning('mixramp is not implemented in bpd') + self.mixrampdelay = delay + self._send_event('options') + + def cmd_replay_gain_mode(self, conn, mode): + """Set the replay gain mode.""" + if mode not in ['off', 'track', 'album', 'auto']: + raise BPDError(ERROR_ARG, 'Unrecognised replay gain mode') + self._log.warning('replay gain is not implemented in bpd') + self.replay_gain_mode = mode + self._send_event('options') + + def cmd_replay_gain_status(self, conn): + """Get the replaygain mode.""" + yield 'replay_gain_mode: ' + str(self.replay_gain_mode) def cmd_clear(self, conn): """Clear the playlist.""" self.playlist = [] self.playlist_version += 1 self.cmd_stop(conn) + self._send_event('playlist') def cmd_delete(self, conn, index): """Remove the song at index from the playlist.""" @@ -378,6 +520,7 @@ class BaseServer(object): elif index < self.current_index: # Deleted before playing. # Shift playing index down. self.current_index -= 1 + self._send_event('playlist') def cmd_deleteid(self, conn, track_id): self.cmd_delete(conn, self._id_to_index(track_id)) @@ -401,6 +544,7 @@ class BaseServer(object): self.current_index += 1 self.playlist_version += 1 + self._send_event('playlist') def cmd_moveid(self, conn, idx_from, idx_to): idx_from = self._id_to_index(idx_from) @@ -426,6 +570,7 @@ class BaseServer(object): self.current_index = i self.playlist_version += 1 + self._send_event('playlist') def cmd_swapid(self, conn, i_id, j_id): i = self._id_to_index(i_id) @@ -436,23 +581,27 @@ class BaseServer(object): """Indicates supported URL schemes. None by default.""" pass - def cmd_playlistinfo(self, conn, index=-1): + def cmd_playlistinfo(self, conn, index=None): """Gives metadata information about the entire playlist or a single track, given by its index. """ - index = cast_arg(int, index) - if index == -1: + if index is None: for track in self.playlist: yield self._item_info(track) else: + indices = self._parse_range(index, accept_single_number=True) try: - track = self.playlist[index] + tracks = [self.playlist[i] for i in indices] except IndexError: raise ArgumentIndexError() - yield self._item_info(track) + for track in tracks: + yield self._item_info(track) - def cmd_playlistid(self, conn, track_id=-1): - return self.cmd_playlistinfo(conn, self._id_to_index(track_id)) + def cmd_playlistid(self, conn, track_id=None): + if track_id is not None: + track_id = cast_arg(int, track_id) + track_id = self._id_to_index(track_id) + return self.cmd_playlistinfo(conn, track_id) def cmd_plchanges(self, conn, version): """Sends playlist changes since the given version. @@ -469,8 +618,8 @@ class BaseServer(object): Also a dummy implementation. """ for idx, track in enumerate(self.playlist): - yield u'cpos: ' + six.text_type(idx) - yield u'Id: ' + six.text_type(track.id) + yield 'cpos: ' + str(idx) + yield 'Id: ' + str(track.id) def cmd_currentsong(self, conn): """Sends information about the currently-playing song. @@ -481,20 +630,38 @@ class BaseServer(object): def cmd_next(self, conn): """Advance to the next song in the playlist.""" + old_index = self.current_index self.current_index = self._succ_idx() + if self.consume: + # TODO how does consume interact with single+repeat? + self.playlist.pop(old_index) + if self.current_index > old_index: + self.current_index -= 1 + self.playlist_version += 1 + self._send_event("playlist") if self.current_index >= len(self.playlist): - # Fallen off the end. Just move to stopped state. + # Fallen off the end. Move to stopped state or loop. + if self.repeat: + self.current_index = -1 + return self.cmd_play(conn) + return self.cmd_stop(conn) + elif self.single and not self.repeat: return self.cmd_stop(conn) else: return self.cmd_play(conn) def cmd_previous(self, conn): """Step back to the last song.""" + old_index = self.current_index self.current_index = self._prev_idx() + if self.consume: + self.playlist.pop(old_index) if self.current_index < 0: - return self.cmd_stop(conn) - else: - return self.cmd_play(conn) + if self.repeat: + self.current_index = len(self.playlist) - 1 + else: + self.current_index = 0 + return self.cmd_play(conn) def cmd_pause(self, conn, state=None): """Set the pause state playback.""" @@ -502,12 +669,13 @@ class BaseServer(object): self.paused = not self.paused # Toggle. else: self.paused = cast_arg('intbool', state) + self._send_event('player') def cmd_play(self, conn, index=-1): """Begin playback, possibly at a specified playlist index.""" index = cast_arg(int, index) - if index < -1 or index > len(self.playlist): + if index < -1 or index >= len(self.playlist): raise ArgumentIndexError() if index == -1: # No index specified: start where we are. @@ -521,6 +689,7 @@ class BaseServer(object): self.current_index = index self.paused = False + self._send_event('player') def cmd_playid(self, conn, track_id=0): track_id = cast_arg(int, track_id) @@ -534,6 +703,7 @@ class BaseServer(object): """Stop playback.""" self.current_index = -1 self.paused = False + self._send_event('player') def cmd_seek(self, conn, index, pos): """Seek to a specified point in a specified song.""" @@ -541,28 +711,40 @@ class BaseServer(object): if index < 0 or index >= len(self.playlist): raise ArgumentIndexError() self.current_index = index + self._send_event('player') def cmd_seekid(self, conn, track_id, pos): index = self._id_to_index(track_id) return self.cmd_seek(conn, index, pos) - def cmd_profile(self, conn): - """Memory profiling for debugging.""" - from guppy import hpy - heap = hpy().heap() - print(heap) + # Additions to the MPD protocol. + + def cmd_crash_TypeError(self, conn): # noqa: N802 + """Deliberately trigger a TypeError for testing purposes. + We want to test that the server properly responds with ERROR_SYSTEM + without crashing, and that this is not treated as ERROR_ARG (since it + is caused by a programming error, not a protocol error). + """ + 'a' + 2 -class Connection(object): - """A connection between a client and the server. Handles input and - output from and to the client. +class Connection: + """A connection between a client and the server. """ def __init__(self, server, sock): """Create a new connection for the accepted socket `client`. """ self.server = server self.sock = sock - self.authenticated = False + self.address = '{}:{}'.format(*sock.sock.getpeername()) + + def debug(self, message, kind=' '): + """Log a debug message about this connection. + """ + self.server._log.debug('{}[{}]: {}', kind, self.address, message) + + def run(self): + pass def send(self, lines): """Send lines, which which is either a single string or an @@ -570,14 +752,35 @@ class Connection(object): added after every string. Returns a Bluelet event that sends the data. """ - if isinstance(lines, six.string_types): + if isinstance(lines, str): lines = [lines] out = NEWLINE.join(lines) + NEWLINE - log.debug('{}', out[:-1]) # Don't log trailing newline. - if isinstance(out, six.text_type): + for l in out.split(NEWLINE)[:-1]: + self.debug(l, kind='>') + if isinstance(out, str): out = out.encode('utf-8') return self.sock.sendall(out) + @classmethod + def handler(cls, server): + def _handle(sock): + """Creates a new `Connection` and runs it. + """ + return cls(server, sock).run() + return _handle + + +class MPDConnection(Connection): + """A connection that receives commands from an MPD-compatible client. + """ + def __init__(self, server, sock): + """Create a new connection for the accepted socket `client`. + """ + super().__init__(server, sock) + self.authenticated = False + self.notifications = set() + self.idle_subscriptions = set() + def do_command(self, command): """A coroutine that runs the given command and sends an appropriate response.""" @@ -590,28 +793,75 @@ class Connection(object): # Send success code. yield self.send(RESP_OK) + def disconnect(self): + """The connection has closed for any reason. + """ + self.server.disconnect(self) + self.debug('disconnected', kind='*') + + def notify(self, event): + """Queue up an event for sending to this client. + """ + self.notifications.add(event) + + def send_notifications(self, force_close_idle=False): + """Send the client any queued events now. + """ + pending = self.notifications.intersection(self.idle_subscriptions) + try: + for event in pending: + yield self.send(f'changed: {event}') + if pending or force_close_idle: + self.idle_subscriptions = set() + self.notifications = self.notifications.difference(pending) + yield self.send(RESP_OK) + except bluelet.SocketClosedError: + self.disconnect() # Client disappeared. + def run(self): """Send a greeting to the client and begin processing commands as they arrive. """ + self.debug('connected', kind='*') + self.server.connect(self) yield self.send(HELLO) clist = None # Initially, no command list is being constructed. while True: line = yield self.sock.readline() if not line: + self.disconnect() # Client disappeared. break line = line.strip() if not line: + err = BPDError(ERROR_UNKNOWN, 'No command given') + yield self.send(err.response()) + self.disconnect() # Client sent a blank line. break line = line.decode('utf8') # MPD protocol uses UTF-8. - log.debug(u'{}', line) + for l in line.split(NEWLINE): + self.debug(l, kind='<') + + if self.idle_subscriptions: + # The connection is in idle mode. + if line == 'noidle': + yield bluelet.call(self.send_notifications(True)) + else: + err = BPDError(ERROR_UNKNOWN, + f'Got command while idle: {line}') + yield self.send(err.response()) + break + continue + if line == 'noidle': + # When not in idle, this command sends no response. + continue if clist is not None: # Command list already opened. if line == CLIST_END: yield bluelet.call(self.do_command(clist)) clist = None # Clear the command list. + yield bluelet.call(self.server.dispatch_events()) else: clist.append(Command(line)) @@ -626,18 +876,74 @@ class Connection(object): except BPDClose: # Command indicates that the conn should close. self.sock.close() + self.disconnect() # Client explicitly closed. return - - @classmethod - def handler(cls, server): - def _handle(sock): - """Creates a new `Connection` and runs it. - """ - return cls(server, sock).run() - return _handle + except BPDIdle as e: + self.idle_subscriptions = e.subsystems + self.debug('awaiting: {}'.format(' '.join(e.subsystems)), + kind='z') + yield bluelet.call(self.server.dispatch_events()) -class Command(object): +class ControlConnection(Connection): + """A connection used to control BPD for debugging and internal events. + """ + def __init__(self, server, sock): + """Create a new connection for the accepted socket `client`. + """ + super().__init__(server, sock) + + def debug(self, message, kind=' '): + self.server._log.debug('CTRL {}[{}]: {}', kind, self.address, message) + + def run(self): + """Listen for control commands and delegate to `ctrl_*` methods. + """ + self.debug('connected', kind='*') + while True: + line = yield self.sock.readline() + if not line: + break # Client disappeared. + line = line.strip() + if not line: + break # Client sent a blank line. + line = line.decode('utf8') # Protocol uses UTF-8. + for l in line.split(NEWLINE): + self.debug(l, kind='<') + command = Command(line) + try: + func = command.delegate('ctrl_', self) + yield bluelet.call(func(*command.args)) + except (AttributeError, TypeError) as e: + yield self.send('ERROR: {}'.format(e.args[0])) + except Exception: + yield self.send(['ERROR: server error', + traceback.format_exc().rstrip()]) + + def ctrl_play_finished(self): + """Callback from the player signalling a song finished playing. + """ + yield bluelet.call(self.server.dispatch_events()) + + def ctrl_profile(self): + """Memory profiling for debugging. + """ + from guppy import hpy + heap = hpy().heap() + yield self.send(heap) + + def ctrl_nickname(self, oldlabel, newlabel): + """Rename a client in the log messages. + """ + for c in self.server.connections: + if c.address == oldlabel: + c.address = newlabel + break + else: + yield self.send(f'ERROR: no such client: {oldlabel}') + + +class Command: """A command issued by the client for processing by the server. """ @@ -657,27 +963,59 @@ class Command(object): if match[0]: # Quoted argument. arg = match[0] - arg = arg.replace(u'\\"', u'"').replace(u'\\\\', u'\\') + arg = arg.replace('\\"', '"').replace('\\\\', '\\') else: # Unquoted argument. arg = match[1] self.args.append(arg) + def delegate(self, prefix, target, extra_args=0): + """Get the target method that corresponds to this command. + The `prefix` is prepended to the command name and then the resulting + name is used to search `target` for a method with a compatible number + of arguments. + """ + # Attempt to get correct command function. + func_name = prefix + self.name + if not hasattr(target, func_name): + raise AttributeError(f'unknown command "{self.name}"') + func = getattr(target, func_name) + + argspec = inspect.getfullargspec(func) + + # Check that `func` is able to handle the number of arguments sent + # by the client (so we can raise ERROR_ARG instead of ERROR_SYSTEM). + # Maximum accepted arguments: argspec includes "self". + max_args = len(argspec.args) - 1 - extra_args + # Minimum accepted arguments: some arguments might be optional. + min_args = max_args + if argspec.defaults: + min_args -= len(argspec.defaults) + wrong_num = (len(self.args) > max_args) or (len(self.args) < min_args) + # If the command accepts a variable number of arguments skip the check. + if wrong_num and not argspec.varargs: + raise TypeError('wrong number of arguments for "{}"' + .format(self.name), self.name) + + return func + def run(self, conn): """A coroutine that executes the command on the given connection. """ - # Attempt to get correct command function. - func_name = 'cmd_' + self.name - if not hasattr(conn.server, func_name): - raise BPDError(ERROR_UNKNOWN, u'unknown command', self.name) - func = getattr(conn.server, func_name) + try: + # `conn` is an extra argument to all cmd handlers. + func = self.delegate('cmd_', conn.server, extra_args=1) + except AttributeError as e: + raise BPDError(ERROR_UNKNOWN, e.args[0]) + except TypeError as e: + raise BPDError(ERROR_ARG, e.args[0], self.name) # Ensure we have permission for this command. if conn.server.password and \ not conn.authenticated and \ self.name not in SAFE_COMMANDS: - raise BPDError(ERROR_PERMISSION, u'insufficient privileges') + raise BPDError(ERROR_PERMISSION, 'insufficient privileges') try: args = [conn] + self.args @@ -697,10 +1035,13 @@ class Command(object): # it on the Connection. raise - except Exception as e: + except BPDIdle: + raise + + except Exception: # An "unintentional" error. Hide it from the client. - log.error('{}', traceback.format_exc(e)) - raise BPDError(ERROR_SYSTEM, u'server error', self.name) + conn.server._log.error('{}', traceback.format_exc()) + raise BPDError(ERROR_SYSTEM, 'server error', self.name) class CommandList(list): @@ -729,7 +1070,7 @@ class CommandList(list): e.index = i # Give the error the correct index. raise e - # Otherwise, possibly send the output delimeter if we're in a + # Otherwise, possibly send the output delimiter if we're in a # verbose ("OK") command list. if self.verbose: yield conn.send(RESP_CLIST_VERBOSE) @@ -743,7 +1084,7 @@ class Server(BaseServer): to store its library. """ - def __init__(self, library, host, port, password): + def __init__(self, library, host, port, password, ctrl_port, log): try: from beetsplug.bpd import gstplayer except ImportError as e: @@ -752,65 +1093,80 @@ class Server(BaseServer): raise NoGstreamerError() else: raise - super(Server, self).__init__(host, port, password) + log.info('Starting server...') + super().__init__(host, port, password, ctrl_port, log) self.lib = library self.player = gstplayer.GstPlayer(self.play_finished) self.cmd_update(None) + log.info('Server ready and listening on {}:{}'.format( + host, port)) + log.debug('Listening for control signals on {}:{}'.format( + host, ctrl_port)) def run(self): self.player.run() - super(Server, self).run() + super().run() def play_finished(self): - """A callback invoked every time our player finishes a - track. + """A callback invoked every time our player finishes a track. """ self.cmd_next(None) + self._ctrl_send('play_finished') # Metadata helper functions. def _item_info(self, item): info_lines = [ - u'file: ' + item.destination(fragment=True), - u'Time: ' + six.text_type(int(item.length)), - u'Title: ' + item.title, - u'Artist: ' + item.artist, - u'Album: ' + item.album, - u'Genre: ' + item.genre, + 'file: ' + item.destination(fragment=True), + 'Time: ' + str(int(item.length)), + 'duration: ' + f'{item.length:.3f}', + 'Id: ' + str(item.id), ] - track = six.text_type(item.track) - if item.tracktotal: - track += u'/' + six.text_type(item.tracktotal) - info_lines.append(u'Track: ' + track) - - info_lines.append(u'Date: ' + six.text_type(item.year)) - try: pos = self._id_to_index(item.id) - info_lines.append(u'Pos: ' + six.text_type(pos)) + info_lines.append('Pos: ' + str(pos)) except ArgumentNotFoundError: # Don't include position if not in playlist. pass - info_lines.append(u'Id: ' + six.text_type(item.id)) + for tagtype, field in self.tagtype_map.items(): + info_lines.append('{}: {}'.format( + tagtype, str(getattr(item, field)))) return info_lines + def _parse_range(self, items, accept_single_number=False): + """Convert a range of positions to a list of item info. + MPD specifies ranges as START:STOP (endpoint excluded) for some + commands. Sometimes a single number can be provided instead. + """ + try: + start, stop = str(items).split(':', 1) + except ValueError: + if accept_single_number: + return [cast_arg(int, items)] + raise BPDError(ERROR_ARG, 'bad range syntax') + start = cast_arg(int, start) + stop = cast_arg(int, stop) + return range(start, stop) + def _item_id(self, item): return item.id # Database updating. - def cmd_update(self, conn, path=u'/'): + def cmd_update(self, conn, path='/'): """Updates the catalog to reflect the current database state. """ # Path is ignored. Also, the real MPD does this asynchronously; # this is done inline. - print(u'Building directory tree...') + self._log.debug('Building directory tree...') self.tree = vfs.libtree(self.lib) - print(u'... done.') + self._log.debug('Finished building directory tree.') self.updated_time = time.time() + self._send_event('update') + self._send_event('database') # Path (directory tree) browsing. @@ -818,7 +1174,7 @@ class Server(BaseServer): """Returns a VFS node or an item ID located at the path given. If the path does not exist, raises a """ - components = path.split(u'/') + components = path.split('/') node = self.tree for component in components: @@ -840,25 +1196,25 @@ class Server(BaseServer): def _path_join(self, p1, p2): """Smashes together two BPD paths.""" - out = p1 + u'/' + p2 - return out.replace(u'//', u'/').replace(u'//', u'/') + out = p1 + '/' + p2 + return out.replace('//', '/').replace('//', '/') - def cmd_lsinfo(self, conn, path=u"/"): + def cmd_lsinfo(self, conn, path="/"): """Sends info on all the items in the path.""" node = self._resolve_path(path) if isinstance(node, int): # Trying to list a track. - raise BPDError(ERROR_ARG, u'this is not a directory') + raise BPDError(ERROR_ARG, 'this is not a directory') else: for name, itemid in iter(sorted(node.files.items())): item = self.lib.get_item(itemid) yield self._item_info(item) for name, _ in iter(sorted(node.dirs.items())): dirpath = self._path_join(path, name) - if dirpath.startswith(u"/"): + if dirpath.startswith("/"): # Strip leading slash (libmpc rejects this). dirpath = dirpath[1:] - yield u'directory: %s' % dirpath + yield 'directory: %s' % dirpath def _listall(self, basepath, node, info=False): """Helper function for recursive listing. If info, show @@ -870,25 +1226,23 @@ class Server(BaseServer): item = self.lib.get_item(node) yield self._item_info(item) else: - yield u'file: ' + basepath + yield 'file: ' + basepath else: # List a directory. Recurse into both directories and files. for name, itemid in sorted(node.files.items()): newpath = self._path_join(basepath, name) # "yield from" - for v in self._listall(newpath, itemid, info): - yield v + yield from self._listall(newpath, itemid, info) for name, subdir in sorted(node.dirs.items()): newpath = self._path_join(basepath, name) - yield u'directory: ' + newpath - for v in self._listall(newpath, subdir, info): - yield v + yield 'directory: ' + newpath + yield from self._listall(newpath, subdir, info) - def cmd_listall(self, conn, path=u"/"): + def cmd_listall(self, conn, path="/"): """Send the paths all items in the directory, recursively.""" return self._listall(path, self._resolve_path(path), False) - def cmd_listallinfo(self, conn, path=u"/"): + def cmd_listallinfo(self, conn, path="/"): """Send info on all the items in the directory, recursively.""" return self._listall(path, self._resolve_path(path), True) @@ -905,11 +1259,9 @@ class Server(BaseServer): # Recurse into a directory. for name, itemid in sorted(node.files.items()): # "yield from" - for v in self._all_items(itemid): - yield v + yield from self._all_items(itemid) for name, subdir in sorted(node.dirs.items()): - for v in self._all_items(subdir): - yield v + yield from self._all_items(subdir) def _add(self, path, send_id=False): """Adds a track or directory to the playlist, specified by the @@ -918,8 +1270,9 @@ class Server(BaseServer): for item in self._all_items(self._resolve_path(path)): self.playlist.append(item) if send_id: - yield u'Id: ' + six.text_type(item.id) + yield 'Id: ' + str(item.id) self.playlist_version += 1 + self._send_event('playlist') def cmd_add(self, conn, path): """Adds a track or directory to the playlist, specified by a @@ -934,16 +1287,28 @@ class Server(BaseServer): # Server info. def cmd_status(self, conn): - for line in super(Server, self).cmd_status(conn): - yield line + yield from super().cmd_status(conn) if self.current_index > -1: item = self.playlist[self.current_index] - yield u'bitrate: ' + six.text_type(item.bitrate / 1000) - # Missing 'audio'. + yield ( + 'bitrate: ' + str(item.bitrate / 1000), + 'audio: {}:{}:{}'.format( + str(item.samplerate), + str(item.bitdepth), + str(item.channels), + ), + ) (pos, total) = self.player.time() - yield u'time: ' + six.text_type(pos) + u':' + six.text_type(total) + yield ( + 'time: {}:{}'.format( + str(int(pos)), + str(int(total)), + ), + 'elapsed: ' + f'{pos:.3f}', + 'duration: ' + f'{total:.3f}', + ) # Also missing 'updating_db'. @@ -958,31 +1323,47 @@ class Server(BaseServer): artists, albums, songs, totaltime = tx.query(statement)[0] yield ( - u'artists: ' + six.text_type(artists), - u'albums: ' + six.text_type(albums), - u'songs: ' + six.text_type(songs), - u'uptime: ' + six.text_type(int(time.time() - self.startup_time)), - u'playtime: ' + u'0', # Missing. - u'db_playtime: ' + six.text_type(int(totaltime)), - u'db_update: ' + six.text_type(int(self.updated_time)), + 'artists: ' + str(artists), + 'albums: ' + str(albums), + 'songs: ' + str(songs), + 'uptime: ' + str(int(time.time() - self.startup_time)), + 'playtime: ' + '0', # Missing. + 'db_playtime: ' + str(int(totaltime)), + 'db_update: ' + str(int(self.updated_time)), ) + def cmd_decoders(self, conn): + """Send list of supported decoders and formats.""" + decoders = self.player.get_decoders() + for name, (mimes, exts) in decoders.items(): + yield f'plugin: {name}' + for ext in exts: + yield f'suffix: {ext}' + for mime in mimes: + yield f'mime_type: {mime}' + # Searching. tagtype_map = { - u'Artist': u'artist', - u'Album': u'album', - u'Title': u'title', - u'Track': u'track', - u'AlbumArtist': u'albumartist', - u'AlbumArtistSort': u'albumartist_sort', - # Name? - u'Genre': u'genre', - u'Date': u'year', - u'Composer': u'composer', - # Performer? - u'Disc': u'disc', - u'filename': u'path', # Suspect. + 'Artist': 'artist', + 'ArtistSort': 'artist_sort', + 'Album': 'album', + 'Title': 'title', + 'Track': 'track', + 'AlbumArtist': 'albumartist', + 'AlbumArtistSort': 'albumartist_sort', + 'Label': 'label', + 'Genre': 'genre', + 'Date': 'year', + 'OriginalDate': 'original_year', + 'Composer': 'composer', + 'Disc': 'disc', + 'Comment': 'comments', + 'MUSICBRAINZ_TRACKID': 'mb_trackid', + 'MUSICBRAINZ_ALBUMID': 'mb_albumid', + 'MUSICBRAINZ_ARTISTID': 'mb_artistid', + 'MUSICBRAINZ_ALBUMARTISTID': 'mb_albumartistid', + 'MUSICBRAINZ_RELEASETRACKID': 'mb_releasetrackid', } def cmd_tagtypes(self, conn): @@ -990,7 +1371,7 @@ class Server(BaseServer): searching. """ for tag in self.tagtype_map: - yield u'tagtype: ' + tag + yield 'tagtype: ' + tag def _tagtype_lookup(self, tag): """Uses `tagtype_map` to look up the beets column name for an @@ -1002,7 +1383,7 @@ class Server(BaseServer): # Match case-insensitively. if test_tag.lower() == tag.lower(): return test_tag, key - raise BPDError(ERROR_UNKNOWN, u'no such tagtype') + raise BPDError(ERROR_UNKNOWN, 'no such tagtype') def _metadata_query(self, query_type, any_query_type, kv): """Helper function returns a query object that will find items @@ -1015,13 +1396,13 @@ class Server(BaseServer): # Iterate pairwise over the arguments. it = iter(kv) for tag, value in zip(it, it): - if tag.lower() == u'any': + if tag.lower() == 'any': if any_query_type: queries.append(any_query_type(value, ITEM_KEYS_WRITABLE, query_type)) else: - raise BPDError(ERROR_UNKNOWN, u'no such tagtype') + raise BPDError(ERROR_UNKNOWN, 'no such tagtype') else: _, key = self._tagtype_lookup(tag) queries.append(query_type(key, value)) @@ -1050,17 +1431,32 @@ class Server(BaseServer): filtered by matching match_tag to match_term. """ show_tag_canon, show_key = self._tagtype_lookup(show_tag) + if len(kv) == 1: + if show_tag_canon == 'Album': + # If no tag was given, assume artist. This is because MPD + # supports a short version of this command for fetching the + # albums belonging to a particular artist, and some clients + # rely on this behaviour (e.g. MPDroid, M.A.L.P.). + kv = ('Artist', kv[0]) + else: + raise BPDError(ERROR_ARG, 'should be "Album" for 3 arguments') + elif len(kv) % 2 != 0: + raise BPDError(ERROR_ARG, 'Incorrect number of filter arguments') query = self._metadata_query(dbcore.query.MatchQuery, None, kv) clause, subvals = query.clause() statement = 'SELECT DISTINCT ' + show_key + \ ' FROM items WHERE ' + clause + \ ' ORDER BY ' + show_key + self._log.debug(statement) with self.lib.transaction() as tx: rows = tx.query(statement, subvals) for row in rows: - yield show_tag_canon + u': ' + six.text_type(row[0]) + if not row[0]: + # Skip any empty values of the field. + continue + yield show_tag_canon + ': ' + str(row[0]) def cmd_count(self, conn, tag, value): """Returns the number and total time of songs matching the @@ -1072,8 +1468,44 @@ class Server(BaseServer): for item in self.lib.items(dbcore.query.MatchQuery(key, value)): songs += 1 playtime += item.length - yield u'songs: ' + six.text_type(songs) - yield u'playtime: ' + six.text_type(int(playtime)) + yield 'songs: ' + str(songs) + yield 'playtime: ' + str(int(playtime)) + + # Persistent playlist manipulation. In MPD this is an optional feature so + # these dummy implementations match MPD's behaviour with the feature off. + + def cmd_listplaylist(self, conn, playlist): + raise BPDError(ERROR_NO_EXIST, 'No such playlist') + + def cmd_listplaylistinfo(self, conn, playlist): + raise BPDError(ERROR_NO_EXIST, 'No such playlist') + + def cmd_listplaylists(self, conn): + raise BPDError(ERROR_UNKNOWN, 'Stored playlists are disabled') + + def cmd_load(self, conn, playlist): + raise BPDError(ERROR_NO_EXIST, 'Stored playlists are disabled') + + def cmd_playlistadd(self, conn, playlist, uri): + raise BPDError(ERROR_UNKNOWN, 'Stored playlists are disabled') + + def cmd_playlistclear(self, conn, playlist): + raise BPDError(ERROR_UNKNOWN, 'Stored playlists are disabled') + + def cmd_playlistdelete(self, conn, playlist, index): + raise BPDError(ERROR_UNKNOWN, 'Stored playlists are disabled') + + def cmd_playlistmove(self, conn, playlist, from_index, to_index): + raise BPDError(ERROR_UNKNOWN, 'Stored playlists are disabled') + + def cmd_rename(self, conn, playlist, new_name): + raise BPDError(ERROR_UNKNOWN, 'Stored playlists are disabled') + + def cmd_rm(self, conn, playlist): + raise BPDError(ERROR_UNKNOWN, 'Stored playlists are disabled') + + def cmd_save(self, conn, playlist): + raise BPDError(ERROR_UNKNOWN, 'Stored playlists are disabled') # "Outputs." Just a dummy implementation because we don't control # any outputs. @@ -1081,9 +1513,9 @@ class Server(BaseServer): def cmd_outputs(self, conn): """List the available outputs.""" yield ( - u'outputid: 0', - u'outputname: gstreamer', - u'outputenabled: 1', + 'outputid: 0', + 'outputname: gstreamer', + 'outputenabled: 1', ) def cmd_enableoutput(self, conn, output_id): @@ -1094,7 +1526,7 @@ class Server(BaseServer): def cmd_disableoutput(self, conn, output_id): output_id = cast_arg(int, output_id) if output_id == 0: - raise BPDError(ERROR_ARG, u'cannot disable this output') + raise BPDError(ERROR_ARG, 'cannot disable this output') else: raise ArgumentIndexError() @@ -1105,7 +1537,7 @@ class Server(BaseServer): def cmd_play(self, conn, index=-1): new_index = index != -1 and index != self.current_index was_paused = self.paused - super(Server, self).cmd_play(conn, index) + super().cmd_play(conn, index) if self.current_index > -1: # Not stopped. if was_paused and not new_index: @@ -1115,28 +1547,28 @@ class Server(BaseServer): self.player.play_file(self.playlist[self.current_index].path) def cmd_pause(self, conn, state=None): - super(Server, self).cmd_pause(conn, state) + super().cmd_pause(conn, state) if self.paused: self.player.pause() elif self.player.playing: self.player.play() def cmd_stop(self, conn): - super(Server, self).cmd_stop(conn) + super().cmd_stop(conn) self.player.stop() def cmd_seek(self, conn, index, pos): """Seeks to the specified position in the specified song.""" index = cast_arg(int, index) - pos = cast_arg(int, pos) - super(Server, self).cmd_seek(conn, index, pos) + pos = cast_arg(float, pos) + super().cmd_seek(conn, index, pos) self.player.seek(pos) # Volume control. def cmd_setvol(self, conn, vol): vol = cast_arg(int, vol) - super(Server, self).cmd_setvol(conn, vol) + super().cmd_setvol(conn, vol) self.player.volume = float(vol) / 100 @@ -1147,37 +1579,30 @@ class BPDPlugin(BeetsPlugin): server. """ def __init__(self): - super(BPDPlugin, self).__init__() + super().__init__() self.config.add({ - 'host': u'', + 'host': '', 'port': 6600, - 'password': u'', + 'control_port': 6601, + 'password': '', 'volume': VOLUME_MAX, }) self.config['password'].redact = True - def start_bpd(self, lib, host, port, password, volume, debug): + def start_bpd(self, lib, host, port, password, volume, ctrl_port): """Starts a BPD server.""" - if debug: # FIXME this should be managed by BeetsPlugin - self._log.setLevel(logging.DEBUG) - else: - self._log.setLevel(logging.WARNING) try: - server = Server(lib, host, port, password) + server = Server(lib, host, port, password, ctrl_port, self._log) server.cmd_setvol(None, volume) server.run() except NoGstreamerError: - global_log.error(u'Gstreamer Python bindings not found.') - global_log.error(u'Install "gstreamer1.0" and "python-gi"' - u'or similar package to use BPD.') + self._log.error('Gstreamer Python bindings not found.') + self._log.error('Install "gstreamer1.0" and "python-gi"' + 'or similar package to use BPD.') def commands(self): cmd = beets.ui.Subcommand( - 'bpd', help=u'run an MPD-compatible music player server' - ) - cmd.parser.add_option( - '-d', '--debug', action='store_true', - help=u'dump all MPD traffic to stdout' + 'bpd', help='run an MPD-compatible music player server' ) def func(lib, opts, args): @@ -1185,11 +1610,15 @@ class BPDPlugin(BeetsPlugin): host = args.pop(0) if args else host port = args.pop(0) if args else self.config['port'].get(int) if args: - raise beets.ui.UserError(u'too many arguments') + ctrl_port = args.pop(0) + else: + ctrl_port = self.config['control_port'].get(int) + if args: + raise beets.ui.UserError('too many arguments') password = self.config['password'].as_str() volume = self.config['volume'].get(int) - debug = opts.debug or False - self.start_bpd(lib, host, int(port), password, volume, debug) + self.start_bpd(lib, host, int(port), password, volume, + int(ctrl_port)) cmd.func = func return [cmd] diff --git a/libs/common/beetsplug/bpd/gstplayer.py b/libs/common/beetsplug/bpd/gstplayer.py index 705692aa..64954b1c 100644 --- a/libs/common/beetsplug/bpd/gstplayer.py +++ b/libs/common/beetsplug/bpd/gstplayer.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -17,15 +16,13 @@ music player. """ -from __future__ import division, absolute_import, print_function -import six import sys import time -from six.moves import _thread +import _thread import os import copy -from six.moves import urllib +import urllib from beets import ui import gi @@ -40,7 +37,7 @@ class QueryError(Exception): pass -class GstPlayer(object): +class GstPlayer: """A music player abstracting GStreamer's Playbin element. Create a player object, then call run() to start a thread with a @@ -64,7 +61,8 @@ class GstPlayer(object): """ # Set up the Gstreamer player. From the pygst tutorial: - # http://pygstdocs.berlios.de/pygst-tutorial/playbin.html + # https://pygstdocs.berlios.de/pygst-tutorial/playbin.html (gone) + # https://brettviren.github.io/pygst-tutorial-org/pygst-tutorial.html #### # Updated to GStreamer 1.0 with: # https://wiki.ubuntu.com/Novacut/GStreamer1.0 @@ -109,7 +107,7 @@ class GstPlayer(object): # error self.player.set_state(Gst.State.NULL) err, debug = message.parse_error() - print(u"Error: {0}".format(err)) + print(f"Error: {err}") self.playing = False def _set_volume(self, volume): @@ -129,7 +127,7 @@ class GstPlayer(object): path. """ self.player.set_state(Gst.State.NULL) - if isinstance(path, six.text_type): + if isinstance(path, str): path = path.encode('utf-8') uri = 'file://' + urllib.parse.quote(path) self.player.set_property("uri", uri) @@ -177,12 +175,12 @@ class GstPlayer(object): posq = self.player.query_position(fmt) if not posq[0]: raise QueryError("query_position failed") - pos = posq[1] // (10 ** 9) + pos = posq[1] / (10 ** 9) lengthq = self.player.query_duration(fmt) if not lengthq[0]: raise QueryError("query_duration failed") - length = lengthq[1] // (10 ** 9) + length = lengthq[1] / (10 ** 9) self.cached_time = (pos, length) return (pos, length) @@ -215,6 +213,59 @@ class GstPlayer(object): while self.playing: time.sleep(1) + def get_decoders(self): + return get_decoders() + + +def get_decoders(): + """Get supported audio decoders from GStreamer. + Returns a dict mapping decoder element names to the associated media types + and file extensions. + """ + # We only care about audio decoder elements. + filt = (Gst.ELEMENT_FACTORY_TYPE_DEPAYLOADER | + Gst.ELEMENT_FACTORY_TYPE_DEMUXER | + Gst.ELEMENT_FACTORY_TYPE_PARSER | + Gst.ELEMENT_FACTORY_TYPE_DECODER | + Gst.ELEMENT_FACTORY_TYPE_MEDIA_AUDIO) + + decoders = {} + mime_types = set() + for f in Gst.ElementFactory.list_get_elements(filt, Gst.Rank.NONE): + for pad in f.get_static_pad_templates(): + if pad.direction == Gst.PadDirection.SINK: + caps = pad.static_caps.get() + mimes = set() + for i in range(caps.get_size()): + struct = caps.get_structure(i) + mime = struct.get_name() + if mime == 'unknown/unknown': + continue + mimes.add(mime) + mime_types.add(mime) + if mimes: + decoders[f.get_name()] = (mimes, set()) + + # Check all the TypeFindFactory plugin features form the registry. If they + # are associated with an audio media type that we found above, get the list + # of corresponding file extensions. + mime_extensions = {mime: set() for mime in mime_types} + for feat in Gst.Registry.get().get_feature_list(Gst.TypeFindFactory): + caps = feat.get_caps() + if caps: + for i in range(caps.get_size()): + struct = caps.get_structure(i) + mime = struct.get_name() + if mime in mime_types: + mime_extensions[mime].update(feat.get_extensions()) + + # Fill in the slot we left for file extensions. + for name, (mimes, exts) in decoders.items(): + for mime in mimes: + exts.update(mime_extensions[mime]) + + return decoders + def play_simple(paths): """Play the files in paths in a straightforward way, without diff --git a/libs/common/beetsplug/bpm.py b/libs/common/beetsplug/bpm.py index 20218bd3..5aa2d95a 100644 --- a/libs/common/beetsplug/bpm.py +++ b/libs/common/beetsplug/bpm.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, aroquen # @@ -15,10 +14,8 @@ """Determine BPM by pressing a key to the rhythm.""" -from __future__ import division, absolute_import, print_function import time -from six.moves import input from beets import ui from beets.plugins import BeetsPlugin @@ -51,16 +48,16 @@ def bpm(max_strokes): class BPMPlugin(BeetsPlugin): def __init__(self): - super(BPMPlugin, self).__init__() + super().__init__() self.config.add({ - u'max_strokes': 3, - u'overwrite': True, + 'max_strokes': 3, + 'overwrite': True, }) def commands(self): cmd = ui.Subcommand('bpm', - help=u'determine bpm of a song by pressing ' - u'a key to the rhythm') + help='determine bpm of a song by pressing ' + 'a key to the rhythm') cmd.func = self.command return [cmd] @@ -72,19 +69,19 @@ class BPMPlugin(BeetsPlugin): def get_bpm(self, items, write=False): overwrite = self.config['overwrite'].get(bool) if len(items) > 1: - raise ValueError(u'Can only get bpm of one song at time') + raise ValueError('Can only get bpm of one song at time') item = items[0] if item['bpm']: - self._log.info(u'Found bpm {0}', item['bpm']) + self._log.info('Found bpm {0}', item['bpm']) if not overwrite: return - self._log.info(u'Press Enter {0} times to the rhythm or Ctrl-D ' - u'to exit', self.config['max_strokes'].get(int)) + self._log.info('Press Enter {0} times to the rhythm or Ctrl-D ' + 'to exit', self.config['max_strokes'].get(int)) new_bpm = bpm(self.config['max_strokes'].get(int)) item['bpm'] = int(new_bpm) if write: item.try_write() item.store() - self._log.info(u'Added new bpm {0}', item['bpm']) + self._log.info('Added new bpm {0}', item['bpm']) diff --git a/libs/common/beetsplug/bpsync.py b/libs/common/beetsplug/bpsync.py new file mode 100644 index 00000000..5b28d6d2 --- /dev/null +++ b/libs/common/beetsplug/bpsync.py @@ -0,0 +1,186 @@ +# This file is part of beets. +# Copyright 2019, Rahul Ahuja. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Update library's tags using Beatport. +""" + +from beets.plugins import BeetsPlugin, apply_item_changes +from beets import autotag, library, ui, util + +from .beatport import BeatportPlugin + + +class BPSyncPlugin(BeetsPlugin): + def __init__(self): + super().__init__() + self.beatport_plugin = BeatportPlugin() + self.beatport_plugin.setup() + + def commands(self): + cmd = ui.Subcommand('bpsync', help='update metadata from Beatport') + cmd.parser.add_option( + '-p', + '--pretend', + action='store_true', + help='show all changes but do nothing', + ) + cmd.parser.add_option( + '-m', + '--move', + action='store_true', + dest='move', + help="move files in the library directory", + ) + cmd.parser.add_option( + '-M', + '--nomove', + action='store_false', + dest='move', + help="don't move files in library", + ) + cmd.parser.add_option( + '-W', + '--nowrite', + action='store_false', + default=None, + dest='write', + help="don't write updated metadata to files", + ) + cmd.parser.add_format_option() + cmd.func = self.func + return [cmd] + + def func(self, lib, opts, args): + """Command handler for the bpsync function. + """ + move = ui.should_move(opts.move) + pretend = opts.pretend + write = ui.should_write(opts.write) + query = ui.decargs(args) + + self.singletons(lib, query, move, pretend, write) + self.albums(lib, query, move, pretend, write) + + def singletons(self, lib, query, move, pretend, write): + """Retrieve and apply info from the autotagger for items matched by + query. + """ + for item in lib.items(query + ['singleton:true']): + if not item.mb_trackid: + self._log.info( + 'Skipping singleton with no mb_trackid: {}', item + ) + continue + + if not self.is_beatport_track(item): + self._log.info( + 'Skipping non-{} singleton: {}', + self.beatport_plugin.data_source, + item, + ) + continue + + # Apply. + trackinfo = self.beatport_plugin.track_for_id(item.mb_trackid) + with lib.transaction(): + autotag.apply_item_metadata(item, trackinfo) + apply_item_changes(lib, item, move, pretend, write) + + @staticmethod + def is_beatport_track(item): + return ( + item.get('data_source') == BeatportPlugin.data_source + and item.mb_trackid.isnumeric() + ) + + def get_album_tracks(self, album): + if not album.mb_albumid: + self._log.info('Skipping album with no mb_albumid: {}', album) + return False + if not album.mb_albumid.isnumeric(): + self._log.info( + 'Skipping album with invalid {} ID: {}', + self.beatport_plugin.data_source, + album, + ) + return False + items = list(album.items()) + if album.get('data_source') == self.beatport_plugin.data_source: + return items + if not all(self.is_beatport_track(item) for item in items): + self._log.info( + 'Skipping non-{} release: {}', + self.beatport_plugin.data_source, + album, + ) + return False + return items + + def albums(self, lib, query, move, pretend, write): + """Retrieve and apply info from the autotagger for albums matched by + query and their items. + """ + # Process matching albums. + for album in lib.albums(query): + # Do we have a valid Beatport album? + items = self.get_album_tracks(album) + if not items: + continue + + # Get the Beatport album information. + albuminfo = self.beatport_plugin.album_for_id(album.mb_albumid) + if not albuminfo: + self._log.info( + 'Release ID {} not found for album {}', + album.mb_albumid, + album, + ) + continue + + beatport_trackid_to_trackinfo = { + track.track_id: track for track in albuminfo.tracks + } + library_trackid_to_item = { + int(item.mb_trackid): item for item in items + } + item_to_trackinfo = { + item: beatport_trackid_to_trackinfo[track_id] + for track_id, item in library_trackid_to_item.items() + } + + self._log.info('applying changes to {}', album) + with lib.transaction(): + autotag.apply_metadata(albuminfo, item_to_trackinfo) + changed = False + # Find any changed item to apply Beatport changes to album. + any_changed_item = items[0] + for item in items: + item_changed = ui.show_model_changes(item) + changed |= item_changed + if item_changed: + any_changed_item = item + apply_item_changes(lib, item, move, pretend, write) + + if pretend or not changed: + continue + + # Update album structure to reflect an item in it. + for key in library.Album.item_keys: + album[key] = any_changed_item[key] + album.store() + + # Move album art (and any inconsistent items). + if move and lib.directory in util.ancestry(items[0].path): + self._log.debug('moving album {}', album) + album.move() diff --git a/libs/common/beetsplug/bucket.py b/libs/common/beetsplug/bucket.py index c4be2a3d..9ed50b45 100644 --- a/libs/common/beetsplug/bucket.py +++ b/libs/common/beetsplug/bucket.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Fabrice Laporte. # @@ -16,12 +15,10 @@ """Provides the %bucket{} function for path formatting. """ -from __future__ import division, absolute_import, print_function from datetime import datetime import re import string -from six.moves import zip from itertools import tee from beets import plugins, ui @@ -49,7 +46,7 @@ def span_from_str(span_str): """Convert string to a 4 digits year """ if yearfrom < 100: - raise BucketError(u"%d must be expressed on 4 digits" % yearfrom) + raise BucketError("%d must be expressed on 4 digits" % yearfrom) # if two digits only, pick closest year that ends by these two # digits starting from yearfrom @@ -60,14 +57,14 @@ def span_from_str(span_str): d = (yearfrom - yearfrom % 100) + d return d - years = [int(x) for x in re.findall('\d+', span_str)] + years = [int(x) for x in re.findall(r'\d+', span_str)] if not years: - raise ui.UserError(u"invalid range defined for year bucket '%s': no " - u"year found" % span_str) + raise ui.UserError("invalid range defined for year bucket '%s': no " + "year found" % span_str) try: years = [normalize_year(x, years[0]) for x in years] except BucketError as exc: - raise ui.UserError(u"invalid range defined for year bucket '%s': %s" % + raise ui.UserError("invalid range defined for year bucket '%s': %s" % (span_str, exc)) res = {'from': years[0], 'str': span_str} @@ -128,10 +125,10 @@ def str2fmt(s): res = {'fromnchars': len(m.group('fromyear')), 'tonchars': len(m.group('toyear'))} - res['fmt'] = "%s%%s%s%s%s" % (m.group('bef'), - m.group('sep'), - '%s' if res['tonchars'] else '', - m.group('after')) + res['fmt'] = "{}%s{}{}{}".format(m.group('bef'), + m.group('sep'), + '%s' if res['tonchars'] else '', + m.group('after')) return res @@ -170,8 +167,8 @@ def build_alpha_spans(alpha_spans_str, alpha_regexs): begin_index = ASCII_DIGITS.index(bucket[0]) end_index = ASCII_DIGITS.index(bucket[-1]) else: - raise ui.UserError(u"invalid range defined for alpha bucket " - u"'%s': no alphanumeric character found" % + raise ui.UserError("invalid range defined for alpha bucket " + "'%s': no alphanumeric character found" % elem) spans.append( re.compile( @@ -184,7 +181,7 @@ def build_alpha_spans(alpha_spans_str, alpha_regexs): class BucketPlugin(plugins.BeetsPlugin): def __init__(self): - super(BucketPlugin, self).__init__() + super().__init__() self.template_funcs['bucket'] = self._tmpl_bucket self.config.add({ diff --git a/libs/common/beetsplug/chroma.py b/libs/common/beetsplug/chroma.py index 57472956..353923aa 100644 --- a/libs/common/beetsplug/chroma.py +++ b/libs/common/beetsplug/chroma.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -16,16 +15,17 @@ """Adds Chromaprint/Acoustid acoustic fingerprinting support to the autotagger. Requires the pyacoustid library. """ -from __future__ import division, absolute_import, print_function from beets import plugins from beets import ui from beets import util from beets import config -from beets.util import confit from beets.autotag import hooks +import confuse import acoustid from collections import defaultdict +from functools import partial +import re API_KEY = '1vOwZtEn' SCORE_THRESH = 0.5 @@ -57,6 +57,30 @@ def prefix(it, count): yield v +def releases_key(release, countries, original_year): + """Used as a key to sort releases by date then preferred country + """ + date = release.get('date') + if date and original_year: + year = date.get('year', 9999) + month = date.get('month', 99) + day = date.get('day', 99) + else: + year = 9999 + month = 99 + day = 99 + + # Uses index of preferred countries to sort + country_key = 99 + if release.get('country'): + for i, country in enumerate(countries): + if country.match(release['country']): + country_key = i + break + + return (year, month, day, country_key) + + def acoustid_match(log, path): """Gets metadata for a file from Acoustid and populates the _matches, _fingerprints, and _acoustids dictionaries accordingly. @@ -64,42 +88,55 @@ def acoustid_match(log, path): try: duration, fp = acoustid.fingerprint_file(util.syspath(path)) except acoustid.FingerprintGenerationError as exc: - log.error(u'fingerprinting of {0} failed: {1}', + log.error('fingerprinting of {0} failed: {1}', util.displayable_path(repr(path)), exc) return None + fp = fp.decode() _fingerprints[path] = fp try: res = acoustid.lookup(API_KEY, fp, duration, meta='recordings releases') except acoustid.AcoustidError as exc: - log.debug(u'fingerprint matching {0} failed: {1}', + log.debug('fingerprint matching {0} failed: {1}', util.displayable_path(repr(path)), exc) return None - log.debug(u'chroma: fingerprinted {0}', + log.debug('chroma: fingerprinted {0}', util.displayable_path(repr(path))) # Ensure the response is usable and parse it. if res['status'] != 'ok' or not res.get('results'): - log.debug(u'no match found') + log.debug('no match found') return None result = res['results'][0] # Best match. if result['score'] < SCORE_THRESH: - log.debug(u'no results above threshold') + log.debug('no results above threshold') return None _acoustids[path] = result['id'] - # Get recording and releases from the result. + # Get recording and releases from the result if not result.get('recordings'): - log.debug(u'no recordings found') + log.debug('no recordings found') return None recording_ids = [] - release_ids = [] + releases = [] for recording in result['recordings']: recording_ids.append(recording['id']) if 'releases' in recording: - release_ids += [rel['id'] for rel in recording['releases']] + releases.extend(recording['releases']) - log.debug(u'matched recordings {0} on releases {1}', + # The releases list is essentially in random order from the Acoustid lookup + # so we optionally sort it using the match.preferred configuration options. + # 'original_year' to sort the earliest first and + # 'countries' to then sort preferred countries first. + country_patterns = config['match']['preferred']['countries'].as_str_seq() + countries = [re.compile(pat, re.I) for pat in country_patterns] + original_year = config['match']['preferred']['original_year'] + releases.sort(key=partial(releases_key, + countries=countries, + original_year=original_year)) + release_ids = [rel['id'] for rel in releases] + + log.debug('matched recordings {0} on releases {1}', recording_ids, release_ids) _matches[path] = recording_ids, release_ids @@ -128,7 +165,7 @@ def _all_releases(items): class AcoustidPlugin(plugins.BeetsPlugin): def __init__(self): - super(AcoustidPlugin, self).__init__() + super().__init__() self.config.add({ 'auto': True, @@ -152,14 +189,14 @@ class AcoustidPlugin(plugins.BeetsPlugin): dist.add_expr('track_id', info.track_id not in recording_ids) return dist - def candidates(self, items, artist, album, va_likely): + def candidates(self, items, artist, album, va_likely, extra_tags=None): albums = [] for relid in prefix(_all_releases(items), MAX_RELEASES): album = hooks.album_for_mbid(relid) if album: albums.append(album) - self._log.debug(u'acoustid album candidates: {0}', len(albums)) + self._log.debug('acoustid album candidates: {0}', len(albums)) return albums def item_candidates(self, item, artist, title): @@ -172,24 +209,24 @@ class AcoustidPlugin(plugins.BeetsPlugin): track = hooks.track_for_mbid(recording_id) if track: tracks.append(track) - self._log.debug(u'acoustid item candidates: {0}', len(tracks)) + self._log.debug('acoustid item candidates: {0}', len(tracks)) return tracks def commands(self): submit_cmd = ui.Subcommand('submit', - help=u'submit Acoustid fingerprints') + help='submit Acoustid fingerprints') def submit_cmd_func(lib, opts, args): try: apikey = config['acoustid']['apikey'].as_str() - except confit.NotFoundError: - raise ui.UserError(u'no Acoustid user API key provided') + except confuse.NotFoundError: + raise ui.UserError('no Acoustid user API key provided') submit_items(self._log, apikey, lib.items(ui.decargs(args))) submit_cmd.func = submit_cmd_func fingerprint_cmd = ui.Subcommand( 'fingerprint', - help=u'generate fingerprints for items without them' + help='generate fingerprints for items without them' ) def fingerprint_cmd_func(lib, opts, args): @@ -232,15 +269,15 @@ def submit_items(log, userkey, items, chunksize=64): def submit_chunk(): """Submit the current accumulated fingerprint data.""" - log.info(u'submitting {0} fingerprints', len(data)) + log.info('submitting {0} fingerprints', len(data)) try: acoustid.submit(API_KEY, userkey, data) except acoustid.AcoustidError as exc: - log.warning(u'acoustid submission error: {0}', exc) + log.warning('acoustid submission error: {0}', exc) del data[:] for item in items: - fp = fingerprint_item(log, item) + fp = fingerprint_item(log, item, write=ui.should_write()) # Construct a submission dictionary for this item. item_data = { @@ -249,7 +286,7 @@ def submit_items(log, userkey, items, chunksize=64): } if item.mb_trackid: item_data['mbid'] = item.mb_trackid - log.debug(u'submitting MBID') + log.debug('submitting MBID') else: item_data.update({ 'track': item.title, @@ -260,7 +297,7 @@ def submit_items(log, userkey, items, chunksize=64): 'trackno': item.track, 'discno': item.disc, }) - log.debug(u'submitting textual metadata') + log.debug('submitting textual metadata') data.append(item_data) # If we have enough data, submit a chunk. @@ -281,28 +318,28 @@ def fingerprint_item(log, item, write=False): """ # Get a fingerprint and length for this track. if not item.length: - log.info(u'{0}: no duration available', + log.info('{0}: no duration available', util.displayable_path(item.path)) elif item.acoustid_fingerprint: if write: - log.info(u'{0}: fingerprint exists, skipping', + log.info('{0}: fingerprint exists, skipping', util.displayable_path(item.path)) else: - log.info(u'{0}: using existing fingerprint', + log.info('{0}: using existing fingerprint', util.displayable_path(item.path)) - return item.acoustid_fingerprint + return item.acoustid_fingerprint else: - log.info(u'{0}: fingerprinting', + log.info('{0}: fingerprinting', util.displayable_path(item.path)) try: _, fp = acoustid.fingerprint_file(util.syspath(item.path)) - item.acoustid_fingerprint = fp + item.acoustid_fingerprint = fp.decode() if write: - log.info(u'{0}: writing fingerprint', + log.info('{0}: writing fingerprint', util.displayable_path(item.path)) item.try_write() if item._db: item.store() return item.acoustid_fingerprint except acoustid.FingerprintGenerationError as exc: - log.info(u'fingerprint generation failed: {0}', exc) + log.info('fingerprint generation failed: {0}', exc) diff --git a/libs/common/beetsplug/convert.py b/libs/common/beetsplug/convert.py index d1223596..6bc07c28 100644 --- a/libs/common/beetsplug/convert.py +++ b/libs/common/beetsplug/convert.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Jakob Schnitzer. # @@ -15,20 +14,18 @@ """Converts tracks or albums to external directory """ -from __future__ import division, absolute_import, print_function +from beets.util import par_map, decode_commandline_path, arg_encoding import os import threading import subprocess import tempfile import shlex -import six from string import Template -import platform from beets import ui, util, plugins, config from beets.plugins import BeetsPlugin -from beets.util.confit import ConfigTypeError +from confuse import ConfigTypeError from beets import art from beets.util.artresizer import ArtResizer from beets.library import parse_query_string @@ -39,8 +36,8 @@ _temp_files = [] # Keep track of temporary transcoded files for deletion. # Some convenient alternate names for formats. ALIASES = { - u'wma': u'windows media', - u'vorbis': u'ogg', + 'wma': 'windows media', + 'vorbis': 'ogg', } LOSSLESS_FORMATS = ['ape', 'flac', 'alac', 'wav', 'aiff'] @@ -68,7 +65,7 @@ def get_format(fmt=None): extension = format_info.get('extension', fmt) except KeyError: raise ui.UserError( - u'convert: format {0} needs the "command" field' + 'convert: format {} needs the "command" field' .format(fmt) ) except ConfigTypeError: @@ -81,7 +78,7 @@ def get_format(fmt=None): command = config['convert']['command'].as_str() elif 'opts' in keys: # Undocumented option for backwards compatibility with < 1.3.1. - command = u'ffmpeg -i $source -y {0} $dest'.format( + command = 'ffmpeg -i $source -y {} $dest'.format( config['convert']['opts'].as_str() ) if 'extension' in keys: @@ -110,70 +107,81 @@ def should_transcode(item, fmt): class ConvertPlugin(BeetsPlugin): def __init__(self): - super(ConvertPlugin, self).__init__() + super().__init__() self.config.add({ - u'dest': None, - u'pretend': False, - u'threads': util.cpu_count(), - u'format': u'mp3', - u'formats': { - u'aac': { - u'command': u'ffmpeg -i $source -y -vn -acodec aac ' - u'-aq 1 $dest', - u'extension': u'm4a', + 'dest': None, + 'pretend': False, + 'link': False, + 'hardlink': False, + 'threads': util.cpu_count(), + 'format': 'mp3', + 'id3v23': 'inherit', + 'formats': { + 'aac': { + 'command': 'ffmpeg -i $source -y -vn -acodec aac ' + '-aq 1 $dest', + 'extension': 'm4a', }, - u'alac': { - u'command': u'ffmpeg -i $source -y -vn -acodec alac $dest', - u'extension': u'm4a', + 'alac': { + 'command': 'ffmpeg -i $source -y -vn -acodec alac $dest', + 'extension': 'm4a', }, - u'flac': u'ffmpeg -i $source -y -vn -acodec flac $dest', - u'mp3': u'ffmpeg -i $source -y -vn -aq 2 $dest', - u'opus': - u'ffmpeg -i $source -y -vn -acodec libopus -ab 96k $dest', - u'ogg': - u'ffmpeg -i $source -y -vn -acodec libvorbis -aq 3 $dest', - u'wma': - u'ffmpeg -i $source -y -vn -acodec wmav2 -vn $dest', + 'flac': 'ffmpeg -i $source -y -vn -acodec flac $dest', + 'mp3': 'ffmpeg -i $source -y -vn -aq 2 $dest', + 'opus': + 'ffmpeg -i $source -y -vn -acodec libopus -ab 96k $dest', + 'ogg': + 'ffmpeg -i $source -y -vn -acodec libvorbis -aq 3 $dest', + 'wma': + 'ffmpeg -i $source -y -vn -acodec wmav2 -vn $dest', }, - u'max_bitrate': 500, - u'auto': False, - u'tmpdir': None, - u'quiet': False, - u'embed': True, - u'paths': {}, - u'no_convert': u'', - u'never_convert_lossy_files': False, - u'copy_album_art': False, - u'album_art_maxwidth': 0, + 'max_bitrate': 500, + 'auto': False, + 'tmpdir': None, + 'quiet': False, + 'embed': True, + 'paths': {}, + 'no_convert': '', + 'never_convert_lossy_files': False, + 'copy_album_art': False, + 'album_art_maxwidth': 0, + 'delete_originals': False, }) self.early_import_stages = [self.auto_convert] self.register_listener('import_task_files', self._cleanup) def commands(self): - cmd = ui.Subcommand('convert', help=u'convert to external location') + cmd = ui.Subcommand('convert', help='convert to external location') cmd.parser.add_option('-p', '--pretend', action='store_true', - help=u'show actions but do nothing') + help='show actions but do nothing') cmd.parser.add_option('-t', '--threads', action='store', type='int', - help=u'change the number of threads, \ + help='change the number of threads, \ defaults to maximum available processors') cmd.parser.add_option('-k', '--keep-new', action='store_true', - dest='keep_new', help=u'keep only the converted \ + dest='keep_new', help='keep only the converted \ and move the old files') cmd.parser.add_option('-d', '--dest', action='store', - help=u'set the destination directory') + help='set the destination directory') cmd.parser.add_option('-f', '--format', action='store', dest='format', - help=u'set the target format of the tracks') + help='set the target format of the tracks') cmd.parser.add_option('-y', '--yes', action='store_true', dest='yes', - help=u'do not ask for confirmation') + help='do not ask for confirmation') + cmd.parser.add_option('-l', '--link', action='store_true', dest='link', + help='symlink files that do not \ + need transcoding.') + cmd.parser.add_option('-H', '--hardlink', action='store_true', + dest='hardlink', + help='hardlink files that do not \ + need transcoding. Overrides --link.') cmd.parser.add_album_option() cmd.func = self.convert_func return [cmd] def auto_convert(self, config, task): if self.config['auto']: - for item in task.imported_items(): - self.convert_on_import(config.lib, item) + par_map(lambda item: self.convert_on_import(config.lib, item), + task.imported_items()) # Utilities converted from functions to methods on logging overhaul @@ -191,22 +199,11 @@ class ConvertPlugin(BeetsPlugin): quiet = self.config['quiet'].get(bool) if not quiet and not pretend: - self._log.info(u'Encoding {0}', util.displayable_path(source)) + self._log.info('Encoding {0}', util.displayable_path(source)) - # On Python 3, we need to construct the command to invoke as a - # Unicode string. On Unix, this is a little unfortunate---the OS is - # expecting bytes---so we use surrogate escaping and decode with the - # argument encoding, which is the same encoding that will then be - # *reversed* to recover the same bytes before invoking the OS. On - # Windows, we want to preserve the Unicode filename "as is." - if not six.PY2: - command = command.decode(util.arg_encoding(), 'surrogateescape') - if platform.system() == 'Windows': - source = source.decode(util._fsencoding()) - dest = dest.decode(util._fsencoding()) - else: - source = source.decode(util.arg_encoding(), 'surrogateescape') - dest = dest.decode(util.arg_encoding(), 'surrogateescape') + command = command.decode(arg_encoding(), 'surrogateescape') + source = decode_commandline_path(source) + dest = decode_commandline_path(dest) # Substitute $source and $dest in the argument list. args = shlex.split(command) @@ -216,22 +213,19 @@ class ConvertPlugin(BeetsPlugin): 'source': source, 'dest': dest, }) - if six.PY2: - encode_cmd.append(args[i]) - else: - encode_cmd.append(args[i].encode(util.arg_encoding())) + encode_cmd.append(args[i].encode(util.arg_encoding())) if pretend: - self._log.info(u'{0}', u' '.join(ui.decargs(args))) + self._log.info('{0}', ' '.join(ui.decargs(args))) return try: util.command_output(encode_cmd) except subprocess.CalledProcessError as exc: # Something went wrong (probably Ctrl+C), remove temporary files - self._log.info(u'Encoding {0} failed. Cleaning up...', + self._log.info('Encoding {0} failed. Cleaning up...', util.displayable_path(source)) - self._log.debug(u'Command {0} exited with status {1}: {2}', + self._log.debug('Command {0} exited with status {1}: {2}', args, exc.returncode, exc.output) @@ -240,17 +234,17 @@ class ConvertPlugin(BeetsPlugin): raise except OSError as exc: raise ui.UserError( - u"convert: couldn't invoke '{0}': {1}".format( - u' '.join(ui.decargs(args)), exc + "convert: couldn't invoke '{}': {}".format( + ' '.join(ui.decargs(args)), exc ) ) if not quiet and not pretend: - self._log.info(u'Finished encoding {0}', + self._log.info('Finished encoding {0}', util.displayable_path(source)) def convert_item(self, dest_dir, keep_new, path_formats, fmt, - pretend=False): + pretend=False, link=False, hardlink=False): """A pipeline thread that converts `Item` objects from a library. """ @@ -283,41 +277,60 @@ class ConvertPlugin(BeetsPlugin): util.mkdirall(dest) if os.path.exists(util.syspath(dest)): - self._log.info(u'Skipping {0} (target file exists)', + self._log.info('Skipping {0} (target file exists)', util.displayable_path(item.path)) continue if keep_new: if pretend: - self._log.info(u'mv {0} {1}', + self._log.info('mv {0} {1}', util.displayable_path(item.path), util.displayable_path(original)) else: - self._log.info(u'Moving to {0}', + self._log.info('Moving to {0}', util.displayable_path(original)) util.move(item.path, original) if should_transcode(item, fmt): + linked = False try: self.encode(command, original, converted, pretend) except subprocess.CalledProcessError: continue else: + linked = link or hardlink if pretend: - self._log.info(u'cp {0} {1}', + msg = 'ln' if hardlink else ('ln -s' if link else 'cp') + + self._log.info('{2} {0} {1}', util.displayable_path(original), - util.displayable_path(converted)) + util.displayable_path(converted), + msg) else: # No transcoding necessary. - self._log.info(u'Copying {0}', - util.displayable_path(item.path)) - util.copy(original, converted) + msg = 'Hardlinking' if hardlink \ + else ('Linking' if link else 'Copying') + + self._log.info('{1} {0}', + util.displayable_path(item.path), + msg) + + if hardlink: + util.hardlink(original, converted) + elif link: + util.link(original, converted) + else: + util.copy(original, converted) if pretend: continue + id3v23 = self.config['id3v23'].as_choice([True, False, 'inherit']) + if id3v23 == 'inherit': + id3v23 = None + # Write tags from the database to the converted file. - item.try_write(path=converted) + item.try_write(path=converted, id3v23=id3v23) if keep_new: # If we're keeping the transcoded file, read it again (after @@ -326,13 +339,13 @@ class ConvertPlugin(BeetsPlugin): item.read() item.store() # Store new path and audio data. - if self.config['embed']: - album = item.get_album() + if self.config['embed'] and not linked: + album = item._cached_album if album and album.artpath: - self._log.debug(u'embedding album art from {}', + self._log.debug('embedding album art from {}', util.displayable_path(album.artpath)) art.embed_item(self._log, item, album.artpath, - itempath=converted) + itempath=converted, id3v23=id3v23) if keep_new: plugins.send('after_convert', item=item, @@ -341,7 +354,8 @@ class ConvertPlugin(BeetsPlugin): plugins.send('after_convert', item=item, dest=converted, keepnew=False) - def copy_album_art(self, album, dest_dir, path_formats, pretend=False): + def copy_album_art(self, album, dest_dir, path_formats, pretend=False, + link=False, hardlink=False): """Copies or converts the associated cover art of the album. Album must have at least one track. """ @@ -369,7 +383,7 @@ class ConvertPlugin(BeetsPlugin): util.mkdirall(dest) if os.path.exists(util.syspath(dest)): - self._log.info(u'Skipping {0} (target file exists)', + self._log.info('Skipping {0} (target file exists)', util.displayable_path(album.artpath)) return @@ -383,31 +397,43 @@ class ConvertPlugin(BeetsPlugin): if size: resize = size[0] > maxwidth else: - self._log.warning(u'Could not get size of image (please see ' - u'documentation for dependencies).') + self._log.warning('Could not get size of image (please see ' + 'documentation for dependencies).') # Either copy or resize (while copying) the image. if resize: - self._log.info(u'Resizing cover art from {0} to {1}', + self._log.info('Resizing cover art from {0} to {1}', util.displayable_path(album.artpath), util.displayable_path(dest)) if not pretend: ArtResizer.shared.resize(maxwidth, album.artpath, dest) else: if pretend: - self._log.info(u'cp {0} {1}', + msg = 'ln' if hardlink else ('ln -s' if link else 'cp') + + self._log.info('{2} {0} {1}', util.displayable_path(album.artpath), - util.displayable_path(dest)) + util.displayable_path(dest), + msg) else: - self._log.info(u'Copying cover art to {0}', + msg = 'Hardlinking' if hardlink \ + else ('Linking' if link else 'Copying') + + self._log.info('{2} cover art from {0} to {1}', util.displayable_path(album.artpath), - util.displayable_path(dest)) - util.copy(album.artpath, dest) + util.displayable_path(dest), + msg) + if hardlink: + util.hardlink(album.artpath, dest) + elif link: + util.link(album.artpath, dest) + else: + util.copy(album.artpath, dest) def convert_func(self, lib, opts, args): dest = opts.dest or self.config['dest'].get() if not dest: - raise ui.UserError(u'no convert destination set') + raise ui.UserError('no convert destination set') dest = util.bytestring_path(dest) threads = opts.threads or self.config['threads'].get(int) @@ -421,33 +447,46 @@ class ConvertPlugin(BeetsPlugin): else: pretend = self.config['pretend'].get(bool) + if opts.hardlink is not None: + hardlink = opts.hardlink + link = False + elif opts.link is not None: + hardlink = False + link = opts.link + else: + hardlink = self.config['hardlink'].get(bool) + link = self.config['link'].get(bool) + if opts.album: albums = lib.albums(ui.decargs(args)) items = [i for a in albums for i in a.items()] if not pretend: for a in albums: - ui.print_(format(a, u'')) + ui.print_(format(a, '')) else: items = list(lib.items(ui.decargs(args))) if not pretend: for i in items: - ui.print_(format(i, u'')) + ui.print_(format(i, '')) if not items: - self._log.error(u'Empty query result.') + self._log.error('Empty query result.') return - if not (pretend or opts.yes or ui.input_yn(u"Convert? (Y/n)")): + if not (pretend or opts.yes or ui.input_yn("Convert? (Y/n)")): return if opts.album and self.config['copy_album_art']: for album in albums: - self.copy_album_art(album, dest, path_formats, pretend) + self.copy_album_art(album, dest, path_formats, pretend, + link, hardlink) convert = [self.convert_item(dest, opts.keep_new, path_formats, fmt, - pretend) + pretend, + link, + hardlink) for _ in range(threads)] pipe = util.pipeline.Pipeline([iter(items), convert]) pipe.run_parallel() @@ -477,11 +516,16 @@ class ConvertPlugin(BeetsPlugin): # Change the newly-imported database entry to point to the # converted file. + source_path = item.path item.path = dest item.write() item.read() # Load new audio information data. item.store() + if self.config['delete_originals']: + self._log.info('Removing original file {0}', source_path) + util.remove(source_path, False) + def _cleanup(self, task, session): for path in task.old_paths: if path in _temp_files: diff --git a/libs/common/beetsplug/cue.py b/libs/common/beetsplug/cue.py deleted file mode 100644 index fd564b55..00000000 --- a/libs/common/beetsplug/cue.py +++ /dev/null @@ -1,57 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2016 Bruno Cauet -# Split an album-file in tracks thanks a cue file - -from __future__ import division, absolute_import, print_function - -import subprocess -from os import path -from glob import glob - -from beets.util import command_output, displayable_path -from beets.plugins import BeetsPlugin -from beets.autotag import TrackInfo - - -class CuePlugin(BeetsPlugin): - def __init__(self): - super(CuePlugin, self).__init__() - # this does not seem supported by shnsplit - self.config.add({ - 'keep_before': .1, - 'keep_after': .9, - }) - - # self.register_listener('import_task_start', self.look_for_cues) - - def candidates(self, items, artist, album, va_likely): - import pdb - pdb.set_trace() - - def item_candidates(self, item, artist, album): - dir = path.dirname(item.path) - cues = glob.glob(path.join(dir, "*.cue")) - if not cues: - return - if len(cues) > 1: - self._log.info(u"Found multiple cue files doing nothing: {0}", - list(map(displayable_path, cues))) - - cue_file = cues[0] - self._log.info("Found {} for {}", displayable_path(cue_file), item) - - try: - # careful: will ask for input in case of conflicts - command_output(['shnsplit', '-f', cue_file, item.path]) - except (subprocess.CalledProcessError, OSError): - self._log.exception(u'shnsplit execution failed') - return - - tracks = glob(path.join(dir, "*.wav")) - self._log.info("Generated {0} tracks", len(tracks)) - for t in tracks: - title = "dunno lol" - track_id = "wtf" - index = int(path.basename(t)[len("split-track"):-len(".wav")]) - yield TrackInfo(title, track_id, index=index, artist=artist) - # generate TrackInfo instances diff --git a/libs/common/beetsplug/deezer.py b/libs/common/beetsplug/deezer.py new file mode 100644 index 00000000..5f158f93 --- /dev/null +++ b/libs/common/beetsplug/deezer.py @@ -0,0 +1,230 @@ +# This file is part of beets. +# Copyright 2019, Rahul Ahuja. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Adds Deezer release and track search support to the autotagger +""" + +import collections + +import unidecode +import requests + +from beets import ui +from beets.autotag import AlbumInfo, TrackInfo +from beets.plugins import MetadataSourcePlugin, BeetsPlugin + + +class DeezerPlugin(MetadataSourcePlugin, BeetsPlugin): + data_source = 'Deezer' + + # Base URLs for the Deezer API + # Documentation: https://developers.deezer.com/api/ + search_url = 'https://api.deezer.com/search/' + album_url = 'https://api.deezer.com/album/' + track_url = 'https://api.deezer.com/track/' + + id_regex = { + 'pattern': r'(^|deezer\.com/)([a-z]*/)?({}/)?(\d+)', + 'match_group': 4, + } + + def __init__(self): + super().__init__() + + def album_for_id(self, album_id): + """Fetch an album by its Deezer ID or URL and return an + AlbumInfo object or None if the album is not found. + + :param album_id: Deezer ID or URL for the album. + :type album_id: str + :return: AlbumInfo object for album. + :rtype: beets.autotag.hooks.AlbumInfo or None + """ + deezer_id = self._get_id('album', album_id) + if deezer_id is None: + return None + + album_data = requests.get(self.album_url + deezer_id).json() + artist, artist_id = self.get_artist(album_data['contributors']) + + release_date = album_data['release_date'] + date_parts = [int(part) for part in release_date.split('-')] + num_date_parts = len(date_parts) + + if num_date_parts == 3: + year, month, day = date_parts + elif num_date_parts == 2: + year, month = date_parts + day = None + elif num_date_parts == 1: + year = date_parts[0] + month = None + day = None + else: + raise ui.UserError( + "Invalid `release_date` returned " + "by {} API: '{}'".format(self.data_source, release_date) + ) + + tracks_data = requests.get( + self.album_url + deezer_id + '/tracks' + ).json()['data'] + if not tracks_data: + return None + tracks = [] + medium_totals = collections.defaultdict(int) + for i, track_data in enumerate(tracks_data, start=1): + track = self._get_track(track_data) + track.index = i + medium_totals[track.medium] += 1 + tracks.append(track) + for track in tracks: + track.medium_total = medium_totals[track.medium] + + return AlbumInfo( + album=album_data['title'], + album_id=deezer_id, + artist=artist, + artist_credit=self.get_artist([album_data['artist']])[0], + artist_id=artist_id, + tracks=tracks, + albumtype=album_data['record_type'], + va=len(album_data['contributors']) == 1 + and artist.lower() == 'various artists', + year=year, + month=month, + day=day, + label=album_data['label'], + mediums=max(medium_totals.keys()), + data_source=self.data_source, + data_url=album_data['link'], + ) + + def _get_track(self, track_data): + """Convert a Deezer track object dict to a TrackInfo object. + + :param track_data: Deezer Track object dict + :type track_data: dict + :return: TrackInfo object for track + :rtype: beets.autotag.hooks.TrackInfo + """ + artist, artist_id = self.get_artist( + track_data.get('contributors', [track_data['artist']]) + ) + return TrackInfo( + title=track_data['title'], + track_id=track_data['id'], + artist=artist, + artist_id=artist_id, + length=track_data['duration'], + index=track_data['track_position'], + medium=track_data['disk_number'], + medium_index=track_data['track_position'], + data_source=self.data_source, + data_url=track_data['link'], + ) + + def track_for_id(self, track_id=None, track_data=None): + """Fetch a track by its Deezer ID or URL and return a + TrackInfo object or None if the track is not found. + + :param track_id: (Optional) Deezer ID or URL for the track. Either + ``track_id`` or ``track_data`` must be provided. + :type track_id: str + :param track_data: (Optional) Simplified track object dict. May be + provided instead of ``track_id`` to avoid unnecessary API calls. + :type track_data: dict + :return: TrackInfo object for track + :rtype: beets.autotag.hooks.TrackInfo or None + """ + if track_data is None: + deezer_id = self._get_id('track', track_id) + if deezer_id is None: + return None + track_data = requests.get(self.track_url + deezer_id).json() + track = self._get_track(track_data) + + # Get album's tracks to set `track.index` (position on the entire + # release) and `track.medium_total` (total number of tracks on + # the track's disc). + album_tracks_data = requests.get( + self.album_url + str(track_data['album']['id']) + '/tracks' + ).json()['data'] + medium_total = 0 + for i, track_data in enumerate(album_tracks_data, start=1): + if track_data['disk_number'] == track.medium: + medium_total += 1 + if track_data['id'] == track.track_id: + track.index = i + track.medium_total = medium_total + return track + + @staticmethod + def _construct_search_query(filters=None, keywords=''): + """Construct a query string with the specified filters and keywords to + be provided to the Deezer Search API + (https://developers.deezer.com/api/search). + + :param filters: (Optional) Field filters to apply. + :type filters: dict + :param keywords: (Optional) Query keywords to use. + :type keywords: str + :return: Query string to be provided to the Search API. + :rtype: str + """ + query_components = [ + keywords, + ' '.join(f'{k}:"{v}"' for k, v in filters.items()), + ] + query = ' '.join([q for q in query_components if q]) + if not isinstance(query, str): + query = query.decode('utf8') + return unidecode.unidecode(query) + + def _search_api(self, query_type, filters=None, keywords=''): + """Query the Deezer Search API for the specified ``keywords``, applying + the provided ``filters``. + + :param query_type: The Deezer Search API method to use. Valid types + are: 'album', 'artist', 'history', 'playlist', 'podcast', + 'radio', 'track', 'user', and 'track'. + :type query_type: str + :param filters: (Optional) Field filters to apply. + :type filters: dict + :param keywords: (Optional) Query keywords to use. + :type keywords: str + :return: JSON data for the class:`Response ` object or None + if no search results are returned. + :rtype: dict or None + """ + query = self._construct_search_query( + keywords=keywords, filters=filters + ) + if not query: + return None + self._log.debug( + f"Searching {self.data_source} for '{query}'" + ) + response = requests.get( + self.search_url + query_type, params={'q': query} + ) + response.raise_for_status() + response_data = response.json().get('data', []) + self._log.debug( + "Found {} result(s) from {} for '{}'", + len(response_data), + self.data_source, + query, + ) + return response_data diff --git a/libs/common/beetsplug/discogs.py b/libs/common/beetsplug/discogs.py index eeb87d31..d015e420 100644 --- a/libs/common/beetsplug/discogs.py +++ b/libs/common/beetsplug/discogs.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -14,19 +13,18 @@ # included in all copies or substantial portions of the Software. """Adds Discogs album search support to the autotagger. Requires the -discogs-client library. +python3-discogs-client library. """ -from __future__ import division, absolute_import, print_function import beets.ui from beets import config -from beets.autotag.hooks import AlbumInfo, TrackInfo, Distance -from beets.plugins import BeetsPlugin -from beets.util import confit +from beets.autotag.hooks import AlbumInfo, TrackInfo +from beets.plugins import MetadataSourcePlugin, BeetsPlugin, get_distance +import confuse from discogs_client import Release, Master, Client from discogs_client.exceptions import DiscogsAPIError from requests.exceptions import ConnectionError -from six.moves import http_client +import http.client import beets import re import time @@ -37,10 +35,12 @@ import traceback from string import ascii_lowercase -USER_AGENT = u'beets/{0} +http://beets.io/'.format(beets.__version__) +USER_AGENT = f'beets/{beets.__version__} +https://beets.io/' +API_KEY = 'rAzVUQYRaoFjeBjyWuWZ' +API_SECRET = 'plxtUTqoCzwxZpqdPysCwGuBSmZNdZVy' # Exceptions that discogs_client should really handle but does not. -CONNECTION_ERRORS = (ConnectionError, socket.error, http_client.HTTPException, +CONNECTION_ERRORS = (ConnectionError, socket.error, http.client.HTTPException, ValueError, # JSON decoding raises a ValueError. DiscogsAPIError) @@ -48,13 +48,15 @@ CONNECTION_ERRORS = (ConnectionError, socket.error, http_client.HTTPException, class DiscogsPlugin(BeetsPlugin): def __init__(self): - super(DiscogsPlugin, self).__init__() + super().__init__() self.config.add({ - 'apikey': 'rAzVUQYRaoFjeBjyWuWZ', - 'apisecret': 'plxtUTqoCzwxZpqdPysCwGuBSmZNdZVy', + 'apikey': API_KEY, + 'apisecret': API_SECRET, 'tokenfile': 'discogs_token.json', 'source_weight': 0.5, 'user_token': '', + 'separator': ', ', + 'index_tracks': False, }) self.config['apikey'].redact = True self.config['apisecret'].redact = True @@ -71,6 +73,8 @@ class DiscogsPlugin(BeetsPlugin): # Try using a configured user token (bypassing OAuth login). user_token = self.config['user_token'].as_str() if user_token: + # The rate limit for authenticated users goes up to 60 + # requests per minute. self.discogs_client = Client(USER_AGENT, user_token=user_token) return @@ -78,7 +82,7 @@ class DiscogsPlugin(BeetsPlugin): try: with open(self._tokenfile()) as f: tokendata = json.load(f) - except IOError: + except OSError: # No token yet. Generate one. token, secret = self.authenticate(c_key, c_secret) else: @@ -97,7 +101,7 @@ class DiscogsPlugin(BeetsPlugin): def _tokenfile(self): """Get the path to the JSON file for storing the OAuth token. """ - return self.config['tokenfile'].get(confit.Filename(in_app_dir=True)) + return self.config['tokenfile'].get(confuse.Filename(in_app_dir=True)) def authenticate(self, c_key, c_secret): # Get the link for the OAuth page. @@ -105,24 +109,24 @@ class DiscogsPlugin(BeetsPlugin): try: _, _, url = auth_client.get_authorize_url() except CONNECTION_ERRORS as e: - self._log.debug(u'connection error: {0}', e) - raise beets.ui.UserError(u'communication with Discogs failed') + self._log.debug('connection error: {0}', e) + raise beets.ui.UserError('communication with Discogs failed') - beets.ui.print_(u"To authenticate with Discogs, visit:") + beets.ui.print_("To authenticate with Discogs, visit:") beets.ui.print_(url) # Ask for the code and validate it. - code = beets.ui.input_(u"Enter the code:") + code = beets.ui.input_("Enter the code:") try: token, secret = auth_client.get_access_token(code) except DiscogsAPIError: - raise beets.ui.UserError(u'Discogs authorization failed') + raise beets.ui.UserError('Discogs authorization failed') except CONNECTION_ERRORS as e: - self._log.debug(u'connection error: {0}', e) - raise beets.ui.UserError(u'Discogs token request failed') + self._log.debug('connection error: {0}', e) + raise beets.ui.UserError('Discogs token request failed') # Save the token for later use. - self._log.debug(u'Discogs token {0}, secret {1}', token, secret) + self._log.debug('Discogs token {0}, secret {1}', token, secret) with open(self._tokenfile(), 'w') as f: json.dump({'token': token, 'secret': secret}, f) @@ -131,12 +135,22 @@ class DiscogsPlugin(BeetsPlugin): def album_distance(self, items, album_info, mapping): """Returns the album distance. """ - dist = Distance() - if album_info.data_source == 'Discogs': - dist.add('source', self.config['source_weight'].as_number()) - return dist + return get_distance( + data_source='Discogs', + info=album_info, + config=self.config + ) - def candidates(self, items, artist, album, va_likely): + def track_distance(self, item, track_info): + """Returns the track distance. + """ + return get_distance( + data_source='Discogs', + info=track_info, + config=self.config + ) + + def candidates(self, items, artist, album, va_likely, extra_tags=None): """Returns a list of AlbumInfo objects for discogs search results matching an album and artist (if not various). """ @@ -146,20 +160,45 @@ class DiscogsPlugin(BeetsPlugin): if va_likely: query = album else: - query = '%s %s' % (artist, album) + query = f'{artist} {album}' try: return self.get_albums(query) except DiscogsAPIError as e: - self._log.debug(u'API Error: {0} (query: {1})', e, query) + self._log.debug('API Error: {0} (query: {1})', e, query) if e.status_code == 401: self.reset_auth() return self.candidates(items, artist, album, va_likely) else: return [] except CONNECTION_ERRORS: - self._log.debug(u'Connection error in album search', exc_info=True) + self._log.debug('Connection error in album search', exc_info=True) return [] + @staticmethod + def extract_release_id_regex(album_id): + """Returns the Discogs_id or None.""" + # Discogs-IDs are simple integers. In order to avoid confusion with + # other metadata plugins, we only look for very specific formats of the + # input string: + # - plain integer, optionally wrapped in brackets and prefixed by an + # 'r', as this is how discogs displays the release ID on its webpage. + # - legacy url format: discogs.com//release/ + # - current url format: discogs.com/release/- + # See #291, #4080 and #4085 for the discussions leading up to these + # patterns. + # Regex has been tested here https://regex101.com/r/wyLdB4/2 + + for pattern in [ + r'^\[?r?(?P\d+)\]?$', + r'discogs\.com/release/(?P\d+)-', + r'discogs\.com/[^/]+/release/(?P\d+)', + ]: + match = re.search(pattern, album_id) + if match: + return int(match.group('id')) + + return None + def album_for_id(self, album_id): """Fetches an album by its Discogs ID and returns an AlbumInfo object or None if the album is not found. @@ -167,28 +206,28 @@ class DiscogsPlugin(BeetsPlugin): if not self.discogs_client: return - self._log.debug(u'Searching for release {0}', album_id) - # Discogs-IDs are simple integers. We only look for those at the end - # of an input string as to avoid confusion with other metadata plugins. - # An optional bracket can follow the integer, as this is how discogs - # displays the release ID on its webpage. - match = re.search(r'(^|\[*r|discogs\.com/.+/release/)(\d+)($|\])', - album_id) - if not match: + self._log.debug('Searching for release {0}', album_id) + + discogs_id = self.extract_release_id_regex(album_id) + + if not discogs_id: return None - result = Release(self.discogs_client, {'id': int(match.group(2))}) + + result = Release(self.discogs_client, {'id': discogs_id}) # Try to obtain title to verify that we indeed have a valid Release try: getattr(result, 'title') except DiscogsAPIError as e: if e.status_code != 404: - self._log.debug(u'API Error: {0} (query: {1})', e, result._uri) + self._log.debug('API Error: {0} (query: {1})', e, + result.data['resource_url']) if e.status_code == 401: self.reset_auth() return self.album_for_id(album_id) return None except CONNECTION_ERRORS: - self._log.debug(u'Connection error in album lookup', exc_info=True) + self._log.debug('Connection error in album lookup', + exc_info=True) return None return self.get_album_info(result) @@ -199,18 +238,17 @@ class DiscogsPlugin(BeetsPlugin): # cause a query to return no results, even if they match the artist or # album title. Use `re.UNICODE` flag to avoid stripping non-english # word characters. - # FIXME: Encode as ASCII to work around a bug: - # https://github.com/beetbox/beets/issues/1051 - # When the library is fixed, we should encode as UTF-8. - query = re.sub(r'(?u)\W+', ' ', query).encode('ascii', "replace") + query = re.sub(r'(?u)\W+', ' ', query) # Strip medium information from query, Things like "CD1" and "disk 1" # can also negate an otherwise positive result. - query = re.sub(br'(?i)\b(CD|disc)\s*\d+', b'', query) + query = re.sub(r'(?i)\b(CD|disc)\s*\d+', '', query) + try: releases = self.discogs_client.search(query, type='release').page(1) + except CONNECTION_ERRORS: - self._log.debug(u"Communication error while searching for {0!r}", + self._log.debug("Communication error while searching for {0!r}", query, exc_info=True) return [] return [album for album in map(self.get_album_info, releases[:5]) @@ -220,20 +258,22 @@ class DiscogsPlugin(BeetsPlugin): """Fetches a master release given its Discogs ID and returns its year or None if the master release is not found. """ - self._log.debug(u'Searching for master release {0}', master_id) + self._log.debug('Searching for master release {0}', master_id) result = Master(self.discogs_client, {'id': master_id}) + try: year = result.fetch('year') return year except DiscogsAPIError as e: if e.status_code != 404: - self._log.debug(u'API Error: {0} (query: {1})', e, result._uri) + self._log.debug('API Error: {0} (query: {1})', e, + result.data['resource_url']) if e.status_code == 401: self.reset_auth() return self.get_master_year(master_id) return None except CONNECTION_ERRORS: - self._log.debug(u'Connection error in master release lookup', + self._log.debug('Connection error in master release lookup', exc_info=True) return None @@ -252,10 +292,12 @@ class DiscogsPlugin(BeetsPlugin): # https://www.discogs.com/help/doc/submission-guidelines-general-rules if not all([result.data.get(k) for k in ['artists', 'title', 'id', 'tracklist']]): - self._log.warn(u"Release does not contain the required fields") + self._log.warning("Release does not contain the required fields") return None - artist, artist_id = self.get_artist([a.data for a in result.artists]) + artist, artist_id = MetadataSourcePlugin.get_artist( + [a.data for a in result.artists] + ) album = re.sub(r' +', ' ', result.title) album_id = result.data['id'] # Use `.data` to access the tracklist directly instead of the @@ -270,10 +312,13 @@ class DiscogsPlugin(BeetsPlugin): mediums = [t.medium for t in tracks] country = result.data.get('country') data_url = result.data.get('uri') + style = self.format(result.data.get('styles')) + genre = self.format(result.data.get('genres')) + discogs_albumid = self.extract_release_id(result.data.get('uri')) # Extract information for the optional AlbumInfo fields that are # contained on nested discogs fields. - albumtype = media = label = catalogno = None + albumtype = media = label = catalogno = labelid = None if result.data.get('formats'): albumtype = ', '.join( result.data['formats'][0].get('descriptions', [])) or None @@ -281,12 +326,13 @@ class DiscogsPlugin(BeetsPlugin): if result.data.get('labels'): label = result.data['labels'][0].get('name') catalogno = result.data['labels'][0].get('catno') + labelid = result.data['labels'][0].get('id') # Additional cleanups (various artists name, catalog number, media). if va: artist = config['va_name'].as_str() if catalogno == 'none': - catalogno = None + catalogno = None # Explicitly set the `media` for the tracks, since it is expected by # `autotag.apply_metadata`, and set `medium_total`. for track in tracks: @@ -302,36 +348,29 @@ class DiscogsPlugin(BeetsPlugin): # a master release, otherwise fetch the master release. original_year = self.get_master_year(master_id) if master_id else year - return AlbumInfo(album, album_id, artist, artist_id, tracks, asin=None, - albumtype=albumtype, va=va, year=year, month=None, - day=None, label=label, mediums=len(set(mediums)), - artist_sort=None, releasegroup_id=master_id, - catalognum=catalogno, script=None, language=None, - country=country, albumstatus=None, media=media, - albumdisambig=None, artist_credit=None, - original_year=original_year, original_month=None, - original_day=None, data_source='Discogs', - data_url=data_url) + return AlbumInfo(album=album, album_id=album_id, artist=artist, + artist_id=artist_id, tracks=tracks, + albumtype=albumtype, va=va, year=year, + label=label, mediums=len(set(mediums)), + releasegroup_id=master_id, catalognum=catalogno, + country=country, style=style, genre=genre, + media=media, original_year=original_year, + data_source='Discogs', data_url=data_url, + discogs_albumid=discogs_albumid, + discogs_labelid=labelid, discogs_artistid=artist_id) - def get_artist(self, artists): - """Returns an artist string (all artists) and an artist_id (the main - artist) for a list of discogs album or track artists. - """ - artist_id = None - bits = [] - for i, artist in enumerate(artists): - if not artist_id: - artist_id = artist['id'] - name = artist['name'] - # Strip disambiguation number. - name = re.sub(r' \(\d+\)$', '', name) - # Move articles to the front. - name = re.sub(r'(?i)^(.*?), (a|an|the)$', r'\2 \1', name) - bits.append(name) - if artist['join'] and i < len(artists) - 1: - bits.append(artist['join']) - artist = ' '.join(bits).replace(' ,', ',') or None - return artist, artist_id + def format(self, classification): + if classification: + return self.config['separator'].as_str() \ + .join(sorted(classification)) + else: + return None + + def extract_release_id(self, uri): + if uri: + return uri.split("/")[-1] + else: + return None def get_tracks(self, tracklist): """Returns a list of TrackInfo objects for a discogs tracklist. @@ -342,20 +381,34 @@ class DiscogsPlugin(BeetsPlugin): # FIXME: this is an extra precaution for making sure there are no # side effects after #2222. It should be removed after further # testing. - self._log.debug(u'{}', traceback.format_exc()) - self._log.error(u'uncaught exception in coalesce_tracks: {}', exc) + self._log.debug('{}', traceback.format_exc()) + self._log.error('uncaught exception in coalesce_tracks: {}', exc) clean_tracklist = tracklist tracks = [] index_tracks = {} index = 0 + # Distinct works and intra-work divisions, as defined by index tracks. + divisions, next_divisions = [], [] for track in clean_tracklist: # Only real tracks have `position`. Otherwise, it's an index track. if track['position']: index += 1 - track_info = self.get_track_info(track, index) + if next_divisions: + # End of a block of index tracks: update the current + # divisions. + divisions += next_divisions + del next_divisions[:] + track_info = self.get_track_info(track, index, divisions) track_info.track_alt = track['position'] tracks.append(track_info) else: + next_divisions.append(track['title']) + # We expect new levels of division at the beginning of the + # tracklist (and possibly elsewhere). + try: + divisions.pop() + except IndexError: + pass index_tracks[index + 1] = track['title'] # Fix up medium and medium_index for each track. Discogs position is @@ -367,7 +420,7 @@ class DiscogsPlugin(BeetsPlugin): # If a medium has two sides (ie. vinyl or cassette), each pair of # consecutive sides should belong to the same medium. if all([track.medium is not None for track in tracks]): - m = sorted(set([track.medium.lower() for track in tracks])) + m = sorted({track.medium.lower() for track in tracks}) # If all track.medium are single consecutive letters, assume it is # a 2-sided medium. if ''.join(m) in ascii_lowercase: @@ -426,7 +479,7 @@ class DiscogsPlugin(BeetsPlugin): # Calculate position based on first subtrack, without subindex. idx, medium_idx, sub_idx = \ self.get_track_index(subtracks[0]['position']) - position = '%s%s' % (idx or '', medium_idx or '') + position = '{}{}'.format(idx or '', medium_idx or '') if tracklist and not tracklist[-1]['position']: # Assume the previous index track contains the track title. @@ -444,6 +497,12 @@ class DiscogsPlugin(BeetsPlugin): for subtrack in subtracks: if not subtrack.get('artists'): subtrack['artists'] = index_track['artists'] + # Concatenate index with track title when index_tracks + # option is set + if self.config['index_tracks']: + for subtrack in subtracks: + subtrack['title'] = '{}: {}'.format( + index_track['title'], subtrack['title']) tracklist.extend(subtracks) else: # Merge the subtracks, pick a title, and append the new track. @@ -490,18 +549,23 @@ class DiscogsPlugin(BeetsPlugin): return tracklist - def get_track_info(self, track, index): + def get_track_info(self, track, index, divisions): """Returns a TrackInfo object for a discogs track. """ title = track['title'] + if self.config['index_tracks']: + prefix = ', '.join(divisions) + if prefix: + title = f'{prefix}: {title}' track_id = None medium, medium_index, _ = self.get_track_index(track['position']) - artist, artist_id = self.get_artist(track.get('artists', [])) + artist, artist_id = MetadataSourcePlugin.get_artist( + track.get('artists', []) + ) length = self.get_track_length(track['duration']) - return TrackInfo(title, track_id, artist=artist, artist_id=artist_id, - length=length, index=index, - medium=medium, medium_index=medium_index, - artist_sort=None, disctitle=None, artist_credit=None) + return TrackInfo(title=title, track_id=track_id, artist=artist, + artist_id=artist_id, length=length, index=index, + medium=medium, medium_index=medium_index) def get_track_index(self, position): """Returns the medium, medium index and subtrack index for a discogs @@ -528,7 +592,7 @@ class DiscogsPlugin(BeetsPlugin): if subindex and subindex.startswith('.'): subindex = subindex[1:] else: - self._log.debug(u'Invalid position: {0}', position) + self._log.debug('Invalid position: {0}', position) medium = index = subindex = None return medium or None, index or None, subindex or None diff --git a/libs/common/beetsplug/duplicates.py b/libs/common/beetsplug/duplicates.py index b316cfda..fdd5c175 100644 --- a/libs/common/beetsplug/duplicates.py +++ b/libs/common/beetsplug/duplicates.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Pedro Silva. # @@ -15,16 +14,15 @@ """List duplicate tracks or albums. """ -from __future__ import division, absolute_import, print_function import shlex from beets.plugins import BeetsPlugin from beets.ui import decargs, print_, Subcommand, UserError from beets.util import command_output, displayable_path, subprocess, \ - bytestring_path, MoveOperation + bytestring_path, MoveOperation, decode_commandline_path from beets.library import Item, Album -import six + PLUGIN = 'duplicates' @@ -33,7 +31,7 @@ class DuplicatesPlugin(BeetsPlugin): """List duplicate tracks or albums """ def __init__(self): - super(DuplicatesPlugin, self).__init__() + super().__init__() self.config.add({ 'album': False, @@ -56,54 +54,54 @@ class DuplicatesPlugin(BeetsPlugin): help=__doc__, aliases=['dup']) self._command.parser.add_option( - u'-c', u'--count', dest='count', + '-c', '--count', dest='count', action='store_true', - help=u'show duplicate counts', + help='show duplicate counts', ) self._command.parser.add_option( - u'-C', u'--checksum', dest='checksum', + '-C', '--checksum', dest='checksum', action='store', metavar='PROG', - help=u'report duplicates based on arbitrary command', + help='report duplicates based on arbitrary command', ) self._command.parser.add_option( - u'-d', u'--delete', dest='delete', + '-d', '--delete', dest='delete', action='store_true', - help=u'delete items from library and disk', + help='delete items from library and disk', ) self._command.parser.add_option( - u'-F', u'--full', dest='full', + '-F', '--full', dest='full', action='store_true', - help=u'show all versions of duplicate tracks or albums', + help='show all versions of duplicate tracks or albums', ) self._command.parser.add_option( - u'-s', u'--strict', dest='strict', + '-s', '--strict', dest='strict', action='store_true', - help=u'report duplicates only if all attributes are set', + help='report duplicates only if all attributes are set', ) self._command.parser.add_option( - u'-k', u'--key', dest='keys', + '-k', '--key', dest='keys', action='append', metavar='KEY', - help=u'report duplicates based on keys (use multiple times)', + help='report duplicates based on keys (use multiple times)', ) self._command.parser.add_option( - u'-M', u'--merge', dest='merge', + '-M', '--merge', dest='merge', action='store_true', - help=u'merge duplicate items', + help='merge duplicate items', ) self._command.parser.add_option( - u'-m', u'--move', dest='move', + '-m', '--move', dest='move', action='store', metavar='DEST', - help=u'move items to dest', + help='move items to dest', ) self._command.parser.add_option( - u'-o', u'--copy', dest='copy', + '-o', '--copy', dest='copy', action='store', metavar='DEST', - help=u'copy items to dest', + help='copy items to dest', ) self._command.parser.add_option( - u'-t', u'--tag', dest='tag', + '-t', '--tag', dest='tag', action='store', - help=u'tag matched items with \'k=v\' attribute', + help='tag matched items with \'k=v\' attribute', ) self._command.parser.add_all_common_options() @@ -135,16 +133,21 @@ class DuplicatesPlugin(BeetsPlugin): keys = ['mb_trackid', 'mb_albumid'] items = lib.items(decargs(args)) + # If there's nothing to do, return early. The code below assumes + # `items` to be non-empty. + if not items: + return + if path: - fmt = u'$path' + fmt = '$path' # Default format string for count mode. if count and not fmt: if album: - fmt = u'$albumartist - $album' + fmt = '$albumartist - $album' else: - fmt = u'$albumartist - $album - $title' - fmt += u': {0}' + fmt = '$albumartist - $album - $title' + fmt += ': {0}' if checksum: for i in items: @@ -170,7 +173,7 @@ class DuplicatesPlugin(BeetsPlugin): return [self._command] def _process_item(self, item, copy=False, move=False, delete=False, - tag=False, fmt=u''): + tag=False, fmt=''): """Process Item `item`. """ print_(format(item, fmt)) @@ -187,7 +190,7 @@ class DuplicatesPlugin(BeetsPlugin): k, v = tag.split('=') except Exception: raise UserError( - u"{}: can't parse k=v tag: {}".format(PLUGIN, tag) + f"{PLUGIN}: can't parse k=v tag: {tag}" ) setattr(item, k, v) item.store() @@ -197,25 +200,26 @@ class DuplicatesPlugin(BeetsPlugin): output as flexattr on a key that is the name of the program, and return the key, checksum tuple. """ - args = [p.format(file=item.path) for p in shlex.split(prog)] + args = [p.format(file=decode_commandline_path(item.path)) + for p in shlex.split(prog)] key = args[0] checksum = getattr(item, key, False) if not checksum: - self._log.debug(u'key {0} on item {1} not cached:' - u'computing checksum', + self._log.debug('key {0} on item {1} not cached:' + 'computing checksum', key, displayable_path(item.path)) try: - checksum = command_output(args) + checksum = command_output(args).stdout setattr(item, key, checksum) item.store() - self._log.debug(u'computed checksum for {0} using {1}', + self._log.debug('computed checksum for {0} using {1}', item.title, key) except subprocess.CalledProcessError as e: - self._log.debug(u'failed to checksum {0}: {1}', + self._log.debug('failed to checksum {0}: {1}', displayable_path(item.path), e) else: - self._log.debug(u'key {0} on item {1} cached:' - u'not computing checksum', + self._log.debug('key {0} on item {1} cached:' + 'not computing checksum', key, displayable_path(item.path)) return key, checksum @@ -231,12 +235,12 @@ class DuplicatesPlugin(BeetsPlugin): values = [getattr(obj, k, None) for k in keys] values = [v for v in values if v not in (None, '')] if strict and len(values) < len(keys): - self._log.debug(u'some keys {0} on item {1} are null or empty:' - u' skipping', + self._log.debug('some keys {0} on item {1} are null or empty:' + ' skipping', keys, displayable_path(obj.path)) elif (not strict and not len(values)): - self._log.debug(u'all keys {0} on item {1} are null or empty:' - u' skipping', + self._log.debug('all keys {0} on item {1} are null or empty:' + ' skipping', keys, displayable_path(obj.path)) else: key = tuple(values) @@ -264,7 +268,7 @@ class DuplicatesPlugin(BeetsPlugin): # between a bytes object and the empty Unicode # string ''. return v is not None and \ - (v != '' if isinstance(v, six.text_type) else True) + (v != '' if isinstance(v, str) else True) fields = Item.all_keys() key = lambda x: sum(1 for f in fields if truthy(getattr(x, f))) else: @@ -284,8 +288,8 @@ class DuplicatesPlugin(BeetsPlugin): if getattr(objs[0], f, None) in (None, ''): value = getattr(o, f, None) if value: - self._log.debug(u'key {0} on item {1} is null ' - u'or empty: setting from item {2}', + self._log.debug('key {0} on item {1} is null ' + 'or empty: setting from item {2}', f, displayable_path(objs[0].path), displayable_path(o.path)) setattr(objs[0], f, value) @@ -305,8 +309,8 @@ class DuplicatesPlugin(BeetsPlugin): missing = Item.from_path(i.path) missing.album_id = objs[0].id missing.add(i._db) - self._log.debug(u'item {0} missing from album {1}:' - u' merging from {2} into {3}', + self._log.debug('item {0} missing from album {1}:' + ' merging from {2} into {3}', missing, objs[0], displayable_path(o.path), diff --git a/libs/common/beetsplug/edit.py b/libs/common/beetsplug/edit.py index 631a1b58..6f03fa4d 100644 --- a/libs/common/beetsplug/edit.py +++ b/libs/common/beetsplug/edit.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016 # @@ -15,7 +14,6 @@ """Open metadata information in a text editor to let the user edit it. """ -from __future__ import division, absolute_import, print_function from beets import plugins from beets import util @@ -28,7 +26,7 @@ import subprocess import yaml from tempfile import NamedTemporaryFile import os -import six +import shlex # These "safe" types can avoid the format/parse cycle that most fields go @@ -45,13 +43,13 @@ class ParseError(Exception): def edit(filename, log): """Open `filename` in a text editor. """ - cmd = util.shlex_split(util.editor_command()) + cmd = shlex.split(util.editor_command()) cmd.append(filename) - log.debug(u'invoking editor command: {!r}', cmd) + log.debug('invoking editor command: {!r}', cmd) try: subprocess.call(cmd) except OSError as exc: - raise ui.UserError(u'could not run editor command {!r}: {}'.format( + raise ui.UserError('could not run editor command {!r}: {}'.format( cmd[0], exc )) @@ -74,20 +72,20 @@ def load(s): """ try: out = [] - for d in yaml.load_all(s): + for d in yaml.safe_load_all(s): if not isinstance(d, dict): raise ParseError( - u'each entry must be a dictionary; found {}'.format( + 'each entry must be a dictionary; found {}'.format( type(d).__name__ ) ) # Convert all keys to strings. They started out as strings, # but the user may have inadvertently messed this up. - out.append({six.text_type(k): v for k, v in d.items()}) + out.append({str(k): v for k, v in d.items()}) except yaml.YAMLError as e: - raise ParseError(u'invalid YAML: {}'.format(e)) + raise ParseError(f'invalid YAML: {e}') return out @@ -143,13 +141,13 @@ def apply_(obj, data): else: # Either the field was stringified originally or the user changed # it from a safe type to an unsafe one. Parse it as a string. - obj.set_parse(key, six.text_type(value)) + obj.set_parse(key, str(value)) class EditPlugin(plugins.BeetsPlugin): def __init__(self): - super(EditPlugin, self).__init__() + super().__init__() self.config.add({ # The default fields to edit. @@ -166,18 +164,18 @@ class EditPlugin(plugins.BeetsPlugin): def commands(self): edit_command = ui.Subcommand( 'edit', - help=u'interactively edit metadata' + help='interactively edit metadata' ) edit_command.parser.add_option( - u'-f', u'--field', + '-f', '--field', metavar='FIELD', action='append', - help=u'edit this field also', + help='edit this field also', ) edit_command.parser.add_option( - u'--all', + '--all', action='store_true', dest='all', - help=u'edit all fields', + help='edit all fields', ) edit_command.parser.add_album_option() edit_command.func = self._edit_command @@ -191,7 +189,7 @@ class EditPlugin(plugins.BeetsPlugin): items, albums = _do_query(lib, query, opts.album, False) objs = albums if opts.album else items if not objs: - ui.print_(u'Nothing to edit.') + ui.print_('Nothing to edit.') return # Get the fields to edit. @@ -244,15 +242,10 @@ class EditPlugin(plugins.BeetsPlugin): old_data = [flatten(o, fields) for o in objs] # Set up a temporary file with the initial data for editing. - if six.PY2: - new = NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) - else: - new = NamedTemporaryFile(mode='w', suffix='.yaml', delete=False, - encoding='utf-8') + new = NamedTemporaryFile(mode='w', suffix='.yaml', delete=False, + encoding='utf-8') old_str = dump(old_data) new.write(old_str) - if six.PY2: - old_str = old_str.decode('utf-8') new.close() # Loop until we have parseable data and the user confirms. @@ -266,15 +259,15 @@ class EditPlugin(plugins.BeetsPlugin): with codecs.open(new.name, encoding='utf-8') as f: new_str = f.read() if new_str == old_str: - ui.print_(u"No changes; aborting.") + ui.print_("No changes; aborting.") return False # Parse the updated data. try: new_data = load(new_str) except ParseError as e: - ui.print_(u"Could not read data: {}".format(e)) - if ui.input_yn(u"Edit again to fix? (Y/n)", True): + ui.print_(f"Could not read data: {e}") + if ui.input_yn("Edit again to fix? (Y/n)", True): continue else: return False @@ -289,18 +282,18 @@ class EditPlugin(plugins.BeetsPlugin): for obj, obj_old in zip(objs, objs_old): changed |= ui.show_model_changes(obj, obj_old) if not changed: - ui.print_(u'No changes to apply.') + ui.print_('No changes to apply.') return False # Confirm the changes. choice = ui.input_options( - (u'continue Editing', u'apply', u'cancel') + ('continue Editing', 'apply', 'cancel') ) - if choice == u'a': # Apply. + if choice == 'a': # Apply. return True - elif choice == u'c': # Cancel. + elif choice == 'c': # Cancel. return False - elif choice == u'e': # Keep editing. + elif choice == 'e': # Keep editing. # Reset the temporary changes to the objects. I we have a # copy from above, use that, else reload from the database. objs = [(old_obj or obj) @@ -322,7 +315,7 @@ class EditPlugin(plugins.BeetsPlugin): are temporary. """ if len(old_data) != len(new_data): - self._log.warning(u'number of objects changed from {} to {}', + self._log.warning('number of objects changed from {} to {}', len(old_data), len(new_data)) obj_by_id = {o.id: o for o in objs} @@ -333,7 +326,7 @@ class EditPlugin(plugins.BeetsPlugin): forbidden = False for key in ignore_fields: if old_dict.get(key) != new_dict.get(key): - self._log.warning(u'ignoring object whose {} changed', key) + self._log.warning('ignoring object whose {} changed', key) forbidden = True break if forbidden: @@ -348,7 +341,7 @@ class EditPlugin(plugins.BeetsPlugin): # Save to the database and possibly write tags. for ob in objs: if ob._dirty: - self._log.debug(u'saving changes to {}', ob) + self._log.debug('saving changes to {}', ob) ob.try_sync(ui.should_write(), ui.should_move()) # Methods for interactive importer execution. diff --git a/libs/common/beetsplug/embedart.py b/libs/common/beetsplug/embedart.py index afe8f86f..6db46f8c 100644 --- a/libs/common/beetsplug/embedart.py +++ b/libs/common/beetsplug/embedart.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -14,7 +13,6 @@ # included in all copies or substantial portions of the Software. """Allows beets to embed album art into file metadata.""" -from __future__ import division, absolute_import, print_function import os.path @@ -34,11 +32,11 @@ def _confirm(objs, album): `album` is a Boolean indicating whether these are albums (as opposed to items). """ - noun = u'album' if album else u'file' - prompt = u'Modify artwork for {} {}{} (Y/n)?'.format( + noun = 'album' if album else 'file' + prompt = 'Modify artwork for {} {}{} (Y/n)?'.format( len(objs), noun, - u's' if len(objs) > 1 else u'' + 's' if len(objs) > 1 else '' ) # Show all the items or albums. @@ -53,39 +51,41 @@ class EmbedCoverArtPlugin(BeetsPlugin): """Allows albumart to be embedded into the actual files. """ def __init__(self): - super(EmbedCoverArtPlugin, self).__init__() + super().__init__() self.config.add({ 'maxwidth': 0, 'auto': True, 'compare_threshold': 0, 'ifempty': False, - 'remove_art_file': False + 'remove_art_file': False, + 'quality': 0, }) if self.config['maxwidth'].get(int) and not ArtResizer.shared.local: self.config['maxwidth'] = 0 - self._log.warning(u"ImageMagick or PIL not found; " - u"'maxwidth' option ignored") + self._log.warning("ImageMagick or PIL not found; " + "'maxwidth' option ignored") if self.config['compare_threshold'].get(int) and not \ ArtResizer.shared.can_compare: self.config['compare_threshold'] = 0 - self._log.warning(u"ImageMagick 6.8.7 or higher not installed; " - u"'compare_threshold' option ignored") + self._log.warning("ImageMagick 6.8.7 or higher not installed; " + "'compare_threshold' option ignored") self.register_listener('art_set', self.process_album) def commands(self): # Embed command. embed_cmd = ui.Subcommand( - 'embedart', help=u'embed image files into file metadata' + 'embedart', help='embed image files into file metadata' ) embed_cmd.parser.add_option( - u'-f', u'--file', metavar='PATH', help=u'the image file to embed' + '-f', '--file', metavar='PATH', help='the image file to embed' ) embed_cmd.parser.add_option( - u"-y", u"--yes", action="store_true", help=u"skip confirmation" + "-y", "--yes", action="store_true", help="skip confirmation" ) maxwidth = self.config['maxwidth'].get(int) + quality = self.config['quality'].get(int) compare_threshold = self.config['compare_threshold'].get(int) ifempty = self.config['ifempty'].get(bool) @@ -93,7 +93,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): if opts.file: imagepath = normpath(opts.file) if not os.path.isfile(syspath(imagepath)): - raise ui.UserError(u'image file {0} not found'.format( + raise ui.UserError('image file {} not found'.format( displayable_path(imagepath) )) @@ -104,8 +104,9 @@ class EmbedCoverArtPlugin(BeetsPlugin): return for item in items: - art.embed_item(self._log, item, imagepath, maxwidth, None, - compare_threshold, ifempty) + art.embed_item(self._log, item, imagepath, maxwidth, + None, compare_threshold, ifempty, + quality=quality) else: albums = lib.albums(decargs(args)) @@ -114,8 +115,9 @@ class EmbedCoverArtPlugin(BeetsPlugin): return for album in albums: - art.embed_album(self._log, album, maxwidth, False, - compare_threshold, ifempty) + art.embed_album(self._log, album, maxwidth, + False, compare_threshold, ifempty, + quality=quality) self.remove_artfile(album) embed_cmd.func = embed_func @@ -123,15 +125,15 @@ class EmbedCoverArtPlugin(BeetsPlugin): # Extract command. extract_cmd = ui.Subcommand( 'extractart', - help=u'extract an image from file metadata', + help='extract an image from file metadata', ) extract_cmd.parser.add_option( - u'-o', dest='outpath', - help=u'image output file', + '-o', dest='outpath', + help='image output file', ) extract_cmd.parser.add_option( - u'-n', dest='filename', - help=u'image filename to create for all matched albums', + '-n', dest='filename', + help='image filename to create for all matched albums', ) extract_cmd.parser.add_option( '-a', dest='associate', action='store_true', @@ -147,7 +149,7 @@ class EmbedCoverArtPlugin(BeetsPlugin): config['art_filename'].get()) if os.path.dirname(filename) != b'': self._log.error( - u"Only specify a name rather than a path for -n") + "Only specify a name rather than a path for -n") return for album in lib.albums(decargs(args)): artpath = normpath(os.path.join(album.path, filename)) @@ -161,10 +163,10 @@ class EmbedCoverArtPlugin(BeetsPlugin): # Clear command. clear_cmd = ui.Subcommand( 'clearart', - help=u'remove images from file metadata', + help='remove images from file metadata', ) clear_cmd.parser.add_option( - u"-y", u"--yes", action="store_true", help=u"skip confirmation" + "-y", "--yes", action="store_true", help="skip confirmation" ) def clear_func(lib, opts, args): @@ -189,11 +191,11 @@ class EmbedCoverArtPlugin(BeetsPlugin): def remove_artfile(self, album): """Possibly delete the album art file for an album (if the - appropriate configuration option is enabled. + appropriate configuration option is enabled). """ if self.config['remove_art_file'] and album.artpath: if os.path.isfile(album.artpath): - self._log.debug(u'Removing album art file for {0}', album) + self._log.debug('Removing album art file for {0}', album) os.remove(album.artpath) album.artpath = None album.store() diff --git a/libs/common/beetsplug/embyupdate.py b/libs/common/beetsplug/embyupdate.py index 5c731954..c17fabad 100644 --- a/libs/common/beetsplug/embyupdate.py +++ b/libs/common/beetsplug/embyupdate.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Updates the Emby Library whenever the beets library is changed. emby: @@ -9,14 +7,11 @@ apikey: apikey password: password """ -from __future__ import division, absolute_import, print_function import hashlib import requests -from six.moves.urllib.parse import urlencode -from six.moves.urllib.parse import urljoin, parse_qs, urlsplit, urlunsplit - +from urllib.parse import urlencode, urljoin, parse_qs, urlsplit, urlunsplit from beets import config from beets.plugins import BeetsPlugin @@ -146,14 +141,14 @@ def get_user(host, port, username): class EmbyUpdate(BeetsPlugin): def __init__(self): - super(EmbyUpdate, self).__init__() + super().__init__() # Adding defaults. config['emby'].add({ - u'host': u'http://localhost', - u'port': 8096, - u'apikey': None, - u'password': None, + 'host': 'http://localhost', + 'port': 8096, + 'apikey': None, + 'password': None, }) self.register_listener('database_change', self.listen_for_db_change) @@ -166,7 +161,7 @@ class EmbyUpdate(BeetsPlugin): def update(self, lib): """When the client exists try to send refresh request to Emby. """ - self._log.info(u'Updating Emby library...') + self._log.info('Updating Emby library...') host = config['emby']['host'].get() port = config['emby']['port'].get() @@ -176,13 +171,13 @@ class EmbyUpdate(BeetsPlugin): # Check if at least a apikey or password is given. if not any([password, token]): - self._log.warning(u'Provide at least Emby password or apikey.') + self._log.warning('Provide at least Emby password or apikey.') return # Get user information from the Emby API. user = get_user(host, port, username) if not user: - self._log.warning(u'User {0} could not be found.'.format(username)) + self._log.warning(f'User {username} could not be found.') return if not token: @@ -194,7 +189,7 @@ class EmbyUpdate(BeetsPlugin): token = get_token(host, port, headers, auth_data) if not token: self._log.warning( - u'Could not get token for user {0}', username + 'Could not get token for user {0}', username ) return @@ -205,6 +200,6 @@ class EmbyUpdate(BeetsPlugin): url = api_url(host, port, '/Library/Refresh') r = requests.post(url, headers=headers) if r.status_code != 204: - self._log.warning(u'Update could not be triggered') + self._log.warning('Update could not be triggered') else: - self._log.info(u'Update triggered.') + self._log.info('Update triggered.') diff --git a/libs/common/beetsplug/export.py b/libs/common/beetsplug/export.py index 641b9fef..99f6d706 100644 --- a/libs/common/beetsplug/export.py +++ b/libs/common/beetsplug/export.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # # Permission is hereby granted, free of charge, to any person obtaining @@ -15,23 +14,25 @@ """Exports data from beets """ -from __future__ import division, absolute_import, print_function import sys -import json import codecs +import json +import csv +from xml.etree import ElementTree from datetime import datetime, date from beets.plugins import BeetsPlugin from beets import ui -from beets import mediafile -from beetsplug.info import make_key_filter, library_data, tag_data +from beets import util +import mediafile +from beetsplug.info import library_data, tag_data class ExportEncoder(json.JSONEncoder): """Deals with dates because JSON doesn't have a standard""" def default(self, o): - if isinstance(o, datetime) or isinstance(o, date): + if isinstance(o, (datetime, date)): return o.isoformat() return json.JSONEncoder.default(self, o) @@ -39,12 +40,12 @@ class ExportEncoder(json.JSONEncoder): class ExportPlugin(BeetsPlugin): def __init__(self): - super(ExportPlugin, self).__init__() + super().__init__() self.config.add({ 'default_format': 'json', 'json': { - # json module formatting options + # JSON module formatting options. 'formatting': { 'ensure_ascii': False, 'indent': 4, @@ -52,100 +53,175 @@ class ExportPlugin(BeetsPlugin): 'sort_keys': True } }, + 'jsonlines': { + # JSON Lines formatting options. + 'formatting': { + 'ensure_ascii': False, + 'separators': (',', ': '), + 'sort_keys': True + } + }, + 'csv': { + # CSV module formatting options. + 'formatting': { + # The delimiter used to seperate columns. + 'delimiter': ',', + # The dialect to use when formating the file output. + 'dialect': 'excel' + } + }, + 'xml': { + # XML module formatting options. + 'formatting': {} + } # TODO: Use something like the edit plugin # 'item_fields': [] }) def commands(self): - # TODO: Add option to use albums - - cmd = ui.Subcommand('export', help=u'export data from beets') + cmd = ui.Subcommand('export', help='export data from beets') cmd.func = self.run cmd.parser.add_option( - u'-l', u'--library', action='store_true', - help=u'show library fields instead of tags', + '-l', '--library', action='store_true', + help='show library fields instead of tags', ) cmd.parser.add_option( - u'--append', action='store_true', default=False, - help=u'if should append data to the file', + '-a', '--album', action='store_true', + help='show album fields instead of tracks (implies "--library")', ) cmd.parser.add_option( - u'-i', u'--include-keys', default=[], + '--append', action='store_true', default=False, + help='if should append data to the file', + ) + cmd.parser.add_option( + '-i', '--include-keys', default=[], action='append', dest='included_keys', - help=u'comma separated list of keys to show', + help='comma separated list of keys to show', ) cmd.parser.add_option( - u'-o', u'--output', - help=u'path for the output file. If not given, will print the data' + '-o', '--output', + help='path for the output file. If not given, will print the data' + ) + cmd.parser.add_option( + '-f', '--format', default='json', + help="the output format: json (default), jsonlines, csv, or xml" ) return [cmd] def run(self, lib, opts, args): - file_path = opts.output - file_format = self.config['default_format'].get(str) file_mode = 'a' if opts.append else 'w' + file_format = opts.format or self.config['default_format'].get(str) + file_format_is_line_based = (file_format == 'jsonlines') format_options = self.config[file_format]['formatting'].get(dict) export_format = ExportFormat.factory( - file_format, **{ + file_type=file_format, + **{ 'file_path': file_path, 'file_mode': file_mode } ) - items = [] - data_collector = library_data if opts.library else tag_data + if opts.library or opts.album: + data_collector = library_data + else: + data_collector = tag_data included_keys = [] for keys in opts.included_keys: included_keys.extend(keys.split(',')) - key_filter = make_key_filter(included_keys) - for data_emitter in data_collector(lib, ui.decargs(args)): + items = [] + for data_emitter in data_collector( + lib, ui.decargs(args), + album=opts.album, + ): try: - data, item = data_emitter() - except (mediafile.UnreadableFileError, IOError) as ex: - self._log.error(u'cannot read file: {0}', ex) + data, item = data_emitter(included_keys or '*') + except (mediafile.UnreadableFileError, OSError) as ex: + self._log.error('cannot read file: {0}', ex) continue - data = key_filter(data) - items += [data] + for key, value in data.items(): + if isinstance(value, bytes): + data[key] = util.displayable_path(value) - export_format.export(items, **format_options) - - -class ExportFormat(object): - """The output format type""" - - @classmethod - def factory(cls, type, **kwargs): - if type == "json": - if kwargs['file_path']: - return JsonFileFormat(**kwargs) + if file_format_is_line_based: + export_format.export(data, **format_options) else: - return JsonPrintFormat() - raise NotImplementedError() + items += [data] - def export(self, data, **kwargs): - raise NotImplementedError() + if not file_format_is_line_based: + export_format.export(items, **format_options) -class JsonPrintFormat(ExportFormat): - """Outputs to the console""" - - def export(self, data, **kwargs): - json.dump(data, sys.stdout, cls=ExportEncoder, **kwargs) - - -class JsonFileFormat(ExportFormat): - """Saves in a json file""" - - def __init__(self, file_path, file_mode=u'w', encoding=u'utf-8'): +class ExportFormat: + """The output format type""" + def __init__(self, file_path, file_mode='w', encoding='utf-8'): self.path = file_path self.mode = file_mode self.encoding = encoding + # creates a file object to write/append or sets to stdout + self.out_stream = codecs.open(self.path, self.mode, self.encoding) \ + if self.path else sys.stdout + + @classmethod + def factory(cls, file_type, **kwargs): + if file_type in ["json", "jsonlines"]: + return JsonFormat(**kwargs) + elif file_type == "csv": + return CSVFormat(**kwargs) + elif file_type == "xml": + return XMLFormat(**kwargs) + else: + raise NotImplementedError() def export(self, data, **kwargs): - with codecs.open(self.path, self.mode, self.encoding) as f: - json.dump(data, f, cls=ExportEncoder, **kwargs) + raise NotImplementedError() + + +class JsonFormat(ExportFormat): + """Saves in a json file""" + def __init__(self, file_path, file_mode='w', encoding='utf-8'): + super().__init__(file_path, file_mode, encoding) + + def export(self, data, **kwargs): + json.dump(data, self.out_stream, cls=ExportEncoder, **kwargs) + self.out_stream.write('\n') + + +class CSVFormat(ExportFormat): + """Saves in a csv file""" + def __init__(self, file_path, file_mode='w', encoding='utf-8'): + super().__init__(file_path, file_mode, encoding) + + def export(self, data, **kwargs): + header = list(data[0].keys()) if data else [] + writer = csv.DictWriter(self.out_stream, fieldnames=header, **kwargs) + writer.writeheader() + writer.writerows(data) + + +class XMLFormat(ExportFormat): + """Saves in a xml file""" + def __init__(self, file_path, file_mode='w', encoding='utf-8'): + super().__init__(file_path, file_mode, encoding) + + def export(self, data, **kwargs): + # Creates the XML file structure. + library = ElementTree.Element('library') + tracks = ElementTree.SubElement(library, 'tracks') + if data and isinstance(data[0], dict): + for index, item in enumerate(data): + track = ElementTree.SubElement(tracks, 'track') + for key, value in item.items(): + track_details = ElementTree.SubElement(track, key) + track_details.text = value + # Depending on the version of python the encoding needs to change + try: + data = ElementTree.tostring(library, encoding='unicode', **kwargs) + except LookupError: + data = ElementTree.tostring(library, encoding='utf-8', **kwargs) + + self.out_stream.write(data) diff --git a/libs/common/beetsplug/fetchart.py b/libs/common/beetsplug/fetchart.py index 0e106694..f2c1e5a7 100644 --- a/libs/common/beetsplug/fetchart.py +++ b/libs/common/beetsplug/fetchart.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,12 +14,12 @@ """Fetches album art. """ -from __future__ import division, absolute_import, print_function from contextlib import closing import os import re from tempfile import NamedTemporaryFile +from collections import OrderedDict import requests @@ -29,17 +28,11 @@ from beets import importer from beets import ui from beets import util from beets import config -from beets.mediafile import image_mime_type +from mediafile import image_mime_type from beets.util.artresizer import ArtResizer -from beets.util import confit +from beets.util import sorted_walk from beets.util import syspath, bytestring_path, py3_path -import six - -try: - import itunes - HAVE_ITUNES = True -except ImportError: - HAVE_ITUNES = False +import confuse CONTENT_TYPES = { 'image/jpeg': [b'jpg', b'jpeg'], @@ -48,18 +41,21 @@ CONTENT_TYPES = { IMAGE_EXTENSIONS = [ext for exts in CONTENT_TYPES.values() for ext in exts] -class Candidate(object): +class Candidate: """Holds information about a matching artwork, deals with validation of dimension restrictions and resizing. """ CANDIDATE_BAD = 0 CANDIDATE_EXACT = 1 CANDIDATE_DOWNSCALE = 2 + CANDIDATE_DOWNSIZE = 3 + CANDIDATE_DEINTERLACE = 4 + CANDIDATE_REFORMAT = 5 MATCH_EXACT = 0 MATCH_FALLBACK = 1 - def __init__(self, log, path=None, url=None, source=u'', + def __init__(self, log, path=None, url=None, source='', match=None, size=None): self._log = log self.path = path @@ -75,32 +71,39 @@ class Candidate(object): Return `CANDIDATE_BAD` if the file is unusable. Return `CANDIDATE_EXACT` if the file is usable as-is. - Return `CANDIDATE_DOWNSCALE` if the file must be resized. + Return `CANDIDATE_DOWNSCALE` if the file must be rescaled. + Return `CANDIDATE_DOWNSIZE` if the file must be resized, and possibly + also rescaled. + Return `CANDIDATE_DEINTERLACE` if the file must be deinterlaced. + Return `CANDIDATE_REFORMAT` if the file has to be converted. """ if not self.path: return self.CANDIDATE_BAD - if not (plugin.enforce_ratio or plugin.minwidth or plugin.maxwidth): + if (not (plugin.enforce_ratio or plugin.minwidth or plugin.maxwidth + or plugin.max_filesize or plugin.deinterlace + or plugin.cover_format)): return self.CANDIDATE_EXACT # get_size returns None if no local imaging backend is available if not self.size: self.size = ArtResizer.shared.get_size(self.path) - self._log.debug(u'image size: {}', self.size) + self._log.debug('image size: {}', self.size) if not self.size: - self._log.warning(u'Could not get size of image (please see ' - u'documentation for dependencies). ' - u'The configuration options `minwidth` and ' - u'`enforce_ratio` may be violated.') + self._log.warning('Could not get size of image (please see ' + 'documentation for dependencies). ' + 'The configuration options `minwidth`, ' + '`enforce_ratio` and `max_filesize` ' + 'may be violated.') return self.CANDIDATE_EXACT short_edge = min(self.size) long_edge = max(self.size) - # Check minimum size. + # Check minimum dimension. if plugin.minwidth and self.size[0] < plugin.minwidth: - self._log.debug(u'image too small ({} < {})', + self._log.debug('image too small ({} < {})', self.size[0], plugin.minwidth) return self.CANDIDATE_BAD @@ -109,38 +112,83 @@ class Candidate(object): if plugin.enforce_ratio: if plugin.margin_px: if edge_diff > plugin.margin_px: - self._log.debug(u'image is not close enough to being ' - u'square, ({} - {} > {})', + self._log.debug('image is not close enough to being ' + 'square, ({} - {} > {})', long_edge, short_edge, plugin.margin_px) return self.CANDIDATE_BAD elif plugin.margin_percent: margin_px = plugin.margin_percent * long_edge if edge_diff > margin_px: - self._log.debug(u'image is not close enough to being ' - u'square, ({} - {} > {})', + self._log.debug('image is not close enough to being ' + 'square, ({} - {} > {})', long_edge, short_edge, margin_px) return self.CANDIDATE_BAD elif edge_diff: # also reached for margin_px == 0 and margin_percent == 0.0 - self._log.debug(u'image is not square ({} != {})', + self._log.debug('image is not square ({} != {})', self.size[0], self.size[1]) return self.CANDIDATE_BAD - # Check maximum size. + # Check maximum dimension. + downscale = False if plugin.maxwidth and self.size[0] > plugin.maxwidth: - self._log.debug(u'image needs resizing ({} > {})', + self._log.debug('image needs rescaling ({} > {})', self.size[0], plugin.maxwidth) - return self.CANDIDATE_DOWNSCALE + downscale = True - return self.CANDIDATE_EXACT + # Check filesize. + downsize = False + if plugin.max_filesize: + filesize = os.stat(syspath(self.path)).st_size + if filesize > plugin.max_filesize: + self._log.debug('image needs resizing ({}B > {}B)', + filesize, plugin.max_filesize) + downsize = True + + # Check image format + reformat = False + if plugin.cover_format: + fmt = ArtResizer.shared.get_format(self.path) + reformat = fmt != plugin.cover_format + if reformat: + self._log.debug('image needs reformatting: {} -> {}', + fmt, plugin.cover_format) + + if downscale: + return self.CANDIDATE_DOWNSCALE + elif downsize: + return self.CANDIDATE_DOWNSIZE + elif plugin.deinterlace: + return self.CANDIDATE_DEINTERLACE + elif reformat: + return self.CANDIDATE_REFORMAT + else: + return self.CANDIDATE_EXACT def validate(self, plugin): self.check = self._validate(plugin) return self.check def resize(self, plugin): - if plugin.maxwidth and self.check == self.CANDIDATE_DOWNSCALE: - self.path = ArtResizer.shared.resize(plugin.maxwidth, self.path) + if self.check == self.CANDIDATE_DOWNSCALE: + self.path = \ + ArtResizer.shared.resize(plugin.maxwidth, self.path, + quality=plugin.quality, + max_filesize=plugin.max_filesize) + elif self.check == self.CANDIDATE_DOWNSIZE: + # dimensions are correct, so maxwidth is set to maximum dimension + self.path = \ + ArtResizer.shared.resize(max(self.size), self.path, + quality=plugin.quality, + max_filesize=plugin.max_filesize) + elif self.check == self.CANDIDATE_DEINTERLACE: + self.path = ArtResizer.shared.deinterlace(self.path) + elif self.check == self.CANDIDATE_REFORMAT: + self.path = ArtResizer.shared.reformat( + self.path, + plugin.cover_format, + deinterlaced=plugin.deinterlace, + ) def _logged_get(log, *args, **kwargs): @@ -169,14 +217,19 @@ def _logged_get(log, *args, **kwargs): message = 'getting URL' req = requests.Request('GET', *args, **req_kwargs) + with requests.Session() as s: s.headers = {'User-Agent': 'beets'} prepped = s.prepare_request(req) + settings = s.merge_environment_settings( + prepped.url, {}, None, None, None + ) + send_kwargs.update(settings) log.debug('{}: {}', message, prepped.url) return s.send(prepped, **send_kwargs) -class RequestMixin(object): +class RequestMixin: """Adds a Requests wrapper to the class that uses the logger, which must be named `self._log`. """ @@ -208,10 +261,13 @@ class ArtSource(RequestMixin): def fetch_image(self, candidate, plugin): raise NotImplementedError() + def cleanup(self, candidate): + pass + class LocalArtSource(ArtSource): IS_LOCAL = True - LOC_STR = u'local' + LOC_STR = 'local' def fetch_image(self, candidate, plugin): pass @@ -219,7 +275,7 @@ class LocalArtSource(ArtSource): class RemoteArtSource(ArtSource): IS_LOCAL = False - LOC_STR = u'remote' + LOC_STR = 'remote' def fetch_image(self, candidate, plugin): """Downloads an image from a URL and checks whether it seems to @@ -231,7 +287,7 @@ class RemoteArtSource(ArtSource): candidate.url) try: with closing(self.request(candidate.url, stream=True, - message=u'downloading image')) as resp: + message='downloading image')) as resp: ct = resp.headers.get('Content-Type', None) # Download the image to a temporary file. As some servers @@ -259,16 +315,16 @@ class RemoteArtSource(ArtSource): real_ct = ct if real_ct not in CONTENT_TYPES: - self._log.debug(u'not a supported image: {}', - real_ct or u'unknown content type') + self._log.debug('not a supported image: {}', + real_ct or 'unknown content type') return ext = b'.' + CONTENT_TYPES[real_ct][0] if real_ct != ct: - self._log.warning(u'Server specified {}, but returned a ' - u'{} image. Correcting the extension ' - u'to {}', + self._log.warning('Server specified {}, but returned a ' + '{} image. Correcting the extension ' + 'to {}', ct, real_ct, ext) suffix = py3_path(ext) @@ -278,45 +334,88 @@ class RemoteArtSource(ArtSource): # download the remaining part of the image for chunk in data: fh.write(chunk) - self._log.debug(u'downloaded art to: {0}', + self._log.debug('downloaded art to: {0}', util.displayable_path(fh.name)) candidate.path = util.bytestring_path(fh.name) return - except (IOError, requests.RequestException, TypeError) as exc: + except (OSError, requests.RequestException, TypeError) as exc: # Handling TypeError works around a urllib3 bug: # https://github.com/shazow/urllib3/issues/556 - self._log.debug(u'error fetching art: {}', exc) + self._log.debug('error fetching art: {}', exc) return + def cleanup(self, candidate): + if candidate.path: + try: + util.remove(path=candidate.path) + except util.FilesystemError as exc: + self._log.debug('error cleaning up tmp art: {}', exc) + class CoverArtArchive(RemoteArtSource): - NAME = u"Cover Art Archive" + NAME = "Cover Art Archive" VALID_MATCHING_CRITERIA = ['release', 'releasegroup'] + VALID_THUMBNAIL_SIZES = [250, 500, 1200] - if util.SNI_SUPPORTED: - URL = 'https://coverartarchive.org/release/{mbid}/front' - GROUP_URL = 'https://coverartarchive.org/release-group/{mbid}/front' - else: - URL = 'http://coverartarchive.org/release/{mbid}/front' - GROUP_URL = 'http://coverartarchive.org/release-group/{mbid}/front' + URL = 'https://coverartarchive.org/release/{mbid}' + GROUP_URL = 'https://coverartarchive.org/release-group/{mbid}' def get(self, album, plugin, paths): """Return the Cover Art Archive and Cover Art Archive release group URLs using album MusicBrainz release ID and release group ID. """ + + def get_image_urls(url, size_suffix=None): + try: + response = self.request(url) + except requests.RequestException: + self._log.debug('{}: error receiving response' + .format(self.NAME)) + return + + try: + data = response.json() + except ValueError: + self._log.debug('{}: error loading response: {}' + .format(self.NAME, response.text)) + return + + for item in data.get('images', []): + try: + if 'Front' not in item['types']: + continue + + if size_suffix: + yield item['thumbnails'][size_suffix] + else: + yield item['image'] + except KeyError: + pass + + release_url = self.URL.format(mbid=album.mb_albumid) + release_group_url = self.GROUP_URL.format(mbid=album.mb_releasegroupid) + + # Cover Art Archive API offers pre-resized thumbnails at several sizes. + # If the maxwidth config matches one of the already available sizes + # fetch it directly intead of fetching the full sized image and + # resizing it. + size_suffix = None + if plugin.maxwidth in self.VALID_THUMBNAIL_SIZES: + size_suffix = "-" + str(plugin.maxwidth) + if 'release' in self.match_by and album.mb_albumid: - yield self._candidate(url=self.URL.format(mbid=album.mb_albumid), - match=Candidate.MATCH_EXACT) + for url in get_image_urls(release_url, size_suffix): + yield self._candidate(url=url, match=Candidate.MATCH_EXACT) + if 'releasegroup' in self.match_by and album.mb_releasegroupid: - yield self._candidate( - url=self.GROUP_URL.format(mbid=album.mb_releasegroupid), - match=Candidate.MATCH_FALLBACK) + for url in get_image_urls(release_group_url): + yield self._candidate(url=url, match=Candidate.MATCH_FALLBACK) class Amazon(RemoteArtSource): - NAME = u"Amazon" - URL = 'http://images.amazon.com/images/P/%s.%02i.LZZZZZZZ.jpg' + NAME = "Amazon" + URL = 'https://images.amazon.com/images/P/%s.%02i.LZZZZZZZ.jpg' INDICES = (1, 2) def get(self, album, plugin, paths): @@ -329,8 +428,8 @@ class Amazon(RemoteArtSource): class AlbumArtOrg(RemoteArtSource): - NAME = u"AlbumArt.org scraper" - URL = 'http://www.albumart.org/index_detail.php' + NAME = "AlbumArt.org scraper" + URL = 'https://www.albumart.org/index_detail.php' PAT = r'href\s*=\s*"([^>"]*)"[^>]*title\s*=\s*"View larger image"' def get(self, album, plugin, paths): @@ -341,9 +440,9 @@ class AlbumArtOrg(RemoteArtSource): # Get the page from albumart.org. try: resp = self.request(self.URL, params={'asin': album.asin}) - self._log.debug(u'scraped art URL: {0}', resp.url) + self._log.debug('scraped art URL: {0}', resp.url) except requests.RequestException: - self._log.debug(u'error scraping art page') + self._log.debug('error scraping art page') return # Search the page for the image URL. @@ -352,15 +451,15 @@ class AlbumArtOrg(RemoteArtSource): image_url = m.group(1) yield self._candidate(url=image_url, match=Candidate.MATCH_EXACT) else: - self._log.debug(u'no image found on page') + self._log.debug('no image found on page') class GoogleImages(RemoteArtSource): - NAME = u"Google Images" - URL = u'https://www.googleapis.com/customsearch/v1' + NAME = "Google Images" + URL = 'https://www.googleapis.com/customsearch/v1' def __init__(self, *args, **kwargs): - super(GoogleImages, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.key = self._config['google_key'].get(), self.cx = self._config['google_engine'].get(), @@ -371,24 +470,29 @@ class GoogleImages(RemoteArtSource): if not (album.albumartist and album.album): return search_string = (album.albumartist + ',' + album.album).encode('utf-8') - response = self.request(self.URL, params={ - 'key': self.key, - 'cx': self.cx, - 'q': search_string, - 'searchType': 'image' - }) + + try: + response = self.request(self.URL, params={ + 'key': self.key, + 'cx': self.cx, + 'q': search_string, + 'searchType': 'image' + }) + except requests.RequestException: + self._log.debug('google: error receiving response') + return # Get results using JSON. try: data = response.json() except ValueError: - self._log.debug(u'google: error loading response: {}' + self._log.debug('google: error loading response: {}' .format(response.text)) return if 'error' in data: reason = data['error']['errors'][0]['reason'] - self._log.debug(u'google fetchart error: {0}', reason) + self._log.debug('google fetchart error: {0}', reason) return if 'items' in data.keys(): @@ -399,103 +503,142 @@ class GoogleImages(RemoteArtSource): class FanartTV(RemoteArtSource): """Art from fanart.tv requested using their API""" - NAME = u"fanart.tv" + NAME = "fanart.tv" API_URL = 'https://webservice.fanart.tv/v3/' API_ALBUMS = API_URL + 'music/albums/' PROJECT_KEY = '61a7d0ab4e67162b7a0c7c35915cd48e' def __init__(self, *args, **kwargs): - super(FanartTV, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.client_key = self._config['fanarttv_key'].get() def get(self, album, plugin, paths): if not album.mb_releasegroupid: return - response = self.request( - self.API_ALBUMS + album.mb_releasegroupid, - headers={'api-key': self.PROJECT_KEY, - 'client-key': self.client_key}) + try: + response = self.request( + self.API_ALBUMS + album.mb_releasegroupid, + headers={'api-key': self.PROJECT_KEY, + 'client-key': self.client_key}) + except requests.RequestException: + self._log.debug('fanart.tv: error receiving response') + return try: data = response.json() except ValueError: - self._log.debug(u'fanart.tv: error loading response: {}', + self._log.debug('fanart.tv: error loading response: {}', response.text) return - if u'status' in data and data[u'status'] == u'error': - if u'not found' in data[u'error message'].lower(): - self._log.debug(u'fanart.tv: no image found') - elif u'api key' in data[u'error message'].lower(): - self._log.warning(u'fanart.tv: Invalid API key given, please ' - u'enter a valid one in your config file.') + if 'status' in data and data['status'] == 'error': + if 'not found' in data['error message'].lower(): + self._log.debug('fanart.tv: no image found') + elif 'api key' in data['error message'].lower(): + self._log.warning('fanart.tv: Invalid API key given, please ' + 'enter a valid one in your config file.') else: - self._log.debug(u'fanart.tv: error on request: {}', - data[u'error message']) + self._log.debug('fanart.tv: error on request: {}', + data['error message']) return matches = [] # can there be more than one releasegroupid per response? - for mbid, art in data.get(u'albums', dict()).items(): + for mbid, art in data.get('albums', {}).items(): # there might be more art referenced, e.g. cdart, and an albumcover - # might not be present, even if the request was succesful - if album.mb_releasegroupid == mbid and u'albumcover' in art: - matches.extend(art[u'albumcover']) + # might not be present, even if the request was successful + if album.mb_releasegroupid == mbid and 'albumcover' in art: + matches.extend(art['albumcover']) # can this actually occur? else: - self._log.debug(u'fanart.tv: unexpected mb_releasegroupid in ' - u'response!') + self._log.debug('fanart.tv: unexpected mb_releasegroupid in ' + 'response!') - matches.sort(key=lambda x: x[u'likes'], reverse=True) + matches.sort(key=lambda x: x['likes'], reverse=True) for item in matches: # fanart.tv has a strict size requirement for album art to be # uploaded - yield self._candidate(url=item[u'url'], + yield self._candidate(url=item['url'], match=Candidate.MATCH_EXACT, size=(1000, 1000)) class ITunesStore(RemoteArtSource): - NAME = u"iTunes Store" + NAME = "iTunes Store" + API_URL = 'https://itunes.apple.com/search' def get(self, album, plugin, paths): """Return art URL from iTunes Store given an album title. """ if not (album.albumartist and album.album): return - search_string = (album.albumartist + ' ' + album.album).encode('utf-8') + + payload = { + 'term': album.albumartist + ' ' + album.album, + 'entity': 'album', + 'media': 'music', + 'limit': 200 + } try: - # Isolate bugs in the iTunes library while searching. + r = self.request(self.API_URL, params=payload) + r.raise_for_status() + except requests.RequestException as e: + self._log.debug('iTunes search failed: {0}', e) + return + + try: + candidates = r.json()['results'] + except ValueError as e: + self._log.debug('Could not decode json response: {0}', e) + return + except KeyError as e: + self._log.debug('{} not found in json. Fields are {} ', + e, + list(r.json().keys())) + return + + if not candidates: + self._log.debug('iTunes search for {!r} got no results', + payload['term']) + return + + if self._config['high_resolution']: + image_suffix = '100000x100000-999' + else: + image_suffix = '1200x1200bb' + + for c in candidates: try: - results = itunes.search_album(search_string) - except Exception as exc: - self._log.debug(u'iTunes search failed: {0}', exc) - return + if (c['artistName'] == album.albumartist + and c['collectionName'] == album.album): + art_url = c['artworkUrl100'] + art_url = art_url.replace('100x100bb', + image_suffix) + yield self._candidate(url=art_url, + match=Candidate.MATCH_EXACT) + except KeyError as e: + self._log.debug('Malformed itunes candidate: {} not found in {}', # NOQA E501 + e, + list(c.keys())) - # Get the first match. - if results: - itunes_album = results[0] - else: - self._log.debug(u'iTunes search for {:r} got no results', - search_string) - return - - if itunes_album.get_artwork()['100']: - small_url = itunes_album.get_artwork()['100'] - big_url = small_url.replace('100x100', '1200x1200') - yield self._candidate(url=big_url, match=Candidate.MATCH_EXACT) - else: - self._log.debug(u'album has no artwork in iTunes Store') - except IndexError: - self._log.debug(u'album not found in iTunes Store') + try: + fallback_art_url = candidates[0]['artworkUrl100'] + fallback_art_url = fallback_art_url.replace('100x100bb', + image_suffix) + yield self._candidate(url=fallback_art_url, + match=Candidate.MATCH_FALLBACK) + except KeyError as e: + self._log.debug('Malformed itunes candidate: {} not found in {}', + e, + list(c.keys())) class Wikipedia(RemoteArtSource): - NAME = u"Wikipedia (queried through DBpedia)" + NAME = "Wikipedia (queried through DBpedia)" DBPEDIA_URL = 'https://dbpedia.org/sparql' WIKIPEDIA_URL = 'https://en.wikipedia.org/w/api.php' - SPARQL_QUERY = u'''PREFIX rdf: + SPARQL_QUERY = '''PREFIX rdf: PREFIX dbpprop: PREFIX owl: PREFIX rdfs: @@ -523,16 +666,22 @@ class Wikipedia(RemoteArtSource): # Find the name of the cover art filename on DBpedia cover_filename, page_id = None, None - dbpedia_response = self.request( - self.DBPEDIA_URL, - params={ - 'format': 'application/sparql-results+json', - 'timeout': 2500, - 'query': self.SPARQL_QUERY.format( - artist=album.albumartist.title(), album=album.album) - }, - headers={'content-type': 'application/json'}, - ) + + try: + dbpedia_response = self.request( + self.DBPEDIA_URL, + params={ + 'format': 'application/sparql-results+json', + 'timeout': 2500, + 'query': self.SPARQL_QUERY.format( + artist=album.albumartist.title(), album=album.album) + }, + headers={'content-type': 'application/json'}, + ) + except requests.RequestException: + self._log.debug('dbpedia: error receiving response') + return + try: data = dbpedia_response.json() results = data['results']['bindings'] @@ -540,9 +689,9 @@ class Wikipedia(RemoteArtSource): cover_filename = 'File:' + results[0]['coverFilename']['value'] page_id = results[0]['pageId']['value'] else: - self._log.debug(u'wikipedia: album not found on dbpedia') + self._log.debug('wikipedia: album not found on dbpedia') except (ValueError, KeyError, IndexError): - self._log.debug(u'wikipedia: error scraping dbpedia response: {}', + self._log.debug('wikipedia: error scraping dbpedia response: {}', dbpedia_response.text) # Ensure we have a filename before attempting to query wikipedia @@ -557,25 +706,29 @@ class Wikipedia(RemoteArtSource): if ' .' in cover_filename and \ '.' not in cover_filename.split(' .')[-1]: self._log.debug( - u'wikipedia: dbpedia provided incomplete cover_filename' + 'wikipedia: dbpedia provided incomplete cover_filename' ) lpart, rpart = cover_filename.rsplit(' .', 1) # Query all the images in the page - wikipedia_response = self.request( - self.WIKIPEDIA_URL, - params={ - 'format': 'json', - 'action': 'query', - 'continue': '', - 'prop': 'images', - 'pageids': page_id, - }, - headers={'content-type': 'application/json'}, - ) + try: + wikipedia_response = self.request( + self.WIKIPEDIA_URL, + params={ + 'format': 'json', + 'action': 'query', + 'continue': '', + 'prop': 'images', + 'pageids': page_id, + }, + headers={'content-type': 'application/json'}, + ) + except requests.RequestException: + self._log.debug('wikipedia: error receiving response') + return # Try to see if one of the images on the pages matches our - # imcomplete cover_filename + # incomplete cover_filename try: data = wikipedia_response.json() results = data['query']['pages'][page_id]['images'] @@ -586,23 +739,27 @@ class Wikipedia(RemoteArtSource): break except (ValueError, KeyError): self._log.debug( - u'wikipedia: failed to retrieve a cover_filename' + 'wikipedia: failed to retrieve a cover_filename' ) return # Find the absolute url of the cover art on Wikipedia - wikipedia_response = self.request( - self.WIKIPEDIA_URL, - params={ - 'format': 'json', - 'action': 'query', - 'continue': '', - 'prop': 'imageinfo', - 'iiprop': 'url', - 'titles': cover_filename.encode('utf-8'), - }, - headers={'content-type': 'application/json'}, - ) + try: + wikipedia_response = self.request( + self.WIKIPEDIA_URL, + params={ + 'format': 'json', + 'action': 'query', + 'continue': '', + 'prop': 'imageinfo', + 'iiprop': 'url', + 'titles': cover_filename.encode('utf-8'), + }, + headers={'content-type': 'application/json'}, + ) + except requests.RequestException: + self._log.debug('wikipedia: error receiving response') + return try: data = wikipedia_response.json() @@ -612,12 +769,12 @@ class Wikipedia(RemoteArtSource): yield self._candidate(url=image_url, match=Candidate.MATCH_EXACT) except (ValueError, KeyError, IndexError): - self._log.debug(u'wikipedia: error scraping imageinfo') + self._log.debug('wikipedia: error scraping imageinfo') return class FileSystem(LocalArtSource): - NAME = u"Filesystem" + NAME = "Filesystem" @staticmethod def filename_priority(filename, cover_names): @@ -644,12 +801,16 @@ class FileSystem(LocalArtSource): # Find all files that look like images in the directory. images = [] - for fn in os.listdir(syspath(path)): - fn = bytestring_path(fn) - for ext in IMAGE_EXTENSIONS: - if fn.lower().endswith(b'.' + ext) and \ - os.path.isfile(syspath(os.path.join(path, fn))): - images.append(fn) + ignore = config['ignore'].as_str_seq() + ignore_hidden = config['ignore_hidden'].get(bool) + for _, _, files in sorted_walk(path, ignore=ignore, + ignore_hidden=ignore_hidden): + for fn in files: + fn = bytestring_path(fn) + for ext in IMAGE_EXTENSIONS: + if fn.lower().endswith(b'.' + ext) and \ + os.path.isfile(syspath(os.path.join(path, fn))): + images.append(fn) # Look for "preferred" filenames. images = sorted(images, @@ -658,7 +819,7 @@ class FileSystem(LocalArtSource): remaining = [] for fn in images: if re.search(cover_pat, os.path.splitext(fn)[0], re.I): - self._log.debug(u'using well-named art file {0}', + self._log.debug('using well-named art file {0}', util.displayable_path(fn)) yield self._candidate(path=os.path.join(path, fn), match=Candidate.MATCH_EXACT) @@ -667,27 +828,86 @@ class FileSystem(LocalArtSource): # Fall back to any image in the folder. if remaining and not plugin.cautious: - self._log.debug(u'using fallback art file {0}', + self._log.debug('using fallback art file {0}', util.displayable_path(remaining[0])) yield self._candidate(path=os.path.join(path, remaining[0]), match=Candidate.MATCH_FALLBACK) +class LastFM(RemoteArtSource): + NAME = "Last.fm" + + # Sizes in priority order. + SIZES = OrderedDict([ + ('mega', (300, 300)), + ('extralarge', (300, 300)), + ('large', (174, 174)), + ('medium', (64, 64)), + ('small', (34, 34)), + ]) + + API_URL = 'https://ws.audioscrobbler.com/2.0' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.key = self._config['lastfm_key'].get(), + + def get(self, album, plugin, paths): + if not album.mb_albumid: + return + + try: + response = self.request(self.API_URL, params={ + 'method': 'album.getinfo', + 'api_key': self.key, + 'mbid': album.mb_albumid, + 'format': 'json', + }) + except requests.RequestException: + self._log.debug('lastfm: error receiving response') + return + + try: + data = response.json() + + if 'error' in data: + if data['error'] == 6: + self._log.debug('lastfm: no results for {}', + album.mb_albumid) + else: + self._log.error( + 'lastfm: failed to get album info: {} ({})', + data['message'], data['error']) + else: + images = {image['size']: image['#text'] + for image in data['album']['image']} + + # Provide candidates in order of size. + for size in self.SIZES.keys(): + if size in images: + yield self._candidate(url=images[size], + size=self.SIZES[size]) + except ValueError: + self._log.debug('lastfm: error loading response: {}' + .format(response.text)) + return + # Try each source in turn. -SOURCES_ALL = [u'filesystem', - u'coverart', u'itunes', u'amazon', u'albumart', - u'wikipedia', u'google', u'fanarttv'] +SOURCES_ALL = ['filesystem', + 'coverart', 'itunes', 'amazon', 'albumart', + 'wikipedia', 'google', 'fanarttv', 'lastfm'] ART_SOURCES = { - u'filesystem': FileSystem, - u'coverart': CoverArtArchive, - u'itunes': ITunesStore, - u'albumart': AlbumArtOrg, - u'amazon': Amazon, - u'wikipedia': Wikipedia, - u'google': GoogleImages, - u'fanarttv': FanartTV, + 'filesystem': FileSystem, + 'coverart': CoverArtArchive, + 'itunes': ITunesStore, + 'albumart': AlbumArtOrg, + 'amazon': Amazon, + 'wikipedia': Wikipedia, + 'google': GoogleImages, + 'fanarttv': FanartTV, + 'lastfm': LastFM, } SOURCE_NAMES = {v: k for k, v in ART_SOURCES.items()} @@ -699,7 +919,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): PAT_PERCENT = r"(100(\.00?)?|[1-9]?[0-9](\.[0-9]{1,2})?)%" def __init__(self): - super(FetchArtPlugin, self).__init__() + super().__init__() # Holds candidates corresponding to downloaded images between # fetching them and placing them in the filesystem. @@ -709,37 +929,47 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): 'auto': True, 'minwidth': 0, 'maxwidth': 0, + 'quality': 0, + 'max_filesize': 0, 'enforce_ratio': False, 'cautious': False, 'cover_names': ['cover', 'front', 'art', 'album', 'folder'], 'sources': ['filesystem', 'coverart', 'itunes', 'amazon', 'albumart'], 'google_key': None, - 'google_engine': u'001442825323518660753:hrh5ch1gjzm', + 'google_engine': '001442825323518660753:hrh5ch1gjzm', 'fanarttv_key': None, + 'lastfm_key': None, 'store_source': False, + 'high_resolution': False, + 'deinterlace': False, + 'cover_format': None, }) self.config['google_key'].redact = True self.config['fanarttv_key'].redact = True + self.config['lastfm_key'].redact = True self.minwidth = self.config['minwidth'].get(int) self.maxwidth = self.config['maxwidth'].get(int) + self.max_filesize = self.config['max_filesize'].get(int) + self.quality = self.config['quality'].get(int) # allow both pixel and percentage-based margin specifications self.enforce_ratio = self.config['enforce_ratio'].get( - confit.OneOf([bool, - confit.String(pattern=self.PAT_PX), - confit.String(pattern=self.PAT_PERCENT)])) + confuse.OneOf([bool, + confuse.String(pattern=self.PAT_PX), + confuse.String(pattern=self.PAT_PERCENT)])) self.margin_px = None self.margin_percent = None - if type(self.enforce_ratio) is six.text_type: - if self.enforce_ratio[-1] == u'%': + self.deinterlace = self.config['deinterlace'].get(bool) + if type(self.enforce_ratio) is str: + if self.enforce_ratio[-1] == '%': self.margin_percent = float(self.enforce_ratio[:-1]) / 100 - elif self.enforce_ratio[-2:] == u'px': + elif self.enforce_ratio[-2:] == 'px': self.margin_px = int(self.enforce_ratio[:-2]) else: # shouldn't happen - raise confit.ConfigValueError() + raise confuse.ConfigValueError() self.enforce_ratio = True cover_names = self.config['cover_names'].as_str_seq() @@ -750,17 +980,22 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): self.src_removed = (config['import']['delete'].get(bool) or config['import']['move'].get(bool)) + self.cover_format = self.config['cover_format'].get( + confuse.Optional(str) + ) + if self.config['auto']: # Enable two import hooks when fetching is enabled. self.import_stages = [self.fetch_art] self.register_listener('import_task_files', self.assign_art) available_sources = list(SOURCES_ALL) - if not HAVE_ITUNES and u'itunes' in available_sources: - available_sources.remove(u'itunes') if not self.config['google_key'].get() and \ - u'google' in available_sources: - available_sources.remove(u'google') + 'google' in available_sources: + available_sources.remove('google') + if not self.config['lastfm_key'].get() and \ + 'lastfm' in available_sources: + available_sources.remove('lastfm') available_sources = [(s, c) for s in available_sources for c in ART_SOURCES[s].VALID_MATCHING_CRITERIA] @@ -770,9 +1005,9 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): if 'remote_priority' in self.config: self._log.warning( - u'The `fetch_art.remote_priority` configuration option has ' - u'been deprecated. Instead, place `filesystem` at the end of ' - u'your `sources` list.') + 'The `fetch_art.remote_priority` configuration option has ' + 'been deprecated. Instead, place `filesystem` at the end of ' + 'your `sources` list.') if self.config['remote_priority'].get(bool): fs = [] others = [] @@ -814,7 +1049,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): if self.store_source: # store the source of the chosen artwork in a flexible field self._log.debug( - u"Storing art_source for {0.albumartist} - {0.album}", + "Storing art_source for {0.albumartist} - {0.album}", album) album.art_source = SOURCE_NAMES[type(candidate.source)] album.store() @@ -834,14 +1069,14 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): def commands(self): cmd = ui.Subcommand('fetchart', help='download album art') cmd.parser.add_option( - u'-f', u'--force', dest='force', + '-f', '--force', dest='force', action='store_true', default=False, - help=u're-download art when already present' + help='re-download art when already present' ) cmd.parser.add_option( - u'-q', u'--quiet', dest='quiet', + '-q', '--quiet', dest='quiet', action='store_true', default=False, - help=u'shows only quiet art' + help='quiet mode: do not output albums that already have artwork' ) def func(lib, opts, args): @@ -855,16 +1090,17 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): def art_for_album(self, album, paths, local_only=False): """Given an Album object, returns a path to downloaded art for the album (or None if no art is found). If `maxwidth`, then images are - resized to this maximum pixel size. If `local_only`, then only local - image files from the filesystem are returned; no network requests - are made. + resized to this maximum pixel size. If `quality` then resized images + are saved at the specified quality level. If `local_only`, then only + local image files from the filesystem are returned; no network + requests are made. """ out = None for source in self.sources: if source.IS_LOCAL or not local_only: self._log.debug( - u'trying source {0} for album {1.albumartist} - {1.album}', + 'trying source {0} for album {1.albumartist} - {1.album}', SOURCE_NAMES[type(source)], album, ) @@ -875,9 +1111,11 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): if candidate.validate(self): out = candidate self._log.debug( - u'using {0.LOC_STR} image {1}'.format( + 'using {0.LOC_STR} image {1}'.format( source, util.displayable_path(out.path))) break + # Remove temporary files for invalid candidates. + source.cleanup(candidate) if out: break @@ -894,8 +1132,8 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): if album.artpath and not force and os.path.isfile(album.artpath): if not quiet: message = ui.colorize('text_highlight_minor', - u'has album art') - self._log.info(u'{0}: {1}', album, message) + 'has album art') + self._log.info('{0}: {1}', album, message) else: # In ordinary invocations, look for images on the # filesystem. When forcing, however, always go to the Web @@ -905,7 +1143,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): candidate = self.art_for_album(album, local_paths) if candidate: self._set_art(album, candidate) - message = ui.colorize('text_success', u'found album art') + message = ui.colorize('text_success', 'found album art') else: - message = ui.colorize('text_error', u'no art found') - self._log.info(u'{0}: {1}', album, message) + message = ui.colorize('text_error', 'no art found') + self._log.info('{0}: {1}', album, message) diff --git a/libs/common/beetsplug/filefilter.py b/libs/common/beetsplug/filefilter.py index 23dac574..ec8fddb4 100644 --- a/libs/common/beetsplug/filefilter.py +++ b/libs/common/beetsplug/filefilter.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Malte Ried. # @@ -16,7 +15,6 @@ """Filter imported files using a regular expression. """ -from __future__ import division, absolute_import, print_function import re from beets import config @@ -27,7 +25,7 @@ from beets.importer import SingletonImportTask class FileFilterPlugin(BeetsPlugin): def __init__(self): - super(FileFilterPlugin, self).__init__() + super().__init__() self.register_listener('import_task_created', self.import_task_created_event) self.config.add({ @@ -43,8 +41,8 @@ class FileFilterPlugin(BeetsPlugin): bytestring_path(self.config['album_path'].get())) if 'singleton_path' in self.config: - self.path_singleton_regex = re.compile( - bytestring_path(self.config['singleton_path'].get())) + self.path_singleton_regex = re.compile( + bytestring_path(self.config['singleton_path'].get())) def import_task_created_event(self, session, task): if task.items and len(task.items) > 0: diff --git a/libs/common/beetsplug/fish.py b/libs/common/beetsplug/fish.py new file mode 100644 index 00000000..21fd67f6 --- /dev/null +++ b/libs/common/beetsplug/fish.py @@ -0,0 +1,285 @@ +# This file is part of beets. +# Copyright 2015, winters jean-marie. +# Copyright 2020, Justin Mayer +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""This plugin generates tab completions for Beets commands for the Fish shell +, including completions for Beets commands, plugin +commands, and option flags. Also generated are completions for all the album +and track fields, suggesting for example `genre:` or `album:` when querying the +Beets database. Completions for the *values* of those fields are not generated +by default but can be added via the `-e` / `--extravalues` flag. For example: +`beet fish -e genre -e albumartist` +""" + + +from beets.plugins import BeetsPlugin +from beets import library, ui +from beets.ui import commands +from operator import attrgetter +import os +BL_NEED2 = """complete -c beet -n '__fish_beet_needs_command' {} {}\n""" +BL_USE3 = """complete -c beet -n '__fish_beet_using_command {}' {} {}\n""" +BL_SUBS = """complete -c beet -n '__fish_at_level {} ""' {} {}\n""" +BL_EXTRA3 = """complete -c beet -n '__fish_beet_use_extra {}' {} {}\n""" + +HEAD = ''' +function __fish_beet_needs_command + set cmd (commandline -opc) + if test (count $cmd) -eq 1 + return 0 + end + return 1 +end + +function __fish_beet_using_command + set cmd (commandline -opc) + set needle (count $cmd) + if test $needle -gt 1 + if begin test $argv[1] = $cmd[2]; + and not contains -- $cmd[$needle] $FIELDS; end + return 0 + end + end + return 1 +end + +function __fish_beet_use_extra + set cmd (commandline -opc) + set needle (count $cmd) + if test $argv[2] = $cmd[$needle] + return 0 + end + return 1 +end +''' + + +class FishPlugin(BeetsPlugin): + + def commands(self): + cmd = ui.Subcommand('fish', help='generate Fish shell tab completions') + cmd.func = self.run + cmd.parser.add_option('-f', '--noFields', action='store_true', + default=False, + help='omit album/track field completions') + cmd.parser.add_option( + '-e', + '--extravalues', + action='append', + type='choice', + choices=library.Item.all_keys() + + library.Album.all_keys(), + help='include specified field *values* in completions') + return [cmd] + + def run(self, lib, opts, args): + # Gather the commands from Beets core and its plugins. + # Collect the album and track fields. + # If specified, also collect the values for these fields. + # Make a giant string of all the above, formatted in a way that + # allows Fish to do tab completion for the `beet` command. + home_dir = os.path.expanduser("~") + completion_dir = os.path.join(home_dir, '.config/fish/completions') + try: + os.makedirs(completion_dir) + except OSError: + if not os.path.isdir(completion_dir): + raise + completion_file_path = os.path.join(completion_dir, 'beet.fish') + nobasicfields = opts.noFields # Do not complete for album/track fields + extravalues = opts.extravalues # e.g., Also complete artists names + beetcmds = sorted( + (commands.default_commands + + commands.plugins.commands()), + key=attrgetter('name')) + fields = sorted(set( + library.Album.all_keys() + library.Item.all_keys())) + # Collect commands, their aliases, and their help text + cmd_names_help = [] + for cmd in beetcmds: + names = list(cmd.aliases) + names.append(cmd.name) + for name in names: + cmd_names_help.append((name, cmd.help)) + # Concatenate the string + totstring = HEAD + "\n" + totstring += get_cmds_list([name[0] for name in cmd_names_help]) + totstring += '' if nobasicfields else get_standard_fields(fields) + totstring += get_extravalues(lib, extravalues) if extravalues else '' + totstring += "\n" + "# ====== {} =====".format( + "setup basic beet completion") + "\n" * 2 + totstring += get_basic_beet_options() + totstring += "\n" + "# ====== {} =====".format( + "setup field completion for subcommands") + "\n" + totstring += get_subcommands( + cmd_names_help, nobasicfields, extravalues) + # Set up completion for all the command options + totstring += get_all_commands(beetcmds) + + with open(completion_file_path, 'w') as fish_file: + fish_file.write(totstring) + + +def _escape(name): + # Escape ? in fish + if name == "?": + name = "\\" + name + return name + + +def get_cmds_list(cmds_names): + # Make a list of all Beets core & plugin commands + substr = '' + substr += ( + "set CMDS " + " ".join(cmds_names) + ("\n" * 2) + ) + return substr + + +def get_standard_fields(fields): + # Make a list of album/track fields and append with ':' + fields = (field + ":" for field in fields) + substr = '' + substr += ( + "set FIELDS " + " ".join(fields) + ("\n" * 2) + ) + return substr + + +def get_extravalues(lib, extravalues): + # Make a list of all values from an album/track field. + # 'beet ls albumartist: ' yields completions for ABBA, Beatles, etc. + word = '' + values_set = get_set_of_values_for_field(lib, extravalues) + for fld in extravalues: + extraname = fld.upper() + 'S' + word += ( + "set " + extraname + " " + " ".join(sorted(values_set[fld])) + + ("\n" * 2) + ) + return word + + +def get_set_of_values_for_field(lib, fields): + # Get unique values from a specified album/track field + fields_dict = {} + for each in fields: + fields_dict[each] = set() + for item in lib.items(): + for field in fields: + fields_dict[field].add(wrap(item[field])) + return fields_dict + + +def get_basic_beet_options(): + word = ( + BL_NEED2.format("-l format-item", + "-f -d 'print with custom format'") + + BL_NEED2.format("-l format-album", + "-f -d 'print with custom format'") + + BL_NEED2.format("-s l -l library", + "-f -r -d 'library database file to use'") + + BL_NEED2.format("-s d -l directory", + "-f -r -d 'destination music directory'") + + BL_NEED2.format("-s v -l verbose", + "-f -d 'print debugging information'") + + + BL_NEED2.format("-s c -l config", + "-f -r -d 'path to configuration file'") + + BL_NEED2.format("-s h -l help", + "-f -d 'print this help message and exit'")) + return word + + +def get_subcommands(cmd_name_and_help, nobasicfields, extravalues): + # Formatting for Fish to complete our fields/values + word = "" + for cmdname, cmdhelp in cmd_name_and_help: + cmdname = _escape(cmdname) + + word += "\n" + "# ------ {} -------".format( + "fieldsetups for " + cmdname) + "\n" + word += ( + BL_NEED2.format( + ("-a " + cmdname), + ("-f " + "-d " + wrap(clean_whitespace(cmdhelp))))) + + if nobasicfields is False: + word += ( + BL_USE3.format( + cmdname, + ("-a " + wrap("$FIELDS")), + ("-f " + "-d " + wrap("fieldname")))) + + if extravalues: + for f in extravalues: + setvar = wrap("$" + f.upper() + "S") + word += " ".join(BL_EXTRA3.format( + (cmdname + " " + f + ":"), + ('-f ' + '-A ' + '-a ' + setvar), + ('-d ' + wrap(f))).split()) + "\n" + return word + + +def get_all_commands(beetcmds): + # Formatting for Fish to complete command options + word = "" + for cmd in beetcmds: + names = list(cmd.aliases) + names.append(cmd.name) + for name in names: + name = _escape(name) + + word += "\n" + word += ("\n" * 2) + "# ====== {} =====".format( + "completions for " + name) + "\n" + + for option in cmd.parser._get_all_options()[1:]: + cmd_l = (" -l " + option._long_opts[0].replace('--', '') + )if option._long_opts else '' + cmd_s = (" -s " + option._short_opts[0].replace('-', '') + ) if option._short_opts else '' + cmd_need_arg = ' -r ' if option.nargs in [1] else '' + cmd_helpstr = (" -d " + wrap(' '.join(option.help.split())) + ) if option.help else '' + cmd_arglist = (' -a ' + wrap(" ".join(option.choices)) + ) if option.choices else '' + + word += " ".join(BL_USE3.format( + name, + (cmd_need_arg + cmd_s + cmd_l + " -f " + cmd_arglist), + cmd_helpstr).split()) + "\n" + + word = (word + " ".join(BL_USE3.format( + name, + ("-s " + "h " + "-l " + "help" + " -f "), + ('-d ' + wrap("print help") + "\n") + ).split())) + return word + + +def clean_whitespace(word): + # Remove excess whitespace and tabs in a string + return " ".join(word.split()) + + +def wrap(word): + # Need " or ' around strings but watch out if they're in the string + sptoken = '\"' + if ('"') in word and ("'") in word: + word.replace('"', sptoken) + return '"' + word + '"' + + tok = '"' if "'" in word else "'" + return tok + word + tok diff --git a/libs/common/beetsplug/freedesktop.py b/libs/common/beetsplug/freedesktop.py index a768be2d..ba4d5879 100644 --- a/libs/common/beetsplug/freedesktop.py +++ b/libs/common/beetsplug/freedesktop.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Matt Lichtenberg. # @@ -16,7 +15,6 @@ """Creates freedesktop.org-compliant .directory files on an album level. """ -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets import ui @@ -26,12 +24,12 @@ class FreedesktopPlugin(BeetsPlugin): def commands(self): deprecated = ui.Subcommand( "freedesktop", - help=u"Print a message to redirect to thumbnails --dolphin") + help="Print a message to redirect to thumbnails --dolphin") deprecated.func = self.deprecation_message return [deprecated] def deprecation_message(self, lib, opts, args): - ui.print_(u"This plugin is deprecated. Its functionality is " - u"superseded by the 'thumbnails' plugin") - ui.print_(u"'thumbnails --dolphin' replaces freedesktop. See doc & " - u"changelog for more information") + ui.print_("This plugin is deprecated. Its functionality is " + "superseded by the 'thumbnails' plugin") + ui.print_("'thumbnails --dolphin' replaces freedesktop. See doc & " + "changelog for more information") diff --git a/libs/common/beetsplug/fromfilename.py b/libs/common/beetsplug/fromfilename.py index 56b68f75..55684a27 100644 --- a/libs/common/beetsplug/fromfilename.py +++ b/libs/common/beetsplug/fromfilename.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Jan-Erik Dahlin # @@ -16,13 +15,11 @@ """If the title is empty, try to extract track and title from the filename. """ -from __future__ import division, absolute_import, print_function from beets import plugins from beets.util import displayable_path import os import re -import six # Filename field extraction patterns. @@ -124,7 +121,7 @@ def apply_matches(d): # Apply the title and track. for item in d: if bad_title(item.title): - item.title = six.text_type(d[item][title_field]) + item.title = str(d[item][title_field]) if 'track' in d[item] and item.track == 0: item.track = int(d[item]['track']) @@ -133,7 +130,7 @@ def apply_matches(d): class FromFilenamePlugin(plugins.BeetsPlugin): def __init__(self): - super(FromFilenamePlugin, self).__init__() + super().__init__() self.register_listener('import_task_start', filename_task) diff --git a/libs/common/beetsplug/ftintitle.py b/libs/common/beetsplug/ftintitle.py index 9303f9cf..57863d2b 100644 --- a/libs/common/beetsplug/ftintitle.py +++ b/libs/common/beetsplug/ftintitle.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Verrus, # @@ -15,7 +14,6 @@ """Moves "featured" artists to the title from the artist field. """ -from __future__ import division, absolute_import, print_function import re @@ -75,22 +73,22 @@ def find_feat_part(artist, albumartist): class FtInTitlePlugin(plugins.BeetsPlugin): def __init__(self): - super(FtInTitlePlugin, self).__init__() + super().__init__() self.config.add({ 'auto': True, 'drop': False, - 'format': u'feat. {0}', + 'format': 'feat. {0}', }) self._command = ui.Subcommand( 'ftintitle', - help=u'move featured artists to the title field') + help='move featured artists to the title field') self._command.parser.add_option( - u'-d', u'--drop', dest='drop', + '-d', '--drop', dest='drop', action='store_true', default=None, - help=u'drop featuring from artists and ignore title update') + help='drop featuring from artists and ignore title update') if self.config['auto']: self.import_stages = [self.imported] @@ -127,7 +125,7 @@ class FtInTitlePlugin(plugins.BeetsPlugin): remove it from the artist field. """ # In all cases, update the artist fields. - self._log.info(u'artist: {0} -> {1}', item.artist, item.albumartist) + self._log.info('artist: {0} -> {1}', item.artist, item.albumartist) item.artist = item.albumartist if item.artist_sort: # Just strip the featured artist from the sort name. @@ -138,8 +136,8 @@ class FtInTitlePlugin(plugins.BeetsPlugin): if not drop_feat and not contains_feat(item.title): feat_format = self.config['format'].as_str() new_format = feat_format.format(feat_part) - new_title = u"{0} {1}".format(item.title, new_format) - self._log.info(u'title: {0} -> {1}', item.title, new_title) + new_title = f"{item.title} {new_format}" + self._log.info('title: {0} -> {1}', item.title, new_title) item.title = new_title def ft_in_title(self, item, drop_feat): @@ -165,4 +163,4 @@ class FtInTitlePlugin(plugins.BeetsPlugin): if feat_part: self.update_metadata(item, feat_part, drop_feat) else: - self._log.info(u'no featuring artists found') + self._log.info('no featuring artists found') diff --git a/libs/common/beetsplug/fuzzy.py b/libs/common/beetsplug/fuzzy.py index a7308a52..41829639 100644 --- a/libs/common/beetsplug/fuzzy.py +++ b/libs/common/beetsplug/fuzzy.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Philippe Mongeau. # @@ -16,7 +15,6 @@ """Provides a fuzzy matching query. """ -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets.dbcore.query import StringFieldQuery @@ -37,7 +35,7 @@ class FuzzyQuery(StringFieldQuery): class FuzzyPlugin(BeetsPlugin): def __init__(self): - super(FuzzyPlugin, self).__init__() + super().__init__() self.config.add({ 'prefix': '~', 'threshold': 0.7, diff --git a/libs/common/beetsplug/gmusic.py b/libs/common/beetsplug/gmusic.py index 259d2725..844234f9 100644 --- a/libs/common/beetsplug/gmusic.py +++ b/libs/common/beetsplug/gmusic.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- # This file is part of beets. -# Copyright 2017, Tigran Kostandyan. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -13,84 +11,15 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -"""Upload files to Google Play Music and list songs in its library.""" - -from __future__ import absolute_import, division, print_function -import os.path +"""Deprecation warning for the removed gmusic plugin.""" from beets.plugins import BeetsPlugin -from beets import ui -from beets import config -from beets.ui import Subcommand -from gmusicapi import Musicmanager, Mobileclient -from gmusicapi.exceptions import NotLoggedIn -import gmusicapi.clients class Gmusic(BeetsPlugin): def __init__(self): - super(Gmusic, self).__init__() - # Checks for OAuth2 credentials, - # if they don't exist - performs authorization - self.m = Musicmanager() - if os.path.isfile(gmusicapi.clients.OAUTH_FILEPATH): - self.m.login() - else: - self.m.perform_oauth() + super().__init__() - def commands(self): - gupload = Subcommand('gmusic-upload', - help=u'upload your tracks to Google Play Music') - gupload.func = self.upload - - search = Subcommand('gmusic-songs', - help=u'list of songs in Google Play Music library' - ) - search.parser.add_option('-t', '--track', dest='track', - action='store_true', - help='Search by track name') - search.parser.add_option('-a', '--artist', dest='artist', - action='store_true', - help='Search by artist') - search.func = self.search - return [gupload, search] - - def upload(self, lib, opts, args): - items = lib.items(ui.decargs(args)) - files = [x.path.decode('utf-8') for x in items] - ui.print_(u'Uploading your files...') - self.m.upload(filepaths=files) - ui.print_(u'Your files were successfully added to library') - - def search(self, lib, opts, args): - password = config['gmusic']['password'] - email = config['gmusic']['email'] - password.redact = True - email.redact = True - # Since Musicmanager doesn't support library management - # we need to use mobileclient interface - mobile = Mobileclient() - try: - mobile.login(email.as_str(), password.as_str(), - Mobileclient.FROM_MAC_ADDRESS) - files = mobile.get_all_songs() - except NotLoggedIn: - ui.print_( - u'Authentication error. Please check your email and password.' - ) - return - if not args: - for i, file in enumerate(files, start=1): - print(i, ui.colorize('blue', file['artist']), - file['title'], ui.colorize('red', file['album'])) - else: - if opts.track: - self.match(files, args, 'title') - else: - self.match(files, args, 'artist') - - @staticmethod - def match(files, args, search_by): - for file in files: - if ' '.join(ui.decargs(args)) in file[search_by]: - print(file['artist'], file['title'], file['album']) + self._log.warning("The 'gmusic' plugin has been removed following the" + " shutdown of Google Play Music. Remove the plugin" + " from your configuration to silence this warning.") diff --git a/libs/common/beetsplug/hook.py b/libs/common/beetsplug/hook.py index b6270fd5..0fe3bffc 100644 --- a/libs/common/beetsplug/hook.py +++ b/libs/common/beetsplug/hook.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2015, Adrian Sampson. # @@ -14,14 +13,13 @@ # included in all copies or substantial portions of the Software. """Allows custom commands to be run when an event is emitted by beets""" -from __future__ import division, absolute_import, print_function import string import subprocess -import six +import shlex from beets.plugins import BeetsPlugin -from beets.util import shlex_split, arg_encoding +from beets.util import arg_encoding class CodingFormatter(string.Formatter): @@ -46,13 +44,11 @@ class CodingFormatter(string.Formatter): See str.format and string.Formatter.format. """ - try: + if isinstance(format_string, bytes): format_string = format_string.decode(self._coding) - except UnicodeEncodeError: - pass - return super(CodingFormatter, self).format(format_string, *args, - **kwargs) + return super().format(format_string, *args, + **kwargs) def convert_field(self, value, conversion): """Converts the provided value given a conversion type. @@ -61,8 +57,8 @@ class CodingFormatter(string.Formatter): See string.Formatter.convert_field. """ - converted = super(CodingFormatter, self).convert_field(value, - conversion) + converted = super().convert_field(value, + conversion) if isinstance(converted, bytes): return converted.decode(self._coding) @@ -72,8 +68,9 @@ class CodingFormatter(string.Formatter): class HookPlugin(BeetsPlugin): """Allows custom commands to be run when an event is emitted by beets""" + def __init__(self): - super(HookPlugin, self).__init__() + super().__init__() self.config.add({ 'hooks': [] @@ -91,28 +88,28 @@ class HookPlugin(BeetsPlugin): def create_and_register_hook(self, event, command): def hook_function(**kwargs): - if command is None or len(command) == 0: - self._log.error('invalid command "{0}"', command) - return + if command is None or len(command) == 0: + self._log.error('invalid command "{0}"', command) + return - # Use a string formatter that works on Unicode strings. - if six.PY2: - formatter = CodingFormatter(arg_encoding()) - else: - formatter = string.Formatter() + # Use a string formatter that works on Unicode strings. + formatter = CodingFormatter(arg_encoding()) - command_pieces = shlex_split(command) + command_pieces = shlex.split(command) - for i, piece in enumerate(command_pieces): - command_pieces[i] = formatter.format(piece, event=event, - **kwargs) + for i, piece in enumerate(command_pieces): + command_pieces[i] = formatter.format(piece, event=event, + **kwargs) - self._log.debug(u'running command "{0}" for event {1}', - u' '.join(command_pieces), event) + self._log.debug('running command "{0}" for event {1}', + ' '.join(command_pieces), event) - try: - subprocess.Popen(command_pieces).wait() - except OSError as exc: - self._log.error(u'hook for {0} failed: {1}', event, exc) + try: + subprocess.check_call(command_pieces) + except subprocess.CalledProcessError as exc: + self._log.error('hook for {0} exited with status {1}', + event, exc.returncode) + except OSError as exc: + self._log.error('hook for {0} failed: {1}', event, exc) self.register_listener(event, hook_function) diff --git a/libs/common/beetsplug/ihate.py b/libs/common/beetsplug/ihate.py index 6ed250fe..91850e09 100644 --- a/libs/common/beetsplug/ihate.py +++ b/libs/common/beetsplug/ihate.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Blemjhoo Tezoulbr . # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function """Warns you about things you hate (or even blocks import).""" @@ -33,14 +31,14 @@ def summary(task): object. """ if task.is_album: - return u'{0} - {1}'.format(task.cur_artist, task.cur_album) + return f'{task.cur_artist} - {task.cur_album}' else: - return u'{0} - {1}'.format(task.item.artist, task.item.title) + return f'{task.item.artist} - {task.item.title}' class IHatePlugin(BeetsPlugin): def __init__(self): - super(IHatePlugin, self).__init__() + super().__init__() self.register_listener('import_task_choice', self.import_task_choice_event) self.config.add({ @@ -69,14 +67,14 @@ class IHatePlugin(BeetsPlugin): if task.choice_flag == action.APPLY: if skip_queries or warn_queries: - self._log.debug(u'processing your hate') + self._log.debug('processing your hate') if self.do_i_hate_this(task, skip_queries): task.choice_flag = action.SKIP - self._log.info(u'skipped: {0}', summary(task)) + self._log.info('skipped: {0}', summary(task)) return if self.do_i_hate_this(task, warn_queries): - self._log.info(u'you may hate this: {0}', summary(task)) + self._log.info('you may hate this: {0}', summary(task)) else: - self._log.debug(u'nothing to do') + self._log.debug('nothing to do') else: - self._log.debug(u'user made a decision, nothing to do') + self._log.debug('user made a decision, nothing to do') diff --git a/libs/common/beetsplug/importadded.py b/libs/common/beetsplug/importadded.py index 36407b14..e6665e0f 100644 --- a/libs/common/beetsplug/importadded.py +++ b/libs/common/beetsplug/importadded.py @@ -1,11 +1,8 @@ -# -*- coding: utf-8 -*- - """Populate an item's `added` and `mtime` fields by using the file modification time (mtime) of the item's source file before import. Reimported albums and items are skipped. """ -from __future__ import division, absolute_import, print_function import os @@ -16,7 +13,7 @@ from beets.plugins import BeetsPlugin class ImportAddedPlugin(BeetsPlugin): def __init__(self): - super(ImportAddedPlugin, self).__init__() + super().__init__() self.config.add({ 'preserve_mtimes': False, 'preserve_write_mtimes': False, @@ -27,7 +24,7 @@ class ImportAddedPlugin(BeetsPlugin): # album.path for old albums that were replaced by a reimported album self.replaced_album_paths = None # item path in the library to the mtime of the source file - self.item_mtime = dict() + self.item_mtime = {} register = self.register_listener register('import_task_created', self.check_config) @@ -53,8 +50,8 @@ class ImportAddedPlugin(BeetsPlugin): def record_if_inplace(self, task, session): if not (session.config['copy'] or session.config['move'] or session.config['link'] or session.config['hardlink']): - self._log.debug(u"In place import detected, recording mtimes from " - u"source paths") + self._log.debug("In place import detected, recording mtimes from " + "source paths") items = [task.item] \ if isinstance(task, importer.SingletonImportTask) \ else task.items @@ -62,9 +59,9 @@ class ImportAddedPlugin(BeetsPlugin): self.record_import_mtime(item, item.path, item.path) def record_reimported(self, task, session): - self.reimported_item_ids = set(item.id for item, replaced_items - in task.replaced_items.items() - if replaced_items) + self.reimported_item_ids = {item.id for item, replaced_items + in task.replaced_items.items() + if replaced_items} self.replaced_album_paths = set(task.replaced_albums.keys()) def write_file_mtime(self, path, mtime): @@ -86,14 +83,14 @@ class ImportAddedPlugin(BeetsPlugin): """ mtime = os.stat(util.syspath(source)).st_mtime self.item_mtime[destination] = mtime - self._log.debug(u"Recorded mtime {0} for item '{1}' imported from " - u"'{2}'", mtime, util.displayable_path(destination), + self._log.debug("Recorded mtime {0} for item '{1}' imported from " + "'{2}'", mtime, util.displayable_path(destination), util.displayable_path(source)) def update_album_times(self, lib, album): if self.reimported_album(album): - self._log.debug(u"Album '{0}' is reimported, skipping import of " - u"added dates for the album and its items.", + self._log.debug("Album '{0}' is reimported, skipping import of " + "added dates for the album and its items.", util.displayable_path(album.path)) return @@ -106,30 +103,30 @@ class ImportAddedPlugin(BeetsPlugin): self.write_item_mtime(item, mtime) item.store() album.added = min(album_mtimes) - self._log.debug(u"Import of album '{0}', selected album.added={1} " - u"from item file mtimes.", album.album, album.added) + self._log.debug("Import of album '{0}', selected album.added={1} " + "from item file mtimes.", album.album, album.added) album.store() def update_item_times(self, lib, item): if self.reimported_item(item): - self._log.debug(u"Item '{0}' is reimported, skipping import of " - u"added date.", util.displayable_path(item.path)) + self._log.debug("Item '{0}' is reimported, skipping import of " + "added date.", util.displayable_path(item.path)) return mtime = self.item_mtime.pop(item.path, None) if mtime: item.added = mtime if self.config['preserve_mtimes'].get(bool): self.write_item_mtime(item, mtime) - self._log.debug(u"Import of item '{0}', selected item.added={1}", + self._log.debug("Import of item '{0}', selected item.added={1}", util.displayable_path(item.path), item.added) item.store() - def update_after_write_time(self, item): + def update_after_write_time(self, item, path): """Update the mtime of the item's file with the item.added value after each write of the item if `preserve_write_mtimes` is enabled. """ if item.added: if self.config['preserve_write_mtimes'].get(bool): self.write_item_mtime(item, item.added) - self._log.debug(u"Write of item '{0}', selected item.added={1}", + self._log.debug("Write of item '{0}', selected item.added={1}", util.displayable_path(item.path), item.added) diff --git a/libs/common/beetsplug/importfeeds.py b/libs/common/beetsplug/importfeeds.py index 35ae2883..ad6d8415 100644 --- a/libs/common/beetsplug/importfeeds.py +++ b/libs/common/beetsplug/importfeeds.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Fabrice Laporte. # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function """Write paths of imported files in various formats to ease later import in a music player. Also allow printing the new file locations to stdout in case @@ -54,11 +52,11 @@ def _write_m3u(m3u_path, items_paths): class ImportFeedsPlugin(BeetsPlugin): def __init__(self): - super(ImportFeedsPlugin, self).__init__() + super().__init__() self.config.add({ 'formats': [], - 'm3u_name': u'imported.m3u', + 'm3u_name': 'imported.m3u', 'dir': None, 'relative_to': None, 'absolute_path': False, @@ -118,9 +116,9 @@ class ImportFeedsPlugin(BeetsPlugin): link(path, dest) if 'echo' in formats: - self._log.info(u"Location of imported music:") + self._log.info("Location of imported music:") for path in paths: - self._log.info(u" {0}", path) + self._log.info(" {0}", path) def album_imported(self, lib, album): self._record_items(lib, album.album, album.items()) diff --git a/libs/common/beetsplug/info.py b/libs/common/beetsplug/info.py index 0d40c597..1e6d4b32 100644 --- a/libs/common/beetsplug/info.py +++ b/libs/common/beetsplug/info.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -16,19 +15,17 @@ """Shows file metadata. """ -from __future__ import division, absolute_import, print_function import os -import re from beets.plugins import BeetsPlugin from beets import ui -from beets import mediafile +import mediafile from beets.library import Item from beets.util import displayable_path, normpath, syspath -def tag_data(lib, args): +def tag_data(lib, args, album=False): query = [] for arg in args: path = normpath(arg) @@ -42,15 +39,29 @@ def tag_data(lib, args): yield tag_data_emitter(item.path) +def tag_fields(): + fields = set(mediafile.MediaFile.readable_fields()) + fields.add('art') + return fields + + def tag_data_emitter(path): - def emitter(): - fields = list(mediafile.MediaFile.readable_fields()) - fields.remove('images') + def emitter(included_keys): + if included_keys == '*': + fields = tag_fields() + else: + fields = included_keys + if 'images' in fields: + # We can't serialize the image data. + fields.remove('images') mf = mediafile.MediaFile(syspath(path)) tags = {} for field in fields: - tags[field] = getattr(mf, field) - tags['art'] = mf.art is not None + if field == 'art': + tags[field] = mf.art is not None + else: + tags[field] = getattr(mf, field, None) + # create a temporary Item to take advantage of __format__ item = Item.from_path(syspath(path)) @@ -58,15 +69,14 @@ def tag_data_emitter(path): return emitter -def library_data(lib, args): - for item in lib.items(args): +def library_data(lib, args, album=False): + for item in lib.albums(args) if album else lib.items(args): yield library_data_emitter(item) def library_data_emitter(item): - def emitter(): - data = dict(item.formatted()) - data.pop('path', None) # path is fetched from item + def emitter(included_keys): + data = dict(item.formatted(included_keys=included_keys)) return data, item return emitter @@ -98,7 +108,7 @@ def print_data(data, item=None, fmt=None): formatted = {} for key, value in data.items(): if isinstance(value, list): - formatted[key] = u'; '.join(value) + formatted[key] = '; '.join(value) if value is not None: formatted[key] = value @@ -106,7 +116,7 @@ def print_data(data, item=None, fmt=None): return maxwidth = max(len(key) for key in formatted) - lineformat = u'{{0:>{0}}}: {{1}}'.format(maxwidth) + lineformat = f'{{0:>{maxwidth}}}: {{1}}' if path: ui.print_(displayable_path(path)) @@ -114,7 +124,7 @@ def print_data(data, item=None, fmt=None): for field in sorted(formatted): value = formatted[field] if isinstance(value, list): - value = u'; '.join(value) + value = '; '.join(value) ui.print_(lineformat.format(field, value)) @@ -129,7 +139,7 @@ def print_data_keys(data, item=None): if len(formatted) == 0: return - line_format = u'{0}{{0}}'.format(u' ' * 4) + line_format = '{0}{{0}}'.format(' ' * 4) if path: ui.print_(displayable_path(path)) @@ -140,24 +150,28 @@ def print_data_keys(data, item=None): class InfoPlugin(BeetsPlugin): def commands(self): - cmd = ui.Subcommand('info', help=u'show file metadata') + cmd = ui.Subcommand('info', help='show file metadata') cmd.func = self.run cmd.parser.add_option( - u'-l', u'--library', action='store_true', - help=u'show library fields instead of tags', + '-l', '--library', action='store_true', + help='show library fields instead of tags', ) cmd.parser.add_option( - u'-s', u'--summarize', action='store_true', - help=u'summarize the tags of all files', + '-a', '--album', action='store_true', + help='show album fields instead of tracks (implies "--library")', ) cmd.parser.add_option( - u'-i', u'--include-keys', default=[], + '-s', '--summarize', action='store_true', + help='summarize the tags of all files', + ) + cmd.parser.add_option( + '-i', '--include-keys', default=[], action='append', dest='included_keys', - help=u'comma separated list of keys to show', + help='comma separated list of keys to show', ) cmd.parser.add_option( - u'-k', u'--keys-only', action='store_true', - help=u'show only the keys', + '-k', '--keys-only', action='store_true', + help='show only the keys', ) cmd.parser.add_format_option(target='item') return [cmd] @@ -176,7 +190,7 @@ class InfoPlugin(BeetsPlugin): dictionary and only prints that. If two files have different values for the same tag, the value is set to '[various]' """ - if opts.library: + if opts.library or opts.album: data_collector = library_data else: data_collector = tag_data @@ -184,18 +198,21 @@ class InfoPlugin(BeetsPlugin): included_keys = [] for keys in opts.included_keys: included_keys.extend(keys.split(',')) - key_filter = make_key_filter(included_keys) + # Drop path even if user provides it multiple times + included_keys = [k for k in included_keys if k != 'path'] first = True summary = {} - for data_emitter in data_collector(lib, ui.decargs(args)): + for data_emitter in data_collector( + lib, ui.decargs(args), + album=opts.album, + ): try: - data, item = data_emitter() - except (mediafile.UnreadableFileError, IOError) as ex: - self._log.error(u'cannot read file: {0}', ex) + data, item = data_emitter(included_keys or '*') + except (mediafile.UnreadableFileError, OSError) as ex: + self._log.error('cannot read file: {0}', ex) continue - data = key_filter(data) if opts.summarize: update_summary(summary, data) else: @@ -210,33 +227,3 @@ class InfoPlugin(BeetsPlugin): if opts.summarize: print_data(summary) - - -def make_key_filter(include): - """Return a function that filters a dictionary. - - The returned filter takes a dictionary and returns another - dictionary that only includes the key-value pairs where the key - glob-matches one of the keys in `include`. - """ - if not include: - return identity - - matchers = [] - for key in include: - key = re.escape(key) - key = key.replace(r'\*', '.*') - matchers.append(re.compile(key + '$')) - - def filter_(data): - filtered = dict() - for key, value in data.items(): - if any([m.match(key) for m in matchers]): - filtered[key] = value - return filtered - - return filter_ - - -def identity(val): - return val diff --git a/libs/common/beetsplug/inline.py b/libs/common/beetsplug/inline.py index fd0e9fc3..e19eaa9d 100644 --- a/libs/common/beetsplug/inline.py +++ b/libs/common/beetsplug/inline.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -15,25 +14,23 @@ """Allows inline path template customization code in the config file. """ -from __future__ import division, absolute_import, print_function import traceback import itertools from beets.plugins import BeetsPlugin from beets import config -import six -FUNC_NAME = u'__INLINE_FUNC__' +FUNC_NAME = '__INLINE_FUNC__' class InlineError(Exception): """Raised when a runtime error occurs in an inline expression. """ def __init__(self, code, exc): - super(InlineError, self).__init__( - (u"error in inline path field code:\n" - u"%s\n%s: %s") % (code, type(exc).__name__, six.text_type(exc)) + super().__init__( + ("error in inline path field code:\n" + "%s\n%s: %s") % (code, type(exc).__name__, str(exc)) ) @@ -41,7 +38,7 @@ def _compile_func(body): """Given Python code for a function body, return a compiled callable that invokes that code. """ - body = u'def {0}():\n {1}'.format( + body = 'def {}():\n {}'.format( FUNC_NAME, body.replace('\n', '\n ') ) @@ -53,7 +50,7 @@ def _compile_func(body): class InlinePlugin(BeetsPlugin): def __init__(self): - super(InlinePlugin, self).__init__() + super().__init__() config.add({ 'pathfields': {}, # Legacy name. @@ -64,14 +61,14 @@ class InlinePlugin(BeetsPlugin): # Item fields. for key, view in itertools.chain(config['item_fields'].items(), config['pathfields'].items()): - self._log.debug(u'adding item field {0}', key) + self._log.debug('adding item field {0}', key) func = self.compile_inline(view.as_str(), False) if func is not None: self.template_fields[key] = func # Album fields. for key, view in config['album_fields'].items(): - self._log.debug(u'adding album field {0}', key) + self._log.debug('adding album field {0}', key) func = self.compile_inline(view.as_str(), True) if func is not None: self.album_template_fields[key] = func @@ -84,14 +81,14 @@ class InlinePlugin(BeetsPlugin): """ # First, try compiling as a single function. try: - code = compile(u'({0})'.format(python_code), 'inline', 'eval') + code = compile(f'({python_code})', 'inline', 'eval') except SyntaxError: # Fall back to a function body. try: func = _compile_func(python_code) except SyntaxError: - self._log.error(u'syntax error in inline field definition:\n' - u'{0}', traceback.format_exc()) + self._log.error('syntax error in inline field definition:\n' + '{0}', traceback.format_exc()) return else: is_expr = False @@ -117,9 +114,13 @@ class InlinePlugin(BeetsPlugin): # For function bodies, invoke the function with values as global # variables. def _func_func(obj): + old_globals = dict(func.__globals__) func.__globals__.update(_dict_for(obj)) try: return func() except Exception as exc: raise InlineError(python_code, exc) + finally: + func.__globals__.clear() + func.__globals__.update(old_globals) return _func_func diff --git a/libs/common/beetsplug/ipfs.py b/libs/common/beetsplug/ipfs.py index 9a9d6aa5..3c42e7c8 100644 --- a/libs/common/beetsplug/ipfs.py +++ b/libs/common/beetsplug/ipfs.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # # Permission is hereby granted, free of charge, to any person obtaining @@ -15,7 +14,6 @@ """Adds support for ipfs. Requires go-ipfs and a running ipfs daemon """ -from __future__ import division, absolute_import, print_function from beets import ui, util, library, config from beets.plugins import BeetsPlugin @@ -29,9 +27,10 @@ import tempfile class IPFSPlugin(BeetsPlugin): def __init__(self): - super(IPFSPlugin, self).__init__() + super().__init__() self.config.add({ 'auto': True, + 'nocopy': False, }) if self.config['auto']: @@ -116,12 +115,15 @@ class IPFSPlugin(BeetsPlugin): self._log.info('Adding {0} to ipfs', album_dir) - cmd = "ipfs add -q -r".split() + if self.config['nocopy']: + cmd = "ipfs add --nocopy -q -r".split() + else: + cmd = "ipfs add -q -r".split() cmd.append(album_dir) try: - output = util.command_output(cmd).split() + output = util.command_output(cmd).stdout.split() except (OSError, subprocess.CalledProcessError) as exc: - self._log.error(u'Failed to add {0}, error: {1}', album_dir, exc) + self._log.error('Failed to add {0}, error: {1}', album_dir, exc) return False length = len(output) @@ -147,6 +149,8 @@ class IPFSPlugin(BeetsPlugin): def ipfs_get(self, lib, query): query = query[0] # Check if query is a hash + # TODO: generalize to other hashes; probably use a multihash + # implementation if query.startswith("Qm") and len(query) == 46: self.ipfs_get_from_hash(lib, query) else: @@ -174,11 +178,14 @@ class IPFSPlugin(BeetsPlugin): with tempfile.NamedTemporaryFile() as tmp: self.ipfs_added_albums(lib, tmp.name) try: - cmd = "ipfs add -q ".split() + if self.config['nocopy']: + cmd = "ipfs add --nocopy -q ".split() + else: + cmd = "ipfs add -q ".split() cmd.append(tmp.name) - output = util.command_output(cmd) + output = util.command_output(cmd).stdout except (OSError, subprocess.CalledProcessError) as err: - msg = "Failed to publish library. Error: {0}".format(err) + msg = f"Failed to publish library. Error: {err}" self._log.error(msg) return False self._log.info("hash of library: {0}", output) @@ -190,26 +197,26 @@ class IPFSPlugin(BeetsPlugin): else: lib_name = _hash lib_root = os.path.dirname(lib.path) - remote_libs = lib_root + "/remotes" + remote_libs = os.path.join(lib_root, b"remotes") if not os.path.exists(remote_libs): try: os.makedirs(remote_libs) except OSError as e: - msg = "Could not create {0}. Error: {1}".format(remote_libs, e) + msg = f"Could not create {remote_libs}. Error: {e}" self._log.error(msg) return False - path = remote_libs + "/" + lib_name + ".db" + path = os.path.join(remote_libs, lib_name.encode() + b".db") if not os.path.exists(path): - cmd = "ipfs get {0} -o".format(_hash).split() + cmd = f"ipfs get {_hash} -o".split() cmd.append(path) try: util.command_output(cmd) except (OSError, subprocess.CalledProcessError): - self._log.error("Could not import {0}".format(_hash)) + self._log.error(f"Could not import {_hash}") return False # add all albums from remotes into a combined library - jpath = remote_libs + "/joined.db" + jpath = os.path.join(remote_libs, b"joined.db") jlib = library.Library(jpath) nlib = library.Library(path) for album in nlib.albums(): @@ -232,12 +239,12 @@ class IPFSPlugin(BeetsPlugin): fmt = config['format_album'].get() try: albums = self.query(lib, args) - except IOError: + except OSError: ui.print_("No imported libraries yet.") return for album in albums: - ui.print_(format(album, fmt), " : ", album.ipfs) + ui.print_(format(album, fmt), " : ", album.ipfs.decode()) def query(self, lib, args): rlib = self.get_remote_lib(lib) @@ -246,10 +253,10 @@ class IPFSPlugin(BeetsPlugin): def get_remote_lib(self, lib): lib_root = os.path.dirname(lib.path) - remote_libs = lib_root + "/remotes" - path = remote_libs + "/joined.db" + remote_libs = os.path.join(lib_root, b"remotes") + path = os.path.join(remote_libs, b"joined.db") if not os.path.isfile(path): - raise IOError + raise OSError return library.Library(path) def ipfs_added_albums(self, rlib, tmpname): @@ -276,7 +283,7 @@ class IPFSPlugin(BeetsPlugin): util._fsencoding(), 'ignore' ) # Clear current path from item - item.path = '/ipfs/{0}/{1}'.format(album.ipfs, item_path) + item.path = f'/ipfs/{album.ipfs}/{item_path}' item.id = None items.append(item) diff --git a/libs/common/beetsplug/keyfinder.py b/libs/common/beetsplug/keyfinder.py index a3fbc821..b695ab54 100644 --- a/libs/common/beetsplug/keyfinder.py +++ b/libs/common/beetsplug/keyfinder.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Thomas Scholtes. # @@ -16,8 +15,8 @@ """Uses the `KeyFinder` program to add the `initial_key` field. """ -from __future__ import division, absolute_import, print_function +import os.path import subprocess from beets import ui @@ -28,11 +27,11 @@ from beets.plugins import BeetsPlugin class KeyFinderPlugin(BeetsPlugin): def __init__(self): - super(KeyFinderPlugin, self).__init__() + super().__init__() self.config.add({ - u'bin': u'KeyFinder', - u'auto': True, - u'overwrite': False, + 'bin': 'KeyFinder', + 'auto': True, + 'overwrite': False, }) if self.config['auto'].get(bool): @@ -40,7 +39,7 @@ class KeyFinderPlugin(BeetsPlugin): def commands(self): cmd = ui.Subcommand('keyfinder', - help=u'detect and add initial key from audio') + help='detect and add initial key from audio') cmd.func = self.command return [cmd] @@ -52,34 +51,45 @@ class KeyFinderPlugin(BeetsPlugin): def find_key(self, items, write=False): overwrite = self.config['overwrite'].get(bool) - bin = self.config['bin'].as_str() + command = [self.config['bin'].as_str()] + # The KeyFinder GUI program needs the -f flag before the path. + # keyfinder-cli is similar, but just wants the path with no flag. + if 'keyfinder-cli' not in os.path.basename(command[0]).lower(): + command.append('-f') for item in items: if item['initial_key'] and not overwrite: continue try: - output = util.command_output([bin, '-f', - util.syspath(item.path)]) + output = util.command_output(command + [util.syspath( + item.path)]).stdout except (subprocess.CalledProcessError, OSError) as exc: - self._log.error(u'execution failed: {0}', exc) + self._log.error('execution failed: {0}', exc) continue except UnicodeEncodeError: # Workaround for Python 2 Windows bug. - # http://bugs.python.org/issue1759845 - self._log.error(u'execution failed for Unicode path: {0!r}', + # https://bugs.python.org/issue1759845 + self._log.error('execution failed for Unicode path: {0!r}', item.path) continue - key_raw = output.rsplit(None, 1)[-1] + try: + key_raw = output.rsplit(None, 1)[-1] + except IndexError: + # Sometimes keyfinder-cli returns 0 but with no key, usually + # when the file is silent or corrupt, so we log and skip. + self._log.error('no key returned for path: {0}', item.path) + continue + try: key = util.text_string(key_raw) except UnicodeDecodeError: - self._log.error(u'output is invalid UTF-8') + self._log.error('output is invalid UTF-8') continue item['initial_key'] = key - self._log.info(u'added computed initial key {0} for {1}', + self._log.info('added computed initial key {0} for {1}', key, util.displayable_path(item.path)) if write: diff --git a/libs/common/beetsplug/kodiupdate.py b/libs/common/beetsplug/kodiupdate.py index ce5cb478..2a885d2c 100644 --- a/libs/common/beetsplug/kodiupdate.py +++ b/libs/common/beetsplug/kodiupdate.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2017, Pauli Kettunen. # @@ -23,18 +22,16 @@ Put something like the following in your config.yaml to configure: user: user pwd: secret """ -from __future__ import division, absolute_import, print_function import requests from beets import config from beets.plugins import BeetsPlugin -import six def update_kodi(host, port, user, password): """Sends request to the Kodi api to start a library refresh. """ - url = "http://{0}:{1}/jsonrpc".format(host, port) + url = f"http://{host}:{port}/jsonrpc" """Content-Type: application/json is mandatory according to the kodi jsonrpc documentation""" @@ -54,14 +51,14 @@ def update_kodi(host, port, user, password): class KodiUpdate(BeetsPlugin): def __init__(self): - super(KodiUpdate, self).__init__() + super().__init__() # Adding defaults. config['kodi'].add({ - u'host': u'localhost', - u'port': 8080, - u'user': u'kodi', - u'pwd': u'kodi'}) + 'host': 'localhost', + 'port': 8080, + 'user': 'kodi', + 'pwd': 'kodi'}) config['kodi']['pwd'].redact = True self.register_listener('database_change', self.listen_for_db_change) @@ -73,7 +70,7 @@ class KodiUpdate(BeetsPlugin): def update(self, lib): """When the client exists try to send refresh request to Kodi server. """ - self._log.info(u'Requesting a Kodi library update...') + self._log.info('Requesting a Kodi library update...') # Try to send update request. try: @@ -85,14 +82,14 @@ class KodiUpdate(BeetsPlugin): r.raise_for_status() except requests.exceptions.RequestException as e: - self._log.warning(u'Kodi update failed: {0}', - six.text_type(e)) + self._log.warning('Kodi update failed: {0}', + str(e)) return json = r.json() if json.get('result') != 'OK': - self._log.warning(u'Kodi update failed: JSON response was {0!r}', + self._log.warning('Kodi update failed: JSON response was {0!r}', json) return - self._log.info(u'Kodi update triggered') + self._log.info('Kodi update triggered') diff --git a/libs/common/beetsplug/lastgenre/__init__.py b/libs/common/beetsplug/lastgenre/__init__.py index 4374310b..05412308 100644 --- a/libs/common/beetsplug/lastgenre/__init__.py +++ b/libs/common/beetsplug/lastgenre/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -13,8 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function -import six """Gets genres for imported music based on Last.fm tags. @@ -46,7 +43,7 @@ PYLAST_EXCEPTIONS = ( ) REPLACE = { - u'\u2010': '-', + '\u2010': '-', } @@ -73,7 +70,7 @@ def flatten_tree(elem, path, branches): for sub in elem: flatten_tree(sub, path, branches) else: - branches.append(path + [six.text_type(elem)]) + branches.append(path + [str(elem)]) def find_parents(candidate, branches): @@ -97,7 +94,7 @@ C14N_TREE = os.path.join(os.path.dirname(__file__), 'genres-tree.yaml') class LastGenrePlugin(plugins.BeetsPlugin): def __init__(self): - super(LastGenrePlugin, self).__init__() + super().__init__() self.config.add({ 'whitelist': True, @@ -108,8 +105,9 @@ class LastGenrePlugin(plugins.BeetsPlugin): 'source': 'album', 'force': True, 'auto': True, - 'separator': u', ', + 'separator': ', ', 'prefer_specific': False, + 'title_case': True, }) self.setup() @@ -132,18 +130,27 @@ class LastGenrePlugin(plugins.BeetsPlugin): with open(wl_filename, 'rb') as f: for line in f: line = line.decode('utf-8').strip().lower() - if line and not line.startswith(u'#'): + if line and not line.startswith('#'): self.whitelist.add(line) # Read the genres tree for canonicalization if enabled. self.c14n_branches = [] c14n_filename = self.config['canonical'].get() - if c14n_filename in (True, ''): # Default tree. + self.canonicalize = c14n_filename is not False + + # Default tree + if c14n_filename in (True, ''): c14n_filename = C14N_TREE + elif not self.canonicalize and self.config['prefer_specific'].get(): + # prefer_specific requires a tree, load default tree + c14n_filename = C14N_TREE + + # Read the tree if c14n_filename: + self._log.debug('Loading canonicalization tree {0}', c14n_filename) c14n_filename = normpath(c14n_filename) with codecs.open(c14n_filename, 'r', encoding='utf-8') as f: - genres_tree = yaml.load(f) + genres_tree = yaml.safe_load(f) flatten_tree(genres_tree, [], self.c14n_branches) @property @@ -186,7 +193,7 @@ class LastGenrePlugin(plugins.BeetsPlugin): return None count = self.config['count'].get(int) - if self.c14n_branches: + if self.canonicalize: # Extend the list to consider tags parents in the c14n tree tags_all = [] for tag in tags: @@ -214,12 +221,17 @@ class LastGenrePlugin(plugins.BeetsPlugin): # c14n only adds allowed genres but we may have had forbidden genres in # the original tags list - tags = [x.title() for x in tags if self._is_allowed(x)] + tags = [self._format_tag(x) for x in tags if self._is_allowed(x)] return self.config['separator'].as_str().join( tags[:self.config['count'].get(int)] ) + def _format_tag(self, tag): + if self.config["title_case"]: + return tag.title() + return tag + def fetch_genre(self, lastfm_obj): """Return the genre for a pylast entity or None if no suitable genre can be found. Ex. 'Electronic, House, Dance' @@ -251,8 +263,8 @@ class LastGenrePlugin(plugins.BeetsPlugin): if any(not s for s in args): return None - key = u'{0}.{1}'.format(entity, - u'-'.join(six.text_type(a) for a in args)) + key = '{}.{}'.format(entity, + '-'.join(str(a) for a in args)) if key in self._genre_cache: return self._genre_cache[key] else: @@ -270,28 +282,28 @@ class LastGenrePlugin(plugins.BeetsPlugin): """Return the album genre for this Item or Album. """ return self._last_lookup( - u'album', LASTFM.get_album, obj.albumartist, obj.album + 'album', LASTFM.get_album, obj.albumartist, obj.album ) def fetch_album_artist_genre(self, obj): """Return the album artist genre for this Item or Album. """ return self._last_lookup( - u'artist', LASTFM.get_artist, obj.albumartist + 'artist', LASTFM.get_artist, obj.albumartist ) def fetch_artist_genre(self, item): """Returns the track artist genre for this Item. """ return self._last_lookup( - u'artist', LASTFM.get_artist, item.artist + 'artist', LASTFM.get_artist, item.artist ) def fetch_track_genre(self, obj): """Returns the track genre for this Item. """ return self._last_lookup( - u'track', LASTFM.get_track, obj.artist, obj.title + 'track', LASTFM.get_track, obj.artist, obj.title ) def _get_genre(self, obj): @@ -361,38 +373,56 @@ class LastGenrePlugin(plugins.BeetsPlugin): return None, None def commands(self): - lastgenre_cmd = ui.Subcommand('lastgenre', help=u'fetch genres') + lastgenre_cmd = ui.Subcommand('lastgenre', help='fetch genres') lastgenre_cmd.parser.add_option( - u'-f', u'--force', dest='force', - action='store_true', default=False, - help=u're-download genre when already present' + '-f', '--force', dest='force', + action='store_true', + help='re-download genre when already present' ) lastgenre_cmd.parser.add_option( - u'-s', u'--source', dest='source', type='string', - help=u'genre source: artist, album, or track' + '-s', '--source', dest='source', type='string', + help='genre source: artist, album, or track' ) + lastgenre_cmd.parser.add_option( + '-A', '--items', action='store_false', dest='album', + help='match items instead of albums') + lastgenre_cmd.parser.add_option( + '-a', '--albums', action='store_true', dest='album', + help='match albums instead of items') + lastgenre_cmd.parser.set_defaults(album=True) def lastgenre_func(lib, opts, args): write = ui.should_write() self.config.set_args(opts) - for album in lib.albums(ui.decargs(args)): - album.genre, src = self._get_genre(album) - self._log.info(u'genre for album {0} ({1}): {0.genre}', - album, src) - album.store() + if opts.album: + # Fetch genres for whole albums + for album in lib.albums(ui.decargs(args)): + album.genre, src = self._get_genre(album) + self._log.info('genre for album {0} ({1}): {0.genre}', + album, src) + album.store() - for item in album.items(): - # If we're using track-level sources, also look up each - # track on the album. - if 'track' in self.sources: - item.genre, src = self._get_genre(item) - item.store() - self._log.info(u'genre for track {0} ({1}): {0.genre}', - item, src) + for item in album.items(): + # If we're using track-level sources, also look up each + # track on the album. + if 'track' in self.sources: + item.genre, src = self._get_genre(item) + item.store() + self._log.info( + 'genre for track {0} ({1}): {0.genre}', + item, src) - if write: - item.try_write() + if write: + item.try_write() + else: + # Just query singletons, i.e. items that are not part of + # an album + for item in lib.items(ui.decargs(args)): + item.genre, src = self._get_genre(item) + self._log.debug('added last.fm item genre ({0}): {1}', + src, item.genre) + item.store() lastgenre_cmd.func = lastgenre_func return [lastgenre_cmd] @@ -402,21 +432,21 @@ class LastGenrePlugin(plugins.BeetsPlugin): if task.is_album: album = task.album album.genre, src = self._get_genre(album) - self._log.debug(u'added last.fm album genre ({0}): {1}', + self._log.debug('added last.fm album genre ({0}): {1}', src, album.genre) album.store() if 'track' in self.sources: for item in album.items(): item.genre, src = self._get_genre(item) - self._log.debug(u'added last.fm item genre ({0}): {1}', + self._log.debug('added last.fm item genre ({0}): {1}', src, item.genre) item.store() else: item = task.item item.genre, src = self._get_genre(item) - self._log.debug(u'added last.fm item genre ({0}): {1}', + self._log.debug('added last.fm item genre ({0}): {1}', src, item.genre) item.store() @@ -438,12 +468,12 @@ class LastGenrePlugin(plugins.BeetsPlugin): try: res = obj.get_top_tags() except PYLAST_EXCEPTIONS as exc: - self._log.debug(u'last.fm error: {0}', exc) + self._log.debug('last.fm error: {0}', exc) return [] except Exception as exc: # Isolate bugs in pylast. - self._log.debug(u'{}', traceback.format_exc()) - self._log.error(u'error in pylast library: {0}', exc) + self._log.debug('{}', traceback.format_exc()) + self._log.error('error in pylast library: {0}', exc) return [] # Filter by weight (optionally). diff --git a/libs/common/beetsplug/lastgenre/genres-tree.yaml b/libs/common/beetsplug/lastgenre/genres-tree.yaml index a09f7e6b..c8ae4247 100644 --- a/libs/common/beetsplug/lastgenre/genres-tree.yaml +++ b/libs/common/beetsplug/lastgenre/genres-tree.yaml @@ -648,35 +648,51 @@ - glam rock - hard rock - heavy metal: - - alternative metal + - alternative metal: + - funk metal - black metal: - viking metal - christian metal - death metal: + - death/doom - goregrind - melodic death metal - technical death metal - - doom metal + - doom metal: + - epic doom metal + - funeral doom - drone metal + - epic metal - folk metal: - celtic metal - medieval metal + - pagan metal - funk metal - glam metal - gothic metal + - industrial metal: + - industrial death metal - metalcore: - deathcore - mathcore: - djent - - power metal + - synthcore + - neoclassical metal + - post-metal + - power metal: + - progressive power metal - progressive metal - sludge metal - speed metal - - stoner rock + - stoner rock: + - stoner metal - symphonic metal - thrash metal: - crossover thrash - groove metal + - progressive thrash metal + - teutonic thrash metal + - traditional heavy metal - math rock - new wave: - world fusion @@ -719,6 +735,7 @@ - street punk - thrashcore - horror punk + - oi! - pop punk - psychobilly - riot grrrl diff --git a/libs/common/beetsplug/lastgenre/genres.txt b/libs/common/beetsplug/lastgenre/genres.txt index 914ee129..7ccd7ad3 100644 --- a/libs/common/beetsplug/lastgenre/genres.txt +++ b/libs/common/beetsplug/lastgenre/genres.txt @@ -450,6 +450,8 @@ emo rap emocore emotronic enka +epic doom metal +epic metal eremwu eu ethereal pop ethereal wave @@ -1024,6 +1026,7 @@ neo-medieval neo-prog neo-psychedelia neoclassical +neoclassical metal neoclassical music neofolk neotraditional country @@ -1176,8 +1179,10 @@ progressive folk progressive folk music progressive house progressive metal +progressive power metal progressive rock progressive trance +progressive thrash metal protopunk psych folk psychedelic music @@ -1396,6 +1401,7 @@ symphonic metal symphonic poem symphonic rock symphony +synthcore synthpop synthpunk t'ong guitar @@ -1428,6 +1434,7 @@ tejano tejano music tekno tembang sunda +teutonic thrash metal texas blues thai pop thillana @@ -1444,6 +1451,7 @@ toeshey togaku trad jazz traditional bluegrass +traditional heavy metal traditional pop music trallalero trance diff --git a/libs/common/beetsplug/lastimport.py b/libs/common/beetsplug/lastimport.py index d7b84b0a..16d53302 100644 --- a/libs/common/beetsplug/lastimport.py +++ b/libs/common/beetsplug/lastimport.py @@ -1,6 +1,5 @@ -# -*- coding: utf-8 -*- # This file is part of beets. -# Copyright 2016, Rafael Bodill http://github.com/rafi +# Copyright 2016, Rafael Bodill https://github.com/rafi # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import pylast from pylast import TopItem, _extract, _number @@ -28,7 +26,7 @@ API_URL = 'https://ws.audioscrobbler.com/2.0/' class LastImportPlugin(plugins.BeetsPlugin): def __init__(self): - super(LastImportPlugin, self).__init__() + super().__init__() config['lastfm'].add({ 'user': '', 'api_key': plugins.LASTFM_KEY, @@ -43,7 +41,7 @@ class LastImportPlugin(plugins.BeetsPlugin): } def commands(self): - cmd = ui.Subcommand('lastimport', help=u'import last.fm play-count') + cmd = ui.Subcommand('lastimport', help='import last.fm play-count') def func(lib, opts, args): import_lastfm(lib, self._log) @@ -59,7 +57,7 @@ class CustomUser(pylast.User): tracks. """ def __init__(self, *args, **kwargs): - super(CustomUser, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def _get_things(self, method, thing, thing_type, params=None, cacheable=True): @@ -114,9 +112,9 @@ def import_lastfm(lib, log): per_page = config['lastimport']['per_page'].get(int) if not user: - raise ui.UserError(u'You must specify a user name for lastimport') + raise ui.UserError('You must specify a user name for lastimport') - log.info(u'Fetching last.fm library for @{0}', user) + log.info('Fetching last.fm library for @{0}', user) page_total = 1 page_current = 0 @@ -125,15 +123,15 @@ def import_lastfm(lib, log): retry_limit = config['lastimport']['retry_limit'].get(int) # Iterate through a yet to be known page total count while page_current < page_total: - log.info(u'Querying page #{0}{1}...', + log.info('Querying page #{0}{1}...', page_current + 1, - '/{}'.format(page_total) if page_total > 1 else '') + f'/{page_total}' if page_total > 1 else '') for retry in range(0, retry_limit): tracks, page_total = fetch_tracks(user, page_current + 1, per_page) if page_total < 1: # It means nothing to us! - raise ui.UserError(u'Last.fm reported no data.') + raise ui.UserError('Last.fm reported no data.') if tracks: found, unknown = process_tracks(lib, tracks, log) @@ -141,22 +139,22 @@ def import_lastfm(lib, log): unknown_total += unknown break else: - log.error(u'ERROR: unable to read page #{0}', + log.error('ERROR: unable to read page #{0}', page_current + 1) if retry < retry_limit: log.info( - u'Retrying page #{0}... ({1}/{2} retry)', + 'Retrying page #{0}... ({1}/{2} retry)', page_current + 1, retry + 1, retry_limit ) else: - log.error(u'FAIL: unable to fetch page #{0}, ', - u'tried {1} times', page_current, retry + 1) + log.error('FAIL: unable to fetch page #{0}, ', + 'tried {1} times', page_current, retry + 1) page_current += 1 - log.info(u'... done!') - log.info(u'finished processing {0} song pages', page_total) - log.info(u'{0} unknown play-counts', unknown_total) - log.info(u'{0} play-counts imported', found_total) + log.info('... done!') + log.info('finished processing {0} song pages', page_total) + log.info('{0} unknown play-counts', unknown_total) + log.info('{0} play-counts imported', found_total) def fetch_tracks(user, page, limit): @@ -190,7 +188,7 @@ def process_tracks(lib, tracks, log): total = len(tracks) total_found = 0 total_fails = 0 - log.info(u'Received {0} tracks in this page, processing...', total) + log.info('Received {0} tracks in this page, processing...', total) for num in range(0, total): song = None @@ -201,7 +199,7 @@ def process_tracks(lib, tracks, log): if 'album' in tracks[num]: album = tracks[num]['album'].get('name', '').strip() - log.debug(u'query: {0} - {1} ({2})', artist, title, album) + log.debug('query: {0} - {1} ({2})', artist, title, album) # First try to query by musicbrainz's trackid if trackid: @@ -211,7 +209,7 @@ def process_tracks(lib, tracks, log): # If not, try just artist/title if song is None: - log.debug(u'no album match, trying by artist/title') + log.debug('no album match, trying by artist/title') query = dbcore.AndQuery([ dbcore.query.SubstringQuery('artist', artist), dbcore.query.SubstringQuery('title', title) @@ -220,8 +218,8 @@ def process_tracks(lib, tracks, log): # Last resort, try just replacing to utf-8 quote if song is None: - title = title.replace("'", u'\u2019') - log.debug(u'no title match, trying utf-8 single quote') + title = title.replace("'", '\u2019') + log.debug('no title match, trying utf-8 single quote') query = dbcore.AndQuery([ dbcore.query.SubstringQuery('artist', artist), dbcore.query.SubstringQuery('title', title) @@ -231,19 +229,19 @@ def process_tracks(lib, tracks, log): if song is not None: count = int(song.get('play_count', 0)) new_count = int(tracks[num]['playcount']) - log.debug(u'match: {0} - {1} ({2}) ' - u'updating: play_count {3} => {4}', + log.debug('match: {0} - {1} ({2}) ' + 'updating: play_count {3} => {4}', song.artist, song.title, song.album, count, new_count) song['play_count'] = new_count song.store() total_found += 1 else: total_fails += 1 - log.info(u' - No match: {0} - {1} ({2})', + log.info(' - No match: {0} - {1} ({2})', artist, title, album) if total_fails > 0: - log.info(u'Acquired {0}/{1} play-counts ({2} unknown)', + log.info('Acquired {0}/{1} play-counts ({2} unknown)', total_found, total, total_fails) return total_found, total_fails diff --git a/libs/common/beetsplug/loadext.py b/libs/common/beetsplug/loadext.py new file mode 100644 index 00000000..191b97a2 --- /dev/null +++ b/libs/common/beetsplug/loadext.py @@ -0,0 +1,44 @@ +# This file is part of beets. +# Copyright 2019, Jack Wilsdon +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Load SQLite extensions. +""" + + +from beets.dbcore import Database +from beets.plugins import BeetsPlugin +import sqlite3 + + +class LoadExtPlugin(BeetsPlugin): + def __init__(self): + super().__init__() + + if not Database.supports_extensions: + self._log.warn('loadext is enabled but the current SQLite ' + 'installation does not support extensions') + return + + self.register_listener('library_opened', self.library_opened) + + def library_opened(self, lib): + for v in self.config: + ext = v.as_filename() + + self._log.debug('loading extension {}', ext) + + try: + lib.load_extension(ext) + except sqlite3.OperationalError as e: + self._log.error('failed to load extension {}: {}', ext, e) diff --git a/libs/common/beetsplug/lyrics.py b/libs/common/beetsplug/lyrics.py index 60f53759..2cb50ca5 100644 --- a/libs/common/beetsplug/lyrics.py +++ b/libs/common/beetsplug/lyrics.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -16,7 +15,6 @@ """Fetches, embeds, and displays lyrics. """ -from __future__ import absolute_import, division, print_function import difflib import errno @@ -29,11 +27,11 @@ import requests import unicodedata from unidecode import unidecode import warnings -import six -from six.moves import urllib +import urllib try: - from bs4 import SoupStrainer, BeautifulSoup + import bs4 + from bs4 import SoupStrainer HAS_BEAUTIFUL_SOUP = True except ImportError: HAS_BEAUTIFUL_SOUP = False @@ -48,7 +46,7 @@ try: # PY3: HTMLParseError was removed in 3.5 as strict mode # was deprecated in 3.3. # https://docs.python.org/3.3/library/html.parser.html - from six.moves.html_parser import HTMLParseError + from html.parser import HTMLParseError except ImportError: class HTMLParseError(Exception): pass @@ -62,23 +60,23 @@ COMMENT_RE = re.compile(r'', re.S) TAG_RE = re.compile(r'<[^>]*>') BREAK_RE = re.compile(r'\n?\s*]*)*>\s*\n?', re.I) URL_CHARACTERS = { - u'\u2018': u"'", - u'\u2019': u"'", - u'\u201c': u'"', - u'\u201d': u'"', - u'\u2010': u'-', - u'\u2011': u'-', - u'\u2012': u'-', - u'\u2013': u'-', - u'\u2014': u'-', - u'\u2015': u'-', - u'\u2016': u'-', - u'\u2026': u'...', + '\u2018': "'", + '\u2019': "'", + '\u201c': '"', + '\u201d': '"', + '\u2010': '-', + '\u2011': '-', + '\u2012': '-', + '\u2013': '-', + '\u2014': '-', + '\u2015': '-', + '\u2016': '-', + '\u2026': '...', } -USER_AGENT = 'beets/{}'.format(beets.__version__) +USER_AGENT = f'beets/{beets.__version__}' # The content for the base index.rst generated in ReST mode. -REST_INDEX_TEMPLATE = u'''Lyrics +REST_INDEX_TEMPLATE = '''Lyrics ====== * :ref:`Song index ` @@ -94,11 +92,11 @@ Artist index: ''' # The content for the base conf.py generated. -REST_CONF_TEMPLATE = u'''# -*- coding: utf-8 -*- +REST_CONF_TEMPLATE = '''# -*- coding: utf-8 -*- master_doc = 'index' -project = u'Lyrics' -copyright = u'none' -author = u'Various Authors' +project = 'Lyrics' +copyright = 'none' +author = 'Various Authors' latex_documents = [ (master_doc, 'Lyrics.tex', project, author, 'manual'), @@ -117,7 +115,7 @@ epub_tocdup = False def unichar(i): try: - return six.unichr(i) + return chr(i) except ValueError: return struct.pack('i', i).decode('utf-32') @@ -126,12 +124,12 @@ def unescape(text): """Resolve &#xxx; HTML entities (and some others).""" if isinstance(text, bytes): text = text.decode('utf-8', 'ignore') - out = text.replace(u' ', u' ') + out = text.replace(' ', ' ') def replchar(m): num = m.group(1) return unichar(int(num)) - out = re.sub(u"&#(\d+);", replchar, out) + out = re.sub("&#(\\d+);", replchar, out) return out @@ -140,43 +138,10 @@ def extract_text_between(html, start_marker, end_marker): _, html = html.split(start_marker, 1) html, _ = html.split(end_marker, 1) except ValueError: - return u'' + return '' return html -def extract_text_in(html, starttag): - """Extract the text from a
tag in the HTML starting with - ``starttag``. Returns None if parsing fails. - """ - # Strip off the leading text before opening tag. - try: - _, html = html.split(starttag, 1) - except ValueError: - return - - # Walk through balanced DIV tags. - level = 0 - parts = [] - pos = 0 - for match in DIV_RE.finditer(html): - if match.group(1): # Closing tag. - level -= 1 - if level == 0: - pos = match.end() - else: # Opening tag. - if level == 0: - parts.append(html[pos:match.start()]) - level += 1 - - if level == -1: - parts.append(html[pos:match.start()]) - break - else: - print(u'no closing tag found!') - return - return u''.join(parts) - - def search_pairs(item): """Yield a pairs of artists and titles to search for. @@ -186,6 +151,9 @@ def search_pairs(item): In addition to the artist and title obtained from the `item` the method tries to strip extra information like paranthesized suffixes and featured artists from the strings and add them as candidates. + The artist sort name is added as a fallback candidate to help in + cases where artist name includes special characters or is in a + non-latin script. The method also tries to split multiple titles separated with `/`. """ def generate_alternatives(string, patterns): @@ -199,19 +167,23 @@ def search_pairs(item): alternatives.append(match.group(1)) return alternatives - title, artist = item.title, item.artist + title, artist, artist_sort = item.title, item.artist, item.artist_sort patterns = [ # Remove any featuring artists from the artists name - r"(.*?) {0}".format(plugins.feat_tokens())] + fr"(.*?) {plugins.feat_tokens()}"] artists = generate_alternatives(artist, patterns) + # Use the artist_sort as fallback only if it differs from artist to avoid + # repeated remote requests with the same search terms + if artist != artist_sort: + artists.append(artist_sort) patterns = [ # Remove a parenthesized suffix from a title string. Common # examples include (live), (remix), and (acoustic). r"(.+?)\s+[(].*[)]$", # Remove any featuring artists from the title - r"(.*?) {0}".format(plugins.feat_tokens(for_artist=False)), + r"(.*?) {}".format(plugins.feat_tokens(for_artist=False)), # Remove part of title after colon ':' for songs with subtitles r"(.+?)\s*:.*"] titles = generate_alternatives(title, patterns) @@ -245,14 +217,27 @@ def slug(text): return re.sub(r'\W+', '-', unidecode(text).lower().strip()).strip('-') -class Backend(object): +if HAS_BEAUTIFUL_SOUP: + def try_parse_html(html, **kwargs): + try: + return bs4.BeautifulSoup(html, 'html.parser', **kwargs) + except HTMLParseError: + return None +else: + def try_parse_html(html, **kwargs): + return None + + +class Backend: + REQUIRES_BS = False + def __init__(self, config, log): self._log = log @staticmethod def _encode(s): """Encode the string for inclusion in a URL""" - if isinstance(s, six.text_type): + if isinstance(s, str): for char, repl in URL_CHARACTERS.items(): s = s.replace(char, repl) s = s.encode('utf-8', 'ignore') @@ -277,20 +262,21 @@ class Backend(object): 'User-Agent': USER_AGENT, }) except requests.RequestException as exc: - self._log.debug(u'lyrics request failed: {0}', exc) + self._log.debug('lyrics request failed: {0}', exc) return if r.status_code == requests.codes.ok: return r.text else: - self._log.debug(u'failed to fetch: {0} ({1})', url, r.status_code) + self._log.debug('failed to fetch: {0} ({1})', url, r.status_code) + return None def fetch(self, artist, title): raise NotImplementedError() -class SymbolsReplaced(Backend): +class MusiXmatch(Backend): REPLACEMENTS = { - r'\s+': '_', + r'\s+': '-', '<': 'Less_Than', '>': 'Greater_Than', '#': 'Number_', @@ -298,39 +284,40 @@ class SymbolsReplaced(Backend): r'[\]\}]': ')', } + URL_PATTERN = 'https://www.musixmatch.com/lyrics/%s/%s' + @classmethod def _encode(cls, s): for old, new in cls.REPLACEMENTS.items(): s = re.sub(old, new, s) - return super(SymbolsReplaced, cls)._encode(s) - - -class MusiXmatch(SymbolsReplaced): - REPLACEMENTS = dict(SymbolsReplaced.REPLACEMENTS, **{ - r'\s+': '-' - }) - - URL_PATTERN = 'https://www.musixmatch.com/lyrics/%s/%s' + return super()._encode(s) def fetch(self, artist, title): url = self.build_url(artist, title) html = self.fetch_url(url) if not html: - return + return None if "We detected that your IP is blocked" in html: - self._log.warning(u'we are blocked at MusixMatch: url %s failed' + self._log.warning('we are blocked at MusixMatch: url %s failed' % url) - return - html_part = html.split('

', '

')) + lyrics = '\n'.join(lyrics_parts) lyrics = lyrics.strip(',"').replace('\\n', '\n') # another odd case: sometimes only that string remains, for # missing songs. this seems to happen after being blocked # above, when filling in the CAPTCHA. if "Instant lyrics for all your music." in lyrics: - return + return None + # sometimes there are non-existent lyrics with some content + if 'Lyrics | Musixmatch' in lyrics: + return None return lyrics @@ -341,87 +328,171 @@ class Genius(Backend): bigishdata.com/2016/09/27/getting-song-lyrics-from-geniuss-api-scraping/ """ + REQUIRES_BS = True + base_url = "https://api.genius.com" def __init__(self, config, log): - super(Genius, self).__init__(config, log) + super().__init__(config, log) self.api_key = config['genius_api_key'].as_str() self.headers = { 'Authorization': "Bearer %s" % self.api_key, 'User-Agent': USER_AGENT, } - def lyrics_from_song_api_path(self, song_api_path): - song_url = self.base_url + song_api_path - response = requests.get(song_url, headers=self.headers) - json = response.json() - path = json["response"]["song"]["path"] - - # Gotta go regular html scraping... come on Genius. - page_url = "https://genius.com" + path - try: - page = requests.get(page_url) - except requests.RequestException as exc: - self._log.debug(u'Genius page request for {0} failed: {1}', - page_url, exc) - return None - html = BeautifulSoup(page.text, "html.parser") - - # Remove script tags that they put in the middle of the lyrics. - [h.extract() for h in html('script')] - - # At least Genius is nice and has a tag called 'lyrics'! - # Updated css where the lyrics are based in HTML. - lyrics = html.find("div", class_="lyrics").get_text() - - return lyrics - def fetch(self, artist, title): - search_url = self.base_url + "/search" - data = {'q': title} - try: - response = requests.get(search_url, data=data, - headers=self.headers) - except requests.RequestException as exc: - self._log.debug(u'Genius API request failed: {0}', exc) + """Fetch lyrics from genius.com + + Because genius doesn't allow accesssing lyrics via the api, + we first query the api for a url matching our artist & title, + then attempt to scrape that url for the lyrics. + """ + json = self._search(artist, title) + if not json: + self._log.debug('Genius API request returned invalid JSON') return None - try: - json = response.json() - except ValueError: - self._log.debug(u'Genius API request returned invalid JSON') - return None - - song_info = None + # find a matching artist in the json for hit in json["response"]["hits"]: - if hit["result"]["primary_artist"]["name"] == artist: - song_info = hit - break + hit_artist = hit["result"]["primary_artist"]["name"] - if song_info: - song_api_path = song_info["result"]["api_path"] - return self.lyrics_from_song_api_path(song_api_path) + if slug(hit_artist) == slug(artist): + html = self.fetch_url(hit["result"]["url"]) + if not html: + return None + return self._scrape_lyrics_from_html(html) + self._log.debug('Genius failed to find a matching artist for \'{0}\'', + artist) + return None -class LyricsWiki(SymbolsReplaced): - """Fetch lyrics from LyricsWiki.""" + def _search(self, artist, title): + """Searches the genius api for a given artist and title - URL_PATTERN = 'http://lyrics.wikia.com/%s:%s' + https://docs.genius.com/#search-h2 - def fetch(self, artist, title): - url = self.build_url(artist, title) - html = self.fetch_url(url) - if not html: + :returns: json response + """ + search_url = self.base_url + "/search" + data = {'q': title + " " + artist.lower()} + try: + response = requests.get( + search_url, data=data, headers=self.headers) + except requests.RequestException as exc: + self._log.debug('Genius API request failed: {0}', exc) + return None + + try: + return response.json() + except ValueError: + return None + + def _scrape_lyrics_from_html(self, html): + """Scrape lyrics from a given genius.com html""" + + soup = try_parse_html(html) + if not soup: return - # Get the HTML fragment inside the appropriate HTML element and then - # extract the text from it. - html_frag = extract_text_in(html, u"
") - if html_frag: - lyrics = _scrape_strip_cruft(html_frag, True) + # Remove script tags that they put in the middle of the lyrics. + [h.extract() for h in soup('script')] - if lyrics and 'Unfortunately, we are not licensed' not in lyrics: - return lyrics + # Most of the time, the page contains a div with class="lyrics" where + # all of the lyrics can be found already correctly formatted + # Sometimes, though, it packages the lyrics into separate divs, most + # likely for easier ad placement + lyrics_div = soup.find("div", class_="lyrics") + if not lyrics_div: + self._log.debug('Received unusual song page html') + verse_div = soup.find("div", + class_=re.compile("Lyrics__Container")) + if not verse_div: + if soup.find("div", + class_=re.compile("LyricsPlaceholder__Message"), + string="This song is an instrumental"): + self._log.debug('Detected instrumental') + return "[Instrumental]" + else: + self._log.debug("Couldn't scrape page using known layouts") + return None + + lyrics_div = verse_div.parent + for br in lyrics_div.find_all("br"): + br.replace_with("\n") + ads = lyrics_div.find_all("div", + class_=re.compile("InreadAd__Container")) + for ad in ads: + ad.replace_with("\n") + + return lyrics_div.get_text() + + +class Tekstowo(Backend): + # Fetch lyrics from Tekstowo.pl. + REQUIRES_BS = True + + BASE_URL = 'http://www.tekstowo.pl' + URL_PATTERN = BASE_URL + '/wyszukaj.html?search-title=%s&search-artist=%s' + + def fetch(self, artist, title): + url = self.build_url(title, artist) + search_results = self.fetch_url(url) + if not search_results: + return None + + song_page_url = self.parse_search_results(search_results) + if not song_page_url: + return None + + song_page_html = self.fetch_url(song_page_url) + if not song_page_html: + return None + + return self.extract_lyrics(song_page_html) + + def parse_search_results(self, html): + html = _scrape_strip_cruft(html) + html = _scrape_merge_paragraphs(html) + + soup = try_parse_html(html) + if not soup: + return None + + content_div = soup.find("div", class_="content") + if not content_div: + return None + + card_div = content_div.find("div", class_="card") + if not card_div: + return None + + song_rows = card_div.find_all("div", class_="box-przeboje") + if not song_rows: + return None + + song_row = song_rows[0] + if not song_row: + return None + + link = song_row.find('a') + if not link: + return None + + return self.BASE_URL + link.get('href') + + def extract_lyrics(self, html): + html = _scrape_strip_cruft(html) + html = _scrape_merge_paragraphs(html) + + soup = try_parse_html(html) + if not soup: + return None + + lyrics_div = soup.find("div", class_="song-text") + if not lyrics_div: + return None + + return lyrics_div.get_text() def remove_credits(text): @@ -446,7 +517,8 @@ def _scrape_strip_cruft(html, plain_text_out=False): html = html.replace('\r', '\n') # Normalize EOL. html = re.sub(r' +', ' ', html) # Whitespaces collapse. html = BREAK_RE.sub('\n', html) #
eats up surrounding '\n'. - html = re.sub(r'<(script).*?(?s)', '', html) # Strip script tags. + html = re.sub(r'(?s)<(script).*?', '', html) # Strip script tags. + html = re.sub('\u2005', " ", html) # replace unicode with regular space if plain_text_out: # Strip remaining HTML tags html = COMMENT_RE.sub('', html) @@ -466,12 +538,6 @@ def scrape_lyrics_from_html(html): """Scrape lyrics from a URL. If no lyrics can be found, return None instead. """ - if not HAS_BEAUTIFUL_SOUP: - return None - - if not html: - return None - def is_text_notcode(text): length = len(text) return (length > 20 and @@ -481,10 +547,8 @@ def scrape_lyrics_from_html(html): html = _scrape_merge_paragraphs(html) # extract all long text blocks that are not code - try: - soup = BeautifulSoup(html, "html.parser", - parse_only=SoupStrainer(text=is_text_notcode)) - except HTMLParseError: + soup = try_parse_html(html, parse_only=SoupStrainer(text=is_text_notcode)) + if not soup: return None # Get the longest text element (if any). @@ -498,8 +562,10 @@ def scrape_lyrics_from_html(html): class Google(Backend): """Fetch lyrics from Google search results.""" + REQUIRES_BS = True + def __init__(self, config, log): - super(Google, self).__init__(config, log) + super().__init__(config, log) self.api_key = config['google_API_key'].as_str() self.engine_id = config['google_engine_ID'].as_str() @@ -511,7 +577,7 @@ class Google(Backend): bad_triggers_occ = [] nb_lines = text.count('\n') if nb_lines <= 1: - self._log.debug(u"Ignoring too short lyrics '{0}'", text) + self._log.debug("Ignoring too short lyrics '{0}'", text) return False elif nb_lines < 5: bad_triggers_occ.append('too_short') @@ -522,14 +588,14 @@ class Google(Backend): bad_triggers = ['lyrics', 'copyright', 'property', 'links'] if artist: - bad_triggers_occ += [artist] + bad_triggers += [artist] for item in bad_triggers: bad_triggers_occ += [item] * len(re.findall(r'\W%s\W' % item, text, re.I)) if bad_triggers_occ: - self._log.debug(u'Bad triggers detected: {0}', bad_triggers_occ) + self._log.debug('Bad triggers detected: {0}', bad_triggers_occ) return len(bad_triggers_occ) < 2 def slugify(self, text): @@ -537,14 +603,14 @@ class Google(Backend): """ text = re.sub(r"[-'_\s]", '_', text) text = re.sub(r"_+", '_', text).strip('_') - pat = "([^,\(]*)\((.*?)\)" # Remove content within parentheses - text = re.sub(pat, '\g<1>', text).strip() + pat = r"([^,\(]*)\((.*?)\)" # Remove content within parentheses + text = re.sub(pat, r'\g<1>', text).strip() try: text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore') - text = six.text_type(re.sub('[-\s]+', ' ', text.decode('utf-8'))) + text = str(re.sub(r'[-\s]+', ' ', text.decode('utf-8'))) except UnicodeDecodeError: - self._log.exception(u"Failing to normalize '{0}'", text) + self._log.exception("Failing to normalize '{0}'", text) return text BY_TRANS = ['by', 'par', 'de', 'von'] @@ -556,7 +622,7 @@ class Google(Backend): """ title = self.slugify(title.lower()) artist = self.slugify(artist.lower()) - sitename = re.search(u"//([^/]+)/.*", + sitename = re.search("//([^/]+)/.*", self.slugify(url_link.lower())).group(1) url_title = self.slugify(url_title.lower()) @@ -570,7 +636,7 @@ class Google(Backend): [artist, sitename, sitename.replace('www.', '')] + \ self.LYRICS_TRANS tokens = [re.escape(t) for t in tokens] - song_title = re.sub(u'(%s)' % u'|'.join(tokens), u'', url_title) + song_title = re.sub('(%s)' % '|'.join(tokens), '', url_title) song_title = song_title.strip('_|') typo_ratio = .9 @@ -578,53 +644,57 @@ class Google(Backend): return ratio >= typo_ratio def fetch(self, artist, title): - query = u"%s %s" % (artist, title) - url = u'https://www.googleapis.com/customsearch/v1?key=%s&cx=%s&q=%s' \ + query = f"{artist} {title}" + url = 'https://www.googleapis.com/customsearch/v1?key=%s&cx=%s&q=%s' \ % (self.api_key, self.engine_id, urllib.parse.quote(query.encode('utf-8'))) data = self.fetch_url(url) if not data: - self._log.debug(u'google backend returned no data') + self._log.debug('google backend returned no data') return None try: data = json.loads(data) except ValueError as exc: - self._log.debug(u'google backend returned malformed JSON: {}', exc) + self._log.debug('google backend returned malformed JSON: {}', exc) if 'error' in data: reason = data['error']['errors'][0]['reason'] - self._log.debug(u'google backend error: {0}', reason) + self._log.debug('google backend error: {0}', reason) return None if 'items' in data.keys(): for item in data['items']: url_link = item['link'] - url_title = item.get('title', u'') + url_title = item.get('title', '') if not self.is_page_candidate(url_link, url_title, title, artist): continue html = self.fetch_url(url_link) + if not html: + continue lyrics = scrape_lyrics_from_html(html) if not lyrics: continue if self.is_lyrics(lyrics, artist): - self._log.debug(u'got lyrics from {0}', + self._log.debug('got lyrics from {0}', item['displayLink']) return lyrics + return None + class LyricsPlugin(plugins.BeetsPlugin): - SOURCES = ['google', 'lyricwiki', 'musixmatch', 'genius'] + SOURCES = ['google', 'musixmatch', 'genius', 'tekstowo'] SOURCE_BACKENDS = { 'google': Google, - 'lyricwiki': LyricsWiki, 'musixmatch': MusiXmatch, 'genius': Genius, + 'tekstowo': Tekstowo, } def __init__(self): - super(LyricsPlugin, self).__init__() + super().__init__() self.import_stages = [self.imported] self.config.add({ 'auto': True, @@ -632,7 +702,7 @@ class LyricsPlugin(plugins.BeetsPlugin): 'bing_lang_from': [], 'bing_lang_to': None, 'google_API_key': None, - 'google_engine_ID': u'009217259823014548361:lndtuqkycfu', + 'google_engine_ID': '009217259823014548361:lndtuqkycfu', 'genius_api_key': "Ryq93pUGm8bM6eUWwD_M3NOFFDAtp2yEE7W" "76V-uFL5jks5dNvcGCdarqFjDhP9c", @@ -648,7 +718,7 @@ class LyricsPlugin(plugins.BeetsPlugin): # State information for the ReST writer. # First, the current artist we're writing. - self.artist = u'Unknown artist' + self.artist = 'Unknown artist' # The current album: False means no album yet. self.album = False # The current rest file content. None means the file is not @@ -659,40 +729,44 @@ class LyricsPlugin(plugins.BeetsPlugin): sources = plugins.sanitize_choices( self.config['sources'].as_str_seq(), available_sources) + if not HAS_BEAUTIFUL_SOUP: + sources = self.sanitize_bs_sources(sources) + if 'google' in sources: if not self.config['google_API_key'].get(): # We log a *debug* message here because the default # configuration includes `google`. This way, the source # is silent by default but can be enabled just by # setting an API key. - self._log.debug(u'Disabling google source: ' - u'no API key configured.') + self._log.debug('Disabling google source: ' + 'no API key configured.') sources.remove('google') - elif not HAS_BEAUTIFUL_SOUP: - self._log.warning(u'To use the google lyrics source, you must ' - u'install the beautifulsoup4 module. See ' - u'the documentation for further details.') - sources.remove('google') - - if 'genius' in sources and not HAS_BEAUTIFUL_SOUP: - self._log.debug( - u'The Genius backend requires BeautifulSoup, which is not ' - u'installed, so the source is disabled.' - ) - sources.remove('genius') self.config['bing_lang_from'] = [ x.lower() for x in self.config['bing_lang_from'].as_str_seq()] self.bing_auth_token = None if not HAS_LANGDETECT and self.config['bing_client_secret'].get(): - self._log.warning(u'To use bing translations, you need to ' - u'install the langdetect module. See the ' - u'documentation for further details.') + self._log.warning('To use bing translations, you need to ' + 'install the langdetect module. See the ' + 'documentation for further details.') self.backends = [self.SOURCE_BACKENDS[source](self.config, self._log) for source in sources] + def sanitize_bs_sources(self, sources): + enabled_sources = [] + for source in sources: + if self.SOURCE_BACKENDS[source].REQUIRES_BS: + self._log.debug('To use the %s lyrics source, you must ' + 'install the beautifulsoup4 module. See ' + 'the documentation for further details.' + % source) + else: + enabled_sources.append(source) + + return enabled_sources + def get_bing_access_token(self): params = { 'client_id': 'beets', @@ -708,30 +782,30 @@ class LyricsPlugin(plugins.BeetsPlugin): if 'access_token' in oauth_token: return "Bearer " + oauth_token['access_token'] else: - self._log.warning(u'Could not get Bing Translate API access token.' - u' Check your "bing_client_secret" password') + self._log.warning('Could not get Bing Translate API access token.' + ' Check your "bing_client_secret" password') def commands(self): cmd = ui.Subcommand('lyrics', help='fetch song lyrics') cmd.parser.add_option( - u'-p', u'--print', dest='printlyr', + '-p', '--print', dest='printlyr', action='store_true', default=False, - help=u'print lyrics to console', + help='print lyrics to console', ) cmd.parser.add_option( - u'-r', u'--write-rest', dest='writerest', + '-r', '--write-rest', dest='writerest', action='store', default=None, metavar='dir', - help=u'write lyrics to given directory as ReST files', + help='write lyrics to given directory as ReST files', ) cmd.parser.add_option( - u'-f', u'--force', dest='force_refetch', + '-f', '--force', dest='force_refetch', action='store_true', default=False, - help=u'always re-download lyrics', + help='always re-download lyrics', ) cmd.parser.add_option( - u'-l', u'--local', dest='local_only', + '-l', '--local', dest='local_only', action='store_true', default=False, - help=u'do not fetch missing lyrics', + help='do not fetch missing lyrics', ) def func(lib, opts, args): @@ -740,7 +814,8 @@ class LyricsPlugin(plugins.BeetsPlugin): write = ui.should_write() if opts.writerest: self.writerest_indexes(opts.writerest) - for item in lib.items(ui.decargs(args)): + items = lib.items(ui.decargs(args)) + for item in items: if not opts.local_only and not self.config['local']: self.fetch_item_lyrics( lib, item, write, @@ -750,51 +825,55 @@ class LyricsPlugin(plugins.BeetsPlugin): if opts.printlyr: ui.print_(item.lyrics) if opts.writerest: - self.writerest(opts.writerest, item) - if opts.writerest: - # flush last artist - self.writerest(opts.writerest, None) - ui.print_(u'ReST files generated. to build, use one of:') - ui.print_(u' sphinx-build -b html %s _build/html' + self.appendrest(opts.writerest, item) + if opts.writerest and items: + # flush last artist & write to ReST + self.writerest(opts.writerest) + ui.print_('ReST files generated. to build, use one of:') + ui.print_(' sphinx-build -b html %s _build/html' % opts.writerest) - ui.print_(u' sphinx-build -b epub %s _build/epub' + ui.print_(' sphinx-build -b epub %s _build/epub' % opts.writerest) - ui.print_((u' sphinx-build -b latex %s _build/latex ' - u'&& make -C _build/latex all-pdf') + ui.print_((' sphinx-build -b latex %s _build/latex ' + '&& make -C _build/latex all-pdf') % opts.writerest) cmd.func = func return [cmd] - def writerest(self, directory, item): - """Write the item to an ReST file + def appendrest(self, directory, item): + """Append the item to an ReST file This will keep state (in the `rest` variable) in order to avoid writing continuously to the same files. """ - if item is None or slug(self.artist) != slug(item.albumartist): - if self.rest is not None: - path = os.path.join(directory, 'artists', - slug(self.artist) + u'.rst') - with open(path, 'wb') as output: - output.write(self.rest.encode('utf-8')) - self.rest = None - if item is None: - return + if slug(self.artist) != slug(item.albumartist): + # Write current file and start a new one ~ item.albumartist + self.writerest(directory) self.artist = item.albumartist.strip() - self.rest = u"%s\n%s\n\n.. contents::\n :local:\n\n" \ + self.rest = "%s\n%s\n\n.. contents::\n :local:\n\n" \ % (self.artist, - u'=' * len(self.artist)) + '=' * len(self.artist)) + if self.album != item.album: tmpalbum = self.album = item.album.strip() if self.album == '': - tmpalbum = u'Unknown album' - self.rest += u"%s\n%s\n\n" % (tmpalbum, u'-' * len(tmpalbum)) - title_str = u":index:`%s`" % item.title.strip() - block = u'| ' + item.lyrics.replace(u'\n', u'\n| ') - self.rest += u"%s\n%s\n\n%s\n\n" % (title_str, - u'~' * len(title_str), - block) + tmpalbum = 'Unknown album' + self.rest += "{}\n{}\n\n".format(tmpalbum, '-' * len(tmpalbum)) + title_str = ":index:`%s`" % item.title.strip() + block = '| ' + item.lyrics.replace('\n', '\n| ') + self.rest += "{}\n{}\n\n{}\n\n".format(title_str, + '~' * len(title_str), + block) + + def writerest(self, directory): + """Write self.rest to a ReST file + """ + if self.rest is not None and self.artist is not None: + path = os.path.join(directory, 'artists', + slug(self.artist) + '.rst') + with open(path, 'wb') as output: + output.write(self.rest.encode('utf-8')) def writerest_indexes(self, directory): """Write conf.py and index.rst files necessary for Sphinx @@ -832,7 +911,7 @@ class LyricsPlugin(plugins.BeetsPlugin): """ # Skip if the item already has lyrics. if not force and item.lyrics: - self._log.info(u'lyrics already present: {0}', item) + self._log.info('lyrics already present: {0}', item) return lyrics = None @@ -841,10 +920,10 @@ class LyricsPlugin(plugins.BeetsPlugin): if any(lyrics): break - lyrics = u"\n\n---\n\n".join([l for l in lyrics if l]) + lyrics = "\n\n---\n\n".join([l for l in lyrics if l]) if lyrics: - self._log.info(u'fetched lyrics: {0}', item) + self._log.info('fetched lyrics: {0}', item) if HAS_LANGDETECT and self.config['bing_client_secret'].get(): lang_from = langdetect.detect(lyrics) if self.config['bing_lang_to'].get() != lang_from and ( @@ -854,7 +933,7 @@ class LyricsPlugin(plugins.BeetsPlugin): lyrics = self.append_translation( lyrics, self.config['bing_lang_to']) else: - self._log.info(u'lyrics not found: {0}', item) + self._log.info('lyrics not found: {0}', item) fallback = self.config['fallback'].get() if fallback: lyrics = fallback @@ -872,12 +951,12 @@ class LyricsPlugin(plugins.BeetsPlugin): for backend in self.backends: lyrics = backend.fetch(artist, title) if lyrics: - self._log.debug(u'got lyrics from backend: {0}', + self._log.debug('got lyrics from backend: {0}', backend.__class__.__name__) return _scrape_strip_cruft(lyrics, True) def append_translation(self, text, to_lang): - import xml.etree.ElementTree as ET + from xml.etree import ElementTree if not self.bing_auth_token: self.bing_auth_token = self.get_bing_access_token() @@ -895,10 +974,11 @@ class LyricsPlugin(plugins.BeetsPlugin): self.bing_auth_token = None return self.append_translation(text, to_lang) return text - lines_translated = ET.fromstring(r.text.encode('utf-8')).text + lines_translated = ElementTree.fromstring( + r.text.encode('utf-8')).text # Use a translation mapping dict to build resulting lyrics translations = dict(zip(text_lines, lines_translated.split('|'))) result = '' for line in text.split('\n'): - result += '%s / %s\n' % (line, translations[line]) + result += '{} / {}\n'.format(line, translations[line]) return result diff --git a/libs/common/beetsplug/mbcollection.py b/libs/common/beetsplug/mbcollection.py index d99c386c..f4a0d161 100644 --- a/libs/common/beetsplug/mbcollection.py +++ b/libs/common/beetsplug/mbcollection.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright (c) 2011, Jeffrey Aylesworth # @@ -13,7 +12,6 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets.ui import Subcommand @@ -34,11 +32,11 @@ def mb_call(func, *args, **kwargs): try: return func(*args, **kwargs) except musicbrainzngs.AuthenticationError: - raise ui.UserError(u'authentication with MusicBrainz failed') + raise ui.UserError('authentication with MusicBrainz failed') except (musicbrainzngs.ResponseError, musicbrainzngs.NetworkError) as exc: - raise ui.UserError(u'MusicBrainz API error: {0}'.format(exc)) + raise ui.UserError(f'MusicBrainz API error: {exc}') except musicbrainzngs.UsageError: - raise ui.UserError(u'MusicBrainz credentials missing') + raise ui.UserError('MusicBrainz credentials missing') def submit_albums(collection_id, release_ids): @@ -55,7 +53,7 @@ def submit_albums(collection_id, release_ids): class MusicBrainzCollectionPlugin(BeetsPlugin): def __init__(self): - super(MusicBrainzCollectionPlugin, self).__init__() + super().__init__() config['musicbrainz']['pass'].redact = True musicbrainzngs.auth( config['musicbrainz']['user'].as_str(), @@ -63,7 +61,7 @@ class MusicBrainzCollectionPlugin(BeetsPlugin): ) self.config.add({ 'auto': False, - 'collection': u'', + 'collection': '', 'remove': False, }) if self.config['auto']: @@ -72,18 +70,18 @@ class MusicBrainzCollectionPlugin(BeetsPlugin): def _get_collection(self): collections = mb_call(musicbrainzngs.get_collections) if not collections['collection-list']: - raise ui.UserError(u'no collections exist for user') + raise ui.UserError('no collections exist for user') # Get all collection IDs, avoiding event collections collection_ids = [x['id'] for x in collections['collection-list']] if not collection_ids: - raise ui.UserError(u'No collection found.') + raise ui.UserError('No collection found.') # Check that the collection exists so we can present a nice error collection = self.config['collection'].as_str() if collection: if collection not in collection_ids: - raise ui.UserError(u'invalid collection ID: {}' + raise ui.UserError('invalid collection ID: {}' .format(collection)) return collection @@ -110,7 +108,7 @@ class MusicBrainzCollectionPlugin(BeetsPlugin): def commands(self): mbupdate = Subcommand('mbupdate', - help=u'Update MusicBrainz collection') + help='Update MusicBrainz collection') mbupdate.parser.add_option('-r', '--remove', action='store_true', default=None, @@ -120,7 +118,7 @@ class MusicBrainzCollectionPlugin(BeetsPlugin): return [mbupdate] def remove_missing(self, collection_id, lib_albums): - lib_ids = set([x.mb_albumid for x in lib_albums]) + lib_ids = {x.mb_albumid for x in lib_albums} albums_in_collection = self._get_albums_in_collection(collection_id) remove_me = list(set(albums_in_collection) - lib_ids) for i in range(0, len(remove_me), FETCH_CHUNK_SIZE): @@ -154,13 +152,13 @@ class MusicBrainzCollectionPlugin(BeetsPlugin): if re.match(UUID_REGEX, aid): album_ids.append(aid) else: - self._log.info(u'skipping invalid MBID: {0}', aid) + self._log.info('skipping invalid MBID: {0}', aid) # Submit to MusicBrainz. self._log.info( - u'Updating MusicBrainz collection {0}...', collection_id + 'Updating MusicBrainz collection {0}...', collection_id ) submit_albums(collection_id, album_ids) if remove_missing: self.remove_missing(collection_id, lib.albums()) - self._log.info(u'...MusicBrainz collection updated.') + self._log.info('...MusicBrainz collection updated.') diff --git a/libs/common/beetsplug/mbsubmit.py b/libs/common/beetsplug/mbsubmit.py index 02bd5f69..3ede0125 100644 --- a/libs/common/beetsplug/mbsubmit.py +++ b/libs/common/beetsplug/mbsubmit.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson and Diego Moreda. # @@ -19,11 +18,9 @@ This plugin allows the user to print track information in a format that is parseable by the MusicBrainz track parser [1]. Programmatic submitting is not implemented by MusicBrainz yet. -[1] http://wiki.musicbrainz.org/History:How_To_Parse_Track_Listings +[1] https://wiki.musicbrainz.org/History:How_To_Parse_Track_Listings """ -from __future__ import division, absolute_import, print_function - from beets.autotag import Recommendation from beets.plugins import BeetsPlugin @@ -33,10 +30,10 @@ from beetsplug.info import print_data class MBSubmitPlugin(BeetsPlugin): def __init__(self): - super(MBSubmitPlugin, self).__init__() + super().__init__() self.config.add({ - 'format': u'$track. $title - $artist ($length)', + 'format': '$track. $title - $artist ($length)', 'threshold': 'medium', }) @@ -53,7 +50,7 @@ class MBSubmitPlugin(BeetsPlugin): def before_choose_candidate_event(self, session, task): if task.rec <= self.threshold: - return [PromptChoice(u'p', u'Print tracks', self.print_tracks)] + return [PromptChoice('p', 'Print tracks', self.print_tracks)] def print_tracks(self, session, task): for i in sorted(task.items, key=lambda i: i.track): diff --git a/libs/common/beetsplug/mbsync.py b/libs/common/beetsplug/mbsync.py index 1764a177..26778830 100644 --- a/libs/common/beetsplug/mbsync.py +++ b/libs/common/beetsplug/mbsync.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Jakob Schnitzer. # @@ -15,47 +14,37 @@ """Update library's tags using MusicBrainz. """ -from __future__ import division, absolute_import, print_function -from beets.plugins import BeetsPlugin +from beets.plugins import BeetsPlugin, apply_item_changes from beets import autotag, library, ui, util from beets.autotag import hooks from collections import defaultdict +import re -def apply_item_changes(lib, item, move, pretend, write): - """Store, move and write the item according to the arguments. - """ - if not pretend: - # Move the item if it's in the library. - if move and lib.directory in util.ancestry(item.path): - item.move(with_album=False) - - if write: - item.try_write() - item.store() +MBID_REGEX = r"(\d|\w){8}-(\d|\w){4}-(\d|\w){4}-(\d|\w){4}-(\d|\w){12}" class MBSyncPlugin(BeetsPlugin): def __init__(self): - super(MBSyncPlugin, self).__init__() + super().__init__() def commands(self): cmd = ui.Subcommand('mbsync', - help=u'update metadata from musicbrainz') + help='update metadata from musicbrainz') cmd.parser.add_option( - u'-p', u'--pretend', action='store_true', - help=u'show all changes but do nothing') + '-p', '--pretend', action='store_true', + help='show all changes but do nothing') cmd.parser.add_option( - u'-m', u'--move', action='store_true', dest='move', - help=u"move files in the library directory") + '-m', '--move', action='store_true', dest='move', + help="move files in the library directory") cmd.parser.add_option( - u'-M', u'--nomove', action='store_false', dest='move', - help=u"don't move files in library") + '-M', '--nomove', action='store_false', dest='move', + help="don't move files in library") cmd.parser.add_option( - u'-W', u'--nowrite', action='store_false', + '-W', '--nowrite', action='store_false', default=None, dest='write', - help=u"don't write updated metadata to files") + help="don't write updated metadata to files") cmd.parser.add_format_option() cmd.func = self.func return [cmd] @@ -75,17 +64,23 @@ class MBSyncPlugin(BeetsPlugin): """Retrieve and apply info from the autotagger for items matched by query. """ - for item in lib.items(query + [u'singleton:true']): + for item in lib.items(query + ['singleton:true']): item_formatted = format(item) if not item.mb_trackid: - self._log.info(u'Skipping singleton with no mb_trackid: {0}', + self._log.info('Skipping singleton with no mb_trackid: {0}', item_formatted) continue + # Do we have a valid MusicBrainz track ID? + if not re.match(MBID_REGEX, item.mb_trackid): + self._log.info('Skipping singleton with invalid mb_trackid:' + + ' {0}', item_formatted) + continue + # Get the MusicBrainz recording info. track_info = hooks.track_for_mbid(item.mb_trackid) if not track_info: - self._log.info(u'Recording ID not found: {0} for track {0}', + self._log.info('Recording ID not found: {0} for track {0}', item.mb_trackid, item_formatted) continue @@ -103,16 +98,22 @@ class MBSyncPlugin(BeetsPlugin): for a in lib.albums(query): album_formatted = format(a) if not a.mb_albumid: - self._log.info(u'Skipping album with no mb_albumid: {0}', + self._log.info('Skipping album with no mb_albumid: {0}', album_formatted) continue items = list(a.items()) + # Do we have a valid MusicBrainz album ID? + if not re.match(MBID_REGEX, a.mb_albumid): + self._log.info('Skipping album with invalid mb_albumid: {0}', + album_formatted) + continue + # Get the MusicBrainz album information. album_info = hooks.album_for_mbid(a.mb_albumid) if not album_info: - self._log.info(u'Release ID {0} not found for album {1}', + self._log.info('Release ID {0} not found for album {1}', a.mb_albumid, album_formatted) continue @@ -120,7 +121,7 @@ class MBSyncPlugin(BeetsPlugin): # Map release track and recording MBIDs to their information. # Recordings can appear multiple times on a release, so each MBID # maps to a list of TrackInfo objects. - releasetrack_index = dict() + releasetrack_index = {} track_index = defaultdict(list) for track_info in album_info.tracks: releasetrack_index[track_info.release_track_id] = track_info @@ -148,7 +149,7 @@ class MBSyncPlugin(BeetsPlugin): break # Apply. - self._log.debug(u'applying changes to {}', album_formatted) + self._log.debug('applying changes to {}', album_formatted) with lib.transaction(): autotag.apply_metadata(album_info, mapping) changed = False @@ -173,5 +174,5 @@ class MBSyncPlugin(BeetsPlugin): # Move album art (and any inconsistent items). if move and lib.directory in util.ancestry(items[0].path): - self._log.debug(u'moving album {0}', album_formatted) + self._log.debug('moving album {0}', album_formatted) a.move() diff --git a/libs/common/beetsplug/metasync/__init__.py b/libs/common/beetsplug/metasync/__init__.py index 02f0b0f9..361071fb 100644 --- a/libs/common/beetsplug/metasync/__init__.py +++ b/libs/common/beetsplug/metasync/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Heinz Wiesinger. # @@ -16,15 +15,13 @@ """Synchronize information from music player libraries """ -from __future__ import division, absolute_import, print_function from abc import abstractmethod, ABCMeta from importlib import import_module -from beets.util.confit import ConfigValueError +from confuse import ConfigValueError from beets import ui from beets.plugins import BeetsPlugin -import six METASYNC_MODULE = 'beetsplug.metasync' @@ -36,7 +33,7 @@ SOURCES = { } -class MetaSource(six.with_metaclass(ABCMeta, object)): +class MetaSource(metaclass=ABCMeta): def __init__(self, config, log): self.item_types = {} self.config = config @@ -77,7 +74,7 @@ class MetaSyncPlugin(BeetsPlugin): item_types = load_item_types() def __init__(self): - super(MetaSyncPlugin, self).__init__() + super().__init__() def commands(self): cmd = ui.Subcommand('metasync', @@ -108,7 +105,7 @@ class MetaSyncPlugin(BeetsPlugin): # Avoid needlessly instantiating meta sources (can be expensive) if not items: - self._log.info(u'No items found matching query') + self._log.info('No items found matching query') return # Instantiate the meta sources @@ -116,18 +113,18 @@ class MetaSyncPlugin(BeetsPlugin): try: cls = META_SOURCES[player] except KeyError: - self._log.error(u'Unknown metadata source \'{0}\''.format( + self._log.error('Unknown metadata source \'{}\''.format( player)) try: meta_source_instances[player] = cls(self.config, self._log) except (ImportError, ConfigValueError) as e: - self._log.error(u'Failed to instantiate metadata source ' - u'\'{0}\': {1}'.format(player, e)) + self._log.error('Failed to instantiate metadata source ' + '\'{}\': {}'.format(player, e)) # Avoid needlessly iterating over items if not meta_source_instances: - self._log.error(u'No valid metadata sources found') + self._log.error('No valid metadata sources found') return # Sync the items with all of the meta sources diff --git a/libs/common/beetsplug/metasync/amarok.py b/libs/common/beetsplug/metasync/amarok.py index 0622fc17..a49eecc3 100644 --- a/libs/common/beetsplug/metasync/amarok.py +++ b/libs/common/beetsplug/metasync/amarok.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Heinz Wiesinger. # @@ -16,7 +15,6 @@ """Synchronize information from amarok's library via dbus """ -from __future__ import division, absolute_import, print_function from os.path import basename from datetime import datetime @@ -49,14 +47,14 @@ class Amarok(MetaSource): 'amarok_lastplayed': DateType(), } - queryXML = u' \ + query_xml = ' \ \ \ \ ' def __init__(self, config, log): - super(Amarok, self).__init__(config, log) + super().__init__(config, log) if not dbus: raise ImportError('failed to import dbus') @@ -72,7 +70,7 @@ class Amarok(MetaSource): # of the result set. So query for the filename and then try to match # the correct item from the results we get back results = self.collection.Query( - self.queryXML % quoteattr(basename(path)) + self.query_xml % quoteattr(basename(path)) ) for result in results: if result['xesam:url'] != path: diff --git a/libs/common/beetsplug/metasync/itunes.py b/libs/common/beetsplug/metasync/itunes.py index 17ab1637..e50a5713 100644 --- a/libs/common/beetsplug/metasync/itunes.py +++ b/libs/common/beetsplug/metasync/itunes.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Tom Jaspers. # @@ -16,7 +15,6 @@ """Synchronize information from iTunes's library """ -from __future__ import division, absolute_import, print_function from contextlib import contextmanager import os @@ -24,13 +22,13 @@ import shutil import tempfile import plistlib -from six.moves.urllib.parse import urlparse, unquote +from urllib.parse import urlparse, unquote from time import mktime from beets import util from beets.dbcore import types from beets.library import DateType -from beets.util.confit import ConfigValueError +from confuse import ConfigValueError from beetsplug.metasync import MetaSource @@ -63,15 +61,16 @@ def _norm_itunes_path(path): class Itunes(MetaSource): item_types = { - 'itunes_rating': types.INTEGER, # 0..100 scale - 'itunes_playcount': types.INTEGER, - 'itunes_skipcount': types.INTEGER, - 'itunes_lastplayed': DateType(), + 'itunes_rating': types.INTEGER, # 0..100 scale + 'itunes_playcount': types.INTEGER, + 'itunes_skipcount': types.INTEGER, + 'itunes_lastplayed': DateType(), 'itunes_lastskipped': DateType(), + 'itunes_dateadded': DateType(), } def __init__(self, config, log): - super(Itunes, self).__init__(config, log) + super().__init__(config, log) config.add({'itunes': { 'library': '~/Music/iTunes/iTunes Library.xml' @@ -82,19 +81,20 @@ class Itunes(MetaSource): try: self._log.debug( - u'loading iTunes library from {0}'.format(library_path)) + f'loading iTunes library from {library_path}') with create_temporary_copy(library_path) as library_copy: - raw_library = plistlib.readPlist(library_copy) - except IOError as e: - raise ConfigValueError(u'invalid iTunes library: ' + e.strerror) + with open(library_copy, 'rb') as library_copy_f: + raw_library = plistlib.load(library_copy_f) + except OSError as e: + raise ConfigValueError('invalid iTunes library: ' + e.strerror) except Exception: # It's likely the user configured their '.itl' library (<> xml) if os.path.splitext(library_path)[1].lower() != '.xml': - hint = u': please ensure that the configured path' \ - u' points to the .XML library' + hint = ': please ensure that the configured path' \ + ' points to the .XML library' else: hint = '' - raise ConfigValueError(u'invalid iTunes library' + hint) + raise ConfigValueError('invalid iTunes library' + hint) # Make the iTunes library queryable using the path self.collection = {_norm_itunes_path(track['Location']): track @@ -105,7 +105,7 @@ class Itunes(MetaSource): result = self.collection.get(util.bytestring_path(item.path).lower()) if not result: - self._log.warning(u'no iTunes match found for {0}'.format(item)) + self._log.warning(f'no iTunes match found for {item}') return item.itunes_rating = result.get('Rating') @@ -119,3 +119,7 @@ class Itunes(MetaSource): if result.get('Skip Date'): item.itunes_lastskipped = mktime( result.get('Skip Date').timetuple()) + + if result.get('Date Added'): + item.itunes_dateadded = mktime( + result.get('Date Added').timetuple()) diff --git a/libs/common/beetsplug/missing.py b/libs/common/beetsplug/missing.py index 8f0790f2..771978c1 100644 --- a/libs/common/beetsplug/missing.py +++ b/libs/common/beetsplug/missing.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Pedro Silva. # Copyright 2017, Quentin Young. @@ -16,7 +15,6 @@ """List missing tracks. """ -from __future__ import division, absolute_import, print_function import musicbrainzngs @@ -93,7 +91,7 @@ class MissingPlugin(BeetsPlugin): } def __init__(self): - super(MissingPlugin, self).__init__() + super().__init__() self.config.add({ 'count': False, @@ -107,14 +105,14 @@ class MissingPlugin(BeetsPlugin): help=__doc__, aliases=['miss']) self._command.parser.add_option( - u'-c', u'--count', dest='count', action='store_true', - help=u'count missing tracks per album') + '-c', '--count', dest='count', action='store_true', + help='count missing tracks per album') self._command.parser.add_option( - u'-t', u'--total', dest='total', action='store_true', - help=u'count total of missing tracks') + '-t', '--total', dest='total', action='store_true', + help='count total of missing tracks') self._command.parser.add_option( - u'-a', u'--album', dest='album', action='store_true', - help=u'show missing albums for artist instead of tracks') + '-a', '--album', dest='album', action='store_true', + help='show missing albums for artist instead of tracks') self._command.parser.add_format_option() def commands(self): @@ -173,10 +171,10 @@ class MissingPlugin(BeetsPlugin): # build dict mapping artist to list of all albums for artist, albums in albums_by_artist.items(): if artist[1] is None or artist[1] == "": - albs_no_mbid = [u"'" + a['album'] + u"'" for a in albums] + albs_no_mbid = ["'" + a['album'] + "'" for a in albums] self._log.info( - u"No musicbrainz ID for artist '{}' found in album(s) {}; " - "skipping", artist[0], u", ".join(albs_no_mbid) + "No musicbrainz ID for artist '{}' found in album(s) {}; " + "skipping", artist[0], ", ".join(albs_no_mbid) ) continue @@ -185,7 +183,7 @@ class MissingPlugin(BeetsPlugin): release_groups = resp['release-group-list'] except MusicBrainzError as err: self._log.info( - u"Couldn't fetch info for artist '{}' ({}) - '{}'", + "Couldn't fetch info for artist '{}' ({}) - '{}'", artist[0], artist[1], err ) continue @@ -207,7 +205,7 @@ class MissingPlugin(BeetsPlugin): missing_titles = {rg['title'] for rg in missing} for release_title in missing_titles: - print_(u"{} - {}".format(artist[0], release_title)) + print_("{} - {}".format(artist[0], release_title)) if total: print(total_missing) @@ -216,13 +214,13 @@ class MissingPlugin(BeetsPlugin): """Query MusicBrainz to determine items missing from `album`. """ item_mbids = [x.mb_trackid for x in album.items()] - if len([i for i in album.items()]) < album.albumtotal: + if len(list(album.items())) < album.albumtotal: # fetch missing items # TODO: Implement caching that without breaking other stuff album_info = hooks.album_for_mbid(album.mb_albumid) for track_info in getattr(album_info, 'tracks', []): if track_info.track_id not in item_mbids: item = _item(track_info, album_info, album.id) - self._log.debug(u'track {0} in album {1}', + self._log.debug('track {0} in album {1}', track_info.track_id, album_info.album_id) yield item diff --git a/libs/common/beetsplug/mpdstats.py b/libs/common/beetsplug/mpdstats.py index e5e82d48..96291cf4 100644 --- a/libs/common/beetsplug/mpdstats.py +++ b/libs/common/beetsplug/mpdstats.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Peter Schnebel and Johann Klähn. # @@ -13,11 +12,8 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function import mpd -import socket -import select import time import os @@ -45,14 +41,21 @@ def is_url(path): return path.split('://', 1)[0] in ['http', 'https'] -class MPDClientWrapper(object): +class MPDClientWrapper: def __init__(self, log): self._log = log - self.music_directory = ( - mpd_config['music_directory'].as_str()) + self.music_directory = mpd_config['music_directory'].as_str() + self.strip_path = mpd_config['strip_path'].as_str() - self.client = mpd.MPDClient(use_unicode=True) + # Ensure strip_path end with '/' + if not self.strip_path.endswith('/'): + self.strip_path += '/' + + self._log.debug('music_directory: {0}', self.music_directory) + self._log.debug('strip_path: {0}', self.strip_path) + + self.client = mpd.MPDClient() def connect(self): """Connect to the MPD. @@ -63,11 +66,11 @@ class MPDClientWrapper(object): if host[0] in ['/', '~']: host = os.path.expanduser(host) - self._log.info(u'connecting to {0}:{1}', host, port) + self._log.info('connecting to {0}:{1}', host, port) try: self.client.connect(host, port) - except socket.error as e: - raise ui.UserError(u'could not connect to MPD: {0}'.format(e)) + except OSError as e: + raise ui.UserError(f'could not connect to MPD: {e}') password = mpd_config['password'].as_str() if password: @@ -75,7 +78,7 @@ class MPDClientWrapper(object): self.client.password(password) except mpd.CommandError as e: raise ui.UserError( - u'could not authenticate to MPD: {0}'.format(e) + f'could not authenticate to MPD: {e}' ) def disconnect(self): @@ -90,12 +93,12 @@ class MPDClientWrapper(object): """ try: return getattr(self.client, command)() - except (select.error, mpd.ConnectionError) as err: - self._log.error(u'{0}', err) + except (OSError, mpd.ConnectionError) as err: + self._log.error('{0}', err) if retries <= 0: # if we exited without breaking, we couldn't reconnect in time :( - raise ui.UserError(u'communication with MPD server failed') + raise ui.UserError('communication with MPD server failed') time.sleep(RETRY_INTERVAL) @@ -107,18 +110,26 @@ class MPDClientWrapper(object): self.connect() return self.get(command, retries=retries - 1) - def playlist(self): - """Return the currently active playlist. Prefixes paths with the - music_directory, to get the absolute path. + def currentsong(self): + """Return the path to the currently playing song, along with its + songid. Prefixes paths with the music_directory, to get the absolute + path. + In some cases, we need to remove the local path from MPD server, + we replace 'strip_path' with ''. + `strip_path` defaults to ''. """ - result = {} - for entry in self.get('playlistinfo'): + result = None + entry = self.get('currentsong') + if 'file' in entry: if not is_url(entry['file']): - result[entry['id']] = os.path.join( - self.music_directory, entry['file']) + file = entry['file'] + if file.startswith(self.strip_path): + file = file[len(self.strip_path):] + result = os.path.join(self.music_directory, file) else: - result[entry['id']] = entry['file'] - return result + result = entry['file'] + self._log.debug('returning: {0}', result) + return result, entry.get('id') def status(self): """Return the current status of the MPD. @@ -132,7 +143,7 @@ class MPDClientWrapper(object): return self.get('idle') -class MPDStats(object): +class MPDStats: def __init__(self, lib, log): self.lib = lib self._log = log @@ -164,7 +175,7 @@ class MPDStats(object): if item: return item else: - self._log.info(u'item not found: {0}', displayable_path(path)) + self._log.info('item not found: {0}', displayable_path(path)) def update_item(self, item, attribute, value=None, increment=None): """Update the beets item. Set attribute to value or increment the value @@ -182,7 +193,7 @@ class MPDStats(object): item[attribute] = value item.store() - self._log.debug(u'updated: {0} = {1} [{2}]', + self._log.debug('updated: {0} = {1} [{2}]', attribute, item[attribute], displayable_path(item.path)) @@ -229,29 +240,31 @@ class MPDStats(object): """Updates the play count of a song. """ self.update_item(song['beets_item'], 'play_count', increment=1) - self._log.info(u'played {0}', displayable_path(song['path'])) + self._log.info('played {0}', displayable_path(song['path'])) def handle_skipped(self, song): """Updates the skip count of a song. """ self.update_item(song['beets_item'], 'skip_count', increment=1) - self._log.info(u'skipped {0}', displayable_path(song['path'])) + self._log.info('skipped {0}', displayable_path(song['path'])) def on_stop(self, status): - self._log.info(u'stop') + self._log.info('stop') - if self.now_playing: + # if the current song stays the same it means that we stopped on the + # current track and should not record a skip. + if self.now_playing and self.now_playing['id'] != status.get('songid'): self.handle_song_change(self.now_playing) self.now_playing = None def on_pause(self, status): - self._log.info(u'pause') + self._log.info('pause') self.now_playing = None def on_play(self, status): - playlist = self.mpd.playlist() - path = playlist.get(status['songid']) + + path, songid = self.mpd.currentsong() if not path: return @@ -276,16 +289,17 @@ class MPDStats(object): self.handle_song_change(self.now_playing) if is_url(path): - self._log.info(u'playing stream {0}', displayable_path(path)) + self._log.info('playing stream {0}', displayable_path(path)) self.now_playing = None return - self._log.info(u'playing {0}', displayable_path(path)) + self._log.info('playing {0}', displayable_path(path)) self.now_playing = { - 'started': time.time(), - 'remaining': remaining, - 'path': path, + 'started': time.time(), + 'remaining': remaining, + 'path': path, + 'id': songid, 'beets_item': self.get_item(path), } @@ -305,7 +319,7 @@ class MPDStats(object): if handler: handler(status) else: - self._log.debug(u'unhandled status "{0}"', status) + self._log.debug('unhandled status "{0}"', status) events = self.mpd.events() @@ -313,37 +327,38 @@ class MPDStats(object): class MPDStatsPlugin(plugins.BeetsPlugin): item_types = { - 'play_count': types.INTEGER, - 'skip_count': types.INTEGER, + 'play_count': types.INTEGER, + 'skip_count': types.INTEGER, 'last_played': library.DateType(), - 'rating': types.FLOAT, + 'rating': types.FLOAT, } def __init__(self): - super(MPDStatsPlugin, self).__init__() + super().__init__() mpd_config.add({ 'music_directory': config['directory'].as_filename(), - 'rating': True, - 'rating_mix': 0.75, - 'host': os.environ.get('MPD_HOST', u'localhost'), - 'port': 6600, - 'password': u'', + 'strip_path': '', + 'rating': True, + 'rating_mix': 0.75, + 'host': os.environ.get('MPD_HOST', 'localhost'), + 'port': int(os.environ.get('MPD_PORT', 6600)), + 'password': '', }) mpd_config['password'].redact = True def commands(self): cmd = ui.Subcommand( 'mpdstats', - help=u'run a MPD client to gather play statistics') + help='run a MPD client to gather play statistics') cmd.parser.add_option( - u'--host', dest='host', type='string', - help=u'set the hostname of the server to connect to') + '--host', dest='host', type='string', + help='set the hostname of the server to connect to') cmd.parser.add_option( - u'--port', dest='port', type='int', - help=u'set the port of the MPD server to connect to') + '--port', dest='port', type='int', + help='set the port of the MPD server to connect to') cmd.parser.add_option( - u'--password', dest='password', type='string', - help=u'set the password of the MPD server to connect to') + '--password', dest='password', type='string', + help='set the password of the MPD server to connect to') def func(lib, opts, args): mpd_config.set_args(opts) diff --git a/libs/common/beetsplug/mpdupdate.py b/libs/common/beetsplug/mpdupdate.py index 6ecc9213..e5264e18 100644 --- a/libs/common/beetsplug/mpdupdate.py +++ b/libs/common/beetsplug/mpdupdate.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -21,19 +20,17 @@ Put something like the following in your config.yaml to configure: port: 6600 password: seekrit """ -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin import os import socket from beets import config -import six # No need to introduce a dependency on an MPD library for such a # simple use case. Here's a simple socket abstraction to make things # easier. -class BufferedSocket(object): +class BufferedSocket: """Socket abstraction that allows reading by line.""" def __init__(self, host, port, sep=b'\n'): if host[0] in ['/', '~']: @@ -66,11 +63,11 @@ class BufferedSocket(object): class MPDUpdatePlugin(BeetsPlugin): def __init__(self): - super(MPDUpdatePlugin, self).__init__() + super().__init__() config['mpd'].add({ - 'host': os.environ.get('MPD_HOST', u'localhost'), - 'port': 6600, - 'password': u'', + 'host': os.environ.get('MPD_HOST', 'localhost'), + 'port': int(os.environ.get('MPD_PORT', 6600)), + 'password': '', }) config['mpd']['password'].redact = True @@ -100,21 +97,21 @@ class MPDUpdatePlugin(BeetsPlugin): try: s = BufferedSocket(host, port) - except socket.error as e: - self._log.warning(u'MPD connection failed: {0}', - six.text_type(e.strerror)) + except OSError as e: + self._log.warning('MPD connection failed: {0}', + str(e.strerror)) return resp = s.readline() if b'OK MPD' not in resp: - self._log.warning(u'MPD connection failed: {0!r}', resp) + self._log.warning('MPD connection failed: {0!r}', resp) return if password: s.send(b'password "%s"\n' % password.encode('utf8')) resp = s.readline() if b'OK' not in resp: - self._log.warning(u'Authentication failed: {0!r}', resp) + self._log.warning('Authentication failed: {0!r}', resp) s.send(b'close\n') s.close() return @@ -122,8 +119,8 @@ class MPDUpdatePlugin(BeetsPlugin): s.send(b'update\n') resp = s.readline() if b'updating_db' not in resp: - self._log.warning(u'Update failed: {0!r}', resp) + self._log.warning('Update failed: {0!r}', resp) s.send(b'close\n') s.close() - self._log.info(u'Database updated.') + self._log.info('Database updated.') diff --git a/libs/common/beetsplug/parentwork.py b/libs/common/beetsplug/parentwork.py new file mode 100644 index 00000000..75307b8f --- /dev/null +++ b/libs/common/beetsplug/parentwork.py @@ -0,0 +1,211 @@ +# This file is part of beets. +# Copyright 2017, Dorian Soergel. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Gets parent work, its disambiguation and id, composer, composer sort name +and work composition date +""" + + +from beets import ui +from beets.plugins import BeetsPlugin + +import musicbrainzngs + + +def direct_parent_id(mb_workid, work_date=None): + """Given a Musicbrainz work id, find the id one of the works the work is + part of and the first composition date it encounters. + """ + work_info = musicbrainzngs.get_work_by_id(mb_workid, + includes=["work-rels", + "artist-rels"]) + if 'artist-relation-list' in work_info['work'] and work_date is None: + for artist in work_info['work']['artist-relation-list']: + if artist['type'] == 'composer': + if 'end' in artist.keys(): + work_date = artist['end'] + + if 'work-relation-list' in work_info['work']: + for direct_parent in work_info['work']['work-relation-list']: + if direct_parent['type'] == 'parts' \ + and direct_parent.get('direction') == 'backward': + direct_id = direct_parent['work']['id'] + return direct_id, work_date + return None, work_date + + +def work_parent_id(mb_workid): + """Find the parent work id and composition date of a work given its id. + """ + work_date = None + while True: + new_mb_workid, work_date = direct_parent_id(mb_workid, work_date) + if not new_mb_workid: + return mb_workid, work_date + mb_workid = new_mb_workid + return mb_workid, work_date + + +def find_parentwork_info(mb_workid): + """Get the MusicBrainz information dict about a parent work, including + the artist relations, and the composition date for a work's parent work. + """ + parent_id, work_date = work_parent_id(mb_workid) + work_info = musicbrainzngs.get_work_by_id(parent_id, + includes=["artist-rels"]) + return work_info, work_date + + +class ParentWorkPlugin(BeetsPlugin): + def __init__(self): + super().__init__() + + self.config.add({ + 'auto': False, + 'force': False, + }) + + if self.config['auto']: + self.import_stages = [self.imported] + + def commands(self): + + def func(lib, opts, args): + self.config.set_args(opts) + force_parent = self.config['force'].get(bool) + write = ui.should_write() + + for item in lib.items(ui.decargs(args)): + changed = self.find_work(item, force_parent) + if changed: + item.store() + if write: + item.try_write() + command = ui.Subcommand( + 'parentwork', + help='fetch parent works, composers and dates') + + command.parser.add_option( + '-f', '--force', dest='force', + action='store_true', default=None, + help='re-fetch when parent work is already present') + + command.func = func + return [command] + + def imported(self, session, task): + """Import hook for fetching parent works automatically. + """ + force_parent = self.config['force'].get(bool) + + for item in task.imported_items(): + self.find_work(item, force_parent) + item.store() + + def get_info(self, item, work_info): + """Given the parent work info dict, fetch parent_composer, + parent_composer_sort, parentwork, parentwork_disambig, mb_workid and + composer_ids. + """ + + parent_composer = [] + parent_composer_sort = [] + parentwork_info = {} + + composer_exists = False + if 'artist-relation-list' in work_info['work']: + for artist in work_info['work']['artist-relation-list']: + if artist['type'] == 'composer': + composer_exists = True + parent_composer.append(artist['artist']['name']) + parent_composer_sort.append(artist['artist']['sort-name']) + if 'end' in artist.keys(): + parentwork_info["parentwork_date"] = artist['end'] + + parentwork_info['parent_composer'] = ', '.join(parent_composer) + parentwork_info['parent_composer_sort'] = ', '.join( + parent_composer_sort) + + if not composer_exists: + self._log.debug( + 'no composer for {}; add one at ' + 'https://musicbrainz.org/work/{}', + item, work_info['work']['id'], + ) + + parentwork_info['parentwork'] = work_info['work']['title'] + parentwork_info['mb_parentworkid'] = work_info['work']['id'] + + if 'disambiguation' in work_info['work']: + parentwork_info['parentwork_disambig'] = work_info[ + 'work']['disambiguation'] + + else: + parentwork_info['parentwork_disambig'] = None + + return parentwork_info + + def find_work(self, item, force): + """Finds the parent work of a recording and populates the tags + accordingly. + + The parent work is found recursively, by finding the direct parent + repeatedly until there are no more links in the chain. We return the + final, topmost work in the chain. + + Namely, the tags parentwork, parentwork_disambig, mb_parentworkid, + parent_composer, parent_composer_sort and work_date are populated. + """ + + if not item.mb_workid: + self._log.info('No work for {}, \ +add one at https://musicbrainz.org/recording/{}', item, item.mb_trackid) + return + + hasparent = hasattr(item, 'parentwork') + work_changed = True + if hasattr(item, 'parentwork_workid_current'): + work_changed = item.parentwork_workid_current != item.mb_workid + if force or not hasparent or work_changed: + try: + work_info, work_date = find_parentwork_info(item.mb_workid) + except musicbrainzngs.musicbrainz.WebServiceError as e: + self._log.debug("error fetching work: {}", e) + return + parent_info = self.get_info(item, work_info) + parent_info['parentwork_workid_current'] = item.mb_workid + if 'parent_composer' in parent_info: + self._log.debug("Work fetched: {} - {}", + parent_info['parentwork'], + parent_info['parent_composer']) + else: + self._log.debug("Work fetched: {} - no parent composer", + parent_info['parentwork']) + + elif hasparent: + self._log.debug("{}: Work present, skipping", item) + return + + # apply all non-null values to the item + for key, value in parent_info.items(): + if value: + item[key] = value + + if work_date: + item['work_date'] = work_date + return ui.show_model_changes( + item, fields=['parentwork', 'parentwork_disambig', + 'mb_parentworkid', 'parent_composer', + 'parent_composer_sort', 'work_date', + 'parentwork_workid_current', 'parentwork_date']) diff --git a/libs/common/beetsplug/permissions.py b/libs/common/beetsplug/permissions.py index dd9e0984..f5aab056 100644 --- a/libs/common/beetsplug/permissions.py +++ b/libs/common/beetsplug/permissions.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- - -from __future__ import division, absolute_import, print_function - """Fixes file permissions after the file gets written on import. Put something like the following in your config.yaml to configure: @@ -13,7 +9,6 @@ import os from beets import config, util from beets.plugins import BeetsPlugin from beets.util import ancestry -import six def convert_perm(perm): @@ -21,8 +16,8 @@ def convert_perm(perm): Or, if `perm` is an integer, reinterpret it as an octal number that has been "misinterpreted" as decimal. """ - if isinstance(perm, six.integer_types): - perm = six.text_type(perm) + if isinstance(perm, int): + perm = str(perm) return int(perm, 8) @@ -40,11 +35,11 @@ def assert_permissions(path, permission, log): """ if not check_permissions(util.syspath(path), permission): log.warning( - u'could not set permissions on {}', + 'could not set permissions on {}', util.displayable_path(path), ) log.debug( - u'set permissions to {}, but permissions are now {}', + 'set permissions to {}, but permissions are now {}', permission, os.stat(util.syspath(path)).st_mode & 0o777, ) @@ -60,20 +55,39 @@ def dirs_in_library(library, item): class Permissions(BeetsPlugin): def __init__(self): - super(Permissions, self).__init__() + super().__init__() # Adding defaults. self.config.add({ - u'file': '644', - u'dir': '755', + 'file': '644', + 'dir': '755', }) self.register_listener('item_imported', self.fix) self.register_listener('album_imported', self.fix) + self.register_listener('art_set', self.fix_art) def fix(self, lib, item=None, album=None): """Fix the permissions for an imported Item or Album. """ + files = [] + dirs = set() + if item: + files.append(item.path) + dirs.update(dirs_in_library(lib.directory, item.path)) + elif album: + for album_item in album.items(): + files.append(album_item.path) + dirs.update(dirs_in_library(lib.directory, album_item.path)) + self.set_permissions(files=files, dirs=dirs) + + def fix_art(self, album): + """Fix the permission for Album art file. + """ + if album.artpath: + self.set_permissions(files=[album.artpath]) + + def set_permissions(self, files=[], dirs=[]): # Get the configured permissions. The user can specify this either a # string (in YAML quotes) or, for convenience, as an integer so the # quotes can be omitted. In the latter case, we need to reinterpret the @@ -83,21 +97,10 @@ class Permissions(BeetsPlugin): file_perm = convert_perm(file_perm) dir_perm = convert_perm(dir_perm) - # Create chmod_queue. - file_chmod_queue = [] - if item: - file_chmod_queue.append(item.path) - elif album: - for album_item in album.items(): - file_chmod_queue.append(album_item.path) - - # A set of directories to change permissions for. - dir_chmod_queue = set() - - for path in file_chmod_queue: + for path in files: # Changing permissions on the destination file. self._log.debug( - u'setting file permissions on {}', + 'setting file permissions on {}', util.displayable_path(path), ) os.chmod(util.syspath(path), file_perm) @@ -105,16 +108,11 @@ class Permissions(BeetsPlugin): # Checks if the destination path has the permissions configured. assert_permissions(path, file_perm, self._log) - # Adding directories to the directory chmod queue. - dir_chmod_queue.update( - dirs_in_library(lib.directory, - path)) - # Change permissions for the directories. - for path in dir_chmod_queue: - # Chaning permissions on the destination directory. + for path in dirs: + # Changing permissions on the destination directory. self._log.debug( - u'setting directory permissions on {}', + 'setting directory permissions on {}', util.displayable_path(path), ) os.chmod(util.syspath(path), dir_perm) diff --git a/libs/common/beetsplug/play.py b/libs/common/beetsplug/play.py index 4d32a357..f4233490 100644 --- a/libs/common/beetsplug/play.py +++ b/libs/common/beetsplug/play.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, David Hamp-Gonsalves # @@ -15,7 +14,6 @@ """Send the results of a query to the configured music player as a playlist. """ -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets.ui import Subcommand @@ -26,6 +24,7 @@ from beets import util from os.path import relpath from tempfile import NamedTemporaryFile import subprocess +import shlex # Indicate where arguments should be inserted into the command string. # If this is missing, they're placed at the end. @@ -39,25 +38,25 @@ def play(command_str, selection, paths, open_args, log, item_type='track', """ # Print number of tracks or albums to be played, log command to be run. item_type += 's' if len(selection) > 1 else '' - ui.print_(u'Playing {0} {1}.'.format(len(selection), item_type)) - log.debug(u'executing command: {} {!r}', command_str, open_args) + ui.print_('Playing {} {}.'.format(len(selection), item_type)) + log.debug('executing command: {} {!r}', command_str, open_args) try: if keep_open: - command = util.shlex_split(command_str) + command = shlex.split(command_str) command = command + open_args subprocess.call(command) else: util.interactive_open(open_args, command_str) except OSError as exc: raise ui.UserError( - "Could not play the query: {0}".format(exc)) + f"Could not play the query: {exc}") class PlayPlugin(BeetsPlugin): def __init__(self): - super(PlayPlugin, self).__init__() + super().__init__() config['play'].add({ 'command': None, @@ -65,6 +64,7 @@ class PlayPlugin(BeetsPlugin): 'relative_to': None, 'raw': False, 'warning_threshold': 100, + 'bom': False, }) self.register_listener('before_choose_candidate', @@ -73,18 +73,18 @@ class PlayPlugin(BeetsPlugin): def commands(self): play_command = Subcommand( 'play', - help=u'send music to a player as a playlist' + help='send music to a player as a playlist' ) play_command.parser.add_album_option() play_command.parser.add_option( - u'-A', u'--args', + '-A', '--args', action='store', - help=u'add additional arguments to the command', + help='add additional arguments to the command', ) play_command.parser.add_option( - u'-y', u'--yes', + '-y', '--yes', action="store_true", - help=u'skip the warning threshold', + help='skip the warning threshold', ) play_command.func = self._play_command return [play_command] @@ -123,7 +123,7 @@ class PlayPlugin(BeetsPlugin): if not selection: ui.print_(ui.colorize('text_warning', - u'No {0} to play.'.format(item_type))) + f'No {item_type} to play.')) return open_args = self._playlist_or_paths(paths) @@ -147,7 +147,7 @@ class PlayPlugin(BeetsPlugin): if ARGS_MARKER in command_str: return command_str.replace(ARGS_MARKER, args) else: - return u"{} {}".format(command_str, args) + return f"{command_str} {args}" else: # Don't include the marker in the command. return command_str.replace(" " + ARGS_MARKER, "") @@ -174,10 +174,10 @@ class PlayPlugin(BeetsPlugin): ui.print_(ui.colorize( 'text_warning', - u'You are about to queue {0} {1}.'.format( + 'You are about to queue {} {}.'.format( len(selection), item_type))) - if ui.input_options((u'Continue', u'Abort')) == 'a': + if ui.input_options(('Continue', 'Abort')) == 'a': return True return False @@ -185,7 +185,12 @@ class PlayPlugin(BeetsPlugin): def _create_tmp_playlist(self, paths_list): """Create a temporary .m3u file. Return the filename. """ + utf8_bom = config['play']['bom'].get(bool) m3u = NamedTemporaryFile('wb', suffix='.m3u', delete=False) + + if utf8_bom: + m3u.write(b'\xEF\xBB\xBF') + for item in paths_list: m3u.write(item + b'\n') m3u.close() diff --git a/libs/common/beetsplug/playlist.py b/libs/common/beetsplug/playlist.py new file mode 100644 index 00000000..265b8bad --- /dev/null +++ b/libs/common/beetsplug/playlist.py @@ -0,0 +1,185 @@ +# This file is part of beets. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + + +import os +import fnmatch +import tempfile +import beets +from beets.util import path_as_posix + + +class PlaylistQuery(beets.dbcore.Query): + """Matches files listed by a playlist file. + """ + def __init__(self, pattern): + self.pattern = pattern + config = beets.config['playlist'] + + # Get the full path to the playlist + playlist_paths = ( + pattern, + os.path.abspath(os.path.join( + config['playlist_dir'].as_filename(), + f'{pattern}.m3u', + )), + ) + + self.paths = [] + for playlist_path in playlist_paths: + if not fnmatch.fnmatch(playlist_path, '*.[mM]3[uU]'): + # This is not am M3U playlist, skip this candidate + continue + + try: + f = open(beets.util.syspath(playlist_path), mode='rb') + except OSError: + continue + + if config['relative_to'].get() == 'library': + relative_to = beets.config['directory'].as_filename() + elif config['relative_to'].get() == 'playlist': + relative_to = os.path.dirname(playlist_path) + else: + relative_to = config['relative_to'].as_filename() + relative_to = beets.util.bytestring_path(relative_to) + + for line in f: + if line[0] == '#': + # ignore comments, and extm3u extension + continue + + self.paths.append(beets.util.normpath( + os.path.join(relative_to, line.rstrip()) + )) + f.close() + break + + def col_clause(self): + if not self.paths: + # Playlist is empty + return '0', () + clause = 'path IN ({})'.format(', '.join('?' for path in self.paths)) + return clause, (beets.library.BLOB_TYPE(p) for p in self.paths) + + def match(self, item): + return item.path in self.paths + + +class PlaylistPlugin(beets.plugins.BeetsPlugin): + item_queries = {'playlist': PlaylistQuery} + + def __init__(self): + super().__init__() + self.config.add({ + 'auto': False, + 'playlist_dir': '.', + 'relative_to': 'library', + 'forward_slash': False, + }) + + self.playlist_dir = self.config['playlist_dir'].as_filename() + self.changes = {} + + if self.config['relative_to'].get() == 'library': + self.relative_to = beets.util.bytestring_path( + beets.config['directory'].as_filename()) + elif self.config['relative_to'].get() != 'playlist': + self.relative_to = beets.util.bytestring_path( + self.config['relative_to'].as_filename()) + else: + self.relative_to = None + + if self.config['auto']: + self.register_listener('item_moved', self.item_moved) + self.register_listener('item_removed', self.item_removed) + self.register_listener('cli_exit', self.cli_exit) + + def item_moved(self, item, source, destination): + self.changes[source] = destination + + def item_removed(self, item): + if not os.path.exists(beets.util.syspath(item.path)): + self.changes[item.path] = None + + def cli_exit(self, lib): + for playlist in self.find_playlists(): + self._log.info(f'Updating playlist: {playlist}') + base_dir = beets.util.bytestring_path( + self.relative_to if self.relative_to + else os.path.dirname(playlist) + ) + + try: + self.update_playlist(playlist, base_dir) + except beets.util.FilesystemError: + self._log.error('Failed to update playlist: {}'.format( + beets.util.displayable_path(playlist))) + + def find_playlists(self): + """Find M3U playlists in the playlist directory.""" + try: + dir_contents = os.listdir(beets.util.syspath(self.playlist_dir)) + except OSError: + self._log.warning('Unable to open playlist directory {}'.format( + beets.util.displayable_path(self.playlist_dir))) + return + + for filename in dir_contents: + if fnmatch.fnmatch(filename, '*.[mM]3[uU]'): + yield os.path.join(self.playlist_dir, filename) + + def update_playlist(self, filename, base_dir): + """Find M3U playlists in the specified directory.""" + changes = 0 + deletions = 0 + + with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tempfp: + new_playlist = tempfp.name + with open(filename, mode='rb') as fp: + for line in fp: + original_path = line.rstrip(b'\r\n') + + # Ensure that path from playlist is absolute + is_relative = not os.path.isabs(line) + if is_relative: + lookup = os.path.join(base_dir, original_path) + else: + lookup = original_path + + try: + new_path = self.changes[beets.util.normpath(lookup)] + except KeyError: + if self.config['forward_slash']: + line = path_as_posix(line) + tempfp.write(line) + else: + if new_path is None: + # Item has been deleted + deletions += 1 + continue + + changes += 1 + if is_relative: + new_path = os.path.relpath(new_path, base_dir) + line = line.replace(original_path, new_path) + if self.config['forward_slash']: + line = path_as_posix(line) + tempfp.write(line) + + if changes or deletions: + self._log.info( + 'Updated playlist {} ({} changes, {} deletions)'.format( + filename, changes, deletions)) + beets.util.copy(new_playlist, filename, replace=True) + beets.util.remove(new_playlist) diff --git a/libs/common/beetsplug/plexupdate.py b/libs/common/beetsplug/plexupdate.py index 17fd8208..2261a55f 100644 --- a/libs/common/beetsplug/plexupdate.py +++ b/libs/common/beetsplug/plexupdate.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Updates an Plex library whenever the beets library is changed. Plex Home users enter the Plex Token to enable updating. @@ -9,42 +7,51 @@ Put something like the following in your config.yaml to configure: port: 32400 token: token """ -from __future__ import division, absolute_import, print_function import requests -import xml.etree.ElementTree as ET -from six.moves.urllib.parse import urljoin, urlencode +from xml.etree import ElementTree +from urllib.parse import urljoin, urlencode from beets import config from beets.plugins import BeetsPlugin -def get_music_section(host, port, token, library_name): +def get_music_section(host, port, token, library_name, secure, + ignore_cert_errors): """Getting the section key for the music library in Plex. """ api_endpoint = append_token('library/sections', token) - url = urljoin('http://{0}:{1}'.format(host, port), api_endpoint) + url = urljoin('{}://{}:{}'.format(get_protocol(secure), host, + port), api_endpoint) # Sends request. - r = requests.get(url) + r = requests.get(url, verify=not ignore_cert_errors) # Parse xml tree and extract music section key. - tree = ET.fromstring(r.content) + tree = ElementTree.fromstring(r.content) for child in tree.findall('Directory'): if child.get('title') == library_name: return child.get('key') -def update_plex(host, port, token, library_name): +def update_plex(host, port, token, library_name, secure, + ignore_cert_errors): + """Ignore certificate errors if configured to. + """ + if ignore_cert_errors: + import urllib3 + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) """Sends request to the Plex api to start a library refresh. """ # Getting section key and build url. - section_key = get_music_section(host, port, token, library_name) - api_endpoint = 'library/sections/{0}/refresh'.format(section_key) + section_key = get_music_section(host, port, token, library_name, + secure, ignore_cert_errors) + api_endpoint = f'library/sections/{section_key}/refresh' api_endpoint = append_token(api_endpoint, token) - url = urljoin('http://{0}:{1}'.format(host, port), api_endpoint) + url = urljoin('{}://{}:{}'.format(get_protocol(secure), host, + port), api_endpoint) # Sends request and returns requests object. - r = requests.get(url) + r = requests.get(url, verify=not ignore_cert_errors) return r @@ -56,16 +63,25 @@ def append_token(url, token): return url +def get_protocol(secure): + if secure: + return 'https' + else: + return 'http' + + class PlexUpdate(BeetsPlugin): def __init__(self): - super(PlexUpdate, self).__init__() + super().__init__() # Adding defaults. config['plex'].add({ - u'host': u'localhost', - u'port': 32400, - u'token': u'', - u'library_name': u'Music'}) + 'host': 'localhost', + 'port': 32400, + 'token': '', + 'library_name': 'Music', + 'secure': False, + 'ignore_cert_errors': False}) config['plex']['token'].redact = True self.register_listener('database_change', self.listen_for_db_change) @@ -77,7 +93,7 @@ class PlexUpdate(BeetsPlugin): def update(self, lib): """When the client exists try to send refresh request to Plex server. """ - self._log.info(u'Updating Plex library...') + self._log.info('Updating Plex library...') # Try to send update request. try: @@ -85,8 +101,10 @@ class PlexUpdate(BeetsPlugin): config['plex']['host'].get(), config['plex']['port'].get(), config['plex']['token'].get(), - config['plex']['library_name'].get()) - self._log.info(u'... started.') + config['plex']['library_name'].get(), + config['plex']['secure'].get(bool), + config['plex']['ignore_cert_errors'].get(bool)) + self._log.info('... started.') except requests.exceptions.RequestException: - self._log.warning(u'Update failed.') + self._log.warning('Update failed.') diff --git a/libs/common/beetsplug/random.py b/libs/common/beetsplug/random.py index 65caaf90..ea9b7b98 100644 --- a/libs/common/beetsplug/random.py +++ b/libs/common/beetsplug/random.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Philippe Mongeau. # @@ -15,101 +14,10 @@ """Get a random song or album from the library. """ -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets.ui import Subcommand, decargs, print_ -import random -from operator import attrgetter -from itertools import groupby - - -def _length(obj, album): - """Get the duration of an item or album. - """ - if album: - return sum(i.length for i in obj.items()) - else: - return obj.length - - -def _equal_chance_permutation(objs, field='albumartist'): - """Generate (lazily) a permutation of the objects where every group - with equal values for `field` have an equal chance of appearing in - any given position. - """ - # Group the objects by artist so we can sample from them. - key = attrgetter(field) - objs.sort(key=key) - objs_by_artists = {} - for artist, v in groupby(objs, key): - objs_by_artists[artist] = list(v) - - # While we still have artists with music to choose from, pick one - # randomly and pick a track from that artist. - while objs_by_artists: - # Choose an artist and an object for that artist, removing - # this choice from the pool. - artist = random.choice(list(objs_by_artists.keys())) - objs_from_artist = objs_by_artists[artist] - i = random.randint(0, len(objs_from_artist) - 1) - yield objs_from_artist.pop(i) - - # Remove the artist if we've used up all of its objects. - if not objs_from_artist: - del objs_by_artists[artist] - - -def _take(iter, num): - """Return a list containing the first `num` values in `iter` (or - fewer, if the iterable ends early). - """ - out = [] - for val in iter: - out.append(val) - num -= 1 - if num <= 0: - break - return out - - -def _take_time(iter, secs, album): - """Return a list containing the first values in `iter`, which should - be Item or Album objects, that add up to the given amount of time in - seconds. - """ - out = [] - total_time = 0.0 - for obj in iter: - length = _length(obj, album) - if total_time + length <= secs: - out.append(obj) - total_time += length - return out - - -def random_objs(objs, album, number=1, time=None, equal_chance=False): - """Get a random subset of the provided `objs`. - - If `number` is provided, produce that many matches. Otherwise, if - `time` is provided, instead select a list whose total time is close - to that number of minutes. If `equal_chance` is true, give each - artist an equal chance of being included so that artists with more - songs are not represented disproportionately. - """ - # Permute the objects either in a straightforward way or an - # artist-balanced way. - if equal_chance: - perm = _equal_chance_permutation(objs) - else: - perm = objs - random.shuffle(perm) # N.B. This shuffles the original list. - - # Select objects by time our count. - if time: - return _take_time(perm, time * 60, album) - else: - return _take(perm, number) +from beets.random import random_objs def random_func(lib, opts, args): @@ -130,16 +38,16 @@ def random_func(lib, opts, args): random_cmd = Subcommand('random', - help=u'choose a random track or album') + help='choose a random track or album') random_cmd.parser.add_option( - u'-n', u'--number', action='store', type="int", - help=u'number of objects to choose', default=1) + '-n', '--number', action='store', type="int", + help='number of objects to choose', default=1) random_cmd.parser.add_option( - u'-e', u'--equal-chance', action='store_true', - help=u'each artist has the same chance') + '-e', '--equal-chance', action='store_true', + help='each artist has the same chance') random_cmd.parser.add_option( - u'-t', u'--time', action='store', type="float", - help=u'total length in minutes of objects to choose') + '-t', '--time', action='store', type="float", + help='total length in minutes of objects to choose') random_cmd.parser.add_all_common_options() random_cmd.func = random_func diff --git a/libs/common/beetsplug/replaygain.py b/libs/common/beetsplug/replaygain.py index a7eb81b5..b6297d93 100644 --- a/libs/common/beetsplug/replaygain.py +++ b/libs/common/beetsplug/replaygain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Fabrice Laporte, Yevgeny Bezman, and Adrian Sampson. # @@ -13,20 +12,23 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function -import subprocess -import os import collections +import enum +import math +import os +import signal +import subprocess import sys import warnings -import xml.parsers.expat -from six.moves import zip +from multiprocessing.pool import ThreadPool, RUN +from six.moves import queue +from threading import Thread, Event from beets import ui from beets.plugins import BeetsPlugin -from beets.util import (syspath, command_output, bytestring_path, - displayable_path, py3_path) +from beets.util import (syspath, command_output, displayable_path, + py3_path, cpu_count) # Utilities. @@ -47,238 +49,342 @@ class FatalGstreamerPluginReplayGainError(FatalReplayGainError): loading the required plugins.""" -def call(args): +def call(args, **kwargs): """Execute the command and return its output or raise a ReplayGainError on failure. """ try: - return command_output(args) + return command_output(args, **kwargs) except subprocess.CalledProcessError as e: raise ReplayGainError( - u"{0} exited with status {1}".format(args[0], e.returncode) + "{} exited with status {}".format(args[0], e.returncode) ) except UnicodeEncodeError: # Due to a bug in Python 2's subprocess on Windows, Unicode # filenames can fail to encode on that platform. See: # https://github.com/google-code-export/beets/issues/499 - raise ReplayGainError(u"argument encoding failed") + raise ReplayGainError("argument encoding failed") + + +def after_version(version_a, version_b): + return tuple(int(s) for s in version_a.split('.')) \ + >= tuple(int(s) for s in version_b.split('.')) + + +def db_to_lufs(db): + """Convert db to LUFS. + + According to https://wiki.hydrogenaud.io/index.php?title= + ReplayGain_2.0_specification#Reference_level + """ + return db - 107 + + +def lufs_to_db(db): + """Convert LUFS to db. + + According to https://wiki.hydrogenaud.io/index.php?title= + ReplayGain_2.0_specification#Reference_level + """ + return db + 107 # Backend base and plumbing classes. +# gain: in LU to reference level +# peak: part of full scale (FS is 1.0) Gain = collections.namedtuple("Gain", "gain peak") +# album_gain: Gain object +# track_gains: list of Gain objects AlbumGain = collections.namedtuple("AlbumGain", "album_gain track_gains") -class Backend(object): +class Peak(enum.Enum): + none = 0 + true = 1 + sample = 2 + + +class Backend: """An abstract class representing engine for calculating RG values. """ + do_parallel = False + def __init__(self, config, log): """Initialize the backend with the configuration view for the plugin. """ self._log = log - def compute_track_gain(self, items): - raise NotImplementedError() - - def compute_album_gain(self, album): - # TODO: implement album gain in terms of track gain of the - # individual tracks which can be used for any backend. - raise NotImplementedError() - - -# bsg1770gain backend -class Bs1770gainBackend(Backend): - """bs1770gain is a loudness scanner compliant with ITU-R BS.1770 and - its flavors EBU R128, ATSC A/85 and Replaygain 2.0. - """ - - def __init__(self, config, log): - super(Bs1770gainBackend, self).__init__(config, log) - config.add({ - 'chunk_at': 5000, - 'method': 'replaygain', - }) - self.chunk_at = config['chunk_at'].as_number() - self.method = '--' + config['method'].as_str() - - cmd = 'bs1770gain' - try: - call([cmd, self.method]) - self.command = cmd - except OSError: - raise FatalReplayGainError( - u'Is bs1770gain installed? Is your method in config correct?' - ) - if not self.command: - raise FatalReplayGainError( - u'no replaygain command found: install bs1770gain' - ) - - def compute_track_gain(self, items): + def compute_track_gain(self, items, target_level, peak): """Computes the track gain of the given tracks, returns a list - of TrackGain objects. + of Gain objects. """ + raise NotImplementedError() - output = self.compute_gain(items, False) - return output - - def compute_album_gain(self, album): + def compute_album_gain(self, items, target_level, peak): """Computes the album gain of the given album, returns an AlbumGain object. """ - # TODO: What should be done when not all tracks in the album are - # supported? + raise NotImplementedError() - supported_items = album.items() - output = self.compute_gain(supported_items, True) - if not output: - raise ReplayGainError(u'no output from bs1770gain') - return AlbumGain(output[-1], output[:-1]) +# ffmpeg backend +class FfmpegBackend(Backend): + """A replaygain backend using ffmpeg's ebur128 filter. + """ - def isplitter(self, items, chunk_at): - """Break an iterable into chunks of at most size `chunk_at`, - generating lists for each chunk. - """ - iterable = iter(items) - while True: - result = [] - for i in range(chunk_at): - try: - a = next(iterable) - except StopIteration: - break - else: - result.append(a) - if result: - yield result - else: - break + do_parallel = True - def compute_gain(self, items, is_album): - """Computes the track or album gain of a list of items, returns - a list of TrackGain objects. - When computing album gain, the last TrackGain object returned is - the album gain - """ + def __init__(self, config, log): + super().__init__(config, log) + self._ffmpeg_path = "ffmpeg" - if len(items) == 0: - return [] - - albumgaintot = 0.0 - albumpeaktot = 0.0 - returnchunks = [] - - # In the case of very large sets of music, we break the tracks - # into smaller chunks and process them one at a time. This - # avoids running out of memory. - if len(items) > self.chunk_at: - i = 0 - for chunk in self.isplitter(items, self.chunk_at): - i += 1 - returnchunk = self.compute_chunk_gain(chunk, is_album) - albumgaintot += returnchunk[-1].gain - albumpeaktot += returnchunk[-1].peak - returnchunks = returnchunks + returnchunk[0:-1] - returnchunks.append(Gain(albumgaintot / i, albumpeaktot / i)) - return returnchunks - else: - return self.compute_chunk_gain(items, is_album) - - def compute_chunk_gain(self, items, is_album): - """Compute ReplayGain values and return a list of results - dictionaries as given by `parse_tool_output`. - """ - # Construct shell command. - cmd = [self.command] - cmd += [self.method] - cmd += ['--xml', '-p'] - - # Workaround for Windows: the underlying tool fails on paths - # with the \\?\ prefix, so we don't use it here. This - # prevents the backend from working with long paths. - args = cmd + [syspath(i.path, prefix=False) for i in items] - path_list = [i.path for i in items] - - # Invoke the command. - self._log.debug( - u'executing {0}', u' '.join(map(displayable_path, args)) - ) - output = call(args) - - self._log.debug(u'analysis finished: {0}', output) - results = self.parse_tool_output(output, path_list, is_album) - self._log.debug(u'{0} items, {1} results', len(items), len(results)) - return results - - def parse_tool_output(self, text, path_list, is_album): - """Given the output from bs1770gain, parse the text and - return a list of dictionaries - containing information about each analyzed file. - """ - per_file_gain = {} - album_gain = {} # mutable variable so it can be set from handlers - parser = xml.parsers.expat.ParserCreate(encoding='utf-8') - state = {'file': None, 'gain': None, 'peak': None} - - def start_element_handler(name, attrs): - if name == u'track': - state['file'] = bytestring_path(attrs[u'file']) - if state['file'] in per_file_gain: - raise ReplayGainError( - u'duplicate filename in bs1770gain output') - elif name == u'integrated': - state['gain'] = float(attrs[u'lu']) - elif name == u'sample-peak': - state['peak'] = float(attrs[u'factor']) - - def end_element_handler(name): - if name == u'track': - if state['gain'] is None or state['peak'] is None: - raise ReplayGainError(u'could not parse gain or peak from ' - 'the output of bs1770gain') - per_file_gain[state['file']] = Gain(state['gain'], - state['peak']) - state['gain'] = state['peak'] = None - elif name == u'summary': - if state['gain'] is None or state['peak'] is None: - raise ReplayGainError(u'could not parse gain or peak from ' - 'the output of bs1770gain') - album_gain["album"] = Gain(state['gain'], state['peak']) - state['gain'] = state['peak'] = None - parser.StartElementHandler = start_element_handler - parser.EndElementHandler = end_element_handler - parser.Parse(text, True) - - if len(per_file_gain) != len(path_list): - raise ReplayGainError( - u'the number of results returned by bs1770gain does not match ' - 'the number of files passed to it') - - # bs1770gain does not return the analysis results in the order that - # files are passed on the command line, because it is sorting the files - # internally. We must recover the order from the filenames themselves. + # check that ffmpeg is installed try: - out = [per_file_gain[os.path.basename(p)] for p in path_list] - except KeyError: + ffmpeg_version_out = call([self._ffmpeg_path, "-version"]) + except OSError: + raise FatalReplayGainError( + f"could not find ffmpeg at {self._ffmpeg_path}" + ) + incompatible_ffmpeg = True + for line in ffmpeg_version_out.stdout.splitlines(): + if line.startswith(b"configuration:"): + if b"--enable-libebur128" in line: + incompatible_ffmpeg = False + if line.startswith(b"libavfilter"): + version = line.split(b" ", 1)[1].split(b"/", 1)[0].split(b".") + version = tuple(map(int, version)) + if version >= (6, 67, 100): + incompatible_ffmpeg = False + if incompatible_ffmpeg: + raise FatalReplayGainError( + "Installed FFmpeg version does not support ReplayGain." + "calculation. Either libavfilter version 6.67.100 or above or" + "the --enable-libebur128 configuration option is required." + ) + + def compute_track_gain(self, items, target_level, peak): + """Computes the track gain of the given tracks, returns a list + of Gain objects (the track gains). + """ + gains = [] + for item in items: + gains.append( + self._analyse_item( + item, + target_level, + peak, + count_blocks=False, + )[0] # take only the gain, discarding number of gating blocks + ) + return gains + + def compute_album_gain(self, items, target_level, peak): + """Computes the album gain of the given album, returns an + AlbumGain object. + """ + target_level_lufs = db_to_lufs(target_level) + + # analyse tracks + # list of track Gain objects + track_gains = [] + # maximum peak + album_peak = 0 + # sum of BS.1770 gating block powers + sum_powers = 0 + # total number of BS.1770 gating blocks + n_blocks = 0 + + for item in items: + track_gain, track_n_blocks = self._analyse_item( + item, target_level, peak + ) + track_gains.append(track_gain) + + # album peak is maximum track peak + album_peak = max(album_peak, track_gain.peak) + + # prepare album_gain calculation + # total number of blocks is sum of track blocks + n_blocks += track_n_blocks + + # convert `LU to target_level` -> LUFS + track_loudness = target_level_lufs - track_gain.gain + # This reverses ITU-R BS.1770-4 p. 6 equation (5) to convert + # from loudness to power. The result is the average gating + # block power. + track_power = 10**((track_loudness + 0.691) / 10) + + # Weight that average power by the number of gating blocks to + # get the sum of all their powers. Add that to the sum of all + # block powers in this album. + sum_powers += track_power * track_n_blocks + + # calculate album gain + if n_blocks > 0: + # compare ITU-R BS.1770-4 p. 6 equation (5) + # Album gain is the replaygain of the concatenation of all tracks. + album_gain = -0.691 + 10 * math.log10(sum_powers / n_blocks) + else: + album_gain = -70 + # convert LUFS -> `LU to target_level` + album_gain = target_level_lufs - album_gain + + self._log.debug( + "{}: gain {} LU, peak {}" + .format(items, album_gain, album_peak) + ) + + return AlbumGain(Gain(album_gain, album_peak), track_gains) + + def _construct_cmd(self, item, peak_method): + """Construct the shell command to analyse items.""" + return [ + self._ffmpeg_path, + "-nostats", + "-hide_banner", + "-i", + item.path, + "-map", + "a:0", + "-filter", + f"ebur128=peak={peak_method}", + "-f", + "null", + "-", + ] + + def _analyse_item(self, item, target_level, peak, count_blocks=True): + """Analyse item. Return a pair of a Gain object and the number + of gating blocks above the threshold. + + If `count_blocks` is False, the number of gating blocks returned + will be 0. + """ + target_level_lufs = db_to_lufs(target_level) + peak_method = peak.name + + # call ffmpeg + self._log.debug(f"analyzing {item}") + cmd = self._construct_cmd(item, peak_method) + self._log.debug( + 'executing {0}', ' '.join(map(displayable_path, cmd)) + ) + output = call(cmd).stderr.splitlines() + + # parse output + + if peak == Peak.none: + peak = 0 + else: + line_peak = self._find_line( + output, + f" {peak_method.capitalize()} peak:".encode(), + start_line=len(output) - 1, step_size=-1, + ) + peak = self._parse_float( + output[self._find_line( + output, b" Peak:", + line_peak, + )] + ) + # convert TPFS -> part of FS + peak = 10**(peak / 20) + + line_integrated_loudness = self._find_line( + output, b" Integrated loudness:", + start_line=len(output) - 1, step_size=-1, + ) + gain = self._parse_float( + output[self._find_line( + output, b" I:", + line_integrated_loudness, + )] + ) + # convert LUFS -> LU from target level + gain = target_level_lufs - gain + + # count BS.1770 gating blocks + n_blocks = 0 + if count_blocks: + gating_threshold = self._parse_float( + output[self._find_line( + output, b" Threshold:", + start_line=line_integrated_loudness, + )] + ) + for line in output: + if not line.startswith(b"[Parsed_ebur128"): + continue + if line.endswith(b"Summary:"): + continue + line = line.split(b"M:", 1) + if len(line) < 2: + continue + if self._parse_float(b"M: " + line[1]) >= gating_threshold: + n_blocks += 1 + self._log.debug( + "{}: {} blocks over {} LUFS" + .format(item, n_blocks, gating_threshold) + ) + + self._log.debug( + "{}: gain {} LU, peak {}" + .format(item, gain, peak) + ) + + return Gain(gain, peak), n_blocks + + def _find_line(self, output, search, start_line=0, step_size=1): + """Return index of line beginning with `search`. + + Begins searching at index `start_line` in `output`. + """ + end_index = len(output) if step_size > 0 else -1 + for i in range(start_line, end_index, step_size): + if output[i].startswith(search): + return i + raise ReplayGainError( + "ffmpeg output: missing {} after line {}" + .format(repr(search), start_line) + ) + + def _parse_float(self, line): + """Extract a float from a key value pair in `line`. + + This format is expected: /[^:]:[[:space:]]*value.*/, where `value` is + the float. + """ + # extract value + value = line.split(b":", 1) + if len(value) < 2: raise ReplayGainError( - u'unrecognized filename in bs1770gain output ' - '(bs1770gain can only deal with utf-8 file names)') - if is_album: - out.append(album_gain["album"]) - return out + "ffmpeg output: expected key value pair, found {}" + .format(line) + ) + value = value[1].lstrip() + # strip unit + value = value.split(b" ", 1)[0] + # cast value to float + try: + return float(value) + except ValueError: + raise ReplayGainError( + "ffmpeg output: expected float value, found {}" + .format(value) + ) # mpgain/aacgain CLI tool backend. class CommandBackend(Backend): + do_parallel = True def __init__(self, config, log): - super(CommandBackend, self).__init__(config, log) + super().__init__(config, log) config.add({ - 'command': u"", + 'command': "", 'noclip': True, }) @@ -288,7 +394,7 @@ class CommandBackend(Backend): # Explicit executable path. if not os.path.isfile(self.command): raise FatalReplayGainError( - u'replaygain command does not exist: {0}'.format( + 'replaygain command does not exist: {}'.format( self.command) ) else: @@ -301,34 +407,32 @@ class CommandBackend(Backend): pass if not self.command: raise FatalReplayGainError( - u'no replaygain command found: install mp3gain or aacgain' + 'no replaygain command found: install mp3gain or aacgain' ) self.noclip = config['noclip'].get(bool) - target_level = config['targetlevel'].as_number() - self.gain_offset = int(target_level - 89) - def compute_track_gain(self, items): + def compute_track_gain(self, items, target_level, peak): """Computes the track gain of the given tracks, returns a list of TrackGain objects. """ supported_items = list(filter(self.format_supported, items)) - output = self.compute_gain(supported_items, False) + output = self.compute_gain(supported_items, target_level, False) return output - def compute_album_gain(self, album): + def compute_album_gain(self, items, target_level, peak): """Computes the album gain of the given album, returns an AlbumGain object. """ # TODO: What should be done when not all tracks in the album are # supported? - supported_items = list(filter(self.format_supported, album.items())) - if len(supported_items) != len(album.items()): - self._log.debug(u'tracks are of unsupported format') + supported_items = list(filter(self.format_supported, items)) + if len(supported_items) != len(items): + self._log.debug('tracks are of unsupported format') return AlbumGain(None, []) - output = self.compute_gain(supported_items, True) + output = self.compute_gain(supported_items, target_level, True) return AlbumGain(output[-1], output[:-1]) def format_supported(self, item): @@ -340,7 +444,7 @@ class CommandBackend(Backend): return False return True - def compute_gain(self, items, is_album): + def compute_gain(self, items, target_level, is_album): """Computes the track or album gain of a list of items, returns a list of TrackGain objects. @@ -348,7 +452,7 @@ class CommandBackend(Backend): the album gain """ if len(items) == 0: - self._log.debug(u'no supported tracks to analyze') + self._log.debug('no supported tracks to analyze') return [] """Compute ReplayGain values and return a list of results @@ -367,13 +471,13 @@ class CommandBackend(Backend): else: # Disable clipping warning. cmd = cmd + ['-c'] - cmd = cmd + ['-d', str(self.gain_offset)] + cmd = cmd + ['-d', str(int(target_level - 89))] cmd = cmd + [syspath(i.path) for i in items] - self._log.debug(u'analyzing {0} files', len(items)) - self._log.debug(u"executing {0}", " ".join(map(displayable_path, cmd))) - output = call(cmd) - self._log.debug(u'analysis finished') + self._log.debug('analyzing {0} files', len(items)) + self._log.debug("executing {0}", " ".join(map(displayable_path, cmd))) + output = call(cmd).stdout + self._log.debug('analysis finished') return self.parse_tool_output(output, len(items) + (1 if is_album else 0)) @@ -386,8 +490,8 @@ class CommandBackend(Backend): for line in text.split(b'\n')[1:num_lines + 1]: parts = line.split(b'\t') if len(parts) != 6 or parts[0] == b'File': - self._log.debug(u'bad tool output: {0}', text) - raise ReplayGainError(u'mp3gain failed') + self._log.debug('bad tool output: {0}', text) + raise ReplayGainError('mp3gain failed') d = { 'file': parts[0], 'mp3gain': int(parts[1]), @@ -404,9 +508,8 @@ class CommandBackend(Backend): # GStreamer-based backend. class GStreamerBackend(Backend): - def __init__(self, config, log): - super(GStreamerBackend, self).__init__(config, log) + super().__init__(config, log) self._import_gst() # Initialized a GStreamer pipeline of the form filesrc -> @@ -423,15 +526,13 @@ class GStreamerBackend(Backend): if self._src is None or self._decbin is None or self._conv is None \ or self._res is None or self._rg is None: raise FatalGstreamerPluginReplayGainError( - u"Failed to load required GStreamer plugins" + "Failed to load required GStreamer plugins" ) # We check which files need gain ourselves, so all files given # to rganalsys should have their gain computed, even if it # already exists. self._rg.set_property("forced", True) - self._rg.set_property("reference-level", - config["targetlevel"].as_number()) self._sink = self.Gst.ElementFactory.make("fakesink", "sink") self._pipe = self.Gst.Pipeline() @@ -470,14 +571,14 @@ class GStreamerBackend(Backend): import gi except ImportError: raise FatalReplayGainError( - u"Failed to load GStreamer: python-gi not found" + "Failed to load GStreamer: python-gi not found" ) try: gi.require_version('Gst', '1.0') except ValueError as e: raise FatalReplayGainError( - u"Failed to load GStreamer 1.0: {0}".format(e) + f"Failed to load GStreamer 1.0: {e}" ) from gi.repository import GObject, Gst, GLib @@ -492,7 +593,7 @@ class GStreamerBackend(Backend): self.GLib = GLib self.Gst = Gst - def compute(self, files, album): + def compute(self, files, target_level, album): self._error = None self._files = list(files) @@ -501,6 +602,8 @@ class GStreamerBackend(Backend): self._file_tags = collections.defaultdict(dict) + self._rg.set_property("reference-level", target_level) + if album: self._rg.set_property("num-tracks", len(self._files)) @@ -509,10 +612,10 @@ class GStreamerBackend(Backend): if self._error is not None: raise self._error - def compute_track_gain(self, items): - self.compute(items, False) + def compute_track_gain(self, items, target_level, peak): + self.compute(items, target_level, False) if len(self._file_tags) != len(items): - raise ReplayGainError(u"Some tracks did not receive tags") + raise ReplayGainError("Some tracks did not receive tags") ret = [] for item in items: @@ -521,11 +624,11 @@ class GStreamerBackend(Backend): return ret - def compute_album_gain(self, album): - items = list(album.items()) - self.compute(items, True) + def compute_album_gain(self, items, target_level, peak): + items = list(items) + self.compute(items, target_level, True) if len(self._file_tags) != len(items): - raise ReplayGainError(u"Some items in album did not receive tags") + raise ReplayGainError("Some items in album did not receive tags") # Collect track gains. track_gains = [] @@ -534,7 +637,7 @@ class GStreamerBackend(Backend): gain = self._file_tags[item]["TRACK_GAIN"] peak = self._file_tags[item]["TRACK_PEAK"] except KeyError: - raise ReplayGainError(u"results missing for track") + raise ReplayGainError("results missing for track") track_gains.append(Gain(gain, peak)) # Get album gain information from the last track. @@ -543,7 +646,7 @@ class GStreamerBackend(Backend): gain = last_tags["ALBUM_GAIN"] peak = last_tags["ALBUM_PEAK"] except KeyError: - raise ReplayGainError(u"results missing for album") + raise ReplayGainError("results missing for album") return AlbumGain(Gain(gain, peak), track_gains) @@ -565,7 +668,7 @@ class GStreamerBackend(Backend): f = self._src.get_property("location") # A GStreamer error, either an unsupported format or a bug. self._error = ReplayGainError( - u"Error {0!r} - {1!r} on file {2!r}".format(err, debug, f) + f"Error {err!r} - {debug!r} on file {f!r}" ) def _on_tag(self, bus, message): @@ -678,7 +781,7 @@ class AudioToolsBackend(Backend): """ def __init__(self, config, log): - super(AudioToolsBackend, self).__init__(config, log) + super().__init__(config, log) self._import_audiotools() def _import_audiotools(self): @@ -692,7 +795,7 @@ class AudioToolsBackend(Backend): import audiotools.replaygain except ImportError: raise FatalReplayGainError( - u"Failed to load audiotools: audiotools not found" + "Failed to load audiotools: audiotools not found" ) self._mod_audiotools = audiotools self._mod_replaygain = audiotools.replaygain @@ -707,14 +810,14 @@ class AudioToolsBackend(Backend): file format is not supported """ try: - audiofile = self._mod_audiotools.open(item.path) - except IOError: + audiofile = self._mod_audiotools.open(py3_path(syspath(item.path))) + except OSError: raise ReplayGainError( - u"File {} was not found".format(item.path) + f"File {item.path} was not found" ) except self._mod_audiotools.UnsupportedFile: raise ReplayGainError( - u"Unsupported file type {}".format(item.format) + f"Unsupported file type {item.format}" ) return audiofile @@ -733,18 +836,25 @@ class AudioToolsBackend(Backend): rg = self._mod_replaygain.ReplayGain(audiofile.sample_rate()) except ValueError: raise ReplayGainError( - u"Unsupported sample rate {}".format(item.samplerate)) + f"Unsupported sample rate {item.samplerate}") return return rg - def compute_track_gain(self, items): + def compute_track_gain(self, items, target_level, peak): """Compute ReplayGain values for the requested items. :return list: list of :class:`Gain` objects """ - return [self._compute_track_gain(item) for item in items] + return [self._compute_track_gain(item, target_level) for item in items] - def _title_gain(self, rg, audiofile): + def _with_target_level(self, gain, target_level): + """Return `gain` relative to `target_level`. + + Assumes `gain` is relative to 89 db. + """ + return gain + (target_level - 89) + + def _title_gain(self, rg, audiofile, target_level): """Get the gain result pair from PyAudioTools using the `ReplayGain` instance `rg` for the given `audiofile`. @@ -754,14 +864,15 @@ class AudioToolsBackend(Backend): try: # The method needs an audiotools.PCMReader instance that can # be obtained from an audiofile instance. - return rg.title_gain(audiofile.to_pcm()) + gain, peak = rg.title_gain(audiofile.to_pcm()) except ValueError as exc: # `audiotools.replaygain` can raise a `ValueError` if the sample # rate is incorrect. - self._log.debug(u'error in rg.title_gain() call: {}', exc) - raise ReplayGainError(u'audiotools audio data error') + self._log.debug('error in rg.title_gain() call: {}', exc) + raise ReplayGainError('audiotools audio data error') + return self._with_target_level(gain, target_level), peak - def _compute_track_gain(self, item): + def _compute_track_gain(self, item, target_level): """Compute ReplayGain value for the requested item. :rtype: :class:`Gain` @@ -771,41 +882,44 @@ class AudioToolsBackend(Backend): # Each call to title_gain on a ReplayGain object returns peak and gain # of the track. - rg_track_gain, rg_track_peak = self._title_gain(rg, audiofile) + rg_track_gain, rg_track_peak = self._title_gain( + rg, audiofile, target_level + ) - self._log.debug(u'ReplayGain for track {0} - {1}: {2:.2f}, {3:.2f}', + self._log.debug('ReplayGain for track {0} - {1}: {2:.2f}, {3:.2f}', item.artist, item.title, rg_track_gain, rg_track_peak) return Gain(gain=rg_track_gain, peak=rg_track_peak) - def compute_album_gain(self, album): + def compute_album_gain(self, items, target_level, peak): """Compute ReplayGain values for the requested album and its items. :rtype: :class:`AlbumGain` """ - self._log.debug(u'Analysing album {0}', album) - # The first item is taken and opened to get the sample rate to # initialize the replaygain object. The object is used for all the # tracks in the album to get the album values. - item = list(album.items())[0] + item = list(items)[0] audiofile = self.open_audio_file(item) rg = self.init_replaygain(audiofile, item) track_gains = [] - for item in album.items(): + for item in items: audiofile = self.open_audio_file(item) - rg_track_gain, rg_track_peak = self._title_gain(rg, audiofile) + rg_track_gain, rg_track_peak = self._title_gain( + rg, audiofile, target_level + ) track_gains.append( Gain(gain=rg_track_gain, peak=rg_track_peak) ) - self._log.debug(u'ReplayGain for track {0}: {1:.2f}, {2:.2f}', + self._log.debug('ReplayGain for track {0}: {1:.2f}, {2:.2f}', item, rg_track_gain, rg_track_peak) # After getting the values for all tracks, it's possible to get the # album values. rg_album_gain, rg_album_peak = rg.album_gain() - self._log.debug(u'ReplayGain for album {0}: {1:.2f}, {2:.2f}', - album, rg_album_gain, rg_album_peak) + rg_album_gain = self._with_target_level(rg_album_gain, target_level) + self._log.debug('ReplayGain for album {0}: {1:.2f}, {2:.2f}', + items[0].album, rg_album_gain, rg_album_peak) return AlbumGain( Gain(gain=rg_album_gain, peak=rg_album_peak), @@ -813,6 +927,33 @@ class AudioToolsBackend(Backend): ) +class ExceptionWatcher(Thread): + """Monitors a queue for exceptions asynchronously. + Once an exception occurs, raise it and execute a callback. + """ + + def __init__(self, queue, callback): + self._queue = queue + self._callback = callback + self._stopevent = Event() + Thread.__init__(self) + + def run(self): + while not self._stopevent.is_set(): + try: + exc = self._queue.get_nowait() + self._callback() + raise exc[1].with_traceback(exc[2]) + except queue.Empty: + # No exceptions yet, loop back to check + # whether `_stopevent` is set + pass + + def join(self, timeout=None): + self._stopevent.set() + Thread.join(self, timeout) + + # Main plugin logic. class ReplayGainPlugin(BeetsPlugin): @@ -823,48 +964,72 @@ class ReplayGainPlugin(BeetsPlugin): "command": CommandBackend, "gstreamer": GStreamerBackend, "audiotools": AudioToolsBackend, - "bs1770gain": Bs1770gainBackend, + "ffmpeg": FfmpegBackend, + } + + peak_methods = { + "true": Peak.true, + "sample": Peak.sample, } def __init__(self): - super(ReplayGainPlugin, self).__init__() + super().__init__() # default backend is 'command' for backward-compatibility. self.config.add({ 'overwrite': False, 'auto': True, - 'backend': u'command', + 'backend': 'command', + 'threads': cpu_count(), + 'parallel_on_import': False, + 'per_disc': False, + 'peak': 'true', 'targetlevel': 89, 'r128': ['Opus'], + 'r128_targetlevel': lufs_to_db(-23), }) self.overwrite = self.config['overwrite'].get(bool) - backend_name = self.config['backend'].as_str() - if backend_name not in self.backends: + self.per_disc = self.config['per_disc'].get(bool) + + # Remember which backend is used for CLI feedback + self.backend_name = self.config['backend'].as_str() + + if self.backend_name not in self.backends: raise ui.UserError( - u"Selected ReplayGain backend {0} is not supported. " - u"Please select one of: {1}".format( - backend_name, - u', '.join(self.backends.keys()) + "Selected ReplayGain backend {} is not supported. " + "Please select one of: {}".format( + self.backend_name, + ', '.join(self.backends.keys()) ) ) + peak_method = self.config["peak"].as_str() + if peak_method not in self.peak_methods: + raise ui.UserError( + "Selected ReplayGain peak method {} is not supported. " + "Please select one of: {}".format( + peak_method, + ', '.join(self.peak_methods.keys()) + ) + ) + self._peak_method = self.peak_methods[peak_method] # On-import analysis. if self.config['auto']: + self.register_listener('import_begin', self.import_begin) + self.register_listener('import', self.import_end) self.import_stages = [self.imported] # Formats to use R128. self.r128_whitelist = self.config['r128'].as_str_seq() try: - self.backend_instance = self.backends[backend_name]( + self.backend_instance = self.backends[self.backend_name]( self.config, self._log ) except (ReplayGainError, FatalReplayGainError) as e: raise ui.UserError( - u'replaygain initialization failed: {0}'.format(e)) - - self.r128_backend_instance = '' + f'replaygain initialization failed: {e}') def should_use_r128(self, item): """Checks the plugin setting to decide whether the calculation @@ -895,29 +1060,47 @@ class ReplayGainPlugin(BeetsPlugin): item.rg_track_gain = track_gain.gain item.rg_track_peak = track_gain.peak item.store() - - self._log.debug(u'applied track gain {0}, peak {1}', + self._log.debug('applied track gain {0} LU, peak {1} of FS', item.rg_track_gain, item.rg_track_peak) + def store_album_gain(self, item, album_gain): + item.rg_album_gain = album_gain.gain + item.rg_album_peak = album_gain.peak + item.store() + self._log.debug('applied album gain {0} LU, peak {1} of FS', + item.rg_album_gain, item.rg_album_peak) + def store_track_r128_gain(self, item, track_gain): - item.r128_track_gain = int(round(track_gain.gain * pow(2, 8))) + item.r128_track_gain = track_gain.gain item.store() - self._log.debug(u'applied track gain {0}', item.r128_track_gain) + self._log.debug('applied r128 track gain {0} LU', + item.r128_track_gain) - def store_album_gain(self, album, album_gain): - album.rg_album_gain = album_gain.gain - album.rg_album_peak = album_gain.peak - album.store() + def store_album_r128_gain(self, item, album_gain): + item.r128_album_gain = album_gain.gain + item.store() + self._log.debug('applied r128 album gain {0} LU', + item.r128_album_gain) - self._log.debug(u'applied album gain {0}, peak {1}', - album.rg_album_gain, album.rg_album_peak) + def tag_specific_values(self, items): + """Return some tag specific values. - def store_album_r128_gain(self, album, album_gain): - album.r128_album_gain = int(round(album_gain.gain * pow(2, 8))) - album.store() + Returns a tuple (store_track_gain, store_album_gain, target_level, + peak_method). + """ + if any([self.should_use_r128(item) for item in items]): + store_track_gain = self.store_track_r128_gain + store_album_gain = self.store_album_r128_gain + target_level = self.config['r128_targetlevel'].as_number() + peak = Peak.none # R128_* tags do not store the track/album peak + else: + store_track_gain = self.store_track_gain + store_album_gain = self.store_album_gain + target_level = self.config['targetlevel'].as_number() + peak = self._peak_method - self._log.debug(u'applied album gain {0}', album.r128_album_gain) + return store_track_gain, store_album_gain, target_level, peak def handle_album(self, album, write, force=False): """Compute album and track replay gain store it in all of the @@ -928,47 +1111,65 @@ class ReplayGainPlugin(BeetsPlugin): items, nothing is done. """ if not force and not self.album_requires_gain(album): - self._log.info(u'Skipping album {0}', album) + self._log.info('Skipping album {0}', album) return - self._log.info(u'analyzing {0}', album) - if (any([self.should_use_r128(item) for item in album.items()]) and not - all(([self.should_use_r128(item) for item in album.items()]))): - raise ReplayGainError( - u"Mix of ReplayGain and EBU R128 detected" - u" for some tracks in album {0}".format(album) - ) + all([self.should_use_r128(item) for item in album.items()])): + self._log.error( + "Cannot calculate gain for album {0} (incompatible formats)", + album) + return - if any([self.should_use_r128(item) for item in album.items()]): - if self.r128_backend_instance == '': - self.init_r128_backend() - backend_instance = self.r128_backend_instance - store_track_gain = self.store_track_r128_gain - store_album_gain = self.store_album_r128_gain + self._log.info('analyzing {0}', album) + + tag_vals = self.tag_specific_values(album.items()) + store_track_gain, store_album_gain, target_level, peak = tag_vals + + discs = {} + if self.per_disc: + for item in album.items(): + if discs.get(item.disc) is None: + discs[item.disc] = [] + discs[item.disc].append(item) else: - backend_instance = self.backend_instance - store_track_gain = self.store_track_gain - store_album_gain = self.store_album_gain + discs[1] = album.items() - try: - album_gain = backend_instance.compute_album_gain(album) - if len(album_gain.track_gains) != len(album.items()): - raise ReplayGainError( - u"ReplayGain backend failed " - u"for some tracks in album {0}".format(album) + for discnumber, items in discs.items(): + def _store_album(album_gain): + if not album_gain or not album_gain.album_gain \ + or len(album_gain.track_gains) != len(items): + # In some cases, backends fail to produce a valid + # `album_gain` without throwing FatalReplayGainError + # => raise non-fatal exception & continue + raise ReplayGainError( + "ReplayGain backend `{}` failed " + "for some tracks in album {}" + .format(self.backend_name, album) + ) + for item, track_gain in zip(items, + album_gain.track_gains): + store_track_gain(item, track_gain) + store_album_gain(item, album_gain.album_gain) + if write: + item.try_write() + self._log.debug('done analyzing {0}', item) + + try: + self._apply( + self.backend_instance.compute_album_gain, args=(), + kwds={ + "items": list(items), + "target_level": target_level, + "peak": peak + }, + callback=_store_album ) - - store_album_gain(album, album_gain.album_gain) - for item, track_gain in zip(album.items(), album_gain.track_gains): - store_track_gain(item, track_gain) - if write: - item.try_write() - except ReplayGainError as e: - self._log.info(u"ReplayGain error: {0}", e) - except FatalReplayGainError as e: - raise ui.UserError( - u"Fatal replay gain error: {0}".format(e)) + except ReplayGainError as e: + self._log.info("ReplayGain error: {0}", e) + except FatalReplayGainError as e: + raise ui.UserError( + f"Fatal replay gain error: {e}") def handle_track(self, item, write, force=False): """Compute track replay gain and store it in the item. @@ -978,83 +1179,190 @@ class ReplayGainPlugin(BeetsPlugin): in the item, nothing is done. """ if not force and not self.track_requires_gain(item): - self._log.info(u'Skipping track {0}', item) + self._log.info('Skipping track {0}', item) return - self._log.info(u'analyzing {0}', item) + tag_vals = self.tag_specific_values([item]) + store_track_gain, store_album_gain, target_level, peak = tag_vals - if self.should_use_r128(item): - if self.r128_backend_instance == '': - self.init_r128_backend() - backend_instance = self.r128_backend_instance - store_track_gain = self.store_track_r128_gain - else: - backend_instance = self.backend_instance - store_track_gain = self.store_track_gain - - try: - track_gains = backend_instance.compute_track_gain([item]) - if len(track_gains) != 1: + def _store_track(track_gains): + if not track_gains or len(track_gains) != 1: + # In some cases, backends fail to produce a valid + # `track_gains` without throwing FatalReplayGainError + # => raise non-fatal exception & continue raise ReplayGainError( - u"ReplayGain backend failed for track {0}".format(item) + "ReplayGain backend `{}` failed for track {}" + .format(self.backend_name, item) ) store_track_gain(item, track_gains[0]) if write: item.try_write() - except ReplayGainError as e: - self._log.info(u"ReplayGain error: {0}", e) - except FatalReplayGainError as e: - raise ui.UserError( - u"Fatal replay gain error: {0}".format(e)) - - def init_r128_backend(self): - backend_name = 'bs1770gain' + self._log.debug('done analyzing {0}', item) try: - self.r128_backend_instance = self.backends[backend_name]( - self.config, self._log + self._apply( + self.backend_instance.compute_track_gain, args=(), + kwds={ + "items": [item], + "target_level": target_level, + "peak": peak, + }, + callback=_store_track ) - except (ReplayGainError, FatalReplayGainError) as e: - raise ui.UserError( - u'replaygain initialization failed: {0}'.format(e)) + except ReplayGainError as e: + self._log.info("ReplayGain error: {0}", e) + except FatalReplayGainError as e: + raise ui.UserError(f"Fatal replay gain error: {e}") - self.r128_backend_instance.method = '--ebu' + def _has_pool(self): + """Check whether a `ThreadPool` is running instance in `self.pool` + """ + if hasattr(self, 'pool'): + if isinstance(self.pool, ThreadPool) and self.pool._state == RUN: + return True + return False + + def open_pool(self, threads): + """Open a `ThreadPool` instance in `self.pool` + """ + if not self._has_pool() and self.backend_instance.do_parallel: + self.pool = ThreadPool(threads) + self.exc_queue = queue.Queue() + + signal.signal(signal.SIGINT, self._interrupt) + + self.exc_watcher = ExceptionWatcher( + self.exc_queue, # threads push exceptions here + self.terminate_pool # abort once an exception occurs + ) + self.exc_watcher.start() + + def _apply(self, func, args, kwds, callback): + if self._has_pool(): + def catch_exc(func, exc_queue, log): + """Wrapper to catch raised exceptions in threads + """ + def wfunc(*args, **kwargs): + try: + return func(*args, **kwargs) + except ReplayGainError as e: + log.info(e.args[0]) # log non-fatal exceptions + except Exception: + exc_queue.put(sys.exc_info()) + return wfunc + + # Wrap function and callback to catch exceptions + func = catch_exc(func, self.exc_queue, self._log) + callback = catch_exc(callback, self.exc_queue, self._log) + + self.pool.apply_async(func, args, kwds, callback) + else: + callback(func(*args, **kwds)) + + def terminate_pool(self): + """Terminate the `ThreadPool` instance in `self.pool` + (e.g. stop execution in case of exception) + """ + # Don't call self._as_pool() here, + # self.pool._state may not be == RUN + if hasattr(self, 'pool') and isinstance(self.pool, ThreadPool): + self.pool.terminate() + self.pool.join() + # self.exc_watcher.join() + + def _interrupt(self, signal, frame): + try: + self._log.info('interrupted') + self.terminate_pool() + sys.exit(0) + except SystemExit: + # Silence raised SystemExit ~ exit(0) + pass + + def close_pool(self): + """Close the `ThreadPool` instance in `self.pool` (if there is one) + """ + if self._has_pool(): + self.pool.close() + self.pool.join() + self.exc_watcher.join() + + def import_begin(self, session): + """Handle `import_begin` event -> open pool + """ + threads = self.config['threads'].get(int) + + if self.config['parallel_on_import'] \ + and self.config['auto'] \ + and threads: + self.open_pool(threads) + + def import_end(self, paths): + """Handle `import` event -> close pool + """ + self.close_pool() def imported(self, session, task): """Add replay gain info to items or albums of ``task``. """ - if task.is_album: - self.handle_album(task.album, False) - else: - self.handle_track(task.item, False) + if self.config['auto']: + if task.is_album: + self.handle_album(task.album, False) + else: + self.handle_track(task.item, False) + + def command_func(self, lib, opts, args): + try: + write = ui.should_write(opts.write) + force = opts.force + + # Bypass self.open_pool() if called with `--threads 0` + if opts.threads != 0: + threads = opts.threads or self.config['threads'].get(int) + self.open_pool(threads) + + if opts.album: + albums = lib.albums(ui.decargs(args)) + self._log.info( + "Analyzing {} albums ~ {} backend..." + .format(len(albums), self.backend_name) + ) + for album in albums: + self.handle_album(album, write, force) + else: + items = lib.items(ui.decargs(args)) + self._log.info( + "Analyzing {} tracks ~ {} backend..." + .format(len(items), self.backend_name) + ) + for item in items: + self.handle_track(item, write, force) + + self.close_pool() + except (SystemExit, KeyboardInterrupt): + # Silence interrupt exceptions + pass def commands(self): """Return the "replaygain" ui subcommand. """ - def func(lib, opts, args): - write = ui.should_write(opts.write) - force = opts.force - - if opts.album: - for album in lib.albums(ui.decargs(args)): - self.handle_album(album, write, force) - - else: - for item in lib.items(ui.decargs(args)): - self.handle_track(item, write, force) - - cmd = ui.Subcommand('replaygain', help=u'analyze for ReplayGain') + cmd = ui.Subcommand('replaygain', help='analyze for ReplayGain') cmd.parser.add_album_option() + cmd.parser.add_option( + "-t", "--threads", dest="threads", type=int, + help='change the number of threads, \ + defaults to maximum available processors' + ) cmd.parser.add_option( "-f", "--force", dest="force", action="store_true", default=False, - help=u"analyze all files, including those that " + help="analyze all files, including those that " "already have ReplayGain metadata") cmd.parser.add_option( "-w", "--write", default=None, action="store_true", - help=u"write new metadata to files' tags") + help="write new metadata to files' tags") cmd.parser.add_option( "-W", "--nowrite", dest="write", action="store_false", - help=u"don't write metadata (opposite of -w)") - cmd.func = func + help="don't write metadata (opposite of -w)") + cmd.func = self.command_func return [cmd] diff --git a/libs/common/beetsplug/rewrite.py b/libs/common/beetsplug/rewrite.py index eadb1425..e02e4080 100644 --- a/libs/common/beetsplug/rewrite.py +++ b/libs/common/beetsplug/rewrite.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -16,7 +15,6 @@ """Uses user-specified rewriting rules to canonicalize names for path formats. """ -from __future__ import division, absolute_import, print_function import re from collections import defaultdict @@ -44,7 +42,7 @@ def rewriter(field, rules): class RewritePlugin(BeetsPlugin): def __init__(self): - super(RewritePlugin, self).__init__() + super().__init__() self.config.add({}) @@ -55,11 +53,11 @@ class RewritePlugin(BeetsPlugin): try: fieldname, pattern = key.split(None, 1) except ValueError: - raise ui.UserError(u"invalid rewrite specification") + raise ui.UserError("invalid rewrite specification") if fieldname not in library.Item._fields: - raise ui.UserError(u"invalid field name (%s) in rewriter" % + raise ui.UserError("invalid field name (%s) in rewriter" % fieldname) - self._log.debug(u'adding template field {0}', key) + self._log.debug('adding template field {0}', key) pattern = re.compile(pattern.lower()) rules[fieldname].append((pattern, value)) if fieldname == 'artist': diff --git a/libs/common/beetsplug/scrub.py b/libs/common/beetsplug/scrub.py index be6e7fd1..d8044668 100644 --- a/libs/common/beetsplug/scrub.py +++ b/libs/common/beetsplug/scrub.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -17,13 +16,12 @@ automatically whenever tags are written. """ -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets import ui from beets import util from beets import config -from beets import mediafile +import mediafile import mutagen _MUTAGEN_FORMATS = { @@ -48,7 +46,7 @@ _MUTAGEN_FORMATS = { class ScrubPlugin(BeetsPlugin): """Removes extraneous metadata from files' tags.""" def __init__(self): - super(ScrubPlugin, self).__init__() + super().__init__() self.config.add({ 'auto': True, }) @@ -60,15 +58,15 @@ class ScrubPlugin(BeetsPlugin): def scrub_func(lib, opts, args): # Walk through matching files and remove tags. for item in lib.items(ui.decargs(args)): - self._log.info(u'scrubbing: {0}', + self._log.info('scrubbing: {0}', util.displayable_path(item.path)) self._scrub_item(item, opts.write) - scrub_cmd = ui.Subcommand('scrub', help=u'clean audio tags') + scrub_cmd = ui.Subcommand('scrub', help='clean audio tags') scrub_cmd.parser.add_option( - u'-W', u'--nowrite', dest='write', + '-W', '--nowrite', dest='write', action='store_false', default=True, - help=u'leave tags empty') + help='leave tags empty') scrub_cmd.func = scrub_func return [scrub_cmd] @@ -79,7 +77,7 @@ class ScrubPlugin(BeetsPlugin): """ classes = [] for modname, clsname in _MUTAGEN_FORMATS.items(): - mod = __import__('mutagen.{0}'.format(modname), + mod = __import__(f'mutagen.{modname}', fromlist=[clsname]) classes.append(getattr(mod, clsname)) return classes @@ -107,8 +105,8 @@ class ScrubPlugin(BeetsPlugin): for tag in f.keys(): del f[tag] f.save() - except (IOError, mutagen.MutagenError) as exc: - self._log.error(u'could not scrub {0}: {1}', + except (OSError, mutagen.MutagenError) as exc: + self._log.error('could not scrub {0}: {1}', util.displayable_path(path), exc) def _scrub_item(self, item, restore=True): @@ -121,7 +119,7 @@ class ScrubPlugin(BeetsPlugin): mf = mediafile.MediaFile(util.syspath(item.path), config['id3v23'].get(bool)) except mediafile.UnreadableFileError as exc: - self._log.error(u'could not open file to scrub: {0}', + self._log.error('could not open file to scrub: {0}', exc) return images = mf.images @@ -131,21 +129,21 @@ class ScrubPlugin(BeetsPlugin): # Restore tags, if enabled. if restore: - self._log.debug(u'writing new tags after scrub') + self._log.debug('writing new tags after scrub') item.try_write() if images: - self._log.debug(u'restoring art') + self._log.debug('restoring art') try: mf = mediafile.MediaFile(util.syspath(item.path), config['id3v23'].get(bool)) mf.images = images mf.save() except mediafile.UnreadableFileError as exc: - self._log.error(u'could not write tags: {0}', exc) + self._log.error('could not write tags: {0}', exc) def import_task_files(self, session, task): """Automatically scrub imported files.""" for item in task.imported_items(): - self._log.debug(u'auto-scrubbing {0}', + self._log.debug('auto-scrubbing {0}', util.displayable_path(item.path)) self._scrub_item(item) diff --git a/libs/common/beetsplug/smartplaylist.py b/libs/common/beetsplug/smartplaylist.py index 009512c5..4c921ecc 100644 --- a/libs/common/beetsplug/smartplaylist.py +++ b/libs/common/beetsplug/smartplaylist.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Dang Mai . # @@ -16,30 +15,38 @@ """Generates smart playlists based on beets queries. """ -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets import ui from beets.util import (mkdirall, normpath, sanitize_path, syspath, - bytestring_path) + bytestring_path, path_as_posix) from beets.library import Item, Album, parse_query_string from beets.dbcore import OrQuery from beets.dbcore.query import MultipleSort, ParsingError import os -import six + +try: + from urllib.request import pathname2url +except ImportError: + # python2 is a bit different + from urllib import pathname2url class SmartPlaylistPlugin(BeetsPlugin): def __init__(self): - super(SmartPlaylistPlugin, self).__init__() + super().__init__() self.config.add({ 'relative_to': None, - 'playlist_dir': u'.', + 'playlist_dir': '.', 'auto': True, - 'playlists': [] + 'playlists': [], + 'forward_slash': False, + 'prefix': '', + 'urlencode': False, }) + self.config['prefix'].redact = True # May contain username/password. self._matched_playlists = None self._unmatched_playlists = None @@ -49,8 +56,8 @@ class SmartPlaylistPlugin(BeetsPlugin): def commands(self): spl_update = ui.Subcommand( 'splupdate', - help=u'update the smart playlists. Playlist names may be ' - u'passed as arguments.' + help='update the smart playlists. Playlist names may be ' + 'passed as arguments.' ) spl_update.func = self.update_cmd return [spl_update] @@ -61,14 +68,14 @@ class SmartPlaylistPlugin(BeetsPlugin): args = set(ui.decargs(args)) for a in list(args): if not a.endswith(".m3u"): - args.add("{0}.m3u".format(a)) + args.add(f"{a}.m3u") - playlists = set((name, q, a_q) - for name, q, a_q in self._unmatched_playlists - if name in args) + playlists = {(name, q, a_q) + for name, q, a_q in self._unmatched_playlists + if name in args} if not playlists: raise ui.UserError( - u'No playlist matching any of {0} found'.format( + 'No playlist matching any of {} found'.format( [name for name, _, _ in self._unmatched_playlists]) ) @@ -81,7 +88,7 @@ class SmartPlaylistPlugin(BeetsPlugin): def build_queries(self): """ - Instanciate queries for the playlists. + Instantiate queries for the playlists. Each playlist has 2 queries: one or items one for albums, each with a sort. We must also remember its name. _unmatched_playlists is a set of @@ -99,22 +106,23 @@ class SmartPlaylistPlugin(BeetsPlugin): for playlist in self.config['playlists'].get(list): if 'name' not in playlist: - self._log.warning(u"playlist configuration is missing name") + self._log.warning("playlist configuration is missing name") continue playlist_data = (playlist['name'],) try: - for key, Model in (('query', Item), ('album_query', Album)): + for key, model_cls in (('query', Item), + ('album_query', Album)): qs = playlist.get(key) if qs is None: query_and_sort = None, None - elif isinstance(qs, six.string_types): - query_and_sort = parse_query_string(qs, Model) + elif isinstance(qs, str): + query_and_sort = parse_query_string(qs, model_cls) elif len(qs) == 1: - query_and_sort = parse_query_string(qs[0], Model) + query_and_sort = parse_query_string(qs[0], model_cls) else: # multiple queries and sorts - queries, sorts = zip(*(parse_query_string(q, Model) + queries, sorts = zip(*(parse_query_string(q, model_cls) for q in qs)) query = OrQuery(queries) final_sorts = [] @@ -135,7 +143,7 @@ class SmartPlaylistPlugin(BeetsPlugin): playlist_data += (query_and_sort,) except ParsingError as exc: - self._log.warning(u"invalid query in playlist {}: {}", + self._log.warning("invalid query in playlist {}: {}", playlist['name'], exc) continue @@ -156,14 +164,14 @@ class SmartPlaylistPlugin(BeetsPlugin): n, (q, _), (a_q, _) = playlist if self.matches(model, q, a_q): self._log.debug( - u"{0} will be updated because of {1}", n, model) + "{0} will be updated because of {1}", n, model) self._matched_playlists.add(playlist) self.register_listener('cli_exit', self.update_playlists) self._unmatched_playlists -= self._matched_playlists def update_playlists(self, lib): - self._log.info(u"Updating {0} smart playlists...", + self._log.info("Updating {0} smart playlists...", len(self._matched_playlists)) playlist_dir = self.config['playlist_dir'].as_filename() @@ -177,7 +185,7 @@ class SmartPlaylistPlugin(BeetsPlugin): for playlist in self._matched_playlists: name, (query, q_sort), (album_query, a_q_sort) = playlist - self._log.debug(u"Creating playlist {0}", name) + self._log.debug("Creating playlist {0}", name) items = [] if query: @@ -199,6 +207,7 @@ class SmartPlaylistPlugin(BeetsPlugin): if item_path not in m3us[m3u_name]: m3us[m3u_name].append(item_path) + prefix = bytestring_path(self.config['prefix'].as_str()) # Write all of the accumulated track lists to files. for m3u in m3us: m3u_path = normpath(os.path.join(playlist_dir, @@ -206,6 +215,10 @@ class SmartPlaylistPlugin(BeetsPlugin): mkdirall(m3u_path) with open(syspath(m3u_path), 'wb') as f: for path in m3us[m3u]: - f.write(path + b'\n') + if self.config['forward_slash'].get(): + path = path_as_posix(path) + if self.config['urlencode']: + path = bytestring_path(pathname2url(path)) + f.write(prefix + path + b'\n') - self._log.info(u"{0} playlists updated", len(self._matched_playlists)) + self._log.info("{0} playlists updated", len(self._matched_playlists)) diff --git a/libs/common/beetsplug/sonosupdate.py b/libs/common/beetsplug/sonosupdate.py index 56a315a1..aeb211d8 100644 --- a/libs/common/beetsplug/sonosupdate.py +++ b/libs/common/beetsplug/sonosupdate.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2018, Tobias Sauerwein. # @@ -16,7 +15,6 @@ """Updates a Sonos library whenever the beets library is changed. This is based on the Kodi Update plugin. """ -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin import soco @@ -24,7 +22,7 @@ import soco class SonosUpdate(BeetsPlugin): def __init__(self): - super(SonosUpdate, self).__init__() + super().__init__() self.register_listener('database_change', self.listen_for_db_change) def listen_for_db_change(self, lib, model): @@ -35,14 +33,14 @@ class SonosUpdate(BeetsPlugin): """When the client exists try to send refresh request to a Sonos controler. """ - self._log.info(u'Requesting a Sonos library update...') + self._log.info('Requesting a Sonos library update...') device = soco.discovery.any_soco() if device: device.music_library.start_library_update() else: - self._log.warning(u'Could not find a Sonos device.') + self._log.warning('Could not find a Sonos device.') return - self._log.info(u'Sonos update triggered') + self._log.info('Sonos update triggered') diff --git a/libs/common/beetsplug/spotify.py b/libs/common/beetsplug/spotify.py index 36231f29..2529160d 100644 --- a/libs/common/beetsplug/spotify.py +++ b/libs/common/beetsplug/spotify.py @@ -1,61 +1,379 @@ -# -*- coding: utf-8 -*- +# This file is part of beets. +# Copyright 2019, Rahul Ahuja. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function +"""Adds Spotify release and track search support to the autotagger, along with +Spotify playlist construction. +""" import re +import json +import base64 import webbrowser +import collections + +import unidecode import requests -from beets.plugins import BeetsPlugin -from beets.ui import decargs +import confuse + from beets import ui -from requests.exceptions import HTTPError +from beets.autotag.hooks import AlbumInfo, TrackInfo +from beets.plugins import MetadataSourcePlugin, BeetsPlugin -class SpotifyPlugin(BeetsPlugin): +class SpotifyPlugin(MetadataSourcePlugin, BeetsPlugin): + data_source = 'Spotify' - # URL for the Web API of Spotify - # Documentation here: https://developer.spotify.com/web-api/search-item/ - base_url = "https://api.spotify.com/v1/search" - open_url = "http://open.spotify.com/track/" - playlist_partial = "spotify:trackset:Playlist:" + # Base URLs for the Spotify API + # Documentation: https://developer.spotify.com/web-api + oauth_token_url = 'https://accounts.spotify.com/api/token' + open_track_url = 'https://open.spotify.com/track/' + search_url = 'https://api.spotify.com/v1/search' + album_url = 'https://api.spotify.com/v1/albums/' + track_url = 'https://api.spotify.com/v1/tracks/' + + # Spotify IDs consist of 22 alphanumeric characters + # (zero-left-padded base62 representation of randomly generated UUID4) + id_regex = { + 'pattern': r'(^|open\.spotify\.com/{}/)([0-9A-Za-z]{{22}})', + 'match_group': 2, + } def __init__(self): - super(SpotifyPlugin, self).__init__() - self.config.add({ - 'mode': 'list', - 'tiebreak': 'popularity', - 'show_failures': False, - 'artist_field': 'albumartist', - 'album_field': 'album', - 'track_field': 'title', - 'region_filter': None, - 'regex': [] - }) + super().__init__() + self.config.add( + { + 'mode': 'list', + 'tiebreak': 'popularity', + 'show_failures': False, + 'artist_field': 'albumartist', + 'album_field': 'album', + 'track_field': 'title', + 'region_filter': None, + 'regex': [], + 'client_id': '4e414367a1d14c75a5c5129a627fcab8', + 'client_secret': 'f82bdc09b2254f1a8286815d02fd46dc', + 'tokenfile': 'spotify_token.json', + } + ) + self.config['client_secret'].redact = True + + self.tokenfile = self.config['tokenfile'].get( + confuse.Filename(in_app_dir=True) + ) # Path to the JSON file for storing the OAuth access token. + self.setup() + + def setup(self): + """Retrieve previously saved OAuth token or generate a new one.""" + try: + with open(self.tokenfile) as f: + token_data = json.load(f) + except OSError: + self._authenticate() + else: + self.access_token = token_data['access_token'] + + def _authenticate(self): + """Request an access token via the Client Credentials Flow: + https://developer.spotify.com/documentation/general/guides/authorization-guide/#client-credentials-flow + """ + headers = { + 'Authorization': 'Basic {}'.format( + base64.b64encode( + ':'.join( + self.config[k].as_str() + for k in ('client_id', 'client_secret') + ).encode() + ).decode() + ) + } + response = requests.post( + self.oauth_token_url, + data={'grant_type': 'client_credentials'}, + headers=headers, + ) + try: + response.raise_for_status() + except requests.exceptions.HTTPError as e: + raise ui.UserError( + 'Spotify authorization failed: {}\n{}'.format( + e, response.text + ) + ) + self.access_token = response.json()['access_token'] + + # Save the token for later use. + self._log.debug( + '{} access token: {}', self.data_source, self.access_token + ) + with open(self.tokenfile, 'w') as f: + json.dump({'access_token': self.access_token}, f) + + def _handle_response(self, request_type, url, params=None): + """Send a request, reauthenticating if necessary. + + :param request_type: Type of :class:`Request` constructor, + e.g. ``requests.get``, ``requests.post``, etc. + :type request_type: function + :param url: URL for the new :class:`Request` object. + :type url: str + :param params: (optional) list of tuples or bytes to send + in the query string for the :class:`Request`. + :type params: dict + :return: JSON data for the class:`Response ` object. + :rtype: dict + """ + response = request_type( + url, + headers={'Authorization': f'Bearer {self.access_token}'}, + params=params, + ) + if response.status_code != 200: + if 'token expired' in response.text: + self._log.debug( + '{} access token has expired. Reauthenticating.', + self.data_source, + ) + self._authenticate() + return self._handle_response(request_type, url, params=params) + else: + raise ui.UserError( + '{} API error:\n{}\nURL:\n{}\nparams:\n{}'.format( + self.data_source, response.text, url, params + ) + ) + return response.json() + + def album_for_id(self, album_id): + """Fetch an album by its Spotify ID or URL and return an + AlbumInfo object or None if the album is not found. + + :param album_id: Spotify ID or URL for the album + :type album_id: str + :return: AlbumInfo object for album + :rtype: beets.autotag.hooks.AlbumInfo or None + """ + spotify_id = self._get_id('album', album_id) + if spotify_id is None: + return None + + album_data = self._handle_response( + requests.get, self.album_url + spotify_id + ) + artist, artist_id = self.get_artist(album_data['artists']) + + date_parts = [ + int(part) for part in album_data['release_date'].split('-') + ] + + release_date_precision = album_data['release_date_precision'] + if release_date_precision == 'day': + year, month, day = date_parts + elif release_date_precision == 'month': + year, month = date_parts + day = None + elif release_date_precision == 'year': + year = date_parts[0] + month = None + day = None + else: + raise ui.UserError( + "Invalid `release_date_precision` returned " + "by {} API: '{}'".format( + self.data_source, release_date_precision + ) + ) + + tracks = [] + medium_totals = collections.defaultdict(int) + for i, track_data in enumerate(album_data['tracks']['items'], start=1): + track = self._get_track(track_data) + track.index = i + medium_totals[track.medium] += 1 + tracks.append(track) + for track in tracks: + track.medium_total = medium_totals[track.medium] + + return AlbumInfo( + album=album_data['name'], + album_id=spotify_id, + artist=artist, + artist_id=artist_id, + tracks=tracks, + albumtype=album_data['album_type'], + va=len(album_data['artists']) == 1 + and artist.lower() == 'various artists', + year=year, + month=month, + day=day, + label=album_data['label'], + mediums=max(medium_totals.keys()), + data_source=self.data_source, + data_url=album_data['external_urls']['spotify'], + ) + + def _get_track(self, track_data): + """Convert a Spotify track object dict to a TrackInfo object. + + :param track_data: Simplified track object + (https://developer.spotify.com/documentation/web-api/reference/object-model/#track-object-simplified) + :type track_data: dict + :return: TrackInfo object for track + :rtype: beets.autotag.hooks.TrackInfo + """ + artist, artist_id = self.get_artist(track_data['artists']) + return TrackInfo( + title=track_data['name'], + track_id=track_data['id'], + artist=artist, + artist_id=artist_id, + length=track_data['duration_ms'] / 1000, + index=track_data['track_number'], + medium=track_data['disc_number'], + medium_index=track_data['track_number'], + data_source=self.data_source, + data_url=track_data['external_urls']['spotify'], + ) + + def track_for_id(self, track_id=None, track_data=None): + """Fetch a track by its Spotify ID or URL and return a + TrackInfo object or None if the track is not found. + + :param track_id: (Optional) Spotify ID or URL for the track. Either + ``track_id`` or ``track_data`` must be provided. + :type track_id: str + :param track_data: (Optional) Simplified track object dict. May be + provided instead of ``track_id`` to avoid unnecessary API calls. + :type track_data: dict + :return: TrackInfo object for track + :rtype: beets.autotag.hooks.TrackInfo or None + """ + if track_data is None: + spotify_id = self._get_id('track', track_id) + if spotify_id is None: + return None + track_data = self._handle_response( + requests.get, self.track_url + spotify_id + ) + track = self._get_track(track_data) + + # Get album's tracks to set `track.index` (position on the entire + # release) and `track.medium_total` (total number of tracks on + # the track's disc). + album_data = self._handle_response( + requests.get, self.album_url + track_data['album']['id'] + ) + medium_total = 0 + for i, track_data in enumerate(album_data['tracks']['items'], start=1): + if track_data['disc_number'] == track.medium: + medium_total += 1 + if track_data['id'] == track.track_id: + track.index = i + track.medium_total = medium_total + return track + + @staticmethod + def _construct_search_query(filters=None, keywords=''): + """Construct a query string with the specified filters and keywords to + be provided to the Spotify Search API + (https://developer.spotify.com/documentation/web-api/reference/search/search/#writing-a-query---guidelines). + + :param filters: (Optional) Field filters to apply. + :type filters: dict + :param keywords: (Optional) Query keywords to use. + :type keywords: str + :return: Query string to be provided to the Search API. + :rtype: str + """ + query_components = [ + keywords, + ' '.join(':'.join((k, v)) for k, v in filters.items()), + ] + query = ' '.join([q for q in query_components if q]) + if not isinstance(query, str): + query = query.decode('utf8') + return unidecode.unidecode(query) + + def _search_api(self, query_type, filters=None, keywords=''): + """Query the Spotify Search API for the specified ``keywords``, applying + the provided ``filters``. + + :param query_type: Item type to search across. Valid types are: + 'album', 'artist', 'playlist', and 'track'. + :type query_type: str + :param filters: (Optional) Field filters to apply. + :type filters: dict + :param keywords: (Optional) Query keywords to use. + :type keywords: str + :return: JSON data for the class:`Response ` object or None + if no search results are returned. + :rtype: dict or None + """ + query = self._construct_search_query( + keywords=keywords, filters=filters + ) + if not query: + return None + self._log.debug( + f"Searching {self.data_source} for '{query}'" + ) + response_data = ( + self._handle_response( + requests.get, + self.search_url, + params={'q': query, 'type': query_type}, + ) + .get(query_type + 's', {}) + .get('items', []) + ) + self._log.debug( + "Found {} result(s) from {} for '{}'", + len(response_data), + self.data_source, + query, + ) + return response_data def commands(self): def queries(lib, opts, args): - success = self.parse_opts(opts) + success = self._parse_opts(opts) if success: - results = self.query_spotify(lib, decargs(args)) - self.output_results(results) + results = self._match_library_tracks(lib, ui.decargs(args)) + self._output_match_results(results) + spotify_cmd = ui.Subcommand( - 'spotify', - help=u'build a Spotify playlist' + 'spotify', help=f'build a {self.data_source} playlist' ) spotify_cmd.parser.add_option( - u'-m', u'--mode', action='store', - help=u'"open" to open Spotify with playlist, ' - u'"list" to print (default)' + '-m', + '--mode', + action='store', + help='"open" to open {} with playlist, ' + '"list" to print (default)'.format(self.data_source), ) spotify_cmd.parser.add_option( - u'-f', u'--show-failures', - action='store_true', dest='show_failures', - help=u'list tracks that did not match a Spotify ID' + '-f', + '--show-failures', + action='store_true', + dest='show_failures', + help='list tracks that did not match a {} ID'.format( + self.data_source + ), ) spotify_cmd.func = queries return [spotify_cmd] - def parse_opts(self, opts): + def _parse_opts(self, opts): if opts.mode: self.config['mode'].set(opts.mode) @@ -63,35 +381,47 @@ class SpotifyPlugin(BeetsPlugin): self.config['show_failures'].set(True) if self.config['mode'].get() not in ['list', 'open']: - self._log.warning(u'{0} is not a valid mode', - self.config['mode'].get()) + self._log.warning( + '{0} is not a valid mode', self.config['mode'].get() + ) return False self.opts = opts return True - def query_spotify(self, lib, query): + def _match_library_tracks(self, library, keywords): + """Get a list of simplified track object dicts for library tracks + matching the specified ``keywords``. + :param library: beets library object to query. + :type library: beets.library.Library + :param keywords: Query to match library items against. + :type keywords: str + :return: List of simplified track object dicts for library items + matching the specified query. + :rtype: list[dict] + """ results = [] failures = [] - items = lib.items(query) + items = library.items(keywords) if not items: - self._log.debug(u'Your beets query returned no items, ' - u'skipping spotify') + self._log.debug( + 'Your beets query returned no items, skipping {}.', + self.data_source, + ) return - self._log.info(u'Processing {0} tracks...', len(items)) + self._log.info('Processing {} tracks...', len(items)) for item in items: - # Apply regex transformations if provided for regex in self.config['regex'].get(): if ( - not regex['field'] or - not regex['search'] or - not regex['replace'] + not regex['field'] + or not regex['search'] + or not regex['replace'] ): continue @@ -103,73 +433,95 @@ class SpotifyPlugin(BeetsPlugin): # Custom values can be passed in the config (just in case) artist = item[self.config['artist_field'].get()] album = item[self.config['album_field'].get()] - query = item[self.config['track_field'].get()] - search_url = query + " album:" + album + " artist:" + artist + keywords = item[self.config['track_field'].get()] # Query the Web API for each track, look for the items' JSON data - r = requests.get(self.base_url, params={ - "q": search_url, "type": "track" - }) - self._log.debug('{}', r.url) - try: - r.raise_for_status() - except HTTPError as e: - self._log.debug(u'URL returned a {0} error', - e.response.status_code) - failures.append(search_url) + query_filters = {'artist': artist, 'album': album} + response_data_tracks = self._search_api( + query_type='track', keywords=keywords, filters=query_filters + ) + if not response_data_tracks: + query = self._construct_search_query( + keywords=keywords, filters=query_filters + ) + failures.append(query) continue - r_data = r.json()['tracks']['items'] - # Apply market filter if requested region_filter = self.config['region_filter'].get() if region_filter: - r_data = [x for x in r_data if region_filter - in x['available_markets']] + response_data_tracks = [ + track_data + for track_data in response_data_tracks + if region_filter in track_data['available_markets'] + ] - # Simplest, take the first result - chosen_result = None - if len(r_data) == 1 or self.config['tiebreak'].get() == "first": - self._log.debug(u'Spotify track(s) found, count: {0}', - len(r_data)) - chosen_result = r_data[0] - elif len(r_data) > 1: - # Use the popularity filter - self._log.debug(u'Most popular track chosen, count: {0}', - len(r_data)) - chosen_result = max(r_data, key=lambda x: x['popularity']) - - if chosen_result: - results.append(chosen_result) + if ( + len(response_data_tracks) == 1 + or self.config['tiebreak'].get() == 'first' + ): + self._log.debug( + '{} track(s) found, count: {}', + self.data_source, + len(response_data_tracks), + ) + chosen_result = response_data_tracks[0] else: - self._log.debug(u'No spotify track found: {0}', search_url) - failures.append(search_url) + # Use the popularity filter + self._log.debug( + 'Most popular track chosen, count: {}', + len(response_data_tracks), + ) + chosen_result = max( + response_data_tracks, key=lambda x: x['popularity'] + ) + results.append(chosen_result) failure_count = len(failures) if failure_count > 0: if self.config['show_failures'].get(): - self._log.info(u'{0} track(s) did not match a Spotify ID:', - failure_count) + self._log.info( + '{} track(s) did not match a {} ID:', + failure_count, + self.data_source, + ) for track in failures: - self._log.info(u'track: {0}', track) - self._log.info(u'') + self._log.info('track: {}', track) + self._log.info('') else: - self._log.warning(u'{0} track(s) did not match a Spotify ID;\n' - u'use --show-failures to display', - failure_count) + self._log.warning( + '{} track(s) did not match a {} ID:\n' + 'use --show-failures to display', + failure_count, + self.data_source, + ) return results - def output_results(self, results): - if results: - ids = [x['id'] for x in results] - if self.config['mode'].get() == "open": - self._log.info(u'Attempting to open Spotify with playlist') - spotify_url = self.playlist_partial + ",".join(ids) - webbrowser.open(spotify_url) + def _output_match_results(self, results): + """Open a playlist or print Spotify URLs for the provided track + object dicts. + :param results: List of simplified track object dicts + (https://developer.spotify.com/documentation/web-api/reference/object-model/#track-object-simplified) + :type results: list[dict] + """ + if results: + spotify_ids = [track_data['id'] for track_data in results] + if self.config['mode'].get() == 'open': + self._log.info( + 'Attempting to open {} with playlist'.format( + self.data_source + ) + ) + spotify_url = 'spotify:trackset:Playlist:' + ','.join( + spotify_ids + ) + webbrowser.open(spotify_url) else: - for item in ids: - print(self.open_url + item) + for spotify_id in spotify_ids: + print(self.open_track_url + spotify_id) else: - self._log.warning(u'No Spotify tracks found from beets query') + self._log.warning( + f'No {self.data_source} tracks found from beets query' + ) diff --git a/libs/common/beetsplug/subsonicplaylist.py b/libs/common/beetsplug/subsonicplaylist.py new file mode 100644 index 00000000..ead78919 --- /dev/null +++ b/libs/common/beetsplug/subsonicplaylist.py @@ -0,0 +1,171 @@ +# This file is part of beets. +# Copyright 2019, Joris Jensen +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + + +import random +import string +from xml.etree import ElementTree +from hashlib import md5 +from urllib.parse import urlencode + +import requests + +from beets.dbcore import AndQuery +from beets.dbcore.query import MatchQuery +from beets.plugins import BeetsPlugin +from beets.ui import Subcommand + +__author__ = 'https://github.com/MrNuggelz' + + +def filter_to_be_removed(items, keys): + if len(items) > len(keys): + dont_remove = [] + for artist, album, title in keys: + for item in items: + if artist == item['artist'] and \ + album == item['album'] and \ + title == item['title']: + dont_remove.append(item) + return [item for item in items if item not in dont_remove] + else: + def to_be_removed(item): + for artist, album, title in keys: + if artist == item['artist'] and\ + album == item['album'] and\ + title == item['title']: + return False + return True + + return [item for item in items if to_be_removed(item)] + + +class SubsonicPlaylistPlugin(BeetsPlugin): + + def __init__(self): + super().__init__() + self.config.add( + { + 'delete': False, + 'playlist_ids': [], + 'playlist_names': [], + 'username': '', + 'password': '' + } + ) + self.config['password'].redact = True + + def update_tags(self, playlist_dict, lib): + with lib.transaction(): + for query, playlist_tag in playlist_dict.items(): + query = AndQuery([MatchQuery("artist", query[0]), + MatchQuery("album", query[1]), + MatchQuery("title", query[2])]) + items = lib.items(query) + if not items: + self._log.warn("{} | track not found ({})", playlist_tag, + query) + continue + for item in items: + item.subsonic_playlist = playlist_tag + item.try_sync(write=True, move=False) + + def get_playlist(self, playlist_id): + xml = self.send('getPlaylist', {'id': playlist_id}).text + playlist = ElementTree.fromstring(xml)[0] + if playlist.attrib.get('code', '200') != '200': + alt_error = 'error getting playlist, but no error message found' + self._log.warn(playlist.attrib.get('message', alt_error)) + return + + name = playlist.attrib.get('name', 'undefined') + tracks = [(t.attrib['artist'], t.attrib['album'], t.attrib['title']) + for t in playlist] + return name, tracks + + def commands(self): + def build_playlist(lib, opts, args): + self.config.set_args(opts) + ids = self.config['playlist_ids'].as_str_seq() + if self.config['playlist_names'].as_str_seq(): + playlists = ElementTree.fromstring( + self.send('getPlaylists').text)[0] + if playlists.attrib.get('code', '200') != '200': + alt_error = 'error getting playlists,' \ + ' but no error message found' + self._log.warn( + playlists.attrib.get('message', alt_error)) + return + for name in self.config['playlist_names'].as_str_seq(): + for playlist in playlists: + if name == playlist.attrib['name']: + ids.append(playlist.attrib['id']) + + playlist_dict = self.get_playlists(ids) + + # delete old tags + if self.config['delete']: + existing = list(lib.items('subsonic_playlist:";"')) + to_be_removed = filter_to_be_removed( + existing, + playlist_dict.keys()) + for item in to_be_removed: + item['subsonic_playlist'] = '' + with lib.transaction(): + item.try_sync(write=True, move=False) + + self.update_tags(playlist_dict, lib) + + subsonicplaylist_cmds = Subcommand( + 'subsonicplaylist', help='import a subsonic playlist' + ) + subsonicplaylist_cmds.parser.add_option( + '-d', + '--delete', + action='store_true', + help='delete tag from items not in any playlist anymore', + ) + subsonicplaylist_cmds.func = build_playlist + return [subsonicplaylist_cmds] + + def generate_token(self): + salt = ''.join(random.choices(string.ascii_lowercase + string.digits)) + return md5( + (self.config['password'].get() + salt).encode()).hexdigest(), salt + + def send(self, endpoint, params=None): + if params is None: + params = {} + a, b = self.generate_token() + params['u'] = self.config['username'] + params['t'] = a + params['s'] = b + params['v'] = '1.12.0' + params['c'] = 'beets' + resp = requests.get('{}/rest/{}?{}'.format( + self.config['base_url'].get(), + endpoint, + urlencode(params)) + ) + return resp + + def get_playlists(self, ids): + output = {} + for playlist_id in ids: + name, tracks = self.get_playlist(playlist_id) + for track in tracks: + if track not in output: + output[track] = ';' + output[track] += name + ';' + return output diff --git a/libs/common/beetsplug/subsonicupdate.py b/libs/common/beetsplug/subsonicupdate.py new file mode 100644 index 00000000..9480bcb4 --- /dev/null +++ b/libs/common/beetsplug/subsonicupdate.py @@ -0,0 +1,144 @@ +# This file is part of beets. +# Copyright 2016, Adrian Sampson. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Updates Subsonic library on Beets import +Your Beets configuration file should contain +a "subsonic" section like the following: + subsonic: + url: https://mydomain.com:443/subsonic + user: username + pass: password + auth: token +For older Subsonic versions, token authentication +is not supported, use password instead: + subsonic: + url: https://mydomain.com:443/subsonic + user: username + pass: password + auth: pass +""" + +import hashlib +import random +import string + +import requests + +from binascii import hexlify +from beets import config +from beets.plugins import BeetsPlugin + +__author__ = 'https://github.com/maffo999' + + +class SubsonicUpdate(BeetsPlugin): + def __init__(self): + super().__init__() + # Set default configuration values + config['subsonic'].add({ + 'user': 'admin', + 'pass': 'admin', + 'url': 'http://localhost:4040', + 'auth': 'token', + }) + config['subsonic']['pass'].redact = True + self.register_listener('import', self.start_scan) + + @staticmethod + def __create_token(): + """Create salt and token from given password. + + :return: The generated salt and hashed token + """ + password = config['subsonic']['pass'].as_str() + + # Pick the random sequence and salt the password + r = string.ascii_letters + string.digits + salt = "".join([random.choice(r) for _ in range(6)]) + salted_password = password + salt + token = hashlib.md5(salted_password.encode('utf-8')).hexdigest() + + # Put together the payload of the request to the server and the URL + return salt, token + + @staticmethod + def __format_url(endpoint): + """Get the Subsonic URL to trigger the given endpoint. + Uses either the url config option or the deprecated host, port, + and context_path config options together. + + :return: Endpoint for updating Subsonic + """ + + url = config['subsonic']['url'].as_str() + if url and url.endswith('/'): + url = url[:-1] + + # @deprecated("Use url config option instead") + if not url: + host = config['subsonic']['host'].as_str() + port = config['subsonic']['port'].get(int) + context_path = config['subsonic']['contextpath'].as_str() + if context_path == '/': + context_path = '' + url = f"http://{host}:{port}{context_path}" + + return url + f'/rest/{endpoint}' + + def start_scan(self): + user = config['subsonic']['user'].as_str() + auth = config['subsonic']['auth'].as_str() + url = self.__format_url("startScan") + self._log.debug('URL is {0}', url) + self._log.debug('auth type is {0}', config['subsonic']['auth']) + + if auth == "token": + salt, token = self.__create_token() + payload = { + 'u': user, + 't': token, + 's': salt, + 'v': '1.13.0', # Subsonic 5.3 and newer + 'c': 'beets', + 'f': 'json' + } + elif auth == "password": + password = config['subsonic']['pass'].as_str() + encpass = hexlify(password.encode()).decode() + payload = { + 'u': user, + 'p': f'enc:{encpass}', + 'v': '1.12.0', + 'c': 'beets', + 'f': 'json' + } + else: + return + try: + response = requests.get(url, params=payload) + json = response.json() + + if response.status_code == 200 and \ + json['subsonic-response']['status'] == "ok": + count = json['subsonic-response']['scanStatus']['count'] + self._log.info( + f'Updating Subsonic; scanning {count} tracks') + elif response.status_code == 200 and \ + json['subsonic-response']['status'] == "failed": + error_message = json['subsonic-response']['error']['message'] + self._log.error(f'Error: {error_message}') + else: + self._log.error('Error: {0}', json) + except Exception as error: + self._log.error(f'Error: {error}') diff --git a/libs/common/beetsplug/the.py b/libs/common/beetsplug/the.py index cfb583ce..e6626d2b 100644 --- a/libs/common/beetsplug/the.py +++ b/libs/common/beetsplug/the.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Blemjhoo Tezoulbr . # @@ -15,7 +14,6 @@ """Moves patterns in path formats (suitable for moving articles).""" -from __future__ import division, absolute_import, print_function import re from beets.plugins import BeetsPlugin @@ -23,9 +21,9 @@ from beets.plugins import BeetsPlugin __author__ = 'baobab@heresiarch.info' __version__ = '1.1' -PATTERN_THE = u'^[the]{3}\s' -PATTERN_A = u'^[a][n]?\s' -FORMAT = u'{0}, {1}' +PATTERN_THE = '^the\\s' +PATTERN_A = '^[a][n]?\\s' +FORMAT = '{0}, {1}' class ThePlugin(BeetsPlugin): @@ -33,14 +31,14 @@ class ThePlugin(BeetsPlugin): patterns = [] def __init__(self): - super(ThePlugin, self).__init__() + super().__init__() self.template_funcs['the'] = self.the_template_func self.config.add({ 'the': True, 'a': True, - 'format': u'{0}, {1}', + 'format': '{0}, {1}', 'strip': False, 'patterns': [], }) @@ -51,17 +49,17 @@ class ThePlugin(BeetsPlugin): try: re.compile(p) except re.error: - self._log.error(u'invalid pattern: {0}', p) + self._log.error('invalid pattern: {0}', p) else: if not (p.startswith('^') or p.endswith('$')): - self._log.warning(u'warning: \"{0}\" will not ' - u'match string start/end', p) + self._log.warning('warning: \"{0}\" will not ' + 'match string start/end', p) if self.config['a']: self.patterns = [PATTERN_A] + self.patterns if self.config['the']: self.patterns = [PATTERN_THE] + self.patterns if not self.patterns: - self._log.warning(u'no patterns defined!') + self._log.warning('no patterns defined!') def unthe(self, text, pattern): """Moves pattern in the path format string or strips it @@ -84,7 +82,7 @@ class ThePlugin(BeetsPlugin): fmt = self.config['format'].as_str() return fmt.format(r, t.strip()).strip() else: - return u'' + return '' def the_template_func(self, text): if not self.patterns: @@ -93,8 +91,8 @@ class ThePlugin(BeetsPlugin): for p in self.patterns: r = self.unthe(text, p) if r != text: + self._log.debug('\"{0}\" -> \"{1}\"', text, r) break - self._log.debug(u'\"{0}\" -> \"{1}\"', text, r) return r else: - return u'' + return '' diff --git a/libs/common/beetsplug/thumbnails.py b/libs/common/beetsplug/thumbnails.py index 04845e88..6bd9cbac 100644 --- a/libs/common/beetsplug/thumbnails.py +++ b/libs/common/beetsplug/thumbnails.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Bruno Cauet # @@ -19,7 +18,6 @@ This plugin is POSIX-only. Spec: standards.freedesktop.org/thumbnail-spec/latest/index.html """ -from __future__ import division, absolute_import, print_function from hashlib import md5 import os @@ -35,7 +33,6 @@ from beets.plugins import BeetsPlugin from beets.ui import Subcommand, decargs from beets import util from beets.util.artresizer import ArtResizer, get_im_version, get_pil_version -import six BASE_DIR = os.path.join(BaseDirectory.xdg_cache_home, "thumbnails") @@ -45,7 +42,7 @@ LARGE_DIR = util.bytestring_path(os.path.join(BASE_DIR, "large")) class ThumbnailsPlugin(BeetsPlugin): def __init__(self): - super(ThumbnailsPlugin, self).__init__() + super().__init__() self.config.add({ 'auto': True, 'force': False, @@ -58,15 +55,15 @@ class ThumbnailsPlugin(BeetsPlugin): def commands(self): thumbnails_command = Subcommand("thumbnails", - help=u"Create album thumbnails") + help="Create album thumbnails") thumbnails_command.parser.add_option( - u'-f', u'--force', + '-f', '--force', dest='force', action='store_true', default=False, - help=u'force regeneration of thumbnails deemed fine (existing & ' - u'recent enough)') + help='force regeneration of thumbnails deemed fine (existing & ' + 'recent enough)') thumbnails_command.parser.add_option( - u'--dolphin', dest='dolphin', action='store_true', default=False, - help=u"create Dolphin-compatible thumbnail information (for KDE)") + '--dolphin', dest='dolphin', action='store_true', default=False, + help="create Dolphin-compatible thumbnail information (for KDE)") thumbnails_command.func = self.process_query return [thumbnails_command] @@ -85,8 +82,8 @@ class ThumbnailsPlugin(BeetsPlugin): - detect whether we'll use GIO or Python to get URIs """ if not ArtResizer.shared.local: - self._log.warning(u"No local image resizing capabilities, " - u"cannot generate thumbnails") + self._log.warning("No local image resizing capabilities, " + "cannot generate thumbnails") return False for dir in (NORMAL_DIR, LARGE_DIR): @@ -100,12 +97,12 @@ class ThumbnailsPlugin(BeetsPlugin): assert get_pil_version() # since we're local self.write_metadata = write_metadata_pil tool = "PIL" - self._log.debug(u"using {0} to write metadata", tool) + self._log.debug("using {0} to write metadata", tool) uri_getter = GioURI() if not uri_getter.available: uri_getter = PathlibURI() - self._log.debug(u"using {0.name} to compute URIs", uri_getter) + self._log.debug("using {0.name} to compute URIs", uri_getter) self.get_uri = uri_getter.uri return True @@ -113,9 +110,9 @@ class ThumbnailsPlugin(BeetsPlugin): def process_album(self, album): """Produce thumbnails for the album folder. """ - self._log.debug(u'generating thumbnail for {0}', album) + self._log.debug('generating thumbnail for {0}', album) if not album.artpath: - self._log.info(u'album {0} has no art', album) + self._log.info('album {0} has no art', album) return if self.config['dolphin']: @@ -123,7 +120,7 @@ class ThumbnailsPlugin(BeetsPlugin): size = ArtResizer.shared.get_size(album.artpath) if not size: - self._log.warning(u'problem getting the picture size for {0}', + self._log.warning('problem getting the picture size for {0}', album.artpath) return @@ -133,9 +130,9 @@ class ThumbnailsPlugin(BeetsPlugin): wrote &= self.make_cover_thumbnail(album, 128, NORMAL_DIR) if wrote: - self._log.info(u'wrote thumbnail for {0}', album) + self._log.info('wrote thumbnail for {0}', album) else: - self._log.info(u'nothing to do for {0}', album) + self._log.info('nothing to do for {0}', album) def make_cover_thumbnail(self, album, size, target_dir): """Make a thumbnail of given size for `album` and put it in @@ -146,11 +143,11 @@ class ThumbnailsPlugin(BeetsPlugin): if os.path.exists(target) and \ os.stat(target).st_mtime > os.stat(album.artpath).st_mtime: if self.config['force']: - self._log.debug(u"found a suitable {1}x{1} thumbnail for {0}, " - u"forcing regeneration", album, size) + self._log.debug("found a suitable {1}x{1} thumbnail for {0}, " + "forcing regeneration", album, size) else: - self._log.debug(u"{1}x{1} thumbnail for {0} exists and is " - u"recent enough", album, size) + self._log.debug("{1}x{1} thumbnail for {0} exists and is " + "recent enough", album, size) return False resized = ArtResizer.shared.resize(size, album.artpath, util.syspath(target)) @@ -160,23 +157,23 @@ class ThumbnailsPlugin(BeetsPlugin): def thumbnail_file_name(self, path): """Compute the thumbnail file name - See http://standards.freedesktop.org/thumbnail-spec/latest/x227.html + See https://standards.freedesktop.org/thumbnail-spec/latest/x227.html """ uri = self.get_uri(path) hash = md5(uri.encode('utf-8')).hexdigest() - return util.bytestring_path("{0}.png".format(hash)) + return util.bytestring_path(f"{hash}.png") def add_tags(self, album, image_path): """Write required metadata to the thumbnail - See http://standards.freedesktop.org/thumbnail-spec/latest/x142.html + See https://standards.freedesktop.org/thumbnail-spec/latest/x142.html """ mtime = os.stat(album.artpath).st_mtime metadata = {"Thumb::URI": self.get_uri(album.artpath), - "Thumb::MTime": six.text_type(mtime)} + "Thumb::MTime": str(mtime)} try: self.write_metadata(image_path, metadata) except Exception: - self._log.exception(u"could not write metadata to {0}", + self._log.exception("could not write metadata to {0}", util.displayable_path(image_path)) def make_dolphin_cover_thumbnail(self, album): @@ -186,9 +183,9 @@ class ThumbnailsPlugin(BeetsPlugin): artfile = os.path.split(album.artpath)[1] with open(outfilename, 'w') as f: f.write('[Desktop Entry]\n') - f.write('Icon=./{0}'.format(artfile.decode('utf-8'))) + f.write('Icon=./{}'.format(artfile.decode('utf-8'))) f.close() - self._log.debug(u"Wrote file {0}", util.displayable_path(outfilename)) + self._log.debug("Wrote file {0}", util.displayable_path(outfilename)) def write_metadata_im(file, metadata): @@ -211,7 +208,7 @@ def write_metadata_pil(file, metadata): return True -class URIGetter(object): +class URIGetter: available = False name = "Abstract base" @@ -224,7 +221,7 @@ class PathlibURI(URIGetter): name = "Python Pathlib" def uri(self, path): - return PurePosixPath(path).as_uri() + return PurePosixPath(util.py3_path(path)).as_uri() def copy_c_string(c_string): @@ -269,7 +266,7 @@ class GioURI(URIGetter): def uri(self, path): g_file_ptr = self.libgio.g_file_new_for_path(path) if not g_file_ptr: - raise RuntimeError(u"No gfile pointer received for {0}".format( + raise RuntimeError("No gfile pointer received for {}".format( util.displayable_path(path))) try: @@ -278,8 +275,8 @@ class GioURI(URIGetter): self.libgio.g_object_unref(g_file_ptr) if not uri_ptr: self.libgio.g_free(uri_ptr) - raise RuntimeError(u"No URI received from the gfile pointer for " - u"{0}".format(util.displayable_path(path))) + raise RuntimeError("No URI received from the gfile pointer for " + "{}".format(util.displayable_path(path))) try: uri = copy_c_string(uri_ptr) @@ -290,5 +287,5 @@ class GioURI(URIGetter): return uri.decode(util._fsencoding()) except UnicodeDecodeError: raise RuntimeError( - "Could not decode filename from GIO: {!r}".format(uri) + f"Could not decode filename from GIO: {uri!r}" ) diff --git a/libs/common/beetsplug/types.py b/libs/common/beetsplug/types.py index 0c078881..930d5e86 100644 --- a/libs/common/beetsplug/types.py +++ b/libs/common/beetsplug/types.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Thomas Scholtes. # @@ -13,11 +12,10 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets.dbcore import types -from beets.util.confit import ConfigValueError +from confuse import ConfigValueError from beets import library @@ -47,6 +45,6 @@ class TypesPlugin(BeetsPlugin): mytypes[key] = library.DateType() else: raise ConfigValueError( - u"unknown type '{0}' for the '{1}' field" + "unknown type '{}' for the '{}' field" .format(value, key)) return mytypes diff --git a/libs/common/beetsplug/unimported.py b/libs/common/beetsplug/unimported.py new file mode 100644 index 00000000..7714ec83 --- /dev/null +++ b/libs/common/beetsplug/unimported.py @@ -0,0 +1,68 @@ +# This file is part of beets. +# Copyright 2019, Joris Jensen +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +""" +List all files in the library folder which are not listed in the + beets library database, including art files +""" + +import os + +from beets import util +from beets.plugins import BeetsPlugin +from beets.ui import Subcommand, print_ + +__author__ = 'https://github.com/MrNuggelz' + + +class Unimported(BeetsPlugin): + + def __init__(self): + super().__init__() + self.config.add( + { + 'ignore_extensions': [] + } + ) + + def commands(self): + def print_unimported(lib, opts, args): + ignore_exts = [ + ('.' + x).encode() + for x in self.config["ignore_extensions"].as_str_seq() + ] + ignore_dirs = [ + os.path.join(lib.directory, x.encode()) + for x in self.config["ignore_subdirectories"].as_str_seq() + ] + in_folder = { + os.path.join(r, file) + for r, d, f in os.walk(lib.directory) + for file in f + if not any( + [file.endswith(ext) for ext in ignore_exts] + + [r in ignore_dirs] + ) + } + in_library = {x.path for x in lib.items()} + art_files = {x.artpath for x in lib.albums()} + for f in in_folder - in_library - art_files: + print_(util.displayable_path(f)) + + unimported = Subcommand( + 'unimported', + help='list all files in the library folder which are not listed' + ' in the beets library database') + unimported.func = print_unimported + return [unimported] diff --git a/libs/common/beetsplug/web/__init__.py b/libs/common/beetsplug/web/__init__.py index 3cf43ed5..240126e9 100644 --- a/libs/common/beetsplug/web/__init__.py +++ b/libs/common/beetsplug/web/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Adrian Sampson. # @@ -14,14 +13,13 @@ # included in all copies or substantial portions of the Software. """A Web interface to beets.""" -from __future__ import division, absolute_import, print_function from beets.plugins import BeetsPlugin from beets import ui from beets import util import beets.library import flask -from flask import g +from flask import g, jsonify from werkzeug.routing import BaseConverter, PathConverter import os from unidecode import unidecode @@ -59,7 +57,10 @@ def _rep(obj, expand=False): return out elif isinstance(obj, beets.library.Album): - del out['artpath'] + if app.config.get('INCLUDE_PATHS', False): + out['artpath'] = util.displayable_path(out['artpath']) + else: + del out['artpath'] if expand: out['items'] = [_rep(item) for item in obj.items()] return out @@ -91,7 +92,20 @@ def is_expand(): return flask.request.args.get('expand') is not None -def resource(name): +def is_delete(): + """Returns whether the current delete request should remove the selected + files. + """ + + return flask.request.args.get('delete') is not None + + +def get_method(): + """Returns the HTTP method of the current request.""" + return flask.request.method + + +def resource(name, patchable=False): """Decorates a function to handle RESTful HTTP requests for a resource. """ def make_responder(retriever): @@ -99,34 +113,98 @@ def resource(name): entities = [retriever(id) for id in ids] entities = [entity for entity in entities if entity] - if len(entities) == 1: - return flask.jsonify(_rep(entities[0], expand=is_expand())) - elif entities: - return app.response_class( - json_generator(entities, root=name), - mimetype='application/json' - ) + if get_method() == "DELETE": + + if app.config.get('READONLY', True): + return flask.abort(405) + + for entity in entities: + entity.remove(delete=is_delete()) + + return flask.make_response(jsonify({'deleted': True}), 200) + + elif get_method() == "PATCH" and patchable: + if app.config.get('READONLY', True): + return flask.abort(405) + + for entity in entities: + entity.update(flask.request.get_json()) + entity.try_sync(True, False) # write, don't move + + if len(entities) == 1: + return flask.jsonify(_rep(entities[0], expand=is_expand())) + elif entities: + return app.response_class( + json_generator(entities, root=name), + mimetype='application/json' + ) + + elif get_method() == "GET": + if len(entities) == 1: + return flask.jsonify(_rep(entities[0], expand=is_expand())) + elif entities: + return app.response_class( + json_generator(entities, root=name), + mimetype='application/json' + ) + else: + return flask.abort(404) + else: - return flask.abort(404) - responder.__name__ = 'get_{0}'.format(name) + return flask.abort(405) + + responder.__name__ = f'get_{name}' + return responder return make_responder -def resource_query(name): +def resource_query(name, patchable=False): """Decorates a function to handle RESTful HTTP queries for resources. """ def make_responder(query_func): def responder(queries): - return app.response_class( - json_generator( - query_func(queries), - root='results', expand=is_expand() - ), - mimetype='application/json' - ) - responder.__name__ = 'query_{0}'.format(name) + entities = query_func(queries) + + if get_method() == "DELETE": + + if app.config.get('READONLY', True): + return flask.abort(405) + + for entity in entities: + entity.remove(delete=is_delete()) + + return flask.make_response(jsonify({'deleted': True}), 200) + + elif get_method() == "PATCH" and patchable: + if app.config.get('READONLY', True): + return flask.abort(405) + + for entity in entities: + entity.update(flask.request.get_json()) + entity.try_sync(True, False) # write, don't move + + return app.response_class( + json_generator(entities, root=name), + mimetype='application/json' + ) + + elif get_method() == "GET": + return app.response_class( + json_generator( + entities, + root='results', expand=is_expand() + ), + mimetype='application/json' + ) + + else: + return flask.abort(405) + + responder.__name__ = f'query_{name}' + return responder + return make_responder @@ -140,7 +218,7 @@ def resource_list(name): json_generator(list_all(), root=name, expand=is_expand()), mimetype='application/json' ) - responder.__name__ = 'all_{0}'.format(name) + responder.__name__ = f'all_{name}' return responder return make_responder @@ -150,7 +228,7 @@ def _get_unique_table_field_values(model, field, sort_field): if field not in model.all_keys() or sort_field not in model.all_keys(): raise KeyError with g.lib.transaction() as tx: - rows = tx.query('SELECT DISTINCT "{0}" FROM "{1}" ORDER BY "{2}"' + rows = tx.query('SELECT DISTINCT "{}" FROM "{}" ORDER BY "{}"' .format(field, model._table, sort_field)) return [row[0] for row in rows] @@ -169,7 +247,7 @@ class IdListConverter(BaseConverter): return ids def to_url(self, value): - return ','.join(value) + return ','.join(str(v) for v in value) class QueryConverter(PathConverter): @@ -177,10 +255,13 @@ class QueryConverter(PathConverter): """ def to_python(self, value): - return value.split('/') + queries = value.split('/') + """Do not do path substitution on regex value tests""" + return [query if '::' in query else query.replace('\\', os.sep) + for query in queries] def to_url(self, value): - return ','.join(value) + return ','.join([v.replace(os.sep, '\\') for v in value]) class EverythingConverter(PathConverter): @@ -202,8 +283,8 @@ def before_request(): # Items. -@app.route('/item/') -@resource('items') +@app.route('/item/', methods=["GET", "DELETE", "PATCH"]) +@resource('items', patchable=True) def get_item(id): return g.lib.get_item(id) @@ -249,8 +330,8 @@ def item_file(item_id): return response -@app.route('/item/query/') -@resource_query('items') +@app.route('/item/query/', methods=["GET", "DELETE", "PATCH"]) +@resource_query('items', patchable=True) def item_query(queries): return g.lib.items(queries) @@ -278,7 +359,7 @@ def item_unique_field_values(key): # Albums. -@app.route('/album/') +@app.route('/album/', methods=["GET", "DELETE"]) @resource('albums') def get_album(id): return g.lib.get_album(id) @@ -291,7 +372,7 @@ def all_albums(): return g.lib.albums() -@app.route('/album/query/') +@app.route('/album/query/', methods=["GET", "DELETE"]) @resource_query('albums') def album_query(queries): return g.lib.albums(queries) @@ -351,20 +432,21 @@ def home(): class WebPlugin(BeetsPlugin): def __init__(self): - super(WebPlugin, self).__init__() + super().__init__() self.config.add({ - 'host': u'127.0.0.1', + 'host': '127.0.0.1', 'port': 8337, 'cors': '', 'cors_supports_credentials': False, 'reverse_proxy': False, 'include_paths': False, + 'readonly': True, }) def commands(self): - cmd = ui.Subcommand('web', help=u'start a Web interface') - cmd.parser.add_option(u'-d', u'--debug', action='store_true', - default=False, help=u'debug mode') + cmd = ui.Subcommand('web', help='start a Web interface') + cmd.parser.add_option('-d', '--debug', action='store_true', + default=False, help='debug mode') def func(lib, opts, args): args = ui.decargs(args) @@ -378,12 +460,13 @@ class WebPlugin(BeetsPlugin): app.config['JSONIFY_PRETTYPRINT_REGULAR'] = False app.config['INCLUDE_PATHS'] = self.config['include_paths'] + app.config['READONLY'] = self.config['readonly'] # Enable CORS if required. if self.config['cors']: - self._log.info(u'Enabling CORS with origin: {0}', + self._log.info('Enabling CORS with origin: {0}', self.config['cors']) - from flask.ext.cors import CORS + from flask_cors import CORS app.config['CORS_ALLOW_HEADERS'] = "Content-Type" app.config['CORS_RESOURCES'] = { r"/*": {"origins": self.config['cors'].get(str)} @@ -407,7 +490,7 @@ class WebPlugin(BeetsPlugin): return [cmd] -class ReverseProxied(object): +class ReverseProxied: '''Wrap the application in this middleware and configure the front-end server to add these headers, to let you quietly bind this to a URL other than / and to an HTTP scheme that is diff --git a/libs/common/beetsplug/web/static/beets.js b/libs/common/beetsplug/web/static/beets.js index 51985c18..97af7011 100644 --- a/libs/common/beetsplug/web/static/beets.js +++ b/libs/common/beetsplug/web/static/beets.js @@ -129,7 +129,7 @@ $.fn.player = function(debug) { // Simple selection disable for jQuery. // Cut-and-paste from: -// http://stackoverflow.com/questions/2700000 +// https://stackoverflow.com/questions/2700000 $.fn.disableSelection = function() { $(this).attr('unselectable', 'on') .css('-moz-user-select', 'none') diff --git a/libs/common/beetsplug/zero.py b/libs/common/beetsplug/zero.py index 022c2c72..f05b1b5a 100644 --- a/libs/common/beetsplug/zero.py +++ b/libs/common/beetsplug/zero.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # This file is part of beets. # Copyright 2016, Blemjhoo Tezoulbr . # @@ -15,23 +14,21 @@ """ Clears tag fields in media files.""" -from __future__ import division, absolute_import, print_function -import six import re from beets.plugins import BeetsPlugin -from beets.mediafile import MediaFile +from mediafile import MediaFile from beets.importer import action from beets.ui import Subcommand, decargs, input_yn -from beets.util import confit +import confuse __author__ = 'baobab@heresiarch.info' class ZeroPlugin(BeetsPlugin): def __init__(self): - super(ZeroPlugin, self).__init__() + super().__init__() self.register_listener('write', self.write_event) self.register_listener('import_task_choice', @@ -56,7 +53,7 @@ class ZeroPlugin(BeetsPlugin): """ if self.config['fields'] and self.config['keep_fields']: self._log.warning( - u'cannot blacklist and whitelist at the same time' + 'cannot blacklist and whitelist at the same time' ) # Blacklist mode. elif self.config['fields']: @@ -75,7 +72,7 @@ class ZeroPlugin(BeetsPlugin): def zero_fields(lib, opts, args): if not decargs(args) and not input_yn( - u"Remove fields for all items? (Y/n)", + "Remove fields for all items? (Y/n)", True): return for item in lib.items(decargs(args)): @@ -89,22 +86,22 @@ class ZeroPlugin(BeetsPlugin): Do some sanity checks then compile the regexes. """ if field not in MediaFile.fields(): - self._log.error(u'invalid field: {0}', field) + self._log.error('invalid field: {0}', field) elif field in ('id', 'path', 'album_id'): - self._log.warning(u'field \'{0}\' ignored, zeroing ' - u'it would be dangerous', field) + self._log.warning('field \'{0}\' ignored, zeroing ' + 'it would be dangerous', field) else: try: for pattern in self.config[field].as_str_seq(): prog = re.compile(pattern, re.IGNORECASE) self.fields_to_progs.setdefault(field, []).append(prog) - except confit.NotFoundError: + except confuse.NotFoundError: # Matches everything self.fields_to_progs[field] = [] def import_task_choice_event(self, session, task): if task.choice_flag == action.ASIS and not self.warned: - self._log.warning(u'cannot zero in \"as-is\" mode') + self._log.warning('cannot zero in \"as-is\" mode') self.warned = True # TODO request write in as-is mode @@ -122,7 +119,7 @@ class ZeroPlugin(BeetsPlugin): fields_set = False if not self.fields_to_progs: - self._log.warning(u'no fields, nothing to do') + self._log.warning('no fields, nothing to do') return False for field, progs in self.fields_to_progs.items(): @@ -135,7 +132,7 @@ class ZeroPlugin(BeetsPlugin): if match: fields_set = True - self._log.debug(u'{0}: {1} -> None', field, value) + self._log.debug('{0}: {1} -> None', field, value) tags[field] = None if self.config['update_database']: item[field] = None @@ -158,6 +155,6 @@ def _match_progs(value, progs): if not progs: return True for prog in progs: - if prog.search(six.text_type(value)): + if prog.search(str(value)): return True return False diff --git a/libs/common/bin/beet.exe b/libs/common/bin/beet.exe index e91e175e2c7ba0e2a22ffbaa812e89f6220437db..742e1c19ef89536fbe6732bc8353ecc3f8cf05fe 100644 GIT binary patch literal 108369 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 literal 93012 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 diff --git a/libs/common/bin/chardetect.exe b/libs/common/bin/chardetect.exe deleted file mode 100644 index 17242a806b0c0f64941749f6dd2160db387ba30a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 93026 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 diff --git a/libs/common/bin/easy_install-3.7.exe b/libs/common/bin/easy_install-3.7.exe deleted file mode 100644 index ba897f334d14c187286e16a1ab2e31b03abf6bc2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 93035 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 diff --git a/libs/common/bin/mid3cp b/libs/common/bin/mid3cp deleted file mode 100644 index 8a773e56..00000000 --- a/libs/common/bin/mid3cp +++ /dev/null @@ -1,16 +0,0 @@ -#!C:\Python\3.7\python.exe -# -*- coding: utf-8 -*- -# Copyright 2016 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import sys - -from mutagen._tools.mid3cp import entry_point - - -if __name__ == "__main__": - sys.exit(entry_point()) diff --git a/libs/common/bin/mid3cp.exe b/libs/common/bin/mid3cp.exe new file mode 100644 index 0000000000000000000000000000000000000000..e3235f666537a7d7e5566c268855e0cd38250d01 GIT binary patch literal 108396 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 Kbz?1E;eP=1(=mbo literal 0 HcmV?d00001 diff --git a/libs/common/bin/mid3iconv b/libs/common/bin/mid3iconv deleted file mode 100644 index 332f6b70..00000000 --- a/libs/common/bin/mid3iconv +++ /dev/null @@ -1,16 +0,0 @@ -#!C:\Python\3.7\python.exe -# -*- coding: utf-8 -*- -# Copyright 2016 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import sys - -from mutagen._tools.mid3iconv import entry_point - - -if __name__ == "__main__": - sys.exit(entry_point()) diff --git a/libs/common/bin/mid3iconv.exe b/libs/common/bin/mid3iconv.exe new file mode 100644 index 0000000000000000000000000000000000000000..c67cd5b2ffcd56080b2d304930d4860d2db0fa65 GIT binary patch literal 108399 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 N7CL6#P@$Tx@IMx6GVK5W literal 0 HcmV?d00001 diff --git a/libs/common/bin/mid3v2 b/libs/common/bin/mid3v2 deleted file mode 100644 index 1bf2d13d..00000000 --- a/libs/common/bin/mid3v2 +++ /dev/null @@ -1,16 +0,0 @@ -#!C:\Python\3.7\python.exe -# -*- coding: utf-8 -*- -# Copyright 2016 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import sys - -from mutagen._tools.mid3v2 import entry_point - - -if __name__ == "__main__": - sys.exit(entry_point()) diff --git a/libs/common/bin/mid3v2.exe b/libs/common/bin/mid3v2.exe new file mode 100644 index 0000000000000000000000000000000000000000..a7c245b47ecf0b38a8d0c50a827a04ec7b86f0c0 GIT binary patch literal 108396 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 Kbz?1E;eP;!Vlept literal 0 HcmV?d00001 diff --git a/libs/common/bin/moggsplit b/libs/common/bin/moggsplit deleted file mode 100644 index f43d1360..00000000 --- a/libs/common/bin/moggsplit +++ /dev/null @@ -1,16 +0,0 @@ -#!C:\Python\3.7\python.exe -# -*- coding: utf-8 -*- -# Copyright 2016 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import sys - -from mutagen._tools.moggsplit import entry_point - - -if __name__ == "__main__": - sys.exit(entry_point()) diff --git a/libs/common/bin/moggsplit.exe b/libs/common/bin/moggsplit.exe new file mode 100644 index 0000000000000000000000000000000000000000..ecf55502106dffc9bea7444648e2d04713db9450 GIT binary patch literal 108399 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 Owa_u^h6>emh5rH1Xf-JS literal 0 HcmV?d00001 diff --git a/libs/common/bin/mutagen-inspect b/libs/common/bin/mutagen-inspect deleted file mode 100644 index 746b414e..00000000 --- a/libs/common/bin/mutagen-inspect +++ /dev/null @@ -1,16 +0,0 @@ -#!C:\Python\3.7\python.exe -# -*- coding: utf-8 -*- -# Copyright 2016 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import sys - -from mutagen._tools.mutagen_inspect import entry_point - - -if __name__ == "__main__": - sys.exit(entry_point()) diff --git a/libs/common/bin/mutagen-inspect.exe b/libs/common/bin/mutagen-inspect.exe new file mode 100644 index 0000000000000000000000000000000000000000..e41e38b85f71417c7f33c475e3815ed5e9be8387 GIT binary patch literal 108405 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 Nh3;85RH>ya{11%xH$eaZ literal 0 HcmV?d00001 diff --git a/libs/common/bin/mutagen-pony b/libs/common/bin/mutagen-pony deleted file mode 100644 index a289a988..00000000 --- a/libs/common/bin/mutagen-pony +++ /dev/null @@ -1,16 +0,0 @@ -#!C:\Python\3.7\python.exe -# -*- coding: utf-8 -*- -# Copyright 2016 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import sys - -from mutagen._tools.mutagen_pony import entry_point - - -if __name__ == "__main__": - sys.exit(entry_point()) diff --git a/libs/common/bin/mutagen-pony.exe b/libs/common/bin/mutagen-pony.exe new file mode 100644 index 0000000000000000000000000000000000000000..efff7a2b6ca4ba719d2ce8511f3d663a5265514d GIT binary patch literal 108402 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 literal 0 HcmV?d00001 diff --git a/libs/common/bin/pbr.exe b/libs/common/bin/pbr.exe deleted file mode 100644 index e7eab92cf2cc657d9c4770725f485ca0a00e1287..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 93016 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 diff --git a/libs/common/bin/srt.exe b/libs/common/bin/srt.exe deleted file mode 100644 index 90e6494dc3f873c9cd9407c4e8b8e511a743d47d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 93018 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 diff --git a/libs/common/bin/subliminal.exe b/libs/common/bin/subliminal.exe deleted file mode 100644 index 280ce315cf27e10110fc508884a54196c8cdf645..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 93030 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

=l z*8PZ?4@_E35785JIU8rqu%56^fj$v_yM3Gb(D|jlP3QQva5cm$gkmIG3mo5w-^5$N z*KEhT@C1@UW|D=lerHIHdC&}@QFJ)XqpRo{`T|?Q(!guX64T^dxmdpB9QL=hB@nCF zGZ+=3r_g%ThxS;9ZxgqJo;CRJvBFP=(x8y$J_mLma{q!^XCFmQ(wrIbU&oT^v;F_*-Q`}T{qMPAg zj}w;ykDSzz`w<8dNEAYlqFlo%#b#AuN!(!@mY zIvFBMy`W2i|7M zTv;HCWSR8I*|JJj%XzX^c7RQXfUd;Zsdl=ZVQ0afq5#yq-TB7paE>@3y0;F4-9)6m zNqcpSjsqXgwbH|MiXN>~b()^2({-8l>DjtUSL=DYR?pXUy57&au16OFT?lj`(1k!3 z0{?diL=Vc58D(>P1*Ih!B+$a&>=&;kcSphqe|~gU;a?;KNDJ5Tvl4{^OombIjWdb5gM9bURvy} zm^rPeu(+@!yC^ZIsL*@)lRupe4K2*~W@eT^64cC0uj6=Quk;ry;U_Y$7n(}UE}vdG z72WN1po>JP8&@71H?$(Bys*rd0mGW*R%Q6767R;uCU|4vc}=DGtF7cgD&@GJdX9^a rhnWXm7aoY>24B(s!`uU)z)Q7%J9I4)X_xbn1i`*bz$JgcFpuY7_#z;> diff --git a/libs/common/bin/unidecode.exe b/libs/common/bin/unidecode.exe index 0880f1b8075589f2dd9616e0bf5b16aad99d0bd1..7fd3c5dc46cc4032af72aaa40899ddb07a9540ac 100644 GIT binary patch literal 108375 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 diff --git a/libs/common/colorama/__init__.py b/libs/common/colorama/__init__.py index 2a3bf471..383101cd 100644 --- a/libs/common/colorama/__init__.py +++ b/libs/common/colorama/__init__.py @@ -1,6 +1,7 @@ # Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. -from .initialise import init, deinit, reinit, colorama_text +from .initialise import init, deinit, reinit, colorama_text, just_fix_windows_console from .ansi import Fore, Back, Style, Cursor from .ansitowin32 import AnsiToWin32 -__version__ = '0.4.1' +__version__ = '0.4.6' + diff --git a/libs/common/colorama/ansi.py b/libs/common/colorama/ansi.py index 78776588..11ec695f 100644 --- a/libs/common/colorama/ansi.py +++ b/libs/common/colorama/ansi.py @@ -6,7 +6,7 @@ See: http://en.wikipedia.org/wiki/ANSI_escape_code CSI = '\033[' OSC = '\033]' -BEL = '\007' +BEL = '\a' def code_to_chars(code): diff --git a/libs/common/colorama/ansitowin32.py b/libs/common/colorama/ansitowin32.py index 359c92be..abf209e6 100644 --- a/libs/common/colorama/ansitowin32.py +++ b/libs/common/colorama/ansitowin32.py @@ -3,8 +3,8 @@ import re import sys import os -from .ansi import AnsiFore, AnsiBack, AnsiStyle, Style -from .winterm import WinTerm, WinColor, WinStyle +from .ansi import AnsiFore, AnsiBack, AnsiStyle, Style, BEL +from .winterm import enable_vt_processing, WinTerm, WinColor, WinStyle from .win32 import windll, winapi_test @@ -37,6 +37,12 @@ class StreamWrapper(object): def __exit__(self, *args, **kwargs): return self.__wrapped.__exit__(*args, **kwargs) + def __setstate__(self, state): + self.__dict__ = state + + def __getstate__(self): + return self.__dict__ + def write(self, text): self.__convertor.write(text) @@ -57,7 +63,9 @@ class StreamWrapper(object): stream = self.__wrapped try: return stream.closed - except AttributeError: + # AttributeError in the case that the stream doesn't support being closed + # ValueError for the case that the stream has already been detached when atexit runs + except (AttributeError, ValueError): return True @@ -68,7 +76,7 @@ class AnsiToWin32(object): win32 function calls. ''' ANSI_CSI_RE = re.compile('\001?\033\\[((?:\\d|;)*)([a-zA-Z])\002?') # Control Sequence Introducer - ANSI_OSC_RE = re.compile('\001?\033\\]((?:.|;)*?)(\x07)\002?') # Operating System Command + ANSI_OSC_RE = re.compile('\001?\033\\]([^\a]*)(\a)\002?') # Operating System Command def __init__(self, wrapped, convert=None, strip=None, autoreset=False): # The wrapped stream (normally sys.stdout or sys.stderr) @@ -86,15 +94,22 @@ class AnsiToWin32(object): # (e.g. Cygwin Terminal). In this case it's up to the terminal # to support the ANSI codes. conversion_supported = on_windows and winapi_test() + try: + fd = wrapped.fileno() + except Exception: + fd = -1 + system_has_native_ansi = not on_windows or enable_vt_processing(fd) + have_tty = not self.stream.closed and self.stream.isatty() + need_conversion = conversion_supported and not system_has_native_ansi # should we strip ANSI sequences from our output? if strip is None: - strip = conversion_supported or (not self.stream.closed and not self.stream.isatty()) + strip = need_conversion or not have_tty self.strip = strip # should we should convert ANSI sequences into win32 calls? if convert is None: - convert = conversion_supported and not self.stream.closed and self.stream.isatty() + convert = need_conversion and have_tty self.convert = convert # dict of ansi codes to win32 functions and parameters @@ -247,11 +262,16 @@ class AnsiToWin32(object): start, end = match.span() text = text[:start] + text[end:] paramstring, command = match.groups() - if command in '\x07': # \x07 = BEL - params = paramstring.split(";") - # 0 - change title and icon (we will only change title) - # 1 - change icon (we don't support this) - # 2 - change title - if params[0] in '02': - winterm.set_title(params[1]) + if command == BEL: + if paramstring.count(";") == 1: + params = paramstring.split(";") + # 0 - change title and icon (we will only change title) + # 1 - change icon (we don't support this) + # 2 - change title + if params[0] in '02': + winterm.set_title(params[1]) return text + + + def flush(self): + self.wrapped.flush() diff --git a/libs/common/colorama/initialise.py b/libs/common/colorama/initialise.py index 430d0668..d5fd4b71 100644 --- a/libs/common/colorama/initialise.py +++ b/libs/common/colorama/initialise.py @@ -6,13 +6,27 @@ import sys from .ansitowin32 import AnsiToWin32 -orig_stdout = None -orig_stderr = None +def _wipe_internal_state_for_tests(): + global orig_stdout, orig_stderr + orig_stdout = None + orig_stderr = None -wrapped_stdout = None -wrapped_stderr = None + global wrapped_stdout, wrapped_stderr + wrapped_stdout = None + wrapped_stderr = None -atexit_done = False + global atexit_done + atexit_done = False + + global fixed_windows_console + fixed_windows_console = False + + try: + # no-op if it wasn't registered + atexit.unregister(reset_all) + except AttributeError: + # python 2: no atexit.unregister. Oh well, we did our best. + pass def reset_all(): @@ -55,6 +69,29 @@ def deinit(): sys.stderr = orig_stderr +def just_fix_windows_console(): + global fixed_windows_console + + if sys.platform != "win32": + return + if fixed_windows_console: + return + if wrapped_stdout is not None or wrapped_stderr is not None: + # Someone already ran init() and it did stuff, so we won't second-guess them + return + + # On newer versions of Windows, AnsiToWin32.__init__ will implicitly enable the + # native ANSI support in the console as a side-effect. We only need to actually + # replace sys.stdout/stderr if we're in the old-style conversion mode. + new_stdout = AnsiToWin32(sys.stdout, convert=None, strip=None, autoreset=False) + if new_stdout.convert: + sys.stdout = new_stdout + new_stderr = AnsiToWin32(sys.stderr, convert=None, strip=None, autoreset=False) + if new_stderr.convert: + sys.stderr = new_stderr + + fixed_windows_console = True + @contextlib.contextmanager def colorama_text(*args, **kwargs): init(*args, **kwargs) @@ -78,3 +115,7 @@ def wrap_stream(stream, convert, strip, autoreset, wrap): if wrapper.should_wrap(): stream = wrapper.stream return stream + + +# Use this for initial setup as well, to reduce code duplication +_wipe_internal_state_for_tests() diff --git a/libs/common/colorama/tests/__init__.py b/libs/common/colorama/tests/__init__.py new file mode 100644 index 00000000..8c5661e9 --- /dev/null +++ b/libs/common/colorama/tests/__init__.py @@ -0,0 +1 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. diff --git a/libs/common/colorama/tests/ansi_test.py b/libs/common/colorama/tests/ansi_test.py new file mode 100644 index 00000000..0a20c80f --- /dev/null +++ b/libs/common/colorama/tests/ansi_test.py @@ -0,0 +1,76 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +import sys +from unittest import TestCase, main + +from ..ansi import Back, Fore, Style +from ..ansitowin32 import AnsiToWin32 + +stdout_orig = sys.stdout +stderr_orig = sys.stderr + + +class AnsiTest(TestCase): + + def setUp(self): + # sanity check: stdout should be a file or StringIO object. + # It will only be AnsiToWin32 if init() has previously wrapped it + self.assertNotEqual(type(sys.stdout), AnsiToWin32) + self.assertNotEqual(type(sys.stderr), AnsiToWin32) + + def tearDown(self): + sys.stdout = stdout_orig + sys.stderr = stderr_orig + + + def testForeAttributes(self): + self.assertEqual(Fore.BLACK, '\033[30m') + self.assertEqual(Fore.RED, '\033[31m') + self.assertEqual(Fore.GREEN, '\033[32m') + self.assertEqual(Fore.YELLOW, '\033[33m') + self.assertEqual(Fore.BLUE, '\033[34m') + self.assertEqual(Fore.MAGENTA, '\033[35m') + self.assertEqual(Fore.CYAN, '\033[36m') + self.assertEqual(Fore.WHITE, '\033[37m') + self.assertEqual(Fore.RESET, '\033[39m') + + # Check the light, extended versions. + self.assertEqual(Fore.LIGHTBLACK_EX, '\033[90m') + self.assertEqual(Fore.LIGHTRED_EX, '\033[91m') + self.assertEqual(Fore.LIGHTGREEN_EX, '\033[92m') + self.assertEqual(Fore.LIGHTYELLOW_EX, '\033[93m') + self.assertEqual(Fore.LIGHTBLUE_EX, '\033[94m') + self.assertEqual(Fore.LIGHTMAGENTA_EX, '\033[95m') + self.assertEqual(Fore.LIGHTCYAN_EX, '\033[96m') + self.assertEqual(Fore.LIGHTWHITE_EX, '\033[97m') + + + def testBackAttributes(self): + self.assertEqual(Back.BLACK, '\033[40m') + self.assertEqual(Back.RED, '\033[41m') + self.assertEqual(Back.GREEN, '\033[42m') + self.assertEqual(Back.YELLOW, '\033[43m') + self.assertEqual(Back.BLUE, '\033[44m') + self.assertEqual(Back.MAGENTA, '\033[45m') + self.assertEqual(Back.CYAN, '\033[46m') + self.assertEqual(Back.WHITE, '\033[47m') + self.assertEqual(Back.RESET, '\033[49m') + + # Check the light, extended versions. + self.assertEqual(Back.LIGHTBLACK_EX, '\033[100m') + self.assertEqual(Back.LIGHTRED_EX, '\033[101m') + self.assertEqual(Back.LIGHTGREEN_EX, '\033[102m') + self.assertEqual(Back.LIGHTYELLOW_EX, '\033[103m') + self.assertEqual(Back.LIGHTBLUE_EX, '\033[104m') + self.assertEqual(Back.LIGHTMAGENTA_EX, '\033[105m') + self.assertEqual(Back.LIGHTCYAN_EX, '\033[106m') + self.assertEqual(Back.LIGHTWHITE_EX, '\033[107m') + + + def testStyleAttributes(self): + self.assertEqual(Style.DIM, '\033[2m') + self.assertEqual(Style.NORMAL, '\033[22m') + self.assertEqual(Style.BRIGHT, '\033[1m') + + +if __name__ == '__main__': + main() diff --git a/libs/common/colorama/tests/ansitowin32_test.py b/libs/common/colorama/tests/ansitowin32_test.py new file mode 100644 index 00000000..91ca551f --- /dev/null +++ b/libs/common/colorama/tests/ansitowin32_test.py @@ -0,0 +1,294 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +from io import StringIO, TextIOWrapper +from unittest import TestCase, main +try: + from contextlib import ExitStack +except ImportError: + # python 2 + from contextlib2 import ExitStack + +try: + from unittest.mock import MagicMock, Mock, patch +except ImportError: + from mock import MagicMock, Mock, patch + +from ..ansitowin32 import AnsiToWin32, StreamWrapper +from ..win32 import ENABLE_VIRTUAL_TERMINAL_PROCESSING +from .utils import osname + + +class StreamWrapperTest(TestCase): + + def testIsAProxy(self): + mockStream = Mock() + wrapper = StreamWrapper(mockStream, None) + self.assertTrue( wrapper.random_attr is mockStream.random_attr ) + + def testDelegatesWrite(self): + mockStream = Mock() + mockConverter = Mock() + wrapper = StreamWrapper(mockStream, mockConverter) + wrapper.write('hello') + self.assertTrue(mockConverter.write.call_args, (('hello',), {})) + + def testDelegatesContext(self): + mockConverter = Mock() + s = StringIO() + with StreamWrapper(s, mockConverter) as fp: + fp.write(u'hello') + self.assertTrue(s.closed) + + def testProxyNoContextManager(self): + mockStream = MagicMock() + mockStream.__enter__.side_effect = AttributeError() + mockConverter = Mock() + with self.assertRaises(AttributeError) as excinfo: + with StreamWrapper(mockStream, mockConverter) as wrapper: + wrapper.write('hello') + + def test_closed_shouldnt_raise_on_closed_stream(self): + stream = StringIO() + stream.close() + wrapper = StreamWrapper(stream, None) + self.assertEqual(wrapper.closed, True) + + def test_closed_shouldnt_raise_on_detached_stream(self): + stream = TextIOWrapper(StringIO()) + stream.detach() + wrapper = StreamWrapper(stream, None) + self.assertEqual(wrapper.closed, True) + +class AnsiToWin32Test(TestCase): + + def testInit(self): + mockStdout = Mock() + auto = Mock() + stream = AnsiToWin32(mockStdout, autoreset=auto) + self.assertEqual(stream.wrapped, mockStdout) + self.assertEqual(stream.autoreset, auto) + + @patch('colorama.ansitowin32.winterm', None) + @patch('colorama.ansitowin32.winapi_test', lambda *_: True) + def testStripIsTrueOnWindows(self): + with osname('nt'): + mockStdout = Mock() + stream = AnsiToWin32(mockStdout) + self.assertTrue(stream.strip) + + def testStripIsFalseOffWindows(self): + with osname('posix'): + mockStdout = Mock(closed=False) + stream = AnsiToWin32(mockStdout) + self.assertFalse(stream.strip) + + def testWriteStripsAnsi(self): + mockStdout = Mock() + stream = AnsiToWin32(mockStdout) + stream.wrapped = Mock() + stream.write_and_convert = Mock() + stream.strip = True + + stream.write('abc') + + self.assertFalse(stream.wrapped.write.called) + self.assertEqual(stream.write_and_convert.call_args, (('abc',), {})) + + def testWriteDoesNotStripAnsi(self): + mockStdout = Mock() + stream = AnsiToWin32(mockStdout) + stream.wrapped = Mock() + stream.write_and_convert = Mock() + stream.strip = False + stream.convert = False + + stream.write('abc') + + self.assertFalse(stream.write_and_convert.called) + self.assertEqual(stream.wrapped.write.call_args, (('abc',), {})) + + def assert_autoresets(self, convert, autoreset=True): + stream = AnsiToWin32(Mock()) + stream.convert = convert + stream.reset_all = Mock() + stream.autoreset = autoreset + stream.winterm = Mock() + + stream.write('abc') + + self.assertEqual(stream.reset_all.called, autoreset) + + def testWriteAutoresets(self): + self.assert_autoresets(convert=True) + self.assert_autoresets(convert=False) + self.assert_autoresets(convert=True, autoreset=False) + self.assert_autoresets(convert=False, autoreset=False) + + def testWriteAndConvertWritesPlainText(self): + stream = AnsiToWin32(Mock()) + stream.write_and_convert( 'abc' ) + self.assertEqual( stream.wrapped.write.call_args, (('abc',), {}) ) + + def testWriteAndConvertStripsAllValidAnsi(self): + stream = AnsiToWin32(Mock()) + stream.call_win32 = Mock() + data = [ + 'abc\033[mdef', + 'abc\033[0mdef', + 'abc\033[2mdef', + 'abc\033[02mdef', + 'abc\033[002mdef', + 'abc\033[40mdef', + 'abc\033[040mdef', + 'abc\033[0;1mdef', + 'abc\033[40;50mdef', + 'abc\033[50;30;40mdef', + 'abc\033[Adef', + 'abc\033[0Gdef', + 'abc\033[1;20;128Hdef', + ] + for datum in data: + stream.wrapped.write.reset_mock() + stream.write_and_convert( datum ) + self.assertEqual( + [args[0] for args in stream.wrapped.write.call_args_list], + [ ('abc',), ('def',) ] + ) + + def testWriteAndConvertSkipsEmptySnippets(self): + stream = AnsiToWin32(Mock()) + stream.call_win32 = Mock() + stream.write_and_convert( '\033[40m\033[41m' ) + self.assertFalse( stream.wrapped.write.called ) + + def testWriteAndConvertCallsWin32WithParamsAndCommand(self): + stream = AnsiToWin32(Mock()) + stream.convert = True + stream.call_win32 = Mock() + stream.extract_params = Mock(return_value='params') + data = { + 'abc\033[adef': ('a', 'params'), + 'abc\033[;;bdef': ('b', 'params'), + 'abc\033[0cdef': ('c', 'params'), + 'abc\033[;;0;;Gdef': ('G', 'params'), + 'abc\033[1;20;128Hdef': ('H', 'params'), + } + for datum, expected in data.items(): + stream.call_win32.reset_mock() + stream.write_and_convert( datum ) + self.assertEqual( stream.call_win32.call_args[0], expected ) + + def test_reset_all_shouldnt_raise_on_closed_orig_stdout(self): + stream = StringIO() + converter = AnsiToWin32(stream) + stream.close() + + converter.reset_all() + + def test_wrap_shouldnt_raise_on_closed_orig_stdout(self): + stream = StringIO() + stream.close() + with \ + patch("colorama.ansitowin32.os.name", "nt"), \ + patch("colorama.ansitowin32.winapi_test", lambda: True): + converter = AnsiToWin32(stream) + self.assertTrue(converter.strip) + self.assertFalse(converter.convert) + + def test_wrap_shouldnt_raise_on_missing_closed_attr(self): + with \ + patch("colorama.ansitowin32.os.name", "nt"), \ + patch("colorama.ansitowin32.winapi_test", lambda: True): + converter = AnsiToWin32(object()) + self.assertTrue(converter.strip) + self.assertFalse(converter.convert) + + def testExtractParams(self): + stream = AnsiToWin32(Mock()) + data = { + '': (0,), + ';;': (0,), + '2': (2,), + ';;002;;': (2,), + '0;1': (0, 1), + ';;003;;456;;': (3, 456), + '11;22;33;44;55': (11, 22, 33, 44, 55), + } + for datum, expected in data.items(): + self.assertEqual(stream.extract_params('m', datum), expected) + + def testCallWin32UsesLookup(self): + listener = Mock() + stream = AnsiToWin32(listener) + stream.win32_calls = { + 1: (lambda *_, **__: listener(11),), + 2: (lambda *_, **__: listener(22),), + 3: (lambda *_, **__: listener(33),), + } + stream.call_win32('m', (3, 1, 99, 2)) + self.assertEqual( + [a[0][0] for a in listener.call_args_list], + [33, 11, 22] ) + + def test_osc_codes(self): + mockStdout = Mock() + stream = AnsiToWin32(mockStdout, convert=True) + with patch('colorama.ansitowin32.winterm') as winterm: + data = [ + '\033]0\x07', # missing arguments + '\033]0;foo\x08', # wrong OSC command + '\033]0;colorama_test_title\x07', # should work + '\033]1;colorama_test_title\x07', # wrong set command + '\033]2;colorama_test_title\x07', # should work + '\033]' + ';' * 64 + '\x08', # see issue #247 + ] + for code in data: + stream.write(code) + self.assertEqual(winterm.set_title.call_count, 2) + + def test_native_windows_ansi(self): + with ExitStack() as stack: + def p(a, b): + stack.enter_context(patch(a, b, create=True)) + # Pretend to be on Windows + p("colorama.ansitowin32.os.name", "nt") + p("colorama.ansitowin32.winapi_test", lambda: True) + p("colorama.win32.winapi_test", lambda: True) + p("colorama.winterm.win32.windll", "non-None") + p("colorama.winterm.get_osfhandle", lambda _: 1234) + + # Pretend that our mock stream has native ANSI support + p( + "colorama.winterm.win32.GetConsoleMode", + lambda _: ENABLE_VIRTUAL_TERMINAL_PROCESSING, + ) + SetConsoleMode = Mock() + p("colorama.winterm.win32.SetConsoleMode", SetConsoleMode) + + stdout = Mock() + stdout.closed = False + stdout.isatty.return_value = True + stdout.fileno.return_value = 1 + + # Our fake console says it has native vt support, so AnsiToWin32 should + # enable that support and do nothing else. + stream = AnsiToWin32(stdout) + SetConsoleMode.assert_called_with(1234, ENABLE_VIRTUAL_TERMINAL_PROCESSING) + self.assertFalse(stream.strip) + self.assertFalse(stream.convert) + self.assertFalse(stream.should_wrap()) + + # Now let's pretend we're on an old Windows console, that doesn't have + # native ANSI support. + p("colorama.winterm.win32.GetConsoleMode", lambda _: 0) + SetConsoleMode = Mock() + p("colorama.winterm.win32.SetConsoleMode", SetConsoleMode) + + stream = AnsiToWin32(stdout) + SetConsoleMode.assert_called_with(1234, ENABLE_VIRTUAL_TERMINAL_PROCESSING) + self.assertTrue(stream.strip) + self.assertTrue(stream.convert) + self.assertTrue(stream.should_wrap()) + + +if __name__ == '__main__': + main() diff --git a/libs/common/colorama/tests/initialise_test.py b/libs/common/colorama/tests/initialise_test.py new file mode 100644 index 00000000..89f9b075 --- /dev/null +++ b/libs/common/colorama/tests/initialise_test.py @@ -0,0 +1,189 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +import sys +from unittest import TestCase, main, skipUnless + +try: + from unittest.mock import patch, Mock +except ImportError: + from mock import patch, Mock + +from ..ansitowin32 import StreamWrapper +from ..initialise import init, just_fix_windows_console, _wipe_internal_state_for_tests +from .utils import osname, replace_by + +orig_stdout = sys.stdout +orig_stderr = sys.stderr + + +class InitTest(TestCase): + + @skipUnless(sys.stdout.isatty(), "sys.stdout is not a tty") + def setUp(self): + # sanity check + self.assertNotWrapped() + + def tearDown(self): + _wipe_internal_state_for_tests() + sys.stdout = orig_stdout + sys.stderr = orig_stderr + + def assertWrapped(self): + self.assertIsNot(sys.stdout, orig_stdout, 'stdout should be wrapped') + self.assertIsNot(sys.stderr, orig_stderr, 'stderr should be wrapped') + self.assertTrue(isinstance(sys.stdout, StreamWrapper), + 'bad stdout wrapper') + self.assertTrue(isinstance(sys.stderr, StreamWrapper), + 'bad stderr wrapper') + + def assertNotWrapped(self): + self.assertIs(sys.stdout, orig_stdout, 'stdout should not be wrapped') + self.assertIs(sys.stderr, orig_stderr, 'stderr should not be wrapped') + + @patch('colorama.initialise.reset_all') + @patch('colorama.ansitowin32.winapi_test', lambda *_: True) + @patch('colorama.ansitowin32.enable_vt_processing', lambda *_: False) + def testInitWrapsOnWindows(self, _): + with osname("nt"): + init() + self.assertWrapped() + + @patch('colorama.initialise.reset_all') + @patch('colorama.ansitowin32.winapi_test', lambda *_: False) + def testInitDoesntWrapOnEmulatedWindows(self, _): + with osname("nt"): + init() + self.assertNotWrapped() + + def testInitDoesntWrapOnNonWindows(self): + with osname("posix"): + init() + self.assertNotWrapped() + + def testInitDoesntWrapIfNone(self): + with replace_by(None): + init() + # We can't use assertNotWrapped here because replace_by(None) + # changes stdout/stderr already. + self.assertIsNone(sys.stdout) + self.assertIsNone(sys.stderr) + + def testInitAutoresetOnWrapsOnAllPlatforms(self): + with osname("posix"): + init(autoreset=True) + self.assertWrapped() + + def testInitWrapOffDoesntWrapOnWindows(self): + with osname("nt"): + init(wrap=False) + self.assertNotWrapped() + + def testInitWrapOffIncompatibleWithAutoresetOn(self): + self.assertRaises(ValueError, lambda: init(autoreset=True, wrap=False)) + + @patch('colorama.win32.SetConsoleTextAttribute') + @patch('colorama.initialise.AnsiToWin32') + def testAutoResetPassedOn(self, mockATW32, _): + with osname("nt"): + init(autoreset=True) + self.assertEqual(len(mockATW32.call_args_list), 2) + self.assertEqual(mockATW32.call_args_list[1][1]['autoreset'], True) + self.assertEqual(mockATW32.call_args_list[0][1]['autoreset'], True) + + @patch('colorama.initialise.AnsiToWin32') + def testAutoResetChangeable(self, mockATW32): + with osname("nt"): + init() + + init(autoreset=True) + self.assertEqual(len(mockATW32.call_args_list), 4) + self.assertEqual(mockATW32.call_args_list[2][1]['autoreset'], True) + self.assertEqual(mockATW32.call_args_list[3][1]['autoreset'], True) + + init() + self.assertEqual(len(mockATW32.call_args_list), 6) + self.assertEqual( + mockATW32.call_args_list[4][1]['autoreset'], False) + self.assertEqual( + mockATW32.call_args_list[5][1]['autoreset'], False) + + + @patch('colorama.initialise.atexit.register') + def testAtexitRegisteredOnlyOnce(self, mockRegister): + init() + self.assertTrue(mockRegister.called) + mockRegister.reset_mock() + init() + self.assertFalse(mockRegister.called) + + +class JustFixWindowsConsoleTest(TestCase): + def _reset(self): + _wipe_internal_state_for_tests() + sys.stdout = orig_stdout + sys.stderr = orig_stderr + + def tearDown(self): + self._reset() + + @patch("colorama.ansitowin32.winapi_test", lambda: True) + def testJustFixWindowsConsole(self): + if sys.platform != "win32": + # just_fix_windows_console should be a no-op + just_fix_windows_console() + self.assertIs(sys.stdout, orig_stdout) + self.assertIs(sys.stderr, orig_stderr) + else: + def fake_std(): + # Emulate stdout=not a tty, stderr=tty + # to check that we handle both cases correctly + stdout = Mock() + stdout.closed = False + stdout.isatty.return_value = False + stdout.fileno.return_value = 1 + sys.stdout = stdout + + stderr = Mock() + stderr.closed = False + stderr.isatty.return_value = True + stderr.fileno.return_value = 2 + sys.stderr = stderr + + for native_ansi in [False, True]: + with patch( + 'colorama.ansitowin32.enable_vt_processing', + lambda *_: native_ansi + ): + self._reset() + fake_std() + + # Regular single-call test + prev_stdout = sys.stdout + prev_stderr = sys.stderr + just_fix_windows_console() + self.assertIs(sys.stdout, prev_stdout) + if native_ansi: + self.assertIs(sys.stderr, prev_stderr) + else: + self.assertIsNot(sys.stderr, prev_stderr) + + # second call without resetting is always a no-op + prev_stdout = sys.stdout + prev_stderr = sys.stderr + just_fix_windows_console() + self.assertIs(sys.stdout, prev_stdout) + self.assertIs(sys.stderr, prev_stderr) + + self._reset() + fake_std() + + # If init() runs first, just_fix_windows_console should be a no-op + init() + prev_stdout = sys.stdout + prev_stderr = sys.stderr + just_fix_windows_console() + self.assertIs(prev_stdout, sys.stdout) + self.assertIs(prev_stderr, sys.stderr) + + +if __name__ == '__main__': + main() diff --git a/libs/common/colorama/tests/isatty_test.py b/libs/common/colorama/tests/isatty_test.py new file mode 100644 index 00000000..0f84e4be --- /dev/null +++ b/libs/common/colorama/tests/isatty_test.py @@ -0,0 +1,57 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +import sys +from unittest import TestCase, main + +from ..ansitowin32 import StreamWrapper, AnsiToWin32 +from .utils import pycharm, replace_by, replace_original_by, StreamTTY, StreamNonTTY + + +def is_a_tty(stream): + return StreamWrapper(stream, None).isatty() + +class IsattyTest(TestCase): + + def test_TTY(self): + tty = StreamTTY() + self.assertTrue(is_a_tty(tty)) + with pycharm(): + self.assertTrue(is_a_tty(tty)) + + def test_nonTTY(self): + non_tty = StreamNonTTY() + self.assertFalse(is_a_tty(non_tty)) + with pycharm(): + self.assertFalse(is_a_tty(non_tty)) + + def test_withPycharm(self): + with pycharm(): + self.assertTrue(is_a_tty(sys.stderr)) + self.assertTrue(is_a_tty(sys.stdout)) + + def test_withPycharmTTYOverride(self): + tty = StreamTTY() + with pycharm(), replace_by(tty): + self.assertTrue(is_a_tty(tty)) + + def test_withPycharmNonTTYOverride(self): + non_tty = StreamNonTTY() + with pycharm(), replace_by(non_tty): + self.assertFalse(is_a_tty(non_tty)) + + def test_withPycharmNoneOverride(self): + with pycharm(): + with replace_by(None), replace_original_by(None): + self.assertFalse(is_a_tty(None)) + self.assertFalse(is_a_tty(StreamNonTTY())) + self.assertTrue(is_a_tty(StreamTTY())) + + def test_withPycharmStreamWrapped(self): + with pycharm(): + self.assertTrue(AnsiToWin32(StreamTTY()).stream.isatty()) + self.assertFalse(AnsiToWin32(StreamNonTTY()).stream.isatty()) + self.assertTrue(AnsiToWin32(sys.stdout).stream.isatty()) + self.assertTrue(AnsiToWin32(sys.stderr).stream.isatty()) + + +if __name__ == '__main__': + main() diff --git a/libs/common/colorama/tests/utils.py b/libs/common/colorama/tests/utils.py new file mode 100644 index 00000000..472fafb4 --- /dev/null +++ b/libs/common/colorama/tests/utils.py @@ -0,0 +1,49 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +from contextlib import contextmanager +from io import StringIO +import sys +import os + + +class StreamTTY(StringIO): + def isatty(self): + return True + +class StreamNonTTY(StringIO): + def isatty(self): + return False + +@contextmanager +def osname(name): + orig = os.name + os.name = name + yield + os.name = orig + +@contextmanager +def replace_by(stream): + orig_stdout = sys.stdout + orig_stderr = sys.stderr + sys.stdout = stream + sys.stderr = stream + yield + sys.stdout = orig_stdout + sys.stderr = orig_stderr + +@contextmanager +def replace_original_by(stream): + orig_stdout = sys.__stdout__ + orig_stderr = sys.__stderr__ + sys.__stdout__ = stream + sys.__stderr__ = stream + yield + sys.__stdout__ = orig_stdout + sys.__stderr__ = orig_stderr + +@contextmanager +def pycharm(): + os.environ["PYCHARM_HOSTED"] = "1" + non_tty = StreamNonTTY() + with replace_by(non_tty), replace_original_by(non_tty): + yield + del os.environ["PYCHARM_HOSTED"] diff --git a/libs/common/colorama/tests/winterm_test.py b/libs/common/colorama/tests/winterm_test.py new file mode 100644 index 00000000..d0955f9e --- /dev/null +++ b/libs/common/colorama/tests/winterm_test.py @@ -0,0 +1,131 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +import sys +from unittest import TestCase, main, skipUnless + +try: + from unittest.mock import Mock, patch +except ImportError: + from mock import Mock, patch + +from ..winterm import WinColor, WinStyle, WinTerm + + +class WinTermTest(TestCase): + + @patch('colorama.winterm.win32') + def testInit(self, mockWin32): + mockAttr = Mock() + mockAttr.wAttributes = 7 + 6 * 16 + 8 + mockWin32.GetConsoleScreenBufferInfo.return_value = mockAttr + term = WinTerm() + self.assertEqual(term._fore, 7) + self.assertEqual(term._back, 6) + self.assertEqual(term._style, 8) + + @skipUnless(sys.platform.startswith("win"), "requires Windows") + def testGetAttrs(self): + term = WinTerm() + + term._fore = 0 + term._back = 0 + term._style = 0 + self.assertEqual(term.get_attrs(), 0) + + term._fore = WinColor.YELLOW + self.assertEqual(term.get_attrs(), WinColor.YELLOW) + + term._back = WinColor.MAGENTA + self.assertEqual( + term.get_attrs(), + WinColor.YELLOW + WinColor.MAGENTA * 16) + + term._style = WinStyle.BRIGHT + self.assertEqual( + term.get_attrs(), + WinColor.YELLOW + WinColor.MAGENTA * 16 + WinStyle.BRIGHT) + + @patch('colorama.winterm.win32') + def testResetAll(self, mockWin32): + mockAttr = Mock() + mockAttr.wAttributes = 1 + 2 * 16 + 8 + mockWin32.GetConsoleScreenBufferInfo.return_value = mockAttr + term = WinTerm() + + term.set_console = Mock() + term._fore = -1 + term._back = -1 + term._style = -1 + + term.reset_all() + + self.assertEqual(term._fore, 1) + self.assertEqual(term._back, 2) + self.assertEqual(term._style, 8) + self.assertEqual(term.set_console.called, True) + + @skipUnless(sys.platform.startswith("win"), "requires Windows") + def testFore(self): + term = WinTerm() + term.set_console = Mock() + term._fore = 0 + + term.fore(5) + + self.assertEqual(term._fore, 5) + self.assertEqual(term.set_console.called, True) + + @skipUnless(sys.platform.startswith("win"), "requires Windows") + def testBack(self): + term = WinTerm() + term.set_console = Mock() + term._back = 0 + + term.back(5) + + self.assertEqual(term._back, 5) + self.assertEqual(term.set_console.called, True) + + @skipUnless(sys.platform.startswith("win"), "requires Windows") + def testStyle(self): + term = WinTerm() + term.set_console = Mock() + term._style = 0 + + term.style(22) + + self.assertEqual(term._style, 22) + self.assertEqual(term.set_console.called, True) + + @patch('colorama.winterm.win32') + def testSetConsole(self, mockWin32): + mockAttr = Mock() + mockAttr.wAttributes = 0 + mockWin32.GetConsoleScreenBufferInfo.return_value = mockAttr + term = WinTerm() + term.windll = Mock() + + term.set_console() + + self.assertEqual( + mockWin32.SetConsoleTextAttribute.call_args, + ((mockWin32.STDOUT, term.get_attrs()), {}) + ) + + @patch('colorama.winterm.win32') + def testSetConsoleOnStderr(self, mockWin32): + mockAttr = Mock() + mockAttr.wAttributes = 0 + mockWin32.GetConsoleScreenBufferInfo.return_value = mockAttr + term = WinTerm() + term.windll = Mock() + + term.set_console(on_stderr=True) + + self.assertEqual( + mockWin32.SetConsoleTextAttribute.call_args, + ((mockWin32.STDERR, term.get_attrs()), {}) + ) + + +if __name__ == '__main__': + main() diff --git a/libs/common/colorama/win32.py b/libs/common/colorama/win32.py index c2d83603..841b0e27 100644 --- a/libs/common/colorama/win32.py +++ b/libs/common/colorama/win32.py @@ -4,6 +4,8 @@ STDOUT = -11 STDERR = -12 +ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 + try: import ctypes from ctypes import LibraryLoader @@ -89,6 +91,20 @@ else: ] _SetConsoleTitleW.restype = wintypes.BOOL + _GetConsoleMode = windll.kernel32.GetConsoleMode + _GetConsoleMode.argtypes = [ + wintypes.HANDLE, + POINTER(wintypes.DWORD) + ] + _GetConsoleMode.restype = wintypes.BOOL + + _SetConsoleMode = windll.kernel32.SetConsoleMode + _SetConsoleMode.argtypes = [ + wintypes.HANDLE, + wintypes.DWORD + ] + _SetConsoleMode.restype = wintypes.BOOL + def _winapi_test(handle): csbi = CONSOLE_SCREEN_BUFFER_INFO() success = _GetConsoleScreenBufferInfo( @@ -150,3 +166,15 @@ else: def SetConsoleTitle(title): return _SetConsoleTitleW(title) + + def GetConsoleMode(handle): + mode = wintypes.DWORD() + success = _GetConsoleMode(handle, byref(mode)) + if not success: + raise ctypes.WinError() + return mode.value + + def SetConsoleMode(handle, mode): + success = _SetConsoleMode(handle, mode) + if not success: + raise ctypes.WinError() diff --git a/libs/common/colorama/winterm.py b/libs/common/colorama/winterm.py index 0fdb4ec4..aad867e8 100644 --- a/libs/common/colorama/winterm.py +++ b/libs/common/colorama/winterm.py @@ -1,7 +1,13 @@ # Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. -from . import win32 +try: + from msvcrt import get_osfhandle +except ImportError: + def get_osfhandle(_): + raise OSError("This isn't windows!") +from . import win32 + # from wincon.h class WinColor(object): BLACK = 0 @@ -167,3 +173,23 @@ class WinTerm(object): def set_title(self, title): win32.SetConsoleTitle(title) + + +def enable_vt_processing(fd): + if win32.windll is None or not win32.winapi_test(): + return False + + try: + handle = get_osfhandle(fd) + mode = win32.GetConsoleMode(handle) + win32.SetConsoleMode( + handle, + mode | win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING, + ) + + mode = win32.GetConsoleMode(handle) + if mode & win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING: + return True + # Can get TypeError in testsuite where 'fd' is a Mock() + except (OSError, TypeError): + return False diff --git a/libs/common/confuse/__init__.py b/libs/common/confuse/__init__.py new file mode 100644 index 00000000..be5090c4 --- /dev/null +++ b/libs/common/confuse/__init__.py @@ -0,0 +1,13 @@ +"""Painless YAML configuration. +""" + +from __future__ import division, absolute_import, print_function + +__version__ = '1.7.0' + +from .exceptions import * # NOQA +from .util import * # NOQA +from .yaml_util import * # NOQA +from .sources import * # NOQA +from .templates import * # NOQA +from .core import * # NOQA diff --git a/libs/common/confuse/core.py b/libs/common/confuse/core.py new file mode 100644 index 00000000..6c6c4b09 --- /dev/null +++ b/libs/common/confuse/core.py @@ -0,0 +1,724 @@ +# -*- coding: utf-8 -*- +# This file is part of Confuse. +# Copyright 2016, Adrian Sampson. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Worry-free YAML configuration files. +""" +from __future__ import division, absolute_import, print_function + +import errno +import os +import yaml +from collections import OrderedDict + +from . import util +from . import templates +from . import yaml_util +from .sources import ConfigSource, EnvSource, YamlSource +from .exceptions import ConfigTypeError, NotFoundError, ConfigError + +CONFIG_FILENAME = 'config.yaml' +DEFAULT_FILENAME = 'config_default.yaml' +ROOT_NAME = 'root' + +REDACTED_TOMBSTONE = 'REDACTED' + + +# Views and sources. + + +class ConfigView(object): + """A configuration "view" is a query into a program's configuration + data. A view represents a hypothetical location in the configuration + tree; to extract the data from the location, a client typically + calls the ``view.get()`` method. The client can access children in + the tree (subviews) by subscripting the parent view (i.e., + ``view[key]``). + """ + + name = None + """The name of the view, depicting the path taken through the + configuration in Python-like syntax (e.g., ``foo['bar'][42]``). + """ + + def resolve(self): + """The core (internal) data retrieval method. Generates (value, + source) pairs for each source that contains a value for this + view. May raise `ConfigTypeError` if a type error occurs while + traversing a source. + """ + raise NotImplementedError + + def first(self): + """Return a (value, source) pair for the first object found for + this view. This amounts to the first element returned by + `resolve`. If no values are available, a `NotFoundError` is + raised. + """ + pairs = self.resolve() + try: + return util.iter_first(pairs) + except ValueError: + raise NotFoundError(u"{0} not found".format(self.name)) + + def exists(self): + """Determine whether the view has a setting in any source. + """ + try: + self.first() + except NotFoundError: + return False + return True + + def add(self, value): + """Set the *default* value for this configuration view. The + specified value is added as the lowest-priority configuration + data source. + """ + raise NotImplementedError + + def set(self, value): + """*Override* the value for this configuration view. The + specified value is added as the highest-priority configuration + data source. + """ + raise NotImplementedError + + def root(self): + """The RootView object from which this view is descended. + """ + raise NotImplementedError + + def __repr__(self): + return '<{}: {}>'.format(self.__class__.__name__, self.name) + + def __iter__(self): + """Iterate over the keys of a dictionary view or the *subviews* + of a list view. + """ + # Try iterating over the keys, if this is a dictionary view. + try: + for key in self.keys(): + yield key + + except ConfigTypeError: + # Otherwise, try iterating over a list view. + try: + for subview in self.sequence(): + yield subview + + except ConfigTypeError: + item, _ = self.first() + raise ConfigTypeError( + u'{0} must be a dictionary or a list, not {1}'.format( + self.name, type(item).__name__ + ) + ) + + def __getitem__(self, key): + """Get a subview of this view.""" + return Subview(self, key) + + def __setitem__(self, key, value): + """Create an overlay source to assign a given key under this + view. + """ + self.set({key: value}) + + def __contains__(self, key): + return self[key].exists() + + def set_args(self, namespace, dots=False): + """Overlay parsed command-line arguments, generated by a library + like argparse or optparse, onto this view's value. + + :param namespace: Dictionary or Namespace to overlay this config with. + Supports nested Dictionaries and Namespaces. + :type namespace: dict or Namespace + :param dots: If True, any properties on namespace that contain dots (.) + will be broken down into child dictionaries. + :Example: + + {'foo.bar': 'car'} + # Will be turned into + {'foo': {'bar': 'car'}} + :type dots: bool + """ + self.set(util.build_dict(namespace, sep='.' if dots else '')) + + # Magical conversions. These special methods make it possible to use + # View objects somewhat transparently in certain circumstances. For + # example, rather than using ``view.get(bool)``, it's possible to + # just say ``bool(view)`` or use ``view`` in a conditional. + + def __str__(self): + """Get the value for this view as a bytestring. + """ + if util.PY3: + return self.__unicode__() + else: + return bytes(self.get()) + + def __unicode__(self): + """Get the value for this view as a Unicode string. + """ + return util.STRING(self.get()) + + def __nonzero__(self): + """Gets the value for this view as a boolean. (Python 2 only.) + """ + return self.__bool__() + + def __bool__(self): + """Gets the value for this view as a boolean. (Python 3 only.) + """ + return bool(self.get()) + + # Dictionary emulation methods. + + def keys(self): + """Returns a list containing all the keys available as subviews + of the current views. This enumerates all the keys in *all* + dictionaries matching the current view, in contrast to + ``view.get(dict).keys()``, which gets all the keys for the + *first* dict matching the view. If the object for this view in + any source is not a dict, then a `ConfigTypeError` is raised. The + keys are ordered according to how they appear in each source. + """ + keys = [] + + for dic, _ in self.resolve(): + try: + cur_keys = dic.keys() + except AttributeError: + raise ConfigTypeError( + u'{0} must be a dict, not {1}'.format( + self.name, type(dic).__name__ + ) + ) + + for key in cur_keys: + if key not in keys: + keys.append(key) + + return keys + + def items(self): + """Iterates over (key, subview) pairs contained in dictionaries + from *all* sources at this view. If the object for this view in + any source is not a dict, then a `ConfigTypeError` is raised. + """ + for key in self.keys(): + yield key, self[key] + + def values(self): + """Iterates over all the subviews contained in dictionaries from + *all* sources at this view. If the object for this view in any + source is not a dict, then a `ConfigTypeError` is raised. + """ + for key in self.keys(): + yield self[key] + + # List/sequence emulation. + + def sequence(self): + """Iterates over the subviews contained in lists from the *first* + source at this view. If the object for this view in the first source + is not a list or tuple, then a `ConfigTypeError` is raised. + """ + try: + collection, _ = self.first() + except NotFoundError: + return + if not isinstance(collection, (list, tuple)): + raise ConfigTypeError( + u'{0} must be a list, not {1}'.format( + self.name, type(collection).__name__ + ) + ) + + # Yield all the indices in the sequence. + for index in range(len(collection)): + yield self[index] + + def all_contents(self): + """Iterates over all subviews from collections at this view from + *all* sources. If the object for this view in any source is not + iterable, then a `ConfigTypeError` is raised. This method is + intended to be used when the view indicates a list; this method + will concatenate the contents of the list from all sources. + """ + for collection, _ in self.resolve(): + try: + it = iter(collection) + except TypeError: + raise ConfigTypeError( + u'{0} must be an iterable, not {1}'.format( + self.name, type(collection).__name__ + ) + ) + for value in it: + yield value + + # Validation and conversion. + + def flatten(self, redact=False): + """Create a hierarchy of OrderedDicts containing the data from + this view, recursively reifying all views to get their + represented values. + + If `redact` is set, then sensitive values are replaced with + the string "REDACTED". + """ + od = OrderedDict() + for key, view in self.items(): + if redact and view.redact: + od[key] = REDACTED_TOMBSTONE + else: + try: + od[key] = view.flatten(redact=redact) + except ConfigTypeError: + od[key] = view.get() + return od + + def get(self, template=templates.REQUIRED): + """Retrieve the value for this view according to the template. + + The `template` against which the values are checked can be + anything convertible to a `Template` using `as_template`. This + means you can pass in a default integer or string value, for + example, or a type to just check that something matches the type + you expect. + + May raise a `ConfigValueError` (or its subclass, + `ConfigTypeError`) or a `NotFoundError` when the configuration + doesn't satisfy the template. + """ + return templates.as_template(template).value(self, template) + + # Shortcuts for common templates. + + def as_filename(self): + """Get the value as a path. Equivalent to `get(Filename())`. + """ + return self.get(templates.Filename()) + + def as_path(self): + """Get the value as a `pathlib.Path` object. Equivalent to `get(Path())`. + """ + return self.get(templates.Path()) + + def as_choice(self, choices): + """Get the value from a list of choices. Equivalent to + `get(Choice(choices))`. + """ + return self.get(templates.Choice(choices)) + + def as_number(self): + """Get the value as any number type: int or float. Equivalent to + `get(Number())`. + """ + return self.get(templates.Number()) + + def as_str_seq(self, split=True): + """Get the value as a sequence of strings. Equivalent to + `get(StrSeq(split=split))`. + """ + return self.get(templates.StrSeq(split=split)) + + def as_pairs(self, default_value=None): + """Get the value as a sequence of pairs of two strings. Equivalent to + `get(Pairs(default_value=default_value))`. + """ + return self.get(templates.Pairs(default_value=default_value)) + + def as_str(self): + """Get the value as a (Unicode) string. Equivalent to + `get(unicode)` on Python 2 and `get(str)` on Python 3. + """ + return self.get(templates.String()) + + def as_str_expanded(self): + """Get the value as a (Unicode) string, with env vars + expanded by `os.path.expandvars()`. + """ + return self.get(templates.String(expand_vars=True)) + + # Redaction. + + @property + def redact(self): + """Whether the view contains sensitive information and should be + redacted from output. + """ + return () in self.get_redactions() + + @redact.setter + def redact(self, flag): + self.set_redaction((), flag) + + def set_redaction(self, path, flag): + """Add or remove a redaction for a key path, which should be an + iterable of keys. + """ + raise NotImplementedError() + + def get_redactions(self): + """Get the set of currently-redacted sub-key-paths at this view. + """ + raise NotImplementedError() + + +class RootView(ConfigView): + """The base of a view hierarchy. This view keeps track of the + sources that may be accessed by subviews. + """ + def __init__(self, sources): + """Create a configuration hierarchy for a list of sources. At + least one source must be provided. The first source in the list + has the highest priority. + """ + self.sources = list(sources) + self.name = ROOT_NAME + self.redactions = set() + + def add(self, obj): + self.sources.append(ConfigSource.of(obj)) + + def set(self, value): + self.sources.insert(0, ConfigSource.of(value)) + + def resolve(self): + return ((dict(s), s) for s in self.sources) + + def clear(self): + """Remove all sources (and redactions) from this + configuration. + """ + del self.sources[:] + self.redactions.clear() + + def root(self): + return self + + def set_redaction(self, path, flag): + if flag: + self.redactions.add(path) + elif path in self.redactions: + self.redactions.remove(path) + + def get_redactions(self): + return self.redactions + + +class Subview(ConfigView): + """A subview accessed via a subscript of a parent view.""" + def __init__(self, parent, key): + """Make a subview of a parent view for a given subscript key. + """ + self.parent = parent + self.key = key + + # Choose a human-readable name for this view. + if isinstance(self.parent, RootView): + self.name = '' + else: + self.name = self.parent.name + if not isinstance(self.key, int): + self.name += '.' + if isinstance(self.key, int): + self.name += u'#{0}'.format(self.key) + elif isinstance(self.key, bytes): + self.name += self.key.decode('utf-8') + elif isinstance(self.key, util.STRING): + self.name += self.key + else: + self.name += repr(self.key) + + def resolve(self): + for collection, source in self.parent.resolve(): + try: + value = collection[self.key] + except IndexError: + # List index out of bounds. + continue + except KeyError: + # Dict key does not exist. + continue + except TypeError: + # Not subscriptable. + raise ConfigTypeError( + u"{0} must be a collection, not {1}".format( + self.parent.name, type(collection).__name__ + ) + ) + yield value, source + + def set(self, value): + self.parent.set({self.key: value}) + + def add(self, value): + self.parent.add({self.key: value}) + + def root(self): + return self.parent.root() + + def set_redaction(self, path, flag): + self.parent.set_redaction((self.key,) + path, flag) + + def get_redactions(self): + return (kp[1:] for kp in self.parent.get_redactions() + if kp and kp[0] == self.key) + +# Main interface. + + +class Configuration(RootView): + def __init__(self, appname, modname=None, read=True, + loader=yaml_util.Loader): + """Create a configuration object by reading the + automatically-discovered config files for the application for a + given name. If `modname` is specified, it should be the import + name of a module whose package will be searched for a default + config file. (Otherwise, no defaults are used.) Pass `False` for + `read` to disable automatic reading of all discovered + configuration files. Use this when creating a configuration + object at module load time and then call the `read` method + later. Specify the Loader class as `loader`. + """ + super(Configuration, self).__init__([]) + self.appname = appname + self.modname = modname + self.loader = loader + + # Resolve default source location. We do this ahead of time to + # avoid unexpected problems if the working directory changes. + if self.modname: + self._package_path = util.find_package_path(self.modname) + else: + self._package_path = None + + self._env_var = '{0}DIR'.format(self.appname.upper()) + + if read: + self.read() + + def user_config_path(self): + """Points to the location of the user configuration. + + The file may not exist. + """ + return os.path.join(self.config_dir(), CONFIG_FILENAME) + + def _add_user_source(self): + """Add the configuration options from the YAML file in the + user's configuration directory (given by `config_dir`) if it + exists. + """ + filename = self.user_config_path() + self.add(YamlSource(filename, loader=self.loader, optional=True)) + + def _add_default_source(self): + """Add the package's default configuration settings. This looks + for a YAML file located inside the package for the module + `modname` if it was given. + """ + if self.modname: + if self._package_path: + filename = os.path.join(self._package_path, DEFAULT_FILENAME) + self.add(YamlSource(filename, loader=self.loader, + optional=True, default=True)) + + def read(self, user=True, defaults=True): + """Find and read the files for this configuration and set them + as the sources for this configuration. To disable either + discovered user configuration files or the in-package defaults, + set `user` or `defaults` to `False`. + """ + if user: + self._add_user_source() + if defaults: + self._add_default_source() + + def config_dir(self): + """Get the path to the user configuration directory. The + directory is guaranteed to exist as a postcondition (one may be + created if none exist). + + If the application's ``...DIR`` environment variable is set, it + is used as the configuration directory. Otherwise, + platform-specific standard configuration locations are searched + for a ``config.yaml`` file. If no configuration file is found, a + fallback path is used. + """ + # If environment variable is set, use it. + if self._env_var in os.environ: + appdir = os.environ[self._env_var] + appdir = os.path.abspath(os.path.expanduser(appdir)) + if os.path.isfile(appdir): + raise ConfigError(u'{0} must be a directory'.format( + self._env_var + )) + + else: + # Search platform-specific locations. If no config file is + # found, fall back to the first directory in the list. + configdirs = util.config_dirs() + for confdir in configdirs: + appdir = os.path.join(confdir, self.appname) + if os.path.isfile(os.path.join(appdir, CONFIG_FILENAME)): + break + else: + appdir = os.path.join(configdirs[0], self.appname) + + # Ensure that the directory exists. + try: + os.makedirs(appdir) + except OSError as e: + if e.errno != errno.EEXIST: + raise + + return appdir + + def set_file(self, filename, base_for_paths=False): + """Parses the file as YAML and inserts it into the configuration + sources with highest priority. + + :param filename: Filename of the YAML file to load. + :param base_for_paths: Indicates whether the directory containing the + YAML file will be used as the base directory for resolving relative + path values stored in the YAML file. Otherwise, by default, the + directory returned by `config_dir()` will be used as the base. + """ + self.set(YamlSource(filename, base_for_paths=base_for_paths, + loader=self.loader)) + + def set_env(self, prefix=None, sep='__'): + """Create a configuration overlay at the highest priority from + environment variables. + + After prefix matching and removal, environment variable names will be + converted to lowercase for use as keys within the configuration. If + there are nested keys, list-like dicts (ie, `{0: 'a', 1: 'b'}`) will + be converted into corresponding lists (ie, `['a', 'b']`). The values + of all environment variables will be parsed as YAML scalars using the + `self.loader` Loader class to ensure type conversion is consistent + with YAML file sources. Use the `EnvSource` class directly to load + environment variables using non-default behavior and to enable full + YAML parsing of values. + + :param prefix: The prefix to identify the environment variables to use. + Defaults to uppercased `self.appname` followed by an underscore. + :param sep: Separator within variable names to define nested keys. + """ + if prefix is None: + prefix = '{0}_'.format(self.appname.upper()) + self.set(EnvSource(prefix, sep=sep, loader=self.loader)) + + def dump(self, full=True, redact=False): + """Dump the Configuration object to a YAML file. + + The order of the keys is determined from the default + configuration file. All keys not in the default configuration + will be appended to the end of the file. + + :param full: Dump settings that don't differ from the defaults + as well + :param redact: Remove sensitive information (views with the `redact` + flag set) from the output + """ + if full: + out_dict = self.flatten(redact=redact) + else: + # Exclude defaults when flattening. + sources = [s for s in self.sources if not s.default] + temp_root = RootView(sources) + temp_root.redactions = self.redactions + out_dict = temp_root.flatten(redact=redact) + + yaml_out = yaml.dump(out_dict, Dumper=yaml_util.Dumper, + default_flow_style=None, indent=4, + width=1000) + + # Restore comments to the YAML text. + default_source = None + for source in self.sources: + if source.default: + default_source = source + break + if default_source and default_source.filename: + with open(default_source.filename, 'rb') as fp: + default_data = fp.read() + yaml_out = yaml_util.restore_yaml_comments( + yaml_out, default_data.decode('utf-8')) + + return yaml_out + + def reload(self): + """Reload all sources from the file system. + + This only affects sources that come from files (i.e., + `YamlSource` objects); other sources, such as dictionaries + inserted with `add` or `set`, will remain unchanged. + """ + for source in self.sources: + if isinstance(source, YamlSource): + source.load() + + +class LazyConfig(Configuration): + """A Configuration at reads files on demand when it is first + accessed. This is appropriate for using as a global config object at + the module level. + """ + def __init__(self, appname, modname=None): + super(LazyConfig, self).__init__(appname, modname, False) + self._materialized = False # Have we read the files yet? + self._lazy_prefix = [] # Pre-materialization calls to set(). + self._lazy_suffix = [] # Calls to add(). + + def read(self, user=True, defaults=True): + self._materialized = True + super(LazyConfig, self).read(user, defaults) + + def resolve(self): + if not self._materialized: + # Read files and unspool buffers. + self.read() + self.sources += self._lazy_suffix + self.sources[:0] = self._lazy_prefix + return super(LazyConfig, self).resolve() + + def add(self, value): + super(LazyConfig, self).add(value) + if not self._materialized: + # Buffer additions to end. + self._lazy_suffix += self.sources + del self.sources[:] + + def set(self, value): + super(LazyConfig, self).set(value) + if not self._materialized: + # Buffer additions to beginning. + self._lazy_prefix[:0] = self.sources + del self.sources[:] + + def clear(self): + """Remove all sources from this configuration.""" + super(LazyConfig, self).clear() + self._lazy_suffix = [] + self._lazy_prefix = [] + + +# "Validated" configuration views: experimental! diff --git a/libs/common/confuse/exceptions.py b/libs/common/confuse/exceptions.py new file mode 100644 index 00000000..782260ea --- /dev/null +++ b/libs/common/confuse/exceptions.py @@ -0,0 +1,56 @@ +from __future__ import division, absolute_import, print_function + +import yaml + +__all__ = [ + 'ConfigError', 'NotFoundError', 'ConfigValueError', 'ConfigTypeError', + 'ConfigTemplateError', 'ConfigReadError'] + +YAML_TAB_PROBLEM = "found character '\\t' that cannot start any token" + +# Exceptions. + + +class ConfigError(Exception): + """Base class for exceptions raised when querying a configuration. + """ + + +class NotFoundError(ConfigError): + """A requested value could not be found in the configuration trees. + """ + + +class ConfigValueError(ConfigError): + """The value in the configuration is illegal.""" + + +class ConfigTypeError(ConfigValueError): + """The value in the configuration did not match the expected type. + """ + + +class ConfigTemplateError(ConfigError): + """Base class for exceptions raised because of an invalid template. + """ + + +class ConfigReadError(ConfigError): + """A configuration source could not be read.""" + def __init__(self, name, reason=None): + self.name = name + self.reason = reason + + message = u'{0} could not be read'.format(name) + if (isinstance(reason, yaml.scanner.ScannerError) + and reason.problem == YAML_TAB_PROBLEM): + # Special-case error message for tab indentation in YAML markup. + message += u': found tab character at line {0}, column {1}'.format( + reason.problem_mark.line + 1, + reason.problem_mark.column + 1, + ) + elif reason: + # Generic error message uses exception's message. + message += u': {0}'.format(reason) + + super(ConfigReadError, self).__init__(message) diff --git a/libs/common/confuse/sources.py b/libs/common/confuse/sources.py new file mode 100644 index 00000000..2b0f53ba --- /dev/null +++ b/libs/common/confuse/sources.py @@ -0,0 +1,184 @@ +from __future__ import division, absolute_import, print_function + +from .util import BASESTRING, build_dict +from . import yaml_util +import os + + +class ConfigSource(dict): + """A dictionary augmented with metadata about the source of the + configuration. + """ + def __init__(self, value, filename=None, default=False, + base_for_paths=False): + """Create a configuration source from a dictionary. + + :param filename: The file with the data for this configuration source. + + :param default: Indicates whether this source provides the + application's default configuration settings. + + :param base_for_paths: Indicates whether the source file's directory + (i.e., the directory component of `self.filename`) should be used as + the base directory for resolving relative path values provided by this + source, instead of using the application's configuration directory. If + no `filename` is provided, `base_for_paths` will be treated as False. + See `templates.Filename` for details of the relative path resolution + behavior. + """ + super(ConfigSource, self).__init__(value) + if (filename is not None + and not isinstance(filename, BASESTRING)): + raise TypeError(u'filename must be a string or None') + self.filename = filename + self.default = default + self.base_for_paths = base_for_paths if filename is not None else False + + def __repr__(self): + return 'ConfigSource({0!r}, {1!r}, {2!r}, {3!r})'.format( + super(ConfigSource, self), + self.filename, + self.default, + self.base_for_paths, + ) + + @classmethod + def of(cls, value): + """Given either a dictionary or a `ConfigSource` object, return + a `ConfigSource` object. This lets a function accept either type + of object as an argument. + """ + if isinstance(value, ConfigSource): + return value + elif isinstance(value, dict): + return ConfigSource(value) + else: + raise TypeError(u'source value must be a dict') + + +class YamlSource(ConfigSource): + """A configuration data source that reads from a YAML file. + """ + + def __init__(self, filename=None, default=False, base_for_paths=False, + optional=False, loader=yaml_util.Loader): + """Create a YAML data source by reading data from a file. + + May raise a `ConfigReadError`. However, if `optional` is + enabled, this exception will not be raised in the case when the + file does not exist---instead, the source will be silently + empty. + """ + filename = os.path.abspath(filename) + super(YamlSource, self).__init__({}, filename, default, base_for_paths) + self.loader = loader + self.optional = optional + self.load() + + def load(self): + """Load YAML data from the source's filename. + """ + if self.optional and not os.path.isfile(self.filename): + value = {} + else: + value = yaml_util.load_yaml(self.filename, + loader=self.loader) or {} + self.update(value) + + +class EnvSource(ConfigSource): + """A configuration data source loaded from environment variables. + """ + def __init__(self, prefix, sep='__', lower=True, handle_lists=True, + parse_yaml_docs=False, loader=yaml_util.Loader): + """Create a configuration source from the environment. + + :param prefix: The prefix used to identify the environment variables + to be loaded into this configuration source. + + :param sep: Separator within variable names to define nested keys. + + :param lower: Indicates whether to convert variable names to lowercase + after prefix matching. + + :param handle_lists: If variables are split into nested keys, indicates + whether to search for sub-dicts with keys that are sequential + integers starting from 0 and convert those dicts to lists. + + :param parse_yaml_docs: Enable parsing the values of environment + variables as full YAML documents. By default, when False, values + are parsed only as YAML scalars. + + :param loader: PyYAML Loader class to use to parse YAML values. + """ + super(EnvSource, self).__init__({}, filename=None, default=False, + base_for_paths=False) + self.prefix = prefix + self.sep = sep + self.lower = lower + self.handle_lists = handle_lists + self.parse_yaml_docs = parse_yaml_docs + self.loader = loader + self.load() + + def load(self): + """Load configuration data from the environment. + """ + # Read config variables with prefix from the environment. + config_vars = {} + for var, value in os.environ.items(): + if var.startswith(self.prefix): + key = var[len(self.prefix):] + if self.lower: + key = key.lower() + if self.parse_yaml_docs: + # Parse the value as a YAML document, which will convert + # string representations of dicts and lists into the + # appropriate object (ie, '{foo: bar}' to {'foo': 'bar'}). + # Will raise a ConfigReadError if YAML parsing fails. + value = yaml_util.load_yaml_string(value, + 'env variable ' + var, + loader=self.loader) + else: + # Parse the value as a YAML scalar so that values are type + # converted using the same rules as the YAML Loader (ie, + # numeric string to int/float, 'true' to True, etc.). Will + # not raise a ConfigReadError. + value = yaml_util.parse_as_scalar(value, + loader=self.loader) + config_vars[key] = value + if self.sep: + # Build a nested dict, keeping keys with `None` values to allow + # environment variables to unset values from lower priority sources + config_vars = build_dict(config_vars, self.sep, keep_none=True) + if self.handle_lists: + for k, v in config_vars.items(): + config_vars[k] = self._convert_dict_lists(v) + self.update(config_vars) + + @classmethod + def _convert_dict_lists(cls, obj): + """Recursively search for dicts where all of the keys are integers + from 0 to the length of the dict, and convert them to lists. + """ + # We only deal with dictionaries + if not isinstance(obj, dict): + return obj + + # Recursively search values for additional dicts to convert to lists + for k, v in obj.items(): + obj[k] = cls._convert_dict_lists(v) + + try: + # Convert the keys to integers, mapping the ints back to the keys + int_to_key = {int(k): k for k in obj.keys()} + except (ValueError): + # Not all of the keys represent integers + return obj + try: + # For the integers from 0 to the length of the dict, try to create + # a list from the dict values using the integer to key mapping + return [obj[int_to_key[i]] for i in range(len(obj))] + except (KeyError): + # At least one integer within the range is not a key of the dict + return obj diff --git a/libs/common/confuse/templates.py b/libs/common/confuse/templates.py new file mode 100644 index 00000000..1b56b28a --- /dev/null +++ b/libs/common/confuse/templates.py @@ -0,0 +1,741 @@ +from __future__ import division, absolute_import, print_function + +import os +import re +import sys + +from . import util +from . import exceptions + +try: + import enum + SUPPORTS_ENUM = True +except ImportError: + SUPPORTS_ENUM = False + +try: + import pathlib + SUPPORTS_PATHLIB = True +except ImportError: + SUPPORTS_PATHLIB = False + +if sys.version_info >= (3, 3): + from collections import abc +else: + import collections as abc + + +REQUIRED = object() +"""A sentinel indicating that there is no default value and an exception +should be raised when the value is missing. +""" + + +class Template(object): + """A value template for configuration fields. + + The template works like a type and instructs Confuse about how to + interpret a deserialized YAML value. This includes type conversions, + providing a default value, and validating for errors. For example, a + filepath type might expand tildes and check that the file exists. + """ + def __init__(self, default=REQUIRED): + """Create a template with a given default value. + + If `default` is the sentinel `REQUIRED` (as it is by default), + then an error will be raised when a value is missing. Otherwise, + missing values will instead return `default`. + """ + self.default = default + + def __call__(self, view): + """Invoking a template on a view gets the view's value according + to the template. + """ + return self.value(view, self) + + def value(self, view, template=None): + """Get the value for a `ConfigView`. + + May raise a `NotFoundError` if the value is missing (and the + template requires it) or a `ConfigValueError` for invalid values. + """ + try: + value, _ = view.first() + return self.convert(value, view) + except exceptions.NotFoundError: + pass + + # Get default value, or raise if required. + return self.get_default_value(view.name) + + def get_default_value(self, key_name='default'): + """Get the default value to return when the value is missing. + + May raise a `NotFoundError` if the value is required. + """ + if not hasattr(self, 'default') or self.default is REQUIRED: + # The value is required. A missing value is an error. + raise exceptions.NotFoundError(u"{} not found".format(key_name)) + # The value is not required. + return self.default + + def convert(self, value, view): + """Convert the YAML-deserialized value to a value of the desired + type. + + Subclasses should override this to provide useful conversions. + May raise a `ConfigValueError` when the configuration is wrong. + """ + # Default implementation does no conversion. + return value + + def fail(self, message, view, type_error=False): + """Raise an exception indicating that a value cannot be + accepted. + + `type_error` indicates whether the error is due to a type + mismatch rather than a malformed value. In this case, a more + specific exception is raised. + """ + exc_class = ( + exceptions.ConfigTypeError if type_error + else exceptions.ConfigValueError) + raise exc_class(u'{0}: {1}'.format(view.name, message)) + + def __repr__(self): + return '{0}({1})'.format( + type(self).__name__, + '' if self.default is REQUIRED else repr(self.default), + ) + + +class Integer(Template): + """An integer configuration value template. + """ + def convert(self, value, view): + """Check that the value is an integer. Floats are rounded. + """ + if isinstance(value, int): + return value + elif isinstance(value, float): + return int(value) + else: + self.fail(u'must be a number', view, True) + + +class Number(Template): + """A numeric type: either an integer or a floating-point number. + """ + def convert(self, value, view): + """Check that the value is an int or a float. + """ + if isinstance(value, util.NUMERIC_TYPES): + return value + else: + self.fail( + u'must be numeric, not {0}'.format(type(value).__name__), + view, + True + ) + + +class MappingTemplate(Template): + """A template that uses a dictionary to specify other types for the + values for a set of keys and produce a validated `AttrDict`. + """ + def __init__(self, mapping): + """Create a template according to a dict (mapping). The + mapping's values should themselves either be Types or + convertible to Types. + """ + subtemplates = {} + for key, typ in mapping.items(): + subtemplates[key] = as_template(typ) + self.subtemplates = subtemplates + + def value(self, view, template=None): + """Get a dict with the same keys as the template and values + validated according to the value types. + """ + out = AttrDict() + for key, typ in self.subtemplates.items(): + out[key] = typ.value(view[key], self) + return out + + def __repr__(self): + return 'MappingTemplate({0})'.format(repr(self.subtemplates)) + + +class Sequence(Template): + """A template used to validate lists of similar items, + based on a given subtemplate. + """ + def __init__(self, subtemplate): + """Create a template for a list with items validated + on a given subtemplate. + """ + self.subtemplate = as_template(subtemplate) + + def value(self, view, template=None): + """Get a list of items validated against the template. + """ + out = [] + for item in view.sequence(): + out.append(self.subtemplate.value(item, self)) + return out + + def __repr__(self): + return 'Sequence({0})'.format(repr(self.subtemplate)) + + +class MappingValues(Template): + """A template used to validate mappings of similar items, + based on a given subtemplate applied to the values. + + All keys in the mapping are considered valid, but values + must pass validation by the subtemplate. Similar to the + Sequence template but for mappings. + """ + def __init__(self, subtemplate): + """Create a template for a mapping with variable keys + and item values validated on a given subtemplate. + """ + self.subtemplate = as_template(subtemplate) + + def value(self, view, template=None): + """Get a dict with the same keys as the view and the + value of each item validated against the subtemplate. + """ + out = {} + for key, item in view.items(): + out[key] = self.subtemplate.value(item, self) + return out + + def __repr__(self): + return 'MappingValues({0})'.format(repr(self.subtemplate)) + + +class String(Template): + """A string configuration value template. + """ + def __init__(self, default=REQUIRED, pattern=None, expand_vars=False): + """Create a template with the added optional `pattern` argument, + a regular expression string that the value should match. + """ + super(String, self).__init__(default) + self.pattern = pattern + self.expand_vars = expand_vars + if pattern: + self.regex = re.compile(pattern) + + def __repr__(self): + args = [] + + if self.default is not REQUIRED: + args.append(repr(self.default)) + + if self.pattern is not None: + args.append('pattern=' + repr(self.pattern)) + + return 'String({0})'.format(', '.join(args)) + + def convert(self, value, view): + """Check that the value is a string and matches the pattern. + """ + if not isinstance(value, util.BASESTRING): + self.fail(u'must be a string', view, True) + + if self.pattern and not self.regex.match(value): + self.fail( + u"must match the pattern {0}".format(self.pattern), + view + ) + + if self.expand_vars: + return os.path.expandvars(value) + else: + return value + + +class Choice(Template): + """A template that permits values from a sequence of choices. + """ + def __init__(self, choices, default=REQUIRED): + """Create a template that validates any of the values from the + iterable `choices`. + + If `choices` is a map, then the corresponding value is emitted. + Otherwise, the value itself is emitted. + + If `choices` is a `Enum`, then the enum entry with the value is + emitted. + """ + super(Choice, self).__init__(default) + self.choices = choices + + def convert(self, value, view): + """Ensure that the value is among the choices (and remap if the + choices are a mapping). + """ + if (SUPPORTS_ENUM and isinstance(self.choices, type) + and issubclass(self.choices, enum.Enum)): + try: + return self.choices(value) + except ValueError: + self.fail( + u'must be one of {0!r}, not {1!r}'.format( + [c.value for c in self.choices], value + ), + view + ) + + if value not in self.choices: + self.fail( + u'must be one of {0!r}, not {1!r}'.format( + list(self.choices), value + ), + view + ) + + if isinstance(self.choices, abc.Mapping): + return self.choices[value] + else: + return value + + def __repr__(self): + return 'Choice({0!r})'.format(self.choices) + + +class OneOf(Template): + """A template that permits values complying to one of the given templates. + """ + def __init__(self, allowed, default=REQUIRED): + super(OneOf, self).__init__(default) + self.allowed = list(allowed) + + def __repr__(self): + args = [] + + if self.allowed is not None: + args.append('allowed=' + repr(self.allowed)) + + if self.default is not REQUIRED: + args.append(repr(self.default)) + + return 'OneOf({0})'.format(', '.join(args)) + + def value(self, view, template): + self.template = template + return super(OneOf, self).value(view, template) + + def convert(self, value, view): + """Ensure that the value follows at least one template. + """ + is_mapping = isinstance(self.template, MappingTemplate) + + for candidate in self.allowed: + try: + if is_mapping: + if isinstance(candidate, Filename) and \ + candidate.relative_to: + next_template = candidate.template_with_relatives( + view, + self.template + ) + + next_template.subtemplates[view.key] = as_template( + candidate + ) + else: + next_template = MappingTemplate({view.key: candidate}) + + return view.parent.get(next_template)[view.key] + else: + return view.get(candidate) + except exceptions.ConfigTemplateError: + raise + except exceptions.ConfigError: + pass + except ValueError as exc: + raise exceptions.ConfigTemplateError(exc) + + self.fail( + u'must be one of {0}, not {1}'.format( + repr(self.allowed), repr(value) + ), + view + ) + + +class StrSeq(Template): + """A template for values that are lists of strings. + + Validates both actual YAML string lists and single strings. Strings + can optionally be split on whitespace. + """ + def __init__(self, split=True, default=REQUIRED): + """Create a new template. + + `split` indicates whether, when the underlying value is a single + string, it should be split on whitespace. Otherwise, the + resulting value is a list containing a single string. + """ + super(StrSeq, self).__init__(default) + self.split = split + + def _convert_value(self, x, view): + if isinstance(x, util.STRING): + return x + elif isinstance(x, bytes): + return x.decode('utf-8', 'ignore') + else: + self.fail(u'must be a list of strings', view, True) + + def convert(self, value, view): + if isinstance(value, bytes): + value = value.decode('utf-8', 'ignore') + + if isinstance(value, util.STRING): + if self.split: + value = value.split() + else: + value = [value] + else: + try: + value = list(value) + except TypeError: + self.fail(u'must be a whitespace-separated string or a list', + view, True) + return [self._convert_value(v, view) for v in value] + + +class Pairs(StrSeq): + """A template for ordered key-value pairs. + + This can either be given with the same syntax as for `StrSeq` (i.e. without + values), or as a list of strings and/or single-element mappings such as:: + + - key: value + - [key, value] + - key + + The result is a list of two-element tuples. If no value is provided, the + `default_value` will be returned as the second element. + """ + + def __init__(self, default_value=None): + """Create a new template. + + `default` is the dictionary value returned for items that are not + a mapping, but a single string. + """ + super(Pairs, self).__init__(split=True) + self.default_value = default_value + + def _convert_value(self, x, view): + try: + return (super(Pairs, self)._convert_value(x, view), + self.default_value) + except exceptions.ConfigTypeError: + if isinstance(x, abc.Mapping): + if len(x) != 1: + self.fail(u'must be a single-element mapping', view, True) + k, v = util.iter_first(x.items()) + elif isinstance(x, abc.Sequence): + if len(x) != 2: + self.fail(u'must be a two-element list', view, True) + k, v = x + else: + # Is this even possible? -> Likely, if some !directive cause + # YAML to parse this to some custom type. + self.fail(u'must be a single string, mapping, or a list' + u'' + str(x), + view, True) + return (super(Pairs, self)._convert_value(k, view), + super(Pairs, self)._convert_value(v, view)) + + +class Filename(Template): + """A template that validates strings as filenames. + + Filenames are returned as absolute, tilde-free paths. + + Relative paths are relative to the template's `cwd` argument + when it is specified. Otherwise, if the paths come from a file, + they will be relative to the configuration directory (see the + `config_dir` method) by default or to the base directory of the + config file if either the source has `base_for_paths` set to True + or the template has `in_source_dir` set to True. Paths from sources + without a file are relative to the current working directory. This + helps attain the expected behavior when using command-line options. + """ + def __init__(self, default=REQUIRED, cwd=None, relative_to=None, + in_app_dir=False, in_source_dir=False): + """`relative_to` is the name of a sibling value that is + being validated at the same time. + + `in_app_dir` indicates whether the path should be resolved + inside the application's config directory (even when the setting + does not come from a file). + + `in_source_dir` indicates whether the path should be resolved + relative to the directory containing the source file, if there is + one, taking precedence over the application's config directory. + """ + super(Filename, self).__init__(default) + self.cwd = cwd + self.relative_to = relative_to + self.in_app_dir = in_app_dir + self.in_source_dir = in_source_dir + + def __repr__(self): + args = [] + + if self.default is not REQUIRED: + args.append(repr(self.default)) + + if self.cwd is not None: + args.append('cwd=' + repr(self.cwd)) + + if self.relative_to is not None: + args.append('relative_to=' + repr(self.relative_to)) + + if self.in_app_dir: + args.append('in_app_dir=True') + + if self.in_source_dir: + args.append('in_source_dir=True') + + return 'Filename({0})'.format(', '.join(args)) + + def resolve_relative_to(self, view, template): + if not isinstance(template, (abc.Mapping, MappingTemplate)): + # disallow config.get(Filename(relative_to='foo')) + raise exceptions.ConfigTemplateError( + u'relative_to may only be used when getting multiple values.' + ) + + elif self.relative_to == view.key: + raise exceptions.ConfigTemplateError( + u'{0} is relative to itself'.format(view.name) + ) + + elif self.relative_to not in view.parent.keys(): + # self.relative_to is not in the config + self.fail( + ( + u'needs sibling value "{0}" to expand relative path' + ).format(self.relative_to), + view + ) + + old_template = {} + old_template.update(template.subtemplates) + + # save time by skipping MappingTemplate's init loop + next_template = MappingTemplate({}) + next_relative = self.relative_to + + # gather all the needed templates and nothing else + while next_relative is not None: + try: + # pop to avoid infinite loop because of recursive + # relative paths + rel_to_template = old_template.pop(next_relative) + except KeyError: + if next_relative in template.subtemplates: + # we encountered this config key previously + raise exceptions.ConfigTemplateError(( + u'{0} and {1} are recursively relative' + ).format(view.name, self.relative_to)) + else: + raise exceptions.ConfigTemplateError(( + u'missing template for {0}, needed to expand {1}\'s' + u'relative path' + ).format(self.relative_to, view.name)) + + next_template.subtemplates[next_relative] = rel_to_template + next_relative = rel_to_template.relative_to + + return view.parent.get(next_template)[self.relative_to] + + def value(self, view, template=None): + try: + path, source = view.first() + except exceptions.NotFoundError: + return self.get_default_value(view.name) + + if not isinstance(path, util.BASESTRING): + self.fail( + u'must be a filename, not {0}'.format(type(path).__name__), + view, + True + ) + path = os.path.expanduser(util.STRING(path)) + + if not os.path.isabs(path): + if self.cwd is not None: + # relative to the template's argument + path = os.path.join(self.cwd, path) + + elif self.relative_to is not None: + path = os.path.join( + self.resolve_relative_to(view, template), + path, + ) + + elif ((source.filename and self.in_source_dir) + or (source.base_for_paths and not self.in_app_dir)): + # relative to the directory the source file is in. + path = os.path.join(os.path.dirname(source.filename), path) + + elif source.filename or self.in_app_dir: + # From defaults: relative to the app's directory. + path = os.path.join(view.root().config_dir(), path) + + return os.path.abspath(path) + + +class Path(Filename): + """A template that validates strings as `pathlib.Path` objects. + + Filenames are parsed equivalent to the `Filename` template and then + converted to `pathlib.Path` objects. + + For Python 2 it returns the original path as returned by the `Filename` + template. + """ + def value(self, view, template=None): + value = super(Path, self).value(view, template) + if value is None: + return + import pathlib + return pathlib.Path(value) + + +class Optional(Template): + """A template that makes a subtemplate optional. + + If the value is present and not null, it must validate against the + subtemplate. However, if the value is null or missing, the template will + still validate, returning a default value. If `allow_missing` is False, + the template will not allow missing values while still permitting null. + """ + + def __init__(self, subtemplate, default=None, allow_missing=True): + self.subtemplate = as_template(subtemplate) + if default is None: + # When no default is passed, try to use the subtemplate's + # default value as the default for this template + try: + default = self.subtemplate.get_default_value() + except exceptions.NotFoundError: + pass + self.default = default + self.allow_missing = allow_missing + + def value(self, view, template=None): + try: + value, _ = view.first() + except exceptions.NotFoundError: + if self.allow_missing: + # Value is missing but not required + return self.default + # Value must be present even though it can be null. Raise an error. + raise exceptions.NotFoundError(u'{} not found'.format(view.name)) + + if value is None: + # None (ie, null) is always a valid value + return self.default + return self.subtemplate.value(view, self) + + def __repr__(self): + return 'Optional({0}, {1}, allow_missing={2})'.format( + repr(self.subtemplate), + repr(self.default), + self.allow_missing, + ) + + +class TypeTemplate(Template): + """A simple template that checks that a value is an instance of a + desired Python type. + """ + def __init__(self, typ, default=REQUIRED): + """Create a template that checks that the value is an instance + of `typ`. + """ + super(TypeTemplate, self).__init__(default) + self.typ = typ + + def convert(self, value, view): + if not isinstance(value, self.typ): + self.fail( + u'must be a {0}, not {1}'.format( + self.typ.__name__, + type(value).__name__, + ), + view, + True + ) + return value + + +class AttrDict(dict): + """A `dict` subclass that can be accessed via attributes (dot + notation) for convenience. + """ + def __getattr__(self, key): + if key in self: + return self[key] + else: + raise AttributeError(key) + + def __setattr__(self, key, value): + self[key] = value + + +def as_template(value): + """Convert a simple "shorthand" Python value to a `Template`. + """ + if isinstance(value, Template): + # If it's already a Template, pass it through. + return value + elif isinstance(value, abc.Mapping): + # Dictionaries work as templates. + return MappingTemplate(value) + elif value is int: + return Integer() + elif isinstance(value, int): + return Integer(value) + elif isinstance(value, type) and issubclass(value, util.BASESTRING): + return String() + elif isinstance(value, util.BASESTRING): + return String(value) + elif isinstance(value, set): + # convert to list to avoid hash related problems + return Choice(list(value)) + elif (SUPPORTS_ENUM and isinstance(value, type) + and issubclass(value, enum.Enum)): + return Choice(value) + elif isinstance(value, list): + return OneOf(value) + elif value is float: + return Number() + elif isinstance(value, float): + return Number(value) + elif SUPPORTS_PATHLIB and isinstance(value, pathlib.PurePath): + return Path(value) + elif value is None: + return Template(None) + elif value is REQUIRED: + return Template() + elif value is dict: + return TypeTemplate(abc.Mapping) + elif value is list: + return TypeTemplate(abc.Sequence) + elif isinstance(value, type): + return TypeTemplate(value) + else: + raise ValueError(u'cannot convert to template: {0!r}'.format(value)) diff --git a/libs/common/confuse/util.py b/libs/common/confuse/util.py new file mode 100644 index 00000000..70bd4569 --- /dev/null +++ b/libs/common/confuse/util.py @@ -0,0 +1,186 @@ +from __future__ import division, absolute_import, print_function + +import os +import sys +import argparse +import optparse +import platform +import pkgutil + + +PY3 = sys.version_info[0] == 3 +STRING = str if PY3 else unicode # noqa: F821 +BASESTRING = str if PY3 else basestring # noqa: F821 +NUMERIC_TYPES = (int, float) if PY3 else (int, float, long) # noqa: F821 + + +UNIX_DIR_FALLBACK = '~/.config' +WINDOWS_DIR_VAR = 'APPDATA' +WINDOWS_DIR_FALLBACK = '~\\AppData\\Roaming' +MAC_DIR = '~/Library/Application Support' + + +def iter_first(sequence): + """Get the first element from an iterable or raise a ValueError if + the iterator generates no values. + """ + it = iter(sequence) + try: + return next(it) + except StopIteration: + raise ValueError() + + +def namespace_to_dict(obj): + """If obj is argparse.Namespace or optparse.Values we'll return + a dict representation of it, else return the original object. + + Redefine this method if using other parsers. + + :param obj: * + :return: + :rtype: dict or * + """ + if isinstance(obj, (argparse.Namespace, optparse.Values)): + return vars(obj) + return obj + + +def build_dict(obj, sep='', keep_none=False): + """Recursively builds a dictionary from an argparse.Namespace, + optparse.Values, or dict object. + + Additionally, if `sep` is a non-empty string, the keys will be split + by `sep` and expanded into a nested dict. Keys with a `None` value + are dropped by default to avoid unsetting options but can be kept + by setting `keep_none` to `True`. + + :param obj: Namespace, Values, or dict to iterate over. Other + values will simply be returned. + :type obj: argparse.Namespace or optparse.Values or dict or * + :param sep: Separator to use for splitting properties/keys of `obj` + for expansion into nested dictionaries. + :type sep: str + :param keep_none: Whether to keep keys whose value is `None`. + :type keep_none: bool + :return: A new dictionary or the value passed if obj was not a + dict, Namespace, or Values. + :rtype: dict or * + """ + # We expect our root object to be a dict, but it may come in as + # a namespace + obj = namespace_to_dict(obj) + # We only deal with dictionaries + if not isinstance(obj, dict): + return obj + + # Get keys iterator + keys = obj.keys() if PY3 else obj.iterkeys() + if sep: + # Splitting keys by `sep` needs sorted keys to prevent parents + # from clobbering children + keys = sorted(list(keys)) + + output = {} + for key in keys: + value = obj[key] + if value is None and not keep_none: # Avoid unset options. + continue + + save_to = output + result = build_dict(value, sep, keep_none) + if sep: + # Split keys by `sep` as this signifies nesting + split = key.split(sep) + if len(split) > 1: + # The last index will be the key we assign result to + key = split.pop() + # Build the dict tree if needed and change where + # we're saving to + for child_key in split: + if child_key in save_to and \ + isinstance(save_to[child_key], dict): + save_to = save_to[child_key] + else: + # Clobber or create + save_to[child_key] = {} + save_to = save_to[child_key] + + # Save + if key in save_to: + save_to[key].update(result) + else: + save_to[key] = result + return output + + +# Config file paths, including platform-specific paths and in-package +# defaults. + +def find_package_path(name): + """Returns the path to the package containing the named module or + None if the path could not be identified (e.g., if + ``name == "__main__"``). + """ + # Based on get_root_path from Flask by Armin Ronacher. + loader = pkgutil.get_loader(name) + if loader is None or name == '__main__': + return None + + if hasattr(loader, 'get_filename'): + filepath = loader.get_filename(name) + else: + # Fall back to importing the specified module. + __import__(name) + filepath = sys.modules[name].__file__ + + return os.path.dirname(os.path.abspath(filepath)) + + +def xdg_config_dirs(): + """Returns a list of paths taken from the XDG_CONFIG_DIRS + and XDG_CONFIG_HOME environment varibables if they exist + """ + paths = [] + if 'XDG_CONFIG_HOME' in os.environ: + paths.append(os.environ['XDG_CONFIG_HOME']) + if 'XDG_CONFIG_DIRS' in os.environ: + paths.extend(os.environ['XDG_CONFIG_DIRS'].split(':')) + else: + paths.append('/etc/xdg') + paths.append('/etc') + return paths + + +def config_dirs(): + """Return a platform-specific list of candidates for user + configuration directories on the system. + + The candidates are in order of priority, from highest to lowest. The + last element is the "fallback" location to be used when no + higher-priority config file exists. + """ + paths = [] + + if platform.system() == 'Darwin': + paths.append(UNIX_DIR_FALLBACK) + paths.append(MAC_DIR) + paths.extend(xdg_config_dirs()) + + elif platform.system() == 'Windows': + paths.append(WINDOWS_DIR_FALLBACK) + if WINDOWS_DIR_VAR in os.environ: + paths.append(os.environ[WINDOWS_DIR_VAR]) + + else: + # Assume Unix. + paths.append(UNIX_DIR_FALLBACK) + paths.extend(xdg_config_dirs()) + + # Expand and deduplicate paths. + out = [] + for path in paths: + path = os.path.abspath(os.path.expanduser(path)) + if path not in out: + out.append(path) + return out diff --git a/libs/common/confuse/yaml_util.py b/libs/common/confuse/yaml_util.py new file mode 100644 index 00000000..2cb4b529 --- /dev/null +++ b/libs/common/confuse/yaml_util.py @@ -0,0 +1,228 @@ +from __future__ import division, absolute_import, print_function + +from collections import OrderedDict +import yaml +from .exceptions import ConfigReadError +from .util import BASESTRING + +# YAML loading. + + +class Loader(yaml.SafeLoader): + """A customized YAML loader. This loader deviates from the official + YAML spec in a few convenient ways: + + - All strings as are Unicode objects. + - All maps are OrderedDicts. + - Strings can begin with % without quotation. + """ + # All strings should be Unicode objects, regardless of contents. + def _construct_unicode(self, node): + return self.construct_scalar(node) + + # Use ordered dictionaries for every YAML map. + # From https://gist.github.com/844388 + def construct_yaml_map(self, node): + data = OrderedDict() + yield data + value = self.construct_mapping(node) + data.update(value) + + def construct_mapping(self, node, deep=False): + if isinstance(node, yaml.MappingNode): + self.flatten_mapping(node) + else: + raise yaml.constructor.ConstructorError( + None, None, + u'expected a mapping node, but found %s' % node.id, + node.start_mark + ) + + mapping = OrderedDict() + for key_node, value_node in node.value: + key = self.construct_object(key_node, deep=deep) + try: + hash(key) + except TypeError as exc: + raise yaml.constructor.ConstructorError( + u'while constructing a mapping', + node.start_mark, 'found unacceptable key (%s)' % exc, + key_node.start_mark + ) + value = self.construct_object(value_node, deep=deep) + mapping[key] = value + return mapping + + # Allow bare strings to begin with %. Directives are still detected. + def check_plain(self): + plain = super(Loader, self).check_plain() + return plain or self.peek() == '%' + + @staticmethod + def add_constructors(loader): + """Modify a PyYAML Loader class to add extra constructors for strings + and maps. Call this method on a custom Loader class to make it behave + like Confuse's own Loader + """ + loader.add_constructor('tag:yaml.org,2002:str', + Loader._construct_unicode) + loader.add_constructor('tag:yaml.org,2002:map', + Loader.construct_yaml_map) + loader.add_constructor('tag:yaml.org,2002:omap', + Loader.construct_yaml_map) + + +Loader.add_constructors(Loader) + + +def load_yaml(filename, loader=Loader): + """Read a YAML document from a file. If the file cannot be read or + parsed, a ConfigReadError is raised. + loader is the PyYAML Loader class to use to parse the YAML. By default, + this is Confuse's own Loader class, which is like SafeLoader with + extra constructors. + """ + try: + with open(filename, 'rb') as f: + return yaml.load(f, Loader=loader) + except (IOError, yaml.error.YAMLError) as exc: + raise ConfigReadError(filename, exc) + + +def load_yaml_string(yaml_string, name, loader=Loader): + """Read a YAML document from a string. If the string cannot be parsed, + a ConfigReadError is raised. + `yaml_string` is a string to be parsed as a YAML document. + `name` is the name to use in error messages. + `loader` is the PyYAML Loader class to use to parse the YAML. By default, + this is Confuse's own Loader class, which is like SafeLoader with + extra constructors. + """ + try: + return yaml.load(yaml_string, Loader=loader) + except yaml.error.YAMLError as exc: + raise ConfigReadError(name, exc) + + +def parse_as_scalar(value, loader=Loader): + """Parse a value as if it were a YAML scalar to perform type conversion + that is consistent with YAML documents. + `value` should be a string. Non-string inputs or strings that raise YAML + errors will be returned unchanged. + `Loader` is the PyYAML Loader class to use for parsing, defaulting to + Confuse's own Loader class. + + Examples with the default Loader: + - '1' will return 1 as an integer + - '1.0' will return 1 as a float + - 'true' will return True + - The empty string '' will return None + """ + # We only deal with strings + if not isinstance(value, BASESTRING): + return value + try: + loader = loader('') + tag = loader.resolve(yaml.ScalarNode, value, (True, False)) + node = yaml.ScalarNode(tag, value) + return loader.construct_object(node) + except yaml.error.YAMLError: + # Fallback to returning the value unchanged + return value + + +# YAML dumping. + +class Dumper(yaml.SafeDumper): + """A PyYAML Dumper that represents OrderedDicts as ordinary mappings + (in order, of course). + """ + # From http://pyyaml.org/attachment/ticket/161/use_ordered_dict.py + def represent_mapping(self, tag, mapping, flow_style=None): + value = [] + node = yaml.MappingNode(tag, value, flow_style=flow_style) + if self.alias_key is not None: + self.represented_objects[self.alias_key] = node + best_style = False + if hasattr(mapping, 'items'): + mapping = list(mapping.items()) + for item_key, item_value in mapping: + node_key = self.represent_data(item_key) + node_value = self.represent_data(item_value) + if not (isinstance(node_key, yaml.ScalarNode) + and not node_key.style): + best_style = False + if not (isinstance(node_value, yaml.ScalarNode) + and not node_value.style): + best_style = False + value.append((node_key, node_value)) + if flow_style is None: + if self.default_flow_style is not None: + node.flow_style = self.default_flow_style + else: + node.flow_style = best_style + return node + + def represent_list(self, data): + """If a list has less than 4 items, represent it in inline style + (i.e. comma separated, within square brackets). + """ + node = super(Dumper, self).represent_list(data) + length = len(data) + if self.default_flow_style is None and length < 4: + node.flow_style = True + elif self.default_flow_style is None: + node.flow_style = False + return node + + def represent_bool(self, data): + """Represent bool as 'yes' or 'no' instead of 'true' or 'false'. + """ + if data: + value = u'yes' + else: + value = u'no' + return self.represent_scalar('tag:yaml.org,2002:bool', value) + + def represent_none(self, data): + """Represent a None value with nothing instead of 'none'. + """ + return self.represent_scalar('tag:yaml.org,2002:null', '') + + +Dumper.add_representer(OrderedDict, Dumper.represent_dict) +Dumper.add_representer(bool, Dumper.represent_bool) +Dumper.add_representer(type(None), Dumper.represent_none) +Dumper.add_representer(list, Dumper.represent_list) + + +def restore_yaml_comments(data, default_data): + """Scan default_data for comments (we include empty lines in our + definition of comments) and place them before the same keys in data. + Only works with comments that are on one or more own lines, i.e. + not next to a yaml mapping. + """ + comment_map = dict() + default_lines = iter(default_data.splitlines()) + for line in default_lines: + if not line: + comment = "\n" + elif line.startswith("#"): + comment = "{0}\n".format(line) + else: + continue + while True: + line = next(default_lines) + if line and not line.startswith("#"): + break + comment += "{0}\n".format(line) + key = line.split(':')[0].strip() + comment_map[key] = comment + out_lines = iter(data.splitlines()) + out_data = "" + for line in out_lines: + key = line.split(':')[0].strip() + if key in comment_map: + out_data += comment_map[key] + out_data += "{0}\n".format(line) + return out_data diff --git a/libs/common/jellyfish/__init__.py b/libs/common/jellyfish/__init__.py index ca124f2a..b34e4dee 100644 --- a/libs/common/jellyfish/__init__.py +++ b/libs/common/jellyfish/__init__.py @@ -1,6 +1,28 @@ +import warnings + try: - from .cjellyfish import * # noqa + from .cjellyfish import * # noqa + library = "C" except ImportError: - from ._jellyfish import * # noqa + from ._jellyfish import * # noqa + library = "Python" + + +def jaro_winkler(s1, s2, long_tolerance=False): + warnings.warn( + "the name 'jaro_winkler' is deprecated and will be removed in jellyfish 1.0, " + "for the same functionality please use jaro_winkler_similarity", + DeprecationWarning, + ) + return jaro_winkler_similarity(s1, s2, long_tolerance) # noqa + + +def jaro_distance(s1, s2): + warnings.warn( + "the jaro_distance function incorrectly returns the jaro similarity, " + "replace your usage with jaro_similarity before 1.0", + DeprecationWarning, + ) + return jaro_similarity(s1, s2) # noqa diff --git a/libs/common/jellyfish/__init__.pyi b/libs/common/jellyfish/__init__.pyi new file mode 100644 index 00000000..78a58da2 --- /dev/null +++ b/libs/common/jellyfish/__init__.pyi @@ -0,0 +1,11 @@ +def levenshtein_distance(s1: str, s2: str) -> int: ... +def jaro_similarity(s1: str, s2: str) -> float: ... +def jaro_winkler_similarity(s1: str, s2: str, long_tolerance: bool = ...) -> float: ... +def damerau_levenshtein_distance(s1: str, s2: str) -> int: ... +def soundex(s: str) -> str: ... +def hamming_distance(s1: str, s2: str) -> int: ... +def nysiis(s: str) -> str: ... +def match_rating_codex(s: str) -> str: ... +def match_rating_comparison(s1: str, s2: str) -> bool: ... +def metaphone(s: str) -> str: ... +def porter_stem(s: str) -> str: ... diff --git a/libs/common/jellyfish/_jellyfish.py b/libs/common/jellyfish/_jellyfish.py index 05dade4f..8acf3f52 100644 --- a/libs/common/jellyfish/_jellyfish.py +++ b/libs/common/jellyfish/_jellyfish.py @@ -1,18 +1,16 @@ import unicodedata from collections import defaultdict -from .compat import _range, _zip_longest, IS_PY3 +from itertools import zip_longest from .porter import Stemmer def _normalize(s): - return unicodedata.normalize('NFKD', s) + return unicodedata.normalize("NFKD", s) def _check_type(s): - if IS_PY3 and not isinstance(s, str): - raise TypeError('expected str or unicode, got %s' % type(s).__name__) - elif not IS_PY3 and not isinstance(s, unicode): - raise TypeError('expected unicode, got %s' % type(s).__name__) + if not isinstance(s, str): + raise TypeError("expected str or unicode, got %s" % type(s).__name__) def levenshtein_distance(s1, s2): @@ -21,53 +19,54 @@ def levenshtein_distance(s1, s2): if s1 == s2: return 0 - rows = len(s1)+1 - cols = len(s2)+1 + rows = len(s1) + 1 + cols = len(s2) + 1 if not s1: - return cols-1 + return cols - 1 if not s2: - return rows-1 + return rows - 1 prev = None cur = range(cols) - for r in _range(1, rows): - prev, cur = cur, [r] + [0]*(cols-1) - for c in _range(1, cols): + for r in range(1, rows): + prev, cur = cur, [r] + [0] * (cols - 1) + for c in range(1, cols): deletion = prev[c] + 1 - insertion = cur[c-1] + 1 - edit = prev[c-1] + (0 if s1[r-1] == s2[c-1] else 1) + insertion = cur[c - 1] + 1 + edit = prev[c - 1] + (0 if s1[r - 1] == s2[c - 1] else 1) cur[c] = min(edit, deletion, insertion) return cur[-1] -def _jaro_winkler(ying, yang, long_tolerance, winklerize): - _check_type(ying) - _check_type(yang) +def _jaro_winkler(s1, s2, long_tolerance, winklerize): + _check_type(s1) + _check_type(s2) - ying_len = len(ying) - yang_len = len(yang) + s1_len = len(s1) + s2_len = len(s2) - if not ying_len or not yang_len: + if not s1_len or not s2_len: return 0.0 - min_len = max(ying_len, yang_len) - search_range = (min_len // 2) - 1 + min_len = min(s1_len, s2_len) + search_range = max(s1_len, s2_len) + search_range = (search_range // 2) - 1 if search_range < 0: search_range = 0 - ying_flags = [False]*ying_len - yang_flags = [False]*yang_len + s1_flags = [False] * s1_len + s2_flags = [False] * s2_len # looking only within search range, count & flag matched pairs common_chars = 0 - for i, ying_ch in enumerate(ying): - low = i - search_range if i > search_range else 0 - hi = i + search_range if i + search_range < yang_len else yang_len - 1 - for j in _range(low, hi+1): - if not yang_flags[j] and yang[j] == ying_ch: - ying_flags[i] = yang_flags[j] = True + for i, s1_ch in enumerate(s1): + low = max(0, i - search_range) + hi = min(i + search_range, s2_len - 1) + for j in range(low, hi + 1): + if not s2_flags[j] and s2[j] == s1_ch: + s1_flags[i] = s2_flags[j] = True common_chars += 1 break @@ -77,27 +76,32 @@ def _jaro_winkler(ying, yang, long_tolerance, winklerize): # count transpositions k = trans_count = 0 - for i, ying_f in enumerate(ying_flags): - if ying_f: - for j in _range(k, yang_len): - if yang_flags[j]: + for i, s1_f in enumerate(s1_flags): + if s1_f: + for j in range(k, s2_len): + if s2_flags[j]: k = j + 1 break - if ying[i] != yang[j]: + if s1[i] != s2[j]: trans_count += 1 - trans_count /= 2 + trans_count //= 2 # adjust for similarities in nonmatched characters common_chars = float(common_chars) - weight = ((common_chars/ying_len + common_chars/yang_len + - (common_chars-trans_count) / common_chars)) / 3 + weight = ( + ( + common_chars / s1_len + + common_chars / s2_len + + (common_chars - trans_count) / common_chars + ) + ) / 3 # winkler modification: continue to boost if strings are similar - if winklerize and weight > 0.7 and ying_len > 3 and yang_len > 3: + if winklerize and weight > 0.7: # adjust for up to first 4 chars in common j = min(min_len, 4) i = 0 - while i < j and ying[i] == yang[i] and ying[i]: + while i < j and s1[i] == s2[i]: i += 1 if i: weight += i * 0.1 * (1.0 - weight) @@ -105,13 +109,27 @@ def _jaro_winkler(ying, yang, long_tolerance, winklerize): # optionally adjust for long strings # after agreeing beginning chars, at least two or more must agree and # agreed characters must be > half of remaining characters - if (long_tolerance and min_len > 4 and common_chars > i+1 and - 2 * common_chars >= min_len + i): - weight += ((1.0 - weight) * (float(common_chars-i-1) / float(ying_len+yang_len-i*2+2))) + if ( + long_tolerance + and min_len > 4 + and common_chars > i + 1 + and 2 * common_chars >= min_len + i + ): + weight += (1.0 - weight) * ( + float(common_chars - i - 1) / float(s1_len + s2_len - i * 2 + 2) + ) return weight +def jaro_similarity(s1, s2): + return _jaro_winkler(s1, s2, False, False) # noqa + + +def jaro_winkler_similarity(s1, s2, long_tolerance=False): + return _jaro_winkler(s1, s2, long_tolerance, True) # noqa + + def damerau_levenshtein_distance(s1, s2): _check_type(s1) _check_type(s2) @@ -124,41 +142,35 @@ def damerau_levenshtein_distance(s1, s2): da = defaultdict(int) # distance matrix - score = [[0]*(len2+2) for x in _range(len1+2)] + score = [[0] * (len2 + 2) for x in range(len1 + 2)] score[0][0] = infinite - for i in _range(0, len1+1): - score[i+1][0] = infinite - score[i+1][1] = i - for i in _range(0, len2+1): - score[0][i+1] = infinite - score[1][i+1] = i + for i in range(0, len1 + 1): + score[i + 1][0] = infinite + score[i + 1][1] = i + for i in range(0, len2 + 1): + score[0][i + 1] = infinite + score[1][i + 1] = i - for i in _range(1, len1+1): + for i in range(1, len1 + 1): db = 0 - for j in _range(1, len2+1): - i1 = da[s2[j-1]] + for j in range(1, len2 + 1): + i1 = da[s2[j - 1]] j1 = db cost = 1 - if s1[i-1] == s2[j-1]: + if s1[i - 1] == s2[j - 1]: cost = 0 db = j - score[i+1][j+1] = min(score[i][j] + cost, - score[i+1][j] + 1, - score[i][j+1] + 1, - score[i1][j1] + (i-i1-1) + 1 + (j-j1-1)) - da[s1[i-1]] = i + score[i + 1][j + 1] = min( + score[i][j] + cost, + score[i + 1][j] + 1, + score[i][j + 1] + 1, + score[i1][j1] + (i - i1 - 1) + 1 + (j - j1 - 1), + ) + da[s1[i - 1]] = i - return score[len1+1][len2+1] - - -def jaro_distance(s1, s2): - return _jaro_winkler(s1, s2, False, False) - - -def jaro_winkler(s1, s2, long_tolerance=False): - return _jaro_winkler(s1, s2, long_tolerance, True) + return score[len1 + 1][len2 + 1] def soundex(s): @@ -166,21 +178,23 @@ def soundex(s): _check_type(s) if not s: - return '' + return "" s = _normalize(s) s = s.upper() - replacements = (('BFPV', '1'), - ('CGJKQSXZ', '2'), - ('DT', '3'), - ('L', '4'), - ('MN', '5'), - ('R', '6')) + replacements = ( + ("BFPV", "1"), + ("CGJKQSXZ", "2"), + ("DT", "3"), + ("L", "4"), + ("MN", "5"), + ("R", "6"), + ) result = [s[0]] count = 1 - # find would-be replacment for first character + # find would-be replacement for first character for lset, sub in replacements: if s[0] in lset: last = sub @@ -197,12 +211,14 @@ def soundex(s): last = sub break else: - last = None + if letter != "H" and letter != "W": + # leave last alone if middle letter is H or W + last = None if count == 4: break - result += '0'*(4-count) - return ''.join(result) + result += "0" * (4 - count) + return "".join(result) def hamming_distance(s1, s2): @@ -227,28 +243,28 @@ def nysiis(s): _check_type(s) if not s: - return '' + return "" s = s.upper() key = [] # step 1 - prefixes - if s.startswith('MAC'): - s = 'MCC' + s[3:] - elif s.startswith('KN'): + if s.startswith("MAC"): + s = "MCC" + s[3:] + elif s.startswith("KN"): s = s[1:] - elif s.startswith('K'): - s = 'C' + s[1:] - elif s.startswith(('PH', 'PF')): - s = 'FF' + s[2:] - elif s.startswith('SCH'): - s = 'SSS' + s[3:] + elif s.startswith("K"): + s = "C" + s[1:] + elif s.startswith(("PH", "PF")): + s = "FF" + s[2:] + elif s.startswith("SCH"): + s = "SSS" + s[3:] # step 2 - suffixes - if s.endswith(('IE', 'EE')): - s = s[:-2] + 'Y' - elif s.endswith(('DT', 'RT', 'RD', 'NT', 'ND')): - s = s[:-2] + 'D' + if s.endswith(("IE", "EE")): + s = s[:-2] + "Y" + elif s.endswith(("DT", "RT", "RD", "NT", "ND")): + s = s[:-2] + "D" # step 3 - first character of key comes from name key.append(s[0]) @@ -258,53 +274,57 @@ def nysiis(s): len_s = len(s) while i < len_s: ch = s[i] - if ch == 'E' and i+1 < len_s and s[i+1] == 'V': - ch = 'AF' + if ch == "E" and i + 1 < len_s and s[i + 1] == "V": + ch = "AF" i += 1 - elif ch in 'AEIOU': - ch = 'A' - elif ch == 'Q': - ch = 'G' - elif ch == 'Z': - ch = 'S' - elif ch == 'M': - ch = 'N' - elif ch == 'K': - if i+1 < len(s) and s[i+1] == 'N': - ch = 'N' + elif ch in "AEIOU": + ch = "A" + elif ch == "Q": + ch = "G" + elif ch == "Z": + ch = "S" + elif ch == "M": + ch = "N" + elif ch == "K": + if i + 1 < len(s) and s[i + 1] == "N": + ch = "N" else: - ch = 'C' - elif ch == 'S' and s[i+1:i+3] == 'CH': - ch = 'SS' + ch = "C" + elif ch == "S" and s[i + 1 : i + 3] == "CH": + ch = "SS" i += 2 - elif ch == 'P' and i+1 < len(s) and s[i+1] == 'H': - ch = 'F' + elif ch == "P" and i + 1 < len(s) and s[i + 1] == "H": + ch = "F" i += 1 - elif ch == 'H' and (s[i-1] not in 'AEIOU' or (i+1 < len(s) and s[i+1] not in 'AEIOU')): - if s[i-1] in 'AEIOU': - ch = 'A' + elif ch == "H" and ( + s[i - 1] not in "AEIOU" + or (i + 1 < len(s) and s[i + 1] not in "AEIOU") + or (i + 1 == len(s)) + ): + if s[i - 1] in "AEIOU": + ch = "A" else: - ch = s[i-1] - elif ch == 'W' and s[i-1] in 'AEIOU': - ch = s[i-1] + ch = s[i - 1] + elif ch == "W" and s[i - 1] in "AEIOU": + ch = s[i - 1] if ch[-1] != key[-1][-1]: key.append(ch) i += 1 - key = ''.join(key) + key = "".join(key) # step 5 - remove trailing S - if key.endswith('S') and key != 'S': + if key.endswith("S") and key != "S": key = key[:-1] # step 6 - replace AY w/ Y - if key.endswith('AY'): - key = key[:-2] + 'Y' + if key.endswith("AY"): + key = key[:-2] + "Y" # step 7 - remove trailing A - if key.endswith('A') and key != 'A': + if key.endswith("A") and key != "A": key = key[:-1] # step 8 was already done @@ -315,24 +335,26 @@ def nysiis(s): def match_rating_codex(s): _check_type(s) - s = s.upper() + # we ignore spaces + s = s.upper().replace(" ", "") codex = [] prev = None - for i, c in enumerate(s): - # not a space OR - # starting character & vowel + first = True + for c in s: + # starting character # or consonant not preceded by same consonant - if (c != ' ' and (i == 0 and c in 'AEIOU') or (c not in 'AEIOU' and c != prev)): + if first or (c not in "AEIOU" and c != prev): codex.append(c) prev = c + first = False # just use first/last 3 if len(codex) > 6: - return ''.join(codex[:3]+codex[-3:]) + return "".join(codex[:3] + codex[-3:]) else: - return ''.join(codex) + return "".join(codex) def match_rating_comparison(s1, s2): @@ -344,7 +366,7 @@ def match_rating_comparison(s1, s2): res2 = [] # length differs by 3 or more, no result - if abs(len1-len2) >= 3: + if abs(len1 - len2) >= 3: return None # get minimum rating based on sums of codexes @@ -359,7 +381,7 @@ def match_rating_comparison(s1, s2): min_rating = 2 # strip off common prefixes - for c1, c2 in _zip_longest(codex1, codex2): + for c1, c2 in zip_longest(codex1, codex2): if c1 != c2: if c1: res1.append(c1) @@ -367,7 +389,7 @@ def match_rating_comparison(s1, s2): res2.append(c2) unmatched_count1 = unmatched_count2 = 0 - for c1, c2 in _zip_longest(reversed(res1), reversed(res2)): + for c1, c2 in zip_longest(reversed(res1), reversed(res2)): if c1 != c2: if c1: unmatched_count1 += 1 @@ -385,112 +407,113 @@ def metaphone(s): s = _normalize(s.lower()) # skip first character if s starts with these - if s.startswith(('kn', 'gn', 'pn', 'ac', 'wr', 'ae')): + if s.startswith(("kn", "gn", "pn", "wr", "ae")): s = s[1:] i = 0 while i < len(s): c = s[i] - next = s[i+1] if i < len(s)-1 else '*****' - nextnext = s[i+2] if i < len(s)-2 else '*****' + next = s[i + 1] if i < len(s) - 1 else "*****" + nextnext = s[i + 2] if i < len(s) - 2 else "*****" # skip doubles except for cc - if c == next and c != 'c': + if c == next and c != "c": i += 1 continue - if c in 'aeiou': - if i == 0 or s[i-1] == ' ': + if c in "aeiou": + if i == 0 or s[i - 1] == " ": result.append(c) - elif c == 'b': - if (not (i != 0 and s[i-1] == 'm')) or next: - result.append('b') - elif c == 'c': - if next == 'i' and nextnext == 'a' or next == 'h': - result.append('x') + elif c == "b": + if (not (i != 0 and s[i - 1] == "m")) or next: + result.append("b") + elif c == "c": + if next == "i" and nextnext == "a" or next == "h": + result.append("x") i += 1 - elif next in 'iey': - result.append('s') + elif next in "iey": + result.append("s") i += 1 else: - result.append('k') - elif c == 'd': - if next == 'g' and nextnext in 'iey': - result.append('j') + result.append("k") + elif c == "d": + if next == "g" and nextnext in "iey": + result.append("j") i += 2 else: - result.append('t') - elif c in 'fjlmnr': + result.append("t") + elif c in "fjlmnr": result.append(c) - elif c == 'g': - if next in 'iey': - result.append('j') - elif next not in 'hn': - result.append('k') - elif next == 'h' and nextnext and nextnext not in 'aeiou': + elif c == "g": + if next in "iey": + result.append("j") + elif next == "h" and nextnext and nextnext not in "aeiou": i += 1 - elif c == 'h': - if i == 0 or next in 'aeiou' or s[i-1] not in 'aeiou': - result.append('h') - elif c == 'k': - if i == 0 or s[i-1] != 'c': - result.append('k') - elif c == 'p': - if next == 'h': - result.append('f') + elif next == "n" and not nextnext: i += 1 else: - result.append('p') - elif c == 'q': - result.append('k') - elif c == 's': - if next == 'h': - result.append('x') + result.append("k") + elif c == "h": + if i == 0 or next in "aeiou" or s[i - 1] not in "aeiou": + result.append("h") + elif c == "k": + if i == 0 or s[i - 1] != "c": + result.append("k") + elif c == "p": + if next == "h": + result.append("f") i += 1 - elif next == 'i' and nextnext in 'oa': - result.append('x') + else: + result.append("p") + elif c == "q": + result.append("k") + elif c == "s": + if next == "h": + result.append("x") + i += 1 + elif next == "i" and nextnext in "oa": + result.append("x") i += 2 else: - result.append('s') - elif c == 't': - if next == 'i' and nextnext in 'oa': - result.append('x') - elif next == 'h': - result.append('0') + result.append("s") + elif c == "t": + if next == "i" and nextnext in "oa": + result.append("x") + elif next == "h": + result.append("0") i += 1 - elif next != 'c' or nextnext != 'h': - result.append('t') - elif c == 'v': - result.append('f') - elif c == 'w': - if i == 0 and next == 'h': + elif next != "c" or nextnext != "h": + result.append("t") + elif c == "v": + result.append("f") + elif c == "w": + if i == 0 and next == "h": i += 1 - if nextnext in 'aeiou' or nextnext == '*****': - result.append('w') - elif next in 'aeiou' or next == '*****': - result.append('w') - elif c == 'x': + result.append("w") + elif next in "aeiou": + result.append("w") + elif c == "x": if i == 0: - if next == 'h' or (next == 'i' and nextnext in 'oa'): - result.append('x') + if next == "h" or (next == "i" and nextnext in "oa"): + result.append("x") else: - result.append('s') + result.append("s") else: - result.append('k') - result.append('s') - elif c == 'y': - if next in 'aeiou': - result.append('y') - elif c == 'z': - result.append('s') - elif c == ' ': - if len(result) > 0 and result[-1] != ' ': - result.append(' ') + result.append("k") + result.append("s") + elif c == "y": + if next in "aeiou": + result.append("y") + elif c == "z": + result.append("s") + elif c == " ": + if len(result) > 0 and result[-1] != " ": + result.append(" ") i += 1 - return ''.join(result).upper() + return "".join(result).upper() def porter_stem(s): diff --git a/libs/common/jellyfish/cjellyfish.cp37-win_amd64.pyd b/libs/common/jellyfish/cjellyfish.cp37-win_amd64.pyd new file mode 100644 index 0000000000000000000000000000000000000000..cc4067cfdc220c870e2806292f844abad9e6156c GIT binary patch literal 31232 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 literal 0 HcmV?d00001 diff --git a/libs/common/jellyfish/compat.py b/libs/common/jellyfish/compat.py deleted file mode 100644 index 180283d1..00000000 --- a/libs/common/jellyfish/compat.py +++ /dev/null @@ -1,11 +0,0 @@ -import sys -import itertools - -IS_PY3 = sys.version_info[0] == 3 - -if IS_PY3: - _range = range - _zip_longest = itertools.zip_longest -else: - _range = xrange - _zip_longest = itertools.izip_longest diff --git a/libs/common/jellyfish/porter.py b/libs/common/jellyfish/porter.py index 2945b22d..1bf64d44 100644 --- a/libs/common/jellyfish/porter.py +++ b/libs/common/jellyfish/porter.py @@ -1,69 +1,84 @@ -from .compat import _range - _s2_options = { - 'a': ((['a', 't', 'i', 'o', 'n', 'a', 'l'], ['a', 't', 'e']), - (['t', 'i', 'o', 'n', 'a', 'l'], ['t', 'i', 'o', 'n'])), - 'c': ((['e', 'n', 'c', 'i'], ['e', 'n', 'c', 'e']), - (['a', 'n', 'c', 'i'], ['a', 'n', 'c', 'e']),), - 'e': ((['i', 'z', 'e', 'r'], ['i', 'z', 'e']),), - 'l': ((['b', 'l', 'i'], ['b', 'l', 'e']), - (['a', 'l', 'l', 'i'], ['a', 'l']), - (['e', 'n', 't', 'l', 'i'], ['e', 'n', 't']), - (['e', 'l', 'i'], ['e']), - (['o', 'u', 's', 'l', 'i'], ['o', 'u', 's']),), - 'o': ((['i', 'z', 'a', 't', 'i', 'o', 'n'], ['i', 'z', 'e']), - (['a', 't', 'i', 'o', 'n'], ['a', 't', 'e']), - (['a', 't', 'o', 'r'], ['a', 't', 'e']),), - 's': ((['a', 'l', 'i', 's', 'm'], ['a', 'l']), - (['i', 'v', 'e', 'n', 'e', 's', 's'], ['i', 'v', 'e']), - (['f', 'u', 'l', 'n', 'e', 's', 's'], ['f', 'u', 'l']), - (['o', 'u', 's', 'n', 'e', 's', 's'], ['o', 'u', 's']),), - 't': ((['a', 'l', 'i', 't', 'i'], ['a', 'l']), - (['i', 'v', 'i', 't', 'i'], ['i', 'v', 'e']), - (['b', 'i', 'l', 'i', 't', 'i'], ['b', 'l', 'e']),), - 'g': ((['l', 'o', 'g', 'i'], ['l', 'o', 'g']),), + "a": ( + (["a", "t", "i", "o", "n", "a", "l"], ["a", "t", "e"]), + (["t", "i", "o", "n", "a", "l"], ["t", "i", "o", "n"]), + ), + "c": ( + (["e", "n", "c", "i"], ["e", "n", "c", "e"]), + (["a", "n", "c", "i"], ["a", "n", "c", "e"]), + ), + "e": ((["i", "z", "e", "r"], ["i", "z", "e"]),), + "l": ( + (["b", "l", "i"], ["b", "l", "e"]), + (["a", "l", "l", "i"], ["a", "l"]), + (["e", "n", "t", "l", "i"], ["e", "n", "t"]), + (["e", "l", "i"], ["e"]), + (["o", "u", "s", "l", "i"], ["o", "u", "s"]), + ), + "o": ( + (["i", "z", "a", "t", "i", "o", "n"], ["i", "z", "e"]), + (["a", "t", "i", "o", "n"], ["a", "t", "e"]), + (["a", "t", "o", "r"], ["a", "t", "e"]), + ), + "s": ( + (["a", "l", "i", "s", "m"], ["a", "l"]), + (["i", "v", "e", "n", "e", "s", "s"], ["i", "v", "e"]), + (["f", "u", "l", "n", "e", "s", "s"], ["f", "u", "l"]), + (["o", "u", "s", "n", "e", "s", "s"], ["o", "u", "s"]), + ), + "t": ( + (["a", "l", "i", "t", "i"], ["a", "l"]), + (["i", "v", "i", "t", "i"], ["i", "v", "e"]), + (["b", "i", "l", "i", "t", "i"], ["b", "l", "e"]), + ), + "g": ((["l", "o", "g", "i"], ["l", "o", "g"]),), } _s3_options = { - 'e': ((['i', 'c', 'a', 't', 'e'], ['i', 'c']), - (['a', 't', 'i', 'v', 'e'], []), - (['a', 'l', 'i', 'z', 'e'], ['a', 'l']),), - 'i': ((['i', 'c', 'i', 't', 'i'], ['i', 'c']),), - 'l': ((['i', 'c', 'a', 'l'], ['i', 'c']), - (['f', 'u', 'l'], []),), - 's': ((['n', 'e', 's', 's'], []),), + "e": ( + (["i", "c", "a", "t", "e"], ["i", "c"]), + (["a", "t", "i", "v", "e"], []), + (["a", "l", "i", "z", "e"], ["a", "l"]), + ), + "i": ((["i", "c", "i", "t", "i"], ["i", "c"]),), + "l": ((["i", "c", "a", "l"], ["i", "c"]), (["f", "u", "l"], [])), + "s": ((["n", "e", "s", "s"], []),), } _s4_endings = { - 'a': (['a', 'l'],), - 'c': (['a', 'n', 'c', 'e'], ['e', 'n', 'c', 'e']), - 'e': (['e', 'r'],), - 'i': (['i', 'c'],), - 'l': (['a', 'b', 'l', 'e'], ['i', 'b', 'l', 'e']), - 'n': (['a', 'n', 't'], ['e', 'm', 'e', 'n', 't'], ['m', 'e', 'n', 't'], - ['e', 'n', 't']), + "a": (["a", "l"],), + "c": (["a", "n", "c", "e"], ["e", "n", "c", "e"]), + "e": (["e", "r"],), + "i": (["i", "c"],), + "l": (["a", "b", "l", "e"], ["i", "b", "l", "e"]), + "n": ( + ["a", "n", "t"], + ["e", "m", "e", "n", "t"], + ["m", "e", "n", "t"], + ["e", "n", "t"], + ), # handle 'o' separately - 's': (['i', 's', 'm'],), - 't': (['a', 't', 'e'], ['i', 't', 'i']), - 'u': (['o', 'u', 's'],), - 'v': (['i', 'v', 'e'],), - 'z': (['i', 'z', 'e'],), + "s": (["i", "s", "m"],), + "t": (["a", "t", "e"], ["i", "t", "i"]), + "u": (["o", "u", "s"],), + "v": (["i", "v", "e"],), + "z": (["i", "z", "e"],), } class Stemmer(object): def __init__(self, b): self.b = list(b) - self.k = len(b)-1 + self.k = len(b) - 1 self.j = 0 def cons(self, i): """ True iff b[i] is a consonant """ - if self.b[i] in 'aeiou': + if self.b[i] in "aeiou": return False - elif self.b[i] == 'y': - return True if i == 0 else not self.cons(i-1) + elif self.b[i] == "y": + return True if i == 0 else not self.cons(i - 1) return True def m(self): @@ -96,31 +111,36 @@ class Stemmer(object): def vowel_in_stem(self): """ True iff 0...j contains vowel """ - for i in _range(0, self.j+1): + for i in range(0, self.j + 1): if not self.cons(i): return True return False def doublec(self, j): """ True iff j, j-1 contains double consonant """ - if j < 1 or self.b[j] != self.b[j-1]: + if j < 1 or self.b[j] != self.b[j - 1]: return False return self.cons(j) def cvc(self, i): - """ True iff i-2,i-1,i is consonent-vowel consonant + """ True iff i-2,i-1,i is consonant-vowel consonant and if second c isn't w,x, or y. used to restore e at end of short words like cave, love, hope, crime """ - if (i < 2 or not self.cons(i) or self.cons(i-1) or not self.cons(i-2) or - self.b[i] in 'wxy'): + if ( + i < 2 + or not self.cons(i) + or self.cons(i - 1) + or not self.cons(i - 2) + or self.b[i] in "wxy" + ): return False return True def ends(self, s): length = len(s) """ True iff 0...k ends with string s """ - res = (self.b[self.k-length+1:self.k+1] == s) + res = self.b[self.k - length + 1 : self.k + 1] == s if res: self.j = self.k - length return res @@ -128,7 +148,7 @@ class Stemmer(object): def setto(self, s): """ set j+1...k to string s, readjusting k """ length = len(s) - self.b[self.j+1:self.j+1+length] = s + self.b[self.j + 1 : self.j + 1 + length] = s self.k = self.j + length def r(self, s): @@ -136,39 +156,40 @@ class Stemmer(object): self.setto(s) def step1ab(self): - if self.b[self.k] == 's': - if self.ends(['s', 's', 'e', 's']): + if self.b[self.k] == "s": + if self.ends(["s", "s", "e", "s"]): self.k -= 2 - elif self.ends(['i', 'e', 's']): - self.setto(['i']) - elif self.b[self.k-1] != 's': + elif self.ends(["i", "e", "s"]): + self.setto(["i"]) + elif self.b[self.k - 1] != "s": self.k -= 1 - if self.ends(['e', 'e', 'd']): + if self.ends(["e", "e", "d"]): if self.m() > 0: self.k -= 1 - elif ((self.ends(['e', 'd']) or self.ends(['i', 'n', 'g'])) and - self.vowel_in_stem()): + elif ( + self.ends(["e", "d"]) or self.ends(["i", "n", "g"]) + ) and self.vowel_in_stem(): self.k = self.j - if self.ends(['a', 't']): - self.setto(['a', 't', 'e']) - elif self.ends(['b', 'l']): - self.setto(['b', 'l', 'e']) - elif self.ends(['i', 'z']): - self.setto(['i', 'z', 'e']) + if self.ends(["a", "t"]): + self.setto(["a", "t", "e"]) + elif self.ends(["b", "l"]): + self.setto(["b", "l", "e"]) + elif self.ends(["i", "z"]): + self.setto(["i", "z", "e"]) elif self.doublec(self.k): self.k -= 1 - if self.b[self.k] in 'lsz': + if self.b[self.k] in "lsz": self.k += 1 elif self.m() == 1 and self.cvc(self.k): - self.setto(['e']) + self.setto(["e"]) def step1c(self): """ turn terminal y into i if there's a vowel in stem """ - if self.ends(['y']) and self.vowel_in_stem(): - self.b[self.k] = 'i' + if self.ends(["y"]) and self.vowel_in_stem(): + self.b[self.k] = "i" def step2and3(self): - for end, repl in _s2_options.get(self.b[self.k-1], []): + for end, repl in _s2_options.get(self.b[self.k - 1], []): if self.ends(end): self.r(repl) break @@ -179,11 +200,13 @@ class Stemmer(object): break def step4(self): - ch = self.b[self.k-1] + ch = self.b[self.k - 1] - if ch == 'o': - if not ((self.ends(['i', 'o', 'n']) and self.b[self.j] in 'st') or - self.ends(['o', 'u'])): + if ch == "o": + if not ( + (self.ends(["i", "o", "n"]) and self.b[self.j] in "st") + or self.ends(["o", "u"]) + ): return else: endings = _s4_endings.get(ch, []) @@ -198,15 +221,15 @@ class Stemmer(object): def step5(self): self.j = self.k - if self.b[self.k] == 'e': + if self.b[self.k] == "e": a = self.m() - if a > 1 or a == 1 and not self.cvc(self.k-1): + if a > 1 or a == 1 and not self.cvc(self.k - 1): self.k -= 1 - if self.b[self.k] == 'l' and self.doublec(self.k) and self.m() > 1: + if self.b[self.k] == "l" and self.doublec(self.k) and self.m() > 1: self.k -= 1 def result(self): - return ''.join(self.b[:self.k+1]) + return "".join(self.b[: self.k + 1]) def stem(self): if self.k > 1: diff --git a/libs/common/jellyfish/py.typed b/libs/common/jellyfish/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/common/jellyfish/test.py b/libs/common/jellyfish/test.py index dea87c25..2fec35ab 100644 --- a/libs/common/jellyfish/test.py +++ b/libs/common/jellyfish/test.py @@ -1,28 +1,24 @@ # -*- coding: utf-8 -*- -import sys -if sys.version_info[0] < 3: - import unicodecsv as csv - open_kwargs = {} -else: - import csv - open_kwargs = {'encoding': 'utf8'} +import csv import platform import pytest +open_kwargs = {"encoding": "utf8"} + def assertAlmostEqual(a, b, places=3): - assert abs(a - b) < (0.1**places) + assert abs(a - b) < (0.1 ** places) -if platform.python_implementation() == 'CPython': - implementations = ['python', 'c'] +if platform.python_implementation() == "CPython": + implementations = ["python", "c"] else: - implementations = ['python'] + implementations = ["python"] @pytest.fixture(params=implementations) def jf(request): - if request.param == 'python': + if request.param == "python": from jellyfish import _jellyfish as jf else: from jellyfish import cjellyfish as jf @@ -30,64 +26,86 @@ def jf(request): def _load_data(name): - with open('testdata/{}.csv'.format(name), **open_kwargs) as f: + with open("testdata/{}.csv".format(name), **open_kwargs) as f: for data in csv.reader(f): yield data -@pytest.mark.parametrize("s1,s2,value", _load_data('jaro_winkler'), ids=str) -def test_jaro_winkler(jf, s1, s2, value): +@pytest.mark.parametrize("s1,s2,value", _load_data("jaro_winkler"), ids=str) +def test_jaro_winkler_similarity(jf, s1, s2, value): value = float(value) - assertAlmostEqual(jf.jaro_winkler(s1, s2), value, places=3) + assertAlmostEqual(jf.jaro_winkler_similarity(s1, s2), value, places=3) -@pytest.mark.parametrize("s1,s2,value", _load_data('jaro_distance'), ids=str) -def test_jaro_distance(jf, s1, s2, value): +@pytest.mark.parametrize("s1,s2,value", _load_data("jaro_winkler_longtol"), ids=str) +def test_jaro_winkler_similarity_longtol(jf, s1, s2, value): value = float(value) - assertAlmostEqual(jf.jaro_distance(s1, s2), value, places=3) + assertAlmostEqual(jf.jaro_winkler_similarity(s1, s2, True), value, places=3) -@pytest.mark.parametrize("s1,s2,value", _load_data('hamming'), ids=str) +def test_jaro_winkler_deprecation(jf): + # backwards compatibility function + from jellyfish import jaro_winkler + + with pytest.deprecated_call(): + assert jaro_winkler("a", "a") == 1 + + +def test_jaro_distance_deprecation(): + # backwards compatibility function + from jellyfish import jaro_distance + + with pytest.deprecated_call(): + assert jaro_distance("a", "a") == 1 + + +@pytest.mark.parametrize("s1,s2,value", _load_data("jaro_distance"), ids=str) +def test_jaro_similarity(jf, s1, s2, value): + value = float(value) + assertAlmostEqual(jf.jaro_similarity(s1, s2), value, places=3) + + +@pytest.mark.parametrize("s1,s2,value", _load_data("hamming"), ids=str) def test_hamming_distance(jf, s1, s2, value): value = int(value) assert jf.hamming_distance(s1, s2) == value -@pytest.mark.parametrize("s1,s2,value", _load_data('levenshtein'), ids=str) +@pytest.mark.parametrize("s1,s2,value", _load_data("levenshtein"), ids=str) def test_levenshtein_distance(jf, s1, s2, value): value = int(value) assert jf.levenshtein_distance(s1, s2) == value -@pytest.mark.parametrize("s1,s2,value", _load_data('damerau_levenshtein'), ids=str) +@pytest.mark.parametrize("s1,s2,value", _load_data("damerau_levenshtein"), ids=str) def test_damerau_levenshtein_distance(jf, s1, s2, value): value = int(value) assert jf.damerau_levenshtein_distance(s1, s2) == value -@pytest.mark.parametrize("s1,code", _load_data('soundex'), ids=str) +@pytest.mark.parametrize("s1,code", _load_data("soundex"), ids=str) def test_soundex(jf, s1, code): assert jf.soundex(s1) == code -@pytest.mark.parametrize("s1,code", _load_data('metaphone'), ids=str) +@pytest.mark.parametrize("s1,code", _load_data("metaphone"), ids=str) def test_metaphone(jf, s1, code): assert jf.metaphone(s1) == code -@pytest.mark.parametrize("s1,s2", _load_data('nysiis'), ids=str) +@pytest.mark.parametrize("s1,s2", _load_data("nysiis"), ids=str) def test_nysiis(jf, s1, s2): assert jf.nysiis(s1) == s2 -@pytest.mark.parametrize("s1,s2", _load_data('match_rating_codex'), ids=str) +@pytest.mark.parametrize("s1,s2", _load_data("match_rating_codex"), ids=str) def test_match_rating_codex(jf, s1, s2): assert jf.match_rating_codex(s1) == s2 -@pytest.mark.parametrize("s1,s2,value", _load_data('match_rating_comparison'), ids=str) +@pytest.mark.parametrize("s1,s2,value", _load_data("match_rating_comparison"), ids=str) def test_match_rating_comparison(jf, s1, s2, value): - value = {'True': True, 'False': False, 'None': None}[value] + value = {"True": True, "False": False, "None": None}[value] assert jf.match_rating_comparison(s1, s2) is value @@ -96,117 +114,125 @@ def test_match_rating_comparison(jf, s1, s2, value): # def test_porter_stem(jf, a, b): # assert jf.porter_stem(a) == b + def test_porter_stem(jf): - with open('testdata/porter.csv', **open_kwargs) as f: + with open("testdata/porter.csv", **open_kwargs) as f: reader = csv.reader(f) for (a, b) in reader: assert jf.porter_stem(a) == b -if platform.python_implementation() == 'CPython': +if platform.python_implementation() == "CPython": + def test_match_rating_comparison_segfault(): import hashlib from jellyfish import cjellyfish as jf - sha1s = [u'{}'.format(hashlib.sha1(str(v).encode('ascii')).hexdigest()) - for v in range(100)] + + sha1s = [ + u"{}".format(hashlib.sha1(str(v).encode("ascii")).hexdigest()) + for v in range(100) + ] # this segfaulted on 0.1.2 assert [[jf.match_rating_comparison(h1, h2) for h1 in sha1s] for h2 in sha1s] def test_damerau_levenshtein_unicode_segfault(): - # unfortunate difference in behavior between Py & C versions + # test that unicode works in C & Python versions now from jellyfish.cjellyfish import damerau_levenshtein_distance as c_dl from jellyfish._jellyfish import damerau_levenshtein_distance as py_dl - s1 = u'mylifeoutdoors' - s2 = u'нахлыст' - with pytest.raises(ValueError): - c_dl(s1, s2) - with pytest.raises(ValueError): - c_dl(s2, s1) + + s1 = u"mylifeoutdoors" + s2 = u"нахлыст" + assert c_dl(s1, s2) == 14 + assert c_dl(s2, s1) == 14 assert py_dl(s1, s2) == 14 assert py_dl(s2, s1) == 14 def test_jaro_winkler_long_tolerance(jf): - no_lt = jf.jaro_winkler(u'two long strings', u'two long stringz', long_tolerance=False) - with_lt = jf.jaro_winkler(u'two long strings', u'two long stringz', long_tolerance=True) + no_lt = jf.jaro_winkler_similarity( + u"two long strings", u"two long stringz", long_tolerance=False + ) + with_lt = jf.jaro_winkler_similarity( + u"two long strings", u"two long stringz", long_tolerance=True + ) # make sure long_tolerance does something assertAlmostEqual(no_lt, 0.975) assertAlmostEqual(with_lt, 0.984) def test_damerau_levenshtein_distance_type(jf): - jf.damerau_levenshtein_distance(u'abc', u'abc') + jf.damerau_levenshtein_distance(u"abc", u"abc") with pytest.raises(TypeError) as exc: - jf.damerau_levenshtein_distance(b'abc', b'abc') - assert 'expected' in str(exc.value) + jf.damerau_levenshtein_distance(b"abc", b"abc") + assert "expected" in str(exc.value) def test_levenshtein_distance_type(jf): - assert jf.levenshtein_distance(u'abc', u'abc') == 0 + assert jf.levenshtein_distance(u"abc", u"abc") == 0 with pytest.raises(TypeError) as exc: - jf.levenshtein_distance(b'abc', b'abc') - assert 'expected' in str(exc.value) + jf.levenshtein_distance(b"abc", b"abc") + assert "expected" in str(exc.value) -def test_jaro_distance_type(jf): - assert jf.jaro_distance(u'abc', u'abc') == 1 +def test_jaro_similarity_type(jf): + assert jf.jaro_similarity(u"abc", u"abc") == 1 with pytest.raises(TypeError) as exc: - jf.jaro_distance(b'abc', b'abc') - assert 'expected' in str(exc.value) + jf.jaro_similarity(b"abc", b"abc") + assert "expected" in str(exc.value) def test_jaro_winkler_type(jf): - assert jf.jaro_winkler(u'abc', u'abc') == 1 + assert jf.jaro_winkler_similarity(u"abc", u"abc") == 1 with pytest.raises(TypeError) as exc: - jf.jaro_winkler(b'abc', b'abc') - assert 'expected' in str(exc.value) + jf.jaro_winkler_similarity(b"abc", b"abc") + assert "expected" in str(exc.value) def test_mra_comparison_type(jf): - assert jf.match_rating_comparison(u'abc', u'abc') is True + assert jf.match_rating_comparison(u"abc", u"abc") is True with pytest.raises(TypeError) as exc: - jf.match_rating_comparison(b'abc', b'abc') - assert 'expected' in str(exc.value) + jf.match_rating_comparison(b"abc", b"abc") + assert "expected" in str(exc.value) def test_hamming_type(jf): - assert jf.hamming_distance(u'abc', u'abc') == 0 + assert jf.hamming_distance(u"abc", u"abc") == 0 with pytest.raises(TypeError) as exc: - jf.hamming_distance(b'abc', b'abc') - assert 'expected' in str(exc.value) + jf.hamming_distance(b"abc", b"abc") + assert "expected" in str(exc.value) def test_soundex_type(jf): - assert jf.soundex(u'ABC') == 'A120' + assert jf.soundex(u"ABC") == "A120" with pytest.raises(TypeError) as exc: - jf.soundex(b'ABC') - assert 'expected' in str(exc.value) + jf.soundex(b"ABC") + assert "expected" in str(exc.value) def test_metaphone_type(jf): - assert jf.metaphone(u'abc') == 'ABK' + assert jf.metaphone(u"abc") == "ABK" with pytest.raises(TypeError) as exc: - jf.metaphone(b'abc') - assert 'expected' in str(exc.value) + jf.metaphone(b"abc") + assert "expected" in str(exc.value) def test_nysiis_type(jf): - assert jf.nysiis(u'abc') == 'ABC' + assert jf.nysiis(u"abc") == "ABC" with pytest.raises(TypeError) as exc: - jf.nysiis(b'abc') - assert 'expected' in str(exc.value) + jf.nysiis(b"abc") + assert "expected" in str(exc.value) def test_mr_codex_type(jf): - assert jf.match_rating_codex(u'abc') == 'ABC' + assert jf.match_rating_codex(u"abc") == "ABC" with pytest.raises(TypeError) as exc: - jf.match_rating_codex(b'abc') - assert 'expected' in str(exc.value) + jf.match_rating_codex(b"abc") + assert "expected" in str(exc.value) def test_porter_type(jf): - assert jf.porter_stem(u'abc') == 'abc' + assert jf.porter_stem(u"abc") == "abc" with pytest.raises(TypeError) as exc: - jf.porter_stem(b'abc') - assert 'expected' in str(exc.value) + jf.porter_stem(b"abc") + assert "expected" in str(exc.value) diff --git a/libs/common/mediafile.py b/libs/common/mediafile.py new file mode 100644 index 00000000..ca70c944 --- /dev/null +++ b/libs/common/mediafile.py @@ -0,0 +1,2398 @@ +# -*- coding: utf-8 -*- +# This file is part of MediaFile. +# Copyright 2016, Adrian Sampson. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +"""Handles low-level interfacing for files' tags. Wraps Mutagen to +automatically detect file types and provide a unified interface for a +useful subset of music files' tags. + +Usage: + + >>> f = MediaFile('Lucy.mp3') + >>> f.title + u'Lucy in the Sky with Diamonds' + >>> f.artist = 'The Beatles' + >>> f.save() + +A field will always return a reasonable value of the correct type, even +if no tag is present. If no value is available, the value will be false +(e.g., zero or the empty string). + +Internally ``MediaFile`` uses ``MediaField`` descriptors to access the +data from the tags. In turn ``MediaField`` uses a number of +``StorageStyle`` strategies to handle format specific logic. +""" +from __future__ import division, absolute_import, print_function + +import mutagen +import mutagen.id3 +import mutagen.mp3 +import mutagen.mp4 +import mutagen.flac +import mutagen.asf +import mutagen._util + +import base64 +import binascii +import codecs +import datetime +import enum +import functools +import imghdr +import logging +import math +import os +import re +import six +import struct +import traceback + + +__version__ = '0.10.1' +__all__ = ['UnreadableFileError', 'FileTypeError', 'MediaFile'] + +log = logging.getLogger(__name__) + +# Human-readable type names. +TYPES = { + 'mp3': 'MP3', + 'aac': 'AAC', + 'alac': 'ALAC', + 'ogg': 'OGG', + 'opus': 'Opus', + 'flac': 'FLAC', + 'ape': 'APE', + 'wv': 'WavPack', + 'mpc': 'Musepack', + 'asf': 'Windows Media', + 'aiff': 'AIFF', + 'dsf': 'DSD Stream File', + 'wav': 'WAVE', +} + +PREFERRED_IMAGE_EXTENSIONS = {'jpeg': 'jpg'} + + +# Exceptions. + +class UnreadableFileError(Exception): + """Mutagen is not able to extract information from the file. + """ + def __init__(self, filename, msg): + Exception.__init__(self, msg if msg else repr(filename)) + + +class FileTypeError(UnreadableFileError): + """Reading this type of file is not supported. + + If passed the `mutagen_type` argument this indicates that the + mutagen type is not supported by `Mediafile`. + """ + def __init__(self, filename, mutagen_type=None): + if mutagen_type is None: + msg = u'{0!r}: not in a recognized format'.format(filename) + else: + msg = u'{0}: of mutagen type {1}'.format( + repr(filename), mutagen_type + ) + Exception.__init__(self, msg) + + +class MutagenError(UnreadableFileError): + """Raised when Mutagen fails unexpectedly---probably due to a bug. + """ + def __init__(self, filename, mutagen_exc): + msg = u'{0}: {1}'.format(repr(filename), mutagen_exc) + Exception.__init__(self, msg) + + +# Interacting with Mutagen. + + +def mutagen_call(action, filename, func, *args, **kwargs): + """Call a Mutagen function with appropriate error handling. + + `action` is a string describing what the function is trying to do, + and `filename` is the relevant filename. The rest of the arguments + describe the callable to invoke. + + We require at least Mutagen 1.33, where `IOError` is *never* used, + neither for internal parsing errors *nor* for ordinary IO error + conditions such as a bad filename. Mutagen-specific parsing errors and IO + errors are reraised as `UnreadableFileError`. Other exceptions + raised inside Mutagen---i.e., bugs---are reraised as `MutagenError`. + """ + try: + return func(*args, **kwargs) + except mutagen.MutagenError as exc: + log.debug(u'%s failed: %s', action, six.text_type(exc)) + raise UnreadableFileError(filename, six.text_type(exc)) + except UnreadableFileError: + # Reraise our errors without changes. + # Used in case of decorating functions (e.g. by `loadfile`). + raise + except Exception as exc: + # Isolate bugs in Mutagen. + log.debug(u'%s', traceback.format_exc()) + log.error(u'uncaught Mutagen exception in %s: %s', action, exc) + raise MutagenError(filename, exc) + + +def loadfile(method=True, writable=False, create=False): + """A decorator that works like `mutagen._util.loadfile` but with + additional error handling. + + Opens a file and passes a `mutagen._utils.FileThing` to the + decorated function. Should be used as a decorator for functions + using a `filething` parameter. + """ + def decorator(func): + f = mutagen._util.loadfile(method, writable, create)(func) + + @functools.wraps(func) + def wrapper(*args, **kwargs): + return mutagen_call('loadfile', '', f, *args, **kwargs) + return wrapper + return decorator + + +# Utility. + +def _update_filething(filething): + """Reopen a `filething` if it's a local file. + + A filething that is *not* an actual file is left unchanged; a + filething with a filename is reopened and a new object is returned. + """ + if filething.filename: + return mutagen._util.FileThing( + None, filething.filename, filething.name + ) + else: + return filething + + +def _safe_cast(out_type, val): + """Try to covert val to out_type but never raise an exception. + + If the value does not exist, return None. Or, if the value + can't be converted, then a sensible default value is returned. + out_type should be bool, int, or unicode; otherwise, the value + is just passed through. + """ + if val is None: + return None + + if out_type == int: + if isinstance(val, int) or isinstance(val, float): + # Just a number. + return int(val) + else: + # Process any other type as a string. + if isinstance(val, bytes): + val = val.decode('utf-8', 'ignore') + elif not isinstance(val, six.string_types): + val = six.text_type(val) + # Get a number from the front of the string. + match = re.match(r'[\+-]?[0-9]+', val.strip()) + return int(match.group(0)) if match else 0 + + elif out_type == bool: + try: + # Should work for strings, bools, ints: + return bool(int(val)) + except ValueError: + return False + + elif out_type == six.text_type: + if isinstance(val, bytes): + return val.decode('utf-8', 'ignore') + elif isinstance(val, six.text_type): + return val + else: + return six.text_type(val) + + elif out_type == float: + if isinstance(val, int) or isinstance(val, float): + return float(val) + else: + if isinstance(val, bytes): + val = val.decode('utf-8', 'ignore') + else: + val = six.text_type(val) + match = re.match(r'[\+-]?([0-9]+\.?[0-9]*|[0-9]*\.[0-9]+)', + val.strip()) + if match: + val = match.group(0) + if val: + return float(val) + return 0.0 + + else: + return val + + +# Image coding for ASF/WMA. + +def _unpack_asf_image(data): + """Unpack image data from a WM/Picture tag. Return a tuple + containing the MIME type, the raw image data, a type indicator, and + the image's description. + + This function is treated as "untrusted" and could throw all manner + of exceptions (out-of-bounds, etc.). We should clean this up + sometime so that the failure modes are well-defined. + """ + type, size = struct.unpack_from(' 0: + gain = math.log10(maxgain / 1000.0) * -10 + else: + # Invalid gain value found. + gain = 0.0 + + # SoundCheck stores peak values as the actual value of the sample, + # and again separately for the left and right channels. We need to + # convert this to a percentage of full scale, which is 32768 for a + # 16 bit sample. Once again, we play it safe by using the larger of + # the two values. + peak = max(soundcheck[6:8]) / 32768.0 + + return round(gain, 2), round(peak, 6) + + +def _sc_encode(gain, peak): + """Encode ReplayGain gain/peak values as a Sound Check string. + """ + # SoundCheck stores the peak value as the actual value of the + # sample, rather than the percentage of full scale that RG uses, so + # we do a simple conversion assuming 16 bit samples. + peak *= 32768.0 + + # SoundCheck stores absolute RMS values in some unknown units rather + # than the dB values RG uses. We can calculate these absolute values + # from the gain ratio using a reference value of 1000 units. We also + # enforce the maximum and minimum value here, which is equivalent to + # about -18.2dB and 30.0dB. + g1 = int(min(round((10 ** (gain / -10)) * 1000), 65534)) or 1 + # Same as above, except our reference level is 2500 units. + g2 = int(min(round((10 ** (gain / -10)) * 2500), 65534)) or 1 + + # The purpose of these values are unknown, but they also seem to be + # unused so we just use zero. + uk = 0 + values = (g1, g1, g2, g2, uk, uk, int(peak), int(peak), uk, uk) + return (u' %08X' * 10) % values + + +# Cover art and other images. +def _imghdr_what_wrapper(data): + """A wrapper around imghdr.what to account for jpeg files that can only be + identified as such using their magic bytes + See #1545 + See https://github.com/file/file/blob/master/magic/Magdir/jpeg#L12 + """ + # imghdr.what returns none for jpegs with only the magic bytes, so + # _wider_test_jpeg is run in that case. It still returns None if it didn't + # match such a jpeg file. + return imghdr.what(None, h=data) or _wider_test_jpeg(data) + + +def _wider_test_jpeg(data): + """Test for a jpeg file following the UNIX file implementation which + uses the magic bytes rather than just looking for the bytes that + represent 'JFIF' or 'EXIF' at a fixed position. + """ + if data[:2] == b'\xff\xd8': + return 'jpeg' + + +def image_mime_type(data): + """Return the MIME type of the image data (a bytestring). + """ + # This checks for a jpeg file with only the magic bytes (unrecognized by + # imghdr.what). imghdr.what returns none for that type of file, so + # _wider_test_jpeg is run in that case. It still returns None if it didn't + # match such a jpeg file. + kind = _imghdr_what_wrapper(data) + if kind in ['gif', 'jpeg', 'png', 'tiff', 'bmp']: + return 'image/{0}'.format(kind) + elif kind == 'pgm': + return 'image/x-portable-graymap' + elif kind == 'pbm': + return 'image/x-portable-bitmap' + elif kind == 'ppm': + return 'image/x-portable-pixmap' + elif kind == 'xbm': + return 'image/x-xbitmap' + else: + return 'image/x-{0}'.format(kind) + + +def image_extension(data): + ext = _imghdr_what_wrapper(data) + return PREFERRED_IMAGE_EXTENSIONS.get(ext, ext) + + +class ImageType(enum.Enum): + """Indicates the kind of an `Image` stored in a file's tag. + """ + other = 0 + icon = 1 + other_icon = 2 + front = 3 + back = 4 + leaflet = 5 + media = 6 + lead_artist = 7 + artist = 8 + conductor = 9 + group = 10 + composer = 11 + lyricist = 12 + recording_location = 13 + recording_session = 14 + performance = 15 + screen_capture = 16 + fish = 17 + illustration = 18 + artist_logo = 19 + publisher_logo = 20 + + +class Image(object): + """Structure representing image data and metadata that can be + stored and retrieved from tags. + + The structure has four properties. + * ``data`` The binary data of the image + * ``desc`` An optional description of the image + * ``type`` An instance of `ImageType` indicating the kind of image + * ``mime_type`` Read-only property that contains the mime type of + the binary data + """ + def __init__(self, data, desc=None, type=None): + assert isinstance(data, bytes) + if desc is not None: + assert isinstance(desc, six.text_type) + self.data = data + self.desc = desc + if isinstance(type, int): + try: + type = list(ImageType)[type] + except IndexError: + log.debug(u"ignoring unknown image type index %s", type) + type = ImageType.other + self.type = type + + @property + def mime_type(self): + if self.data: + return image_mime_type(self.data) + + @property + def type_index(self): + if self.type is None: + # This method is used when a tag format requires the type + # index to be set, so we return "other" as the default value. + return 0 + return self.type.value + + +# StorageStyle classes describe strategies for accessing values in +# Mutagen file objects. + +class StorageStyle(object): + """A strategy for storing a value for a certain tag format (or set + of tag formats). This basic StorageStyle describes simple 1:1 + mapping from raw values to keys in a Mutagen file object; subclasses + describe more sophisticated translations or format-specific access + strategies. + + MediaFile uses a StorageStyle via three methods: ``get()``, + ``set()``, and ``delete()``. It passes a Mutagen file object to + each. + + Internally, the StorageStyle implements ``get()`` and ``set()`` + using two steps that may be overridden by subtypes. To get a value, + the StorageStyle first calls ``fetch()`` to retrieve the value + corresponding to a key and then ``deserialize()`` to convert the raw + Mutagen value to a consumable Python value. Similarly, to set a + field, we call ``serialize()`` to encode the value and then + ``store()`` to assign the result into the Mutagen object. + + Each StorageStyle type has a class-level `formats` attribute that is + a list of strings indicating the formats that the style applies to. + MediaFile only uses StorageStyles that apply to the correct type for + a given audio file. + """ + + formats = ['FLAC', 'OggOpus', 'OggTheora', 'OggSpeex', 'OggVorbis', + 'OggFlac', 'APEv2File', 'WavPack', 'Musepack', 'MonkeysAudio'] + """List of mutagen classes the StorageStyle can handle. + """ + + def __init__(self, key, as_type=six.text_type, suffix=None, + float_places=2, read_only=False): + """Create a basic storage strategy. Parameters: + + - `key`: The key on the Mutagen file object used to access the + field's data. + - `as_type`: The Python type that the value is stored as + internally (`unicode`, `int`, `bool`, or `bytes`). + - `suffix`: When `as_type` is a string type, append this before + storing the value. + - `float_places`: When the value is a floating-point number and + encoded as a string, the number of digits to store after the + decimal point. + - `read_only`: When true, writing to this field is disabled. + Primary use case is so wrongly named fields can be addressed + in a graceful manner. This does not block the delete method. + + """ + self.key = key + self.as_type = as_type + self.suffix = suffix + self.float_places = float_places + self.read_only = read_only + + # Convert suffix to correct string type. + if self.suffix and self.as_type is six.text_type \ + and not isinstance(self.suffix, six.text_type): + self.suffix = self.suffix.decode('utf-8') + + # Getter. + + def get(self, mutagen_file): + """Get the value for the field using this style. + """ + return self.deserialize(self.fetch(mutagen_file)) + + def fetch(self, mutagen_file): + """Retrieve the raw value of for this tag from the Mutagen file + object. + """ + try: + return mutagen_file[self.key][0] + except (KeyError, IndexError): + return None + + def deserialize(self, mutagen_value): + """Given a raw value stored on a Mutagen object, decode and + return the represented value. + """ + if self.suffix and isinstance(mutagen_value, six.text_type) \ + and mutagen_value.endswith(self.suffix): + return mutagen_value[:-len(self.suffix)] + else: + return mutagen_value + + # Setter. + + def set(self, mutagen_file, value): + """Assign the value for the field using this style. + """ + self.store(mutagen_file, self.serialize(value)) + + def store(self, mutagen_file, value): + """Store a serialized value in the Mutagen file object. + """ + mutagen_file[self.key] = [value] + + def serialize(self, value): + """Convert the external Python value to a type that is suitable for + storing in a Mutagen file object. + """ + if isinstance(value, float) and self.as_type is six.text_type: + value = u'{0:.{1}f}'.format(value, self.float_places) + value = self.as_type(value) + elif self.as_type is six.text_type: + if isinstance(value, bool): + # Store bools as 1/0 instead of True/False. + value = six.text_type(int(bool(value))) + elif isinstance(value, bytes): + value = value.decode('utf-8', 'ignore') + else: + value = six.text_type(value) + else: + value = self.as_type(value) + + if self.suffix: + value += self.suffix + + return value + + def delete(self, mutagen_file): + """Remove the tag from the file. + """ + if self.key in mutagen_file: + del mutagen_file[self.key] + + +class ListStorageStyle(StorageStyle): + """Abstract storage style that provides access to lists. + + The ListMediaField descriptor uses a ListStorageStyle via two + methods: ``get_list()`` and ``set_list()``. It passes a Mutagen file + object to each. + + Subclasses may overwrite ``fetch`` and ``store``. ``fetch`` must + return a (possibly empty) list and ``store`` receives a serialized + list of values as the second argument. + + The `serialize` and `deserialize` methods (from the base + `StorageStyle`) are still called with individual values. This class + handles packing and unpacking the values into lists. + """ + def get(self, mutagen_file): + """Get the first value in the field's value list. + """ + try: + return self.get_list(mutagen_file)[0] + except IndexError: + return None + + def get_list(self, mutagen_file): + """Get a list of all values for the field using this style. + """ + return [self.deserialize(item) for item in self.fetch(mutagen_file)] + + def fetch(self, mutagen_file): + """Get the list of raw (serialized) values. + """ + try: + return mutagen_file[self.key] + except KeyError: + return [] + + def set(self, mutagen_file, value): + """Set an individual value as the only value for the field using + this style. + """ + self.set_list(mutagen_file, [value]) + + def set_list(self, mutagen_file, values): + """Set all values for the field using this style. `values` + should be an iterable. + """ + self.store(mutagen_file, [self.serialize(value) for value in values]) + + def store(self, mutagen_file, values): + """Set the list of all raw (serialized) values for this field. + """ + mutagen_file[self.key] = values + + +class SoundCheckStorageStyleMixin(object): + """A mixin for storage styles that read and write iTunes SoundCheck + analysis values. The object must have an `index` field that + indicates which half of the gain/peak pair---0 or 1---the field + represents. + """ + def get(self, mutagen_file): + data = self.fetch(mutagen_file) + if data is not None: + return _sc_decode(data)[self.index] + + def set(self, mutagen_file, value): + data = self.fetch(mutagen_file) + if data is None: + gain_peak = [0, 0] + else: + gain_peak = list(_sc_decode(data)) + gain_peak[self.index] = value or 0 + data = self.serialize(_sc_encode(*gain_peak)) + self.store(mutagen_file, data) + + +class ASFStorageStyle(ListStorageStyle): + """A general storage style for Windows Media/ASF files. + """ + formats = ['ASF'] + + def deserialize(self, data): + if isinstance(data, mutagen.asf.ASFBaseAttribute): + data = data.value + return data + + +class MP4StorageStyle(StorageStyle): + """A general storage style for MPEG-4 tags. + """ + formats = ['MP4'] + + def serialize(self, value): + value = super(MP4StorageStyle, self).serialize(value) + if self.key.startswith('----:') and isinstance(value, six.text_type): + value = value.encode('utf-8') + return value + + +class MP4TupleStorageStyle(MP4StorageStyle): + """A style for storing values as part of a pair of numbers in an + MPEG-4 file. + """ + def __init__(self, key, index=0, **kwargs): + super(MP4TupleStorageStyle, self).__init__(key, **kwargs) + self.index = index + + def deserialize(self, mutagen_value): + items = mutagen_value or [] + packing_length = 2 + return list(items) + [0] * (packing_length - len(items)) + + def get(self, mutagen_file): + value = super(MP4TupleStorageStyle, self).get(mutagen_file)[self.index] + if value == 0: + # The values are always present and saved as integers. So we + # assume that "0" indicates it is not set. + return None + else: + return value + + def set(self, mutagen_file, value): + if value is None: + value = 0 + items = self.deserialize(self.fetch(mutagen_file)) + items[self.index] = int(value) + self.store(mutagen_file, items) + + def delete(self, mutagen_file): + if self.index == 0: + super(MP4TupleStorageStyle, self).delete(mutagen_file) + else: + self.set(mutagen_file, None) + + +class MP4ListStorageStyle(ListStorageStyle, MP4StorageStyle): + pass + + +class MP4SoundCheckStorageStyle(SoundCheckStorageStyleMixin, MP4StorageStyle): + def __init__(self, key, index=0, **kwargs): + super(MP4SoundCheckStorageStyle, self).__init__(key, **kwargs) + self.index = index + + +class MP4BoolStorageStyle(MP4StorageStyle): + """A style for booleans in MPEG-4 files. (MPEG-4 has an atom type + specifically for representing booleans.) + """ + def get(self, mutagen_file): + try: + return mutagen_file[self.key] + except KeyError: + return None + + def get_list(self, mutagen_file): + raise NotImplementedError(u'MP4 bool storage does not support lists') + + def set(self, mutagen_file, value): + mutagen_file[self.key] = value + + def set_list(self, mutagen_file, values): + raise NotImplementedError(u'MP4 bool storage does not support lists') + + +class MP4ImageStorageStyle(MP4ListStorageStyle): + """Store images as MPEG-4 image atoms. Values are `Image` objects. + """ + def __init__(self, **kwargs): + super(MP4ImageStorageStyle, self).__init__(key='covr', **kwargs) + + def deserialize(self, data): + return Image(data) + + def serialize(self, image): + if image.mime_type == 'image/png': + kind = mutagen.mp4.MP4Cover.FORMAT_PNG + elif image.mime_type == 'image/jpeg': + kind = mutagen.mp4.MP4Cover.FORMAT_JPEG + else: + raise ValueError(u'MP4 files only supports PNG and JPEG images') + return mutagen.mp4.MP4Cover(image.data, kind) + + +class MP3StorageStyle(StorageStyle): + """Store data in ID3 frames. + """ + formats = ['MP3', 'AIFF', 'DSF', 'WAVE'] + + def __init__(self, key, id3_lang=None, **kwargs): + """Create a new ID3 storage style. `id3_lang` is the value for + the language field of newly created frames. + """ + self.id3_lang = id3_lang + super(MP3StorageStyle, self).__init__(key, **kwargs) + + def fetch(self, mutagen_file): + try: + return mutagen_file[self.key].text[0] + except (KeyError, IndexError): + return None + + def store(self, mutagen_file, value): + frame = mutagen.id3.Frames[self.key](encoding=3, text=[value]) + mutagen_file.tags.setall(self.key, [frame]) + + +class MP3PeopleStorageStyle(MP3StorageStyle): + """Store list of people in ID3 frames. + """ + def __init__(self, key, involvement='', **kwargs): + self.involvement = involvement + super(MP3PeopleStorageStyle, self).__init__(key, **kwargs) + + def store(self, mutagen_file, value): + frames = mutagen_file.tags.getall(self.key) + + # Try modifying in place. + found = False + for frame in frames: + if frame.encoding == mutagen.id3.Encoding.UTF8: + for pair in frame.people: + if pair[0].lower() == self.involvement.lower(): + pair[1] = value + found = True + + # Try creating a new frame. + if not found: + frame = mutagen.id3.Frames[self.key]( + encoding=mutagen.id3.Encoding.UTF8, + people=[[self.involvement, value]] + ) + mutagen_file.tags.add(frame) + + def fetch(self, mutagen_file): + for frame in mutagen_file.tags.getall(self.key): + for pair in frame.people: + if pair[0].lower() == self.involvement.lower(): + try: + return pair[1] + except IndexError: + return None + + +class MP3ListStorageStyle(ListStorageStyle, MP3StorageStyle): + """Store lists of data in multiple ID3 frames. + """ + def fetch(self, mutagen_file): + try: + return mutagen_file[self.key].text + except KeyError: + return [] + + def store(self, mutagen_file, values): + frame = mutagen.id3.Frames[self.key](encoding=3, text=values) + mutagen_file.tags.setall(self.key, [frame]) + + +class MP3UFIDStorageStyle(MP3StorageStyle): + """Store string data in a UFID ID3 frame with a particular owner. + """ + def __init__(self, owner, **kwargs): + self.owner = owner + super(MP3UFIDStorageStyle, self).__init__('UFID:' + owner, **kwargs) + + def fetch(self, mutagen_file): + try: + return mutagen_file[self.key].data + except KeyError: + return None + + def store(self, mutagen_file, value): + # This field type stores text data as encoded data. + assert isinstance(value, six.text_type) + value = value.encode('utf-8') + + frames = mutagen_file.tags.getall(self.key) + for frame in frames: + # Replace existing frame data. + if frame.owner == self.owner: + frame.data = value + else: + # New frame. + frame = mutagen.id3.UFID(owner=self.owner, data=value) + mutagen_file.tags.setall(self.key, [frame]) + + +class MP3DescStorageStyle(MP3StorageStyle): + """Store data in a TXXX (or similar) ID3 frame. The frame is + selected based its ``desc`` field. + ``attr`` allows to specify name of data accessor property in the frame. + Most of frames use `text`. + ``multispec`` specifies if frame data is ``mutagen.id3.MultiSpec`` + which means that the data is being packed in the list. + """ + def __init__(self, desc=u'', key='TXXX', attr='text', multispec=True, + **kwargs): + assert isinstance(desc, six.text_type) + self.description = desc + self.attr = attr + self.multispec = multispec + super(MP3DescStorageStyle, self).__init__(key=key, **kwargs) + + def store(self, mutagen_file, value): + frames = mutagen_file.tags.getall(self.key) + if self.multispec: + value = [value] + + # Try modifying in place. + found = False + for frame in frames: + if frame.desc.lower() == self.description.lower(): + setattr(frame, self.attr, value) + frame.encoding = mutagen.id3.Encoding.UTF8 + found = True + + # Try creating a new frame. + if not found: + frame = mutagen.id3.Frames[self.key]( + desc=self.description, + encoding=mutagen.id3.Encoding.UTF8, + **{self.attr: value} + ) + if self.id3_lang: + frame.lang = self.id3_lang + mutagen_file.tags.add(frame) + + def fetch(self, mutagen_file): + for frame in mutagen_file.tags.getall(self.key): + if frame.desc.lower() == self.description.lower(): + if not self.multispec: + return getattr(frame, self.attr) + try: + return getattr(frame, self.attr)[0] + except IndexError: + return None + + def delete(self, mutagen_file): + found_frame = None + for frame in mutagen_file.tags.getall(self.key): + if frame.desc.lower() == self.description.lower(): + found_frame = frame + break + if found_frame is not None: + del mutagen_file[frame.HashKey] + + +class MP3ListDescStorageStyle(MP3DescStorageStyle, ListStorageStyle): + def __init__(self, desc=u'', key='TXXX', split_v23=False, **kwargs): + self.split_v23 = split_v23 + super(MP3ListDescStorageStyle, self).__init__( + desc=desc, key=key, **kwargs + ) + + def fetch(self, mutagen_file): + for frame in mutagen_file.tags.getall(self.key): + if frame.desc.lower() == self.description.lower(): + if mutagen_file.tags.version == (2, 3, 0) and self.split_v23: + return sum((el.split('/') for el in frame.text), []) + else: + return frame.text + return [] + + def store(self, mutagen_file, values): + self.delete(mutagen_file) + frame = mutagen.id3.Frames[self.key]( + desc=self.description, + text=values, + encoding=mutagen.id3.Encoding.UTF8, + ) + if self.id3_lang: + frame.lang = self.id3_lang + mutagen_file.tags.add(frame) + + +class MP3SlashPackStorageStyle(MP3StorageStyle): + """Store value as part of pair that is serialized as a slash- + separated string. + """ + def __init__(self, key, pack_pos=0, **kwargs): + super(MP3SlashPackStorageStyle, self).__init__(key, **kwargs) + self.pack_pos = pack_pos + + def _fetch_unpacked(self, mutagen_file): + data = self.fetch(mutagen_file) + if data: + items = six.text_type(data).split('/') + else: + items = [] + packing_length = 2 + return list(items) + [None] * (packing_length - len(items)) + + def get(self, mutagen_file): + return self._fetch_unpacked(mutagen_file)[self.pack_pos] + + def set(self, mutagen_file, value): + items = self._fetch_unpacked(mutagen_file) + items[self.pack_pos] = value + if items[0] is None: + items[0] = '' + if items[1] is None: + items.pop() # Do not store last value + self.store(mutagen_file, '/'.join(map(six.text_type, items))) + + def delete(self, mutagen_file): + if self.pack_pos == 0: + super(MP3SlashPackStorageStyle, self).delete(mutagen_file) + else: + self.set(mutagen_file, None) + + +class MP3ImageStorageStyle(ListStorageStyle, MP3StorageStyle): + """Converts between APIC frames and ``Image`` instances. + + The `get_list` method inherited from ``ListStorageStyle`` returns a + list of ``Image``s. Similarly, the `set_list` method accepts a + list of ``Image``s as its ``values`` argument. + """ + def __init__(self): + super(MP3ImageStorageStyle, self).__init__(key='APIC') + self.as_type = bytes + + def deserialize(self, apic_frame): + """Convert APIC frame into Image.""" + return Image(data=apic_frame.data, desc=apic_frame.desc, + type=apic_frame.type) + + def fetch(self, mutagen_file): + return mutagen_file.tags.getall(self.key) + + def store(self, mutagen_file, frames): + mutagen_file.tags.setall(self.key, frames) + + def delete(self, mutagen_file): + mutagen_file.tags.delall(self.key) + + def serialize(self, image): + """Return an APIC frame populated with data from ``image``. + """ + assert isinstance(image, Image) + frame = mutagen.id3.Frames[self.key]() + frame.data = image.data + frame.mime = image.mime_type + frame.desc = image.desc or u'' + + # For compatibility with OS X/iTunes prefer latin-1 if possible. + # See issue #899 + try: + frame.desc.encode("latin-1") + except UnicodeEncodeError: + frame.encoding = mutagen.id3.Encoding.UTF16 + else: + frame.encoding = mutagen.id3.Encoding.LATIN1 + + frame.type = image.type_index + return frame + + +class MP3SoundCheckStorageStyle(SoundCheckStorageStyleMixin, + MP3DescStorageStyle): + def __init__(self, index=0, **kwargs): + super(MP3SoundCheckStorageStyle, self).__init__(**kwargs) + self.index = index + + +class ASFImageStorageStyle(ListStorageStyle): + """Store images packed into Windows Media/ASF byte array attributes. + Values are `Image` objects. + """ + formats = ['ASF'] + + def __init__(self): + super(ASFImageStorageStyle, self).__init__(key='WM/Picture') + + def deserialize(self, asf_picture): + mime, data, type, desc = _unpack_asf_image(asf_picture.value) + return Image(data, desc=desc, type=type) + + def serialize(self, image): + pic = mutagen.asf.ASFByteArrayAttribute() + pic.value = _pack_asf_image(image.mime_type, image.data, + type=image.type_index, + description=image.desc or u'') + return pic + + +class VorbisImageStorageStyle(ListStorageStyle): + """Store images in Vorbis comments. Both legacy COVERART fields and + modern METADATA_BLOCK_PICTURE tags are supported. Data is + base64-encoded. Values are `Image` objects. + """ + formats = ['OggOpus', 'OggTheora', 'OggSpeex', 'OggVorbis', + 'OggFlac'] + + def __init__(self): + super(VorbisImageStorageStyle, self).__init__( + key='metadata_block_picture' + ) + self.as_type = bytes + + def fetch(self, mutagen_file): + images = [] + if 'metadata_block_picture' not in mutagen_file: + # Try legacy COVERART tags. + if 'coverart' in mutagen_file: + for data in mutagen_file['coverart']: + images.append(Image(base64.b64decode(data))) + return images + for data in mutagen_file["metadata_block_picture"]: + try: + pic = mutagen.flac.Picture(base64.b64decode(data)) + except (TypeError, AttributeError): + continue + images.append(Image(data=pic.data, desc=pic.desc, + type=pic.type)) + return images + + def store(self, mutagen_file, image_data): + # Strip all art, including legacy COVERART. + if 'coverart' in mutagen_file: + del mutagen_file['coverart'] + if 'coverartmime' in mutagen_file: + del mutagen_file['coverartmime'] + super(VorbisImageStorageStyle, self).store(mutagen_file, image_data) + + def serialize(self, image): + """Turn a Image into a base64 encoded FLAC picture block. + """ + pic = mutagen.flac.Picture() + pic.data = image.data + pic.type = image.type_index + pic.mime = image.mime_type + pic.desc = image.desc or u'' + + # Encoding with base64 returns bytes on both Python 2 and 3. + # Mutagen requires the data to be a Unicode string, so we decode + # it before passing it along. + return base64.b64encode(pic.write()).decode('ascii') + + +class FlacImageStorageStyle(ListStorageStyle): + """Converts between ``mutagen.flac.Picture`` and ``Image`` instances. + """ + formats = ['FLAC'] + + def __init__(self): + super(FlacImageStorageStyle, self).__init__(key='') + + def fetch(self, mutagen_file): + return mutagen_file.pictures + + def deserialize(self, flac_picture): + return Image(data=flac_picture.data, desc=flac_picture.desc, + type=flac_picture.type) + + def store(self, mutagen_file, pictures): + """``pictures`` is a list of mutagen.flac.Picture instances. + """ + mutagen_file.clear_pictures() + for pic in pictures: + mutagen_file.add_picture(pic) + + def serialize(self, image): + """Turn a Image into a mutagen.flac.Picture. + """ + pic = mutagen.flac.Picture() + pic.data = image.data + pic.type = image.type_index + pic.mime = image.mime_type + pic.desc = image.desc or u'' + return pic + + def delete(self, mutagen_file): + """Remove all images from the file. + """ + mutagen_file.clear_pictures() + + +class APEv2ImageStorageStyle(ListStorageStyle): + """Store images in APEv2 tags. Values are `Image` objects. + """ + formats = ['APEv2File', 'WavPack', 'Musepack', 'MonkeysAudio', 'OptimFROG'] + + TAG_NAMES = { + ImageType.other: 'Cover Art (other)', + ImageType.icon: 'Cover Art (icon)', + ImageType.other_icon: 'Cover Art (other icon)', + ImageType.front: 'Cover Art (front)', + ImageType.back: 'Cover Art (back)', + ImageType.leaflet: 'Cover Art (leaflet)', + ImageType.media: 'Cover Art (media)', + ImageType.lead_artist: 'Cover Art (lead)', + ImageType.artist: 'Cover Art (artist)', + ImageType.conductor: 'Cover Art (conductor)', + ImageType.group: 'Cover Art (band)', + ImageType.composer: 'Cover Art (composer)', + ImageType.lyricist: 'Cover Art (lyricist)', + ImageType.recording_location: 'Cover Art (studio)', + ImageType.recording_session: 'Cover Art (recording)', + ImageType.performance: 'Cover Art (performance)', + ImageType.screen_capture: 'Cover Art (movie scene)', + ImageType.fish: 'Cover Art (colored fish)', + ImageType.illustration: 'Cover Art (illustration)', + ImageType.artist_logo: 'Cover Art (band logo)', + ImageType.publisher_logo: 'Cover Art (publisher logo)', + } + + def __init__(self): + super(APEv2ImageStorageStyle, self).__init__(key='') + + def fetch(self, mutagen_file): + images = [] + for cover_type, cover_tag in self.TAG_NAMES.items(): + try: + frame = mutagen_file[cover_tag] + text_delimiter_index = frame.value.find(b'\x00') + if text_delimiter_index > 0: + comment = frame.value[0:text_delimiter_index] + comment = comment.decode('utf-8', 'replace') + else: + comment = None + image_data = frame.value[text_delimiter_index + 1:] + images.append(Image(data=image_data, type=cover_type, + desc=comment)) + except KeyError: + pass + + return images + + def set_list(self, mutagen_file, values): + self.delete(mutagen_file) + + for image in values: + image_type = image.type or ImageType.other + comment = image.desc or '' + image_data = comment.encode('utf-8') + b'\x00' + image.data + cover_tag = self.TAG_NAMES[image_type] + mutagen_file[cover_tag] = image_data + + def delete(self, mutagen_file): + """Remove all images from the file. + """ + for cover_tag in self.TAG_NAMES.values(): + try: + del mutagen_file[cover_tag] + except KeyError: + pass + + +# MediaField is a descriptor that represents a single logical field. It +# aggregates several StorageStyles describing how to access the data for +# each file type. + +class MediaField(object): + """A descriptor providing access to a particular (abstract) metadata + field. + """ + def __init__(self, *styles, **kwargs): + """Creates a new MediaField. + + :param styles: `StorageStyle` instances that describe the strategy + for reading and writing the field in particular + formats. There must be at least one style for + each possible file format. + + :param out_type: the type of the value that should be returned when + getting this property. + + """ + self.out_type = kwargs.get('out_type', six.text_type) + self._styles = styles + + def styles(self, mutagen_file): + """Yields the list of storage styles of this field that can + handle the MediaFile's format. + """ + for style in self._styles: + if mutagen_file.__class__.__name__ in style.formats: + yield style + + def __get__(self, mediafile, owner=None): + out = None + for style in self.styles(mediafile.mgfile): + out = style.get(mediafile.mgfile) + if out: + break + return _safe_cast(self.out_type, out) + + def __set__(self, mediafile, value): + if value is None: + value = self._none_value() + for style in self.styles(mediafile.mgfile): + if not style.read_only: + style.set(mediafile.mgfile, value) + + def __delete__(self, mediafile): + for style in self.styles(mediafile.mgfile): + style.delete(mediafile.mgfile) + + def _none_value(self): + """Get an appropriate "null" value for this field's type. This + is used internally when setting the field to None. + """ + if self.out_type == int: + return 0 + elif self.out_type == float: + return 0.0 + elif self.out_type == bool: + return False + elif self.out_type == six.text_type: + return u'' + + +class ListMediaField(MediaField): + """Property descriptor that retrieves a list of multiple values from + a tag. + + Uses ``get_list`` and set_list`` methods of its ``StorageStyle`` + strategies to do the actual work. + """ + def __get__(self, mediafile, _=None): + for style in self.styles(mediafile.mgfile): + values = style.get_list(mediafile.mgfile) + if values: + return [_safe_cast(self.out_type, value) for value in values] + return [] + + def __set__(self, mediafile, values): + for style in self.styles(mediafile.mgfile): + if not style.read_only: + style.set_list(mediafile.mgfile, values) + + def single_field(self): + """Returns a ``MediaField`` descriptor that gets and sets the + first item. + """ + options = {'out_type': self.out_type} + return MediaField(*self._styles, **options) + + +class DateField(MediaField): + """Descriptor that handles serializing and deserializing dates + + The getter parses value from tags into a ``datetime.date`` instance + and setter serializes such an instance into a string. + + For granular access to year, month, and day, use the ``*_field`` + methods to create corresponding `DateItemField`s. + """ + def __init__(self, *date_styles, **kwargs): + """``date_styles`` is a list of ``StorageStyle``s to store and + retrieve the whole date from. The ``year`` option is an + additional list of fallback styles for the year. The year is + always set on this style, but is only retrieved if the main + storage styles do not return a value. + """ + super(DateField, self).__init__(*date_styles) + year_style = kwargs.get('year', None) + if year_style: + self._year_field = MediaField(*year_style) + + def __get__(self, mediafile, owner=None): + year, month, day = self._get_date_tuple(mediafile) + if not year: + return None + try: + return datetime.date( + year, + month or 1, + day or 1 + ) + except ValueError: # Out of range values. + return None + + def __set__(self, mediafile, date): + if date is None: + self._set_date_tuple(mediafile, None, None, None) + else: + self._set_date_tuple(mediafile, date.year, date.month, date.day) + + def __delete__(self, mediafile): + super(DateField, self).__delete__(mediafile) + if hasattr(self, '_year_field'): + self._year_field.__delete__(mediafile) + + def _get_date_tuple(self, mediafile): + """Get a 3-item sequence representing the date consisting of a + year, month, and day number. Each number is either an integer or + None. + """ + # Get the underlying data and split on hyphens and slashes. + datestring = super(DateField, self).__get__(mediafile, None) + if isinstance(datestring, six.string_types): + datestring = re.sub(r'[Tt ].*$', '', six.text_type(datestring)) + items = re.split('[-/]', six.text_type(datestring)) + else: + items = [] + + # Ensure that we have exactly 3 components, possibly by + # truncating or padding. + items = items[:3] + if len(items) < 3: + items += [None] * (3 - len(items)) + + # Use year field if year is missing. + if not items[0] and hasattr(self, '_year_field'): + items[0] = self._year_field.__get__(mediafile) + + # Convert each component to an integer if possible. + items_ = [] + for item in items: + try: + items_.append(int(item)) + except (TypeError, ValueError): + items_.append(None) + return items_ + + def _set_date_tuple(self, mediafile, year, month=None, day=None): + """Set the value of the field given a year, month, and day + number. Each number can be an integer or None to indicate an + unset component. + """ + if year is None: + self.__delete__(mediafile) + return + + date = [u'{0:04d}'.format(int(year))] + if month: + date.append(u'{0:02d}'.format(int(month))) + if month and day: + date.append(u'{0:02d}'.format(int(day))) + date = map(six.text_type, date) + super(DateField, self).__set__(mediafile, u'-'.join(date)) + + if hasattr(self, '_year_field'): + self._year_field.__set__(mediafile, year) + + def year_field(self): + return DateItemField(self, 0) + + def month_field(self): + return DateItemField(self, 1) + + def day_field(self): + return DateItemField(self, 2) + + +class DateItemField(MediaField): + """Descriptor that gets and sets constituent parts of a `DateField`: + the month, day, or year. + """ + def __init__(self, date_field, item_pos): + self.date_field = date_field + self.item_pos = item_pos + + def __get__(self, mediafile, _): + return self.date_field._get_date_tuple(mediafile)[self.item_pos] + + def __set__(self, mediafile, value): + items = self.date_field._get_date_tuple(mediafile) + items[self.item_pos] = value + self.date_field._set_date_tuple(mediafile, *items) + + def __delete__(self, mediafile): + self.__set__(mediafile, None) + + +class CoverArtField(MediaField): + """A descriptor that provides access to the *raw image data* for the + cover image on a file. This is used for backwards compatibility: the + full `ImageListField` provides richer `Image` objects. + + When there are multiple images we try to pick the most likely to be a front + cover. + """ + def __init__(self): + pass + + def __get__(self, mediafile, _): + candidates = mediafile.images + if candidates: + return self.guess_cover_image(candidates).data + else: + return None + + @staticmethod + def guess_cover_image(candidates): + if len(candidates) == 1: + return candidates[0] + try: + return next(c for c in candidates if c.type == ImageType.front) + except StopIteration: + return candidates[0] + + def __set__(self, mediafile, data): + if data: + mediafile.images = [Image(data=data)] + else: + mediafile.images = [] + + def __delete__(self, mediafile): + delattr(mediafile, 'images') + + +class QNumberField(MediaField): + """Access integer-represented Q number fields. + + Access a fixed-point fraction as a float. The stored value is shifted by + `fraction_bits` binary digits to the left and then rounded, yielding a + simple integer. + """ + def __init__(self, fraction_bits, *args, **kwargs): + super(QNumberField, self).__init__(out_type=int, *args, **kwargs) + self.__fraction_bits = fraction_bits + + def __get__(self, mediafile, owner=None): + q_num = super(QNumberField, self).__get__(mediafile, owner) + if q_num is None: + return None + return q_num / pow(2, self.__fraction_bits) + + def __set__(self, mediafile, value): + q_num = round(value * pow(2, self.__fraction_bits)) + q_num = int(q_num) # needed for py2.7 + super(QNumberField, self).__set__(mediafile, q_num) + + +class ImageListField(ListMediaField): + """Descriptor to access the list of images embedded in tags. + + The getter returns a list of `Image` instances obtained from + the tags. The setter accepts a list of `Image` instances to be + written to the tags. + """ + def __init__(self): + # The storage styles used here must implement the + # `ListStorageStyle` interface and get and set lists of + # `Image`s. + super(ImageListField, self).__init__( + MP3ImageStorageStyle(), + MP4ImageStorageStyle(), + ASFImageStorageStyle(), + VorbisImageStorageStyle(), + FlacImageStorageStyle(), + APEv2ImageStorageStyle(), + out_type=Image, + ) + + +# MediaFile is a collection of fields. + +class MediaFile(object): + """Represents a multimedia file on disk and provides access to its + metadata. + """ + @loadfile() + def __init__(self, filething, id3v23=False): + """Constructs a new `MediaFile` reflecting the provided file. + + `filething` can be a path to a file (i.e., a string) or a + file-like object. + + May throw `UnreadableFileError`. + + By default, MP3 files are saved with ID3v2.4 tags. You can use + the older ID3v2.3 standard by specifying the `id3v23` option. + """ + self.filething = filething + + self.mgfile = mutagen_call( + 'open', self.filename, mutagen.File, filething + ) + + if self.mgfile is None: + # Mutagen couldn't guess the type + raise FileTypeError(self.filename) + elif type(self.mgfile).__name__ in ['M4A', 'MP4']: + info = self.mgfile.info + if info.codec and info.codec.startswith('alac'): + self.type = 'alac' + else: + self.type = 'aac' + elif type(self.mgfile).__name__ in ['ID3', 'MP3']: + self.type = 'mp3' + elif type(self.mgfile).__name__ == 'FLAC': + self.type = 'flac' + elif type(self.mgfile).__name__ == 'OggOpus': + self.type = 'opus' + elif type(self.mgfile).__name__ == 'OggVorbis': + self.type = 'ogg' + elif type(self.mgfile).__name__ == 'MonkeysAudio': + self.type = 'ape' + elif type(self.mgfile).__name__ == 'WavPack': + self.type = 'wv' + elif type(self.mgfile).__name__ == 'Musepack': + self.type = 'mpc' + elif type(self.mgfile).__name__ == 'ASF': + self.type = 'asf' + elif type(self.mgfile).__name__ == 'AIFF': + self.type = 'aiff' + elif type(self.mgfile).__name__ == 'DSF': + self.type = 'dsf' + elif type(self.mgfile).__name__ == 'WAVE': + self.type = 'wav' + else: + raise FileTypeError(self.filename, type(self.mgfile).__name__) + + # Add a set of tags if it's missing. + if self.mgfile.tags is None: + self.mgfile.add_tags() + + # Set the ID3v2.3 flag only for MP3s. + self.id3v23 = id3v23 and self.type == 'mp3' + + @property + def filename(self): + """The name of the file. + + This is the path if this object was opened from the filesystem, + or the name of the file-like object. + """ + return self.filething.name + + @filename.setter + def filename(self, val): + """Silently skips setting filename. + Workaround for `mutagen._util._openfile` setting instance's filename. + """ + pass + + @property + def path(self): + """The path to the file. + + This is `None` if the data comes from a file-like object instead + of a filesystem path. + """ + return self.filething.filename + + @property + def filesize(self): + """The size (in bytes) of the underlying file. + """ + if self.filething.filename: + return os.path.getsize(self.filething.filename) + if hasattr(self.filething.fileobj, '__len__'): + return len(self.filething.fileobj) + else: + tell = self.filething.fileobj.tell() + filesize = self.filething.fileobj.seek(0, 2) + self.filething.fileobj.seek(tell) + return filesize + + def save(self, **kwargs): + """Write the object's tags back to the file. + + May throw `UnreadableFileError`. Accepts keyword arguments to be + passed to Mutagen's `save` function. + """ + # Possibly save the tags to ID3v2.3. + if self.id3v23: + id3 = self.mgfile + if hasattr(id3, 'tags'): + # In case this is an MP3 object, not an ID3 object. + id3 = id3.tags + id3.update_to_v23() + kwargs['v2_version'] = 3 + + mutagen_call('save', self.filename, self.mgfile.save, + _update_filething(self.filething), **kwargs) + + def delete(self): + """Remove the current metadata tag from the file. May + throw `UnreadableFileError`. + """ + mutagen_call('delete', self.filename, self.mgfile.delete, + _update_filething(self.filething)) + + # Convenient access to the set of available fields. + + @classmethod + def fields(cls): + """Get the names of all writable properties that reflect + metadata tags (i.e., those that are instances of + :class:`MediaField`). + """ + for property, descriptor in cls.__dict__.items(): + if isinstance(descriptor, MediaField): + if isinstance(property, bytes): + # On Python 2, class field names are bytes. This method + # produces text strings. + yield property.decode('utf8', 'ignore') + else: + yield property + + @classmethod + def _field_sort_name(cls, name): + """Get a sort key for a field name that determines the order + fields should be written in. + + Fields names are kept unchanged, unless they are instances of + :class:`DateItemField`, in which case `year`, `month`, and `day` + are replaced by `date0`, `date1`, and `date2`, respectively, to + make them appear in that order. + """ + if isinstance(cls.__dict__[name], DateItemField): + name = re.sub('year', 'date0', name) + name = re.sub('month', 'date1', name) + name = re.sub('day', 'date2', name) + return name + + @classmethod + def sorted_fields(cls): + """Get the names of all writable metadata fields, sorted in the + order that they should be written. + + This is a lexicographic order, except for instances of + :class:`DateItemField`, which are sorted in year-month-day + order. + """ + for property in sorted(cls.fields(), key=cls._field_sort_name): + yield property + + @classmethod + def readable_fields(cls): + """Get all metadata fields: the writable ones from + :meth:`fields` and also other audio properties. + """ + for property in cls.fields(): + yield property + for property in ('length', 'samplerate', 'bitdepth', 'bitrate', + 'bitrate_mode', 'channels', 'encoder_info', + 'encoder_settings', 'format'): + yield property + + @classmethod + def add_field(cls, name, descriptor): + """Add a field to store custom tags. + + :param name: the name of the property the field is accessed + through. It must not already exist on this class. + + :param descriptor: an instance of :class:`MediaField`. + """ + if not isinstance(descriptor, MediaField): + raise ValueError( + u'{0} must be an instance of MediaField'.format(descriptor)) + if name in cls.__dict__: + raise ValueError( + u'property "{0}" already exists on MediaFile'.format(name)) + setattr(cls, name, descriptor) + + def update(self, dict): + """Set all field values from a dictionary. + + For any key in `dict` that is also a field to store tags the + method retrieves the corresponding value from `dict` and updates + the `MediaFile`. If a key has the value `None`, the + corresponding property is deleted from the `MediaFile`. + """ + for field in self.sorted_fields(): + if field in dict: + if dict[field] is None: + delattr(self, field) + else: + setattr(self, field, dict[field]) + + def as_dict(self): + """Get a dictionary with all writable properties that reflect + metadata tags (i.e., those that are instances of + :class:`MediaField`). + """ + return dict((x, getattr(self, x)) for x in self.fields()) + + # Field definitions. + + title = MediaField( + MP3StorageStyle('TIT2'), + MP4StorageStyle('\xa9nam'), + StorageStyle('TITLE'), + ASFStorageStyle('Title'), + ) + artist = MediaField( + MP3StorageStyle('TPE1'), + MP4StorageStyle('\xa9ART'), + StorageStyle('ARTIST'), + ASFStorageStyle('Author'), + ) + artists = ListMediaField( + MP3ListDescStorageStyle(desc=u'ARTISTS'), + MP4ListStorageStyle('----:com.apple.iTunes:ARTISTS'), + ListStorageStyle('ARTISTS'), + ASFStorageStyle('WM/ARTISTS'), + ) + album = MediaField( + MP3StorageStyle('TALB'), + MP4StorageStyle('\xa9alb'), + StorageStyle('ALBUM'), + ASFStorageStyle('WM/AlbumTitle'), + ) + genres = ListMediaField( + MP3ListStorageStyle('TCON'), + MP4ListStorageStyle('\xa9gen'), + ListStorageStyle('GENRE'), + ASFStorageStyle('WM/Genre'), + ) + genre = genres.single_field() + + lyricist = MediaField( + MP3StorageStyle('TEXT'), + MP4StorageStyle('----:com.apple.iTunes:LYRICIST'), + StorageStyle('LYRICIST'), + ASFStorageStyle('WM/Writer'), + ) + composer = MediaField( + MP3StorageStyle('TCOM'), + MP4StorageStyle('\xa9wrt'), + StorageStyle('COMPOSER'), + ASFStorageStyle('WM/Composer'), + ) + composer_sort = MediaField( + MP3StorageStyle('TSOC'), + MP4StorageStyle('soco'), + StorageStyle('COMPOSERSORT'), + ASFStorageStyle('WM/Composersortorder'), + ) + arranger = MediaField( + MP3PeopleStorageStyle('TIPL', involvement='arranger'), + MP4StorageStyle('----:com.apple.iTunes:Arranger'), + StorageStyle('ARRANGER'), + ASFStorageStyle('beets/Arranger'), + ) + + grouping = MediaField( + MP3StorageStyle('TIT1'), + MP4StorageStyle('\xa9grp'), + StorageStyle('GROUPING'), + ASFStorageStyle('WM/ContentGroupDescription'), + ) + track = MediaField( + MP3SlashPackStorageStyle('TRCK', pack_pos=0), + MP4TupleStorageStyle('trkn', index=0), + StorageStyle('TRACK'), + StorageStyle('TRACKNUMBER'), + ASFStorageStyle('WM/TrackNumber'), + out_type=int, + ) + tracktotal = MediaField( + MP3SlashPackStorageStyle('TRCK', pack_pos=1), + MP4TupleStorageStyle('trkn', index=1), + StorageStyle('TRACKTOTAL'), + StorageStyle('TRACKC'), + StorageStyle('TOTALTRACKS'), + ASFStorageStyle('TotalTracks'), + out_type=int, + ) + disc = MediaField( + MP3SlashPackStorageStyle('TPOS', pack_pos=0), + MP4TupleStorageStyle('disk', index=0), + StorageStyle('DISC'), + StorageStyle('DISCNUMBER'), + ASFStorageStyle('WM/PartOfSet'), + out_type=int, + ) + disctotal = MediaField( + MP3SlashPackStorageStyle('TPOS', pack_pos=1), + MP4TupleStorageStyle('disk', index=1), + StorageStyle('DISCTOTAL'), + StorageStyle('DISCC'), + StorageStyle('TOTALDISCS'), + ASFStorageStyle('TotalDiscs'), + out_type=int, + ) + + url = MediaField( + MP3DescStorageStyle(key='WXXX', attr='url', multispec=False), + MP4StorageStyle('\xa9url'), + StorageStyle('URL'), + ASFStorageStyle('WM/URL'), + ) + lyrics = MediaField( + MP3DescStorageStyle(key='USLT', multispec=False), + MP4StorageStyle('\xa9lyr'), + StorageStyle('LYRICS'), + ASFStorageStyle('WM/Lyrics'), + ) + comments = MediaField( + MP3DescStorageStyle(key='COMM'), + MP4StorageStyle('\xa9cmt'), + StorageStyle('DESCRIPTION'), + StorageStyle('COMMENT'), + ASFStorageStyle('WM/Comments'), + ASFStorageStyle('Description') + ) + copyright = MediaField( + MP3StorageStyle('TCOP'), + MP4StorageStyle('cprt'), + StorageStyle('COPYRIGHT'), + ASFStorageStyle('Copyright'), + ) + bpm = MediaField( + MP3StorageStyle('TBPM'), + MP4StorageStyle('tmpo', as_type=int), + StorageStyle('BPM'), + ASFStorageStyle('WM/BeatsPerMinute'), + out_type=int, + ) + comp = MediaField( + MP3StorageStyle('TCMP'), + MP4BoolStorageStyle('cpil'), + StorageStyle('COMPILATION'), + ASFStorageStyle('WM/IsCompilation', as_type=bool), + out_type=bool, + ) + albumartist = MediaField( + MP3StorageStyle('TPE2'), + MP4StorageStyle('aART'), + StorageStyle('ALBUM ARTIST'), + StorageStyle('ALBUM_ARTIST'), + StorageStyle('ALBUMARTIST'), + ASFStorageStyle('WM/AlbumArtist'), + ) + albumartists = ListMediaField( + MP3ListDescStorageStyle(desc=u'ALBUMARTISTS'), + MP3ListDescStorageStyle(desc=u'ALBUM_ARTISTS'), + MP3ListDescStorageStyle(desc=u'ALBUM ARTISTS', read_only=True), + MP4ListStorageStyle('----:com.apple.iTunes:ALBUMARTISTS'), + MP4ListStorageStyle('----:com.apple.iTunes:ALBUM_ARTISTS'), + MP4ListStorageStyle( + '----:com.apple.iTunes:ALBUM ARTISTS', read_only=True + ), + ListStorageStyle('ALBUMARTISTS'), + ListStorageStyle('ALBUM_ARTISTS'), + ListStorageStyle('ALBUM ARTISTS', read_only=True), + ASFStorageStyle('WM/AlbumArtists'), + ) + albumtypes = ListMediaField( + MP3ListDescStorageStyle('MusicBrainz Album Type', split_v23=True), + MP4ListStorageStyle('----:com.apple.iTunes:MusicBrainz Album Type'), + ListStorageStyle('RELEASETYPE'), + ListStorageStyle('MUSICBRAINZ_ALBUMTYPE'), + ASFStorageStyle('MusicBrainz/Album Type'), + ) + albumtype = albumtypes.single_field() + + label = MediaField( + MP3StorageStyle('TPUB'), + MP4StorageStyle('----:com.apple.iTunes:LABEL'), + MP4StorageStyle('----:com.apple.iTunes:publisher'), + MP4StorageStyle('----:com.apple.iTunes:Label', read_only=True), + StorageStyle('LABEL'), + StorageStyle('PUBLISHER'), # Traktor + ASFStorageStyle('WM/Publisher'), + ) + artist_sort = MediaField( + MP3StorageStyle('TSOP'), + MP4StorageStyle('soar'), + StorageStyle('ARTISTSORT'), + ASFStorageStyle('WM/ArtistSortOrder'), + ) + albumartist_sort = MediaField( + MP3DescStorageStyle(u'ALBUMARTISTSORT'), + MP4StorageStyle('soaa'), + StorageStyle('ALBUMARTISTSORT'), + ASFStorageStyle('WM/AlbumArtistSortOrder'), + ) + asin = MediaField( + MP3DescStorageStyle(u'ASIN'), + MP4StorageStyle('----:com.apple.iTunes:ASIN'), + StorageStyle('ASIN'), + ASFStorageStyle('MusicBrainz/ASIN'), + ) + catalognums = ListMediaField( + MP3ListDescStorageStyle('CATALOGNUMBER', split_v23=True), + MP3ListDescStorageStyle('CATALOGID', read_only=True), + MP3ListDescStorageStyle('DISCOGS_CATALOG', read_only=True), + MP4ListStorageStyle('----:com.apple.iTunes:CATALOGNUMBER'), + MP4ListStorageStyle( + '----:com.apple.iTunes:CATALOGID', read_only=True + ), + MP4ListStorageStyle( + '----:com.apple.iTunes:DISCOGS_CATALOG', read_only=True + ), + ListStorageStyle('CATALOGNUMBER'), + ListStorageStyle('CATALOGID', read_only=True), + ListStorageStyle('DISCOGS_CATALOG', read_only=True), + ASFStorageStyle('WM/CatalogNo'), + ASFStorageStyle('CATALOGID', read_only=True), + ASFStorageStyle('DISCOGS_CATALOG', read_only=True), + ) + catalognum = catalognums.single_field() + + barcode = MediaField( + MP3DescStorageStyle(u'BARCODE'), + MP4StorageStyle('----:com.apple.iTunes:BARCODE'), + StorageStyle('BARCODE'), + StorageStyle('UPC', read_only=True), + StorageStyle('EAN/UPN', read_only=True), + StorageStyle('EAN', read_only=True), + StorageStyle('UPN', read_only=True), + ASFStorageStyle('WM/Barcode'), + ) + isrc = MediaField( + MP3StorageStyle(u'TSRC'), + MP4StorageStyle('----:com.apple.iTunes:ISRC'), + StorageStyle('ISRC'), + ASFStorageStyle('WM/ISRC'), + ) + disctitle = MediaField( + MP3StorageStyle('TSST'), + MP4StorageStyle('----:com.apple.iTunes:DISCSUBTITLE'), + StorageStyle('DISCSUBTITLE'), + ASFStorageStyle('WM/SetSubTitle'), + ) + encoder = MediaField( + MP3StorageStyle('TENC'), + MP4StorageStyle('\xa9too'), + StorageStyle('ENCODEDBY'), + StorageStyle('ENCODER'), + ASFStorageStyle('WM/EncodedBy'), + ) + script = MediaField( + MP3DescStorageStyle(u'Script'), + MP4StorageStyle('----:com.apple.iTunes:SCRIPT'), + StorageStyle('SCRIPT'), + ASFStorageStyle('WM/Script'), + ) + languages = ListMediaField( + MP3ListStorageStyle('TLAN'), + MP4ListStorageStyle('----:com.apple.iTunes:LANGUAGE'), + ListStorageStyle('LANGUAGE'), + ASFStorageStyle('WM/Language'), + ) + language = languages.single_field() + + country = MediaField( + MP3DescStorageStyle(u'MusicBrainz Album Release Country'), + MP4StorageStyle('----:com.apple.iTunes:MusicBrainz ' + 'Album Release Country'), + StorageStyle('RELEASECOUNTRY'), + ASFStorageStyle('MusicBrainz/Album Release Country'), + ) + albumstatus = MediaField( + MP3DescStorageStyle(u'MusicBrainz Album Status'), + MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Album Status'), + StorageStyle('RELEASESTATUS'), + StorageStyle('MUSICBRAINZ_ALBUMSTATUS'), + ASFStorageStyle('MusicBrainz/Album Status'), + ) + media = MediaField( + MP3StorageStyle('TMED'), + MP4StorageStyle('----:com.apple.iTunes:MEDIA'), + StorageStyle('MEDIA'), + ASFStorageStyle('WM/Media'), + ) + albumdisambig = MediaField( + # This tag mapping was invented for beets (not used by Picard, etc). + MP3DescStorageStyle(u'MusicBrainz Album Comment'), + MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Album Comment'), + StorageStyle('MUSICBRAINZ_ALBUMCOMMENT'), + ASFStorageStyle('MusicBrainz/Album Comment'), + ) + + # Release date. + date = DateField( + MP3StorageStyle('TDRC'), + MP4StorageStyle('\xa9day'), + StorageStyle('DATE'), + ASFStorageStyle('WM/Year'), + year=(StorageStyle('YEAR'),)) + + year = date.year_field() + month = date.month_field() + day = date.day_field() + + # *Original* release date. + original_date = DateField( + MP3StorageStyle('TDOR'), + MP4StorageStyle('----:com.apple.iTunes:ORIGINAL YEAR'), + StorageStyle('ORIGINALDATE'), + ASFStorageStyle('WM/OriginalReleaseYear')) + + original_year = original_date.year_field() + original_month = original_date.month_field() + original_day = original_date.day_field() + + # Nonstandard metadata. + artist_credit = MediaField( + MP3DescStorageStyle(u'Artist Credit'), + MP4StorageStyle('----:com.apple.iTunes:Artist Credit'), + StorageStyle('ARTIST_CREDIT'), + ASFStorageStyle('beets/Artist Credit'), + ) + albumartist_credit = MediaField( + MP3DescStorageStyle(u'Album Artist Credit'), + MP4StorageStyle('----:com.apple.iTunes:Album Artist Credit'), + StorageStyle('ALBUMARTIST_CREDIT'), + ASFStorageStyle('beets/Album Artist Credit'), + ) + + # Legacy album art field + art = CoverArtField() + + # Image list + images = ImageListField() + + # MusicBrainz IDs. + mb_trackid = MediaField( + MP3UFIDStorageStyle(owner='http://musicbrainz.org'), + MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Track Id'), + StorageStyle('MUSICBRAINZ_TRACKID'), + ASFStorageStyle('MusicBrainz/Track Id'), + ) + mb_releasetrackid = MediaField( + MP3DescStorageStyle(u'MusicBrainz Release Track Id'), + MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Release Track Id'), + StorageStyle('MUSICBRAINZ_RELEASETRACKID'), + ASFStorageStyle('MusicBrainz/Release Track Id'), + ) + mb_workid = MediaField( + MP3DescStorageStyle(u'MusicBrainz Work Id'), + MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Work Id'), + StorageStyle('MUSICBRAINZ_WORKID'), + ASFStorageStyle('MusicBrainz/Work Id'), + ) + mb_albumid = MediaField( + MP3DescStorageStyle(u'MusicBrainz Album Id'), + MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Album Id'), + StorageStyle('MUSICBRAINZ_ALBUMID'), + ASFStorageStyle('MusicBrainz/Album Id'), + ) + mb_artistids = ListMediaField( + MP3ListDescStorageStyle(u'MusicBrainz Artist Id', split_v23=True), + MP4ListStorageStyle('----:com.apple.iTunes:MusicBrainz Artist Id'), + ListStorageStyle('MUSICBRAINZ_ARTISTID'), + ASFStorageStyle('MusicBrainz/Artist Id'), + ) + mb_artistid = mb_artistids.single_field() + + mb_albumartistids = ListMediaField( + MP3ListDescStorageStyle( + u'MusicBrainz Album Artist Id', + split_v23=True, + ), + MP4ListStorageStyle( + '----:com.apple.iTunes:MusicBrainz Album Artist Id', + ), + ListStorageStyle('MUSICBRAINZ_ALBUMARTISTID'), + ASFStorageStyle('MusicBrainz/Album Artist Id'), + ) + mb_albumartistid = mb_albumartistids.single_field() + + mb_releasegroupid = MediaField( + MP3DescStorageStyle(u'MusicBrainz Release Group Id'), + MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Release Group Id'), + StorageStyle('MUSICBRAINZ_RELEASEGROUPID'), + ASFStorageStyle('MusicBrainz/Release Group Id'), + ) + + # Acoustid fields. + acoustid_fingerprint = MediaField( + MP3DescStorageStyle(u'Acoustid Fingerprint'), + MP4StorageStyle('----:com.apple.iTunes:Acoustid Fingerprint'), + StorageStyle('ACOUSTID_FINGERPRINT'), + ASFStorageStyle('Acoustid/Fingerprint'), + ) + acoustid_id = MediaField( + MP3DescStorageStyle(u'Acoustid Id'), + MP4StorageStyle('----:com.apple.iTunes:Acoustid Id'), + StorageStyle('ACOUSTID_ID'), + ASFStorageStyle('Acoustid/Id'), + ) + + # ReplayGain fields. + rg_track_gain = MediaField( + MP3DescStorageStyle( + u'REPLAYGAIN_TRACK_GAIN', + float_places=2, suffix=u' dB' + ), + MP3DescStorageStyle( + u'replaygain_track_gain', + float_places=2, suffix=u' dB' + ), + MP3SoundCheckStorageStyle( + key='COMM', + index=0, desc=u'iTunNORM', + id3_lang='eng' + ), + MP4StorageStyle( + '----:com.apple.iTunes:replaygain_track_gain', + float_places=2, suffix=' dB' + ), + MP4SoundCheckStorageStyle( + '----:com.apple.iTunes:iTunNORM', + index=0 + ), + StorageStyle( + u'REPLAYGAIN_TRACK_GAIN', + float_places=2, suffix=u' dB' + ), + ASFStorageStyle( + u'replaygain_track_gain', + float_places=2, suffix=u' dB' + ), + out_type=float + ) + rg_album_gain = MediaField( + MP3DescStorageStyle( + u'REPLAYGAIN_ALBUM_GAIN', + float_places=2, suffix=u' dB' + ), + MP3DescStorageStyle( + u'replaygain_album_gain', + float_places=2, suffix=u' dB' + ), + MP4StorageStyle( + '----:com.apple.iTunes:replaygain_album_gain', + float_places=2, suffix=' dB' + ), + StorageStyle( + u'REPLAYGAIN_ALBUM_GAIN', + float_places=2, suffix=u' dB' + ), + ASFStorageStyle( + u'replaygain_album_gain', + float_places=2, suffix=u' dB' + ), + out_type=float + ) + rg_track_peak = MediaField( + MP3DescStorageStyle( + u'REPLAYGAIN_TRACK_PEAK', + float_places=6 + ), + MP3DescStorageStyle( + u'replaygain_track_peak', + float_places=6 + ), + MP3SoundCheckStorageStyle( + key=u'COMM', + index=1, desc=u'iTunNORM', + id3_lang='eng' + ), + MP4StorageStyle( + '----:com.apple.iTunes:replaygain_track_peak', + float_places=6 + ), + MP4SoundCheckStorageStyle( + '----:com.apple.iTunes:iTunNORM', + index=1 + ), + StorageStyle(u'REPLAYGAIN_TRACK_PEAK', float_places=6), + ASFStorageStyle(u'replaygain_track_peak', float_places=6), + out_type=float, + ) + rg_album_peak = MediaField( + MP3DescStorageStyle( + u'REPLAYGAIN_ALBUM_PEAK', + float_places=6 + ), + MP3DescStorageStyle( + u'replaygain_album_peak', + float_places=6 + ), + MP4StorageStyle( + '----:com.apple.iTunes:replaygain_album_peak', + float_places=6 + ), + StorageStyle(u'REPLAYGAIN_ALBUM_PEAK', float_places=6), + ASFStorageStyle(u'replaygain_album_peak', float_places=6), + out_type=float, + ) + + # EBU R128 fields. + r128_track_gain = QNumberField( + 8, + MP3DescStorageStyle( + u'R128_TRACK_GAIN' + ), + MP4StorageStyle( + '----:com.apple.iTunes:R128_TRACK_GAIN' + ), + StorageStyle( + u'R128_TRACK_GAIN' + ), + ASFStorageStyle( + u'R128_TRACK_GAIN' + ), + ) + r128_album_gain = QNumberField( + 8, + MP3DescStorageStyle( + u'R128_ALBUM_GAIN' + ), + MP4StorageStyle( + '----:com.apple.iTunes:R128_ALBUM_GAIN' + ), + StorageStyle( + u'R128_ALBUM_GAIN' + ), + ASFStorageStyle( + u'R128_ALBUM_GAIN' + ), + ) + + initial_key = MediaField( + MP3StorageStyle('TKEY'), + MP4StorageStyle('----:com.apple.iTunes:initialkey'), + StorageStyle('INITIALKEY'), + ASFStorageStyle('INITIALKEY'), + ) + + @property + def length(self): + """The duration of the audio in seconds (a float).""" + return self.mgfile.info.length + + @property + def samplerate(self): + """The audio's sample rate (an int).""" + if hasattr(self.mgfile.info, 'sample_rate'): + return self.mgfile.info.sample_rate + elif self.type == 'opus': + # Opus is always 48kHz internally. + return 48000 + return 0 + + @property + def bitdepth(self): + """The number of bits per sample in the audio encoding (an int). + Only available for certain file formats (zero where + unavailable). + """ + if hasattr(self.mgfile.info, 'bits_per_sample'): + return self.mgfile.info.bits_per_sample + return 0 + + @property + def channels(self): + """The number of channels in the audio (an int).""" + if hasattr(self.mgfile.info, 'channels'): + return self.mgfile.info.channels + return 0 + + @property + def bitrate(self): + """The number of bits per seconds used in the audio coding (an + int). If this is provided explicitly by the compressed file + format, this is a precise reflection of the encoding. Otherwise, + it is estimated from the on-disk file size. In this case, some + imprecision is possible because the file header is incorporated + in the file size. + """ + if hasattr(self.mgfile.info, 'bitrate') and self.mgfile.info.bitrate: + # Many formats provide it explicitly. + return self.mgfile.info.bitrate + else: + # Otherwise, we calculate bitrate from the file size. (This + # is the case for all of the lossless formats.) + if not self.length: + # Avoid division by zero if length is not available. + return 0 + return int(self.filesize * 8 / self.length) + + @property + def bitrate_mode(self): + """The mode of the bitrate used in the audio coding + (a string, eg. "CBR", "VBR" or "ABR"). + Only available for the MP3 file format (empty where unavailable). + """ + if hasattr(self.mgfile.info, 'bitrate_mode'): + return { + mutagen.mp3.BitrateMode.CBR: 'CBR', + mutagen.mp3.BitrateMode.VBR: 'VBR', + mutagen.mp3.BitrateMode.ABR: 'ABR', + }.get(self.mgfile.info.bitrate_mode, '') + else: + return '' + + @property + def encoder_info(self): + """The name and/or version of the encoder used + (a string, eg. "LAME 3.97.0"). + Only available for some formats (empty where unavailable). + """ + if hasattr(self.mgfile.info, 'encoder_info'): + return self.mgfile.info.encoder_info + else: + return '' + + @property + def encoder_settings(self): + """A guess of the settings used for the encoder (a string, eg. "-V2"). + Only available for the MP3 file format (empty where unavailable). + """ + if hasattr(self.mgfile.info, 'encoder_settings'): + return self.mgfile.info.encoder_settings + else: + return '' + + @property + def format(self): + """A string describing the file format/codec.""" + return TYPES[self.type] diff --git a/libs/common/munkres.py b/libs/common/munkres.py index 3a70ff06..2f2edbc5 100644 --- a/libs/common/munkres.py +++ b/libs/common/munkres.py @@ -1,8 +1,3 @@ -#!/usr/bin/env python -# -*- coding: iso-8859-1 -*- - -# Documentation is intended to be processed by Epydoc. - """ Introduction ============ @@ -11,286 +6,10 @@ The Munkres module provides an implementation of the Munkres algorithm (also called the Hungarian algorithm or the Kuhn-Munkres algorithm), useful for solving the Assignment Problem. -Assignment Problem -================== - -Let *C* be an *n* by *n* matrix representing the costs of each of *n* workers -to perform any of *n* jobs. The assignment problem is to assign jobs to -workers in a way that minimizes the total cost. Since each worker can perform -only one job and each job can be assigned to only one worker the assignments -represent an independent set of the matrix *C*. - -One way to generate the optimal set is to create all permutations of -the indexes necessary to traverse the matrix so that no row and column -are used more than once. For instance, given this matrix (expressed in -Python): - - matrix = [[5, 9, 1], - [10, 3, 2], - [8, 7, 4]] - -You could use this code to generate the traversal indexes: - - def permute(a, results): - if len(a) == 1: - results.insert(len(results), a) - - else: - for i in range(0, len(a)): - element = a[i] - a_copy = [a[j] for j in range(0, len(a)) if j != i] - subresults = [] - permute(a_copy, subresults) - for subresult in subresults: - result = [element] + subresult - results.insert(len(results), result) - - results = [] - permute(range(len(matrix)), results) # [0, 1, 2] for a 3x3 matrix - -After the call to permute(), the results matrix would look like this: - - [[0, 1, 2], - [0, 2, 1], - [1, 0, 2], - [1, 2, 0], - [2, 0, 1], - [2, 1, 0]] - -You could then use that index matrix to loop over the original cost matrix -and calculate the smallest cost of the combinations: - - minval = sys.maxsize - for indexes in results: - cost = 0 - for row, col in enumerate(indexes): - cost += matrix[row][col] - minval = min(cost, minval) - - print minval - -While this approach works fine for small matrices, it does not scale. It -executes in O(*n*!) time: Calculating the permutations for an *n*\ x\ *n* -matrix requires *n*! operations. For a 12x12 matrix, that's 479,001,600 -traversals. Even if you could manage to perform each traversal in just one -millisecond, it would still take more than 133 hours to perform the entire -traversal. A 20x20 matrix would take 2,432,902,008,176,640,000 operations. At -an optimistic millisecond per operation, that's more than 77 million years. - -The Munkres algorithm runs in O(*n*\ ^3) time, rather than O(*n*!). This -package provides an implementation of that algorithm. - -This version is based on -http://csclab.murraystate.edu/~bob.pilgrim/445/munkres.html - -This version was written for Python by Brian Clapper from the algorithm -at the above web site. (The ``Algorithm:Munkres`` Perl version, in CPAN, was -clearly adapted from the same web site.) - -Usage -===== - -Construct a Munkres object: - - from munkres import Munkres - - m = Munkres() - -Then use it to compute the lowest cost assignment from a cost matrix. Here's -a sample program: - - from munkres import Munkres, print_matrix - - matrix = [[5, 9, 1], - [10, 3, 2], - [8, 7, 4]] - m = Munkres() - indexes = m.compute(matrix) - print_matrix(matrix, msg='Lowest cost through this matrix:') - total = 0 - for row, column in indexes: - value = matrix[row][column] - total += value - print '(%d, %d) -> %d' % (row, column, value) - print 'total cost: %d' % total - -Running that program produces: - - Lowest cost through this matrix: - [5, 9, 1] - [10, 3, 2] - [8, 7, 4] - (0, 0) -> 5 - (1, 1) -> 3 - (2, 2) -> 4 - total cost=12 - -The instantiated Munkres object can be used multiple times on different -matrices. - -Non-square Cost Matrices -======================== - -The Munkres algorithm assumes that the cost matrix is square. However, it's -possible to use a rectangular matrix if you first pad it with 0 values to make -it square. This module automatically pads rectangular cost matrices to make -them square. - -Notes: - -- The module operates on a *copy* of the caller's matrix, so any padding will - not be seen by the caller. -- The cost matrix must be rectangular or square. An irregular matrix will - *not* work. - -Calculating Profit, Rather than Cost -==================================== - -The cost matrix is just that: A cost matrix. The Munkres algorithm finds -the combination of elements (one from each row and column) that results in -the smallest cost. It's also possible to use the algorithm to maximize -profit. To do that, however, you have to convert your profit matrix to a -cost matrix. The simplest way to do that is to subtract all elements from a -large value. For example: - - from munkres import Munkres, print_matrix - - matrix = [[5, 9, 1], - [10, 3, 2], - [8, 7, 4]] - cost_matrix = [] - for row in matrix: - cost_row = [] - for col in row: - cost_row += [sys.maxsize - col] - cost_matrix += [cost_row] - - m = Munkres() - indexes = m.compute(cost_matrix) - print_matrix(matrix, msg='Highest profit through this matrix:') - total = 0 - for row, column in indexes: - value = matrix[row][column] - total += value - print '(%d, %d) -> %d' % (row, column, value) - - print 'total profit=%d' % total - -Running that program produces: - - Highest profit through this matrix: - [5, 9, 1] - [10, 3, 2] - [8, 7, 4] - (0, 1) -> 9 - (1, 0) -> 10 - (2, 2) -> 4 - total profit=23 - -The ``munkres`` module provides a convenience method for creating a cost -matrix from a profit matrix. By default, it calculates the maximum profit -and subtracts every profit from it to obtain a cost. If, however, you -need a more general function, you can provide the -conversion function; but the convenience method takes care of the actual -creation of the matrix: - - import munkres - - cost_matrix = munkres.make_cost_matrix( - matrix, - lambda profit: 1000.0 - math.sqrt(profit)) - -So, the above profit-calculation program can be recast as: - - from munkres import Munkres, print_matrix, make_cost_matrix - - matrix = [[5, 9, 1], - [10, 3, 2], - [8, 7, 4]] - cost_matrix = make_cost_matrix(matrix) - # cost_matrix == [[5, 1, 9], - # [0, 7, 8], - # [2, 3, 6]] - m = Munkres() - indexes = m.compute(cost_matrix) - print_matrix(matrix, msg='Highest profits through this matrix:') - total = 0 - for row, column in indexes: - value = matrix[row][column] - total += value - print '(%d, %d) -> %d' % (row, column, value) - print 'total profit=%d' % total - -Disallowed Assignments -====================== - -You can also mark assignments in your cost or profit matrix as disallowed. -Simply use the munkres.DISALLOWED constant. - - from munkres import Munkres, print_matrix, make_cost_matrix, DISALLOWED - - matrix = [[5, 9, DISALLOWED], - [10, DISALLOWED, 2], - [8, 7, 4]] - cost_matrix = make_cost_matrix(matrix, lambda cost: (sys.maxsize - cost) if - (cost != DISALLOWED) else DISALLOWED) - m = Munkres() - indexes = m.compute(cost_matrix) - print_matrix(matrix, msg='Highest profit through this matrix:') - total = 0 - for row, column in indexes: - value = matrix[row][column] - total += value - print '(%d, %d) -> %d' % (row, column, value) - print 'total profit=%d' % total - -Running this program produces: - - Lowest cost through this matrix: - [ 5, 9, D] - [10, D, 2] - [ 8, 7, 4] - (0, 1) -> 9 - (1, 0) -> 10 - (2, 2) -> 4 - total profit=23 - -References -========== - -1. http://www.public.iastate.edu/~ddoty/HungarianAlgorithm.html - -2. Harold W. Kuhn. The Hungarian Method for the assignment problem. - *Naval Research Logistics Quarterly*, 2:83-97, 1955. - -3. Harold W. Kuhn. Variants of the Hungarian method for assignment - problems. *Naval Research Logistics Quarterly*, 3: 253-258, 1956. - -4. Munkres, J. Algorithms for the Assignment and Transportation Problems. - *Journal of the Society of Industrial and Applied Mathematics*, - 5(1):32-38, March, 1957. - -5. http://en.wikipedia.org/wiki/Hungarian_algorithm - -Copyright and License -===================== - -Copyright 2008-2016 Brian M. Clapper - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. +For complete usage documentation, see: https://software.clapper.org/munkres/ """ -__docformat__ = 'restructuredtext' +__docformat__ = 'markdown' # --------------------------------------------------------------------------- # Imports @@ -298,6 +17,7 @@ __docformat__ = 'restructuredtext' import sys import copy +from typing import Union, NewType, Sequence, Tuple, Optional, Callable # --------------------------------------------------------------------------- # Exports @@ -309,11 +29,14 @@ __all__ = ['Munkres', 'make_cost_matrix', 'DISALLOWED'] # Globals # --------------------------------------------------------------------------- +AnyNum = NewType('AnyNum', Union[int, float]) +Matrix = NewType('Matrix', Sequence[Sequence[AnyNum]]) + # Info about the module -__version__ = "1.0.12" +__version__ = "1.1.4" __author__ = "Brian Clapper, bmc@clapper.org" -__url__ = "http://software.clapper.org/munkres/" -__copyright__ = "(c) 2008-2017 Brian M. Clapper" +__url__ = "https://software.clapper.org/munkres/" +__copyright__ = "(c) 2008-2020 Brian M. Clapper" __license__ = "Apache Software License" # Constants @@ -353,30 +76,18 @@ class Munkres: self.marked = None self.path = None - def make_cost_matrix(profit_matrix, inversion_function): - """ - **DEPRECATED** - - Please use the module function ``make_cost_matrix()``. - """ - import munkres - return munkres.make_cost_matrix(profit_matrix, inversion_function) - - make_cost_matrix = staticmethod(make_cost_matrix) - - def pad_matrix(self, matrix, pad_value=0): + def pad_matrix(self, matrix: Matrix, pad_value: int=0) -> Matrix: """ Pad a possibly non-square matrix to make it square. - :Parameters: - matrix : list of lists - matrix to pad + **Parameters** - pad_value : int - value to use to pad the matrix + - `matrix` (list of lists of numbers): matrix to pad + - `pad_value` (`int`): value to use to pad the matrix - :rtype: list of lists - :return: a new, possibly padded, matrix + **Returns** + + a new, possibly padded, matrix """ max_columns = 0 total_rows = len(matrix) @@ -400,26 +111,27 @@ class Munkres: return new_matrix - def compute(self, cost_matrix): + def compute(self, cost_matrix: Matrix) -> Sequence[Tuple[int, int]]: """ Compute the indexes for the lowest-cost pairings between rows and - columns in the database. Returns a list of (row, column) tuples + columns in the database. Returns a list of `(row, column)` tuples that can be used to traverse the matrix. - :Parameters: - cost_matrix : list of lists - The cost matrix. If this cost matrix is not square, it - will be padded with zeros, via a call to ``pad_matrix()``. - (This method does *not* modify the caller's matrix. It - operates on a copy of the matrix.) + **WARNING**: This code handles square and rectangular matrices. It + does *not* handle irregular matrices. - **WARNING**: This code handles square and rectangular - matrices. It does *not* handle irregular matrices. + **Parameters** - :rtype: list - :return: A list of ``(row, column)`` tuples that describe the lowest - cost path through the matrix + - `cost_matrix` (list of lists of numbers): The cost matrix. If this + cost matrix is not square, it will be padded with zeros, via a call + to `pad_matrix()`. (This method does *not* modify the caller's + matrix. It operates on a copy of the matrix.) + + **Returns** + + A list of `(row, column)` tuples that describe the lowest cost path + through the matrix """ self.C = self.pad_matrix(cost_matrix) self.n = len(self.C) @@ -458,18 +170,18 @@ class Munkres: return results - def __copy_matrix(self, matrix): + def __copy_matrix(self, matrix: Matrix) -> Matrix: """Return an exact copy of the supplied matrix""" return copy.deepcopy(matrix) - def __make_matrix(self, n, val): + def __make_matrix(self, n: int, val: AnyNum) -> Matrix: """Create an *n*x*n* matrix, populating it with the specific value.""" matrix = [] for i in range(n): matrix += [[val for j in range(n)]] return matrix - def __step1(self): + def __step1(self) -> int: """ For each row of the matrix, find the smallest element and subtract it from every element in its row. Go to Step 2. @@ -492,7 +204,7 @@ class Munkres: self.C[i][j] -= minval return 2 - def __step2(self): + def __step2(self) -> int: """ Find a zero (Z) in the resulting matrix. If there is no starred zero in its row or column, star Z. Repeat for each element in the @@ -512,7 +224,7 @@ class Munkres: self.__clear_covers() return 3 - def __step3(self): + def __step3(self) -> int: """ Cover each column containing a starred zero. If K columns are covered, the starred zeros describe a complete set of unique @@ -533,7 +245,7 @@ class Munkres: return step - def __step4(self): + def __step4(self) -> int: """ Find a noncovered zero and prime it. If there is no starred zero in the row containing this primed zero, Go to Step 5. Otherwise, @@ -566,7 +278,7 @@ class Munkres: return step - def __step5(self): + def __step5(self) -> int: """ Construct a series of alternating primed and starred zeros as follows. Let Z0 represent the uncovered primed zero found in Step 4. @@ -602,7 +314,7 @@ class Munkres: self.__erase_primes() return 3 - def __step6(self): + def __step6(self) -> int: """ Add the value found in Step 4 to every element of each covered row, and subtract it from every element of each uncovered column. @@ -627,7 +339,7 @@ class Munkres: raise UnsolvableMatrix("Matrix cannot be solved!") return 4 - def __find_smallest(self): + def __find_smallest(self) -> AnyNum: """Find the smallest uncovered value in the matrix.""" minval = sys.maxsize for i in range(self.n): @@ -638,7 +350,7 @@ class Munkres: return minval - def __find_a_zero(self, i0=0, j0=0): + def __find_a_zero(self, i0: int = 0, j0: int = 0) -> Tuple[int, int]: """Find the first uncovered element with value 0""" row = -1 col = -1 @@ -664,7 +376,7 @@ class Munkres: return (row, col) - def __find_star_in_row(self, row): + def __find_star_in_row(self, row: Sequence[AnyNum]) -> int: """ Find the first starred element in the specified row. Returns the column index, or -1 if no starred element was found. @@ -677,7 +389,7 @@ class Munkres: return col - def __find_star_in_col(self, col): + def __find_star_in_col(self, col: Sequence[AnyNum]) -> int: """ Find the first starred element in the specified row. Returns the row index, or -1 if no starred element was found. @@ -690,7 +402,7 @@ class Munkres: return row - def __find_prime_in_row(self, row): + def __find_prime_in_row(self, row) -> int: """ Find the first prime element in the specified row. Returns the column index, or -1 if no starred element was found. @@ -703,20 +415,22 @@ class Munkres: return col - def __convert_path(self, path, count): + def __convert_path(self, + path: Sequence[Sequence[int]], + count: int) -> None: for i in range(count+1): if self.marked[path[i][0]][path[i][1]] == 1: self.marked[path[i][0]][path[i][1]] = 0 else: self.marked[path[i][0]][path[i][1]] = 1 - def __clear_covers(self): + def __clear_covers(self) -> None: """Clear all covered matrix cells""" for i in range(self.n): self.row_covered[i] = False self.col_covered[i] = False - def __erase_primes(self): + def __erase_primes(self) -> None: """Erase all prime markings""" for i in range(self.n): for j in range(self.n): @@ -727,37 +441,38 @@ class Munkres: # Functions # --------------------------------------------------------------------------- -def make_cost_matrix(profit_matrix, inversion_function=None): +def make_cost_matrix( + profit_matrix: Matrix, + inversion_function: Optional[Callable[[AnyNum], AnyNum]] = None + ) -> Matrix: """ - Create a cost matrix from a profit matrix by calling - 'inversion_function' to invert each value. The inversion - function must take one numeric argument (of any type) and return - another numeric argument which is presumed to be the cost inverse - of the original profit. In case the inversion function is not provided, - calculate it as max(matrix) - matrix. + Create a cost matrix from a profit matrix by calling `inversion_function()` + to invert each value. The inversion function must take one numeric argument + (of any type) and return another numeric argument which is presumed to be + the cost inverse of the original profit value. If the inversion function + is not provided, a given cell's inverted value is calculated as + `max(matrix) - value`. This is a static method. Call it like this: - .. python: - + from munkres import Munkres cost_matrix = Munkres.make_cost_matrix(matrix, inversion_func) For example: - .. python: - + from munkres import Munkres cost_matrix = Munkres.make_cost_matrix(matrix, lambda x : sys.maxsize - x) - :Parameters: - profit_matrix : list of lists - The matrix to convert from a profit to a cost matrix + **Parameters** - inversion_function : function - The function to use to invert each entry in the profit matrix. - In case it is not provided, calculate it as max(matrix) - matrix. + - `profit_matrix` (list of lists of numbers): The matrix to convert from + profit to cost values. + - `inversion_function` (`function`): The function to use to invert each + entry in the profit matrix. - :rtype: list of lists - :return: The converted matrix + **Returns** + + A new matrix representing the inversion of `profix_matrix`. """ if not inversion_function: maximum = max(max(row) for row in profit_matrix) @@ -768,16 +483,14 @@ def make_cost_matrix(profit_matrix, inversion_function=None): cost_matrix.append([inversion_function(value) for value in row]) return cost_matrix -def print_matrix(matrix, msg=None): +def print_matrix(matrix: Matrix, msg: Optional[str] = None) -> None: """ - Convenience function: Displays the contents of a matrix of integers. + Convenience function: Displays the contents of a matrix. - :Parameters: - matrix : list of lists - Matrix to print + **Parameters** - msg : str - Optional message to print before displaying the matrix + - `matrix` (list of lists of numbers): The matrix to print + - `msg` (`str`): Optional message to print before displaying the matrix """ import math @@ -800,8 +513,8 @@ def print_matrix(matrix, msg=None): sep = '[' for val in row: if val is DISALLOWED: - formatted = ((format + 's') % DISALLOWED_PRINTVAL) - else: formatted = ((format + 'd') % val) + val = DISALLOWED_PRINTVAL + formatted = ((format + 's') % val) sys.stdout.write(sep + formatted) sep = ', ' sys.stdout.write(']\n') @@ -832,12 +545,24 @@ if __name__ == '__main__': [9, 7, 4]], 18), + # Square variant with floating point value + ([[10.1, 10.2, 8.3], + [9.4, 8.5, 1.6], + [9.7, 7.8, 4.9]], + 19.5), + # Rectangular variant ([[10, 10, 8, 11], [9, 8, 1, 1], [9, 7, 4, 10]], 15), + # Rectangular variant with floating point value + ([[10.01, 10.02, 8.03, 11.04], + [9.05, 8.06, 1.07, 1.08], + [9.09, 7.1, 4.11, 10.12]], + 15.2), + # Rectangular with DISALLOWED ([[4, 5, 6, DISALLOWED], [1, 9, 12, 11], @@ -845,12 +570,26 @@ if __name__ == '__main__': [12, 12, 12, 10]], 20), + # Rectangular variant with DISALLOWED and floating point value + ([[4.001, 5.002, 6.003, DISALLOWED], + [1.004, 9.005, 12.006, 11.007], + [DISALLOWED, 5.008, 4.009, DISALLOWED], + [12.01, 12.011, 12.012, 10.013]], + 20.028), + # DISALLOWED to force pairings ([[1, DISALLOWED, DISALLOWED, DISALLOWED], [DISALLOWED, 2, DISALLOWED, DISALLOWED], [DISALLOWED, DISALLOWED, 3, DISALLOWED], [DISALLOWED, DISALLOWED, DISALLOWED, 4]], - 10)] + 10), + + # DISALLOWED to force pairings with floating point value + ([[1.1, DISALLOWED, DISALLOWED, DISALLOWED], + [DISALLOWED, 2.2, DISALLOWED, DISALLOWED], + [DISALLOWED, DISALLOWED, 3.3, DISALLOWED], + [DISALLOWED, DISALLOWED, DISALLOWED, 4.4]], + 11.0)] m = Munkres() for cost_matrix, expected_total in matrices: @@ -860,6 +599,6 @@ if __name__ == '__main__': for r, c in indexes: x = cost_matrix[r][c] total_cost += x - print('(%d, %d) -> %d' % (r, c, x)) - print('lowest cost=%d' % total_cost) + print(('(%d, %d) -> %s' % (r, c, x))) + print(('lowest cost=%s' % total_cost)) assert expected_total == total_cost diff --git a/libs/common/musicbrainzngs/caa.py b/libs/common/musicbrainzngs/caa.py index 12fa8d35..caa0c5f3 100644 --- a/libs/common/musicbrainzngs/caa.py +++ b/libs/common/musicbrainzngs/caa.py @@ -13,15 +13,24 @@ import json from musicbrainzngs import compat from musicbrainzngs import musicbrainz +from musicbrainzngs.util import _unicode hostname = "coverartarchive.org" +https = True -def set_caa_hostname(new_hostname): +def set_caa_hostname(new_hostname, use_https=False): """Set the base hostname for Cover Art Archive requests. - Defaults to 'coverartarchive.org'.""" + Defaults to 'coverartarchive.org', accessing over https. + For backwards compatibility, `use_https` is False by default. + + :param str new_hostname: The hostname (and port) of the CAA server to connect to + :param bool use_https: `True` if the host should be accessed using https. Default is `False` +""" global hostname + global https hostname = new_hostname + https = use_https def _caa_request(mbid, imageid=None, size=None, entitytype="release"): @@ -31,7 +40,7 @@ def _caa_request(mbid, imageid=None, size=None, entitytype="release"): with :meth:`get_image_list`. :type imageid: str - :param size: 250, 500 + :param size: "250", "500", "1200" :type size: str or None :param entitytype: ``release`` or ``release-group`` @@ -45,7 +54,7 @@ def _caa_request(mbid, imageid=None, size=None, entitytype="release"): elif imageid: path.append(imageid) url = compat.urlunparse(( - 'http', + 'https' if https else 'http', hostname, '/%s' % '/'.join(path), '', @@ -78,7 +87,8 @@ def _caa_request(mbid, imageid=None, size=None, entitytype="release"): return resp else: # Otherwise it's json - return json.loads(resp) + data = _unicode(resp) + return json.loads(data) def get_image_list(releaseid): @@ -121,7 +131,7 @@ def get_release_group_image_front(releasegroupid, size=None): :meth:`get_image`. """ return get_image(releasegroupid, "front", size=size, - entitytype="release-group") + entitytype="release-group") def get_image_front(releaseid, size=None): @@ -158,10 +168,10 @@ def get_image(mbid, coverid, size=None, entitytype="release"): :meth:`get_image_list` :type coverid: int or str - :param size: 250, 500 or None. If it is None, the largest available picture - will be downloaded. If the image originally uploaded to the - Cover Art Archive was smaller than the requested size, only - the original image will be returned. + :param size: "250", "500", "1200" or None. If it is None, the largest + available picture will be downloaded. If the image originally + uploaded to the Cover Art Archive was smaller than the + requested size, only the original image will be returned. :type size: str or None :param entitytype: The type of entity for which to download the cover art. diff --git a/libs/common/musicbrainzngs/compat.py b/libs/common/musicbrainzngs/compat.py index 36574b5c..689e0834 100644 --- a/libs/common/musicbrainzngs/compat.py +++ b/libs/common/musicbrainzngs/compat.py @@ -40,11 +40,10 @@ is_py3 = (_ver[0] == 3) if is_py2: from StringIO import StringIO from urllib2 import HTTPPasswordMgr, HTTPDigestAuthHandler, Request,\ - HTTPHandler, build_opener, HTTPError, URLError,\ - build_opener + HTTPHandler, build_opener, HTTPError, URLError from httplib import BadStatusLine, HTTPException from urlparse import urlunparse - from urllib import urlencode + from urllib import urlencode, quote_plus bytes = str unicode = unicode @@ -55,7 +54,7 @@ elif is_py3: HTTPHandler, build_opener from urllib.error import HTTPError, URLError from http.client import HTTPException, BadStatusLine - from urllib.parse import urlunparse, urlencode + from urllib.parse import urlunparse, urlencode, quote_plus unicode = str bytes = bytes diff --git a/libs/common/musicbrainzngs/mbxml.py b/libs/common/musicbrainzngs/mbxml.py index 60236dc7..e85fe7cb 100644 --- a/libs/common/musicbrainzngs/mbxml.py +++ b/libs/common/musicbrainzngs/mbxml.py @@ -7,29 +7,26 @@ import re import xml.etree.ElementTree as ET import logging -from musicbrainzngs import util +from . import util -try: - from ET import fixtag -except: - # Python < 2.7 - def fixtag(tag, namespaces): - # given a decorated tag (of the form {uri}tag), return prefixed - # tag and namespace declaration, if any - if isinstance(tag, ET.QName): - tag = tag.text - namespace_uri, tag = tag[1:].split("}", 1) - prefix = namespaces.get(namespace_uri) - if prefix is None: - prefix = "ns%d" % len(namespaces) - namespaces[namespace_uri] = prefix - if prefix == "xml": - xmlns = None - else: - xmlns = ("xmlns:%s" % prefix, namespace_uri) - else: + +def fixtag(tag, namespaces): + # given a decorated tag (of the form {uri}tag), return prefixed + # tag and namespace declaration, if any + if isinstance(tag, ET.QName): + tag = tag.text + namespace_uri, tag = tag[1:].split("}", 1) + prefix = namespaces.get(namespace_uri) + if prefix is None: + prefix = "ns%d" % len(namespaces) + namespaces[namespace_uri] = prefix + if prefix == "xml": xmlns = None - return "%s:%s" % (prefix, tag), xmlns + else: + xmlns = ("xmlns:%s" % prefix, namespace_uri) + else: + xmlns = None + return "%s:%s" % (prefix, tag), xmlns NS_MAP = {"http://musicbrainz.org/ns/mmd-2.0#": "ws2", @@ -377,7 +374,7 @@ def parse_relation(relation): result.update(parse_elements(elements, inner_els, relation)) # We parse attribute-list again to get attributes that have both # text and attribute values - result.update(parse_elements([], {"attribute-list": parse_relation_attribute_list}, relation)) + result.update(parse_elements(['target-credit'], {"attribute-list": parse_relation_attribute_list}, relation)) return result @@ -505,7 +502,6 @@ def parse_recording(recording): "user-tag-list": parse_tag_list, "rating": parse_rating, "isrc-list": parse_external_id_list, - "echoprint-list": parse_external_id_list, "relation-list": parse_relation_list, "annotation": parse_annotation} diff --git a/libs/common/musicbrainzngs/musicbrainz.py b/libs/common/musicbrainzngs/musicbrainz.py index 953c79b8..beb0a79b 100644 --- a/libs/common/musicbrainzngs/musicbrainz.py +++ b/libs/common/musicbrainzngs/musicbrainz.py @@ -20,7 +20,7 @@ from musicbrainzngs import mbxml from musicbrainzngs import util from musicbrainzngs import compat -_version = "0.6" +_version = "0.7.1" _log = logging.getLogger("musicbrainzngs") LUCENE_SPECIAL = r'([+\-&|!(){}\[\]\^"~*?:\\\/])' @@ -54,11 +54,11 @@ VALID_INCLUDES = { 'recording': [ "artists", "releases", # Subqueries "discids", "media", "artist-credits", "isrcs", - "annotation", "aliases" + "work-level-rels", "annotation", "aliases" ] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES, 'release': [ "artists", "labels", "recordings", "release-groups", "media", - "artist-credits", "discids", "puids", "isrcs", + "artist-credits", "discids", "isrcs", "recording-level-rels", "work-level-rels", "annotation", "aliases" ] + TAG_INCLUDES + RELATION_INCLUDES, 'release-group': [ @@ -69,16 +69,15 @@ VALID_INCLUDES = { "annotation", "aliases" ] + RELATION_INCLUDES, 'work': [ - "artists", # Subqueries "aliases", "annotation" ] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES, 'url': RELATION_INCLUDES, 'discid': [ # Discid should be the same as release "artists", "labels", "recordings", "release-groups", "media", - "artist-credits", "discids", "puids", "isrcs", + "artist-credits", "discids", "isrcs", "recording-level-rels", "work-level-rels", "annotation", "aliases" ] + RELATION_INCLUDES, - 'isrc': ["artists", "releases", "puids", "isrcs"], + 'isrc': ["artists", "releases", "isrcs"], 'iswc': ["artists"], 'collection': ['releases'], } @@ -109,48 +108,58 @@ VALID_SEARCH_FIELDS = { 'entity', 'name', 'text', 'type' ], 'area': [ - 'aid', 'area', 'alias', 'begin', 'comment', 'end', 'ended', - 'iso', 'iso1', 'iso2', 'iso3', 'type' + 'aid', 'alias', 'area', 'areaaccent', 'begin', 'comment', 'end', + 'ended', 'iso', 'iso1', 'iso2', 'iso3', 'sortname', 'tag', 'type' ], 'artist': [ - 'arid', 'artist', 'artistaccent', 'alias', 'begin', 'comment', - 'country', 'end', 'ended', 'gender', 'ipi', 'sortname', 'tag', 'type', - 'area', 'beginarea', 'endarea' + 'alias', 'area', 'arid', 'artist', 'artistaccent', 'begin', 'beginarea', + 'comment', 'country', 'end', 'endarea', 'ended', 'gender', + 'ipi', 'isni', 'primary_alias', 'sortname', 'tag', 'type' + ], + 'event': [ + 'aid', 'alias', 'area', 'arid', 'artist', 'begin', 'comment', 'eid', + 'end', 'ended', 'event', 'eventaccent', 'pid', 'place', 'tag', 'type' + ], + 'instrument': [ + 'alias', 'comment', 'description', 'iid', 'instrument', + 'instrumentaccent', 'tag', 'type' ], 'label': [ - 'alias', 'begin', 'code', 'comment', 'country', 'end', 'ended', - 'ipi', 'label', 'labelaccent', 'laid', 'sortname', 'type', 'tag', - 'area' - ], - 'recording': [ - 'arid', 'artist', 'artistname', 'creditname', 'comment', - 'country', 'date', 'dur', 'format', 'isrc', 'number', - 'position', 'primarytype', 'puid', 'qdur', 'recording', - 'recordingaccent', 'reid', 'release', 'rgid', 'rid', - 'secondarytype', 'status', 'tnum', 'tracks', 'tracksrelease', - 'tag', 'type', 'video' - ], - 'release-group': [ - 'arid', 'artist', 'artistname', 'comment', 'creditname', - 'primarytype', 'rgid', 'releasegroup', 'releasegroupaccent', - 'releases', 'release', 'reid', 'secondarytype', 'status', + 'alias', 'area', 'begin', 'code', 'comment', 'country', 'end', 'ended', + 'ipi', 'label', 'labelaccent', 'laid', 'release_count', 'sortname', 'tag', 'type' ], + 'place': [ + 'address', 'alias', 'area', 'begin', 'comment', 'end', 'ended', 'lat', 'long', + 'pid', 'place', 'placeaccent', 'type' + ], + 'recording': [ + 'alias', 'arid', 'artist', 'artistname', 'comment', 'country', + 'creditname', 'date', 'dur', 'format', 'isrc', 'number', 'position', + 'primarytype', 'qdur', 'recording', 'recordingaccent', 'reid', + 'release', 'rgid', 'rid', 'secondarytype', 'status', 'tag', 'tid', + 'tnum', 'tracks', 'tracksrelease', 'type', 'video'], + + 'release-group': [ + 'alias', 'arid', 'artist', 'artistname', 'comment', 'creditname', + 'primarytype', 'reid', 'release', 'releasegroup', 'releasegroupaccent', + 'releases', 'rgid', 'secondarytype', 'status', 'tag', 'type' + ], 'release': [ - 'arid', 'artist', 'artistname', 'asin', 'barcode', 'creditname', - 'catno', 'comment', 'country', 'creditname', 'date', 'discids', - 'discidsmedium', 'format', 'laid', 'label', 'lang', 'mediums', - 'primarytype', 'puid', 'quality', 'reid', 'release', 'releaseaccent', - 'rgid', 'script', 'secondarytype', 'status', 'tag', 'tracks', - 'tracksmedium', 'type' + 'alias', 'arid', 'artist', 'artistname', 'asin', 'barcode', 'catno', + 'comment', 'country', 'creditname', 'date', 'discids', 'discidsmedium', + 'format', 'label', 'laid', 'lang', 'mediums', 'primarytype', 'quality', + 'reid', 'release', 'releaseaccent', 'rgid', 'script', 'secondarytype', + 'status', 'tag', 'tracks', 'tracksmedium', 'type' ], 'series': [ - 'alias', 'comment', 'sid', 'series', 'type' + 'alias', 'comment', 'orderingattribute', 'series', 'seriesaccent', + 'sid', 'tag', 'type' ], 'work': [ - 'alias', 'arid', 'artist', 'comment', 'iswc', 'lang', 'tag', - 'type', 'wid', 'work', 'workaccent' - ], + 'alias', 'arid', 'artist', 'comment', 'iswc', 'lang', 'recording', + 'recording_count', 'rid', 'tag', 'type', 'wid', 'work', 'workaccent' + ] } # Constants @@ -278,8 +287,6 @@ def _docstring_search(entity): def _docstring_impl(name, values): def _decorator(func): - # puids are allowed so nothing breaks, but not documented - if "puids" in values: values.remove("puids") vstr = ", ".join(values) args = {name: vstr} if func.__doc__: @@ -293,6 +300,7 @@ def _docstring_impl(name, values): user = password = "" hostname = "musicbrainz.org" +https = True _client = "" _useragent = "" @@ -317,12 +325,21 @@ def set_useragent(app, version, contact=None): _client = "%s-%s" % (app, version) _log.debug("set user-agent to %s" % _useragent) -def set_hostname(new_hostname): + +def set_hostname(new_hostname, use_https=False): """Set the hostname for MusicBrainz webservice requests. - Defaults to 'musicbrainz.org'. - You can also include a port: 'localhost:8000'.""" + Defaults to 'musicbrainz.org', accessing over https. + For backwards compatibility, `use_https` is False by default. + + :param str new_hostname: The hostname (and port) of the MusicBrainz server to connect to + :param bool use_https: `True` if the host should be accessed using https. Default is `False` + + Specify a non-standard port by adding it to the hostname, + for example 'localhost:8000'.""" global hostname + global https hostname = new_hostname + https = use_https # Rate limiting. @@ -626,7 +643,7 @@ def _mb_request(path, method='GET', auth_required=AUTH_NO, # Construct the full URL for the request, including hostname and # query string. url = compat.urlunparse(( - 'http', + 'https' if https else 'http', hostname, '/ws/2/%s' % path, '', @@ -737,10 +754,6 @@ def _do_mb_search(entity, query='', fields={}, raise InvalidSearchFieldError( '%s is not a valid search field for %s' % (key, entity) ) - elif key == "puid": - warn("PUID support was removed from server\n" - "the 'puid' field is ignored", - Warning, stacklevel=2) # Escape Lucene's special characters. value = util._unicode(value) @@ -1024,27 +1037,6 @@ def get_releases_by_discid(id, includes=[], toc=None, cdstubs=True, media_format params["media-format"] = media_format return _do_mb_query("discid", id, includes, params) -@_docstring_get("recording") -def get_recordings_by_echoprint(echoprint, includes=[], release_status=[], - release_type=[]): - """Search for recordings with an `echoprint `_. - (not available on server)""" - warn("Echoprints were never introduced\n" - "and will not be found (404)", - Warning, stacklevel=2) - raise ResponseError(cause=compat.HTTPError( - None, 404, "Not Found", None, None)) - -@_docstring_get("recording") -def get_recordings_by_puid(puid, includes=[], release_status=[], - release_type=[]): - """Search for recordings with a :musicbrainz:`PUID`. - (not available on server)""" - warn("PUID support was removed from the server\n" - "and no PUIDs will be found (404)", - Warning, stacklevel=2) - raise ResponseError(cause=compat.HTTPError( - None, 404, "Not Found", None, None)) @_docstring_get("recording") def get_recordings_by_isrc(isrc, includes=[], release_status=[], @@ -1261,23 +1253,6 @@ def submit_barcodes(release_barcode): query = mbxml.make_barcode_request(release_barcode) return _do_mb_post("release", query) -def submit_puids(recording_puids): - """Submit PUIDs. - (Functionality removed from server) - """ - warn("PUID support was dropped at the server\n" - "nothing will be submitted", - Warning, stacklevel=2) - return {'message': {'text': 'OK'}} - -def submit_echoprints(recording_echoprints): - """Submit echoprints. - (Functionality removed from server) - """ - warn("Echoprints were never introduced\n" - "nothing will be submitted", - Warning, stacklevel=2) - return {'message': {'text': 'OK'}} def submit_isrcs(recording_isrcs): """Submit ISRCs. diff --git a/libs/common/mutagen/__init__.py b/libs/common/mutagen/__init__.py index 94f2509c..efff8dee 100644 --- a/libs/common/mutagen/__init__.py +++ b/libs/common/mutagen/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2005 Michael Urman # # This program is free software; you can redistribute it and/or modify @@ -23,7 +22,7 @@ from mutagen._util import MutagenError from mutagen._file import FileType, StreamInfo, File from mutagen._tags import Tags, Metadata, PaddingInfo -version = (1, 41, 1) +version = (1, 46, 0) """Version tuple.""" version_string = ".".join(map(str, version)) diff --git a/libs/common/mutagen/_compat.py b/libs/common/mutagen/_compat.py deleted file mode 100644 index 8a60d68d..00000000 --- a/libs/common/mutagen/_compat.py +++ /dev/null @@ -1,92 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2013 Christoph Reiter -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. - -import sys - - -PY2 = sys.version_info[0] == 2 -PY3 = not PY2 - -if PY2: - from StringIO import StringIO - BytesIO = StringIO - from cStringIO import StringIO as cBytesIO - from itertools import izip - - long_ = long - integer_types = (int, long) - string_types = (str, unicode) - text_type = unicode - - xrange = xrange - cmp = cmp - chr_ = chr - - def endswith(text, end): - return text.endswith(end) - - iteritems = lambda d: d.iteritems() - itervalues = lambda d: d.itervalues() - iterkeys = lambda d: d.iterkeys() - - iterbytes = lambda b: iter(b) - - exec("def reraise(tp, value, tb):\n raise tp, value, tb") - - def swap_to_string(cls): - if "__str__" in cls.__dict__: - cls.__unicode__ = cls.__str__ - - if "__bytes__" in cls.__dict__: - cls.__str__ = cls.__bytes__ - - return cls - - import __builtin__ as builtins - builtins - -elif PY3: - from io import StringIO - StringIO = StringIO - from io import BytesIO - cBytesIO = BytesIO - - long_ = int - integer_types = (int,) - string_types = (str,) - text_type = str - - izip = zip - xrange = range - cmp = lambda a, b: (a > b) - (a < b) - chr_ = lambda x: bytes([x]) - - def endswith(text, end): - # usefull for paths which can be both, str and bytes - if isinstance(text, str): - if not isinstance(end, str): - end = end.decode("ascii") - else: - if not isinstance(end, bytes): - end = end.encode("ascii") - return text.endswith(end) - - iteritems = lambda d: iter(d.items()) - itervalues = lambda d: iter(d.values()) - iterkeys = lambda d: iter(d.keys()) - - iterbytes = lambda b: (bytes([v]) for v in b) - - def reraise(tp, value, tb): - raise tp(value).with_traceback(tb) - - def swap_to_string(cls): - return cls - - import builtins - builtins diff --git a/libs/common/mutagen/_constants.py b/libs/common/mutagen/_constants.py index 5c1c1a10..77282315 100644 --- a/libs/common/mutagen/_constants.py +++ b/libs/common/mutagen/_constants.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/libs/common/mutagen/_file.py b/libs/common/mutagen/_file.py index 2405a523..5c4c295a 100644 --- a/libs/common/mutagen/_file.py +++ b/libs/common/mutagen/_file.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2005 Michael Urman # # This program is free software; you can redistribute it and/or modify @@ -9,7 +8,6 @@ import warnings from mutagen._util import DictMixin, loadfile -from mutagen._compat import izip class FileType(DictMixin): @@ -97,7 +95,7 @@ class FileType(DictMixin): return self.tags.keys() @loadfile(writable=True) - def delete(self, filething): + def delete(self, filething=None): """delete(filething=None) Remove tags from a file. @@ -120,7 +118,7 @@ class FileType(DictMixin): return self.tags.delete(filething) @loadfile(writable=True) - def save(self, filething, **kwargs): + def save(self, filething=None, **kwargs): """save(filething=None, **kwargs) Save metadata tags. @@ -221,13 +219,13 @@ def File(filething, options=None, easy=False): filething (filething) options: Sequence of :class:`FileType` implementations, defaults to all included ones. - easy (bool): If the easy wrappers should be returnd if available. + easy (bool): If the easy wrappers should be returned if available. For example :class:`EasyMP3 ` instead of :class:`MP3 `. Returns: FileType: A FileType instance for the detected type or `None` in case - the type couln't be determined. + the type couldn't be determined. Raises: MutagenError: in case the detected type fails to load the file. @@ -264,12 +262,16 @@ def File(filething, options=None, easy=False): from mutagen.optimfrog import OptimFROG from mutagen.aiff import AIFF from mutagen.aac import AAC + from mutagen.ac3 import AC3 from mutagen.smf import SMF + from mutagen.tak import TAK from mutagen.dsf import DSF + from mutagen.dsdiff import DSDIFF + from mutagen.wave import WAVE options = [MP3, TrueAudio, OggTheora, OggSpeex, OggVorbis, OggFLAC, FLAC, AIFF, APEv2File, MP4, ID3FileType, WavPack, - Musepack, MonkeysAudio, OptimFROG, ASF, OggOpus, AAC, - SMF, DSF] + Musepack, MonkeysAudio, OptimFROG, ASF, OggOpus, AAC, AC3, + SMF, TAK, DSF, DSDIFF, WAVE] if not options: return None @@ -287,7 +289,7 @@ def File(filething, options=None, easy=False): results = [(Kind.score(filething.name, fileobj, header), Kind.__name__) for Kind in options] - results = list(izip(results, options)) + results = list(zip(results, options)) results.sort() (score, name), Kind = results[-1] if score > 0: diff --git a/libs/common/mutagen/_iff.py b/libs/common/mutagen/_iff.py new file mode 100644 index 00000000..01f14db6 --- /dev/null +++ b/libs/common/mutagen/_iff.py @@ -0,0 +1,386 @@ +# Copyright (C) 2014 Evan Purkhiser +# 2014 Ben Ockmore +# 2017 Borewit +# 2019-2020 Philipp Wolfer +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +"""Base classes for various IFF based formats (e.g. AIFF or RIFF).""" + +import sys + +from mutagen.id3 import ID3 +from mutagen.id3._util import ID3NoHeaderError, error as ID3Error +from mutagen._util import ( + MutagenError, + convert_error, + delete_bytes, + insert_bytes, + loadfile, + reraise, + resize_bytes, +) + + +class error(MutagenError): + pass + + +class InvalidChunk(error): + pass + + +class EmptyChunk(InvalidChunk): + pass + + +def is_valid_chunk_id(id): + """ is_valid_chunk_id(FOURCC) + + Arguments: + id (FOURCC) + Returns: + true if valid; otherwise false + + Check if argument id is valid FOURCC type. + """ + + assert isinstance(id, str), \ + 'id is of type %s, must be str: %r' % (type(id), id) + + return ((0 < len(id) <= 4) and (min(id) >= ' ') and + (max(id) <= '~')) + + +# Assert FOURCC formatted valid +def assert_valid_chunk_id(id): + if not is_valid_chunk_id(id): + raise ValueError("IFF chunk ID must be four ASCII characters.") + + +class IffChunk(object): + """Generic representation of a single IFF chunk. + + IFF chunks always consist of an ID followed by the chunk size. The exact + format varies between different IFF based formats, e.g. AIFF uses + big-endian while RIFF uses little-endian. + """ + + # Chunk headers are usually 8 bytes long (4 for ID and 4 for the size) + HEADER_SIZE = 8 + + @classmethod + def parse_header(cls, header): + """Read ID and data_size from the given header. + Must be implemented in subclasses.""" + raise error("Not implemented") + + def write_new_header(self, id_, size): + """Write the chunk header with id_ and size to the file. + Must be implemented in subclasses. The data must be written + to the current position in self._fileobj.""" + raise error("Not implemented") + + def write_size(self): + """Write self.data_size to the file. + Must be implemented in subclasses. The data must be written + to the current position in self._fileobj.""" + raise error("Not implemented") + + @classmethod + def get_class(cls, id): + """Returns the class for a new chunk for a given ID. + Can be overridden in subclasses to implement specific chunk types.""" + return cls + + @classmethod + def parse(cls, fileobj, parent_chunk=None): + header = fileobj.read(cls.HEADER_SIZE) + if len(header) < cls.HEADER_SIZE: + raise EmptyChunk('Header size < %i' % cls.HEADER_SIZE) + id, data_size = cls.parse_header(header) + try: + id = id.decode('ascii').rstrip() + except UnicodeDecodeError as e: + raise InvalidChunk(e) + + if not is_valid_chunk_id(id): + raise InvalidChunk('Invalid chunk ID %r' % id) + + return cls.get_class(id)(fileobj, id, data_size, parent_chunk) + + def __init__(self, fileobj, id, data_size, parent_chunk): + self._fileobj = fileobj + self.id = id + self.data_size = data_size + self.parent_chunk = parent_chunk + self.data_offset = fileobj.tell() + self.offset = self.data_offset - self.HEADER_SIZE + self._calculate_size() + + def __repr__(self): + return ("<%s id=%s, offset=%i, size=%i, data_offset=%i, data_size=%i>" + % (type(self).__name__, self.id, self.offset, self.size, + self.data_offset, self.data_size)) + + def read(self): + """Read the chunks data""" + + self._fileobj.seek(self.data_offset) + return self._fileobj.read(self.data_size) + + def write(self, data): + """Write the chunk data""" + + if len(data) > self.data_size: + raise ValueError + + self._fileobj.seek(self.data_offset) + self._fileobj.write(data) + # Write the padding bytes + padding = self.padding() + if padding: + self._fileobj.seek(self.data_offset + self.data_size) + self._fileobj.write(b'\x00' * padding) + + def delete(self): + """Removes the chunk from the file""" + + delete_bytes(self._fileobj, self.size, self.offset) + if self.parent_chunk is not None: + self.parent_chunk._remove_subchunk(self) + self._fileobj.flush() + + def _update_size(self, size_diff, changed_subchunk=None): + """Update the size of the chunk""" + + old_size = self.size + self.data_size += size_diff + self._fileobj.seek(self.offset + 4) + self.write_size() + self._calculate_size() + if self.parent_chunk is not None: + self.parent_chunk._update_size(self.size - old_size, self) + if changed_subchunk: + self._update_sibling_offsets( + changed_subchunk, old_size - self.size) + + def _calculate_size(self): + self.size = self.HEADER_SIZE + self.data_size + self.padding() + assert self.size % 2 == 0 + + def resize(self, new_data_size): + """Resize the file and update the chunk sizes""" + + padding = new_data_size % 2 + resize_bytes(self._fileobj, self.data_size + self.padding(), + new_data_size + padding, self.data_offset) + size_diff = new_data_size - self.data_size + self._update_size(size_diff) + self._fileobj.flush() + + def padding(self): + """Returns the number of padding bytes (0 or 1). + IFF chunks are required to be a even number in total length. If + data_size is odd a padding byte will be added at the end. + """ + return self.data_size % 2 + + +class IffContainerChunkMixin(): + """A IFF chunk containing other chunks. + + A container chunk can have an additional name as the first 4 bytes of the + chunk data followed by an arbitrary number of subchunks. The root chunk of + the file is always a container chunk (e.g. the AIFF chunk or the FORM chunk + for RIFF) but there can be other types of container chunks (e.g. the LIST + chunks used in RIFF). + """ + + def parse_next_subchunk(self): + """""" + raise error("Not implemented") + + def init_container(self, name_size=4): + # Lists can store an additional name identifier before the subchunks + self.__name_size = name_size + if self.data_size < name_size: + raise InvalidChunk( + 'Container chunk data size < %i' % name_size) + + # Read the container name + if name_size > 0: + try: + self.name = self._fileobj.read(name_size).decode('ascii') + except UnicodeDecodeError as e: + raise error(e) + else: + self.name = None + + # Load all IFF subchunks + self.__subchunks = [] + + def subchunks(self): + """Returns a list of all subchunks. + The list is lazily loaded on first access. + """ + if not self.__subchunks: + next_offset = self.data_offset + self.__name_size + while next_offset < self.offset + self.size: + self._fileobj.seek(next_offset) + try: + chunk = self.parse_next_subchunk() + except EmptyChunk: + break + except InvalidChunk: + break + self.__subchunks.append(chunk) + + # Calculate the location of the next chunk + next_offset = chunk.offset + chunk.size + return self.__subchunks + + def insert_chunk(self, id_, data=None): + """Insert a new chunk at the end of the container chunk""" + + if not is_valid_chunk_id(id_): + raise KeyError("Invalid IFF key.") + + next_offset = self.offset + self.size + size = self.HEADER_SIZE + data_size = 0 + if data: + data_size = len(data) + padding = data_size % 2 + size += data_size + padding + insert_bytes(self._fileobj, size, next_offset) + self._fileobj.seek(next_offset) + self.write_new_header(id_.ljust(4).encode('ascii'), data_size) + self._fileobj.seek(next_offset) + chunk = self.parse_next_subchunk() + self._update_size(chunk.size) + if data: + chunk.write(data) + self.subchunks().append(chunk) + self._fileobj.flush() + return chunk + + def __contains__(self, id_): + """Check if this chunk contains a specific subchunk.""" + assert_valid_chunk_id(id_) + try: + self[id_] + return True + except KeyError: + return False + + def __getitem__(self, id_): + """Get a subchunk by ID.""" + assert_valid_chunk_id(id_) + found_chunk = None + for chunk in self.subchunks(): + if chunk.id == id_: + found_chunk = chunk + break + else: + raise KeyError("No %r chunk found" % id_) + return found_chunk + + def __delitem__(self, id_): + """Remove a chunk from the IFF file""" + assert_valid_chunk_id(id_) + self[id_].delete() + + def _remove_subchunk(self, chunk): + assert chunk in self.__subchunks + self._update_size(-chunk.size, chunk) + self.__subchunks.remove(chunk) + + def _update_sibling_offsets(self, changed_subchunk, size_diff): + """Update the offsets of subchunks after `changed_subchunk`. + """ + index = self.__subchunks.index(changed_subchunk) + sibling_chunks = self.__subchunks[index + 1:len(self.__subchunks)] + for sibling in sibling_chunks: + sibling.offset -= size_diff + sibling.data_offset -= size_diff + + +class IffFile: + """Representation of a IFF file""" + + def __init__(self, chunk_cls, fileobj): + fileobj.seek(0) + self.root = chunk_cls.parse(fileobj) + + def __contains__(self, id_): + """Check if the IFF file contains a specific chunk""" + return id_ in self.root + + def __getitem__(self, id_): + """Get a chunk from the IFF file""" + return self.root[id_] + + def __delitem__(self, id_): + """Remove a chunk from the IFF file""" + self.delete_chunk(id_) + + def delete_chunk(self, id_): + """Remove a chunk from the IFF file""" + del self.root[id_] + + def insert_chunk(self, id_, data=None): + """Insert a new chunk at the end of the IFF file""" + return self.root.insert_chunk(id_, data) + + +class IffID3(ID3): + """A generic IFF file with ID3v2 tags""" + + def _load_file(self, fileobj): + raise error("Not implemented") + + def _pre_load_header(self, fileobj): + try: + fileobj.seek(self._load_file(fileobj)['ID3'].data_offset) + except (InvalidChunk, KeyError): + raise ID3NoHeaderError("No ID3 chunk") + + @convert_error(IOError, error) + @loadfile(writable=True) + def save(self, filething=None, v2_version=4, v23_sep='/', padding=None): + """Save ID3v2 data to the IFF file""" + + fileobj = filething.fileobj + + iff_file = self._load_file(fileobj) + + if 'ID3' not in iff_file: + iff_file.insert_chunk('ID3') + + chunk = iff_file['ID3'] + + try: + data = self._prepare_data( + fileobj, chunk.data_offset, chunk.data_size, v2_version, + v23_sep, padding) + except ID3Error as e: + reraise(error, e, sys.exc_info()[2]) + + chunk.resize(len(data)) + chunk.write(data) + + @convert_error(IOError, error) + @loadfile(writable=True) + def delete(self, filething=None): + """Completely removes the ID3 chunk from the IFF file""" + + try: + iff_file = self._load_file(filething.fileobj) + del iff_file['ID3'] + except KeyError: + pass + self.clear() diff --git a/libs/common/mutagen/_riff.py b/libs/common/mutagen/_riff.py new file mode 100644 index 00000000..f3f23f6a --- /dev/null +++ b/libs/common/mutagen/_riff.py @@ -0,0 +1,69 @@ +# Copyright (C) 2017 Borewit +# Copyright (C) 2019-2020 Philipp Wolfer +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +"""Resource Interchange File Format (RIFF).""" + +import struct +from struct import pack + +from mutagen._iff import ( + IffChunk, + IffContainerChunkMixin, + IffFile, + InvalidChunk, +) + + +class RiffChunk(IffChunk): + """Generic RIFF chunk""" + + @classmethod + def parse_header(cls, header): + return struct.unpack('<4sI', header) + + @classmethod + def get_class(cls, id): + if id in (u'LIST', u'RIFF'): + return RiffListChunk + else: + return cls + + def write_new_header(self, id_, size): + self._fileobj.write(pack('<4sI', id_, size)) + + def write_size(self): + self._fileobj.write(pack(' use our ctypes one as well - try: - del_windows_env_var(key) - except WindowsError: - pass - else: - os.unsetenv(key) - - -def putenv(key, value): - """Like `os.putenv` but takes unicode under Windows + Python 2 - - Args: - key (pathlike): The env var to get - value (pathlike): The value to set - Raises: - ValueError - """ - - key = path2fsn(key) - value = path2fsn(value) - - if is_win and PY2: - try: - set_windows_env_var(key, value) - except WindowsError: - # py3 + win fails here - raise ValueError - else: - try: - os.putenv(key, value) - except OSError: - # win + py3 raise here for invalid keys which is probably a bug. - # ValueError seems better - raise ValueError diff --git a/libs/common/mutagen/_senf/_fsnative.py b/libs/common/mutagen/_senf/_fsnative.py deleted file mode 100644 index a1e5967c..00000000 --- a/libs/common/mutagen/_senf/_fsnative.py +++ /dev/null @@ -1,666 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2016 Christoph Reiter -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -import os -import sys -import ctypes -import codecs - -from . import _winapi as winapi -from ._compat import text_type, PY3, PY2, urlparse, quote, unquote, urlunparse - - -is_win = os.name == "nt" -is_unix = not is_win -is_darwin = sys.platform == "darwin" - -_surrogatepass = "strict" if PY2 else "surrogatepass" - - -def _normalize_codec(codec, _cache={}): - """Raises LookupError""" - - try: - return _cache[codec] - except KeyError: - _cache[codec] = codecs.lookup(codec).name - return _cache[codec] - - -def _swap_bytes(data): - """swaps bytes for 16 bit, leaves remaining trailing bytes alone""" - - a, b = data[1::2], data[::2] - data = bytearray().join(bytearray(x) for x in zip(a, b)) - if len(b) > len(a): - data += b[-1:] - return bytes(data) - - -def _codec_fails_on_encode_surrogates(codec, _cache={}): - """Returns if a codec fails correctly when passing in surrogates with - a surrogatepass/surrogateescape error handler. Some codecs were broken - in Python <3.4 - """ - - try: - return _cache[codec] - except KeyError: - try: - u"\uD800\uDC01".encode(codec) - except UnicodeEncodeError: - _cache[codec] = True - else: - _cache[codec] = False - return _cache[codec] - - -def _codec_can_decode_with_surrogatepass(codec, _cache={}): - """Returns if a codec supports the surrogatepass error handler when - decoding. Some codecs were broken in Python <3.4 - """ - - try: - return _cache[codec] - except KeyError: - try: - u"\ud83d".encode( - codec, _surrogatepass).decode(codec, _surrogatepass) - except UnicodeDecodeError: - _cache[codec] = False - else: - _cache[codec] = True - return _cache[codec] - - -def _decode_surrogatepass(data, codec): - """Like data.decode(codec, 'surrogatepass') but makes utf-16-le/be work - on Python < 3.4 + Windows - - https://bugs.python.org/issue27971 - - Raises UnicodeDecodeError, LookupError - """ - - try: - return data.decode(codec, _surrogatepass) - except UnicodeDecodeError: - if not _codec_can_decode_with_surrogatepass(codec): - if _normalize_codec(codec) == "utf-16-be": - data = _swap_bytes(data) - codec = "utf-16-le" - if _normalize_codec(codec) == "utf-16-le": - buffer_ = ctypes.create_string_buffer(data + b"\x00\x00") - value = ctypes.wstring_at(buffer_, len(data) // 2) - if value.encode("utf-16-le", _surrogatepass) != data: - raise - return value - else: - raise - else: - raise - - -def _winpath2bytes_py3(text, codec): - """Fallback implementation for text including surrogates""" - - # merge surrogate codepoints - if _normalize_codec(codec).startswith("utf-16"): - # fast path, utf-16 merges anyway - return text.encode(codec, _surrogatepass) - return _decode_surrogatepass( - text.encode("utf-16-le", _surrogatepass), - "utf-16-le").encode(codec, _surrogatepass) - - -if PY2: - def _winpath2bytes(text, codec): - return text.encode(codec) -else: - def _winpath2bytes(text, codec): - if _codec_fails_on_encode_surrogates(codec): - try: - return text.encode(codec) - except UnicodeEncodeError: - return _winpath2bytes_py3(text, codec) - else: - return _winpath2bytes_py3(text, codec) - - -def fsn2norm(path): - """ - Args: - path (fsnative): The path to normalize - Returns: - `fsnative` - - Normalizes an fsnative path. - - The same underlying path can have multiple representations as fsnative - (due to surrogate pairs and variable length encodings). When concatenating - fsnative the result might be different than concatenating the serialized - form and then deserializing it. - - This returns the normalized form i.e. the form which os.listdir() would - return. This is useful when you alter fsnative but require that the same - underlying path always maps to the same fsnative value. - - All functions like :func:`bytes2fsn`, :func:`fsnative`, :func:`text2fsn` - and :func:`path2fsn` always return a normalized path, independent of their - input. - """ - - native = _fsn2native(path) - - if is_win: - return _decode_surrogatepass( - native.encode("utf-16-le", _surrogatepass), - "utf-16-le") - elif PY3: - return bytes2fsn(native, None) - else: - return path - - -def _fsn2legacy(path): - """Takes a fsnative path and returns a path that can be put into os.environ - or sys.argv. Might result in a mangled path on Python2 + Windows. - Can't fail. - - Args: - path (fsnative) - Returns: - str - """ - - if PY2 and is_win: - return path.encode(_encoding, "replace") - return path - - -def _fsnative(text): - if not isinstance(text, text_type): - raise TypeError("%r needs to be a text type (%r)" % (text, text_type)) - - if is_unix: - # First we go to bytes so we can be sure we have a valid source. - # Theoretically we should fail here in case we have a non-unicode - # encoding. But this would make everything complicated and there is - # no good way to handle a failure from the user side. Instead - # fall back to utf-8 which is the most likely the right choice in - # a mis-configured environment - encoding = _encoding - try: - path = text.encode(encoding, _surrogatepass) - except UnicodeEncodeError: - path = text.encode("utf-8", _surrogatepass) - - if b"\x00" in path: - path = path.replace(b"\x00", fsn2bytes(_fsnative(u"\uFFFD"), None)) - - if PY3: - return path.decode(_encoding, "surrogateescape") - return path - else: - if u"\x00" in text: - text = text.replace(u"\x00", u"\uFFFD") - text = fsn2norm(text) - return text - - -def _create_fsnative(type_): - # a bit of magic to make fsnative(u"foo") and isinstance(path, fsnative) - # work - - class meta(type): - - def __instancecheck__(self, instance): - return _typecheck_fsnative(instance) - - def __subclasscheck__(self, subclass): - return issubclass(subclass, type_) - - class impl(object): - """fsnative(text=u"") - - Args: - text (text): The text to convert to a path - Returns: - fsnative: The new path. - Raises: - TypeError: In case something other then `text` has been passed - - This type is a virtual base class for the real path type. - Instantiating it returns an instance of the real path type and it - overrides instance and subclass checks so that `isinstance` and - `issubclass` checks work: - - :: - - isinstance(fsnative(u"foo"), fsnative) == True - issubclass(type(fsnative(u"foo")), fsnative) == True - - The real returned type is: - - - **Python 2 + Windows:** :obj:`python:unicode`, with ``surrogates``, - without ``null`` - - **Python 2 + Unix:** :obj:`python:str`, without ``null`` - - **Python 3 + Windows:** :obj:`python3:str`, with ``surrogates``, - without ``null`` - - **Python 3 + Unix:** :obj:`python3:str`, with ``surrogates``, without - ``null``, without code points not encodable with the locale encoding - - Constructing a `fsnative` can't fail. - - Passing a `fsnative` to :func:`open` will never lead to `ValueError` - or `TypeError`. - - Any operation on `fsnative` can also use the `str` type, as long as - the `str` only contains ASCII and no NULL. - """ - - def __new__(cls, text=u""): - return _fsnative(text) - - new_type = meta("fsnative", (object,), dict(impl.__dict__)) - new_type.__module__ = "senf" - return new_type - - -fsnative_type = text_type if is_win or PY3 else bytes -fsnative = _create_fsnative(fsnative_type) - - -def _typecheck_fsnative(path): - """ - Args: - path (object) - Returns: - bool: if path is a fsnative - """ - - if not isinstance(path, fsnative_type): - return False - - if PY3 or is_win: - if u"\x00" in path: - return False - - if is_unix: - try: - path.encode(_encoding, "surrogateescape") - except UnicodeEncodeError: - return False - elif b"\x00" in path: - return False - - return True - - -def _fsn2native(path): - """ - Args: - path (fsnative) - Returns: - `text` on Windows, `bytes` on Unix - Raises: - TypeError: in case the type is wrong or the ´str` on Py3 + Unix - can't be converted to `bytes` - - This helper allows to validate the type and content of a path. - To reduce overhead the encoded value for Py3 + Unix is returned so - it can be reused. - """ - - if not isinstance(path, fsnative_type): - raise TypeError("path needs to be %s, not %s" % ( - fsnative_type.__name__, type(path).__name__)) - - if is_unix: - if PY3: - try: - path = path.encode(_encoding, "surrogateescape") - except UnicodeEncodeError: - # This look more like ValueError, but raising only one error - # makes things simpler... also one could say str + surrogates - # is its own type - raise TypeError( - "path contained Unicode code points not valid in" - "the current path encoding. To create a valid " - "path from Unicode use text2fsn()") - - if b"\x00" in path: - raise TypeError("fsnative can't contain nulls") - else: - if u"\x00" in path: - raise TypeError("fsnative can't contain nulls") - - return path - - -def _get_encoding(): - """The encoding used for paths, argv, environ, stdout and stdin""" - - encoding = sys.getfilesystemencoding() - if encoding is None: - if is_darwin: - encoding = "utf-8" - elif is_win: - encoding = "mbcs" - else: - encoding = "ascii" - encoding = _normalize_codec(encoding) - return encoding - - -_encoding = _get_encoding() - - -def path2fsn(path): - """ - Args: - path (pathlike): The path to convert - Returns: - `fsnative` - Raises: - TypeError: In case the type can't be converted to a `fsnative` - ValueError: In case conversion fails - - Returns a `fsnative` path for a `pathlike`. - """ - - # allow mbcs str on py2+win and bytes on py3 - if PY2: - if is_win: - if isinstance(path, bytes): - path = path.decode(_encoding) - else: - if isinstance(path, text_type): - path = path.encode(_encoding) - if "\x00" in path: - raise ValueError("embedded null") - else: - path = getattr(os, "fspath", lambda x: x)(path) - if isinstance(path, bytes): - if b"\x00" in path: - raise ValueError("embedded null") - path = path.decode(_encoding, "surrogateescape") - elif is_unix and isinstance(path, str): - # make sure we can encode it and this is not just some random - # unicode string - data = path.encode(_encoding, "surrogateescape") - if b"\x00" in data: - raise ValueError("embedded null") - path = fsn2norm(path) - else: - if u"\x00" in path: - raise ValueError("embedded null") - path = fsn2norm(path) - - if not isinstance(path, fsnative_type): - raise TypeError("path needs to be %s", fsnative_type.__name__) - - return path - - -def fsn2text(path, strict=False): - """ - Args: - path (fsnative): The path to convert - strict (bool): Fail in case the conversion is not reversible - Returns: - `text` - Raises: - TypeError: In case no `fsnative` has been passed - ValueError: In case ``strict`` was True and the conversion failed - - Converts a `fsnative` path to `text`. - - Can be used to pass a path to some unicode API, like for example a GUI - toolkit. - - If ``strict`` is True the conversion will fail in case it is not - reversible. This can be useful for converting program arguments that are - supposed to be text and erroring out in case they are not. - - Encoding with a Unicode encoding will always succeed with the result. - """ - - path = _fsn2native(path) - - errors = "strict" if strict else "replace" - - if is_win: - return path.encode("utf-16-le", _surrogatepass).decode("utf-16-le", - errors) - else: - return path.decode(_encoding, errors) - - -def text2fsn(text): - """ - Args: - text (text): The text to convert - Returns: - `fsnative` - Raises: - TypeError: In case no `text` has been passed - - Takes `text` and converts it to a `fsnative`. - - This operation is not reversible and can't fail. - """ - - return fsnative(text) - - -def fsn2bytes(path, encoding="utf-8"): - """ - Args: - path (fsnative): The path to convert - encoding (`str`): encoding used for Windows - Returns: - `bytes` - Raises: - TypeError: If no `fsnative` path is passed - ValueError: If encoding fails or the encoding is invalid - - Converts a `fsnative` path to `bytes`. - - The passed *encoding* is only used on platforms where paths are not - associated with an encoding (Windows for example). - - For Windows paths, lone surrogates will be encoded like normal code points - and surrogate pairs will be merged before encoding. In case of ``utf-8`` - or ``utf-16-le`` this is equal to the `WTF-8 and WTF-16 encoding - `__. - """ - - path = _fsn2native(path) - - if is_win: - if encoding is None: - raise ValueError("invalid encoding %r" % encoding) - - try: - return _winpath2bytes(path, encoding) - except LookupError: - raise ValueError("invalid encoding %r" % encoding) - else: - return path - - -def bytes2fsn(data, encoding="utf-8"): - """ - Args: - data (bytes): The data to convert - encoding (`str`): encoding used for Windows - Returns: - `fsnative` - Raises: - TypeError: If no `bytes` path is passed - ValueError: If decoding fails or the encoding is invalid - - Turns `bytes` to a `fsnative` path. - - The passed *encoding* is only used on platforms where paths are not - associated with an encoding (Windows for example). - - For Windows paths ``WTF-8`` is accepted if ``utf-8`` is used and - ``WTF-16`` accepted if ``utf-16-le`` is used. - """ - - if not isinstance(data, bytes): - raise TypeError("data needs to be bytes") - - if is_win: - if encoding is None: - raise ValueError("invalid encoding %r" % encoding) - try: - path = _decode_surrogatepass(data, encoding) - except LookupError: - raise ValueError("invalid encoding %r" % encoding) - if u"\x00" in path: - raise ValueError("contains nulls") - return path - else: - if b"\x00" in data: - raise ValueError("contains nulls") - if PY2: - return data - else: - return data.decode(_encoding, "surrogateescape") - - -def uri2fsn(uri): - """ - Args: - uri (`text` or :obj:`python:str`): A file URI - Returns: - `fsnative` - Raises: - TypeError: In case an invalid type is passed - ValueError: In case the URI isn't a valid file URI - - Takes a file URI and returns a `fsnative` path - """ - - if PY2: - if isinstance(uri, text_type): - uri = uri.encode("utf-8") - if not isinstance(uri, bytes): - raise TypeError("uri needs to be ascii str or unicode") - else: - if not isinstance(uri, str): - raise TypeError("uri needs to be str") - - parsed = urlparse(uri) - scheme = parsed.scheme - netloc = parsed.netloc - path = parsed.path - - if scheme != "file": - raise ValueError("Not a file URI: %r" % uri) - - if not path: - raise ValueError("Invalid file URI: %r" % uri) - - uri = urlunparse(parsed)[7:] - - if is_win: - try: - drive, rest = uri.split(":", 1) - except ValueError: - path = "" - rest = uri.replace("/", "\\") - else: - path = drive[-1] + ":" - rest = rest.replace("/", "\\") - if PY2: - path += unquote(rest) - else: - path += unquote(rest, encoding="utf-8", errors="surrogatepass") - if netloc: - path = "\\\\" + path - if PY2: - path = path.decode("utf-8") - if u"\x00" in path: - raise ValueError("embedded null") - return path - else: - if PY2: - path = unquote(uri) - else: - path = unquote(uri, encoding=_encoding, errors="surrogateescape") - if "\x00" in path: - raise ValueError("embedded null") - return path - - -def fsn2uri(path): - """ - Args: - path (fsnative): The path to convert to an URI - Returns: - `text`: An ASCII only URI - Raises: - TypeError: If no `fsnative` was passed - ValueError: If the path can't be converted - - Takes a `fsnative` path and returns a file URI. - - On Windows non-ASCII characters will be encoded using utf-8 and then - percent encoded. - """ - - path = _fsn2native(path) - - def _quote_path(path): - # RFC 2396 - path = quote(path, "/:@&=+$,") - if PY2: - path = path.decode("ascii") - return path - - if is_win: - buf = ctypes.create_unicode_buffer(winapi.INTERNET_MAX_URL_LENGTH) - length = winapi.DWORD(winapi.INTERNET_MAX_URL_LENGTH) - flags = 0 - try: - winapi.UrlCreateFromPathW(path, buf, ctypes.byref(length), flags) - except WindowsError as e: - raise ValueError(e) - uri = buf[:length.value] - - # For some reason UrlCreateFromPathW escapes some chars outside of - # ASCII and some not. Unquote and re-quote with utf-8. - if PY3: - # latin-1 maps code points directly to bytes, which is what we want - uri = unquote(uri, "latin-1") - else: - # Python 2 does what we want by default - uri = unquote(uri) - - return _quote_path(uri.encode("utf-8", _surrogatepass)) - - else: - return u"file://" + _quote_path(path) diff --git a/libs/common/mutagen/_senf/_print.py b/libs/common/mutagen/_senf/_print.py deleted file mode 100644 index 63c50fa5..00000000 --- a/libs/common/mutagen/_senf/_print.py +++ /dev/null @@ -1,424 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2016 Christoph Reiter -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -import sys -import os -import ctypes -import re - -from ._fsnative import _encoding, is_win, is_unix, _surrogatepass, bytes2fsn -from ._compat import text_type, PY2, PY3 -from ._winansi import AnsiState, ansi_split -from . import _winapi as winapi - - -def print_(*objects, **kwargs): - """print_(*objects, sep=None, end=None, file=None, flush=False) - - Args: - objects (object): zero or more objects to print - sep (str): Object separator to use, defaults to ``" "`` - end (str): Trailing string to use, defaults to ``"\\n"``. - If end is ``"\\n"`` then `os.linesep` is used. - file (object): A file-like object, defaults to `sys.stdout` - flush (bool): If the file stream should be flushed - Raises: - EnvironmentError - - Like print(), but: - - * Supports printing filenames under Unix + Python 3 and Windows + Python 2 - * Emulates ANSI escape sequence support under Windows - * Never fails due to encoding/decoding errors. Tries hard to get everything - on screen as is, but will fall back to "?" if all fails. - - This does not conflict with ``colorama``, but will not use it on Windows. - """ - - sep = kwargs.get("sep") - sep = sep if sep is not None else " " - end = kwargs.get("end") - end = end if end is not None else "\n" - file = kwargs.get("file") - file = file if file is not None else sys.stdout - flush = bool(kwargs.get("flush", False)) - - if is_win: - _print_windows(objects, sep, end, file, flush) - else: - _print_unix(objects, sep, end, file, flush) - - -def _print_unix(objects, sep, end, file, flush): - """A print_() implementation which writes bytes""" - - encoding = _encoding - - if isinstance(sep, text_type): - sep = sep.encode(encoding, "replace") - if not isinstance(sep, bytes): - raise TypeError - - if isinstance(end, text_type): - end = end.encode(encoding, "replace") - if not isinstance(end, bytes): - raise TypeError - - if end == b"\n": - end = os.linesep - if PY3: - end = end.encode("ascii") - - parts = [] - for obj in objects: - if not isinstance(obj, text_type) and not isinstance(obj, bytes): - obj = text_type(obj) - if isinstance(obj, text_type): - if PY2: - obj = obj.encode(encoding, "replace") - else: - try: - obj = obj.encode(encoding, "surrogateescape") - except UnicodeEncodeError: - obj = obj.encode(encoding, "replace") - assert isinstance(obj, bytes) - parts.append(obj) - - data = sep.join(parts) + end - assert isinstance(data, bytes) - - file = getattr(file, "buffer", file) - - try: - file.write(data) - except TypeError: - if PY3: - # For StringIO, first try with surrogates - surr_data = data.decode(encoding, "surrogateescape") - try: - file.write(surr_data) - except (TypeError, ValueError): - file.write(data.decode(encoding, "replace")) - else: - # for file like objects with don't support bytes - file.write(data.decode(encoding, "replace")) - - if flush: - file.flush() - - -ansi_state = AnsiState() - - -def _print_windows(objects, sep, end, file, flush): - """The windows implementation of print_()""" - - h = winapi.INVALID_HANDLE_VALUE - - try: - fileno = file.fileno() - except (EnvironmentError, AttributeError): - pass - else: - if fileno == 1: - h = winapi.GetStdHandle(winapi.STD_OUTPUT_HANDLE) - elif fileno == 2: - h = winapi.GetStdHandle(winapi.STD_ERROR_HANDLE) - - encoding = _encoding - - parts = [] - for obj in objects: - if isinstance(obj, bytes): - obj = obj.decode(encoding, "replace") - if not isinstance(obj, text_type): - obj = text_type(obj) - parts.append(obj) - - if isinstance(sep, bytes): - sep = sep.decode(encoding, "replace") - if not isinstance(sep, text_type): - raise TypeError - - if isinstance(end, bytes): - end = end.decode(encoding, "replace") - if not isinstance(end, text_type): - raise TypeError - - if end == u"\n": - end = os.linesep - - text = sep.join(parts) + end - assert isinstance(text, text_type) - - is_console = True - if h == winapi.INVALID_HANDLE_VALUE: - is_console = False - else: - # get the default value - info = winapi.CONSOLE_SCREEN_BUFFER_INFO() - if not winapi.GetConsoleScreenBufferInfo(h, ctypes.byref(info)): - is_console = False - - if is_console: - # make sure we flush before we apply any console attributes - file.flush() - - # try to force a utf-8 code page, use the output CP if that fails - cp = winapi.GetConsoleOutputCP() - try: - encoding = "utf-8" - if winapi.SetConsoleOutputCP(65001) == 0: - encoding = None - - for is_ansi, part in ansi_split(text): - if is_ansi: - ansi_state.apply(h, part) - else: - if encoding is not None: - data = part.encode(encoding, _surrogatepass) - else: - data = _encode_codepage(cp, part) - os.write(fileno, data) - finally: - # reset the code page to what we had before - winapi.SetConsoleOutputCP(cp) - else: - # try writing bytes first, so in case of Python 2 StringIO we get - # the same type on all platforms - try: - file.write(text.encode("utf-8", _surrogatepass)) - except (TypeError, ValueError): - file.write(text) - - if flush: - file.flush() - - -def _readline_windows(): - """Raises OSError""" - - try: - fileno = sys.stdin.fileno() - except (EnvironmentError, AttributeError): - fileno = -1 - - # In case stdin is replaced, read from that - if fileno != 0: - return _readline_windows_fallback() - - h = winapi.GetStdHandle(winapi.STD_INPUT_HANDLE) - if h == winapi.INVALID_HANDLE_VALUE: - return _readline_windows_fallback() - - buf_size = 1024 - buf = ctypes.create_string_buffer(buf_size * ctypes.sizeof(winapi.WCHAR)) - read = winapi.DWORD() - - text = u"" - while True: - if winapi.ReadConsoleW( - h, buf, buf_size, ctypes.byref(read), None) == 0: - if not text: - return _readline_windows_fallback() - raise ctypes.WinError() - data = buf[:read.value * ctypes.sizeof(winapi.WCHAR)] - text += data.decode("utf-16-le", _surrogatepass) - if text.endswith(u"\r\n"): - return text[:-2] - - -def _decode_codepage(codepage, data): - """ - Args: - codepage (int) - data (bytes) - Returns: - `text` - - Decodes data using the given codepage. If some data can't be decoded - using the codepage it will not fail. - """ - - assert isinstance(data, bytes) - - if not data: - return u"" - - # get the required buffer length first - length = winapi.MultiByteToWideChar(codepage, 0, data, len(data), None, 0) - if length == 0: - raise ctypes.WinError() - - # now decode - buf = ctypes.create_unicode_buffer(length) - length = winapi.MultiByteToWideChar( - codepage, 0, data, len(data), buf, length) - if length == 0: - raise ctypes.WinError() - - return buf[:] - - -def _encode_codepage(codepage, text): - """ - Args: - codepage (int) - text (text) - Returns: - `bytes` - - Encode text using the given code page. Will not fail if a char - can't be encoded using that codepage. - """ - - assert isinstance(text, text_type) - - if not text: - return b"" - - size = (len(text.encode("utf-16-le", _surrogatepass)) // - ctypes.sizeof(winapi.WCHAR)) - - # get the required buffer size - length = winapi.WideCharToMultiByte( - codepage, 0, text, size, None, 0, None, None) - if length == 0: - raise ctypes.WinError() - - # decode to the buffer - buf = ctypes.create_string_buffer(length) - length = winapi.WideCharToMultiByte( - codepage, 0, text, size, buf, length, None, None) - if length == 0: - raise ctypes.WinError() - return buf[:length] - - -def _readline_windows_fallback(): - # In case reading from the console failed (maybe we get piped data) - # we assume the input was generated according to the output encoding. - # Got any better ideas? - assert is_win - cp = winapi.GetConsoleOutputCP() - data = getattr(sys.stdin, "buffer", sys.stdin).readline().rstrip(b"\r\n") - return _decode_codepage(cp, data) - - -def _readline_default(): - assert is_unix - data = getattr(sys.stdin, "buffer", sys.stdin).readline().rstrip(b"\r\n") - if PY3: - return data.decode(_encoding, "surrogateescape") - else: - return data - - -def _readline(): - if is_win: - return _readline_windows() - else: - return _readline_default() - - -def input_(prompt=None): - """ - Args: - prompt (object): Prints the passed object to stdout without - adding a trailing newline - Returns: - `fsnative` - Raises: - EnvironmentError - - Like :func:`python3:input` but returns a `fsnative` and allows printing - filenames as prompt to stdout. - - Use :func:`fsn2text` on the result if you just want to deal with text. - """ - - if prompt is not None: - print_(prompt, end="") - - return _readline() - - -def _get_file_name_for_handle(handle): - """(Windows only) Returns a file name for a file handle. - - Args: - handle (winapi.HANDLE) - Returns: - `text` or `None` if no file name could be retrieved. - """ - - assert is_win - assert handle != winapi.INVALID_HANDLE_VALUE - - size = winapi.FILE_NAME_INFO.FileName.offset + \ - winapi.MAX_PATH * ctypes.sizeof(winapi.WCHAR) - buf = ctypes.create_string_buffer(size) - - if winapi.GetFileInformationByHandleEx is None: - # Windows XP - return None - - status = winapi.GetFileInformationByHandleEx( - handle, winapi.FileNameInfo, buf, size) - if status == 0: - return None - - name_info = ctypes.cast( - buf, ctypes.POINTER(winapi.FILE_NAME_INFO)).contents - offset = winapi.FILE_NAME_INFO.FileName.offset - data = buf[offset:offset + name_info.FileNameLength] - return bytes2fsn(data, "utf-16-le") - - -def supports_ansi_escape_codes(fd): - """Returns whether the output device is capable of interpreting ANSI escape - codes when :func:`print_` is used. - - Args: - fd (int): file descriptor (e.g. ``sys.stdout.fileno()``) - Returns: - `bool` - """ - - if os.isatty(fd): - return True - - if not is_win: - return False - - # Check for cygwin/msys terminal - handle = winapi._get_osfhandle(fd) - if handle == winapi.INVALID_HANDLE_VALUE: - return False - - if winapi.GetFileType(handle) != winapi.FILE_TYPE_PIPE: - return False - - file_name = _get_file_name_for_handle(handle) - match = re.match( - "^\\\\(cygwin|msys)-[a-z0-9]+-pty[0-9]+-(from|to)-master$", file_name) - return match is not None diff --git a/libs/common/mutagen/_senf/_stdlib.py b/libs/common/mutagen/_senf/_stdlib.py deleted file mode 100644 index f3193d33..00000000 --- a/libs/common/mutagen/_senf/_stdlib.py +++ /dev/null @@ -1,154 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2016 Christoph Reiter -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -import re -import os - -from ._fsnative import path2fsn, fsnative, is_win -from ._compat import PY2 -from ._environ import environ - - -sep = path2fsn(os.sep) -pathsep = path2fsn(os.pathsep) -curdir = path2fsn(os.curdir) -pardir = path2fsn(os.pardir) -altsep = path2fsn(os.altsep) if os.altsep is not None else None -extsep = path2fsn(os.extsep) -devnull = path2fsn(os.devnull) -defpath = path2fsn(os.defpath) - - -def getcwd(): - """Like `os.getcwd` but returns a `fsnative` path - - Returns: - `fsnative` - """ - - if is_win and PY2: - return os.getcwdu() - return os.getcwd() - - -def _get_userdir(user=None): - """Returns the user dir or None""" - - if user is not None and not isinstance(user, fsnative): - raise TypeError - - if is_win: - if "HOME" in environ: - path = environ["HOME"] - elif "USERPROFILE" in environ: - path = environ["USERPROFILE"] - elif "HOMEPATH" in environ and "HOMEDRIVE" in environ: - path = os.path.join(environ["HOMEDRIVE"], environ["HOMEPATH"]) - else: - return - - if user is None: - return path - else: - return os.path.join(os.path.dirname(path), user) - else: - import pwd - - if user is None: - if "HOME" in environ: - return environ["HOME"] - else: - try: - return path2fsn(pwd.getpwuid(os.getuid()).pw_dir) - except KeyError: - return - else: - try: - return path2fsn(pwd.getpwnam(user).pw_dir) - except KeyError: - return - - -def expanduser(path): - """ - Args: - path (pathlike): A path to expand - Returns: - `fsnative` - - Like :func:`python:os.path.expanduser` but supports unicode home - directories under Windows + Python 2 and always returns a `fsnative`. - """ - - path = path2fsn(path) - - if path == "~": - return _get_userdir() - elif path.startswith("~" + sep) or ( - altsep is not None and path.startswith("~" + altsep)): - userdir = _get_userdir() - if userdir is None: - return path - return userdir + path[1:] - elif path.startswith("~"): - sep_index = path.find(sep) - if altsep is not None: - alt_index = path.find(altsep) - if alt_index != -1 and alt_index < sep_index: - sep_index = alt_index - - if sep_index == -1: - user = path[1:] - rest = "" - else: - user = path[1:sep_index] - rest = path[sep_index:] - - userdir = _get_userdir(user) - if userdir is not None: - return userdir + rest - else: - return path - else: - return path - - -def expandvars(path): - """ - Args: - path (pathlike): A path to expand - Returns: - `fsnative` - - Like :func:`python:os.path.expandvars` but supports unicode under Windows - + Python 2 and always returns a `fsnative`. - """ - - path = path2fsn(path) - - def repl_func(match): - return environ.get(match.group(1), match.group(0)) - - path = re.compile(r"\$(\w+)", flags=re.UNICODE).sub(repl_func, path) - if os.name == "nt": - path = re.sub(r"%([^%]+)%", repl_func, path) - return re.sub(r"\$\{([^\}]+)\}", repl_func, path) diff --git a/libs/common/mutagen/_senf/_temp.py b/libs/common/mutagen/_senf/_temp.py deleted file mode 100644 index d29b7217..00000000 --- a/libs/common/mutagen/_senf/_temp.py +++ /dev/null @@ -1,96 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2016 Christoph Reiter -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -import tempfile - -from ._fsnative import path2fsn, fsnative - - -def gettempdir(): - """ - Returns: - `fsnative` - - Like :func:`python3:tempfile.gettempdir`, but always returns a `fsnative` - path - """ - - # FIXME: I don't want to reimplement all that logic, reading env vars etc. - # At least for the default it works. - return path2fsn(tempfile.gettempdir()) - - -def gettempprefix(): - """ - Returns: - `fsnative` - - Like :func:`python3:tempfile.gettempprefix`, but always returns a - `fsnative` path - """ - - return path2fsn(tempfile.gettempprefix()) - - -def mkstemp(suffix=None, prefix=None, dir=None, text=False): - """ - Args: - suffix (`pathlike` or `None`): suffix or `None` to use the default - prefix (`pathlike` or `None`): prefix or `None` to use the default - dir (`pathlike` or `None`): temp dir or `None` to use the default - text (bool): if the file should be opened in text mode - Returns: - Tuple[`int`, `fsnative`]: - A tuple containing the file descriptor and the file path - Raises: - EnvironmentError - - Like :func:`python3:tempfile.mkstemp` but always returns a `fsnative` - path. - """ - - suffix = fsnative() if suffix is None else path2fsn(suffix) - prefix = gettempprefix() if prefix is None else path2fsn(prefix) - dir = gettempdir() if dir is None else path2fsn(dir) - - return tempfile.mkstemp(suffix, prefix, dir, text) - - -def mkdtemp(suffix=None, prefix=None, dir=None): - """ - Args: - suffix (`pathlike` or `None`): suffix or `None` to use the default - prefix (`pathlike` or `None`): prefix or `None` to use the default - dir (`pathlike` or `None`): temp dir or `None` to use the default - Returns: - `fsnative`: A path to a directory - Raises: - EnvironmentError - - Like :func:`python3:tempfile.mkstemp` but always returns a `fsnative` path. - """ - - suffix = fsnative() if suffix is None else path2fsn(suffix) - prefix = gettempprefix() if prefix is None else path2fsn(prefix) - dir = gettempdir() if dir is None else path2fsn(dir) - - return tempfile.mkdtemp(suffix, prefix, dir) diff --git a/libs/common/mutagen/_senf/_winansi.py b/libs/common/mutagen/_senf/_winansi.py deleted file mode 100644 index fbbc1c22..00000000 --- a/libs/common/mutagen/_senf/_winansi.py +++ /dev/null @@ -1,319 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2016 Christoph Reiter -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -import ctypes -import re -import atexit - -from . import _winapi as winapi - - -def ansi_parse(code): - """Returns command, (args)""" - - return code[-1:], tuple([int(v or "0") for v in code[2:-1].split(";")]) - - -def ansi_split(text, _re=re.compile(u"(\x1b\\[(\\d*;?)*\\S)")): - """Yields (is_ansi, text)""" - - for part in _re.split(text): - if part: - yield (bool(_re.match(part)), part) - - -class AnsiCommand(object): - TEXT = "m" - - MOVE_UP = "A" - MOVE_DOWN = "B" - MOVE_FORWARD = "C" - MOVE_BACKWARD = "D" - - SET_POS = "H" - SET_POS_ALT = "f" - - SAVE_POS = "s" - RESTORE_POS = "u" - - -class TextAction(object): - RESET_ALL = 0 - - SET_BOLD = 1 - SET_DIM = 2 - SET_ITALIC = 3 - SET_UNDERLINE = 4 - SET_BLINK = 5 - SET_BLINK_FAST = 6 - SET_REVERSE = 7 - SET_HIDDEN = 8 - - RESET_BOLD = 21 - RESET_DIM = 22 - RESET_ITALIC = 23 - RESET_UNDERLINE = 24 - RESET_BLINK = 25 - RESET_BLINK_FAST = 26 - RESET_REVERSE = 27 - RESET_HIDDEN = 28 - - FG_BLACK = 30 - FG_RED = 31 - FG_GREEN = 32 - FG_YELLOW = 33 - FG_BLUE = 34 - FG_MAGENTA = 35 - FG_CYAN = 36 - FG_WHITE = 37 - - FG_DEFAULT = 39 - - FG_LIGHT_BLACK = 90 - FG_LIGHT_RED = 91 - FG_LIGHT_GREEN = 92 - FG_LIGHT_YELLOW = 93 - FG_LIGHT_BLUE = 94 - FG_LIGHT_MAGENTA = 95 - FG_LIGHT_CYAN = 96 - FG_LIGHT_WHITE = 97 - - BG_BLACK = 40 - BG_RED = 41 - BG_GREEN = 42 - BG_YELLOW = 43 - BG_BLUE = 44 - BG_MAGENTA = 45 - BG_CYAN = 46 - BG_WHITE = 47 - - BG_DEFAULT = 49 - - BG_LIGHT_BLACK = 100 - BG_LIGHT_RED = 101 - BG_LIGHT_GREEN = 102 - BG_LIGHT_YELLOW = 103 - BG_LIGHT_BLUE = 104 - BG_LIGHT_MAGENTA = 105 - BG_LIGHT_CYAN = 106 - BG_LIGHT_WHITE = 107 - - -class AnsiState(object): - - def __init__(self): - self.default_attrs = None - - self.bold = False - self.bg_light = False - self.fg_light = False - - self.saved_pos = (0, 0) - - def do_text_action(self, attrs, action): - # In case the external state has changed, apply it it to ours. - # Mostly the first time this is called. - if attrs & winapi.FOREGROUND_INTENSITY and not self.fg_light \ - and not self.bold: - self.fg_light = True - if attrs & winapi.BACKGROUND_INTENSITY and not self.bg_light: - self.bg_light = True - - dark_fg = { - TextAction.FG_BLACK: 0, - TextAction.FG_RED: winapi.FOREGROUND_RED, - TextAction.FG_GREEN: winapi.FOREGROUND_GREEN, - TextAction.FG_YELLOW: - winapi.FOREGROUND_GREEN | winapi.FOREGROUND_RED, - TextAction.FG_BLUE: winapi.FOREGROUND_BLUE, - TextAction.FG_MAGENTA: winapi.FOREGROUND_BLUE | - winapi.FOREGROUND_RED, - TextAction.FG_CYAN: - winapi.FOREGROUND_BLUE | winapi.FOREGROUND_GREEN, - TextAction.FG_WHITE: - winapi.FOREGROUND_BLUE | winapi.FOREGROUND_GREEN | - winapi.FOREGROUND_RED, - } - - dark_bg = { - TextAction.BG_BLACK: 0, - TextAction.BG_RED: winapi.BACKGROUND_RED, - TextAction.BG_GREEN: winapi.BACKGROUND_GREEN, - TextAction.BG_YELLOW: - winapi.BACKGROUND_GREEN | winapi.BACKGROUND_RED, - TextAction.BG_BLUE: winapi.BACKGROUND_BLUE, - TextAction.BG_MAGENTA: - winapi.BACKGROUND_BLUE | winapi.BACKGROUND_RED, - TextAction.BG_CYAN: - winapi.BACKGROUND_BLUE | winapi.BACKGROUND_GREEN, - TextAction.BG_WHITE: - winapi.BACKGROUND_BLUE | winapi.BACKGROUND_GREEN | - winapi.BACKGROUND_RED, - } - - light_fg = { - TextAction.FG_LIGHT_BLACK: 0, - TextAction.FG_LIGHT_RED: winapi.FOREGROUND_RED, - TextAction.FG_LIGHT_GREEN: winapi.FOREGROUND_GREEN, - TextAction.FG_LIGHT_YELLOW: - winapi.FOREGROUND_GREEN | winapi.FOREGROUND_RED, - TextAction.FG_LIGHT_BLUE: winapi.FOREGROUND_BLUE, - TextAction.FG_LIGHT_MAGENTA: - winapi.FOREGROUND_BLUE | winapi.FOREGROUND_RED, - TextAction.FG_LIGHT_CYAN: - winapi.FOREGROUND_BLUE | winapi.FOREGROUND_GREEN, - TextAction.FG_LIGHT_WHITE: - winapi.FOREGROUND_BLUE | winapi.FOREGROUND_GREEN | - winapi.FOREGROUND_RED, - } - - light_bg = { - TextAction.BG_LIGHT_BLACK: 0, - TextAction.BG_LIGHT_RED: winapi.BACKGROUND_RED, - TextAction.BG_LIGHT_GREEN: winapi.BACKGROUND_GREEN, - TextAction.BG_LIGHT_YELLOW: - winapi.BACKGROUND_GREEN | winapi.BACKGROUND_RED, - TextAction.BG_LIGHT_BLUE: winapi.BACKGROUND_BLUE, - TextAction.BG_LIGHT_MAGENTA: - winapi.BACKGROUND_BLUE | winapi.BACKGROUND_RED, - TextAction.BG_LIGHT_CYAN: - winapi.BACKGROUND_BLUE | winapi.BACKGROUND_GREEN, - TextAction.BG_LIGHT_WHITE: - winapi.BACKGROUND_BLUE | winapi.BACKGROUND_GREEN | - winapi.BACKGROUND_RED, - } - - if action == TextAction.RESET_ALL: - attrs = self.default_attrs - self.bold = self.fg_light = self.bg_light = False - elif action == TextAction.SET_BOLD: - self.bold = True - elif action == TextAction.RESET_BOLD: - self.bold = False - elif action == TextAction.SET_DIM: - self.bold = False - elif action == TextAction.SET_REVERSE: - attrs |= winapi.COMMON_LVB_REVERSE_VIDEO - elif action == TextAction.RESET_REVERSE: - attrs &= ~winapi.COMMON_LVB_REVERSE_VIDEO - elif action == TextAction.SET_UNDERLINE: - attrs |= winapi.COMMON_LVB_UNDERSCORE - elif action == TextAction.RESET_UNDERLINE: - attrs &= ~winapi.COMMON_LVB_UNDERSCORE - elif action == TextAction.FG_DEFAULT: - attrs = (attrs & ~0xF) | (self.default_attrs & 0xF) - self.fg_light = False - elif action == TextAction.BG_DEFAULT: - attrs = (attrs & ~0xF0) | (self.default_attrs & 0xF0) - self.bg_light = False - elif action in dark_fg: - attrs = (attrs & ~0xF) | dark_fg[action] - self.fg_light = False - elif action in dark_bg: - attrs = (attrs & ~0xF0) | dark_bg[action] - self.bg_light = False - elif action in light_fg: - attrs = (attrs & ~0xF) | light_fg[action] - self.fg_light = True - elif action in light_bg: - attrs = (attrs & ~0xF0) | light_bg[action] - self.bg_light = True - - if self.fg_light or self.bold: - attrs |= winapi.FOREGROUND_INTENSITY - else: - attrs &= ~winapi.FOREGROUND_INTENSITY - - if self.bg_light: - attrs |= winapi.BACKGROUND_INTENSITY - else: - attrs &= ~winapi.BACKGROUND_INTENSITY - - return attrs - - def apply(self, handle, code): - buffer_info = winapi.CONSOLE_SCREEN_BUFFER_INFO() - if not winapi.GetConsoleScreenBufferInfo(handle, - ctypes.byref(buffer_info)): - return - - attrs = buffer_info.wAttributes - - # We take the first attrs we see as default - if self.default_attrs is None: - self.default_attrs = attrs - # Make sure that like with linux terminals the program doesn't - # affect the prompt after it exits - atexit.register( - winapi.SetConsoleTextAttribute, handle, self.default_attrs) - - cmd, args = ansi_parse(code) - if cmd == AnsiCommand.TEXT: - for action in args: - attrs = self.do_text_action(attrs, action) - winapi.SetConsoleTextAttribute(handle, attrs) - elif cmd in (AnsiCommand.MOVE_UP, AnsiCommand.MOVE_DOWN, - AnsiCommand.MOVE_FORWARD, AnsiCommand.MOVE_BACKWARD): - - coord = buffer_info.dwCursorPosition - x, y = coord.X, coord.Y - - amount = max(args[0], 1) - - if cmd == AnsiCommand.MOVE_UP: - y -= amount - elif cmd == AnsiCommand.MOVE_DOWN: - y += amount - elif cmd == AnsiCommand.MOVE_FORWARD: - x += amount - elif cmd == AnsiCommand.MOVE_BACKWARD: - x -= amount - - x = max(x, 0) - y = max(y, 0) - winapi.SetConsoleCursorPosition(handle, winapi.COORD(x, y)) - elif cmd in (AnsiCommand.SET_POS, AnsiCommand.SET_POS_ALT): - args = list(args) - while len(args) < 2: - args.append(0) - x, y = args[:2] - - win_rect = buffer_info.srWindow - x += win_rect.Left - 1 - y += win_rect.Top - 1 - - x = max(x, 0) - y = max(y, 0) - winapi.SetConsoleCursorPosition(handle, winapi.COORD(x, y)) - elif cmd == AnsiCommand.SAVE_POS: - win_rect = buffer_info.srWindow - coord = buffer_info.dwCursorPosition - x, y = coord.X, coord.Y - x -= win_rect.Left - y -= win_rect.Top - self.saved_pos = (x, y) - elif cmd == AnsiCommand.RESTORE_POS: - win_rect = buffer_info.srWindow - x, y = self.saved_pos - x += win_rect.Left - y += win_rect.Top - winapi.SetConsoleCursorPosition(handle, winapi.COORD(x, y)) diff --git a/libs/common/mutagen/_senf/_winapi.py b/libs/common/mutagen/_senf/_winapi.py deleted file mode 100644 index 5e0f7854..00000000 --- a/libs/common/mutagen/_senf/_winapi.py +++ /dev/null @@ -1,222 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2016 Christoph Reiter -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be included -# in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -import ctypes -from ctypes import WinDLL, CDLL, wintypes - - -shell32 = WinDLL("shell32") -kernel32 = WinDLL("kernel32") -shlwapi = WinDLL("shlwapi") -msvcrt = CDLL("msvcrt") - -GetCommandLineW = kernel32.GetCommandLineW -GetCommandLineW.argtypes = [] -GetCommandLineW.restype = wintypes.LPCWSTR - -CommandLineToArgvW = shell32.CommandLineToArgvW -CommandLineToArgvW.argtypes = [ - wintypes.LPCWSTR, ctypes.POINTER(ctypes.c_int)] -CommandLineToArgvW.restype = ctypes.POINTER(wintypes.LPWSTR) - -LocalFree = kernel32.LocalFree -LocalFree.argtypes = [wintypes.HLOCAL] -LocalFree.restype = wintypes.HLOCAL - -# https://msdn.microsoft.com/en-us/library/windows/desktop/aa383751.aspx -LPCTSTR = ctypes.c_wchar_p -LPWSTR = wintypes.LPWSTR -LPCWSTR = ctypes.c_wchar_p -LPTSTR = LPWSTR -PCWSTR = ctypes.c_wchar_p -PCTSTR = PCWSTR -PWSTR = ctypes.c_wchar_p -PTSTR = PWSTR -LPVOID = wintypes.LPVOID -WCHAR = wintypes.WCHAR -LPSTR = ctypes.c_char_p - -BOOL = wintypes.BOOL -LPBOOL = ctypes.POINTER(BOOL) -UINT = wintypes.UINT -WORD = wintypes.WORD -DWORD = wintypes.DWORD -SHORT = wintypes.SHORT -HANDLE = wintypes.HANDLE -ULONG = wintypes.ULONG -LPCSTR = wintypes.LPCSTR - -STD_INPUT_HANDLE = DWORD(-10) -STD_OUTPUT_HANDLE = DWORD(-11) -STD_ERROR_HANDLE = DWORD(-12) - -INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value - -INTERNET_MAX_SCHEME_LENGTH = 32 -INTERNET_MAX_PATH_LENGTH = 2048 -INTERNET_MAX_URL_LENGTH = ( - INTERNET_MAX_SCHEME_LENGTH + len("://") + INTERNET_MAX_PATH_LENGTH) - -FOREGROUND_BLUE = 0x0001 -FOREGROUND_GREEN = 0x0002 -FOREGROUND_RED = 0x0004 -FOREGROUND_INTENSITY = 0x0008 - -BACKGROUND_BLUE = 0x0010 -BACKGROUND_GREEN = 0x0020 -BACKGROUND_RED = 0x0040 -BACKGROUND_INTENSITY = 0x0080 - -COMMON_LVB_REVERSE_VIDEO = 0x4000 -COMMON_LVB_UNDERSCORE = 0x8000 - -UrlCreateFromPathW = shlwapi.UrlCreateFromPathW -UrlCreateFromPathW.argtypes = [ - PCTSTR, PTSTR, ctypes.POINTER(DWORD), DWORD] -UrlCreateFromPathW.restype = ctypes.HRESULT - -SetEnvironmentVariableW = kernel32.SetEnvironmentVariableW -SetEnvironmentVariableW.argtypes = [LPCTSTR, LPCTSTR] -SetEnvironmentVariableW.restype = wintypes.BOOL - -GetEnvironmentVariableW = kernel32.GetEnvironmentVariableW -GetEnvironmentVariableW.argtypes = [LPCTSTR, LPTSTR, DWORD] -GetEnvironmentVariableW.restype = DWORD - -GetEnvironmentStringsW = kernel32.GetEnvironmentStringsW -GetEnvironmentStringsW.argtypes = [] -GetEnvironmentStringsW.restype = ctypes.c_void_p - -FreeEnvironmentStringsW = kernel32.FreeEnvironmentStringsW -FreeEnvironmentStringsW.argtypes = [ctypes.c_void_p] -FreeEnvironmentStringsW.restype = ctypes.c_bool - -GetStdHandle = kernel32.GetStdHandle -GetStdHandle.argtypes = [DWORD] -GetStdHandle.restype = HANDLE - - -class COORD(ctypes.Structure): - - _fields_ = [ - ("X", SHORT), - ("Y", SHORT), - ] - - -class SMALL_RECT(ctypes.Structure): - - _fields_ = [ - ("Left", SHORT), - ("Top", SHORT), - ("Right", SHORT), - ("Bottom", SHORT), - ] - - -class CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure): - - _fields_ = [ - ("dwSize", COORD), - ("dwCursorPosition", COORD), - ("wAttributes", WORD), - ("srWindow", SMALL_RECT), - ("dwMaximumWindowSize", COORD), - ] - - -GetConsoleScreenBufferInfo = kernel32.GetConsoleScreenBufferInfo -GetConsoleScreenBufferInfo.argtypes = [ - HANDLE, ctypes.POINTER(CONSOLE_SCREEN_BUFFER_INFO)] -GetConsoleScreenBufferInfo.restype = BOOL - -GetConsoleOutputCP = kernel32.GetConsoleOutputCP -GetConsoleOutputCP.argtypes = [] -GetConsoleOutputCP.restype = UINT - -SetConsoleOutputCP = kernel32.SetConsoleOutputCP -SetConsoleOutputCP.argtypes = [UINT] -SetConsoleOutputCP.restype = BOOL - -GetConsoleCP = kernel32.GetConsoleCP -GetConsoleCP.argtypes = [] -GetConsoleCP.restype = UINT - -SetConsoleCP = kernel32.SetConsoleCP -SetConsoleCP.argtypes = [UINT] -SetConsoleCP.restype = BOOL - -SetConsoleTextAttribute = kernel32.SetConsoleTextAttribute -SetConsoleTextAttribute.argtypes = [HANDLE, WORD] -SetConsoleTextAttribute.restype = BOOL - -SetConsoleCursorPosition = kernel32.SetConsoleCursorPosition -SetConsoleCursorPosition.argtypes = [HANDLE, COORD] -SetConsoleCursorPosition.restype = BOOL - -ReadConsoleW = kernel32.ReadConsoleW -ReadConsoleW.argtypes = [HANDLE, LPVOID, DWORD, ctypes.POINTER(DWORD), LPVOID] -ReadConsoleW.restype = BOOL - -MultiByteToWideChar = kernel32.MultiByteToWideChar -MultiByteToWideChar.argtypes = [ - UINT, DWORD, LPCSTR, ctypes.c_int, LPWSTR, ctypes.c_int] -MultiByteToWideChar.restype = ctypes.c_int - -WideCharToMultiByte = kernel32.WideCharToMultiByte -WideCharToMultiByte.argtypes = [ - UINT, DWORD, LPCWSTR, ctypes.c_int, LPSTR, ctypes.c_int, LPCSTR, LPBOOL] -WideCharToMultiByte.restpye = ctypes.c_int - -MoveFileW = kernel32.MoveFileW -MoveFileW.argtypes = [LPCTSTR, LPCTSTR] -MoveFileW.restype = BOOL - -if hasattr(kernel32, "GetFileInformationByHandleEx"): - GetFileInformationByHandleEx = kernel32.GetFileInformationByHandleEx - GetFileInformationByHandleEx.argtypes = [ - HANDLE, ctypes.c_int, ctypes.c_void_p, DWORD] - GetFileInformationByHandleEx.restype = BOOL -else: - # Windows XP - GetFileInformationByHandleEx = None - -MAX_PATH = 260 -FileNameInfo = 2 - - -class FILE_NAME_INFO(ctypes.Structure): - _fields_ = [ - ("FileNameLength", DWORD), - ("FileName", WCHAR), - ] - - -_get_osfhandle = msvcrt._get_osfhandle -_get_osfhandle.argtypes = [ctypes.c_int] -_get_osfhandle.restype = HANDLE - -GetFileType = kernel32.GetFileType -GetFileType.argtypes = [HANDLE] -GetFileType.restype = DWORD - -FILE_TYPE_PIPE = 0x0003 diff --git a/libs/common/mutagen/_tags.py b/libs/common/mutagen/_tags.py index c3f2ebf6..aa62a771 100644 --- a/libs/common/mutagen/_tags.py +++ b/libs/common/mutagen/_tags.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2005 Michael Urman # # This program is free software; you can redistribute it and/or modify @@ -115,7 +114,7 @@ class Metadata(Tags): raise NotImplementedError @loadfile(writable=False) - def save(self, filething, **kwargs): + def save(self, filething=None, **kwargs): """save(filething=None, **kwargs) Save changes to a file. @@ -129,7 +128,7 @@ class Metadata(Tags): raise NotImplementedError @loadfile(writable=False) - def delete(self, filething): + def delete(self, filething=None): """delete(filething=None) Remove tags from a file. diff --git a/libs/common/mutagen/_tools/__init__.py b/libs/common/mutagen/_tools/__init__.py index 3e6b1556..94b5bb2f 100644 --- a/libs/common/mutagen/_tools/__init__.py +++ b/libs/common/mutagen/_tools/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2016 Christoph Reiter # # This program is free software; you can redistribute it and/or modify diff --git a/libs/common/mutagen/_tools/_util.py b/libs/common/mutagen/_tools/_util.py index 4e050769..513f3989 100644 --- a/libs/common/mutagen/_tools/_util.py +++ b/libs/common/mutagen/_tools/_util.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015 Christoph Reiter # # This program is free software; you can redistribute it and/or modify @@ -11,8 +10,7 @@ import signal import contextlib import optparse -from mutagen._senf import print_ -from mutagen._compat import text_type, iterbytes +from mutagen._util import iterbytes def split_escape(string, sep, maxsplit=None, escape_char="\\"): @@ -25,7 +23,7 @@ def split_escape(string, sep, maxsplit=None, escape_char="\\"): assert len(escape_char) == 1 if isinstance(string, bytes): - if isinstance(escape_char, text_type): + if isinstance(escape_char, str): escape_char = escape_char.encode("ascii") iter_ = iterbytes else: @@ -88,8 +86,4 @@ class SignalHandler(object): raise SystemExit("Aborted...") -class OptionParser(optparse.OptionParser): - """OptionParser subclass which supports printing Unicode under Windows""" - - def print_help(self, file=None): - print_(self.format_help(), file=file) +OptionParser = optparse.OptionParser diff --git a/libs/common/mutagen/_tools/mid3cp.py b/libs/common/mutagen/_tools/mid3cp.py index 1339548d..b48d285c 100644 --- a/libs/common/mutagen/_tools/mid3cp.py +++ b/libs/common/mutagen/_tools/mid3cp.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2014 Marcus Sundman # # This program is free software; you can redistribute it and/or modify @@ -15,8 +14,6 @@ import os.path import mutagen import mutagen.id3 -from mutagen._senf import print_, argv -from mutagen._compat import text_type from ._util import SignalHandler, OptionParser @@ -25,11 +22,6 @@ VERSION = (0, 1) _sig = SignalHandler() -def printerr(*args, **kwargs): - kwargs.setdefault("file", sys.stderr) - print_(*args, **kwargs) - - class ID3OptionParser(OptionParser): def __init__(self): mutagen_version = mutagen.version_string @@ -52,15 +44,15 @@ def copy(src, dst, merge, write_v1=True, excluded_tags=None, verbose=False): try: id3 = mutagen.id3.ID3(src, translate=False) except mutagen.id3.ID3NoHeaderError: - print_(u"No ID3 header found in ", src, file=sys.stderr) + print(u"No ID3 header found in ", src, file=sys.stderr) return 1 except Exception as err: - print_(str(err), file=sys.stderr) + print(str(err), file=sys.stderr) return 1 if verbose: - print_(u"File", src, u"contains:", file=sys.stderr) - print_(id3.pprint(), file=sys.stderr) + print(u"File", src, u"contains:", file=sys.stderr) + print(id3.pprint(), file=sys.stderr) for tag in excluded_tags: id3.delall(tag) @@ -72,7 +64,7 @@ def copy(src, dst, merge, write_v1=True, excluded_tags=None, verbose=False): # no need to merge pass except Exception as err: - print_(str(err), file=sys.stderr) + print(str(err), file=sys.stderr) return 1 else: for frame in id3.values(): @@ -91,12 +83,12 @@ def copy(src, dst, merge, write_v1=True, excluded_tags=None, verbose=False): try: id3.save(dst, v1=(2 if write_v1 else 0), v2_version=v2_version) except Exception as err: - print_(u"Error saving", dst, u":\n%s" % text_type(err), - file=sys.stderr) + print(u"Error saving", dst, u":\n%s" % str(err), + file=sys.stderr) return 1 else: if verbose: - print_(u"Successfully saved", dst, file=sys.stderr) + print(u"Successfully saved", dst, file=sys.stderr) return 0 @@ -120,12 +112,12 @@ def main(argv): (src, dst) = args if not os.path.isfile(src): - print_(u"File not found:", src, file=sys.stderr) + print(u"File not found:", src, file=sys.stderr) parser.print_help(file=sys.stderr) return 1 if not os.path.isfile(dst): - printerr(u"File not found:", dst, file=sys.stderr) + print(u"File not found:", dst, file=sys.stderr) parser.print_help(file=sys.stderr) return 1 @@ -139,4 +131,4 @@ def main(argv): def entry_point(): _sig.init() - return main(argv) + return main(sys.argv) diff --git a/libs/common/mutagen/_tools/mid3iconv.py b/libs/common/mutagen/_tools/mid3iconv.py index 554f6bb8..501066a4 100644 --- a/libs/common/mutagen/_tools/mid3iconv.py +++ b/libs/common/mutagen/_tools/mid3iconv.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2006 Emfox Zhou # # This program is free software; you can redistribute it and/or modify @@ -15,8 +14,6 @@ import locale import mutagen import mutagen.id3 -from mutagen._senf import argv, print_, fsnative -from mutagen._compat import text_type from ._util import SignalHandler, OptionParser @@ -75,7 +72,7 @@ def update(options, filenames): for filename in filenames: with _sig.block(): if verbose != "quiet": - print_(u"Updating", filename) + print(u"Updating", filename) if has_id3v1(filename) and not noupdate and force_v1: mutagen.id3.delete(filename, False, True) @@ -84,10 +81,10 @@ def update(options, filenames): id3 = mutagen.id3.ID3(filename) except mutagen.id3.ID3NoHeaderError: if verbose != "quiet": - print_(u"No ID3 header found; skipping...") + print(u"No ID3 header found; skipping...") continue except Exception as err: - print_(text_type(err), file=sys.stderr) + print(str(err), file=sys.stderr) continue for tag in filter(lambda t: t.startswith(("T", "COMM")), id3): @@ -111,7 +108,7 @@ def update(options, filenames): frame.encoding = 1 if verbose == "debug": - print_(id3.pprint()) + print(id3.pprint()) if not noupdate: if remove_v1: @@ -154,9 +151,9 @@ def main(argv): for i, arg in enumerate(argv): if arg == "-v1": - argv[i] = fsnative(u"--force-v1") + argv[i] = "--force-v1" elif arg == "-removev1": - argv[i] = fsnative(u"--remove-v1") + argv[i] = "--remove-v1" (options, args) = parser.parse_args(argv[1:]) @@ -168,4 +165,4 @@ def main(argv): def entry_point(): _sig.init() - return main(argv) + return main(sys.argv) diff --git a/libs/common/mutagen/_tools/mid3v2.py b/libs/common/mutagen/_tools/mid3v2.py index 2a79e3b8..b6949b76 100644 --- a/libs/common/mutagen/_tools/mid3v2.py +++ b/libs/common/mutagen/_tools/mid3v2.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2005 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify @@ -8,18 +7,17 @@ """Pretend to be /usr/bin/id3v2 from id3lib, sort of.""" +import os import sys import codecs import mimetypes +import warnings from optparse import SUPPRESS_HELP import mutagen import mutagen.id3 from mutagen.id3 import Encoding, PictureType -from mutagen._senf import fsnative, print_, argv, fsn2text, fsn2bytes, \ - bytes2fsn -from mutagen._compat import PY2, text_type from ._util import split_escape, SignalHandler, OptionParser @@ -57,7 +55,7 @@ Any editing operation will cause the ID3 tag to be upgraded to ID3v2.4. def list_frames(option, opt, value, parser): items = mutagen.id3.Frames.items() for name, frame in sorted(items): - print_(u" --%s %s" % (name, frame.__doc__.split("\n")[0])) + print(u" --%s %s" % (name, frame.__doc__.split("\n")[0])) raise SystemExit @@ -65,13 +63,13 @@ def list_frames_2_2(option, opt, value, parser): items = mutagen.id3.Frames_2_2.items() items.sort() for name, frame in items: - print_(u" --%s %s" % (name, frame.__doc__.split("\n")[0])) + print(u" --%s %s" % (name, frame.__doc__.split("\n")[0])) raise SystemExit def list_genres(option, opt, value, parser): for i, genre in enumerate(mutagen.id3.TCON.GENRES): - print_(u"%3d: %s" % (i, genre)) + print(u"%3d: %s" % (i, genre)) raise SystemExit @@ -79,7 +77,7 @@ def delete_tags(filenames, v1, v2): for filename in filenames: with _sig.block(): if verbose: - print_(u"deleting ID3 tag info in", filename, file=sys.stderr) + print(u"deleting ID3 tag info in", filename, file=sys.stderr) mutagen.id3.delete(filename, v1, v2) @@ -88,22 +86,22 @@ def delete_frames(deletes, filenames): try: deletes = frame_from_fsnative(deletes) except ValueError as err: - print_(text_type(err), file=sys.stderr) + print(str(err), file=sys.stderr) frames = deletes.split(",") for filename in filenames: with _sig.block(): if verbose: - print_(u"deleting %s from" % deletes, filename, - file=sys.stderr) + print("deleting %s from" % deletes, filename, + file=sys.stderr) try: id3 = mutagen.id3.ID3(filename) except mutagen.id3.ID3NoHeaderError: if verbose: - print_(u"No ID3 header found; skipping.", file=sys.stderr) + print(u"No ID3 header found; skipping.", file=sys.stderr) except Exception as err: - print_(text_type(err), file=sys.stderr) + print(str(err), file=sys.stderr) raise SystemExit(1) else: for frame in frames: @@ -116,36 +114,32 @@ def frame_from_fsnative(arg): or raises ValueError. """ - assert isinstance(arg, fsnative) - - text = fsn2text(arg, strict=True) - if PY2: - return text.encode("ascii") - else: - return text.encode("ascii").decode("ascii") + assert isinstance(arg, str) + return arg.encode("ascii").decode("ascii") def value_from_fsnative(arg, escape): - """Takes an item from argv and returns a text_type value without + """Takes an item from argv and returns a str value without surrogate escapes or raises ValueError. """ - assert isinstance(arg, fsnative) + assert isinstance(arg, str) if escape: - bytes_ = fsn2bytes(arg) - if PY2: - bytes_ = bytes_.decode("string_escape") - else: + bytes_ = os.fsencode(arg) + # With py3.7 this has started to warn for invalid escapes, but we + # don't control the input so ignore it. + with warnings.catch_warnings(): + warnings.simplefilter("ignore") bytes_ = codecs.escape_decode(bytes_)[0] - arg = bytes2fsn(bytes_) + arg = os.fsdecode(bytes_) - text = fsn2text(arg, strict=True) + text = arg.encode("utf-8").decode("utf-8") return text def error(*args): - print_(*args, file=sys.stderr) + print(*args, file=sys.stderr) raise SystemExit(1) @@ -167,7 +161,7 @@ def write_files(edits, filenames, escape): try: frame = frame_from_fsnative(frame) except ValueError as err: - print_(text_type(err), file=sys.stderr) + print(str(err), file=sys.stderr) assert isinstance(frame, str) @@ -177,9 +171,9 @@ def write_files(edits, filenames, escape): try: value = value_from_fsnative(value, escape) except ValueError as err: - error(u"%s: %s" % (frame, text_type(err))) + error(u"%s: %s" % (frame, str(err))) - assert isinstance(value, text_type) + assert isinstance(value, str) encoded_edits.append((frame, value)) edits = encoded_edits @@ -205,16 +199,16 @@ def write_files(edits, filenames, escape): for filename in filenames: with _sig.block(): if verbose: - print_(u"Writing", filename, file=sys.stderr) + print(u"Writing", filename, file=sys.stderr) try: id3 = mutagen.id3.ID3(filename) except mutagen.id3.ID3NoHeaderError: if verbose: - print_(u"No ID3 header found; creating a new tag", + print(u"No ID3 header found; creating a new tag", file=sys.stderr) id3 = mutagen.id3.ID3() except Exception as err: - print_(str(err), file=sys.stderr) + print(str(err), file=sys.stderr) continue for (frame, vlist) in edits.items(): if frame == "POPM": @@ -264,7 +258,7 @@ def write_files(edits, filenames, escape): with open(fn, "rb") as h: data = h.read() except IOError as e: - error(text_type(e)) + error(str(e)) frame = mutagen.id3.APIC(encoding=encoding, mime=mime, desc=desc, type=picture_type, data=data) @@ -338,31 +332,31 @@ def write_files(edits, filenames, escape): def list_tags(filenames): for filename in filenames: - print_("IDv2 tag info for", filename) + print("IDv2 tag info for", filename) try: id3 = mutagen.id3.ID3(filename, translate=False) except mutagen.id3.ID3NoHeaderError: - print_(u"No ID3 header found; skipping.") + print(u"No ID3 header found; skipping.") except Exception as err: - print_(text_type(err), file=sys.stderr) + print(str(err), file=sys.stderr) raise SystemExit(1) else: - print_(id3.pprint()) + print(id3.pprint()) def list_tags_raw(filenames): for filename in filenames: - print_("Raw IDv2 tag info for", filename) + print("Raw IDv2 tag info for", filename) try: id3 = mutagen.id3.ID3(filename, translate=False) except mutagen.id3.ID3NoHeaderError: - print_(u"No ID3 header found; skipping.") + print(u"No ID3 header found; skipping.") except Exception as err: - print_(text_type(err), file=sys.stderr) + print(str(err), file=sys.stderr) raise SystemExit(1) else: for frame in id3.values(): - print_(text_type(repr(frame))) + print(str(repr(frame))) def main(argv): @@ -411,43 +405,43 @@ def main(argv): parser.add_option( "-a", "--artist", metavar='"ARTIST"', action="callback", help="Set the artist information", type="string", - callback=lambda *args: args[3].edits.append((fsnative(u"--TPE1"), + callback=lambda *args: args[3].edits.append(("--TPE1", args[2]))) parser.add_option( "-A", "--album", metavar='"ALBUM"', action="callback", help="Set the album title information", type="string", - callback=lambda *args: args[3].edits.append((fsnative(u"--TALB"), + callback=lambda *args: args[3].edits.append(("--TALB", args[2]))) parser.add_option( "-t", "--song", metavar='"SONG"', action="callback", help="Set the song title information", type="string", - callback=lambda *args: args[3].edits.append((fsnative(u"--TIT2"), + callback=lambda *args: args[3].edits.append(("--TIT2", args[2]))) parser.add_option( "-c", "--comment", metavar='"DESCRIPTION":"COMMENT":"LANGUAGE"', action="callback", help="Set the comment information", type="string", - callback=lambda *args: args[3].edits.append((fsnative(u"--COMM"), + callback=lambda *args: args[3].edits.append(("--COMM", args[2]))) parser.add_option( "-p", "--picture", metavar='"FILENAME":"DESCRIPTION":"IMAGE-TYPE":"MIME-TYPE"', action="callback", help="Set the picture", type="string", - callback=lambda *args: args[3].edits.append((fsnative(u"--APIC"), + callback=lambda *args: args[3].edits.append(("--APIC", args[2]))) parser.add_option( "-g", "--genre", metavar='"GENRE"', action="callback", help="Set the genre or genre number", type="string", - callback=lambda *args: args[3].edits.append((fsnative(u"--TCON"), + callback=lambda *args: args[3].edits.append(("--TCON", args[2]))) parser.add_option( "-y", "--year", "--date", metavar='YYYY[-MM-DD]', action="callback", help="Set the year/date", type="string", - callback=lambda *args: args[3].edits.append((fsnative(u"--TDRC"), + callback=lambda *args: args[3].edits.append(("--TDRC", args[2]))) parser.add_option( "-T", "--track", metavar='"num/num"', action="callback", help="Set the track number/(optional) total tracks", type="string", - callback=lambda *args: args[3].edits.append((fsnative(u"--TRCK"), + callback=lambda *args: args[3].edits.append(("--TRCK", args[2]))) for key, frame in mutagen.id3.Frames.items(): @@ -487,4 +481,4 @@ def main(argv): def entry_point(): _sig.init() - return main(argv) + return main(sys.argv) diff --git a/libs/common/mutagen/_tools/moggsplit.py b/libs/common/mutagen/_tools/moggsplit.py index 710f0dfe..de382060 100644 --- a/libs/common/mutagen/_tools/moggsplit.py +++ b/libs/common/mutagen/_tools/moggsplit.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify @@ -9,9 +8,9 @@ """Split a multiplex/chained Ogg file into its component parts.""" import os +import sys import mutagen.ogg -from mutagen._senf import argv from ._util import SignalHandler, OptionParser @@ -72,4 +71,4 @@ def main(argv): def entry_point(): _sig.init() - return main(argv) + return main(sys.argv) diff --git a/libs/common/mutagen/_tools/mutagen_inspect.py b/libs/common/mutagen/_tools/mutagen_inspect.py index 6bd6c614..fac529a9 100644 --- a/libs/common/mutagen/_tools/mutagen_inspect.py +++ b/libs/common/mutagen/_tools/mutagen_inspect.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2005 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify @@ -8,8 +7,7 @@ """Full tag list for any given file.""" -from mutagen._senf import print_, argv -from mutagen._compat import text_type +import sys from ._util import SignalHandler, OptionParser @@ -20,7 +18,7 @@ _sig = SignalHandler() def main(argv): from mutagen import File - parser = OptionParser() + parser = OptionParser(usage="usage: %prog [options] FILE [FILE...]") parser.add_option("--no-flac", help="Compatibility; does nothing.") parser.add_option("--no-mp3", help="Compatibility; does nothing.") parser.add_option("--no-apev2", help="Compatibility; does nothing.") @@ -30,16 +28,16 @@ def main(argv): raise SystemExit(parser.print_help() or 1) for filename in args: - print_(u"--", filename) + print(u"--", filename) try: - print_(u"-", File(filename).pprint()) + print(u"-", File(filename).pprint()) except AttributeError: - print_(u"- Unknown file type") + print(u"- Unknown file type") except Exception as err: - print_(text_type(err)) - print_(u"") + print(str(err)) + print(u"") def entry_point(): _sig.init() - return main(argv) + return main(sys.argv) diff --git a/libs/common/mutagen/_tools/mutagen_pony.py b/libs/common/mutagen/_tools/mutagen_pony.py index e4a496c7..69b29a67 100644 --- a/libs/common/mutagen/_tools/mutagen_pony.py +++ b/libs/common/mutagen/_tools/mutagen_pony.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2005 Joe Wreschnig, Michael Urman # # This program is free software; you can redistribute it and/or modify @@ -10,8 +9,6 @@ import os import sys import traceback -from mutagen._senf import print_, argv - from ._util import SignalHandler @@ -83,7 +80,7 @@ def check_dir(path): from mutagen.mp3 import MP3 rep = Report(path) - print_(u"Scanning", path) + print(u"Scanning", path) for path, dirs, files in os.walk(path): files.sort() for fn in files: @@ -100,12 +97,12 @@ def check_dir(path): else: rep.success(mp3.tags) - print_(str(rep)) + print(str(rep)) def main(argv): if len(argv) == 1: - print_(u"Usage:", argv[0], u"directory ...") + print(u"Usage:", argv[0], u"directory ...") else: for path in argv[1:]: check_dir(path) @@ -113,4 +110,4 @@ def main(argv): def entry_point(): SignalHandler().init() - return main(argv) + return main(sys.argv) diff --git a/libs/common/mutagen/_util.py b/libs/common/mutagen/_util.py index 1332f9d3..8975380e 100644 --- a/libs/common/mutagen/_util.py +++ b/libs/common/mutagen/_util.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify @@ -19,20 +18,36 @@ import errno import decimal from io import BytesIO -try: - import mmap -except ImportError: - # Google App Engine has no mmap: - # https://github.com/quodlibet/mutagen/issues/286 - mmap = None - from collections import namedtuple from contextlib import contextmanager from functools import wraps from fnmatch import fnmatchcase -from ._compat import chr_, PY2, iteritems, iterbytes, integer_types, xrange, \ - izip, text_type, reraise + +_DEFAULT_BUFFER_SIZE = 2 ** 20 + + +def endswith(text, end): + # usefull for paths which can be both, str and bytes + if isinstance(text, str): + if not isinstance(end, str): + end = end.decode("ascii") + else: + if not isinstance(end, bytes): + end = end.encode("ascii") + return text.endswith(end) + + +def reraise(tp, value, tb): + raise tp(value).with_traceback(tb) + + +def bchr(x): + return bytes([x]) + + +def iterbytes(b): + return (bytes([v]) for v in b) def intround(value): @@ -50,7 +65,7 @@ def is_fileobj(fileobj): file object """ - return not (isinstance(fileobj, (text_type, bytes)) or + return not (isinstance(fileobj, (str, bytes)) or hasattr(fileobj, "__fspath__")) @@ -105,8 +120,8 @@ def fileobj_name(fileobj): """ value = getattr(fileobj, "name", u"") - if not isinstance(value, (text_type, bytes)): - value = text_type(value) + if not isinstance(value, (str, bytes)): + value = str(value) return value @@ -212,7 +227,7 @@ def _openfile(instance, filething, filename, fileobj, writable, create): fileobj = filething elif hasattr(filething, "__fspath__"): filename = filething.__fspath__() - if not isinstance(filename, (bytes, text_type)): + if not isinstance(filename, (bytes, str)): raise TypeError("expected __fspath__() to return a filename") else: filename = filething @@ -302,9 +317,6 @@ def hashable(cls): Needs a working __eq__ and __hash__ and will add a __ne__. """ - # py2 - assert "__hash__" in cls.__dict__ - # py3 assert cls.__dict__["__hash__"] is not None assert "__eq__" in cls.__dict__ @@ -340,8 +352,8 @@ def enum(cls): new_type.__module__ = cls.__module__ map_ = {} - for key, value in iteritems(d): - if key.upper() == key and isinstance(value, integer_types): + for key, value in d.items(): + if key.upper() == key and isinstance(value, int): value_instance = new_type(value) setattr(new_type, key, value_instance) map_[value] = key @@ -389,8 +401,8 @@ def flags(cls): new_type.__module__ = cls.__module__ map_ = {} - for key, value in iteritems(d): - if key.upper() == key and isinstance(value, integer_types): + for key, value in d.items(): + if key.upper() == key and isinstance(value, int): value_instance = new_type(value) setattr(new_type, key, value_instance) map_[value] = key @@ -403,7 +415,7 @@ def flags(cls): matches.append("%s.%s" % (type(self).__name__, v)) value &= ~k if value != 0 or not matches: - matches.append(text_type(value)) + matches.append(str(value)) return " | ".join(matches) @@ -443,25 +455,13 @@ class DictMixin(object): else: return True - if PY2: - has_key = __has_key - __contains__ = __has_key - if PY2: - iterkeys = lambda self: iter(self.keys()) - def values(self): return [self[k] for k in self.keys()] - if PY2: - itervalues = lambda self: iter(self.values()) - def items(self): - return list(izip(self.keys(), self.values())) - - if PY2: - iteritems = lambda s: iter(s.items()) + return list(zip(self.keys(), self.values())) def clear(self): for key in list(self.keys()): @@ -591,7 +591,7 @@ def _fill_cdata(cls): funcs["to_%s%s%s" % (prefix, name, esuffix)] = pack funcs["to_%sint%s%s" % (prefix, bits, esuffix)] = pack - for key, func in iteritems(funcs): + for key, func in funcs.items(): setattr(cls, key, staticmethod(func)) @@ -602,12 +602,11 @@ class cdata(object): uint32_le(data)/to_uint32_le(num)/uint32_le_from(data, offset=0) """ - from struct import error - error = error + error = struct.error bitswap = b''.join( - chr_(sum(((val >> i) & 1) << (7 - i) for i in xrange(8))) - for val in xrange(256)) + bchr(sum(((val >> i) & 1) << (7 - i) for i in range(8))) + for val in range(256)) test_bit = staticmethod(lambda value, n: bool((value >> n) & 1)) @@ -683,65 +682,7 @@ def seek_end(fileobj, offset): fileobj.seek(-offset, 2) -def mmap_move(fileobj, dest, src, count): - """Mmaps the file object if possible and moves 'count' data - from 'src' to 'dest'. All data has to be inside the file size - (enlarging the file through this function isn't possible) - - Will adjust the file offset. - - Args: - fileobj (fileobj) - dest (int): The destination offset - src (int): The source offset - count (int) The amount of data to move - Raises: - mmap.error: In case move failed - IOError: In case an operation on the fileobj fails - ValueError: In case invalid parameters were given - """ - - assert mmap is not None, "no mmap support" - - if dest < 0 or src < 0 or count < 0: - raise ValueError("Invalid parameters") - - try: - fileno = fileobj.fileno() - except (AttributeError, IOError): - raise mmap.error( - "File object does not expose/support a file descriptor") - - fileobj.seek(0, 2) - filesize = fileobj.tell() - length = max(dest, src) + count - - if length > filesize: - raise ValueError("Not in file size boundary") - - offset = ((min(dest, src) // mmap.ALLOCATIONGRANULARITY) * - mmap.ALLOCATIONGRANULARITY) - assert dest >= offset - assert src >= offset - assert offset % mmap.ALLOCATIONGRANULARITY == 0 - - # Windows doesn't handle empty mappings, add a fast path here instead - if count == 0: - return - - # fast path - if src == dest: - return - - fileobj.flush() - file_map = mmap.mmap(fileno, length - offset, offset=offset) - try: - file_map.move(dest - offset, src - offset, count) - finally: - file_map.close() - - -def resize_file(fobj, diff, BUFFER_SIZE=2 ** 16): +def resize_file(fobj, diff, BUFFER_SIZE=_DEFAULT_BUFFER_SIZE): """Resize a file by `diff`. New space will be filled with zeros. @@ -778,7 +719,7 @@ def resize_file(fobj, diff, BUFFER_SIZE=2 ** 16): raise -def fallback_move(fobj, dest, src, count, BUFFER_SIZE=2 ** 16): +def move_bytes(fobj, dest, src, count, BUFFER_SIZE=_DEFAULT_BUFFER_SIZE): """Moves data around using read()/write(). Args: @@ -821,12 +762,11 @@ def fallback_move(fobj, dest, src, count, BUFFER_SIZE=2 ** 16): fobj.flush() -def insert_bytes(fobj, size, offset, BUFFER_SIZE=2 ** 16): +def insert_bytes(fobj, size, offset, BUFFER_SIZE=_DEFAULT_BUFFER_SIZE): """Insert size bytes of empty space starting at offset. fobj must be an open file object, open rb+ or - equivalent. Mutagen tries to use mmap to resize the file, but - falls back to a significantly slower method if mmap fails. + equivalent. Args: fobj (fileobj) @@ -847,22 +787,14 @@ def insert_bytes(fobj, size, offset, BUFFER_SIZE=2 ** 16): raise ValueError resize_file(fobj, size, BUFFER_SIZE) - - if mmap is not None: - try: - mmap_move(fobj, offset + size, offset, movesize) - except mmap.error: - fallback_move(fobj, offset + size, offset, movesize, BUFFER_SIZE) - else: - fallback_move(fobj, offset + size, offset, movesize, BUFFER_SIZE) + move_bytes(fobj, offset + size, offset, movesize, BUFFER_SIZE) -def delete_bytes(fobj, size, offset, BUFFER_SIZE=2 ** 16): +def delete_bytes(fobj, size, offset, BUFFER_SIZE=_DEFAULT_BUFFER_SIZE): """Delete size bytes of empty space starting at offset. fobj must be an open file object, open rb+ or - equivalent. Mutagen tries to use mmap to resize the file, but - falls back to a significantly slower method if mmap fails. + equivalent. Args: fobj (fileobj) @@ -882,14 +814,7 @@ def delete_bytes(fobj, size, offset, BUFFER_SIZE=2 ** 16): if movesize < 0: raise ValueError - if mmap is not None: - try: - mmap_move(fobj, offset, offset + size, movesize) - except mmap.error: - fallback_move(fobj, offset, offset + size, movesize, BUFFER_SIZE) - else: - fallback_move(fobj, offset, offset + size, movesize, BUFFER_SIZE) - + move_bytes(fobj, offset, offset + size, movesize, BUFFER_SIZE) resize_file(fobj, -size, BUFFER_SIZE) @@ -933,7 +858,7 @@ def dict_match(d, key, default=None): if key in d and "[" not in key: return d[key] else: - for pattern, value in iteritems(d): + for pattern, value in d.items(): if fnmatchcase(key, pattern): return value return default @@ -1075,7 +1000,7 @@ class BitReader(object): raise BitReaderError("not enough data") return data - return bytes(bytearray(self.bits(8) for _ in xrange(count))) + return bytes(bytearray(self.bits(8) for _ in range(count))) def skip(self, count): """Skip `count` bits. diff --git a/libs/common/mutagen/_vorbis.py b/libs/common/mutagen/_vorbis.py index f8b0ee7a..3fd4718c 100644 --- a/libs/common/mutagen/_vorbis.py +++ b/libs/common/mutagen/_vorbis.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2005-2006 Joe Wreschnig # 2013 Christoph Reiter # @@ -17,10 +16,10 @@ The specification is at http://www.xiph.org/vorbis/doc/v-comment.html. """ import sys +from io import BytesIO import mutagen -from ._compat import reraise, BytesIO, text_type, xrange, PY3, PY2 -from mutagen._util import DictMixin, cdata, MutagenError +from mutagen._util import DictMixin, cdata, MutagenError, reraise def is_valid_key(key): @@ -32,7 +31,7 @@ def is_valid_key(key): Takes str/unicode in Python 2, unicode in Python 3 """ - if PY3 and isinstance(key, bytes): + if isinstance(key, bytes): raise TypeError("needs to be str not bytes") for c in key: @@ -104,7 +103,7 @@ class VComment(mutagen.Tags, list): vendor_length = cdata.uint_le(fileobj.read(4)) self.vendor = fileobj.read(vendor_length).decode('utf-8', errors) count = cdata.uint_le(fileobj.read(4)) - for i in xrange(count): + for i in range(count): length = cdata.uint_le(fileobj.read(4)) try: string = fileobj.read(length).decode('utf-8', errors) @@ -124,9 +123,7 @@ class VComment(mutagen.Tags, list): except UnicodeEncodeError: raise VorbisEncodingError("invalid tag name %r" % tag) else: - # string keys in py3k - if PY3: - tag = tag.decode("ascii") + tag = tag.decode("ascii") if is_valid_key(tag): self.append((tag, value)) @@ -145,30 +142,19 @@ class VComment(mutagen.Tags, list): In Python 3 all keys and values have to be a string. """ - if not isinstance(self.vendor, text_type): - if PY3: - raise ValueError("vendor needs to be str") - - try: - self.vendor.decode('utf-8') - except UnicodeDecodeError: - raise ValueError + if not isinstance(self.vendor, str): + raise ValueError("vendor needs to be str") for key, value in self: try: if not is_valid_key(key): - raise ValueError + raise ValueError("%r is not a valid key" % key) except TypeError: raise ValueError("%r is not a valid key" % key) - if not isinstance(value, text_type): - if PY3: - raise ValueError("%r needs to be str" % key) - - try: - value.decode("utf-8") - except Exception: - raise ValueError("%r is not a valid value" % value) + if not isinstance(value, str): + err = "%r needs to be str for key %r" % (value, key) + raise ValueError(err) return True @@ -213,7 +199,7 @@ class VComment(mutagen.Tags, list): def pprint(self): def _decode(value): - if not isinstance(value, text_type): + if not isinstance(value, str): return value.decode('utf-8', 'replace') return value @@ -221,7 +207,7 @@ class VComment(mutagen.Tags, list): return u"\n".join(tags) -class VCommentDict(VComment, DictMixin): +class VCommentDict(VComment, DictMixin): # type: ignore """A VComment that looks like a dictionary. This object differs from a dictionary in two ways. First, @@ -242,7 +228,6 @@ class VCommentDict(VComment, DictMixin): work. """ - # PY3 only if isinstance(key, slice): return VComment.__getitem__(self, key) @@ -260,7 +245,6 @@ class VCommentDict(VComment, DictMixin): def __delitem__(self, key): """Delete all values associated with the key.""" - # PY3 only if isinstance(key, slice): return VComment.__delitem__(self, key) @@ -296,7 +280,6 @@ class VCommentDict(VComment, DictMixin): string. """ - # PY3 only if isinstance(key, slice): return VComment.__setitem__(self, key, values) @@ -310,9 +293,6 @@ class VCommentDict(VComment, DictMixin): except KeyError: pass - if PY2: - key = key.encode('ascii') - for value in values: self.append((key, value)) diff --git a/libs/common/mutagen/aac.py b/libs/common/mutagen/aac.py index fa6f7064..274b20df 100644 --- a/libs/common/mutagen/aac.py +++ b/libs/common/mutagen/aac.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2014 Christoph Reiter # # This program is free software; you can redistribute it and/or modify @@ -15,9 +14,8 @@ from mutagen import StreamInfo from mutagen._file import FileType from mutagen._util import BitReader, BitReaderError, MutagenError, loadfile, \ - convert_error + convert_error, endswith from mutagen.id3._util import BitPaddedInt -from mutagen._compat import endswith, xrange _FREQS = [ @@ -243,7 +241,7 @@ class ProgramConfigElement(object): elms = num_front_channel_elements + num_side_channel_elements + \ num_back_channel_elements channels = 0 - for i in xrange(elms): + for i in range(elms): channels += 1 element_is_cpe = r.bits(1) if element_is_cpe: @@ -323,7 +321,7 @@ class AACInfo(StreamInfo): self.channels = pce.channels # other pces.. - for i in xrange(npce): + for i in range(npce): ProgramConfigElement(r) r.align() except BitReaderError as e: @@ -347,7 +345,7 @@ class AACInfo(StreamInfo): # Try up to X times to find a sync word and read up to Y frames. # If more than Z frames are valid we assume a valid stream offset = start_offset - for i in xrange(max_sync_tries): + for i in range(max_sync_tries): fileobj.seek(offset) s = _ADTSStream.find_stream(fileobj, max_initial_read) if s is None: @@ -355,7 +353,7 @@ class AACInfo(StreamInfo): # start right after the last found offset offset += s.offset + 1 - for i in xrange(frames_max): + for i in range(frames_max): if not s.parse_frame(): break if not s.sync(max_resync_read): @@ -375,7 +373,10 @@ class AACInfo(StreamInfo): fileobj.seek(0, 2) stream_size = fileobj.tell() - (offset + s.offset) # approx - self.length = float(s.samples * stream_size) / (s.size * s.frequency) + self.length = 0.0 + if s.frequency != 0: + self.length = \ + float(s.samples * stream_size) / (s.size * s.frequency) def pprint(self): return u"AAC (%s), %d Hz, %.2f seconds, %d channel(s), %d bps" % ( diff --git a/libs/common/mutagen/ac3.py b/libs/common/mutagen/ac3.py new file mode 100644 index 00000000..34558734 --- /dev/null +++ b/libs/common/mutagen/ac3.py @@ -0,0 +1,329 @@ +# Copyright (C) 2019 Philipp Wolfer +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + + +"""Pure AC3 file information. +""" + +__all__ = ["AC3", "Open"] + +from mutagen import StreamInfo +from mutagen._file import FileType +from mutagen._util import ( + BitReader, + BitReaderError, + MutagenError, + convert_error, + enum, + loadfile, + endswith, +) + + +@enum +class ChannelMode(object): + DUALMONO = 0 + MONO = 1 + STEREO = 2 + C3F = 3 + C2F1R = 4 + C3F1R = 5 + C2F2R = 6 + C3F2R = 7 + + +AC3_CHANNELS = { + ChannelMode.DUALMONO: 2, + ChannelMode.MONO: 1, + ChannelMode.STEREO: 2, + ChannelMode.C3F: 3, + ChannelMode.C2F1R: 3, + ChannelMode.C3F1R: 4, + ChannelMode.C2F2R: 4, + ChannelMode.C3F2R: 5 +} + +AC3_HEADER_SIZE = 7 + +AC3_SAMPLE_RATES = [48000, 44100, 32000] + +AC3_BITRATES = [ + 32, 40, 48, 56, 64, 80, 96, 112, 128, + 160, 192, 224, 256, 320, 384, 448, 512, 576, 640 +] + + +@enum +class EAC3FrameType(object): + INDEPENDENT = 0 + DEPENDENT = 1 + AC3_CONVERT = 2 + RESERVED = 3 + + +EAC3_BLOCKS = [1, 2, 3, 6] + + +class AC3Error(MutagenError): + pass + + +class AC3Info(StreamInfo): + + """AC3 stream information. + The length of the stream is just a guess and might not be correct. + + Attributes: + channels (`int`): number of audio channels + length (`float`): file length in seconds, as a float + sample_rate (`int`): audio sampling rate in Hz + bitrate (`int`): audio bitrate, in bits per second + codec (`str`): ac-3 or ec-3 (Enhanced AC-3) + """ + + channels = 0 + length = 0 + sample_rate = 0 + bitrate = 0 + codec = 'ac-3' + + @convert_error(IOError, AC3Error) + def __init__(self, fileobj): + """Raises AC3Error""" + header = bytearray(fileobj.read(6)) + + if len(header) < 6: + raise AC3Error("not enough data") + + if not header.startswith(b"\x0b\x77"): + raise AC3Error("not a AC3 file") + + bitstream_id = header[5] >> 3 + if bitstream_id > 16: + raise AC3Error("invalid bitstream_id %i" % bitstream_id) + + fileobj.seek(2) + self._read_header(fileobj, bitstream_id) + + def _read_header(self, fileobj, bitstream_id): + bitreader = BitReader(fileobj) + try: + # This is partially based on code from + # https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/ac3_parser.c + if bitstream_id <= 10: # Normal AC-3 + self._read_header_normal(bitreader, bitstream_id) + else: # Enhanced AC-3 + self._read_header_enhanced(bitreader) + except BitReaderError as e: + raise AC3Error(e) + + self.length = self._guess_length(fileobj) + + def _read_header_normal(self, bitreader, bitstream_id): + r = bitreader + r.skip(16) # 16 bit CRC + sr_code = r.bits(2) + if sr_code == 3: + raise AC3Error("invalid sample rate code %i" % sr_code) + + frame_size_code = r.bits(6) + if frame_size_code > 37: + raise AC3Error("invalid frame size code %i" % frame_size_code) + + r.skip(5) # bitstream ID, already read + r.skip(3) # bitstream mode, not needed + channel_mode = ChannelMode(r.bits(3)) + r.skip(2) # dolby surround mode or surround mix level + lfe_on = r.bits(1) + + sr_shift = max(bitstream_id, 8) - 8 + try: + self.sample_rate = AC3_SAMPLE_RATES[sr_code] >> sr_shift + self.bitrate = (AC3_BITRATES[frame_size_code >> 1] * 1000 + ) >> sr_shift + except KeyError as e: + raise AC3Error(e) + self.channels = self._get_channels(channel_mode, lfe_on) + self._skip_unused_header_bits_normal(r, channel_mode) + + def _read_header_enhanced(self, bitreader): + r = bitreader + self.codec = "ec-3" + frame_type = r.bits(2) + if frame_type == EAC3FrameType.RESERVED: + raise AC3Error("invalid frame type %i" % frame_type) + + r.skip(3) # substream ID, not needed + + frame_size = (r.bits(11) + 1) << 1 + if frame_size < AC3_HEADER_SIZE: + raise AC3Error("invalid frame size %i" % frame_size) + + sr_code = r.bits(2) + try: + if sr_code == 3: + sr_code2 = r.bits(2) + if sr_code2 == 3: + raise AC3Error("invalid sample rate code %i" % sr_code2) + + numblocks_code = 3 + self.sample_rate = AC3_SAMPLE_RATES[sr_code2] // 2 + else: + numblocks_code = r.bits(2) + self.sample_rate = AC3_SAMPLE_RATES[sr_code] + + channel_mode = ChannelMode(r.bits(3)) + lfe_on = r.bits(1) + self.bitrate = 8 * frame_size * self.sample_rate // ( + EAC3_BLOCKS[numblocks_code] * 256) + except KeyError as e: + raise AC3Error(e) + r.skip(5) # bitstream ID, already read + self.channels = self._get_channels(channel_mode, lfe_on) + self._skip_unused_header_bits_enhanced( + r, frame_type, channel_mode, sr_code, numblocks_code) + + @staticmethod + def _skip_unused_header_bits_normal(bitreader, channel_mode): + r = bitreader + r.skip(5) # Dialogue Normalization + if r.bits(1): # Compression Gain Word Exists + r.skip(8) # Compression Gain Word + if r.bits(1): # Language Code Exists + r.skip(8) # Language Code + if r.bits(1): # Audio Production Information Exists + # Mixing Level, 5 Bits + # Room Type, 2 Bits + r.skip(7) + if channel_mode == ChannelMode.DUALMONO: + r.skip(5) # Dialogue Normalization, ch2 + if r.bits(1): # Compression Gain Word Exists, ch2 + r.skip(8) # Compression Gain Word, ch2 + if r.bits(1): # Language Code Exists, ch2 + r.skip(8) # Language Code, ch2 + if r.bits(1): # Audio Production Information Exists, ch2 + # Mixing Level, ch2, 5 Bits + # Room Type, ch2, 2 Bits + r.skip(7) + # Copyright Bit, 1 Bit + # Original Bit Stream, 1 Bit + r.skip(2) + timecod1e = r.bits(1) # Time Code First Halve Exists + timecod2e = r.bits(1) # Time Code Second Halve Exists + if timecod1e: + r.skip(14) # Time Code First Half + if timecod2e: + r.skip(14) # Time Code Second Half + if r.bits(1): # Additional Bit Stream Information Exists + addbsil = r.bit(6) # Additional Bit Stream Information Length + r.skip((addbsil + 1) * 8) + + @staticmethod + def _skip_unused_header_bits_enhanced(bitreader, frame_type, channel_mode, + sr_code, numblocks_code): + r = bitreader + r.skip(5) # Dialogue Normalization + if r.bits(1): # Compression Gain Word Exists + r.skip(8) # Compression Gain Word + if channel_mode == ChannelMode.DUALMONO: + r.skip(5) # Dialogue Normalization, ch2 + if r.bits(1): # Compression Gain Word Exists, ch2 + r.skip(8) # Compression Gain Word, ch2 + if frame_type == EAC3FrameType.DEPENDENT: + if r.bits(1): # chanmap exists + r.skip(16) # chanmap + if r.bits(1): # mixmdate, 1 Bit + # FIXME: Handle channel dependent fields + return + if r.bits(1): # Informational Metadata Exists + # bsmod, 3 Bits + # Copyright Bit, 1 Bit + # Original Bit Stream, 1 Bit + r.skip(5) + if channel_mode == ChannelMode.STEREO: + # dsurmod. 2 Bits + # dheadphonmod, 2 Bits + r.skip(4) + elif channel_mode >= ChannelMode.C2F2R: + r.skip(2) # dsurexmod + if r.bits(1): # Audio Production Information Exists + # Mixing Level, 5 Bits + # Room Type, 2 Bits + # adconvtyp, 1 Bit + r.skip(8) + if channel_mode == ChannelMode.DUALMONO: + if r.bits(1): # Audio Production Information Exists, ch2 + # Mixing Level, ch2, 5 Bits + # Room Type, ch2, 2 Bits + # adconvtyp, ch2, 1 Bit + r.skip(8) + if sr_code < 3: # if not half sample rate + r.skip(1) # sourcefscod + if frame_type == EAC3FrameType.INDEPENDENT and numblocks_code == 3: + r.skip(1) # convsync + if frame_type == EAC3FrameType.AC3_CONVERT: + if numblocks_code != 3: + if r.bits(1): # blkid + r.skip(6) # frmsizecod + if r.bits(1): # Additional Bit Stream Information Exists + addbsil = r.bit(6) # Additional Bit Stream Information Length + r.skip((addbsil + 1) * 8) + + @staticmethod + def _get_channels(channel_mode, lfe_on): + try: + return AC3_CHANNELS[channel_mode] + lfe_on + except KeyError as e: + raise AC3Error(e) + + def _guess_length(self, fileobj): + # use bitrate + data size to guess length + if self.bitrate == 0: + return + start = fileobj.tell() + fileobj.seek(0, 2) + length = fileobj.tell() - start + return 8.0 * length / self.bitrate + + def pprint(self): + return u"%s, %d Hz, %.2f seconds, %d channel(s), %d bps" % ( + self.codec, self.sample_rate, self.length, self.channels, + self.bitrate) + + +class AC3(FileType): + """AC3(filething) + + Arguments: + filething (filething) + + Load AC3 or EAC3 files. + + Tagging is not supported. + Use the ID3/APEv2 classes directly instead. + + Attributes: + info (`AC3Info`) + """ + + _mimes = ["audio/ac3"] + + @loadfile() + def load(self, filething): + self.info = AC3Info(filething.fileobj) + + def add_tags(self): + raise AC3Error("doesn't support tags") + + @staticmethod + def score(filename, fileobj, header): + return header.startswith(b"\x0b\x77") * 2 \ + + (endswith(filename, ".ac3") or endswith(filename, ".eac3")) + + +Open = AC3 +error = AC3Error diff --git a/libs/common/mutagen/aiff.py b/libs/common/mutagen/aiff.py index 66ec3af0..74d2f03d 100644 --- a/libs/common/mutagen/aiff.py +++ b/libs/common/mutagen/aiff.py @@ -1,6 +1,6 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2014 Evan Purkhiser # 2014 Ben Ockmore +# 2019-2020 Philipp Wolfer # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -9,26 +9,30 @@ """AIFF audio stream information and tags.""" -import sys import struct from struct import pack -from ._compat import endswith, text_type, reraise from mutagen import StreamInfo, FileType -from mutagen.id3 import ID3 from mutagen.id3._util import ID3NoHeaderError, error as ID3Error -from mutagen._util import resize_bytes, delete_bytes, MutagenError, loadfile, \ - convert_error +from mutagen._iff import ( + IffChunk, + IffContainerChunkMixin, + IffFile, + IffID3, + InvalidChunk, + error as IffError, +) +from mutagen._util import ( + convert_error, + loadfile, + endswith, +) __all__ = ["AIFF", "Open", "delete"] -class error(MutagenError): - pass - - -class InvalidChunk(error): +class error(IffError): pass @@ -36,22 +40,10 @@ class InvalidChunk(error): _HUGE_VAL = 1.79769313486231e+308 -def is_valid_chunk_id(id): - assert isinstance(id, text_type) +def read_float(data): + """Raises OverflowError""" - return ((len(id) <= 4) and (min(id) >= u' ') and - (max(id) <= u'~')) - - -def assert_valid_chunk_id(id): - - assert isinstance(id, text_type) - - if not is_valid_chunk_id(id): - raise ValueError("AIFF key must be four ASCII characters.") - - -def read_float(data): # 10 bytes + assert len(data) == 10 expon, himant, lomant = struct.unpack('>hLL', data) sign = 1 if expon < 0: @@ -60,156 +52,70 @@ def read_float(data): # 10 bytes if expon == himant == lomant == 0: f = 0.0 elif expon == 0x7FFF: - f = _HUGE_VAL + raise OverflowError("inf and nan not supported") else: expon = expon - 16383 + # this can raise OverflowError too f = (himant * 0x100000000 + lomant) * pow(2.0, expon - 63) return sign * f -class IFFChunk(object): +class AIFFChunk(IffChunk): """Representation of a single IFF chunk""" - # Chunk headers are 8 bytes long (4 for ID and 4 for the size) - HEADER_SIZE = 8 + @classmethod + def parse_header(cls, header): + return struct.unpack('>4sI', header) - def __init__(self, fileobj, parent_chunk=None): - self.__fileobj = fileobj - self.parent_chunk = parent_chunk - self.offset = fileobj.tell() + @classmethod + def get_class(cls, id): + if id == 'FORM': + return AIFFFormChunk + else: + return cls - header = fileobj.read(self.HEADER_SIZE) - if len(header) < self.HEADER_SIZE: - raise InvalidChunk() + def write_new_header(self, id_, size): + self._fileobj.write(pack('>4sI', id_, size)) - self.id, self.data_size = struct.unpack('>4si', header) - - try: - self.id = self.id.decode('ascii') - except UnicodeDecodeError: - raise InvalidChunk() - - if not is_valid_chunk_id(self.id): - raise InvalidChunk() - - self.size = self.HEADER_SIZE + self.data_size - self.data_offset = fileobj.tell() - - def read(self): - """Read the chunks data""" - - self.__fileobj.seek(self.data_offset) - return self.__fileobj.read(self.data_size) - - def write(self, data): - """Write the chunk data""" - - if len(data) > self.data_size: - raise ValueError - - self.__fileobj.seek(self.data_offset) - self.__fileobj.write(data) - - def delete(self): - """Removes the chunk from the file""" - - delete_bytes(self.__fileobj, self.size, self.offset) - if self.parent_chunk is not None: - self.parent_chunk._update_size( - self.parent_chunk.data_size - self.size) - - def _update_size(self, data_size): - """Update the size of the chunk""" - - self.__fileobj.seek(self.offset + 4) - self.__fileobj.write(pack('>I', data_size)) - if self.parent_chunk is not None: - size_diff = self.data_size - data_size - self.parent_chunk._update_size( - self.parent_chunk.data_size - size_diff) - self.data_size = data_size - self.size = data_size + self.HEADER_SIZE - - def resize(self, new_data_size): - """Resize the file and update the chunk sizes""" - - resize_bytes( - self.__fileobj, self.data_size, new_data_size, self.data_offset) - self._update_size(new_data_size) + def write_size(self): + self._fileobj.write(pack('>I', self.data_size)) -class IFFFile(object): - """Representation of a IFF file""" +class AIFFFormChunk(AIFFChunk, IffContainerChunkMixin): + """The AIFF root chunk.""" + + def parse_next_subchunk(self): + return AIFFChunk.parse(self._fileobj, self) + + def __init__(self, fileobj, id, data_size, parent_chunk): + if id != u'FORM': + raise InvalidChunk('Expected FORM chunk, got %s' % id) + + AIFFChunk.__init__(self, fileobj, id, data_size, parent_chunk) + self.init_container() + + +class AIFFFile(IffFile): + """Representation of a AIFF file""" def __init__(self, fileobj): - self.__fileobj = fileobj - self.__chunks = {} - # AIFF Files always start with the FORM chunk which contains a 4 byte # ID before the start of other chunks - fileobj.seek(0) - self.__chunks[u'FORM'] = IFFChunk(fileobj) + super().__init__(AIFFChunk, fileobj) - # Skip past the 4 byte FORM id - fileobj.seek(IFFChunk.HEADER_SIZE + 4) - - # Where the next chunk can be located. We need to keep track of this - # since the size indicated in the FORM header may not match up with the - # offset determined from the size of the last chunk in the file - self.__next_offset = fileobj.tell() - - # Load all of the chunks - while True: - try: - chunk = IFFChunk(fileobj, self[u'FORM']) - except InvalidChunk: - break - self.__chunks[chunk.id.strip()] = chunk - - # Calculate the location of the next chunk, - # considering the pad byte - self.__next_offset = chunk.offset + chunk.size - self.__next_offset += self.__next_offset % 2 - fileobj.seek(self.__next_offset) + if self.root.id != u'FORM': + raise InvalidChunk("Root chunk must be a FORM chunk, got %s" + % self.root.id) def __contains__(self, id_): - """Check if the IFF file contains a specific chunk""" - - assert_valid_chunk_id(id_) - - return id_ in self.__chunks + if id_ == 'FORM': # For backwards compatibility + return True + return super().__contains__(id_) def __getitem__(self, id_): - """Get a chunk from the IFF file""" - - assert_valid_chunk_id(id_) - - try: - return self.__chunks[id_] - except KeyError: - raise KeyError( - "%r has no %r chunk" % (self.__fileobj, id_)) - - def __delitem__(self, id_): - """Remove a chunk from the IFF file""" - - assert_valid_chunk_id(id_) - - self.__chunks.pop(id_).delete() - - def insert_chunk(self, id_): - """Insert a new chunk at the end of the IFF file""" - - assert_valid_chunk_id(id_) - - self.__fileobj.seek(self.__next_offset) - self.__fileobj.write(pack('>4si', id_.ljust(4).encode('ascii'), 0)) - self.__fileobj.seek(self.__next_offset) - chunk = IFFChunk(self.__fileobj, self[u'FORM']) - self[u'FORM']._update_size(self[u'FORM'].data_size + chunk.size) - - self.__chunks[id_] = chunk - self.__next_offset = chunk.offset + chunk.size + if id_ == 'FORM': # For backwards compatibility + return self.root + return super().__getitem__(id_) class AIFFInfo(StreamInfo): @@ -224,7 +130,7 @@ class AIFFInfo(StreamInfo): bitrate (`int`): audio bitrate, in bits per second channels (`int`): The number of audio channels sample_rate (`int`): audio sample rate, in Hz - sample_size (`int`): The audio sample size + bits_per_sample (`int`): The audio sample size """ length = 0 @@ -236,7 +142,7 @@ class AIFFInfo(StreamInfo): def __init__(self, fileobj): """Raises error""" - iff = IFFFile(fileobj) + iff = AIFFFile(fileobj) try: common_chunk = iff[u'COMM'] except KeyError as e: @@ -249,61 +155,30 @@ class AIFFInfo(StreamInfo): info = struct.unpack('>hLh10s', data[:18]) channels, frame_count, sample_size, sample_rate = info - self.sample_rate = int(read_float(sample_rate)) - self.sample_size = sample_size + try: + self.sample_rate = int(read_float(sample_rate)) + except OverflowError: + raise error("Invalid sample rate") + if self.sample_rate < 0: + raise error("Invalid sample rate") + if self.sample_rate != 0: + self.length = frame_count / float(self.sample_rate) + + self.bits_per_sample = sample_size + self.sample_size = sample_size # For backward compatibility self.channels = channels self.bitrate = channels * sample_size * self.sample_rate - self.length = frame_count / float(self.sample_rate) def pprint(self): return u"%d channel AIFF @ %d bps, %s Hz, %.2f seconds" % ( self.channels, self.bitrate, self.sample_rate, self.length) -class _IFFID3(ID3): +class _IFFID3(IffID3): """A AIFF file with ID3v2 tags""" - def _pre_load_header(self, fileobj): - try: - fileobj.seek(IFFFile(fileobj)[u'ID3'].data_offset) - except (InvalidChunk, KeyError): - raise ID3NoHeaderError("No ID3 chunk") - - @convert_error(IOError, error) - @loadfile(writable=True) - def save(self, filething, v2_version=4, v23_sep='/', padding=None): - """Save ID3v2 data to the AIFF file""" - - fileobj = filething.fileobj - - iff_file = IFFFile(fileobj) - - if u'ID3' not in iff_file: - iff_file.insert_chunk(u'ID3') - - chunk = iff_file[u'ID3'] - - try: - data = self._prepare_data( - fileobj, chunk.data_offset, chunk.data_size, v2_version, - v23_sep, padding) - except ID3Error as e: - reraise(error, e, sys.exc_info()[2]) - - new_size = len(data) - new_size += new_size % 2 # pad byte - assert new_size % 2 == 0 - chunk.resize(new_size) - data += (new_size - len(data)) * b'\x00' - assert new_size == len(data) - chunk.write(data) - - @loadfile(writable=True) - def delete(self, filething): - """Completely removes the ID3 chunk from the AIFF file""" - - delete(filething) - self.clear() + def _load_file(self, fileobj): + return AIFFFile(fileobj) @convert_error(IOError, error) @@ -312,7 +187,7 @@ def delete(filething): """Completely removes the ID3 chunk from the AIFF file""" try: - del IFFFile(filething.fileobj)[u'ID3'] + del AIFFFile(filething.fileobj)[u'ID3'] except KeyError: pass diff --git a/libs/common/mutagen/apev2.py b/libs/common/mutagen/apev2.py index 8789d634..a42dc3d1 100644 --- a/libs/common/mutagen/apev2.py +++ b/libs/common/mutagen/apev2.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2005 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify @@ -32,24 +31,17 @@ __all__ = ["APEv2", "APEv2File", "Open", "delete"] import sys import struct -from collections import MutableSequence +from io import BytesIO +from collections.abc import MutableSequence -from ._compat import (cBytesIO, PY3, text_type, PY2, reraise, swap_to_string, - xrange) from mutagen import Metadata, FileType, StreamInfo from mutagen._util import DictMixin, cdata, delete_bytes, total_ordering, \ - MutagenError, loadfile, convert_error, seek_end, get_size + MutagenError, loadfile, convert_error, seek_end, get_size, reraise def is_valid_apev2_key(key): - if not isinstance(key, text_type): - if PY3: - raise TypeError("APEv2 key must be str") - - try: - key = key.decode('ascii') - except UnicodeDecodeError: - return False + if not isinstance(key, str): + raise TypeError("APEv2 key must be str") # PY26 - Change to set literal syntax (since set is faster than list here) return ((2 <= len(key) <= 255) and (min(key) >= u' ') and @@ -61,7 +53,7 @@ def is_valid_apev2_key(key): # 1: Item contains binary information # 2: Item is a locator of external stored information [e.g. URL] # 3: reserved" -TEXT, BINARY, EXTERNAL = xrange(3) +TEXT, BINARY, EXTERNAL = range(3) HAS_HEADER = 1 << 31 HAS_NO_FOOTER = 1 << 30 @@ -301,9 +293,9 @@ class APEv2(_CIDictProxy, Metadata): def __parse_tag(self, tag, count): """Raises IOError and APEBadItemError""" - fileobj = cBytesIO(tag) + fileobj = BytesIO(tag) - for i in xrange(count): + for i in range(count): tag_data = fileobj.read(8) # someone writes wrong item counts if not tag_data: @@ -330,11 +322,10 @@ class APEv2(_CIDictProxy, Metadata): if key[-1:] == b"\x00": key = key[:-1] - if PY3: - try: - key = key.decode("ascii") - except UnicodeError as err: - reraise(APEBadItemError, err, sys.exc_info()[2]) + try: + key = key.decode("ascii") + except UnicodeError as err: + reraise(APEBadItemError, err, sys.exc_info()[2]) value = fileobj.read(size) if len(value) != size: raise APEBadItemError @@ -346,16 +337,12 @@ class APEv2(_CIDictProxy, Metadata): def __getitem__(self, key): if not is_valid_apev2_key(key): raise KeyError("%r is not a valid APEv2 key" % key) - if PY2: - key = key.encode('ascii') return super(APEv2, self).__getitem__(key) def __delitem__(self, key): if not is_valid_apev2_key(key): raise KeyError("%r is not a valid APEv2 key" % key) - if PY2: - key = key.encode('ascii') super(APEv2, self).__delitem__(key) @@ -383,43 +370,28 @@ class APEv2(_CIDictProxy, Metadata): if not is_valid_apev2_key(key): raise KeyError("%r is not a valid APEv2 key" % key) - if PY2: - key = key.encode('ascii') - if not isinstance(value, _APEValue): # let's guess at the content if we're not already a value... - if isinstance(value, text_type): + if isinstance(value, str): # unicode? we've got to be text. value = APEValue(value, TEXT) elif isinstance(value, list): items = [] for v in value: - if not isinstance(v, text_type): - if PY3: - raise TypeError("item in list not str") - v = v.decode("utf-8") + if not isinstance(v, str): + raise TypeError("item in list not str") items.append(v) # list? text. value = APEValue(u"\0".join(items), TEXT) else: - if PY3: - value = APEValue(value, BINARY) - else: - try: - value.decode("utf-8") - except UnicodeError: - # invalid UTF8 text, probably binary - value = APEValue(value, BINARY) - else: - # valid UTF8, probably text - value = APEValue(value, TEXT) + value = APEValue(value, BINARY) super(APEv2, self).__setitem__(key, value) @convert_error(IOError, error) @loadfile(writable=True, create=True) - def save(self, filething): + def save(self, filething=None): """Save changes to a file. If no filename is given, the one most recently loaded is used. @@ -481,7 +453,7 @@ class APEv2(_CIDictProxy, Metadata): @convert_error(IOError, error) @loadfile(writable=True) - def delete(self, filething): + def delete(self, filething=None): """Remove tags from a file.""" fileobj = filething.fileobj @@ -544,7 +516,7 @@ def APEValue(value, kind): class _APEValue(object): - kind = None + kind: int value = None def __init__(self, value, kind=None): @@ -578,7 +550,6 @@ class _APEValue(object): return "%s(%r, %d)" % (type(self).__name__, self.value, self.kind) -@swap_to_string @total_ordering class _APEUtf8Value(_APEValue): @@ -589,11 +560,8 @@ class _APEUtf8Value(_APEValue): reraise(APEBadItemError, e, sys.exc_info()[2]) def _validate(self, value): - if not isinstance(value, text_type): - if PY3: - raise TypeError("value not str") - else: - value = value.decode("utf-8") + if not isinstance(value, str): + raise TypeError("value not str") return value def _write(self): @@ -636,22 +604,16 @@ class APETextValue(_APEUtf8Value, MutableSequence): return self.value.count(u"\0") + 1 def __setitem__(self, index, value): - if not isinstance(value, text_type): - if PY3: - raise TypeError("value not str") - else: - value = value.decode("utf-8") + if not isinstance(value, str): + raise TypeError("value not str") values = list(self) values[index] = value self.value = u"\0".join(values) def insert(self, index, value): - if not isinstance(value, text_type): - if PY3: - raise TypeError("value not str") - else: - value = value.decode("utf-8") + if not isinstance(value, str): + raise TypeError("value not str") values = list(self) values.insert(index, value) @@ -666,7 +628,6 @@ class APETextValue(_APEUtf8Value, MutableSequence): return u" / ".join(self) -@swap_to_string @total_ordering class APEBinaryValue(_APEValue): """An APEv2 binary value.""" diff --git a/libs/common/mutagen/asf/__init__.py b/libs/common/mutagen/asf/__init__.py index 32e1bedb..41e00901 100644 --- a/libs/common/mutagen/asf/__init__.py +++ b/libs/common/mutagen/asf/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2005-2006 Joe Wreschnig # Copyright (C) 2006-2007 Lukas Lalinsky # @@ -13,7 +12,6 @@ __all__ = ["ASF", "Open"] from mutagen import FileType, Tags, StreamInfo from mutagen._util import resize_bytes, DictMixin, loadfile, convert_error -from mutagen._compat import string_types, long_, PY3, izip from ._util import error, ASFError, ASFHeaderError from ._objects import HeaderObject, MetadataLibraryObject, MetadataObject, \ @@ -24,7 +22,7 @@ from ._attrs import ASFGUIDAttribute, ASFWordAttribute, ASFQWordAttribute, \ ASFUnicodeAttribute, ASFBaseAttribute, ASFValue -# pyflakes +# flake8 error, ASFError, ASFHeaderError, ASFValue @@ -75,7 +73,7 @@ class ASFInfo(StreamInfo): return s -class ASFTags(list, DictMixin, Tags): +class ASFTags(list, DictMixin, Tags): # type: ignore """ASFTags() Dictionary containing ASF attributes. @@ -89,7 +87,6 @@ class ASFTags(list, DictMixin, Tags): """ - # PY3 only if isinstance(key, slice): return list.__getitem__(self, key) @@ -102,7 +99,6 @@ class ASFTags(list, DictMixin, Tags): def __delitem__(self, key): """Delete all values associated with the key.""" - # PY3 only if isinstance(key, slice): return list.__delitem__(self, key) @@ -129,7 +125,6 @@ class ASFTags(list, DictMixin, Tags): string. """ - # PY3 only if isinstance(key, slice): return list.__setitem__(self, key, values) @@ -139,16 +134,14 @@ class ASFTags(list, DictMixin, Tags): to_append = [] for value in values: if not isinstance(value, ASFBaseAttribute): - if isinstance(value, string_types): + if isinstance(value, str): value = ASFUnicodeAttribute(value) - elif PY3 and isinstance(value, bytes): + elif isinstance(value, bytes): value = ASFByteArrayAttribute(value) elif isinstance(value, bool): value = ASFBoolAttribute(value) elif isinstance(value, int): value = ASFDWordAttribute(value) - elif isinstance(value, long_): - value = ASFQWordAttribute(value) else: raise TypeError("Invalid type %r" % type(value)) to_append.append((key, value)) @@ -163,7 +156,7 @@ class ASFTags(list, DictMixin, Tags): def keys(self): """Return a sequence of all keys in the comment.""" - return self and set(next(izip(*self))) + return self and set(next(zip(*self))) def as_dict(self): """Return a copy of the comment data in a real dict.""" @@ -252,7 +245,7 @@ class ASF(FileType): @convert_error(IOError, error) @loadfile(writable=True) - def save(self, filething, padding=None): + def save(self, filething=None, padding=None): """save(filething=None, padding=None) Save tag changes back to the loaded file. @@ -319,7 +312,7 @@ class ASF(FileType): raise ASFError @loadfile(writable=True) - def delete(self, filething): + def delete(self, filething=None): """delete(filething=None) Args: diff --git a/libs/common/mutagen/asf/_attrs.py b/libs/common/mutagen/asf/_attrs.py index 8111c1c2..0417d9db 100644 --- a/libs/common/mutagen/asf/_attrs.py +++ b/libs/common/mutagen/asf/_attrs.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2005-2006 Joe Wreschnig # Copyright (C) 2006-2007 Lukas Lalinsky # @@ -9,9 +8,9 @@ import sys import struct +from typing import Dict, Type -from mutagen._compat import swap_to_string, text_type, PY2, reraise -from mutagen._util import total_ordering +from mutagen._util import total_ordering, reraise from ._util import ASFError @@ -19,9 +18,9 @@ from ._util import ASFError class ASFBaseAttribute(object): """Generic attribute.""" - TYPE = None + TYPE: int - _TYPES = {} + _TYPES: "Dict[int, Type[ASFBaseAttribute]]" = {} value = None """The Python value of this attribute (type depends on the class)""" @@ -103,7 +102,6 @@ class ASFBaseAttribute(object): @ASFBaseAttribute._register -@swap_to_string @total_ordering class ASFUnicodeAttribute(ASFBaseAttribute): """Unicode string attribute. @@ -122,11 +120,8 @@ class ASFUnicodeAttribute(ASFBaseAttribute): reraise(ASFError, e, sys.exc_info()[2]) def _validate(self, value): - if not isinstance(value, text_type): - if PY2: - return value.decode("utf-8") - else: - raise TypeError("%r not str" % value) + if not isinstance(value, str): + raise TypeError("%r not str" % value) return value def _render(self): @@ -142,16 +137,15 @@ class ASFUnicodeAttribute(ASFBaseAttribute): return self.value def __eq__(self, other): - return text_type(self) == other + return str(self) == other def __lt__(self, other): - return text_type(self) < other + return str(self) < other __hash__ = ASFBaseAttribute.__hash__ @ASFBaseAttribute._register -@swap_to_string @total_ordering class ASFByteArrayAttribute(ASFBaseAttribute): """Byte array attribute. @@ -194,7 +188,6 @@ class ASFByteArrayAttribute(ASFBaseAttribute): @ASFBaseAttribute._register -@swap_to_string @total_ordering class ASFBoolAttribute(ASFBaseAttribute): """Bool attribute. @@ -228,10 +221,10 @@ class ASFBoolAttribute(ASFBaseAttribute): return bool(self.value) def __bytes__(self): - return text_type(self.value).encode('utf-8') + return str(self.value).encode('utf-8') def __str__(self): - return text_type(self.value) + return str(self.value) def __eq__(self, other): return bool(self.value) == other @@ -243,7 +236,6 @@ class ASFBoolAttribute(ASFBaseAttribute): @ASFBaseAttribute._register -@swap_to_string @total_ordering class ASFDWordAttribute(ASFBaseAttribute): """DWORD attribute. @@ -274,10 +266,10 @@ class ASFDWordAttribute(ASFBaseAttribute): return self.value def __bytes__(self): - return text_type(self.value).encode('utf-8') + return str(self.value).encode('utf-8') def __str__(self): - return text_type(self.value) + return str(self.value) def __eq__(self, other): return int(self.value) == other @@ -289,7 +281,6 @@ class ASFDWordAttribute(ASFBaseAttribute): @ASFBaseAttribute._register -@swap_to_string @total_ordering class ASFQWordAttribute(ASFBaseAttribute): """QWORD attribute. @@ -320,10 +311,10 @@ class ASFQWordAttribute(ASFBaseAttribute): return self.value def __bytes__(self): - return text_type(self.value).encode('utf-8') + return str(self.value).encode('utf-8') def __str__(self): - return text_type(self.value) + return str(self.value) def __eq__(self, other): return int(self.value) == other @@ -335,7 +326,6 @@ class ASFQWordAttribute(ASFBaseAttribute): @ASFBaseAttribute._register -@swap_to_string @total_ordering class ASFWordAttribute(ASFBaseAttribute): """WORD attribute. @@ -366,10 +356,10 @@ class ASFWordAttribute(ASFBaseAttribute): return self.value def __bytes__(self): - return text_type(self.value).encode('utf-8') + return str(self.value).encode('utf-8') def __str__(self): - return text_type(self.value) + return str(self.value) def __eq__(self, other): return int(self.value) == other @@ -381,7 +371,6 @@ class ASFWordAttribute(ASFBaseAttribute): @ASFBaseAttribute._register -@swap_to_string @total_ordering class ASFGUIDAttribute(ASFBaseAttribute): """GUID attribute.""" diff --git a/libs/common/mutagen/asf/_objects.py b/libs/common/mutagen/asf/_objects.py index 1c15d613..df95bace 100644 --- a/libs/common/mutagen/asf/_objects.py +++ b/libs/common/mutagen/asf/_objects.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2005-2006 Joe Wreschnig # Copyright (C) 2006-2007 Lukas Lalinsky # @@ -8,9 +7,9 @@ # (at your option) any later version. import struct +from typing import Dict, Type from mutagen._util import cdata, get_size -from mutagen._compat import text_type, xrange, izip from mutagen._tags import PaddingInfo from ._util import guid2bytes, bytes2guid, CODECS, ASFError, ASFHeaderError @@ -20,8 +19,8 @@ from ._attrs import ASFBaseAttribute, ASFUnicodeAttribute class BaseObject(object): """Base ASF object.""" - GUID = None - _TYPES = {} + GUID: bytes + _TYPES: "Dict[bytes, Type[BaseObject]]" = {} def __init__(self): self.objects = [] @@ -89,7 +88,7 @@ class HeaderObject(BaseObject): remaining_header, num_objects = cls.parse_size(fileobj) remaining_header -= 30 - for i in xrange(num_objects): + for i in range(num_objects): obj_header_size = 24 if remaining_header < obj_header_size: raise ASFHeaderError("invalid header size") @@ -108,13 +107,16 @@ class HeaderObject(BaseObject): try: data = fileobj.read(payload_size) - except OverflowError: + except (OverflowError, MemoryError): # read doesn't take 64bit values raise ASFHeaderError("invalid header size") if len(data) != payload_size: raise ASFHeaderError("truncated") - obj.parse(asf, data) + try: + obj.parse(asf, data) + except struct.error: + raise ASFHeaderError("truncated") header.objects.append(obj) return header @@ -151,7 +153,8 @@ class HeaderObject(BaseObject): # ask the user for padding adjustments file_size = get_size(fileobj) content_size = file_size - available - assert content_size >= 0 + if content_size < 0: + raise ASFHeaderError("truncated content") info = PaddingInfo(available - needed_size, content_size) # add padding @@ -200,7 +203,7 @@ class ContentDescriptionObject(BaseObject): texts.append(None) pos = end - for key, value in izip(self.NAMES, texts): + for key, value in zip(self.NAMES, texts): if value is not None: value = ASFUnicodeAttribute(value=value) asf._tags.setdefault(self.GUID, []).append((key, value)) @@ -209,7 +212,7 @@ class ContentDescriptionObject(BaseObject): def render_text(name): value = asf.to_content_description.get(name) if value is not None: - return text_type(value).encode("utf-16-le") + b"\x00\x00" + return str(value).encode("utf-16-le") + b"\x00\x00" else: return b"" @@ -228,7 +231,7 @@ class ExtendedContentDescriptionObject(BaseObject): super(ExtendedContentDescriptionObject, self).parse(asf, data) num_attributes, = struct.unpack("= 0 asf.info.length = max((length / 10000000.0) - (preroll / 1000.0), 0.0) @@ -319,7 +324,7 @@ class CodecListObject(BaseObject): offset = 16 count, offset = cdata.uint32_le_from(data, offset) - for i in xrange(count): + for i in range(count): try: offset, type_, name, desc, codec = \ self._parse_entry(data, offset) @@ -377,6 +382,8 @@ class HeaderExtensionObject(BaseObject): while datapos < datasize: guid, size = struct.unpack( "<16sQ", data[22 + datapos:22 + datapos + 24]) + if size < 1: + raise ASFHeaderError("invalid size in header extension") obj = BaseObject._get_object(guid) obj.parse(asf, data[22 + datapos + 24:22 + datapos + size]) self.objects.append(obj) @@ -407,7 +414,7 @@ class MetadataObject(BaseObject): super(MetadataObject, self).parse(asf, data) num_attributes, = struct.unpack("4sQ', header) + + @classmethod + def get_class(cls, id): + if id in DSDIFFListChunk.LIST_CHUNK_IDS: + return DSDIFFListChunk + elif id == 'DST': + return DSTChunk + else: + return cls + + def write_new_header(self, id_, size): + self._fileobj.write(struct.pack('>4sQ', id_, size)) + + def write_size(self): + self._fileobj.write(struct.pack('>Q', self.data_size)) + + +class DSDIFFListChunk(DSDIFFChunk, IffContainerChunkMixin): + """A DSDIFF chunk containing other chunks. + """ + + LIST_CHUNK_IDS = ['FRM8', 'PROP'] + + def parse_next_subchunk(self): + return DSDIFFChunk.parse(self._fileobj, self) + + def __init__(self, fileobj, id, data_size, parent_chunk): + if id not in self.LIST_CHUNK_IDS: + raise InvalidChunk('Not a list chunk: %s' % id) + + DSDIFFChunk.__init__(self, fileobj, id, data_size, parent_chunk) + self.init_container() + + +class DSTChunk(DSDIFFChunk, IffContainerChunkMixin): + """A DSDIFF chunk containing other chunks. + """ + + def parse_next_subchunk(self): + return DSDIFFChunk.parse(self._fileobj, self) + + def __init__(self, fileobj, id, data_size, parent_chunk): + if id != 'DST': + raise InvalidChunk('Not a DST chunk: %s' % id) + + DSDIFFChunk.__init__(self, fileobj, id, data_size, parent_chunk) + self.init_container(name_size=0) + + +class DSDIFFFile(IffFile): + """Representation of a DSDIFF file""" + + def __init__(self, fileobj): + super().__init__(DSDIFFChunk, fileobj) + + if self.root.id != u'FRM8': + raise InvalidChunk("Root chunk must be a FRM8 chunk, got %r" + % self.root) + + +class DSDIFFInfo(StreamInfo): + + """DSDIFF stream information. + + Attributes: + channels (`int`): number of audio channels + length (`float`): file length in seconds, as a float + sample_rate (`int`): audio sampling rate in Hz + bits_per_sample (`int`): audio sample size (for DSD this is always 1) + bitrate (`int`): audio bitrate, in bits per second + compression (`str`): DSD (uncompressed) or DST + """ + + channels = 0 + length = 0 + sample_rate = 0 + bits_per_sample = 1 + bitrate = 0 + compression = None + + @convert_error(IOError, error) + def __init__(self, fileobj): + """Raises error""" + + iff = DSDIFFFile(fileobj) + try: + prop_chunk = iff['PROP'] + except KeyError as e: + raise error(str(e)) + + if prop_chunk.name == 'SND ': + for chunk in prop_chunk.subchunks(): + if chunk.id == 'FS' and chunk.data_size == 4: + data = chunk.read() + if len(data) < 4: + raise InvalidChunk("Not enough data in FS chunk") + self.sample_rate, = struct.unpack('>L', data[:4]) + elif chunk.id == 'CHNL' and chunk.data_size >= 2: + data = chunk.read() + if len(data) < 2: + raise InvalidChunk("Not enough data in CHNL chunk") + self.channels, = struct.unpack('>H', data[:2]) + elif chunk.id == 'CMPR' and chunk.data_size >= 4: + data = chunk.read() + if len(data) < 4: + raise InvalidChunk("Not enough data in CMPR chunk") + compression_id, = struct.unpack('>4s', data[:4]) + self.compression = compression_id.decode('ascii').rstrip() + + if self.sample_rate < 0: + raise error("Invalid sample rate") + + if self.compression == 'DSD': # not compressed + try: + dsd_chunk = iff['DSD'] + except KeyError as e: + raise error(str(e)) + + # DSD data has one bit per sample. Eight samples of a channel + # are clustered together for a channel byte. For multiple channels + # the channel bytes are interleaved (in the order specified in the + # CHNL chunk). See DSDIFF spec chapter 3.3. + sample_count = dsd_chunk.data_size * 8 / (self.channels or 1) + + if self.sample_rate != 0: + self.length = sample_count / float(self.sample_rate) + + self.bitrate = (self.channels * self.bits_per_sample + * self.sample_rate) + elif self.compression == 'DST': + try: + dst_frame = iff['DST'] + dst_frame_info = dst_frame['FRTE'] + except KeyError as e: + raise error(str(e)) + + if dst_frame_info.data_size >= 6: + data = dst_frame_info.read() + if len(data) < 6: + raise InvalidChunk("Not enough data in FRTE chunk") + frame_count, frame_rate = struct.unpack('>LH', data[:6]) + if frame_rate: + self.length = frame_count / frame_rate + + if frame_count: + dst_data_size = dst_frame.data_size - dst_frame_info.size + avg_frame_size = dst_data_size / frame_count + self.bitrate = avg_frame_size * 8 * frame_rate + + def pprint(self): + return u"%d channel DSDIFF (%s) @ %d bps, %s Hz, %.2f seconds" % ( + self.channels, self.compression, self.bitrate, self.sample_rate, + self.length) + + +class _DSDIFFID3(IffID3): + """A DSDIFF file with ID3v2 tags""" + + def _load_file(self, fileobj): + return DSDIFFFile(fileobj) + + +@convert_error(IOError, error) +@loadfile(method=False, writable=True) +def delete(filething): + """Completely removes the ID3 chunk from the DSDIFF file""" + + try: + del DSDIFFFile(filething.fileobj)[u'ID3'] + except KeyError: + pass + + +class DSDIFF(FileType): + """DSDIFF(filething) + + An DSDIFF audio file. + + For tagging ID3v2 data is added to a chunk with the ID "ID3 ". + + Arguments: + filething (filething) + + Attributes: + tags (`mutagen.id3.ID3`) + info (`DSDIFFInfo`) + """ + + _mimes = ["audio/x-dff"] + + @convert_error(IOError, error) + @loadfile() + def load(self, filething, **kwargs): + fileobj = filething.fileobj + + try: + self.tags = _DSDIFFID3(fileobj, **kwargs) + except ID3NoHeaderError: + self.tags = None + except ID3Error as e: + raise error(e) + else: + self.tags.filename = self.filename + + fileobj.seek(0, 0) + self.info = DSDIFFInfo(fileobj) + + def add_tags(self): + """Add empty ID3 tags to the file.""" + if self.tags is None: + self.tags = _DSDIFFID3() + else: + raise error("an ID3 tag already exists") + + @staticmethod + def score(filename, fileobj, header): + return header.startswith(b"FRM8") * 2 + endswith(filename, ".dff") + + +Open = DSDIFF diff --git a/libs/common/mutagen/dsf.py b/libs/common/mutagen/dsf.py index ed5faae2..121a01ba 100644 --- a/libs/common/mutagen/dsf.py +++ b/libs/common/mutagen/dsf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2017 Boris Pruessmann # # This program is free software; you can redistribute it and/or modify @@ -11,11 +10,11 @@ import sys import struct - -from ._compat import cBytesIO, reraise, endswith +from io import BytesIO from mutagen import FileType, StreamInfo -from mutagen._util import cdata, MutagenError, loadfile, convert_error +from mutagen._util import cdata, MutagenError, loadfile, \ + convert_error, reraise, endswith from mutagen.id3 import ID3 from mutagen.id3._util import ID3NoHeaderError, error as ID3Error @@ -80,7 +79,7 @@ class DSDChunk(DSFChunk): self.offset_metdata_chunk = cdata.ulonglong_le(data[20:28]) def write(self): - f = cBytesIO() + f = BytesIO() f.write(self.chunk_header) f.write(struct.pack("I", self.min_blocksize)[-2:]) f.write(struct.pack(">I", self.max_blocksize)[-2:]) f.write(struct.pack(">I", self.min_framesize)[-3:]) @@ -244,11 +243,11 @@ class StreamInfo(MetadataBlock, mutagen.StreamInfo): byte = (self.sample_rate & 0xF) << 4 byte += ((self.channels - 1) & 7) << 1 byte += ((self.bits_per_sample - 1) >> 4) & 1 - f.write(chr_(byte)) + f.write(bchr(byte)) # 4 bits of bps, 4 of sample count byte = ((self.bits_per_sample - 1) & 0xF) << 4 byte += (self.total_samples >> 32) & 0xF - f.write(chr_(byte)) + f.write(bchr(byte)) # last 32 of sample count f.write(struct.pack(">I", self.total_samples & 0xFFFFFFFF)) # MD5 signature @@ -284,6 +283,9 @@ class SeekPoint(tuple): return super(SeekPoint, cls).__new__( cls, (first_sample, byte_offset, num_samples)) + def __getnewargs__(self): + return self.first_sample, self.byte_offset, self.num_samples + first_sample = property(lambda self: self[0]) byte_offset = property(lambda self: self[1]) num_samples = property(lambda self: self[2]) @@ -322,7 +324,7 @@ class SeekTable(MetadataBlock): sp = data.tryread(self.__SEEKPOINT_SIZE) def write(self): - f = cBytesIO() + f = BytesIO() for seekpoint in self.seekpoints: packed = struct.pack( self.__SEEKPOINT_FORMAT, @@ -371,7 +373,7 @@ class CueSheetTrackIndex(tuple): """ def __new__(cls, index_number, index_offset): - return super(cls, CueSheetTrackIndex).__new__( + return super(CueSheetTrackIndex, cls).__new__( cls, (index_number, index_offset)) index_number = property(lambda self: self[0]) @@ -394,7 +396,7 @@ class CueSheetTrack(object): isrc (`mutagen.text`): ISRC code, exactly 12 characters type (`int`): 0 for audio, 1 for digital data pre_emphasis (`bool`): true if the track is recorded with pre-emphasis - indexes (List[`mutagen.flac.CueSheetTrackIndex`]): + indexes (list[CueSheetTrackIndex]): list of CueSheetTrackIndex objects """ @@ -442,9 +444,9 @@ class CueSheet(MetadataBlock): lead_in_samples (`int`): number of lead-in samples compact_disc (`bool`): true if the cuesheet corresponds to a compact disc - tracks (List[`mutagen.flac.CueSheetTrack`]): + tracks (list[CueSheetTrack]): list of CueSheetTrack objects - lead_out (`mutagen.flac.CueSheetTrack` or `None`): + lead_out (`CueSheetTrack` or `None`): lead-out as CueSheetTrack or None if lead-out was not found """ @@ -484,7 +486,7 @@ class CueSheet(MetadataBlock): self.lead_in_samples = lead_in_samples self.compact_disc = bool(flags & 0x80) self.tracks = [] - for i in xrange(num_tracks): + for i in range(num_tracks): track = data.read(self.__CUESHEET_TRACK_SIZE) start_offset, track_number, isrc_padded, flags, num_indexes = \ struct.unpack(self.__CUESHEET_TRACK_FORMAT, track) @@ -493,7 +495,7 @@ class CueSheet(MetadataBlock): pre_emphasis = bool(flags & 0x40) val = CueSheetTrack( track_number, start_offset, isrc, type_, pre_emphasis) - for j in xrange(num_indexes): + for j in range(num_indexes): index = data.read(self.__CUESHEET_TRACKINDEX_SIZE) index_offset, index_number = struct.unpack( self.__CUESHEET_TRACKINDEX_FORMAT, index) @@ -502,7 +504,7 @@ class CueSheet(MetadataBlock): self.tracks.append(val) def write(self): - f = cBytesIO() + f = BytesIO() flags = 0 if self.compact_disc: flags |= 0x80 @@ -608,7 +610,7 @@ class Picture(MetadataBlock): self.data = data.read(length) def write(self): - f = cBytesIO() + f = BytesIO() mime = self.mime.encode('UTF-8') f.write(struct.pack('>2I', self.type, len(mime))) f.write(mime) @@ -678,14 +680,13 @@ class FLAC(mutagen.FileType): Attributes: cuesheet (`CueSheet`): if any or `None` seektable (`SeekTable`): if any or `None` - pictures (List[`Picture`]): list of embedded pictures + pictures (list[Picture]): list of embedded pictures info (`StreamInfo`) tags (`mutagen._vorbis.VCommentDict`) """ _mimes = ["audio/flac", "audio/x-flac", "application/x-flac"] - info = None tags = None METADATA_BLOCKS = [StreamInfo, Padding, None, SeekTable, VCFLACDict, @@ -711,7 +712,7 @@ class FLAC(mutagen.FileType): if block_type._distrust_size: # Some jackass is writing broken Metadata block length # for Vorbis comment blocks, and the FLAC reference - # implementaton can parse them (mostly by accident), + # implementation can parse them (mostly by accident), # so we have to too. Instead of parsing the size # given, parse an actual Vorbis comment, leaving # fileobj in the right position. @@ -732,7 +733,9 @@ class FLAC(mutagen.FileType): if self.tags is None: self.tags = block else: - raise FLACVorbisError("> 1 Vorbis comment block found") + # https://github.com/quodlibet/mutagen/issues/377 + # Something writes multiple and metaflac doesn't care + pass elif block.code == CueSheet.code: if self.cuesheet is None: self.cuesheet = block @@ -756,19 +759,21 @@ class FLAC(mutagen.FileType): add_vorbiscomment = add_tags + @convert_error(IOError, error) @loadfile(writable=True) - def delete(self, filething): + def delete(self, filething=None): """Remove Vorbis comments from a file. If no filename is given, the one most recently loaded is used. """ if self.tags is not None: - self.metadata_blocks.remove(self.tags) - try: - self.save(filething, padding=lambda x: 0) - finally: - self.metadata_blocks.append(self.tags) + temp_blocks = [ + b for b in self.metadata_blocks if b.code != VCFLACDict.code] + self._save(filething, temp_blocks, False, padding=lambda x: 0) + self.metadata_blocks[:] = [ + b for b in self.metadata_blocks + if b.code != VCFLACDict.code or b is self.tags] self.tags.clear() vc = property(lambda s: s.tags, doc="Alias for tags; don't use this.") @@ -791,7 +796,7 @@ class FLAC(mutagen.FileType): pass try: - self.metadata_blocks[0].length + self.info.length except (AttributeError, IndexError): raise FLACNoHeaderError("Stream info block not found") @@ -805,7 +810,11 @@ class FLAC(mutagen.FileType): @property def info(self): - return self.metadata_blocks[0] + streaminfo_blocks = [ + block for block in self.metadata_blocks + if block.code == StreamInfo.code + ] + return streaminfo_blocks[0] def add_picture(self, picture): """Add a new picture to the file. @@ -823,16 +832,11 @@ class FLAC(mutagen.FileType): @property def pictures(self): - """ - Returns: - List[`Picture`]: List of embedded pictures - """ - return [b for b in self.metadata_blocks if b.code == Picture.code] @convert_error(IOError, error) @loadfile(writable=True) - def save(self, filething, deleteid3=False, padding=None): + def save(self, filething=None, deleteid3=False, padding=None): """Save metadata blocks to a file. Args: @@ -843,6 +847,9 @@ class FLAC(mutagen.FileType): If no filename is given, the one most recently loaded is used. """ + self._save(filething, self.metadata_blocks, deleteid3, padding) + + def _save(self, filething, metadata_blocks, deleteid3, padding): f = StrictFileObject(filething.fileobj) header = self.__check_header(f, filething.name) audio_offset = self.__find_audio_offset(f) @@ -857,7 +864,7 @@ class FLAC(mutagen.FileType): content_size = get_size(f) - audio_offset assert content_size >= 0 data = MetadataBlock._writeblocks( - self.metadata_blocks, available, content_size, padding) + metadata_blocks, available, content_size, padding) data_size = len(data) resize_bytes(filething.fileobj, available, data_size, header) diff --git a/libs/common/mutagen/id3/__init__.py b/libs/common/mutagen/id3/__init__.py index 9033c76c..b70638bd 100644 --- a/libs/common/mutagen/id3/__init__.py +++ b/libs/common/mutagen/id3/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2005 Michael Urman # 2006 Lukas Lalinsky # 2013 Christoph Reiter @@ -61,7 +60,7 @@ from ._util import ID3EncryptionUnsupportedError, ID3JunkFrameError, \ # support open(filename) as interface Open = ID3 -# pyflakes +# flake8 ID3, ID3FileType, delete, ID3v1SaveOptions, Encoding, PictureType, CTOCFlags, ID3TimeStamp, Frames, Frames_2_2, Frame, TextFrame, UrlFrame, UrlFrameU, TimeStampTextFrame, BinaryFrame, NumericPartTextFrame, NumericTextFrame, diff --git a/libs/common/mutagen/id3/_file.py b/libs/common/mutagen/id3/_file.py index cb8794fc..647db3f3 100644 --- a/libs/common/mutagen/id3/_file.py +++ b/libs/common/mutagen/id3/_file.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2005 Michael Urman # 2006 Lukas Lalinsky # 2013 Christoph Reiter @@ -53,8 +52,8 @@ class ID3(ID3Tags, mutagen.Metadata): filething (filething): or `None` Attributes: - version (Tuple[int]): ID3 tag version as a tuple - unknown_frames (List[bytes]): raw frame data of any unknown frames + version (tuple[int]): ID3 tag version as a tuple + unknown_frames (list[bytes]): raw frame data of any unknown frames found size (int): the total size of the ID3 tag, including the header """ @@ -78,8 +77,6 @@ class ID3(ID3Tags, mutagen.Metadata): @property def version(self): - """`tuple`: ID3 tag version as a tuple (of the loaded file)""" - if self._header is not None: return self._header.version return self._version @@ -112,10 +109,9 @@ class ID3(ID3Tags, mutagen.Metadata): @convert_error(IOError, error) @loadfile() - def load(self, filething, known_frames=None, translate=True, v2_version=4): - """load(filething, known_frames=None, translate=True, v2_version=4) - - Load tags from a filename. + def load(self, filething, known_frames=None, translate=True, v2_version=4, + load_v1=True): + """Load tags from a filename. Args: filename (filething): filename or file object to load tag data from @@ -126,6 +122,11 @@ class ID3(ID3Tags, mutagen.Metadata): call update_to_v23() / update_to_v24() manually. v2_version (int): if update_to_v23 or update_to_v24 get called (3 or 4) + load_v1 (bool): Load tags from ID3v1 header if present. If both + ID3v1 and ID3v2 headers are present, combine the tags from + the two, with ID3v2 having precedence. + + .. versionadded:: 1.42 Example of loading a custom frame:: @@ -149,13 +150,17 @@ class ID3(ID3Tags, mutagen.Metadata): try: self._header = ID3Header(fileobj) except (ID3NoHeaderError, ID3UnsupportedVersionError): - frames, offset = find_id3v1(fileobj) + if not load_v1: + raise + + frames, offset = find_id3v1(fileobj, v2_version, known_frames) if frames is None: raise self.version = ID3Header._V11 for v in frames.values(): - self.add(v) + if len(self.getall(v.HashKey)) == 0: + self.add(v) else: # XXX: attach to the header object so we have it in spec parsing.. if known_frames is not None: @@ -165,6 +170,14 @@ class ID3(ID3Tags, mutagen.Metadata): remaining_data = self._read(self._header, data) self._padding = len(remaining_data) + if load_v1: + v1v2_ver = 4 if self.version[1] == 4 else 3 + frames, offset = find_id3v1(fileobj, v1v2_ver, known_frames) + if frames: + for v in frames.values(): + if len(self.getall(v.HashKey)) == 0: + self.add(v) + if translate: if v2_version == 3: self.update_to_v23() @@ -204,13 +217,14 @@ class ID3(ID3Tags, mutagen.Metadata): @convert_error(IOError, error) @loadfile(writable=True, create=True) - def save(self, filething, v1=1, v2_version=4, v23_sep='/', padding=None): + def save(self, filething=None, v1=1, v2_version=4, v23_sep='/', + padding=None): """save(filething=None, v1=1, v2_version=4, v23_sep='/', padding=None) Save changes to a file. Args: - filename (fspath): + filething (filething): Filename to save the tag to. If no filename is given, the one most recently loaded is used. v1 (ID3v1SaveOptions): @@ -268,7 +282,7 @@ class ID3(ID3Tags, mutagen.Metadata): f.truncate() @loadfile(writable=True) - def delete(self, filething, delete_v1=True, delete_v2=True): + def delete(self, filething=None, delete_v1=True, delete_v2=True): """delete(filething=None, delete_v1=True, delete_v2=True) Remove tags from a file. diff --git a/libs/common/mutagen/id3/_frames.py b/libs/common/mutagen/id3/_frames.py index f50752aa..e30ba20b 100644 --- a/libs/common/mutagen/id3/_frames.py +++ b/libs/common/mutagen/id3/_frames.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2005 Michael Urman # # This program is free software; you can redistribute it and/or modify @@ -8,6 +7,7 @@ import zlib from struct import unpack +from typing import Sequence from ._util import ID3JunkFrameError, ID3EncryptionUnsupportedError, unsynch, \ ID3SaveConfig, error @@ -17,9 +17,7 @@ from ._specs import BinaryDataSpec, StringSpec, Latin1TextSpec, \ VolumeAdjustmentSpec, ChannelSpec, MultiSpec, SynchronizedTextSpec, \ KeyEventSpec, TimeStampSpec, EncodedNumericPartTextSpec, \ EncodedNumericTextSpec, SpecError, PictureTypeSpec, ID3FramesSpec, \ - Latin1TextListSpec, CTOCFlagsSpec, FrameIDSpec, RVASpec -from .._compat import text_type, string_types, swap_to_string, iteritems, \ - izip, itervalues + Latin1TextListSpec, CTOCFlagsSpec, FrameIDSpec, RVASpec, Spec def _bytes2key(b): @@ -51,8 +49,8 @@ class Frame(object): FLAG24_UNSYNCH = 0x0002 FLAG24_DATALEN = 0x0001 - _framespec = [] - _optionalspec = [] + _framespec: Sequence[Spec] = [] + _optionalspec: Sequence[Spec] = [] def __init__(self, *args, **kwargs): if len(args) == 1 and len(kwargs) == 0 and \ @@ -61,7 +59,7 @@ class Frame(object): # ask the sub class to fill in our data other._to_other(self) else: - for checker, val in izip(self._framespec, args): + for checker, val in zip(self._framespec, args): setattr(self, checker.name, val) for checker in self._framespec[len(args):]: setattr(self, checker.name, @@ -277,6 +275,8 @@ class Frame(object): elif header.version >= header._V23: if tflags & Frame.FLAG23_COMPRESS: + if len(data) < 4: + raise ID3JunkFrameError('frame too small: %r' % data) usize, = unpack('>L', data[:4]) data = data[4:] if tflags & Frame.FLAG23_ENCRYPT: @@ -291,7 +291,7 @@ class Frame(object): frame._readData(header, data) return frame - def __hash__(self): + def __hash__(self: object): raise TypeError("Frame objects are unhashable") @@ -330,7 +330,7 @@ class CHAP(Frame): def _pprint(self): frame_pprint = u"" - for frame in itervalues(self.sub_frames): + for frame in self.sub_frames.values(): for line in frame.pprint().splitlines(): frame_pprint += "\n" + " " * 4 + line return u"%s time=%d..%d offset=%d..%d%s" % ( @@ -377,7 +377,6 @@ class CTOC(Frame): u",".join(self.child_element_ids), frame_pprint) -@swap_to_string class TextFrame(Frame): """Text strings. @@ -399,7 +398,7 @@ class TextFrame(Frame): ] def __bytes__(self): - return text_type(self).encode('utf-8') + return str(self).encode('utf-8') def __str__(self): return u'\u0000'.join(self.text) @@ -407,8 +406,8 @@ class TextFrame(Frame): def __eq__(self, other): if isinstance(other, bytes): return bytes(self) == other - elif isinstance(other, text_type): - return text_type(self) == other + elif isinstance(other, str): + return str(self) == other return self.text == other __hash__ = Frame.__hash__ @@ -481,7 +480,6 @@ class NumericPartTextFrame(TextFrame): return int(self.text[0].split("/")[0]) -@swap_to_string class TimeStampTextFrame(TextFrame): """A list of time stamps. @@ -495,7 +493,7 @@ class TimeStampTextFrame(TextFrame): ] def __bytes__(self): - return text_type(self).encode('utf-8') + return str(self).encode('utf-8') def __str__(self): return u','.join([stamp.text for stamp in self.text]) @@ -504,7 +502,6 @@ class TimeStampTextFrame(TextFrame): return u" / ".join([stamp.text for stamp in self.text]) -@swap_to_string class UrlFrame(Frame): """A frame containing a URL string. @@ -517,7 +514,7 @@ class UrlFrame(Frame): ASCII. """ - _framespec = [ + _framespec: Sequence[Spec] = [ Latin1TextSpec('url'), ] @@ -587,7 +584,7 @@ class TCON(TextFrame): if genreid: for gid in genreid[1:-1].split(")("): if gid.isdigit() and int(gid) < len(self.GENRES): - gid = text_type(self.GENRES[int(gid)]) + gid = str(self.GENRES[int(gid)]) newgenres.append(gid) elif gid == "CR": newgenres.append(u"Cover") @@ -608,7 +605,7 @@ class TCON(TextFrame): return genres def __set_genres(self, genres): - if isinstance(genres, string_types): + if isinstance(genres, str): genres = [genres] self.text = [self.__decode(g) for g in genres] @@ -1044,7 +1041,6 @@ class SYTC(Frame): __hash__ = Frame.__hash__ -@swap_to_string class USLT(Frame): """Unsynchronised lyrics/text transcription. @@ -1078,7 +1074,6 @@ class USLT(Frame): return "%s=%s=%s" % (self.desc, self.lang, self.text) -@swap_to_string class SYLT(Frame): """Synchronised lyrics/text.""" @@ -1095,16 +1090,21 @@ class SYLT(Frame): def HashKey(self): return '%s:%s:%s' % (self.FrameID, self.desc, self.lang) + def _pprint(self): + return str(self) + def __eq__(self, other): return str(self) == other __hash__ = Frame.__hash__ def __str__(self): - return u"".join(text for (text, time) in self.text) + unit = 'fr' if self.format == 1 else 'ms' + return u"\n".join("[{0}{1}]: {2}".format(time, unit, text) + for (text, time) in self.text) def __bytes__(self): - return text_type(self).encode("utf-8") + return str(self).encode("utf-8") class COMM(TextFrame): @@ -1279,7 +1279,7 @@ class APIC(Frame): return other def _pprint(self): - type_desc = text_type(self.type) + type_desc = str(self.type) if hasattr(self.type, "_pprint"): type_desc = self.type._pprint() @@ -1309,7 +1309,7 @@ class PCNT(Frame): return self.count def _pprint(self): - return text_type(self.count) + return str(self.count) class PCST(Frame): @@ -1328,7 +1328,7 @@ class PCST(Frame): return self.value def _pprint(self): - return text_type(self.value) + return str(self.value) class POPM(Frame): @@ -1432,7 +1432,6 @@ class RBUF(Frame): return self.size -@swap_to_string class AENC(Frame): """Audio encryption. @@ -1549,7 +1548,6 @@ class UFID(Frame): return "%s=%r" % (self.owner, self.data) -@swap_to_string class USER(Frame): """Terms of use. @@ -1585,7 +1583,6 @@ class USER(Frame): return "%r=%s" % (self.lang, self.text) -@swap_to_string class OWNE(Frame): """Ownership frame.""" @@ -1636,7 +1633,6 @@ class COMR(Frame): __hash__ = Frame.__hash__ -@swap_to_string class ENCR(Frame): """Encryption method registration. @@ -1663,7 +1659,6 @@ class ENCR(Frame): __hash__ = Frame.__hash__ -@swap_to_string class GRID(Frame): """Group identification registration.""" @@ -1692,7 +1687,6 @@ class GRID(Frame): __hash__ = Frame.__hash__ -@swap_to_string class PRIV(Frame): """Private frame.""" @@ -1718,7 +1712,6 @@ class PRIV(Frame): __hash__ = Frame.__hash__ -@swap_to_string class SIGN(Frame): """Signature frame.""" @@ -2130,7 +2123,7 @@ Frames_2_2 = {} k, v = None, None -for k, v in iteritems(globals()): +for k, v in globals().items(): if isinstance(v, type) and issubclass(v, Frame): v.__module__ = "mutagen.id3" diff --git a/libs/common/mutagen/id3/_id3v1.py b/libs/common/mutagen/id3/_id3v1.py index d41d00d0..4e1ca05f 100644 --- a/libs/common/mutagen/id3/_id3v1.py +++ b/libs/common/mutagen/id3/_id3v1.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2005 Michael Urman # 2006 Lukas Lalinsky # 2013 Christoph Reiter @@ -11,22 +10,32 @@ import errno from struct import error as StructError, unpack -from mutagen._util import chr_, text_type +from mutagen._util import bchr -from ._frames import TCON, TRCK, COMM, TDRC, TALB, TPE1, TIT2 +from ._frames import TCON, TRCK, COMM, TDRC, TYER, TALB, TPE1, TIT2 -def find_id3v1(fileobj): +def find_id3v1(fileobj, v2_version=4, known_frames=None): """Returns a tuple of (id3tag, offset_to_end) or (None, 0) offset mainly because we used to write too short tags in some cases and we need the offset to delete them. + + v2_version: Decides whether ID3v2.3 or ID3v2.4 tags + should be returned. Must be 3 or 4. + + known_frames (Dict[`mutagen.text`, `Frame`]): dict mapping frame + IDs to Frame objects """ + if v2_version not in (3, 4): + raise ValueError("Only 3 and 4 possible for v2_version") + # id3v1 is always at the end (after apev2) extra_read = b"APETAGEX".index(b"TAG") + old_pos = fileobj.tell() try: fileobj.seek(-128 - extra_read, 2) except IOError as e: @@ -38,6 +47,7 @@ def find_id3v1(fileobj): raise data = fileobj.read(128 + extra_read) + fileobj.seek(old_pos, 0) try: idx = data.index(b"TAG") except ValueError: @@ -53,7 +63,7 @@ def find_id3v1(fileobj): if idx == ape_idx + extra_read: return (None, 0) - tag = ParseID3v1(data[idx:]) + tag = ParseID3v1(data[idx:], v2_version, known_frames) if tag is None: return (None, 0) @@ -62,12 +72,21 @@ def find_id3v1(fileobj): # ID3v1.1 support. -def ParseID3v1(data): - """Parse an ID3v1 tag, returning a list of ID3v2.4 frames. +def ParseID3v1(data, v2_version=4, known_frames=None): + """Parse an ID3v1 tag, returning a list of ID3v2 frames Returns a {frame_name: frame} dict or None. + + v2_version: Decides whether ID3v2.3 or ID3v2.4 tags + should be returned. Must be 3 or 4. + + known_frames (Dict[`mutagen.text`, `Frame`]): dict mapping frame + IDs to Frame objects """ + if v2_version not in (3, 4): + raise ValueError("Only 3 and 4 possible for v2_version") + try: data = data[data.index(b"TAG"):] except ValueError: @@ -97,23 +116,45 @@ def ParseID3v1(data): title, artist, album, year, comment = map( fix, [title, artist, album, year, comment]) + frame_class = { + "TIT2": TIT2, + "TPE1": TPE1, + "TALB": TALB, + "TYER": TYER, + "TDRC": TDRC, + "COMM": COMM, + "TRCK": TRCK, + "TCON": TCON, + } + for key in frame_class: + if known_frames is not None: + if key in known_frames: + frame_class[key] = known_frames[key] + else: + frame_class[key] = None + frames = {} - if title: - frames["TIT2"] = TIT2(encoding=0, text=title) - if artist: - frames["TPE1"] = TPE1(encoding=0, text=[artist]) - if album: - frames["TALB"] = TALB(encoding=0, text=album) + if title and frame_class["TIT2"]: + frames["TIT2"] = frame_class["TIT2"](encoding=0, text=title) + if artist and frame_class["TPE1"]: + frames["TPE1"] = frame_class["TPE1"](encoding=0, text=[artist]) + if album and frame_class["TALB"]: + frames["TALB"] = frame_class["TALB"](encoding=0, text=album) if year: - frames["TDRC"] = TDRC(encoding=0, text=year) - if comment: - frames["COMM"] = COMM( + if v2_version == 3 and frame_class["TYER"]: + frames["TYER"] = frame_class["TYER"](encoding=0, text=year) + elif frame_class["TDRC"]: + frames["TDRC"] = frame_class["TDRC"](encoding=0, text=year) + if comment and frame_class["COMM"]: + frames["COMM"] = frame_class["COMM"]( encoding=0, lang="eng", desc="ID3v1 Comment", text=comment) + # Don't read a track number if it looks like the comment was # padded with spaces instead of nulls (thanks, WinAmp). - if track and ((track != 32) or (data[-3] == b'\x00'[0])): + if (track and frame_class["TRCK"] and + ((track != 32) or (data[-3] == b'\x00'[0]))): frames["TRCK"] = TRCK(encoding=0, text=str(track)) - if genre != 255: + if genre != 255 and frame_class["TCON"]: frames["TCON"] = TCON(encoding=0, text=str(genre)) return frames @@ -139,7 +180,7 @@ def MakeID3v1(id3): if "TRCK" in id3: try: - v1["track"] = chr_(+id3["TRCK"]) + v1["track"] = bchr(+id3["TRCK"]) except ValueError: v1["track"] = b"\x00" else: @@ -152,14 +193,14 @@ def MakeID3v1(id3): pass else: if genre in TCON.GENRES: - v1["genre"] = chr_(TCON.GENRES.index(genre)) + v1["genre"] = bchr(TCON.GENRES.index(genre)) if "genre" not in v1: v1["genre"] = b"\xff" if "TDRC" in id3: - year = text_type(id3["TDRC"]).encode('ascii') + year = str(id3["TDRC"]).encode('ascii') elif "TYER" in id3: - year = text_type(id3["TYER"]).encode('ascii') + year = str(id3["TYER"]).encode('ascii') else: year = b"" v1["year"] = (year + b"\x00\x00\x00\x00")[:4] diff --git a/libs/common/mutagen/id3/_specs.py b/libs/common/mutagen/id3/_specs.py index 63784333..e8f968b6 100644 --- a/libs/common/mutagen/id3/_specs.py +++ b/libs/common/mutagen/id3/_specs.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2005 Michael Urman # # This program is free software; you can redistribute it and/or modify @@ -10,10 +9,8 @@ import struct import codecs from struct import unpack, pack -from .._compat import text_type, chr_, PY3, swap_to_string, string_types, \ - xrange -from .._util import total_ordering, decode_terminated, enum, izip, flags, \ - cdata, encode_endian, intround +from .._util import total_ordering, decode_terminated, enum, flags, \ + cdata, encode_endian, intround, bchr from ._util import BitPaddedInt, is_valid_frame_id @@ -87,7 +84,7 @@ class PictureType(object): """Publisher/Studio logotype""" def _pprint(self): - return text_type(self).split(".", 1)[-1].lower().replace("_", " ") + return str(self).split(".", 1)[-1].lower().replace("_", " ") @flags @@ -165,11 +162,11 @@ class ByteSpec(Spec): return bytearray(data)[0], data[1:] def write(self, config, frame, value): - return chr_(value) + return bchr(value) def validate(self, frame, value): if value is not None: - chr_(value) + bchr(value) return value @@ -289,26 +286,22 @@ class StringSpec(Spec): except UnicodeDecodeError: raise SpecError("not ascii") else: - if PY3: - chunk = ascii + chunk = ascii return chunk, data[s.len:] def write(self, config, frame, value): - if PY3: - value = value.encode("ascii") + + value = value.encode("ascii") return (bytes(value) + b'\x00' * self.len)[:self.len] def validate(self, frame, value): if value is None: raise TypeError - if PY3: - if not isinstance(value, str): - raise TypeError("%s has to be str" % self.name) - value.encode("ascii") - else: - if not isinstance(value, bytes): - value = value.encode("ascii") + + if not isinstance(value, str): + raise TypeError("%s has to be str" % self.name) + value.encode("ascii") if len(value) == self.len: return value @@ -424,7 +417,7 @@ class BinaryDataSpec(Spec): def write(self, config, frame, value): if isinstance(value, bytes): return value - value = text_type(value).encode("ascii") + value = str(value).encode("ascii") return value def validate(self, frame, value): @@ -432,10 +425,10 @@ class BinaryDataSpec(Spec): raise TypeError if isinstance(value, bytes): return value - elif PY3: + else: raise TypeError("%s has to be bytes" % self.name) - value = text_type(value).encode("ascii") + value = str(value).encode("ascii") return value @@ -493,7 +486,7 @@ class EncodedTextSpec(Spec): raise SpecError(e) def validate(self, frame, value): - return text_type(value) + return str(value) class MultiSpec(Spec): @@ -522,26 +515,26 @@ class MultiSpec(Spec): data.append(self.specs[0].write(config, frame, v)) else: for record in value: - for v, s in izip(record, self.specs): + for v, s in zip(record, self.specs): data.append(s.write(config, frame, v)) return b''.join(data) def validate(self, frame, value): - if self.sep and isinstance(value, string_types): + if self.sep and isinstance(value, str): value = value.split(self.sep) if isinstance(value, list): if len(self.specs) == 1: return [self.specs[0].validate(frame, v) for v in value] else: return [ - [s.validate(frame, v) for (v, s) in izip(val, self.specs)] + [s.validate(frame, v) for (v, s) in zip(val, self.specs)] for val in value] raise ValueError('Invalid MultiSpec data: %r' % value) def _validate23(self, frame, value, **kwargs): if len(self.specs) != 1: return [[s._validate23(frame, v, **kwargs) - for (v, s) in izip(val, self.specs)] + for (v, s) in zip(val, self.specs)] for val in value] spec = self.specs[0] @@ -582,7 +575,7 @@ class Latin1TextSpec(Spec): return value.encode('latin1') + b'\x00' def validate(self, frame, value): - return text_type(value) + return str(value) class ID3FramesSpec(Spec): @@ -632,7 +625,7 @@ class Latin1TextListSpec(Spec): def read(self, header, frame, data): count, data = self._bspec.read(header, frame, data) entries = [] - for i in xrange(count): + for i in range(count): entry, data = self._lspec.read(header, frame, data) entries.append(entry) return entries, data @@ -647,7 +640,6 @@ class Latin1TextListSpec(Spec): return [self._lspec.validate(frame, v) for v in value] -@swap_to_string @total_ordering class ID3TimeStamp(object): """A time stamp in ID3v2 format. @@ -665,10 +657,8 @@ class ID3TimeStamp(object): def __init__(self, text): if isinstance(text, ID3TimeStamp): text = text.text - elif not isinstance(text, text_type): - if PY3: - raise TypeError("not a str") - text = text.decode("utf-8") + elif not isinstance(text, str): + raise TypeError("not a str") self.text = text @@ -736,7 +726,7 @@ class TimeStampSpec(EncodedTextSpec): class ChannelSpec(ByteSpec): (OTHER, MASTER, FRONTRIGHT, FRONTLEFT, BACKRIGHT, BACKLEFT, FRONTCENTRE, - BACKCENTRE, SUBWOOFER) = xrange(9) + BACKCENTRE, SUBWOOFER) = range(9) class VolumeAdjustmentSpec(Spec): @@ -771,7 +761,7 @@ class VolumePeakSpec(Spec): if vol_bytes + 1 > len(data): raise SpecError("not enough frame data") shift = ((8 - (bits & 7)) & 7) + (4 - vol_bytes) * 8 - for i in xrange(1, vol_bytes + 1): + for i in range(1, vol_bytes + 1): peak *= 256 peak += data_array[i] peak *= 2 ** shift diff --git a/libs/common/mutagen/id3/_tags.py b/libs/common/mutagen/id3/_tags.py index 99845faa..c49dba29 100644 --- a/libs/common/mutagen/id3/_tags.py +++ b/libs/common/mutagen/id3/_tags.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2005 Michael Urman # Copyright 2016 Christoph Reiter # @@ -7,11 +6,12 @@ # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. +import re import struct +from itertools import zip_longest from mutagen._tags import Tags from mutagen._util import DictProxy, convert_error, read_full -from mutagen._compat import PY3, text_type, itervalues from ._util import BitPaddedInt, unsynch, ID3JunkFrameError, \ ID3EncryptionUnsupportedError, is_valid_frame_id, error, \ @@ -82,10 +82,7 @@ class ID3Header(object): if self.f_extended: extsize_data = read_full(fileobj, 4) - if PY3: - frame_id = extsize_data.decode("ascii", "replace") - else: - frame_id = extsize_data + frame_id = extsize_data.decode("ascii", "replace") if frame_id in Frames: # Some tagger sets the extended header flag but @@ -131,11 +128,10 @@ def determine_bpi(data, frames, EMPTY=b"\x00" * 10): name, size, flags = struct.unpack('>4sLH', part) size = BitPaddedInt(size) o += 10 + size - if PY3: - try: - name = name.decode("ascii") - except UnicodeDecodeError: - continue + try: + name = name.decode("ascii") + except UnicodeDecodeError: + continue if name in frames: asbpi += 1 else: @@ -151,11 +147,10 @@ def determine_bpi(data, frames, EMPTY=b"\x00" * 10): break name, size, flags = struct.unpack('>4sLH', part) o += 10 + size - if PY3: - try: - name = name.decode("ascii") - except UnicodeDecodeError: - continue + try: + name = name.decode("ascii") + except UnicodeDecodeError: + continue if name in frames: asint += 1 else: @@ -191,7 +186,7 @@ class ID3Tags(DictProxy, Tags): order = ["TIT2", "TPE1", "TRCK", "TALB", "TPOS", "TDRC", "TCON"] framedata = [ - (f, save_frame(f, config=config)) for f in itervalues(self)] + (f, save_frame(f, config=config)) for f in self.values()] def get_prio(frame): try: @@ -243,7 +238,7 @@ class ID3Tags(DictProxy, Tags): Args: key (text): key for frames to delete - values (List[`Frame`]): frames to add + values (list[Frame]): frames to add """ self.delall(key) @@ -369,24 +364,23 @@ class ID3Tags(DictProxy, Tags): self.__update_common() # TDAT, TYER, and TIME have been turned into TDRC. - try: - date = text_type(self.get("TYER", "")) - if date.strip(u"\x00"): - self.pop("TYER") - dat = text_type(self.get("TDAT", "")) - if dat.strip("\x00"): - self.pop("TDAT") - date = "%s-%s-%s" % (date, dat[2:], dat[:2]) - time = text_type(self.get("TIME", "")) - if time.strip("\x00"): - self.pop("TIME") - date += "T%s:%s:00" % (time[:2], time[2:]) - if "TDRC" not in self: - self.add(TDRC(encoding=0, text=date)) - except UnicodeDecodeError: - # Old ID3 tags have *lots* of Unicode problems, so if TYER - # is bad, just chuck the frames. - pass + timestamps = [] + old_frames = [self.pop(n, []) for n in ["TYER", "TDAT", "TIME"]] + for y, d, t in zip_longest(*old_frames, fillvalue=u""): + ym = re.match(r"([0-9]+)\Z", y) + dm = re.match(r"([0-9]{2})([0-9]{2})\Z", d) + tm = re.match(r"([0-9]{2})([0-9]{2})\Z", t) + timestamp = "" + if ym: + timestamp += u"%s" % ym.groups() + if dm: + timestamp += u"-%s-%s" % dm.groups()[::-1] + if tm: + timestamp += u"T%s:%s:00" % tm.groups() + if timestamp: + timestamps.append(timestamp) + if timestamps and "TDRC" not in self: + self.add(TDRC(encoding=0, text=timestamps)) # TORY can be the first part of a TDOR. if "TORY" in self: @@ -533,8 +527,7 @@ def save_frame(frame, name=None, config=None): frame_name = name else: frame_name = type(frame).__name__ - if PY3: - frame_name = frame_name.encode("ascii") + frame_name = frame_name.encode("ascii") header = struct.pack('>4s4sH', frame_name, datasize, flags) return header + framedata @@ -575,11 +568,10 @@ def read_frames(id3, data, frames): if size == 0: continue # drop empty frames - if PY3: - try: - name = name.decode('ascii') - except UnicodeDecodeError: - continue + try: + name = name.decode('ascii') + except UnicodeDecodeError: + continue try: # someone writes 2.3 frames with 2.2 names @@ -614,11 +606,10 @@ def read_frames(id3, data, frames): if size == 0: continue # drop empty frames - if PY3: - try: - name = name.decode('ascii') - except UnicodeDecodeError: - continue + try: + name = name.decode('ascii') + except UnicodeDecodeError: + continue try: tag = frames[name] diff --git a/libs/common/mutagen/id3/_util.py b/libs/common/mutagen/id3/_util.py index 93bb264e..b028d9c6 100644 --- a/libs/common/mutagen/id3/_util.py +++ b/libs/common/mutagen/id3/_util.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2005 Michael Urman # 2013 Christoph Reiter # 2014 Ben Ockmore @@ -8,7 +7,6 @@ # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. -from mutagen._compat import long_, integer_types, PY3 from mutagen._util import MutagenError @@ -110,7 +108,7 @@ class _BitPaddedMixin(object): mask = (((1 << (8 - bits)) - 1) << bits) - if isinstance(value, integer_types): + if isinstance(value, int): while value: if value & mask: return False @@ -133,7 +131,7 @@ class BitPaddedInt(int, _BitPaddedMixin): numeric_value = 0 shift = 0 - if isinstance(value, integer_types): + if isinstance(value, int): if value < 0: raise ValueError while value: @@ -149,21 +147,12 @@ class BitPaddedInt(int, _BitPaddedMixin): else: raise TypeError - if isinstance(numeric_value, int): - self = int.__new__(BitPaddedInt, numeric_value) - else: - self = long_.__new__(BitPaddedLong, numeric_value) + self = int.__new__(BitPaddedInt, numeric_value) self.bits = bits self.bigendian = bigendian return self -if PY3: - BitPaddedLong = BitPaddedInt -else: - class BitPaddedLong(long_, _BitPaddedMixin): - pass - class ID3BadUnsynchData(error, ValueError): """Deprecated""" diff --git a/libs/common/mutagen/m4a.py b/libs/common/mutagen/m4a.py index c7583f8e..eea2136b 100644 --- a/libs/common/mutagen/m4a.py +++ b/libs/common/mutagen/m4a.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify diff --git a/libs/common/mutagen/monkeysaudio.py b/libs/common/mutagen/monkeysaudio.py index 82bfcd24..30cf78c0 100644 --- a/libs/common/mutagen/monkeysaudio.py +++ b/libs/common/mutagen/monkeysaudio.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2006 Lukas Lalinsky # # This program is free software; you can redistribute it and/or modify @@ -18,10 +17,9 @@ __all__ = ["MonkeysAudio", "Open", "delete"] import struct -from ._compat import endswith from mutagen import StreamInfo from mutagen.apev2 import APEv2File, error, delete -from mutagen._util import cdata, convert_error +from mutagen._util import cdata, convert_error, endswith class MonkeysAudioHeaderError(error): diff --git a/libs/common/mutagen/mp3/__init__.py b/libs/common/mutagen/mp3/__init__.py index 8ce70e35..1c9b7e5c 100644 --- a/libs/common/mutagen/mp3/__init__.py +++ b/libs/common/mutagen/mp3/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify @@ -12,8 +11,7 @@ import struct from mutagen import StreamInfo from mutagen._util import MutagenError, enum, BitReader, BitReaderError, \ - convert_error, intround -from mutagen._compat import endswith, xrange + convert_error, intround, endswith from mutagen.id3 import ID3FileType, delete from mutagen.id3._util import BitPaddedInt @@ -75,27 +73,27 @@ def _guess_xing_bitrate_mode(xing): # Mode values. -STEREO, JOINTSTEREO, DUALCHANNEL, MONO = xrange(4) +STEREO, JOINTSTEREO, DUALCHANNEL, MONO = range(4) class MPEGFrame(object): # Map (version, layer) tuples to bitrates. __BITRATE = { - (1, 1): [0, 32, 64, 96, 128, 160, 192, 224, - 256, 288, 320, 352, 384, 416, 448], - (1, 2): [0, 32, 48, 56, 64, 80, 96, 112, 128, - 160, 192, 224, 256, 320, 384], - (1, 3): [0, 32, 40, 48, 56, 64, 80, 96, 112, - 128, 160, 192, 224, 256, 320], - (2, 1): [0, 32, 48, 56, 64, 80, 96, 112, 128, - 144, 160, 176, 192, 224, 256], - (2, 2): [0, 8, 16, 24, 32, 40, 48, 56, 64, - 80, 96, 112, 128, 144, 160], + (1., 1): [0, 32, 64, 96, 128, 160, 192, 224, + 256, 288, 320, 352, 384, 416, 448], + (1., 2): [0, 32, 48, 56, 64, 80, 96, 112, 128, + 160, 192, 224, 256, 320, 384], + (1., 3): [0, 32, 40, 48, 56, 64, 80, 96, 112, + 128, 160, 192, 224, 256, 320], + (2., 1): [0, 32, 48, 56, 64, 80, 96, 112, 128, + 144, 160, 176, 192, 224, 256], + (2., 2): [0, 8, 16, 24, 32, 40, 48, 56, 64, + 80, 96, 112, 128, 144, 160], } __BITRATE[(2, 3)] = __BITRATE[(2, 2)] - for i in xrange(1, 4): + for i in range(1, 4): __BITRATE[(2.5, i)] = __BITRATE[(2, i)] # Map version to sample rates. @@ -306,7 +304,7 @@ class MPEGInfo(StreamInfo): bitrate (`int`): audio bitrate, in bits per second. In case :attr:`bitrate_mode` is :attr:`BitrateMode.UNKNOWN` the bitrate is guessed based on the first frame. - sample_rate (`int`) audio sample rate, in Hz + sample_rate (`int`): audio sample rate, in Hz encoder_info (`mutagen.text`): a string containing encoder name and possibly version. In case a lame tag is present this will start with ``"LAME "``, if unknown it is empty, otherwise the @@ -357,7 +355,7 @@ class MPEGInfo(StreamInfo): # find a sync in the first 1024K, give up after some invalid syncs max_read = 1024 * 1024 - max_syncs = 1000 + max_syncs = 1500 enough_frames = 4 min_frames = 2 @@ -370,7 +368,7 @@ class MPEGInfo(StreamInfo): if max_syncs <= 0: break - for _ in xrange(enough_frames): + for _ in range(enough_frames): try: frame = MPEGFrame(fileobj) except HeaderNotFoundError: @@ -480,4 +478,4 @@ class EasyMP3(MP3): """ from mutagen.easyid3 import EasyID3 as ID3 - ID3 = ID3 + ID3 = ID3 # type: ignore diff --git a/libs/common/mutagen/mp3/_util.py b/libs/common/mutagen/mp3/_util.py index fd1b5ca3..b67ab6aa 100644 --- a/libs/common/mutagen/mp3/_util.py +++ b/libs/common/mutagen/mp3/_util.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015 Christoph Reiter # # This program is free software; you can redistribute it and/or modify @@ -13,9 +12,10 @@ http://wiki.hydrogenaud.io/index.php?title=MP3 from __future__ import division from functools import partial +from io import BytesIO +from typing import List -from mutagen._util import cdata, BitReader -from mutagen._compat import xrange, iterbytes, cBytesIO +from mutagen._util import cdata, BitReader, iterbytes class LAMEError(Exception): @@ -109,7 +109,7 @@ class LAMEHeader(object): raise LAMEError("Not enough data") # extended lame header - r = BitReader(cBytesIO(payload)) + r = BitReader(BytesIO(payload)) revision = r.bits(4) if revision != 0: raise LAMEError("unsupported header revision %d" % revision) @@ -356,7 +356,7 @@ class XingHeader(object): bytes = -1 """Number of bytes, -1 if unknown""" - toc = [] + toc: List[int] = [] """List of 100 file offsets in percent encoded as 0-255. E.g. entry 50 contains the file offset in percent at 50% play time. Empty if unknown. @@ -474,7 +474,7 @@ class VBRIHeader(object): toc_frames = 0 """Number of frames per table entry""" - toc = [] + toc: List[int] = [] """TOC""" def __init__(self, fileobj): @@ -515,7 +515,7 @@ class VBRIHeader(object): else: raise VBRIHeaderError("Invalid TOC entry size") - self.toc = [unpack(i)[0] for i in xrange(0, toc_size, toc_entry_size)] + self.toc = [unpack(i)[0] for i in range(0, toc_size, toc_entry_size)] @classmethod def get_offset(cls, info): diff --git a/libs/common/mutagen/mp4/__init__.py b/libs/common/mutagen/mp4/__init__.py index 75ec5769..87a38f66 100644 --- a/libs/common/mutagen/mp4/__init__.py +++ b/libs/common/mutagen/mp4/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify @@ -25,13 +24,15 @@ were all consulted. import struct import sys +from io import BytesIO +from collections.abc import Sequence +from datetime import timedelta from mutagen import FileType, Tags, StreamInfo, PaddingInfo from mutagen._constants import GENRES from mutagen._util import cdata, insert_bytes, DictProxy, MutagenError, \ - hashable, enum, get_size, resize_bytes, loadfile, convert_error -from mutagen._compat import (reraise, PY2, string_types, text_type, chr_, - iteritems, PY3, cBytesIO, izip, xrange) + hashable, enum, get_size, resize_bytes, loadfile, convert_error, bchr, \ + reraise from ._atom import Atoms, Atom, AtomError from ._util import parse_full_atom from ._as_entry import AudioSampleEntry, ASEntryError @@ -205,14 +206,10 @@ class MP4FreeForm(bytes): def _name2key(name): - if PY2: - return name return name.decode("latin-1") def _key2name(key): - if PY2: - return key return key.encode("latin-1") @@ -246,7 +243,7 @@ def _item_sort_key(key, value): "\xa9gen", "gnre", "trkn", "disk", "\xa9day", "cpil", "pgap", "pcst", "tmpo", "\xa9too", "----", "covr", "\xa9lyr"] - order = dict(izip(order, xrange(len(order)))) + order = dict(zip(order, range(len(order)))) last = len(order) # If there's no key-based way to distinguish, order by length. # If there's still no way, go by string comparison on the @@ -311,6 +308,7 @@ class MP4Tags(DictProxy, Tags): * '\\xa9mvi' -- Movement Index * 'shwm' -- work/movement * 'stik' -- Media Kind + * 'hdvd' -- HD Video * 'rtng' -- Content Rating * 'tves' -- TV Episode * 'tvsn' -- TV Season @@ -366,8 +364,7 @@ class MP4Tags(DictProxy, Tags): self.__parse_text(atom, data, implicit=False) except MP4MetadataError: # parsing failed, save them so we can write them back - key = _name2key(atom.name) - self._failed_atoms.setdefault(key, []).append(data) + self._failed_atoms.setdefault(_name2key(atom.name), []).append(data) def __setitem__(self, key, value): if not isinstance(key, str): @@ -392,7 +389,7 @@ class MP4Tags(DictProxy, Tags): @convert_error(IOError, error) @loadfile(writable=True) - def save(self, filething, padding=None): + def save(self, filething=None, padding=None): values = [] items = sorted(self.items(), key=lambda kv: _item_sort_key(*kv)) @@ -402,7 +399,7 @@ class MP4Tags(DictProxy, Tags): except (TypeError, ValueError) as s: reraise(MP4MetadataValueError, s, sys.exc_info()[2]) - for key, failed in iteritems(self._failed_atoms): + for key, failed in self._failed_atoms.items(): # don't write atoms back if we have added a new one with # the same name, this excludes freeform which can have # multiple atoms with the same key (most parsers seem to be able @@ -562,6 +559,9 @@ class MP4Tags(DictProxy, Tags): if len(head) != 12: raise MP4MetadataError("truncated atom % r" % atom.name) length, name = struct.unpack(">I4s", head[:8]) + if length < 1: + raise MP4MetadataError( + "atom %r has a length of zero" % atom.name) version = ord(head[8:9]) flags = struct.unpack(">I", b"\x00" + head[9:12])[0] if name != b"data": @@ -601,7 +601,9 @@ class MP4Tags(DictProxy, Tags): if atom_name != b"data": raise MP4MetadataError( "unexpected atom %r inside %r" % (atom_name, atom.name)) - + if length < 1: + raise MP4MetadataError( + "atom %r has a length of zero" % atom.name) version = ord(data[pos + 8:pos + 8 + 1]) flags = struct.unpack(">I", b"\x00" + data[pos + 9:pos + 12])[0] value.append(MP4FreeForm(data[pos + 16:pos + length], @@ -746,7 +748,7 @@ class MP4Tags(DictProxy, Tags): def __render_bool(self, key, value): return self.__render_data( - key, 0, AtomDataType.INTEGER, [chr_(bool(value))]) + key, 0, AtomDataType.INTEGER, [bchr(bool(value))]) def __parse_cover(self, atom, data): values = [] @@ -760,6 +762,9 @@ class MP4Tags(DictProxy, Tags): continue raise MP4MetadataError( "unexpected atom %r inside 'covr'" % name) + if length < 1: + raise MP4MetadataError( + "atom %r has a length of zero" % atom.name) if imageformat not in (MP4Cover.FORMAT_JPEG, MP4Cover.FORMAT_PNG): # Sometimes AtomDataType.IMPLICIT or simply wrong. # In all cases it was jpeg, so default to it @@ -807,18 +812,14 @@ class MP4Tags(DictProxy, Tags): self.__add(key, values) def __render_text(self, key, value, flags=AtomDataType.UTF8): - if isinstance(value, string_types): + if isinstance(value, str): value = [value] encoded = [] for v in value: - if not isinstance(v, text_type): - if PY3: - raise TypeError("%r not str" % v) - try: - v = v.decode("utf-8") - except (AttributeError, UnicodeDecodeError) as e: - raise TypeError(e) + if not isinstance(v, str): + raise TypeError("%r not str" % v) + encoded.append(v.encode("utf-8")) return self.__render_data(key, 0, flags, encoded) @@ -852,6 +853,7 @@ class MP4Tags(DictProxy, Tags): b"pcst": (__parse_bool, __render_bool), b"shwm": (__parse_integer, __render_integer, 1), b"stik": (__parse_integer, __render_integer, 1), + b"hdvd": (__parse_integer, __render_integer, 1), b"rtng": (__parse_integer, __render_integer, 1), b"covr": (__parse_cover, __render_cover), b"purl": (__parse_text, __render_text), @@ -869,14 +871,14 @@ class MP4Tags(DictProxy, Tags): def pprint(self): def to_line(key, value): - assert isinstance(key, text_type) - if isinstance(value, text_type): + assert isinstance(key, str) + if isinstance(value, str): return u"%s=%s" % (key, value) return u"%s=%r" % (key, value) values = [] - for key, value in sorted(iteritems(self)): - if not isinstance(key, text_type): + for key, value in sorted(self.items()): + if not isinstance(key, str): key = key.decode("latin-1") if key == "covr": values.append(u"%s=%s" % (key, u", ".join( @@ -889,6 +891,123 @@ class MP4Tags(DictProxy, Tags): return u"\n".join(values) +class Chapter(object): + """Chapter() + + Chapter information container + """ + def __init__(self, start, title): + self.start = start + self.title = title + + +class MP4Chapters(Sequence): + """MP4Chapters() + + MPEG-4 Chapter information. + + Supports the 'moov.udta.chpl' box. + + A sequence of Chapter objects with the following members: + start (`float`): position from the start of the file in seconds + title (`str`): title of the chapter + + """ + + def __init__(self, *args, **kwargs): + self._timescale = None + self._duration = None + self._chapters = [] + super(MP4Chapters, self).__init__() + if args or kwargs: + self.load(*args, **kwargs) + + def __len__(self): + return self._chapters.__len__() + + def __getitem__(self, key): + return self._chapters.__getitem__(key) + + def load(self, atoms, fileobj): + try: + mvhd = atoms.path(b"moov", b"mvhd")[-1] + except KeyError as key: + return MP4MetadataError(key) + + self._parse_mvhd(mvhd, fileobj) + + if not self._timescale: + raise MP4MetadataError("Unable to get timescale") + + try: + chpl = atoms.path(b"moov", b"udta", b"chpl")[-1] + except KeyError as key: + return MP4MetadataError(key) + + self._parse_chpl(chpl, fileobj) + + @classmethod + def _can_load(cls, atoms): + return b"moov.udta.chpl" in atoms and b"moov.mvhd" in atoms + + def _parse_mvhd(self, atom, fileobj): + assert atom.name == b"mvhd" + + ok, data = atom.read(fileobj) + if not ok: + raise MP4StreamInfoError("Invalid mvhd") + + version = data[0] + + pos = 4 + if version == 0: + pos += 8 # created, modified + + self._timescale = struct.unpack(">l", data[pos:pos + 4])[0] + pos += 4 + + self._duration = struct.unpack(">l", data[pos:pos + 4])[0] + pos += 4 + elif version == 1: + pos += 16 # created, modified + + self._timescale = struct.unpack(">l", data[pos:pos + 4])[0] + pos += 4 + + self._duration = struct.unpack(">q", data[pos:pos + 8])[0] + pos += 8 + + def _parse_chpl(self, atom, fileobj): + assert atom.name == b"chpl" + + ok, data = atom.read(fileobj) + if not ok: + raise MP4StreamInfoError("Invalid atom") + + chapters = data[8] + + pos = 9 + for i in range(chapters): + start = struct.unpack(">Q", data[pos:pos + 8])[0] / 10000 + pos += 8 + + title_len = data[pos] + pos += 1 + + try: + title = data[pos:pos + title_len].decode() + except UnicodeDecodeError as e: + raise MP4MetadataError("chapter %d title: %s" % (i, e)) + pos += title_len + + self._chapters.append(Chapter(start / self._timescale, title)) + + def pprint(self): + chapters = ["%s %s" % (timedelta(seconds=chapter.start), chapter.title) + for chapter in self._chapters] + return "chapters=%s" % '\n '.join(chapters) + + class MP4Info(StreamInfo): """MP4Info() @@ -1004,7 +1123,7 @@ class MP4Info(StreamInfo): return # look at the first entry if there is one - entry_fileobj = cBytesIO(data[offset:]) + entry_fileobj = BytesIO(data[offset:]) try: entry_atom = Atom(entry_fileobj) except AtomError as e: @@ -1044,6 +1163,7 @@ class MP4(FileType): """ MP4Tags = MP4Tags + MP4Chapters = MP4Chapters _mimes = ["audio/mp4", "audio/x-m4a", "audio/mpeg4", "audio/aac"] @@ -1076,6 +1196,16 @@ class MP4(FileType): except Exception as err: reraise(MP4MetadataError, err, sys.exc_info()[2]) + if not MP4Chapters._can_load(atoms): + self.chapters = None + else: + try: + self.chapters = self.MP4Chapters(atoms, fileobj) + except error: + raise + except Exception as err: + reraise(MP4MetadataError, err, sys.exc_info()[2]) + @property def _padding(self): if self.tags is None: @@ -1088,6 +1218,28 @@ class MP4(FileType): super(MP4, self).save(*args, **kwargs) + def pprint(self): + """ + Returns: + text: stream information, comment key=value pairs and chapters. + """ + stream = "%s (%s)" % (self.info.pprint(), self.mime[0]) + try: + tags = self.tags.pprint() + except AttributeError: + pass + else: + stream += ((tags and "\n" + tags) or "") + + try: + chapters = self.chapters.pprint() + except AttributeError: + pass + else: + stream += "\n" + chapters + + return stream + def add_tags(self): if self.tags is None: self.tags = self.MP4Tags() diff --git a/libs/common/mutagen/mp4/_as_entry.py b/libs/common/mutagen/mp4/_as_entry.py index 15b7e6bc..e5013c42 100644 --- a/libs/common/mutagen/mp4/_as_entry.py +++ b/libs/common/mutagen/mp4/_as_entry.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2014 Christoph Reiter # # This program is free software; you can redistribute it and/or modify @@ -6,10 +5,10 @@ # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. -from mutagen._compat import cBytesIO, xrange +from io import BytesIO + from mutagen.aac import ProgramConfigElement from mutagen._util import BitReader, BitReaderError, cdata -from mutagen._compat import text_type from ._util import parse_full_atom from ._atom import Atom, AtomError @@ -47,7 +46,7 @@ class AudioSampleEntry(object): if not ok: raise ASEntryError("too short %r atom" % atom.name) - fileobj = cBytesIO(data) + fileobj = BytesIO(data) r = BitReader(fileobj) try: @@ -93,7 +92,7 @@ class AudioSampleEntry(object): ok, data = atom.read(fileobj) if not ok: raise ASEntryError("truncated %s atom" % atom.name) - fileobj = cBytesIO(data) + fileobj = BytesIO(data) r = BitReader(fileobj) # sample_rate in AudioSampleEntry covers values in @@ -134,7 +133,7 @@ class AudioSampleEntry(object): if version != 0: raise ASEntryError("Unsupported version %d" % version) - fileobj = cBytesIO(data) + fileobj = BytesIO(data) r = BitReader(fileobj) try: @@ -168,7 +167,7 @@ class AudioSampleEntry(object): if version != 0: raise ASEntryError("Unsupported version %d" % version) - fileobj = cBytesIO(data) + fileobj = BytesIO(data) r = BitReader(fileobj) try: @@ -204,14 +203,14 @@ class DescriptorError(Exception): class BaseDescriptor(object): - TAG = None + TAG: int @classmethod def _parse_desc_length_file(cls, fileobj): """May raise ValueError""" value = 0 - for i in xrange(4): + for i in range(4): try: b = cdata.uint8(fileobj.read(1)) except cdata.error as e: @@ -239,9 +238,13 @@ class BaseDescriptor(object): pos = fileobj.tell() instance = cls(fileobj, length) left = length - (fileobj.tell() - pos) - if left < 0: - raise DescriptorError("descriptor parsing read too much data") - fileobj.seek(left, 1) + if left > 0: + fileobj.seek(left, 1) + else: + # XXX: In case the instance length is shorted than the content + # assume the size is wrong and just continue parsing + # https://github.com/quodlibet/mutagen/issues/444 + pass return instance @@ -371,7 +374,7 @@ class DecoderSpecificInfo(BaseDescriptor): name += "+SBR" if self.psPresentFlag == 1: name += "+PS" - return text_type(name) + return str(name) @property def sample_rate(self): diff --git a/libs/common/mutagen/mp4/_atom.py b/libs/common/mutagen/mp4/_atom.py index cd43a1fe..e1bf1535 100644 --- a/libs/common/mutagen/mp4/_atom.py +++ b/libs/common/mutagen/mp4/_atom.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify @@ -8,7 +7,6 @@ import struct -from mutagen._compat import PY2 from mutagen._util import convert_error # This is not an exhaustive list of container atoms, but just the @@ -180,12 +178,8 @@ class Atoms(object): specifying the complete path ('moov.udta'). """ - if PY2: - if isinstance(names, basestring): - names = names.split(b".") - else: - if isinstance(names, bytes): - names = names.split(b".") + if isinstance(names, bytes): + names = names.split(b".") for child in self.atoms: if child.name == names[0]: diff --git a/libs/common/mutagen/mp4/_util.py b/libs/common/mutagen/mp4/_util.py index 43d81c82..b8e208a1 100644 --- a/libs/common/mutagen/mp4/_util.py +++ b/libs/common/mutagen/mp4/_util.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2014 Christoph Reiter # # This program is free software; you can redistribute it and/or modify diff --git a/libs/common/mutagen/musepack.py b/libs/common/mutagen/musepack.py index c966d939..944de6d5 100644 --- a/libs/common/mutagen/musepack.py +++ b/libs/common/mutagen/musepack.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2006 Lukas Lalinsky # Copyright (C) 2012 Christoph Reiter # @@ -19,11 +18,10 @@ __all__ = ["Musepack", "Open", "delete"] import struct -from ._compat import endswith, xrange from mutagen import StreamInfo from mutagen.apev2 import APEv2File, error, delete from mutagen.id3._util import BitPaddedInt -from mutagen._util import cdata, convert_error, intround +from mutagen._util import cdata, convert_error, intround, endswith class MusepackHeaderError(error): @@ -44,7 +42,7 @@ def _parse_sv8_int(fileobj, limit=9): """ num = 0 - for i in xrange(limit): + for i in range(limit): c = fileobj.read(1) if len(c) != 1: raise EOFError @@ -143,9 +141,13 @@ class MusepackInfo(StreamInfo): # packets can be at maximum data_size big and are padded with zeros if frame_type == b"SH": + if frame_type not in mandatory_packets: + raise MusepackHeaderError("Duplicate SH packet") mandatory_packets.remove(frame_type) self.__parse_stream_header(fileobj, data_size) elif frame_type == b"RG": + if frame_type not in mandatory_packets: + raise MusepackHeaderError("Duplicate RG packet") mandatory_packets.remove(frame_type) self.__parse_replaygain_packet(fileobj, data_size) else: @@ -184,9 +186,13 @@ class MusepackInfo(StreamInfo): remaining_size -= l1 + l2 data = fileobj.read(remaining_size) - if len(data) != remaining_size: + if len(data) != remaining_size or len(data) < 2: raise MusepackHeaderError("SH packet ended unexpectedly.") - self.sample_rate = RATES[bytearray(data)[0] >> 5] + rate_index = (bytearray(data)[0] >> 5) + try: + self.sample_rate = RATES[rate_index] + except IndexError: + raise MusepackHeaderError("Invalid sample rate") self.channels = (bytearray(data)[1] >> 4) + 1 def __parse_replaygain_packet(self, fileobj, data_size): diff --git a/libs/common/mutagen/ogg.py b/libs/common/mutagen/ogg.py index 22d7442c..659e6cd4 100644 --- a/libs/common/mutagen/ogg.py +++ b/libs/common/mutagen/ogg.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify @@ -19,10 +18,14 @@ http://www.xiph.org/ogg/doc/rfc3533.txt. import struct import sys import zlib +from io import BytesIO +from typing import Type from mutagen import FileType -from mutagen._util import cdata, resize_bytes, MutagenError, loadfile, seek_end -from ._compat import cBytesIO, reraise, chr_, izip, xrange +from mutagen._util import cdata, resize_bytes, MutagenError, loadfile, \ + seek_end, bchr, reraise +from mutagen._file import StreamInfo +from mutagen._tags import Tags class error(MutagenError): @@ -37,7 +40,7 @@ class OggPage(object): A page is a header of 26 bytes, followed by the length of the data, followed by the data. - The constructor is givin a file-like object pointing to the start + The constructor is given a file-like object pointing to the start of an Ogg page. After the constructor is finished it is pointing to the start of the next page. @@ -50,7 +53,7 @@ class OggPage(object): offset (`int` or `None`): offset this page was read from (default None) complete (`bool`): if the last packet on this page is complete (default True) - packets (List[`bytes`]): list of raw packet data (default []) + packets (list[bytes]): list of raw packet data (default []) Note that if 'complete' is false, the next page's 'continued' property must be true (so set both when constructing pages). @@ -145,11 +148,11 @@ class OggPage(object): lacing_data = [] for datum in self.packets: quot, rem = divmod(len(datum), 255) - lacing_data.append(b"\xff" * quot + chr_(rem)) + lacing_data.append(b"\xff" * quot + bchr(rem)) lacing_data = b"".join(lacing_data) if not self.complete and lacing_data.endswith(b"\x00"): lacing_data = lacing_data[:-1] - data.append(chr_(len(lacing_data))) + data.append(bchr(len(lacing_data))) data.append(lacing_data) data.extend(self.packets) data = b"".join(data) @@ -210,13 +213,13 @@ class OggPage(object): to logical stream 'serial'. Other pages will be ignored. fileobj must point to the start of a valid Ogg page; any - occuring after it and part of the specified logical stream + occurring after it and part of the specified logical stream will be numbered. No adjustment will be made to the data in the pages nor the granule position; only the page number, and so also the CRC. If an error occurs (e.g. non-Ogg data is found), fileobj will - be left pointing to the place in the stream the error occured, + be left pointing to the place in the stream the error occurred, but the invalid data will be left intact (since this function does not change the total file size). """ @@ -267,11 +270,12 @@ class OggPage(object): else: sequence += 1 - if page.continued: - packets[-1].append(page.packets[0]) - else: - packets.append([page.packets[0]]) - packets.extend([p] for p in page.packets[1:]) + if page.packets: + if page.continued: + packets[-1].append(page.packets[0]) + else: + packets.append([page.packets[0]]) + packets.extend([p] for p in page.packets[1:]) return [b"".join(p) for p in packets] @@ -387,8 +391,8 @@ class OggPage(object): # Number the new pages starting from the first old page. first = old_pages[0].sequence - for page, seq in izip(new_pages, - xrange(first, first + len(new_pages))): + for page, seq in zip(new_pages, + range(first, first + len(new_pages))): page.sequence = seq page.serial = old_pages[0].serial @@ -416,7 +420,7 @@ class OggPage(object): offset_adjust = 0 new_data_end = None assert len(old_pages) == len(new_data) - for old_page, data in izip(old_pages, new_data): + for old_page, data in zip(old_pages, new_data): offset = old_page.offset + offset_adjust data_size = len(data) resize_bytes(fileobj, old_page.size, data_size, offset) @@ -460,7 +464,7 @@ class OggPage(object): index = data.rindex(b"OggS") except ValueError: raise error("unable to find final Ogg header") - bytesobj = cBytesIO(data[index:]) + bytesobj = BytesIO(data[index:]) def is_valid(page): return not finishing or page.position != -1 @@ -506,9 +510,9 @@ class OggFileType(FileType): filething (filething) """ - _Info = None - _Tags = None - _Error = None + _Info: Type[StreamInfo] + _Tags: Type[Tags] + _Error: Type[error] _mimes = ["application/ogg", "application/x-ogg"] @loadfile() @@ -535,7 +539,7 @@ class OggFileType(FileType): raise self._Error("no appropriate stream found") @loadfile(writable=True) - def delete(self, filething): + def delete(self, filething=None): """delete(filething=None) Remove tags from a file. @@ -567,7 +571,7 @@ class OggFileType(FileType): raise self._Error @loadfile(writable=True) - def save(self, filething, padding=None): + def save(self, filething=None, padding=None): """save(filething=None, padding=None) Save a tag to a file. diff --git a/libs/common/mutagen/oggflac.py b/libs/common/mutagen/oggflac.py index bc073094..9a4ce3e7 100644 --- a/libs/common/mutagen/oggflac.py +++ b/libs/common/mutagen/oggflac.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify @@ -18,8 +17,7 @@ http://flac.sourceforge.net/ogg_mapping.html. __all__ = ["OggFLAC", "Open", "delete"] import struct - -from ._compat import cBytesIO +from io import BytesIO from mutagen import StreamInfo from mutagen.flac import StreamInfo as FLACStreamInfo, error as FLACError @@ -65,7 +63,7 @@ class OggFLACStreamInfo(StreamInfo): self.serial = page.serial # Skip over the block header. - stringobj = cBytesIO(page.packets[0][17:]) + stringobj = BytesIO(page.packets[0][17:]) try: flac_info = FLACStreamInfo(stringobj) @@ -101,7 +99,7 @@ class OggFLACVComment(VCommentDict): if page.serial == info.serial: pages.append(page) complete = page.complete or (len(page.packets) > 1) - comment = cBytesIO(OggPage.to_packets(pages)[0][4:]) + comment = BytesIO(OggPage.to_packets(pages)[0][4:]) super(OggFLACVComment, self).__init__(comment, framing=False) def _inject(self, fileobj, padding_func): diff --git a/libs/common/mutagen/oggopus.py b/libs/common/mutagen/oggopus.py index df9c32e8..486b5853 100644 --- a/libs/common/mutagen/oggopus.py +++ b/libs/common/mutagen/oggopus.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2012, 2013 Christoph Reiter # # This program is free software; you can redistribute it and/or modify @@ -17,9 +16,9 @@ Based on http://tools.ietf.org/html/draft-terriberry-oggopus-01 __all__ = ["OggOpus", "Open", "delete"] import struct +from io import BytesIO from mutagen import StreamInfo -from mutagen._compat import BytesIO from mutagen._util import get_size, loadfile, convert_error from mutagen._tags import PaddingInfo from mutagen._vorbis import VCommentDict diff --git a/libs/common/mutagen/oggspeex.py b/libs/common/mutagen/oggspeex.py index de02a449..a3edb247 100644 --- a/libs/common/mutagen/oggspeex.py +++ b/libs/common/mutagen/oggspeex.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify diff --git a/libs/common/mutagen/oggtheora.py b/libs/common/mutagen/oggtheora.py index a18dfd53..51f0a451 100644 --- a/libs/common/mutagen/oggtheora.py +++ b/libs/common/mutagen/oggtheora.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify @@ -50,17 +49,22 @@ class OggTheoraInfo(StreamInfo): def __init__(self, fileobj): page = OggPage(fileobj) - while not page.packets[0].startswith(b"\x80theora"): + while not page.packets or \ + not page.packets[0].startswith(b"\x80theora"): page = OggPage(fileobj) if not page.first: raise OggTheoraHeaderError( "page has ID header, but doesn't start a stream") data = page.packets[0] + if len(data) < 42: + raise OggTheoraHeaderError("Truncated header") vmaj, vmin = struct.unpack("2B", data[7:9]) if (vmaj, vmin) != (3, 2): raise OggTheoraHeaderError( "found Theora version %d.%d != 3.2" % (vmaj, vmin)) fps_num, fps_den = struct.unpack(">2I", data[22:30]) + if not fps_den or not fps_num: + raise OggTheoraHeaderError("FRN or FRD is equal to zero") self.fps = fps_num / float(fps_den) self.bitrate = cdata.uint_be(b"\x00" + data[37:40]) self.granule_shift = (cdata.ushort_be(data[40:42]) >> 5) & 0x1F @@ -73,6 +77,7 @@ class OggTheoraInfo(StreamInfo): position = page.position mask = (1 << self.granule_shift) - 1 frames = (position >> self.granule_shift) + (position & mask) + assert self.fps self.length = frames / float(self.fps) def pprint(self): @@ -91,7 +96,10 @@ class OggTheoraCommentDict(VCommentDict): if page.serial == info.serial: pages.append(page) complete = page.complete or (len(page.packets) > 1) - data = OggPage.to_packets(pages)[0][7:] + packets = OggPage.to_packets(pages) + if not packets: + raise error("Missing metadata packet") + data = packets[0][7:] super(OggTheoraCommentDict, self).__init__(data, framing=False) self._padding = len(data) - self._size @@ -100,7 +108,8 @@ class OggTheoraCommentDict(VCommentDict): fileobj.seek(0) page = OggPage(fileobj) - while not page.packets[0].startswith(b"\x81theora"): + while not page.packets or \ + not page.packets[0].startswith(b"\x81theora"): page = OggPage(fileobj) old_pages = [page] diff --git a/libs/common/mutagen/oggvorbis.py b/libs/common/mutagen/oggvorbis.py index e3292ae3..c1c90732 100644 --- a/libs/common/mutagen/oggvorbis.py +++ b/libs/common/mutagen/oggvorbis.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify @@ -43,7 +42,7 @@ class OggVorbisInfo(StreamInfo): length (`float`): File length in seconds, as a float channels (`int`): Number of channels bitrate (`int`): Nominal ('average') bitrate in bits per second - sample_Rate (`int`): Sample rate in Hz + sample_rate (`int`): Sample rate in Hz """ @@ -56,13 +55,20 @@ class OggVorbisInfo(StreamInfo): """Raises ogg.error, IOError""" page = OggPage(fileobj) + if not page.packets: + raise OggVorbisHeaderError("page has not packets") while not page.packets[0].startswith(b"\x01vorbis"): page = OggPage(fileobj) if not page.first: raise OggVorbisHeaderError( "page has ID header, but doesn't start a stream") + if len(page.packets[0]) < 28: + raise OggVorbisHeaderError( + "page contains a packet too short to be valid") (self.channels, self.sample_rate, max_bitrate, nominal_bitrate, - min_bitrate) = struct.unpack("= 15: + encoder_id = struct.unpack("> 4) + 4500) + self.encoder_info = "%s.%s" % (version[0], version[1:]) + else: + self.encoder_info = "" def pprint(self): return u"OptimFROG, %.2f seconds, %d Hz" % (self.length, diff --git a/libs/common/mutagen/py.typed b/libs/common/mutagen/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/common/mutagen/smf.py b/libs/common/mutagen/smf.py index f41d8142..5d664085 100644 --- a/libs/common/mutagen/smf.py +++ b/libs/common/mutagen/smf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2015 Christoph Reiter # # This program is free software; you can redistribute it and/or modify @@ -12,8 +11,7 @@ import struct from mutagen import StreamInfo, MutagenError from mutagen._file import FileType -from mutagen._util import loadfile -from mutagen._compat import xrange, endswith +from mutagen._util import loadfile, endswith class SMFError(MutagenError): @@ -123,7 +121,7 @@ def _read_midi_length(fileobj): # get a list of events and tempo changes for each track tracks = [] first_tempos = None - for tracknum in xrange(ntracks): + for tracknum in range(ntracks): identifier, chunk = read_chunk(fileobj) if identifier != b"MTrk": continue diff --git a/libs/common/mutagen/tak.py b/libs/common/mutagen/tak.py new file mode 100644 index 00000000..fefe7b71 --- /dev/null +++ b/libs/common/mutagen/tak.py @@ -0,0 +1,237 @@ +# Copyright (C) 2008 Lukáš Lalinský +# Copyright (C) 2019 Philipp Wolfer +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +"""Tom's lossless Audio Kompressor (TAK) streams with APEv2 tags. + +TAK is a lossless audio compressor developed by Thomas Becker. + +For more information, see: + +* http://www.thbeck.de/Tak/Tak.html +* http://wiki.hydrogenaudio.org/index.php?title=TAK +""" + +__all__ = ["TAK", "Open", "delete"] + +import struct + +from mutagen import StreamInfo +from mutagen.apev2 import ( + APEv2File, + delete, + error, +) +from mutagen._util import ( + BitReader, + BitReaderError, + convert_error, + enum, + endswith, +) + + +@enum +class TAKMetadata(object): + END = 0 + STREAM_INFO = 1 + SEEK_TABLE = 2 # Removed in TAK 1.1.1 + SIMPLE_WAVE_DATA = 3 + ENCODER_INFO = 4 + UNUSED_SPACE = 5 # New in TAK 1.0.3 + MD5 = 6 # New in TAK 1.1.1 + LAST_FRAME_INFO = 7 # New in TAK 1.1.1 + + +CRC_SIZE = 3 + +ENCODER_INFO_CODEC_BITS = 6 +ENCODER_INFO_PROFILE_BITS = 4 +ENCODER_INFO_TOTAL_BITS = ENCODER_INFO_CODEC_BITS + ENCODER_INFO_PROFILE_BITS + +SIZE_INFO_FRAME_DURATION_BITS = 4 +SIZE_INFO_SAMPLE_NUM_BITS = 35 +SIZE_INFO_TOTAL_BITS = (SIZE_INFO_FRAME_DURATION_BITS + + SIZE_INFO_SAMPLE_NUM_BITS) + +AUDIO_FORMAT_DATA_TYPE_BITS = 3 +AUDIO_FORMAT_SAMPLE_RATE_BITS = 18 +AUDIO_FORMAT_SAMPLE_BITS_BITS = 5 +AUDIO_FORMAT_CHANNEL_NUM_BITS = 4 +AUDIO_FORMAT_HAS_EXTENSION_BITS = 1 +AUDIO_FORMAT_BITS_MIN = 31 +AUDIO_FORMAT_BITS_MAX = 31 + 102 + +SAMPLE_RATE_MIN = 6000 +SAMPLE_BITS_MIN = 8 +CHANNEL_NUM_MIN = 1 + +STREAM_INFO_BITS_MIN = (ENCODER_INFO_TOTAL_BITS + + SIZE_INFO_TOTAL_BITS + + AUDIO_FORMAT_BITS_MIN) +STREAM_INFO_BITS_MAX = (ENCODER_INFO_TOTAL_BITS + + SIZE_INFO_TOTAL_BITS + + AUDIO_FORMAT_BITS_MAX) +STREAM_INFO_SIZE_MIN = (STREAM_INFO_BITS_MIN + 7) / 8 +STREAM_INFO_SIZE_MAX = (STREAM_INFO_BITS_MAX + 7) / 8 + + +class _LSBBitReader(BitReader): + """BitReader implementation which reads bits starting at LSB in each byte. + """ + + def _lsb(self, count): + value = self._buffer & 0xff >> (8 - count) + self._buffer = self._buffer >> count + self._bits -= count + return value + + def bits(self, count): + """Reads `count` bits and returns an uint, LSB read first. + + May raise BitReaderError if not enough data could be read or + IOError by the underlying file object. + """ + if count < 0: + raise ValueError + + value = 0 + if count <= self._bits: + value = self._lsb(count) + else: + # First read all available bits + shift = 0 + remaining = count + if self._bits > 0: + remaining -= self._bits + shift = self._bits + value = self._lsb(self._bits) + assert self._bits == 0 + + # Now add additional bytes + n_bytes = (remaining - self._bits + 7) // 8 + data = self._fileobj.read(n_bytes) + if len(data) != n_bytes: + raise BitReaderError("not enough data") + for b in bytearray(data): + if remaining > 8: # Use full byte + remaining -= 8 + value = (b << shift) | value + shift += 8 + else: + self._buffer = b + self._bits = 8 + b = self._lsb(remaining) + value = (b << shift) | value + + assert 0 <= self._bits < 8 + return value + + +class TAKHeaderError(error): + pass + + +class TAKInfo(StreamInfo): + + """TAK stream information. + + Attributes: + channels (`int`): number of audio channels + length (`float`): file length in seconds, as a float + sample_rate (`int`): audio sampling rate in Hz + bits_per_sample (`int`): audio sample size + encoder_info (`mutagen.text`): encoder version + """ + + channels = 0 + length = 0 + sample_rate = 0 + bitrate = 0 + encoder_info = "" + + @convert_error(IOError, TAKHeaderError) + @convert_error(BitReaderError, TAKHeaderError) + def __init__(self, fileobj): + stream_id = fileobj.read(4) + if len(stream_id) != 4 or not stream_id == b"tBaK": + raise TAKHeaderError("not a TAK file") + + bitreader = _LSBBitReader(fileobj) + while True: + type = TAKMetadata(bitreader.bits(7)) + bitreader.skip(1) # Unused + size = struct.unpack(" 0: + self.length = self.number_of_samples / float(self.sample_rate) + + def _parse_stream_info(self, bitreader, size): + if size < STREAM_INFO_SIZE_MIN or size > STREAM_INFO_SIZE_MAX: + raise TAKHeaderError("stream info has invalid length") + + # Encoder Info + bitreader.skip(ENCODER_INFO_CODEC_BITS) + bitreader.skip(ENCODER_INFO_PROFILE_BITS) + + # Size Info + bitreader.skip(SIZE_INFO_FRAME_DURATION_BITS) + self.number_of_samples = bitreader.bits(SIZE_INFO_SAMPLE_NUM_BITS) + + # Audio Format + bitreader.skip(AUDIO_FORMAT_DATA_TYPE_BITS) + self.sample_rate = (bitreader.bits(AUDIO_FORMAT_SAMPLE_RATE_BITS) + + SAMPLE_RATE_MIN) + self.bits_per_sample = (bitreader.bits(AUDIO_FORMAT_SAMPLE_BITS_BITS) + + SAMPLE_BITS_MIN) + self.channels = (bitreader.bits(AUDIO_FORMAT_CHANNEL_NUM_BITS) + + CHANNEL_NUM_MIN) + bitreader.skip(AUDIO_FORMAT_HAS_EXTENSION_BITS) + + def _parse_encoder_info(self, bitreader, size): + patch = bitreader.bits(8) + minor = bitreader.bits(8) + major = bitreader.bits(8) + self.encoder_info = "TAK %d.%d.%d" % (major, minor, patch) + + def pprint(self): + return u"%s, %d Hz, %d bits, %.2f seconds, %d channel(s)" % ( + self.encoder_info or "TAK", self.sample_rate, self.bits_per_sample, + self.length, self.channels) + + +class TAK(APEv2File): + """TAK(filething) + + Arguments: + filething (filething) + + Attributes: + info (`TAKInfo`) + """ + + _Info = TAKInfo + _mimes = ["audio/x-tak"] + + @staticmethod + def score(filename, fileobj, header): + return header.startswith(b"tBaK") + endswith(filename.lower(), ".tak") + + +Open = TAK diff --git a/libs/common/mutagen/trueaudio.py b/libs/common/mutagen/trueaudio.py index e62f4556..0d1e4a57 100644 --- a/libs/common/mutagen/trueaudio.py +++ b/libs/common/mutagen/trueaudio.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (C) 2006 Joe Wreschnig # # This program is free software; you can redistribute it and/or modify @@ -17,10 +16,9 @@ True Audio files use ID3 tags. __all__ = ["TrueAudio", "Open", "delete", "EasyTrueAudio"] -from ._compat import endswith from mutagen import StreamInfo from mutagen.id3 import ID3FileType, delete -from mutagen._util import cdata, MutagenError, convert_error +from mutagen._util import cdata, MutagenError, convert_error, endswith class error(MutagenError): @@ -99,4 +97,4 @@ class EasyTrueAudio(TrueAudio): """ from mutagen.easyid3 import EasyID3 as ID3 - ID3 = ID3 + ID3 = ID3 # type: ignore diff --git a/libs/common/mutagen/wave.py b/libs/common/mutagen/wave.py new file mode 100644 index 00000000..4e0c55f9 --- /dev/null +++ b/libs/common/mutagen/wave.py @@ -0,0 +1,209 @@ +# Copyright (C) 2017 Borewit +# Copyright (C) 2019-2020 Philipp Wolfer +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +"""Microsoft WAVE/RIFF audio file/stream information and tags.""" + +import sys +import struct + +from mutagen import StreamInfo, FileType + +from mutagen.id3 import ID3 +from mutagen._riff import RiffFile, InvalidChunk +from mutagen._iff import error as IffError +from mutagen.id3._util import ID3NoHeaderError, error as ID3Error +from mutagen._util import ( + convert_error, + endswith, + loadfile, + reraise, +) + +__all__ = ["WAVE", "Open", "delete"] + + +class error(IffError): + """WAVE stream parsing errors.""" + + +class _WaveFile(RiffFile): + """Representation of a RIFF/WAVE file""" + + def __init__(self, fileobj): + RiffFile.__init__(self, fileobj) + + if self.file_type != u'WAVE': + raise error("Expected RIFF/WAVE.") + + # Normalize ID3v2-tag-chunk to lowercase + if u'ID3' in self: + self[u'ID3'].id = u'id3' + + +class WaveStreamInfo(StreamInfo): + """WaveStreamInfo() + + Microsoft WAVE file information. + + Information is parsed from the 'fmt' & 'data'chunk of the RIFF/WAVE file + + Attributes: + length (`float`): audio length, in seconds + bitrate (`int`): audio bitrate, in bits per second + channels (`int`): The number of audio channels + sample_rate (`int`): audio sample rate, in Hz + bits_per_sample (`int`): The audio sample size + """ + + length = 0.0 + bitrate = 0 + channels = 0 + sample_rate = 0 + bits_per_sample = 0 + + SIZE = 16 + + @convert_error(IOError, error) + def __init__(self, fileobj): + """Raises error""" + + wave_file = _WaveFile(fileobj) + try: + format_chunk = wave_file[u'fmt'] + except KeyError as e: + raise error(str(e)) + + data = format_chunk.read() + if len(data) < 16: + raise InvalidChunk() + + # RIFF: http://soundfile.sapp.org/doc/WaveFormat/ + # Python struct.unpack: + # https://docs.python.org/2/library/struct.html#byte-order-size-and-alignment + info = struct.unpack(' 0: + try: + data_chunk = wave_file[u'data'] + self._number_of_samples = data_chunk.data_size / block_align + except KeyError: + pass + + if self.sample_rate > 0: + self.length = self._number_of_samples / self.sample_rate + + def pprint(self): + return u"%d channel RIFF @ %d bps, %s Hz, %.2f seconds" % ( + self.channels, self.bitrate, self.sample_rate, self.length) + + +class _WaveID3(ID3): + """A Wave file with ID3v2 tags""" + + def _pre_load_header(self, fileobj): + try: + fileobj.seek(_WaveFile(fileobj)[u'id3'].data_offset) + except (InvalidChunk, KeyError): + raise ID3NoHeaderError("No ID3 chunk") + + @convert_error(IOError, error) + @loadfile(writable=True) + def save(self, filething, v1=1, v2_version=4, v23_sep='/', padding=None): + """Save ID3v2 data to the Wave/RIFF file""" + + fileobj = filething.fileobj + wave_file = _WaveFile(fileobj) + + if u'id3' not in wave_file: + wave_file.insert_chunk(u'id3') + + chunk = wave_file[u'id3'] + + try: + data = self._prepare_data( + fileobj, chunk.data_offset, chunk.data_size, v2_version, + v23_sep, padding) + except ID3Error as e: + reraise(error, e, sys.exc_info()[2]) + + chunk.resize(len(data)) + chunk.write(data) + + def delete(self, filething): + """Completely removes the ID3 chunk from the RIFF/WAVE file""" + + delete(filething) + self.clear() + + +@convert_error(IOError, error) +@loadfile(method=False, writable=True) +def delete(filething): + """Completely removes the ID3 chunk from the RIFF/WAVE file""" + + try: + _WaveFile(filething.fileobj).delete_chunk(u'id3') + except KeyError: + pass + + +class WAVE(FileType): + """WAVE(filething) + + A Waveform Audio File Format + (WAVE, or more commonly known as WAV due to its filename extension) + + Arguments: + filething (filething) + + Attributes: + tags (`mutagen.id3.ID3`) + info (`WaveStreamInfo`) + """ + + _mimes = ["audio/wav", "audio/wave"] + + @staticmethod + def score(filename, fileobj, header): + filename = filename.lower() + + return (header.startswith(b"RIFF") + (header[8:12] == b'WAVE') + + endswith(filename, b".wav") + endswith(filename, b".wave")) + + def add_tags(self): + """Add an empty ID3 tag to the file.""" + if self.tags is None: + self.tags = _WaveID3() + else: + raise error("an ID3 tag already exists") + + @convert_error(IOError, error) + @loadfile() + def load(self, filething, **kwargs): + """Load stream and tag information from a file.""" + + fileobj = filething.fileobj + self.info = WaveStreamInfo(fileobj) + fileobj.seek(0, 0) + + try: + self.tags = _WaveID3(fileobj, **kwargs) + except ID3NoHeaderError: + self.tags = None + except ID3Error as e: + raise error(e) + else: + self.tags.filename = self.filename + + +Open = WAVE diff --git a/libs/common/mutagen/wavpack.py b/libs/common/mutagen/wavpack.py index 290b90c3..c2515eef 100644 --- a/libs/common/mutagen/wavpack.py +++ b/libs/common/mutagen/wavpack.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2006 Joe Wreschnig # 2014 Christoph Reiter # @@ -76,9 +75,10 @@ class WavPackInfo(StreamInfo): Attributes: channels (int): number of audio channels (1 or 2) - length (float: file length in seconds, as a float + length (float): file length in seconds, as a float sample_rate (int): audio sampling rate in Hz - version (int) WavPack stream version + bits_per_sample (int): audio sample size + version (int): WavPack stream version """ def __init__(self, fileobj): @@ -90,6 +90,12 @@ class WavPackInfo(StreamInfo): self.version = header.version self.channels = bool(header.flags & 4) or 2 self.sample_rate = RATES[(header.flags >> 23) & 0xF] + self.bits_per_sample = ((header.flags & 3) + 1) * 8 + + # most common multiplier (DSD64) + if (header.flags >> 31) & 1: + self.sample_rate *= 4 + self.bits_per_sample = 1 if header.total_samples == -1 or header.block_index != 0: # TODO: we could make this faster by using the tag size @@ -114,6 +120,15 @@ class WavPackInfo(StreamInfo): class WavPack(APEv2File): + """WavPack(filething) + + Arguments: + filething (filething) + + Attributes: + info (`WavPackInfo`) + """ + _Info = WavPackInfo _mimes = ["audio/x-wavpack"] diff --git a/libs/common/share/man/man1/mid3cp.1 b/libs/common/share/man/man1/mid3cp.1 new file mode 100644 index 00000000..02c97f96 --- /dev/null +++ b/libs/common/share/man/man1/mid3cp.1 @@ -0,0 +1,66 @@ +.\" Man page generated from reStructuredText. +. +.TH MID3CP 1 "" "" "" +.SH NAME +mid3cp \- copy ID3 tags +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +\fBmid3cp\fP [\fIoptions\fP] \fIsource\fP \fIdest\fP +.SH DESCRIPTION +.sp +\fBmid3cp\fP copies the ID3 tags from a source file to a destination file. +.sp +It is designed to provide similar functionality to id3lib\(aqs id3cp tool, and can +optionally write ID3v1 tags. It can also exclude specific tags from being +copied. +.SH OPTIONS +.INDENT 0.0 +.TP +.B \-\-verbose\fP,\fB \-v +Be verbose: state all operations performed, and list tags in source file. +.TP +.B \-\-write\-v1 +Write ID3v1 tags to the destination file, derived from the ID3v2 tags. +.TP +.B \-\-exclude\-tag\fP,\fB \-x +Exclude a specific tag from being copied. Can be specified multiple times. +.TP +.B \-\-merge +Copy over frames instead of replacing the whole ID3 tag. The tag version +of \fIdest\fP will be used. In case \fIdest\fP has no ID3 tag this option has no +effect. +.UNINDENT +.SH AUTHOR +.sp +Marcus Sundman. +.sp +Based on id3cp (part of id3lib) by Dirk Mahoney and Scott Thomas Haug. +.\" Generated by docutils manpage writer. +. diff --git a/libs/common/share/man/man1/mid3iconv.1 b/libs/common/share/man/man1/mid3iconv.1 new file mode 100644 index 00000000..c6916b09 --- /dev/null +++ b/libs/common/share/man/man1/mid3iconv.1 @@ -0,0 +1,68 @@ +.\" Man page generated from reStructuredText. +. +.TH MID3ICONV 1 "" "" "" +.SH NAME +mid3iconv \- convert ID3 tag encodings +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +\fBmid3iconv\fP [\fIoptions\fP] \fIfilename\fP ... +.SH DESCRIPTION +.sp +\fBmid3iconv\fP converts ID3 tags from legacy encodings to Unicode and stores +them using the ID3v2 format. +.SH OPTIONS +.INDENT 0.0 +.TP +.B \-\-debug\fP,\fB \-d +Print updated tags +.TP +.B \-\-dry\-run\fP,\fB \-p +Do not actually modify files +.TP +.B \-\-encoding\fP,\fB \-e +Convert from this encoding. By default, your locale\(aqs default encoding is +used. +.TP +.B \-\-force\-v1 +Use an ID3v1 tag even if an ID3v2 tag is present +.TP +.B \-\-quiet\fP,\fB \-q +Only output errors +.TP +.B \-\-remove\-v1 +Remove any ID3v1 tag after processing the files +.UNINDENT +.SH AUTHOR +.sp +Emfox Zhou. +.sp +Based on id3iconv (\fI\%http://www.cs.berkeley.edu/~zf/id3iconv/\fP) by Feng Zhou. +.\" Generated by docutils manpage writer. +. diff --git a/libs/common/share/man/man1/mid3v2.1 b/libs/common/share/man/man1/mid3v2.1 new file mode 100644 index 00000000..fd5f866c --- /dev/null +++ b/libs/common/share/man/man1/mid3v2.1 @@ -0,0 +1,151 @@ +.\" Man page generated from reStructuredText. +. +.TH MID3V2 1 "" "" "" +.SH NAME +mid3v2 \- audio tag editor similar to 'id3v2' +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +\fBmid3v2\fP [\fIoptions\fP] \fIfilename\fP ... +.SH DESCRIPTION +.sp +\fBmid3v2\fP is a Mutagen\-based replacement for id3lib\(aqs id3v2. It supports +ID3v2.4 and more frames; it also does not have the numerous bugs that plague +id3v2. +.sp +This program exists mostly for compatibility with programs that want to tag +files using id3v2. For a more usable interface, we recommend Ex Falso. +.SH OPTIONS +.INDENT 0.0 +.TP +.B \-q\fP,\fB \-\-quiet +Be quiet: do not mention file operations that perform the user\(aqs +request. Warnings will still be printed. +.TP +.B \-v\fP,\fB \-\-verbose +Be verbose: state all operations performed. This is the opposite of +\-\-quiet. This is the default. +.TP +.B \-e\fP,\fB \-\-escape +Enable interpretation of backslash escapes for tag values. +Makes it possible to escape the colon\-separator in TXXX, WXXX, COMM +values like \(aq\e:\(aq and insert escape sequences like \(aq\en\(aq, \(aq\et\(aq etc. +.TP +.B \-f\fP,\fB \-\-list\-frames +Display all supported ID3v2.3/2.4 frames and their meanings. +.TP +.B \-L\fP,\fB \-\-list\-genres +List all ID3v1 numeric genres. These can be used to set TCON frames, +but it is not recommended. +.TP +.B \-l\fP,\fB \-\-list +List all tags in the files. The output format is \fInot\fP the same as +id3v2\(aqs; instead, it is easily parsable and readable. Some tags may not +have human\-readable representations. +.TP +.B \-\-list\-raw +List all tags in the files, in raw format. Although this format is +nominally human\-readable, it may be very long if the tag contains +embedded binary data. +.TP +.B \-d\fP,\fB \-\-delete\-v2 +Delete ID3v2 tags. +.TP +.B \-s\fP,\fB \-\-delete\-v1 +Delete ID3v1 tags. +.TP +.B \-D\fP,\fB \-\-delete\-all +Delete all ID3 tags. +.TP +.BI \-\-delete\-frames\fB= FRAMES +Delete specific ID3v2 frames (or groups of frames) from the files. +\fBFRAMES\fP is a "," separated list of frame names e.g. \fB"TPE1,TALB"\fP +.TP +.B \-C\fP,\fB \-\-convert +Convert ID3v1 tags to ID3v2 tags. This will also happen automatically +during any editing. +.TP +.BI \-a\fP,\fB \-\-artist\fB= ARTIST +Set the artist information (TPE1). +.TP +.BI \-A\fP,\fB \-\-album\fB= ALBUM +Set the album information (TALB). +.TP +.BI \-t\fP,\fB \-\-song\fB= TITLE +Set the title information (TIT2). +.TP +.BI \-c\fP,\fB \-\-comment\fB= +Set a comment (COMM). The language and description may be omitted, in +which case the language defaults to English, and the description to an +empty string. +.TP +.BI \-p\fP,\fB \-\-picture\fB= +Set the attached picture (APIC). Everything except the filename can be +omitted in which case default values will be used. +.TP +.BI \-g\fP,\fB \-\-genre\fB= GENRE +Set the genre information (TCON). +.TP +.BI \-y\fP,\fB \-\-year\fB= \fP,\fB \ \-\-date\fB= +Set the year/date information (TDRC). +.TP +.BI \-T\fP,\fB \-\-track\fB= +Set the track number (TRCK). +.UNINDENT +.sp +Any text or URL frame (those beginning with T or W) can be modified or +added by prefixing the name of the frame with "\-\-". For example, \fB\-\-TIT3 +"Monkey!"\fP will set the TIT3 (subtitle) frame to \fBMonkey!\fP\&. +.sp +The TXXX frame has the format ; many TXXX frames may be +set in the file as long as they have different keys. To set this key, just +separate the text with a colon, e.g. \fB\-\-TXXX "ALBUMARTISTSORT:Examples, +The"\fP\&. The description can be omitted in which case it defaults to an empty +string. +.sp +The WXXX frame has the same format as TXXX but since URLs usually contain a +":" you have provide a description or enable escaping (\-e): +\fB\-\-WXXX "desc:http://foo.bar"\fP or \fB\-e \-\-WXXX "http\e\e://foo.bar"\fP +.sp +The USLT frame has the format . The language and +description may be omitted, in which case the language defaults to English, +and the description to an empty string. +.sp +The special POPM frame can be set in a similar way: \fB\-\-POPM +"bob@example.com:128:2"\fP to set Bob\(aqs rating to 128/255 with 2 plays. +.SH BUGS +.sp +No sanity checking is done on the editing operations you perform, so mid3v2 +will happily accept \-\-TSIZ when editing an ID3v2.4 frame. However, it will +also automatically throw it out during the next edit operation. +.SH AUTHOR +.sp +Joe Wreschnig is the author of mid3v2, but he doesn\(aqt like to admit it. +.\" Generated by docutils manpage writer. +. diff --git a/libs/common/share/man/man1/moggsplit.1 b/libs/common/share/man/man1/moggsplit.1 new file mode 100644 index 00000000..5ca456cf --- /dev/null +++ b/libs/common/share/man/man1/moggsplit.1 @@ -0,0 +1,64 @@ +.\" Man page generated from reStructuredText. +. +.TH MOGGSPLIT 1 "" "" "" +.SH NAME +moggsplit \- split Ogg logical streams +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +\fBmoggsplit\fP \fIfilename\fP ... +.SH DESCRIPTION +.sp +\fBmoggsplit\fP splits a multiplexed Ogg stream into separate files. For +example, it can separate an OGM into separate Ogg DivX and Ogg Vorbis +streams, or a chained Ogg Vorbis file into two separate files. +.SH OPTIONS +.INDENT 0.0 +.TP +.B \-\-extension +Use the supplied extension when generating new files; the default is +\fBogg\fP\&. +.TP +.B \-\-pattern +Use the supplied pattern when generating new files. This is a Python +keyword format string with three variables, \fIbase\fP for the original +file\(aqs base name, \fIstream\fP for the stream\(aqs serial number, and ext for +the extension give by \fB\-\-extension\fP\&. +.sp +The default is \fB%(base)s\-%(stream)d.%(ext)s\fP\&. +.TP +.B \-\-m3u +Generate an m3u playlist along with the newly generated files. Useful +for large chained Oggs. +.UNINDENT +.SH AUTHOR +.sp +Joe Wreschnig +.\" Generated by docutils manpage writer. +. diff --git a/libs/common/share/man/man1/mutagen-inspect.1 b/libs/common/share/man/man1/mutagen-inspect.1 new file mode 100644 index 00000000..6d4dc42b --- /dev/null +++ b/libs/common/share/man/man1/mutagen-inspect.1 @@ -0,0 +1,47 @@ +.\" Man page generated from reStructuredText. +. +.TH MUTAGEN-INSPECT 1 "" "" "" +.SH NAME +mutagen-inspect \- view Mutagen-supported audio tags +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +\fBmutagen\-inspect\fP \fIfilename\fP ... +.SH DESCRIPTION +.sp +\fBmutagen\-inspect\fP loads and prints information about an audio file and +its tags. +.sp +It is primarily intended as a debugging tool for Mutagen, but can be useful +for extracting tags from the command line. +.SH AUTHOR +.sp +Joe Wreschnig +.\" Generated by docutils manpage writer. +. diff --git a/libs/common/share/man/man1/mutagen-pony.1 b/libs/common/share/man/man1/mutagen-pony.1 new file mode 100644 index 00000000..b603fc3a --- /dev/null +++ b/libs/common/share/man/man1/mutagen-pony.1 @@ -0,0 +1,46 @@ +.\" Man page generated from reStructuredText. +. +.TH MUTAGEN-PONY 1 "" "" "" +.SH NAME +mutagen-pony \- scan a collection of MP3 files +. +.nr rst2man-indent-level 0 +. +.de1 rstReportMargin +\\$1 \\n[an-margin] +level \\n[rst2man-indent-level] +level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] +- +\\n[rst2man-indent0] +\\n[rst2man-indent1] +\\n[rst2man-indent2] +.. +.de1 INDENT +.\" .rstReportMargin pre: +. RS \\$1 +. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] +. nr rst2man-indent-level +1 +.\" .rstReportMargin post: +.. +.de UNINDENT +. RE +.\" indent \\n[an-margin] +.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] +.nr rst2man-indent-level -1 +.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] +.in \\n[rst2man-indent\\n[rst2man-indent-level]]u +.. +.SH SYNOPSIS +.sp +\fBmutagen\-pony\fP \fIdirectory\fP ... +.SH DESCRIPTION +.sp +\fBmutagen\-pony\fP scans any directories given and reports on the kinds of +tags in the MP3s it finds in them. Ride the pony. +.sp +It is primarily intended as a debugging tool for Mutagen. +.SH AUTHORS +.sp +Michael Urman and Joe Wreschnig +.\" Generated by docutils manpage writer. +. diff --git a/libs/common/unidecode/__init__.py b/libs/common/unidecode/__init__.py index 5d968fdb..5633c458 100644 --- a/libs/common/unidecode/__init__.py +++ b/libs/common/unidecode/__init__.py @@ -3,35 +3,39 @@ """Transliterate Unicode text into plain 7-bit ASCII. Example usage: + >>> from unidecode import unidecode ->>> unidecode(u"\u5317\u4EB0") +>>> unidecode("\u5317\u4EB0") "Bei Jing " The transliteration uses a straightforward map, and doesn't have alternatives for the same character based on language, position, or anything else. -In Python 3, a standard string object will be returned. If you need bytes, use: +A standard string object will be returned. If you need bytes, use: + >>> unidecode("Κνωσός").encode("ascii") b'Knosos' """ import warnings -from sys import version_info +from typing import Dict, Optional, Sequence -Cache = {} +Cache = {} # type: Dict[int, Optional[Sequence[Optional[str]]]] + +class UnidecodeError(ValueError): + def __init__(self, message: str, index: Optional[int] = None) -> None: + """Raised for Unidecode-related errors. + + The index attribute contains the index of the character that caused + the error. + """ + super(UnidecodeError, self).__init__(message) + self.index = index -def _warn_if_not_unicode(string): - if version_info[0] < 3 and not isinstance(string, unicode): - warnings.warn( "Argument %r is not an unicode object. " - "Passing an encoded string will likely have " - "unexpected results." % (type(string),), - RuntimeWarning, 2) - - -def unidecode_expect_ascii(string): +def unidecode_expect_ascii(string: str, errors: str = 'ignore', replace_str: str = '?') -> str: """Transliterate an Unicode object into an ASCII string - >>> unidecode(u"\u5317\u4EB0") + >>> unidecode("\u5317\u4EB0") "Bei Jing " This function first tries to convert the string using ASCII codec. @@ -39,65 +43,96 @@ def unidecode_expect_ascii(string): transliteration using the character tables. This is approx. five times faster if the string only contains ASCII - characters, but slightly slower than using unidecode directly if non-ASCII - chars are present. + characters, but slightly slower than unicode_expect_nonascii if + non-ASCII characters are present. + + errors specifies what to do with characters that have not been + found in replacement tables. The default is 'ignore' which ignores + the character. 'strict' raises an UnidecodeError. 'replace' + substitutes the character with replace_str (default is '?'). + 'preserve' keeps the original character. + + Note that if 'preserve' is used the returned string might not be + ASCII! """ - _warn_if_not_unicode(string) try: bytestring = string.encode('ASCII') except UnicodeEncodeError: - return _unidecode(string) - if version_info[0] >= 3: - return string + pass else: - return bytestring + return string -def unidecode_expect_nonascii(string): + return _unidecode(string, errors, replace_str) + +def unidecode_expect_nonascii(string: str, errors: str = 'ignore', replace_str: str = '?') -> str: """Transliterate an Unicode object into an ASCII string - >>> unidecode(u"\u5317\u4EB0") + >>> unidecode("\u5317\u4EB0") "Bei Jing " + + See unidecode_expect_ascii. """ - _warn_if_not_unicode(string) - return _unidecode(string) + return _unidecode(string, errors, replace_str) unidecode = unidecode_expect_ascii -def _unidecode(string): +def _get_repl_str(char: str) -> Optional[str]: + codepoint = ord(char) + + if codepoint < 0x80: + # Already ASCII + return str(char) + + if codepoint > 0xeffff: + # No data on characters in Private Use Area and above. + return None + + if 0xd800 <= codepoint <= 0xdfff: + warnings.warn( "Surrogate character %r will be ignored. " + "You might be using a narrow Python build." % (char,), + RuntimeWarning, 2) + + section = codepoint >> 8 # Chop off the last two hex digits + position = codepoint % 256 # Last two hex digits + + try: + table = Cache[section] + except KeyError: + try: + mod = __import__('unidecode.x%03x'%(section), globals(), locals(), ['data']) + except ImportError: + # No data on this character + Cache[section] = None + return None + + Cache[section] = table = mod.data + + if table and len(table) > position: + return table[position] + else: + return None + +def _unidecode(string: str, errors: str, replace_str:str) -> str: retval = [] - for char in string: - codepoint = ord(char) + for index, char in enumerate(string): + repl = _get_repl_str(char) - if codepoint < 0x80: # Basic ASCII - retval.append(str(char)) - continue - - if codepoint > 0xeffff: - continue # Characters in Private Use Area and above are ignored + if repl is None: + if errors == 'ignore': + repl = '' + elif errors == 'strict': + raise UnidecodeError('no replacement found for character %r ' + 'in position %d' % (char, index), index) + elif errors == 'replace': + repl = replace_str + elif errors == 'preserve': + repl = char + else: + raise UnidecodeError('invalid value for errors parameter %r' % (errors,)) - if 0xd800 <= codepoint <= 0xdfff: - warnings.warn( "Surrogate character %r will be ignored. " - "You might be using a narrow Python build." % (char,), - RuntimeWarning, 2) - - section = codepoint >> 8 # Chop off the last two hex digits - position = codepoint % 256 # Last two hex digits - - try: - table = Cache[section] - except KeyError: - try: - mod = __import__('unidecode.x%03x'%(section), globals(), locals(), ['data']) - except ImportError: - Cache[section] = None - continue # No match: ignore this character and carry on. - - Cache[section] = table = mod.data - - if table and len(table) > position: - retval.append( table[position] ) + retval.append(repl) return ''.join(retval) diff --git a/libs/common/unidecode/__init__.pyi b/libs/common/unidecode/__init__.pyi new file mode 100644 index 00000000..5963674c --- /dev/null +++ b/libs/common/unidecode/__init__.pyi @@ -0,0 +1,11 @@ +from typing import Any, Dict, Optional, Sequence + +Cache: Dict[int, Optional[Sequence[Optional[str]]]] + +class UnidecodeError(ValueError): + index: Optional[int] = ... + def __init__(self, message: str, index: Optional[int] = ...) -> None: ... + +def unidecode_expect_ascii(string: str, errors: str = ..., replace_str: str = ...) -> str: ... +def unidecode_expect_nonascii(string: str, errors: str = ..., replace_str: str = ...) -> str: ... +unidecode = unidecode_expect_ascii diff --git a/libs/common/unidecode/__main__.py b/libs/common/unidecode/__main__.py new file mode 100644 index 00000000..390f842a --- /dev/null +++ b/libs/common/unidecode/__main__.py @@ -0,0 +1,3 @@ +from unidecode.util import main + +main() diff --git a/libs/common/unidecode/py.typed b/libs/common/unidecode/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/common/unidecode/util.py b/libs/common/unidecode/util.py index 477280d1..05f08bd8 100644 --- a/libs/common/unidecode/util.py +++ b/libs/common/unidecode/util.py @@ -1,15 +1,12 @@ # vim:ts=4 sw=4 expandtab softtabstop=4 -from __future__ import print_function -import optparse +import argparse +import io import locale import os import sys -import warnings from unidecode import unidecode -PY3 = sys.version_info[0] >= 3 - def fatal(msg): sys.stderr.write(msg + "\n") sys.exit(1) @@ -17,42 +14,38 @@ def fatal(msg): def main(): default_encoding = locale.getpreferredencoding() - parser = optparse.OptionParser('%prog [options] [FILE]', + parser = argparse.ArgumentParser( description="Transliterate Unicode text into ASCII. FILE is path to file to transliterate. " "Standard input is used if FILE is omitted and -c is not specified.") - parser.add_option('-e', '--encoding', metavar='ENCODING', default=default_encoding, + parser.add_argument('-e', '--encoding', metavar='ENCODING', default=default_encoding, help='Specify an encoding (default is %s)' % (default_encoding,)) - parser.add_option('-c', metavar='TEXT', dest='text', + parser.add_argument('-c', metavar='TEXT', dest='text', help='Transliterate TEXT instead of FILE') + parser.add_argument('path', nargs='?', metavar='FILE') - options, args = parser.parse_args() + args = parser.parse_args() - encoding = options.encoding + encoding = args.encoding - if args: - if options.text: + if args.path: + if args.text: fatal("Can't use both FILE and -c option") else: - with open(args[0], 'rb') as f: - stream = f.read() - elif options.text: - if PY3: - stream = os.fsencode(options.text) - else: - stream = options.text + stream = open(args.path, 'rb') + elif args.text: + text = os.fsencode(args.text) # add a newline to the string if it comes from the # command line so that the result is printed nicely # on the console. - stream += '\n'.encode('ascii') + stream = io.BytesIO(text + b'\n') else: - if PY3: - stream = sys.stdin.buffer.read() - else: - stream = sys.stdin.read() + stream = sys.stdin.buffer - try: - stream = stream.decode(encoding) - except UnicodeDecodeError as e: - fatal('Unable to decode input: %s, start: %d, end: %d' % (e.reason, e.start, e.end)) + for line_nr, line in enumerate(stream): + try: + line = line.decode(encoding) + except UnicodeDecodeError as e: + fatal('Unable to decode input line %s: %s, start: %d, end: %d' % (line_nr, e.reason, e.start, e.end)) - sys.stdout.write(unidecode(stream)) + sys.stdout.write(unidecode(line)) + stream.close() diff --git a/libs/common/unidecode/x000.py b/libs/common/unidecode/x000.py index 27e8d684..2c288a68 100644 --- a/libs/common/unidecode/x000.py +++ b/libs/common/unidecode/x000.py @@ -76,9 +76,9 @@ data = ( '1', # 0xb9 'o', # 0xba '>>', # 0xbb -' 1/4 ', # 0xbc -' 1/2 ', # 0xbd -' 3/4 ', # 0xbe +' 1/4', # 0xbc +' 1/2', # 0xbd +' 3/4', # 0xbe '?', # 0xbf 'A', # 0xc0 'A', # 0xc1 diff --git a/libs/common/unidecode/x002.py b/libs/common/unidecode/x002.py index d7028cdf..710942c8 100644 --- a/libs/common/unidecode/x002.py +++ b/libs/common/unidecode/x002.py @@ -64,8 +64,8 @@ data = ( 'T', # 0x3e 's', # 0x3f 'z', # 0x40 -'[?]', # 0x41 -'[?]', # 0x42 +None, # 0x41 +None, # 0x42 'B', # 0x43 'U', # 0x44 '^', # 0x45 @@ -238,20 +238,20 @@ data = ( 'V', # 0xec '=', # 0xed '"', # 0xee -'[?]', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x003.py b/libs/common/unidecode/x003.py index 4ba8d726..215faf2b 100644 --- a/libs/common/unidecode/x003.py +++ b/libs/common/unidecode/x003.py @@ -78,23 +78,23 @@ data = ( '', # 0x4c '', # 0x4d '', # 0x4e -'[?]', # 0x4f -'[?]', # 0x50 -'[?]', # 0x51 -'[?]', # 0x52 -'[?]', # 0x53 -'[?]', # 0x54 -'[?]', # 0x55 -'[?]', # 0x56 -'[?]', # 0x57 -'[?]', # 0x58 -'[?]', # 0x59 -'[?]', # 0x5a -'[?]', # 0x5b -'[?]', # 0x5c -'[?]', # 0x5d -'[?]', # 0x5e -'[?]', # 0x5f +None, # 0x4f +None, # 0x50 +None, # 0x51 +None, # 0x52 +None, # 0x53 +None, # 0x54 +None, # 0x55 +None, # 0x56 +None, # 0x57 +None, # 0x58 +None, # 0x59 +None, # 0x5a +None, # 0x5b +None, # 0x5c +None, # 0x5d +None, # 0x5e +None, # 0x5f '', # 0x60 '', # 0x61 '', # 0x62 @@ -111,26 +111,26 @@ data = ( 't', # 0x6d 'v', # 0x6e 'x', # 0x6f -'[?]', # 0x70 -'[?]', # 0x71 -'[?]', # 0x72 -'[?]', # 0x73 +None, # 0x70 +None, # 0x71 +None, # 0x72 +None, # 0x73 '\'', # 0x74 ',', # 0x75 -'[?]', # 0x76 -'[?]', # 0x77 -'[?]', # 0x78 -'[?]', # 0x79 +None, # 0x76 +None, # 0x77 +None, # 0x78 +None, # 0x79 '', # 0x7a -'[?]', # 0x7b -'[?]', # 0x7c -'[?]', # 0x7d +None, # 0x7b +None, # 0x7c +None, # 0x7d '?', # 0x7e -'[?]', # 0x7f -'[?]', # 0x80 -'[?]', # 0x81 -'[?]', # 0x82 -'[?]', # 0x83 +None, # 0x7f +None, # 0x80 +None, # 0x81 +None, # 0x82 +None, # 0x83 '', # 0x84 '', # 0x85 'A', # 0x86 @@ -138,9 +138,9 @@ data = ( 'E', # 0x88 'E', # 0x89 'I', # 0x8a -'[?]', # 0x8b +None, # 0x8b 'O', # 0x8c -'[?]', # 0x8d +None, # 0x8d 'U', # 0x8e 'O', # 0x8f 'I', # 0x90 @@ -161,7 +161,7 @@ data = ( 'O', # 0x9f 'P', # 0xa0 'R', # 0xa1 -'[?]', # 0xa2 +None, # 0xa2 'S', # 0xa3 'T', # 0xa4 'U', # 0xa5 @@ -206,7 +206,7 @@ data = ( 'o', # 0xcc 'u', # 0xcd 'o', # 0xce -'[?]', # 0xcf +None, # 0xcf 'b', # 0xd0 'th', # 0xd1 'U', # 0xd2 @@ -215,8 +215,8 @@ data = ( 'ph', # 0xd5 'p', # 0xd6 '&', # 0xd7 -'[?]', # 0xd8 -'[?]', # 0xd9 +None, # 0xd8 +None, # 0xd9 'St', # 0xda 'st', # 0xdb 'W', # 0xdc @@ -243,15 +243,15 @@ data = ( 'r', # 0xf1 'c', # 0xf2 'j', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x004.py b/libs/common/unidecode/x004.py index 1cc3dbc4..d2b5ff5f 100644 --- a/libs/common/unidecode/x004.py +++ b/libs/common/unidecode/x004.py @@ -134,11 +134,11 @@ data = ( '', # 0x84 '', # 0x85 '', # 0x86 -'[?]', # 0x87 +None, # 0x87 '*100.000*', # 0x88 '*1.000.000*', # 0x89 -'[?]', # 0x8a -'[?]', # 0x8b +None, # 0x8a +None, # 0x8b '"', # 0x8c '"', # 0x8d 'R\'', # 0x8e @@ -196,17 +196,17 @@ data = ( 'zh', # 0xc2 'K\'', # 0xc3 'k\'', # 0xc4 -'[?]', # 0xc5 -'[?]', # 0xc6 +None, # 0xc5 +None, # 0xc6 'N\'', # 0xc7 'n\'', # 0xc8 -'[?]', # 0xc9 -'[?]', # 0xca +None, # 0xc9 +None, # 0xca 'Ch', # 0xcb 'ch', # 0xcc -'[?]', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf +None, # 0xcd +None, # 0xce +None, # 0xcf 'a', # 0xd0 'a', # 0xd1 'A', # 0xd2 @@ -245,13 +245,13 @@ data = ( 'u', # 0xf3 'Ch', # 0xf4 'ch', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 +None, # 0xf6 +None, # 0xf7 'Y', # 0xf8 'y', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x005.py b/libs/common/unidecode/x005.py index ced54426..8779da6a 100644 --- a/libs/common/unidecode/x005.py +++ b/libs/common/unidecode/x005.py @@ -1,53 +1,53 @@ data = ( -'[?]', # 0x00 -'[?]', # 0x01 -'[?]', # 0x02 -'[?]', # 0x03 -'[?]', # 0x04 -'[?]', # 0x05 -'[?]', # 0x06 -'[?]', # 0x07 -'[?]', # 0x08 -'[?]', # 0x09 -'[?]', # 0x0a -'[?]', # 0x0b -'[?]', # 0x0c -'[?]', # 0x0d -'[?]', # 0x0e -'[?]', # 0x0f -'[?]', # 0x10 -'[?]', # 0x11 -'[?]', # 0x12 -'[?]', # 0x13 -'[?]', # 0x14 -'[?]', # 0x15 -'[?]', # 0x16 -'[?]', # 0x17 -'[?]', # 0x18 -'[?]', # 0x19 -'[?]', # 0x1a -'[?]', # 0x1b -'[?]', # 0x1c -'[?]', # 0x1d -'[?]', # 0x1e -'[?]', # 0x1f -'[?]', # 0x20 -'[?]', # 0x21 -'[?]', # 0x22 -'[?]', # 0x23 -'[?]', # 0x24 -'[?]', # 0x25 -'[?]', # 0x26 -'[?]', # 0x27 -'[?]', # 0x28 -'[?]', # 0x29 -'[?]', # 0x2a -'[?]', # 0x2b -'[?]', # 0x2c -'[?]', # 0x2d -'[?]', # 0x2e -'[?]', # 0x2f -'[?]', # 0x30 +None, # 0x00 +None, # 0x01 +None, # 0x02 +None, # 0x03 +None, # 0x04 +None, # 0x05 +None, # 0x06 +None, # 0x07 +None, # 0x08 +None, # 0x09 +None, # 0x0a +None, # 0x0b +None, # 0x0c +None, # 0x0d +None, # 0x0e +None, # 0x0f +None, # 0x10 +None, # 0x11 +None, # 0x12 +None, # 0x13 +None, # 0x14 +None, # 0x15 +None, # 0x16 +None, # 0x17 +None, # 0x18 +None, # 0x19 +None, # 0x1a +None, # 0x1b +None, # 0x1c +None, # 0x1d +None, # 0x1e +None, # 0x1f +None, # 0x20 +None, # 0x21 +None, # 0x22 +None, # 0x23 +None, # 0x24 +None, # 0x25 +None, # 0x26 +None, # 0x27 +None, # 0x28 +None, # 0x29 +None, # 0x2a +None, # 0x2b +None, # 0x2c +None, # 0x2d +None, # 0x2e +None, # 0x2f +None, # 0x30 'A', # 0x31 'B', # 0x32 'G', # 0x33 @@ -86,8 +86,8 @@ data = ( 'K`', # 0x54 'O', # 0x55 'F', # 0x56 -'[?]', # 0x57 -'[?]', # 0x58 +None, # 0x57 +None, # 0x58 '<', # 0x59 '\'', # 0x5a '/', # 0x5b @@ -95,7 +95,7 @@ data = ( ',', # 0x5d '?', # 0x5e '.', # 0x5f -'[?]', # 0x60 +None, # 0x60 'a', # 0x61 'b', # 0x62 'g', # 0x63 @@ -135,15 +135,15 @@ data = ( 'o', # 0x85 'f', # 0x86 'ew', # 0x87 -'[?]', # 0x88 +None, # 0x88 ':', # 0x89 '-', # 0x8a -'[?]', # 0x8b -'[?]', # 0x8c -'[?]', # 0x8d -'[?]', # 0x8e -'[?]', # 0x8f -'[?]', # 0x90 +None, # 0x8b +None, # 0x8c +None, # 0x8d +None, # 0x8e +None, # 0x8f +None, # 0x90 '', # 0x91 '', # 0x92 '', # 0x93 @@ -175,7 +175,7 @@ data = ( '', # 0xad '', # 0xae '', # 0xaf -'@', # 0xb0 +'', # 0xb0 'e', # 0xb1 'a', # 0xb2 'o', # 0xb3 @@ -187,26 +187,26 @@ data = ( 'o', # 0xb9 'o', # 0xba 'u', # 0xbb -'\'', # 0xbc +'', # 0xbc '', # 0xbd '-', # 0xbe -'-', # 0xbf +'', # 0xbf '|', # 0xc0 '', # 0xc1 '', # 0xc2 -':', # 0xc3 +'.', # 0xc3 '', # 0xc4 '', # 0xc5 'n', # 0xc6 'o', # 0xc7 -'[?]', # 0xc8 -'[?]', # 0xc9 -'[?]', # 0xca -'[?]', # 0xcb -'[?]', # 0xcc -'[?]', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf 'A', # 0xd0 'b', # 0xd1 'g', # 0xd2 @@ -214,11 +214,11 @@ data = ( 'h', # 0xd4 'v', # 0xd5 'z', # 0xd6 -'KH', # 0xd7 -'t', # 0xd8 +'H', # 0xd7 +'T', # 0xd8 'y', # 0xd9 -'k', # 0xda -'k', # 0xdb +'KH', # 0xda +'KH', # 0xdb 'l', # 0xdc 'm', # 0xdd 'm', # 0xde @@ -230,28 +230,28 @@ data = ( 'p', # 0xe4 'TS', # 0xe5 'TS', # 0xe6 -'q', # 0xe7 +'k', # 0xe7 'r', # 0xe8 'SH', # 0xe9 't', # 0xea -'[?]', # 0xeb -'[?]', # 0xec -'[?]', # 0xed -'[?]', # 0xee -'[?]', # 0xef +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +'YYY', # 0xef 'V', # 0xf0 'OY', # 0xf1 -'i', # 0xf2 +'EY', # 0xf2 '\'', # 0xf3 '"', # 0xf4 -'v', # 0xf5 -'n', # 0xf6 -'q', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x006.py b/libs/common/unidecode/x006.py index 09440b28..79296330 100644 --- a/libs/common/unidecode/x006.py +++ b/libs/common/unidecode/x006.py @@ -1,37 +1,37 @@ data = ( -'[?]', # 0x00 -'[?]', # 0x01 -'[?]', # 0x02 -'[?]', # 0x03 -'[?]', # 0x04 -'[?]', # 0x05 -'[?]', # 0x06 -'[?]', # 0x07 -'[?]', # 0x08 -'[?]', # 0x09 -'[?]', # 0x0a -'[?]', # 0x0b +None, # 0x00 +None, # 0x01 +None, # 0x02 +None, # 0x03 +None, # 0x04 +None, # 0x05 +None, # 0x06 +None, # 0x07 +None, # 0x08 +None, # 0x09 +None, # 0x0a +None, # 0x0b ',', # 0x0c -'[?]', # 0x0d -'[?]', # 0x0e -'[?]', # 0x0f -'[?]', # 0x10 -'[?]', # 0x11 -'[?]', # 0x12 -'[?]', # 0x13 -'[?]', # 0x14 -'[?]', # 0x15 -'[?]', # 0x16 -'[?]', # 0x17 -'[?]', # 0x18 -'[?]', # 0x19 -'[?]', # 0x1a +None, # 0x0d +None, # 0x0e +None, # 0x0f +None, # 0x10 +None, # 0x11 +None, # 0x12 +None, # 0x13 +None, # 0x14 +None, # 0x15 +None, # 0x16 +None, # 0x17 +None, # 0x18 +None, # 0x19 +None, # 0x1a ';', # 0x1b -'[?]', # 0x1c -'[?]', # 0x1d -'[?]', # 0x1e +None, # 0x1c +None, # 0x1d +None, # 0x1e '?', # 0x1f -'[?]', # 0x20 +None, # 0x20 '', # 0x21 'a', # 0x22 '\'', # 0x23 @@ -58,11 +58,11 @@ data = ( 'Z', # 0x38 '`', # 0x39 'G', # 0x3a -'[?]', # 0x3b -'[?]', # 0x3c -'[?]', # 0x3d -'[?]', # 0x3e -'[?]', # 0x3f +None, # 0x3b +None, # 0x3c +None, # 0x3d +None, # 0x3e +None, # 0x3f '', # 0x40 'f', # 0x41 'q', # 0x42 @@ -85,16 +85,16 @@ data = ( '', # 0x53 '\'', # 0x54 '\'', # 0x55 -'[?]', # 0x56 -'[?]', # 0x57 -'[?]', # 0x58 -'[?]', # 0x59 -'[?]', # 0x5a -'[?]', # 0x5b -'[?]', # 0x5c -'[?]', # 0x5d -'[?]', # 0x5e -'[?]', # 0x5f +None, # 0x56 +None, # 0x57 +None, # 0x58 +None, # 0x59 +None, # 0x5a +None, # 0x5b +None, # 0x5c +None, # 0x5d +None, # 0x5e +None, # 0x5f '0', # 0x60 '1', # 0x61 '2', # 0x62 @@ -109,8 +109,8 @@ data = ( '.', # 0x6b ',', # 0x6c '*', # 0x6d -'[?]', # 0x6e -'[?]', # 0x6f +None, # 0x6e +None, # 0x6f '', # 0x70 '\'', # 0x71 '\'', # 0x72 @@ -237,8 +237,8 @@ data = ( '', # 0xeb '', # 0xec '', # 0xed -'[?]', # 0xee -'[?]', # 0xef +None, # 0xee +None, # 0xef '0', # 0xf0 '1', # 0xf1 '2', # 0xf2 diff --git a/libs/common/unidecode/x007.py b/libs/common/unidecode/x007.py index d2c00213..f2b3ecd7 100644 --- a/libs/common/unidecode/x007.py +++ b/libs/common/unidecode/x007.py @@ -13,7 +13,7 @@ data = ( '{', # 0x0b '}', # 0x0c '*', # 0x0d -'[?]', # 0x0e +None, # 0x0e '', # 0x0f '\'', # 0x10 '', # 0x11 @@ -44,9 +44,9 @@ data = ( 'r', # 0x2a 'sh', # 0x2b 't', # 0x2c -'[?]', # 0x2d -'[?]', # 0x2e -'[?]', # 0x2f +None, # 0x2d +None, # 0x2e +None, # 0x2f 'a', # 0x30 'a', # 0x31 'a', # 0x32 @@ -74,59 +74,59 @@ data = ( '@', # 0x48 '|', # 0x49 '+', # 0x4a -'[?]', # 0x4b -'[?]', # 0x4c -'[?]', # 0x4d -'[?]', # 0x4e -'[?]', # 0x4f -'[?]', # 0x50 -'[?]', # 0x51 -'[?]', # 0x52 -'[?]', # 0x53 -'[?]', # 0x54 -'[?]', # 0x55 -'[?]', # 0x56 -'[?]', # 0x57 -'[?]', # 0x58 -'[?]', # 0x59 -'[?]', # 0x5a -'[?]', # 0x5b -'[?]', # 0x5c -'[?]', # 0x5d -'[?]', # 0x5e -'[?]', # 0x5f -'[?]', # 0x60 -'[?]', # 0x61 -'[?]', # 0x62 -'[?]', # 0x63 -'[?]', # 0x64 -'[?]', # 0x65 -'[?]', # 0x66 -'[?]', # 0x67 -'[?]', # 0x68 -'[?]', # 0x69 -'[?]', # 0x6a -'[?]', # 0x6b -'[?]', # 0x6c -'[?]', # 0x6d -'[?]', # 0x6e -'[?]', # 0x6f -'[?]', # 0x70 -'[?]', # 0x71 -'[?]', # 0x72 -'[?]', # 0x73 -'[?]', # 0x74 -'[?]', # 0x75 -'[?]', # 0x76 -'[?]', # 0x77 -'[?]', # 0x78 -'[?]', # 0x79 -'[?]', # 0x7a -'[?]', # 0x7b -'[?]', # 0x7c -'[?]', # 0x7d -'[?]', # 0x7e -'[?]', # 0x7f +None, # 0x4b +None, # 0x4c +None, # 0x4d +None, # 0x4e +None, # 0x4f +None, # 0x50 +None, # 0x51 +None, # 0x52 +None, # 0x53 +None, # 0x54 +None, # 0x55 +None, # 0x56 +None, # 0x57 +None, # 0x58 +None, # 0x59 +None, # 0x5a +None, # 0x5b +None, # 0x5c +None, # 0x5d +None, # 0x5e +None, # 0x5f +None, # 0x60 +None, # 0x61 +None, # 0x62 +None, # 0x63 +None, # 0x64 +None, # 0x65 +None, # 0x66 +None, # 0x67 +None, # 0x68 +None, # 0x69 +None, # 0x6a +None, # 0x6b +None, # 0x6c +None, # 0x6d +None, # 0x6e +None, # 0x6f +None, # 0x70 +None, # 0x71 +None, # 0x72 +None, # 0x73 +None, # 0x74 +None, # 0x75 +None, # 0x76 +None, # 0x77 +None, # 0x78 +None, # 0x79 +None, # 0x7a +None, # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f 'h', # 0x80 'sh', # 0x81 'n', # 0x82 @@ -176,82 +176,82 @@ data = ( 'o', # 0xae 'oa', # 0xaf '', # 0xb0 -'[?]', # 0xb1 -'[?]', # 0xb2 -'[?]', # 0xb3 -'[?]', # 0xb4 -'[?]', # 0xb5 -'[?]', # 0xb6 -'[?]', # 0xb7 -'[?]', # 0xb8 -'[?]', # 0xb9 -'[?]', # 0xba -'[?]', # 0xbb -'[?]', # 0xbc -'[?]', # 0xbd -'[?]', # 0xbe -'[?]', # 0xbf -'[?]', # 0xc0 -'[?]', # 0xc1 -'[?]', # 0xc2 -'[?]', # 0xc3 -'[?]', # 0xc4 -'[?]', # 0xc5 -'[?]', # 0xc6 -'[?]', # 0xc7 -'[?]', # 0xc8 -'[?]', # 0xc9 -'[?]', # 0xca -'[?]', # 0xcb -'[?]', # 0xcc -'[?]', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf -'[?]', # 0xd0 -'[?]', # 0xd1 -'[?]', # 0xd2 -'[?]', # 0xd3 -'[?]', # 0xd4 -'[?]', # 0xd5 -'[?]', # 0xd6 -'[?]', # 0xd7 -'[?]', # 0xd8 -'[?]', # 0xd9 -'[?]', # 0xda -'[?]', # 0xdb -'[?]', # 0xdc -'[?]', # 0xdd -'[?]', # 0xde -'[?]', # 0xdf -'[?]', # 0xe0 -'[?]', # 0xe1 -'[?]', # 0xe2 -'[?]', # 0xe3 -'[?]', # 0xe4 -'[?]', # 0xe5 -'[?]', # 0xe6 -'[?]', # 0xe7 -'[?]', # 0xe8 -'[?]', # 0xe9 -'[?]', # 0xea -'[?]', # 0xeb -'[?]', # 0xec -'[?]', # 0xed -'[?]', # 0xee -'[?]', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xb1 +None, # 0xb2 +None, # 0xb3 +None, # 0xb4 +None, # 0xb5 +None, # 0xb6 +None, # 0xb7 +None, # 0xb8 +None, # 0xb9 +None, # 0xba +None, # 0xbb +None, # 0xbc +None, # 0xbd +None, # 0xbe +None, # 0xbf +None, # 0xc0 +None, # 0xc1 +None, # 0xc2 +None, # 0xc3 +None, # 0xc4 +None, # 0xc5 +None, # 0xc6 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x009.py b/libs/common/unidecode/x009.py index 564ec784..c910be2f 100644 --- a/libs/common/unidecode/x009.py +++ b/libs/common/unidecode/x009.py @@ -1,9 +1,9 @@ data = ( -'[?]', # 0x00 +None, # 0x00 'N', # 0x01 'N', # 0x02 'H', # 0x03 -'[?]', # 0x04 +None, # 0x04 'a', # 0x05 'aa', # 0x06 'i', # 0x07 @@ -57,8 +57,8 @@ data = ( 'ss', # 0x37 's', # 0x38 'h', # 0x39 -'[?]', # 0x3a -'[?]', # 0x3b +None, # 0x3a +None, # 0x3b '\'', # 0x3c '\'', # 0x3d 'aa', # 0x3e @@ -77,16 +77,16 @@ data = ( 'o', # 0x4b 'au', # 0x4c '', # 0x4d -'[?]', # 0x4e -'[?]', # 0x4f +None, # 0x4e +None, # 0x4f 'AUM', # 0x50 '\'', # 0x51 '\'', # 0x52 '`', # 0x53 '\'', # 0x54 -'[?]', # 0x55 -'[?]', # 0x56 -'[?]', # 0x57 +None, # 0x55 +None, # 0x56 +None, # 0x57 'q', # 0x58 'khh', # 0x59 'ghh', # 0x5a @@ -112,26 +112,26 @@ data = ( '8', # 0x6e '9', # 0x6f '.', # 0x70 -'[?]', # 0x71 -'[?]', # 0x72 -'[?]', # 0x73 -'[?]', # 0x74 -'[?]', # 0x75 -'[?]', # 0x76 -'[?]', # 0x77 -'[?]', # 0x78 -'[?]', # 0x79 -'[?]', # 0x7a -'[?]', # 0x7b -'[?]', # 0x7c -'[?]', # 0x7d -'[?]', # 0x7e -'[?]', # 0x7f -'[?]', # 0x80 +None, # 0x71 +None, # 0x72 +None, # 0x73 +None, # 0x74 +None, # 0x75 +None, # 0x76 +None, # 0x77 +None, # 0x78 +None, # 0x79 +None, # 0x7a +None, # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f +None, # 0x80 'N', # 0x81 'N', # 0x82 'H', # 0x83 -'[?]', # 0x84 +None, # 0x84 'a', # 0x85 'aa', # 0x86 'i', # 0x87 @@ -140,12 +140,12 @@ data = ( 'uu', # 0x8a 'R', # 0x8b 'RR', # 0x8c -'[?]', # 0x8d -'[?]', # 0x8e +None, # 0x8d +None, # 0x8e 'e', # 0x8f 'ai', # 0x90 -'[?]', # 0x91 -'[?]', # 0x92 +None, # 0x91 +None, # 0x92 'o', # 0x93 'au', # 0x94 'k', # 0x95 @@ -168,7 +168,7 @@ data = ( 'd', # 0xa6 'dh', # 0xa7 'n', # 0xa8 -'[?]', # 0xa9 +None, # 0xa9 'p', # 0xaa 'ph', # 0xab 'b', # 0xac @@ -176,19 +176,19 @@ data = ( 'm', # 0xae 'y', # 0xaf 'r', # 0xb0 -'[?]', # 0xb1 +None, # 0xb1 'l', # 0xb2 -'[?]', # 0xb3 -'[?]', # 0xb4 -'[?]', # 0xb5 +None, # 0xb3 +None, # 0xb4 +None, # 0xb5 'sh', # 0xb6 'ss', # 0xb7 's', # 0xb8 'h', # 0xb9 -'[?]', # 0xba -'[?]', # 0xbb +None, # 0xba +None, # 0xbb '\'', # 0xbc -'[?]', # 0xbd +None, # 0xbd 'aa', # 0xbe 'i', # 0xbf 'ii', # 0xc0 @@ -196,39 +196,39 @@ data = ( 'uu', # 0xc2 'R', # 0xc3 'RR', # 0xc4 -'[?]', # 0xc5 -'[?]', # 0xc6 +None, # 0xc5 +None, # 0xc6 'e', # 0xc7 'ai', # 0xc8 -'[?]', # 0xc9 -'[?]', # 0xca +None, # 0xc9 +None, # 0xca 'o', # 0xcb 'au', # 0xcc '', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf -'[?]', # 0xd0 -'[?]', # 0xd1 -'[?]', # 0xd2 -'[?]', # 0xd3 -'[?]', # 0xd4 -'[?]', # 0xd5 -'[?]', # 0xd6 +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 '+', # 0xd7 -'[?]', # 0xd8 -'[?]', # 0xd9 -'[?]', # 0xda -'[?]', # 0xdb +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb 'rr', # 0xdc 'rh', # 0xdd -'[?]', # 0xde +None, # 0xde 'yy', # 0xdf 'RR', # 0xe0 'LL', # 0xe1 'L', # 0xe2 'LL', # 0xe3 -'[?]', # 0xe4 -'[?]', # 0xe5 +None, # 0xe4 +None, # 0xe5 '0', # 0xe6 '1', # 0xe7 '2', # 0xe8 @@ -250,8 +250,8 @@ data = ( ' 1 - 1/', # 0xf8 '/16', # 0xf9 '', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x00a.py b/libs/common/unidecode/x00a.py index 1ccd9df6..26c43a58 100644 --- a/libs/common/unidecode/x00a.py +++ b/libs/common/unidecode/x00a.py @@ -1,23 +1,23 @@ data = ( -'[?]', # 0x00 -'[?]', # 0x01 +None, # 0x00 +None, # 0x01 'N', # 0x02 -'[?]', # 0x03 -'[?]', # 0x04 +None, # 0x03 +None, # 0x04 'a', # 0x05 'aa', # 0x06 'i', # 0x07 'ii', # 0x08 'u', # 0x09 'uu', # 0x0a -'[?]', # 0x0b -'[?]', # 0x0c -'[?]', # 0x0d -'[?]', # 0x0e +None, # 0x0b +None, # 0x0c +None, # 0x0d +None, # 0x0e 'ee', # 0x0f 'ai', # 0x10 -'[?]', # 0x11 -'[?]', # 0x12 +None, # 0x11 +None, # 0x12 'oo', # 0x13 'au', # 0x14 'k', # 0x15 @@ -40,7 +40,7 @@ data = ( 'd', # 0x26 'dh', # 0x27 'n', # 0x28 -'[?]', # 0x29 +None, # 0x29 'p', # 0x2a 'ph', # 0x2b 'b', # 0x2c @@ -48,59 +48,59 @@ data = ( 'm', # 0x2e 'y', # 0x2f 'r', # 0x30 -'[?]', # 0x31 +None, # 0x31 'l', # 0x32 'll', # 0x33 -'[?]', # 0x34 +None, # 0x34 'v', # 0x35 'sh', # 0x36 -'[?]', # 0x37 +None, # 0x37 's', # 0x38 'h', # 0x39 -'[?]', # 0x3a -'[?]', # 0x3b +None, # 0x3a +None, # 0x3b '\'', # 0x3c -'[?]', # 0x3d +None, # 0x3d 'aa', # 0x3e 'i', # 0x3f 'ii', # 0x40 'u', # 0x41 'uu', # 0x42 -'[?]', # 0x43 -'[?]', # 0x44 -'[?]', # 0x45 -'[?]', # 0x46 +None, # 0x43 +None, # 0x44 +None, # 0x45 +None, # 0x46 'ee', # 0x47 'ai', # 0x48 -'[?]', # 0x49 -'[?]', # 0x4a +None, # 0x49 +None, # 0x4a 'oo', # 0x4b 'au', # 0x4c '', # 0x4d -'[?]', # 0x4e -'[?]', # 0x4f -'[?]', # 0x50 -'[?]', # 0x51 -'[?]', # 0x52 -'[?]', # 0x53 -'[?]', # 0x54 -'[?]', # 0x55 -'[?]', # 0x56 -'[?]', # 0x57 -'[?]', # 0x58 +None, # 0x4e +None, # 0x4f +None, # 0x50 +None, # 0x51 +None, # 0x52 +None, # 0x53 +None, # 0x54 +None, # 0x55 +None, # 0x56 +None, # 0x57 +None, # 0x58 'khh', # 0x59 'ghh', # 0x5a 'z', # 0x5b 'rr', # 0x5c -'[?]', # 0x5d +None, # 0x5d 'f', # 0x5e -'[?]', # 0x5f -'[?]', # 0x60 -'[?]', # 0x61 -'[?]', # 0x62 -'[?]', # 0x63 -'[?]', # 0x64 -'[?]', # 0x65 +None, # 0x5f +None, # 0x60 +None, # 0x61 +None, # 0x62 +None, # 0x63 +None, # 0x64 +None, # 0x65 '0', # 0x66 '1', # 0x67 '2', # 0x68 @@ -116,22 +116,22 @@ data = ( '', # 0x72 '', # 0x73 'G.E.O.', # 0x74 -'[?]', # 0x75 -'[?]', # 0x76 -'[?]', # 0x77 -'[?]', # 0x78 -'[?]', # 0x79 -'[?]', # 0x7a -'[?]', # 0x7b -'[?]', # 0x7c -'[?]', # 0x7d -'[?]', # 0x7e -'[?]', # 0x7f -'[?]', # 0x80 +None, # 0x75 +None, # 0x76 +None, # 0x77 +None, # 0x78 +None, # 0x79 +None, # 0x7a +None, # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f +None, # 0x80 'N', # 0x81 'N', # 0x82 'H', # 0x83 -'[?]', # 0x84 +None, # 0x84 'a', # 0x85 'aa', # 0x86 'i', # 0x87 @@ -139,13 +139,13 @@ data = ( 'u', # 0x89 'uu', # 0x8a 'R', # 0x8b -'[?]', # 0x8c +None, # 0x8c 'eN', # 0x8d -'[?]', # 0x8e +None, # 0x8e 'e', # 0x8f 'ai', # 0x90 'oN', # 0x91 -'[?]', # 0x92 +None, # 0x92 'o', # 0x93 'au', # 0x94 'k', # 0x95 @@ -168,7 +168,7 @@ data = ( 'd', # 0xa6 'dh', # 0xa7 'n', # 0xa8 -'[?]', # 0xa9 +None, # 0xa9 'p', # 0xaa 'ph', # 0xab 'b', # 0xac @@ -176,17 +176,17 @@ data = ( 'm', # 0xae 'ya', # 0xaf 'r', # 0xb0 -'[?]', # 0xb1 +None, # 0xb1 'l', # 0xb2 'll', # 0xb3 -'[?]', # 0xb4 +None, # 0xb4 'v', # 0xb5 'sh', # 0xb6 'ss', # 0xb7 's', # 0xb8 'h', # 0xb9 -'[?]', # 0xba -'[?]', # 0xbb +None, # 0xba +None, # 0xbb '\'', # 0xbc '\'', # 0xbd 'aa', # 0xbe @@ -197,38 +197,38 @@ data = ( 'R', # 0xc3 'RR', # 0xc4 'eN', # 0xc5 -'[?]', # 0xc6 +None, # 0xc6 'e', # 0xc7 'ai', # 0xc8 'oN', # 0xc9 -'[?]', # 0xca +None, # 0xca 'o', # 0xcb 'au', # 0xcc '', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf +None, # 0xce +None, # 0xcf 'AUM', # 0xd0 -'[?]', # 0xd1 -'[?]', # 0xd2 -'[?]', # 0xd3 -'[?]', # 0xd4 -'[?]', # 0xd5 -'[?]', # 0xd6 -'[?]', # 0xd7 -'[?]', # 0xd8 -'[?]', # 0xd9 -'[?]', # 0xda -'[?]', # 0xdb -'[?]', # 0xdc -'[?]', # 0xdd -'[?]', # 0xde -'[?]', # 0xdf +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf 'RR', # 0xe0 -'[?]', # 0xe1 -'[?]', # 0xe2 -'[?]', # 0xe3 -'[?]', # 0xe4 -'[?]', # 0xe5 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 '0', # 0xe6 '1', # 0xe7 '2', # 0xe8 @@ -239,19 +239,19 @@ data = ( '7', # 0xed '8', # 0xee '9', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x00b.py b/libs/common/unidecode/x00b.py index 19d18488..294ea45b 100644 --- a/libs/common/unidecode/x00b.py +++ b/libs/common/unidecode/x00b.py @@ -1,9 +1,9 @@ data = ( -'[?]', # 0x00 +None, # 0x00 'N', # 0x01 'N', # 0x02 'H', # 0x03 -'[?]', # 0x04 +None, # 0x04 'a', # 0x05 'aa', # 0x06 'i', # 0x07 @@ -12,12 +12,12 @@ data = ( 'uu', # 0x0a 'R', # 0x0b 'L', # 0x0c -'[?]', # 0x0d -'[?]', # 0x0e +None, # 0x0d +None, # 0x0e 'e', # 0x0f 'ai', # 0x10 -'[?]', # 0x11 -'[?]', # 0x12 +None, # 0x11 +None, # 0x12 'o', # 0x13 'au', # 0x14 'k', # 0x15 @@ -40,7 +40,7 @@ data = ( 'd', # 0x26 'dh', # 0x27 'n', # 0x28 -'[?]', # 0x29 +None, # 0x29 'p', # 0x2a 'ph', # 0x2b 'b', # 0x2c @@ -48,17 +48,17 @@ data = ( 'm', # 0x2e 'y', # 0x2f 'r', # 0x30 -'[?]', # 0x31 +None, # 0x31 'l', # 0x32 'll', # 0x33 -'[?]', # 0x34 +None, # 0x34 '', # 0x35 'sh', # 0x36 'ss', # 0x37 's', # 0x38 'h', # 0x39 -'[?]', # 0x3a -'[?]', # 0x3b +None, # 0x3a +None, # 0x3b '\'', # 0x3c '\'', # 0x3d 'aa', # 0x3e @@ -67,40 +67,40 @@ data = ( 'u', # 0x41 'uu', # 0x42 'R', # 0x43 -'[?]', # 0x44 -'[?]', # 0x45 -'[?]', # 0x46 +None, # 0x44 +None, # 0x45 +None, # 0x46 'e', # 0x47 'ai', # 0x48 -'[?]', # 0x49 -'[?]', # 0x4a +None, # 0x49 +None, # 0x4a 'o', # 0x4b 'au', # 0x4c '', # 0x4d -'[?]', # 0x4e -'[?]', # 0x4f -'[?]', # 0x50 -'[?]', # 0x51 -'[?]', # 0x52 -'[?]', # 0x53 -'[?]', # 0x54 -'[?]', # 0x55 +None, # 0x4e +None, # 0x4f +None, # 0x50 +None, # 0x51 +None, # 0x52 +None, # 0x53 +None, # 0x54 +None, # 0x55 '+', # 0x56 '+', # 0x57 -'[?]', # 0x58 -'[?]', # 0x59 -'[?]', # 0x5a -'[?]', # 0x5b +None, # 0x58 +None, # 0x59 +None, # 0x5a +None, # 0x5b 'rr', # 0x5c 'rh', # 0x5d -'[?]', # 0x5e +None, # 0x5e 'yy', # 0x5f 'RR', # 0x60 'LL', # 0x61 -'[?]', # 0x62 -'[?]', # 0x63 -'[?]', # 0x64 -'[?]', # 0x65 +None, # 0x62 +None, # 0x63 +None, # 0x64 +None, # 0x65 '0', # 0x66 '1', # 0x67 '2', # 0x68 @@ -112,67 +112,67 @@ data = ( '8', # 0x6e '9', # 0x6f '', # 0x70 -'[?]', # 0x71 -'[?]', # 0x72 -'[?]', # 0x73 -'[?]', # 0x74 -'[?]', # 0x75 -'[?]', # 0x76 -'[?]', # 0x77 -'[?]', # 0x78 -'[?]', # 0x79 -'[?]', # 0x7a -'[?]', # 0x7b -'[?]', # 0x7c -'[?]', # 0x7d -'[?]', # 0x7e -'[?]', # 0x7f -'[?]', # 0x80 -'[?]', # 0x81 +None, # 0x71 +None, # 0x72 +None, # 0x73 +None, # 0x74 +None, # 0x75 +None, # 0x76 +None, # 0x77 +None, # 0x78 +None, # 0x79 +None, # 0x7a +None, # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f +None, # 0x80 +None, # 0x81 'N', # 0x82 'H', # 0x83 -'[?]', # 0x84 +None, # 0x84 'a', # 0x85 'aa', # 0x86 'i', # 0x87 'ii', # 0x88 'u', # 0x89 'uu', # 0x8a -'[?]', # 0x8b -'[?]', # 0x8c -'[?]', # 0x8d +None, # 0x8b +None, # 0x8c +None, # 0x8d 'e', # 0x8e 'ee', # 0x8f 'ai', # 0x90 -'[?]', # 0x91 +None, # 0x91 'o', # 0x92 'oo', # 0x93 'au', # 0x94 'k', # 0x95 -'[?]', # 0x96 -'[?]', # 0x97 -'[?]', # 0x98 +None, # 0x96 +None, # 0x97 +None, # 0x98 'ng', # 0x99 'c', # 0x9a -'[?]', # 0x9b +None, # 0x9b 'j', # 0x9c -'[?]', # 0x9d +None, # 0x9d 'ny', # 0x9e 'tt', # 0x9f -'[?]', # 0xa0 -'[?]', # 0xa1 -'[?]', # 0xa2 +None, # 0xa0 +None, # 0xa1 +None, # 0xa2 'nn', # 0xa3 't', # 0xa4 -'[?]', # 0xa5 -'[?]', # 0xa6 -'[?]', # 0xa7 +None, # 0xa5 +None, # 0xa6 +None, # 0xa7 'n', # 0xa8 'nnn', # 0xa9 'p', # 0xaa -'[?]', # 0xab -'[?]', # 0xac -'[?]', # 0xad +None, # 0xab +None, # 0xac +None, # 0xad 'm', # 0xae 'y', # 0xaf 'r', # 0xb0 @@ -181,54 +181,54 @@ data = ( 'll', # 0xb3 'lll', # 0xb4 'v', # 0xb5 -'[?]', # 0xb6 +None, # 0xb6 'ss', # 0xb7 's', # 0xb8 'h', # 0xb9 -'[?]', # 0xba -'[?]', # 0xbb -'[?]', # 0xbc -'[?]', # 0xbd +None, # 0xba +None, # 0xbb +None, # 0xbc +None, # 0xbd 'aa', # 0xbe 'i', # 0xbf 'ii', # 0xc0 'u', # 0xc1 'uu', # 0xc2 -'[?]', # 0xc3 -'[?]', # 0xc4 -'[?]', # 0xc5 +None, # 0xc3 +None, # 0xc4 +None, # 0xc5 'e', # 0xc6 'ee', # 0xc7 'ai', # 0xc8 -'[?]', # 0xc9 +None, # 0xc9 'o', # 0xca 'oo', # 0xcb 'au', # 0xcc '', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf -'[?]', # 0xd0 -'[?]', # 0xd1 -'[?]', # 0xd2 -'[?]', # 0xd3 -'[?]', # 0xd4 -'[?]', # 0xd5 -'[?]', # 0xd6 +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 '+', # 0xd7 -'[?]', # 0xd8 -'[?]', # 0xd9 -'[?]', # 0xda -'[?]', # 0xdb -'[?]', # 0xdc -'[?]', # 0xdd -'[?]', # 0xde -'[?]', # 0xdf -'[?]', # 0xe0 -'[?]', # 0xe1 -'[?]', # 0xe2 -'[?]', # 0xe3 -'[?]', # 0xe4 -'[?]', # 0xe5 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 '0', # 0xe6 '1', # 0xe7 '2', # 0xe8 @@ -242,16 +242,16 @@ data = ( '+10+', # 0xf0 '+100+', # 0xf1 '+1000+', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x00c.py b/libs/common/unidecode/x00c.py index 56f3654f..54987d82 100644 --- a/libs/common/unidecode/x00c.py +++ b/libs/common/unidecode/x00c.py @@ -1,9 +1,9 @@ data = ( -'[?]', # 0x00 +None, # 0x00 'N', # 0x01 'N', # 0x02 'H', # 0x03 -'[?]', # 0x04 +None, # 0x04 'a', # 0x05 'aa', # 0x06 'i', # 0x07 @@ -12,11 +12,11 @@ data = ( 'uu', # 0x0a 'R', # 0x0b 'L', # 0x0c -'[?]', # 0x0d +None, # 0x0d 'e', # 0x0e 'ee', # 0x0f 'ai', # 0x10 -'[?]', # 0x11 +None, # 0x11 'o', # 0x12 'oo', # 0x13 'au', # 0x14 @@ -40,7 +40,7 @@ data = ( 'd', # 0x26 'dh', # 0x27 'n', # 0x28 -'[?]', # 0x29 +None, # 0x29 'p', # 0x2a 'ph', # 0x2b 'b', # 0x2c @@ -51,16 +51,16 @@ data = ( 'rr', # 0x31 'l', # 0x32 'll', # 0x33 -'[?]', # 0x34 +None, # 0x34 'v', # 0x35 'sh', # 0x36 'ss', # 0x37 's', # 0x38 'h', # 0x39 -'[?]', # 0x3a -'[?]', # 0x3b -'[?]', # 0x3c -'[?]', # 0x3d +None, # 0x3a +None, # 0x3b +None, # 0x3c +None, # 0x3d 'aa', # 0x3e 'i', # 0x3f 'ii', # 0x40 @@ -68,39 +68,39 @@ data = ( 'uu', # 0x42 'R', # 0x43 'RR', # 0x44 -'[?]', # 0x45 +None, # 0x45 'e', # 0x46 'ee', # 0x47 'ai', # 0x48 -'[?]', # 0x49 +None, # 0x49 'o', # 0x4a 'oo', # 0x4b 'au', # 0x4c '', # 0x4d -'[?]', # 0x4e -'[?]', # 0x4f -'[?]', # 0x50 -'[?]', # 0x51 -'[?]', # 0x52 -'[?]', # 0x53 -'[?]', # 0x54 +None, # 0x4e +None, # 0x4f +None, # 0x50 +None, # 0x51 +None, # 0x52 +None, # 0x53 +None, # 0x54 '+', # 0x55 '+', # 0x56 -'[?]', # 0x57 -'[?]', # 0x58 -'[?]', # 0x59 -'[?]', # 0x5a -'[?]', # 0x5b -'[?]', # 0x5c -'[?]', # 0x5d -'[?]', # 0x5e -'[?]', # 0x5f +None, # 0x57 +None, # 0x58 +None, # 0x59 +None, # 0x5a +None, # 0x5b +None, # 0x5c +None, # 0x5d +None, # 0x5e +None, # 0x5f 'RR', # 0x60 'LL', # 0x61 -'[?]', # 0x62 -'[?]', # 0x63 -'[?]', # 0x64 -'[?]', # 0x65 +None, # 0x62 +None, # 0x63 +None, # 0x64 +None, # 0x65 '0', # 0x66 '1', # 0x67 '2', # 0x68 @@ -111,27 +111,27 @@ data = ( '7', # 0x6d '8', # 0x6e '9', # 0x6f -'[?]', # 0x70 -'[?]', # 0x71 -'[?]', # 0x72 -'[?]', # 0x73 -'[?]', # 0x74 -'[?]', # 0x75 -'[?]', # 0x76 -'[?]', # 0x77 -'[?]', # 0x78 -'[?]', # 0x79 -'[?]', # 0x7a -'[?]', # 0x7b -'[?]', # 0x7c -'[?]', # 0x7d -'[?]', # 0x7e -'[?]', # 0x7f -'[?]', # 0x80 -'[?]', # 0x81 +None, # 0x70 +None, # 0x71 +None, # 0x72 +None, # 0x73 +None, # 0x74 +None, # 0x75 +None, # 0x76 +None, # 0x77 +None, # 0x78 +None, # 0x79 +None, # 0x7a +None, # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f +None, # 0x80 +None, # 0x81 'N', # 0x82 'H', # 0x83 -'[?]', # 0x84 +None, # 0x84 'a', # 0x85 'aa', # 0x86 'i', # 0x87 @@ -140,11 +140,11 @@ data = ( 'uu', # 0x8a 'R', # 0x8b 'L', # 0x8c -'[?]', # 0x8d +None, # 0x8d 'e', # 0x8e 'ee', # 0x8f 'ai', # 0x90 -'[?]', # 0x91 +None, # 0x91 'o', # 0x92 'oo', # 0x93 'au', # 0x94 @@ -168,7 +168,7 @@ data = ( 'd', # 0xa6 'dh', # 0xa7 'n', # 0xa8 -'[?]', # 0xa9 +None, # 0xa9 'p', # 0xaa 'ph', # 0xab 'b', # 0xac @@ -179,16 +179,16 @@ data = ( 'rr', # 0xb1 'l', # 0xb2 'll', # 0xb3 -'[?]', # 0xb4 +None, # 0xb4 'v', # 0xb5 'sh', # 0xb6 'ss', # 0xb7 's', # 0xb8 'h', # 0xb9 -'[?]', # 0xba -'[?]', # 0xbb -'[?]', # 0xbc -'[?]', # 0xbd +None, # 0xba +None, # 0xbb +None, # 0xbc +None, # 0xbd 'aa', # 0xbe 'i', # 0xbf 'ii', # 0xc0 @@ -196,39 +196,39 @@ data = ( 'uu', # 0xc2 'R', # 0xc3 'RR', # 0xc4 -'[?]', # 0xc5 +None, # 0xc5 'e', # 0xc6 'ee', # 0xc7 'ai', # 0xc8 -'[?]', # 0xc9 +None, # 0xc9 'o', # 0xca 'oo', # 0xcb 'au', # 0xcc '', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf -'[?]', # 0xd0 -'[?]', # 0xd1 -'[?]', # 0xd2 -'[?]', # 0xd3 -'[?]', # 0xd4 +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 '+', # 0xd5 '+', # 0xd6 -'[?]', # 0xd7 -'[?]', # 0xd8 -'[?]', # 0xd9 -'[?]', # 0xda -'[?]', # 0xdb -'[?]', # 0xdc -'[?]', # 0xdd +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd 'lll', # 0xde -'[?]', # 0xdf +None, # 0xdf 'RR', # 0xe0 'LL', # 0xe1 -'[?]', # 0xe2 -'[?]', # 0xe3 -'[?]', # 0xe4 -'[?]', # 0xe5 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 '0', # 0xe6 '1', # 0xe7 '2', # 0xe8 @@ -239,19 +239,19 @@ data = ( '7', # 0xed '8', # 0xee '9', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x00d.py b/libs/common/unidecode/x00d.py index d105c437..093df92e 100644 --- a/libs/common/unidecode/x00d.py +++ b/libs/common/unidecode/x00d.py @@ -1,9 +1,9 @@ data = ( -'[?]', # 0x00 -'[?]', # 0x01 +None, # 0x00 +None, # 0x01 'N', # 0x02 'H', # 0x03 -'[?]', # 0x04 +None, # 0x04 'a', # 0x05 'aa', # 0x06 'i', # 0x07 @@ -12,11 +12,11 @@ data = ( 'uu', # 0x0a 'R', # 0x0b 'L', # 0x0c -'[?]', # 0x0d +None, # 0x0d 'e', # 0x0e 'ee', # 0x0f 'ai', # 0x10 -'[?]', # 0x11 +None, # 0x11 'o', # 0x12 'oo', # 0x13 'au', # 0x14 @@ -40,7 +40,7 @@ data = ( 'd', # 0x26 'dh', # 0x27 'n', # 0x28 -'[?]', # 0x29 +None, # 0x29 'p', # 0x2a 'ph', # 0x2b 'b', # 0x2c @@ -57,18 +57,18 @@ data = ( 'ss', # 0x37 's', # 0x38 'h', # 0x39 -'[?]', # 0x3a -'[?]', # 0x3b -'[?]', # 0x3c -'[?]', # 0x3d +None, # 0x3a +None, # 0x3b +None, # 0x3c +None, # 0x3d 'aa', # 0x3e 'i', # 0x3f 'ii', # 0x40 'u', # 0x41 'uu', # 0x42 'R', # 0x43 -'[?]', # 0x44 -'[?]', # 0x45 +None, # 0x44 +None, # 0x45 'e', # 0x46 'ee', # 0x47 'ai', # 0x48 @@ -77,30 +77,30 @@ data = ( 'oo', # 0x4b 'au', # 0x4c '', # 0x4d -'[?]', # 0x4e -'[?]', # 0x4f -'[?]', # 0x50 -'[?]', # 0x51 -'[?]', # 0x52 -'[?]', # 0x53 -'[?]', # 0x54 -'[?]', # 0x55 -'[?]', # 0x56 +None, # 0x4e +None, # 0x4f +None, # 0x50 +None, # 0x51 +None, # 0x52 +None, # 0x53 +None, # 0x54 +None, # 0x55 +None, # 0x56 '+', # 0x57 -'[?]', # 0x58 -'[?]', # 0x59 -'[?]', # 0x5a -'[?]', # 0x5b -'[?]', # 0x5c -'[?]', # 0x5d -'[?]', # 0x5e -'[?]', # 0x5f +None, # 0x58 +None, # 0x59 +None, # 0x5a +None, # 0x5b +None, # 0x5c +None, # 0x5d +None, # 0x5e +None, # 0x5f 'RR', # 0x60 'LL', # 0x61 -'[?]', # 0x62 -'[?]', # 0x63 -'[?]', # 0x64 -'[?]', # 0x65 +None, # 0x62 +None, # 0x63 +None, # 0x64 +None, # 0x65 '0', # 0x66 '1', # 0x67 '2', # 0x68 @@ -111,27 +111,27 @@ data = ( '7', # 0x6d '8', # 0x6e '9', # 0x6f -'[?]', # 0x70 -'[?]', # 0x71 -'[?]', # 0x72 -'[?]', # 0x73 -'[?]', # 0x74 -'[?]', # 0x75 -'[?]', # 0x76 -'[?]', # 0x77 -'[?]', # 0x78 -'[?]', # 0x79 -'[?]', # 0x7a -'[?]', # 0x7b -'[?]', # 0x7c -'[?]', # 0x7d -'[?]', # 0x7e -'[?]', # 0x7f -'[?]', # 0x80 -'[?]', # 0x81 +None, # 0x70 +None, # 0x71 +None, # 0x72 +None, # 0x73 +None, # 0x74 +None, # 0x75 +None, # 0x76 +None, # 0x77 +None, # 0x78 +None, # 0x79 +None, # 0x7a +None, # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f +None, # 0x80 +None, # 0x81 'N', # 0x82 'H', # 0x83 -'[?]', # 0x84 +None, # 0x84 'a', # 0x85 'aa', # 0x86 'ae', # 0x87 @@ -150,9 +150,9 @@ data = ( 'o', # 0x94 'oo', # 0x95 'au', # 0x96 -'[?]', # 0x97 -'[?]', # 0x98 -'[?]', # 0x99 +None, # 0x97 +None, # 0x98 +None, # 0x99 'k', # 0x9a 'kh', # 0x9b 'g', # 0x9c @@ -177,7 +177,7 @@ data = ( 'd', # 0xaf 'dh', # 0xb0 'n', # 0xb1 -'[?]', # 0xb2 +None, # 0xb2 'nd', # 0xb3 'p', # 0xb4 'ph', # 0xb5 @@ -187,10 +187,10 @@ data = ( 'mb', # 0xb9 'y', # 0xba 'r', # 0xbb -'[?]', # 0xbc +None, # 0xbc 'l', # 0xbd -'[?]', # 0xbe -'[?]', # 0xbf +None, # 0xbe +None, # 0xbf 'v', # 0xc0 'sh', # 0xc1 'ss', # 0xc2 @@ -198,23 +198,23 @@ data = ( 'h', # 0xc4 'll', # 0xc5 'f', # 0xc6 -'[?]', # 0xc7 -'[?]', # 0xc8 -'[?]', # 0xc9 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 '', # 0xca -'[?]', # 0xcb -'[?]', # 0xcc -'[?]', # 0xcd -'[?]', # 0xce +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce 'aa', # 0xcf 'ae', # 0xd0 'aae', # 0xd1 'i', # 0xd2 'ii', # 0xd3 'u', # 0xd4 -'[?]', # 0xd5 +None, # 0xd5 'uu', # 0xd6 -'[?]', # 0xd7 +None, # 0xd7 'R', # 0xd8 'e', # 0xd9 'ee', # 0xda @@ -223,35 +223,35 @@ data = ( 'oo', # 0xdd 'au', # 0xde 'L', # 0xdf -'[?]', # 0xe0 -'[?]', # 0xe1 -'[?]', # 0xe2 -'[?]', # 0xe3 -'[?]', # 0xe4 -'[?]', # 0xe5 -'[?]', # 0xe6 -'[?]', # 0xe7 -'[?]', # 0xe8 -'[?]', # 0xe9 -'[?]', # 0xea -'[?]', # 0xeb -'[?]', # 0xec -'[?]', # 0xed -'[?]', # 0xee -'[?]', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 'RR', # 0xf2 'LL', # 0xf3 ' . ', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x00e.py b/libs/common/unidecode/x00e.py index 775b5f4a..5f1c085d 100644 --- a/libs/common/unidecode/x00e.py +++ b/libs/common/unidecode/x00e.py @@ -1,5 +1,5 @@ data = ( -'[?]', # 0x00 +None, # 0x00 'k', # 0x01 'kh', # 0x02 'kh', # 0x03 @@ -58,10 +58,10 @@ data = ( 'u', # 0x38 'uu', # 0x39 '\'', # 0x3a -'[?]', # 0x3b -'[?]', # 0x3c -'[?]', # 0x3d -'[?]', # 0x3e +None, # 0x3b +None, # 0x3c +None, # 0x3d +None, # 0x3e 'Bh.', # 0x3f 'e', # 0x40 'ae', # 0x41 @@ -91,67 +91,67 @@ data = ( '9', # 0x59 ' // ', # 0x5a ' /// ', # 0x5b -'[?]', # 0x5c -'[?]', # 0x5d -'[?]', # 0x5e -'[?]', # 0x5f -'[?]', # 0x60 -'[?]', # 0x61 -'[?]', # 0x62 -'[?]', # 0x63 -'[?]', # 0x64 -'[?]', # 0x65 -'[?]', # 0x66 -'[?]', # 0x67 -'[?]', # 0x68 -'[?]', # 0x69 -'[?]', # 0x6a -'[?]', # 0x6b -'[?]', # 0x6c -'[?]', # 0x6d -'[?]', # 0x6e -'[?]', # 0x6f -'[?]', # 0x70 -'[?]', # 0x71 -'[?]', # 0x72 -'[?]', # 0x73 -'[?]', # 0x74 -'[?]', # 0x75 -'[?]', # 0x76 -'[?]', # 0x77 -'[?]', # 0x78 -'[?]', # 0x79 -'[?]', # 0x7a -'[?]', # 0x7b -'[?]', # 0x7c -'[?]', # 0x7d -'[?]', # 0x7e -'[?]', # 0x7f -'[?]', # 0x80 +None, # 0x5c +None, # 0x5d +None, # 0x5e +None, # 0x5f +None, # 0x60 +None, # 0x61 +None, # 0x62 +None, # 0x63 +None, # 0x64 +None, # 0x65 +None, # 0x66 +None, # 0x67 +None, # 0x68 +None, # 0x69 +None, # 0x6a +None, # 0x6b +None, # 0x6c +None, # 0x6d +None, # 0x6e +None, # 0x6f +None, # 0x70 +None, # 0x71 +None, # 0x72 +None, # 0x73 +None, # 0x74 +None, # 0x75 +None, # 0x76 +None, # 0x77 +None, # 0x78 +None, # 0x79 +None, # 0x7a +None, # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f +None, # 0x80 'k', # 0x81 'kh', # 0x82 -'[?]', # 0x83 +None, # 0x83 'kh', # 0x84 -'[?]', # 0x85 -'[?]', # 0x86 +None, # 0x85 +None, # 0x86 'ng', # 0x87 'ch', # 0x88 -'[?]', # 0x89 +None, # 0x89 's', # 0x8a -'[?]', # 0x8b -'[?]', # 0x8c +None, # 0x8b +None, # 0x8c 'ny', # 0x8d -'[?]', # 0x8e -'[?]', # 0x8f -'[?]', # 0x90 -'[?]', # 0x91 -'[?]', # 0x92 -'[?]', # 0x93 +None, # 0x8e +None, # 0x8f +None, # 0x90 +None, # 0x91 +None, # 0x92 +None, # 0x93 'd', # 0x94 'h', # 0x95 'th', # 0x96 'th', # 0x97 -'[?]', # 0x98 +None, # 0x98 'n', # 0x99 'b', # 0x9a 'p', # 0x9b @@ -159,19 +159,19 @@ data = ( 'f', # 0x9d 'ph', # 0x9e 'f', # 0x9f -'[?]', # 0xa0 +None, # 0xa0 'm', # 0xa1 'y', # 0xa2 'r', # 0xa3 -'[?]', # 0xa4 +None, # 0xa4 'l', # 0xa5 -'[?]', # 0xa6 +None, # 0xa6 'w', # 0xa7 -'[?]', # 0xa8 -'[?]', # 0xa9 +None, # 0xa8 +None, # 0xa9 's', # 0xaa 'h', # 0xab -'[?]', # 0xac +None, # 0xac '`', # 0xad '', # 0xae '~', # 0xaf @@ -185,28 +185,28 @@ data = ( 'yy', # 0xb7 'u', # 0xb8 'uu', # 0xb9 -'[?]', # 0xba +None, # 0xba 'o', # 0xbb 'l', # 0xbc 'ny', # 0xbd -'[?]', # 0xbe -'[?]', # 0xbf +None, # 0xbe +None, # 0xbf 'e', # 0xc0 'ei', # 0xc1 'o', # 0xc2 'ay', # 0xc3 'ai', # 0xc4 -'[?]', # 0xc5 +None, # 0xc5 '+', # 0xc6 -'[?]', # 0xc7 +None, # 0xc7 '', # 0xc8 '', # 0xc9 '', # 0xca '', # 0xcb '', # 0xcc 'M', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf +None, # 0xce +None, # 0xcf '0', # 0xd0 '1', # 0xd1 '2', # 0xd2 @@ -217,41 +217,41 @@ data = ( '7', # 0xd7 '8', # 0xd8 '9', # 0xd9 -'[?]', # 0xda -'[?]', # 0xdb +None, # 0xda +None, # 0xdb 'hn', # 0xdc 'hm', # 0xdd -'[?]', # 0xde -'[?]', # 0xdf -'[?]', # 0xe0 -'[?]', # 0xe1 -'[?]', # 0xe2 -'[?]', # 0xe3 -'[?]', # 0xe4 -'[?]', # 0xe5 -'[?]', # 0xe6 -'[?]', # 0xe7 -'[?]', # 0xe8 -'[?]', # 0xe9 -'[?]', # 0xea -'[?]', # 0xeb -'[?]', # 0xec -'[?]', # 0xed -'[?]', # 0xee -'[?]', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x00f.py b/libs/common/unidecode/x00f.py index 4c2410e0..8b66eb07 100644 --- a/libs/common/unidecode/x00f.py +++ b/libs/common/unidecode/x00f.py @@ -57,7 +57,7 @@ data = ( '_', # 0x37 '', # 0x38 '~', # 0x39 -'[?]', # 0x3a +None, # 0x3a ']', # 0x3b '[[', # 0x3c ']]', # 0x3d @@ -71,7 +71,7 @@ data = ( 'c', # 0x45 'ch', # 0x46 'j', # 0x47 -'[?]', # 0x48 +None, # 0x48 'ny', # 0x49 'tt', # 0x4a 'tth', # 0x4b @@ -106,12 +106,12 @@ data = ( 'a', # 0x68 'kss', # 0x69 'r', # 0x6a -'[?]', # 0x6b -'[?]', # 0x6c -'[?]', # 0x6d -'[?]', # 0x6e -'[?]', # 0x6f -'[?]', # 0x70 +None, # 0x6b +None, # 0x6c +None, # 0x6d +None, # 0x6e +None, # 0x6f +None, # 0x70 'aa', # 0x71 'i', # 0x72 'ii', # 0x73 @@ -139,10 +139,10 @@ data = ( '', # 0x89 '', # 0x8a '', # 0x8b -'[?]', # 0x8c -'[?]', # 0x8d -'[?]', # 0x8e -'[?]', # 0x8f +None, # 0x8c +None, # 0x8d +None, # 0x8e +None, # 0x8f 'k', # 0x90 'kh', # 0x91 'g', # 0x92 @@ -151,7 +151,7 @@ data = ( 'c', # 0x95 'ch', # 0x96 'j', # 0x97 -'[?]', # 0x98 +None, # 0x98 'ny', # 0x99 'tt', # 0x9a 'tth', # 0x9b @@ -188,7 +188,7 @@ data = ( 'w', # 0xba 'y', # 0xbb 'r', # 0xbc -'[?]', # 0xbd +None, # 0xbd 'X', # 0xbe ' :X: ', # 0xbf ' /O/ ', # 0xc0 @@ -204,54 +204,54 @@ data = ( '', # 0xca '', # 0xcb '', # 0xcc -'[?]', # 0xcd -'[?]', # 0xce +None, # 0xcd +None, # 0xce '', # 0xcf -'[?]', # 0xd0 -'[?]', # 0xd1 -'[?]', # 0xd2 -'[?]', # 0xd3 -'[?]', # 0xd4 -'[?]', # 0xd5 -'[?]', # 0xd6 -'[?]', # 0xd7 -'[?]', # 0xd8 -'[?]', # 0xd9 -'[?]', # 0xda -'[?]', # 0xdb -'[?]', # 0xdc -'[?]', # 0xdd -'[?]', # 0xde -'[?]', # 0xdf -'[?]', # 0xe0 -'[?]', # 0xe1 -'[?]', # 0xe2 -'[?]', # 0xe3 -'[?]', # 0xe4 -'[?]', # 0xe5 -'[?]', # 0xe6 -'[?]', # 0xe7 -'[?]', # 0xe8 -'[?]', # 0xe9 -'[?]', # 0xea -'[?]', # 0xeb -'[?]', # 0xec -'[?]', # 0xed -'[?]', # 0xee -'[?]', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x010.py b/libs/common/unidecode/x010.py index aaf820d9..7f9ef764 100644 --- a/libs/common/unidecode/x010.py +++ b/libs/common/unidecode/x010.py @@ -33,16 +33,16 @@ data = ( 'h', # 0x1f 'll', # 0x20 'a', # 0x21 -'[?]', # 0x22 +None, # 0x22 'i', # 0x23 'ii', # 0x24 'u', # 0x25 'uu', # 0x26 'e', # 0x27 -'[?]', # 0x28 +None, # 0x28 'o', # 0x29 'au', # 0x2a -'[?]', # 0x2b +None, # 0x2b 'aa', # 0x2c 'i', # 0x2d 'ii', # 0x2e @@ -50,19 +50,19 @@ data = ( 'uu', # 0x30 'e', # 0x31 'ai', # 0x32 -'[?]', # 0x33 -'[?]', # 0x34 -'[?]', # 0x35 +None, # 0x33 +None, # 0x34 +None, # 0x35 'N', # 0x36 '\'', # 0x37 ':', # 0x38 '', # 0x39 -'[?]', # 0x3a -'[?]', # 0x3b -'[?]', # 0x3c -'[?]', # 0x3d -'[?]', # 0x3e -'[?]', # 0x3f +None, # 0x3a +None, # 0x3b +None, # 0x3c +None, # 0x3d +None, # 0x3e +None, # 0x3f '0', # 0x40 '1', # 0x41 '2', # 0x42 @@ -89,76 +89,76 @@ data = ( 'RR', # 0x57 'L', # 0x58 'LL', # 0x59 -'[?]', # 0x5a -'[?]', # 0x5b -'[?]', # 0x5c -'[?]', # 0x5d -'[?]', # 0x5e -'[?]', # 0x5f -'[?]', # 0x60 -'[?]', # 0x61 -'[?]', # 0x62 -'[?]', # 0x63 -'[?]', # 0x64 -'[?]', # 0x65 -'[?]', # 0x66 -'[?]', # 0x67 -'[?]', # 0x68 -'[?]', # 0x69 -'[?]', # 0x6a -'[?]', # 0x6b -'[?]', # 0x6c -'[?]', # 0x6d -'[?]', # 0x6e -'[?]', # 0x6f -'[?]', # 0x70 -'[?]', # 0x71 -'[?]', # 0x72 -'[?]', # 0x73 -'[?]', # 0x74 -'[?]', # 0x75 -'[?]', # 0x76 -'[?]', # 0x77 -'[?]', # 0x78 -'[?]', # 0x79 -'[?]', # 0x7a -'[?]', # 0x7b -'[?]', # 0x7c -'[?]', # 0x7d -'[?]', # 0x7e -'[?]', # 0x7f -'[?]', # 0x80 -'[?]', # 0x81 -'[?]', # 0x82 -'[?]', # 0x83 -'[?]', # 0x84 -'[?]', # 0x85 -'[?]', # 0x86 -'[?]', # 0x87 -'[?]', # 0x88 -'[?]', # 0x89 -'[?]', # 0x8a -'[?]', # 0x8b -'[?]', # 0x8c -'[?]', # 0x8d -'[?]', # 0x8e -'[?]', # 0x8f -'[?]', # 0x90 -'[?]', # 0x91 -'[?]', # 0x92 -'[?]', # 0x93 -'[?]', # 0x94 -'[?]', # 0x95 -'[?]', # 0x96 -'[?]', # 0x97 -'[?]', # 0x98 -'[?]', # 0x99 -'[?]', # 0x9a -'[?]', # 0x9b -'[?]', # 0x9c -'[?]', # 0x9d -'[?]', # 0x9e -'[?]', # 0x9f +None, # 0x5a +None, # 0x5b +None, # 0x5c +None, # 0x5d +None, # 0x5e +None, # 0x5f +None, # 0x60 +None, # 0x61 +None, # 0x62 +None, # 0x63 +None, # 0x64 +None, # 0x65 +None, # 0x66 +None, # 0x67 +None, # 0x68 +None, # 0x69 +None, # 0x6a +None, # 0x6b +None, # 0x6c +None, # 0x6d +None, # 0x6e +None, # 0x6f +None, # 0x70 +None, # 0x71 +None, # 0x72 +None, # 0x73 +None, # 0x74 +None, # 0x75 +None, # 0x76 +None, # 0x77 +None, # 0x78 +None, # 0x79 +None, # 0x7a +None, # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f +None, # 0x80 +None, # 0x81 +None, # 0x82 +None, # 0x83 +None, # 0x84 +None, # 0x85 +None, # 0x86 +None, # 0x87 +None, # 0x88 +None, # 0x89 +None, # 0x8a +None, # 0x8b +None, # 0x8c +None, # 0x8d +None, # 0x8e +None, # 0x8f +None, # 0x90 +None, # 0x91 +None, # 0x92 +None, # 0x93 +None, # 0x94 +None, # 0x95 +None, # 0x96 +None, # 0x97 +None, # 0x98 +None, # 0x99 +None, # 0x9a +None, # 0x9b +None, # 0x9c +None, # 0x9d +None, # 0x9e +None, # 0x9f 'A', # 0xa0 'B', # 0xa1 'G', # 0xa2 @@ -197,16 +197,16 @@ data = ( 'W', # 0xc3 'Xh', # 0xc4 'OE', # 0xc5 -'[?]', # 0xc6 -'[?]', # 0xc7 -'[?]', # 0xc8 -'[?]', # 0xc9 -'[?]', # 0xca -'[?]', # 0xcb -'[?]', # 0xcc -'[?]', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf +None, # 0xc6 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf 'a', # 0xd0 'b', # 0xd1 'g', # 0xd2 @@ -246,12 +246,12 @@ data = ( 'xh', # 0xf4 'oe', # 0xf5 'f', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa ' // ', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x011.py b/libs/common/unidecode/x011.py index f0d8f929..51bfb0a0 100644 --- a/libs/common/unidecode/x011.py +++ b/libs/common/unidecode/x011.py @@ -89,11 +89,11 @@ data = ( 'pN', # 0x57 'hh', # 0x58 'Q', # 0x59 -'[?]', # 0x5a -'[?]', # 0x5b -'[?]', # 0x5c -'[?]', # 0x5d -'[?]', # 0x5e +None, # 0x5a +None, # 0x5b +None, # 0x5c +None, # 0x5d +None, # 0x5e '', # 0x5f '', # 0x60 'a', # 0x61 @@ -162,11 +162,11 @@ data = ( 'U-u', # 0xa0 'U-i', # 0xa1 'UU', # 0xa2 -'[?]', # 0xa3 -'[?]', # 0xa4 -'[?]', # 0xa5 -'[?]', # 0xa6 -'[?]', # 0xa7 +None, # 0xa3 +None, # 0xa4 +None, # 0xa5 +None, # 0xa6 +None, # 0xa7 'g', # 0xa8 'gg', # 0xa9 'gs', # 0xaa @@ -249,9 +249,9 @@ data = ( 'hm', # 0xf7 'hb', # 0xf8 'Q', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x012.py b/libs/common/unidecode/x012.py index f2670650..2314a559 100644 --- a/libs/common/unidecode/x012.py +++ b/libs/common/unidecode/x012.py @@ -6,7 +6,7 @@ data = ( 'hee', # 0x04 'he', # 0x05 'ho', # 0x06 -'[?]', # 0x07 +None, # 0x07 'la', # 0x08 'lu', # 0x09 'li', # 0x0a @@ -70,15 +70,15 @@ data = ( 'qee', # 0x44 'qe', # 0x45 'qo', # 0x46 -'[?]', # 0x47 +None, # 0x47 'qwa', # 0x48 -'[?]', # 0x49 +None, # 0x49 'qwi', # 0x4a 'qwaa', # 0x4b 'qwee', # 0x4c 'qwe', # 0x4d -'[?]', # 0x4e -'[?]', # 0x4f +None, # 0x4e +None, # 0x4f 'qha', # 0x50 'qhu', # 0x51 'qhi', # 0x52 @@ -86,15 +86,15 @@ data = ( 'qhee', # 0x54 'qhe', # 0x55 'qho', # 0x56 -'[?]', # 0x57 +None, # 0x57 'qhwa', # 0x58 -'[?]', # 0x59 +None, # 0x59 'qhwi', # 0x5a 'qhwaa', # 0x5b 'qhwee', # 0x5c 'qhwe', # 0x5d -'[?]', # 0x5e -'[?]', # 0x5f +None, # 0x5e +None, # 0x5f 'ba', # 0x60 'bu', # 0x61 'bi', # 0x62 @@ -134,15 +134,15 @@ data = ( 'xee', # 0x84 'xe', # 0x85 'xo', # 0x86 -'[?]', # 0x87 +None, # 0x87 'xwa', # 0x88 -'[?]', # 0x89 +None, # 0x89 'xwi', # 0x8a 'xwaa', # 0x8b 'xwee', # 0x8c 'xwe', # 0x8d -'[?]', # 0x8e -'[?]', # 0x8f +None, # 0x8e +None, # 0x8f 'na', # 0x90 'nu', # 0x91 'ni', # 0x92 @@ -161,7 +161,7 @@ data = ( 'nywa', # 0x9f '\'a', # 0xa0 '\'u', # 0xa1 -'[?]', # 0xa2 +None, # 0xa2 '\'aa', # 0xa3 '\'ee', # 0xa4 '\'e', # 0xa5 @@ -174,15 +174,15 @@ data = ( 'kee', # 0xac 'ke', # 0xad 'ko', # 0xae -'[?]', # 0xaf +None, # 0xaf 'kwa', # 0xb0 -'[?]', # 0xb1 +None, # 0xb1 'kwi', # 0xb2 'kwaa', # 0xb3 'kwee', # 0xb4 'kwe', # 0xb5 -'[?]', # 0xb6 -'[?]', # 0xb7 +None, # 0xb6 +None, # 0xb7 'kxa', # 0xb8 'kxu', # 0xb9 'kxi', # 0xba @@ -190,15 +190,15 @@ data = ( 'kxee', # 0xbc 'kxe', # 0xbd 'kxo', # 0xbe -'[?]', # 0xbf +None, # 0xbf 'kxwa', # 0xc0 -'[?]', # 0xc1 +None, # 0xc1 'kxwi', # 0xc2 'kxwaa', # 0xc3 'kxwee', # 0xc4 'kxwe', # 0xc5 -'[?]', # 0xc6 -'[?]', # 0xc7 +None, # 0xc6 +None, # 0xc7 'wa', # 0xc8 'wu', # 0xc9 'wi', # 0xca @@ -206,7 +206,7 @@ data = ( 'wee', # 0xcc 'we', # 0xcd 'wo', # 0xce -'[?]', # 0xcf +None, # 0xcf '`a', # 0xd0 '`u', # 0xd1 '`i', # 0xd2 @@ -214,7 +214,7 @@ data = ( '`ee', # 0xd4 '`e', # 0xd5 '`o', # 0xd6 -'[?]', # 0xd7 +None, # 0xd7 'za', # 0xd8 'zu', # 0xd9 'zi', # 0xda @@ -238,7 +238,7 @@ data = ( 'yee', # 0xec 'ye', # 0xed 'yo', # 0xee -'[?]', # 0xef +None, # 0xef 'da', # 0xf0 'du', # 0xf1 'di', # 0xf2 diff --git a/libs/common/unidecode/x013.py b/libs/common/unidecode/x013.py index 8a8c3f9c..f1e4b997 100644 --- a/libs/common/unidecode/x013.py +++ b/libs/common/unidecode/x013.py @@ -14,15 +14,15 @@ data = ( 'gee', # 0x0c 'ge', # 0x0d 'go', # 0x0e -'[?]', # 0x0f +None, # 0x0f 'gwa', # 0x10 -'[?]', # 0x11 +None, # 0x11 'gwi', # 0x12 'gwaa', # 0x13 'gwee', # 0x14 'gwe', # 0x15 -'[?]', # 0x16 -'[?]', # 0x17 +None, # 0x16 +None, # 0x17 'gga', # 0x18 'ggu', # 0x19 'ggi', # 0x1a @@ -30,7 +30,7 @@ data = ( 'ggee', # 0x1c 'gge', # 0x1d 'ggo', # 0x1e -'[?]', # 0x1f +None, # 0x1f 'tha', # 0x20 'thu', # 0x21 'thi', # 0x22 @@ -70,7 +70,7 @@ data = ( 'tzee', # 0x44 'tze', # 0x45 'tzo', # 0x46 -'[?]', # 0x47 +None, # 0x47 'fa', # 0x48 'fu', # 0x49 'fi', # 0x4a @@ -90,12 +90,12 @@ data = ( 'rya', # 0x58 'mya', # 0x59 'fya', # 0x5a -'[?]', # 0x5b -'[?]', # 0x5c -'[?]', # 0x5d -'[?]', # 0x5e -'[?]', # 0x5f -'[?]', # 0x60 +None, # 0x5b +None, # 0x5c +None, # 0x5d +None, # 0x5e +None, # 0x5f +None, # 0x60 ' ', # 0x61 '.', # 0x62 ',', # 0x63 @@ -124,41 +124,41 @@ data = ( '90+', # 0x7a '100+', # 0x7b '10,000+', # 0x7c -'[?]', # 0x7d -'[?]', # 0x7e -'[?]', # 0x7f -'[?]', # 0x80 -'[?]', # 0x81 -'[?]', # 0x82 -'[?]', # 0x83 -'[?]', # 0x84 -'[?]', # 0x85 -'[?]', # 0x86 -'[?]', # 0x87 -'[?]', # 0x88 -'[?]', # 0x89 -'[?]', # 0x8a -'[?]', # 0x8b -'[?]', # 0x8c -'[?]', # 0x8d -'[?]', # 0x8e -'[?]', # 0x8f -'[?]', # 0x90 -'[?]', # 0x91 -'[?]', # 0x92 -'[?]', # 0x93 -'[?]', # 0x94 -'[?]', # 0x95 -'[?]', # 0x96 -'[?]', # 0x97 -'[?]', # 0x98 -'[?]', # 0x99 -'[?]', # 0x9a -'[?]', # 0x9b -'[?]', # 0x9c -'[?]', # 0x9d -'[?]', # 0x9e -'[?]', # 0x9f +None, # 0x7d +None, # 0x7e +None, # 0x7f +None, # 0x80 +None, # 0x81 +None, # 0x82 +None, # 0x83 +None, # 0x84 +None, # 0x85 +None, # 0x86 +None, # 0x87 +None, # 0x88 +None, # 0x89 +None, # 0x8a +None, # 0x8b +None, # 0x8c +None, # 0x8d +None, # 0x8e +None, # 0x8f +None, # 0x90 +None, # 0x91 +None, # 0x92 +None, # 0x93 +None, # 0x94 +None, # 0x95 +None, # 0x96 +None, # 0x97 +None, # 0x98 +None, # 0x99 +None, # 0x9a +None, # 0x9b +None, # 0x9c +None, # 0x9d +None, # 0x9e +None, # 0x9f 'a', # 0xa0 'e', # 0xa1 'i', # 0xa2 @@ -244,14 +244,14 @@ data = ( 'yo', # 0xf2 'yu', # 0xf3 'yv', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x014.py b/libs/common/unidecode/x014.py index e8c01809..0f56a91f 100644 --- a/libs/common/unidecode/x014.py +++ b/libs/common/unidecode/x014.py @@ -1,5 +1,5 @@ data = ( -'[?]', # 0x00 +None, # 0x00 'e', # 0x01 'aai', # 0x02 'i', # 0x03 @@ -37,7 +37,7 @@ data = ( 'n', # 0x23 'w', # 0x24 'n', # 0x25 -'[?]', # 0x26 +None, # 0x26 'w', # 0x27 'c', # 0x28 '?', # 0x29 diff --git a/libs/common/unidecode/x016.py b/libs/common/unidecode/x016.py index 613d1e90..0998689d 100644 --- a/libs/common/unidecode/x016.py +++ b/libs/common/unidecode/x016.py @@ -118,15 +118,15 @@ data = ( 'nngoo', # 0x74 'nnga', # 0x75 'nngaa', # 0x76 -'[?]', # 0x77 -'[?]', # 0x78 -'[?]', # 0x79 -'[?]', # 0x7a -'[?]', # 0x7b -'[?]', # 0x7c -'[?]', # 0x7d -'[?]', # 0x7e -'[?]', # 0x7f +None, # 0x77 +None, # 0x78 +None, # 0x79 +None, # 0x7a +None, # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f ' ', # 0x80 'b', # 0x81 'l', # 0x82 @@ -156,9 +156,9 @@ data = ( 'p', # 0x9a '<', # 0x9b '>', # 0x9c -'[?]', # 0x9d -'[?]', # 0x9e -'[?]', # 0x9f +None, # 0x9d +None, # 0x9e +None, # 0x9f 'f', # 0xa0 'v', # 0xa1 'u', # 0xa2 @@ -240,18 +240,18 @@ data = ( '17', # 0xee '18', # 0xef '19', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x017.py b/libs/common/unidecode/x017.py index e0a8f447..09ca04c2 100644 --- a/libs/common/unidecode/x017.py +++ b/libs/common/unidecode/x017.py @@ -1,132 +1,132 @@ data = ( -'[?]', # 0x00 -'[?]', # 0x01 -'[?]', # 0x02 -'[?]', # 0x03 -'[?]', # 0x04 -'[?]', # 0x05 -'[?]', # 0x06 -'[?]', # 0x07 -'[?]', # 0x08 -'[?]', # 0x09 -'[?]', # 0x0a -'[?]', # 0x0b -'[?]', # 0x0c -'[?]', # 0x0d -'[?]', # 0x0e -'[?]', # 0x0f -'[?]', # 0x10 -'[?]', # 0x11 -'[?]', # 0x12 -'[?]', # 0x13 -'[?]', # 0x14 -'[?]', # 0x15 -'[?]', # 0x16 -'[?]', # 0x17 -'[?]', # 0x18 -'[?]', # 0x19 -'[?]', # 0x1a -'[?]', # 0x1b -'[?]', # 0x1c -'[?]', # 0x1d -'[?]', # 0x1e -'[?]', # 0x1f -'[?]', # 0x20 -'[?]', # 0x21 -'[?]', # 0x22 -'[?]', # 0x23 -'[?]', # 0x24 -'[?]', # 0x25 -'[?]', # 0x26 -'[?]', # 0x27 -'[?]', # 0x28 -'[?]', # 0x29 -'[?]', # 0x2a -'[?]', # 0x2b -'[?]', # 0x2c -'[?]', # 0x2d -'[?]', # 0x2e -'[?]', # 0x2f -'[?]', # 0x30 -'[?]', # 0x31 -'[?]', # 0x32 -'[?]', # 0x33 -'[?]', # 0x34 -'[?]', # 0x35 -'[?]', # 0x36 -'[?]', # 0x37 -'[?]', # 0x38 -'[?]', # 0x39 -'[?]', # 0x3a -'[?]', # 0x3b -'[?]', # 0x3c -'[?]', # 0x3d -'[?]', # 0x3e -'[?]', # 0x3f -'[?]', # 0x40 -'[?]', # 0x41 -'[?]', # 0x42 -'[?]', # 0x43 -'[?]', # 0x44 -'[?]', # 0x45 -'[?]', # 0x46 -'[?]', # 0x47 -'[?]', # 0x48 -'[?]', # 0x49 -'[?]', # 0x4a -'[?]', # 0x4b -'[?]', # 0x4c -'[?]', # 0x4d -'[?]', # 0x4e -'[?]', # 0x4f -'[?]', # 0x50 -'[?]', # 0x51 -'[?]', # 0x52 -'[?]', # 0x53 -'[?]', # 0x54 -'[?]', # 0x55 -'[?]', # 0x56 -'[?]', # 0x57 -'[?]', # 0x58 -'[?]', # 0x59 -'[?]', # 0x5a -'[?]', # 0x5b -'[?]', # 0x5c -'[?]', # 0x5d -'[?]', # 0x5e -'[?]', # 0x5f -'[?]', # 0x60 -'[?]', # 0x61 -'[?]', # 0x62 -'[?]', # 0x63 -'[?]', # 0x64 -'[?]', # 0x65 -'[?]', # 0x66 -'[?]', # 0x67 -'[?]', # 0x68 -'[?]', # 0x69 -'[?]', # 0x6a -'[?]', # 0x6b -'[?]', # 0x6c -'[?]', # 0x6d -'[?]', # 0x6e -'[?]', # 0x6f -'[?]', # 0x70 -'[?]', # 0x71 -'[?]', # 0x72 -'[?]', # 0x73 -'[?]', # 0x74 -'[?]', # 0x75 -'[?]', # 0x76 -'[?]', # 0x77 -'[?]', # 0x78 -'[?]', # 0x79 -'[?]', # 0x7a -'[?]', # 0x7b -'[?]', # 0x7c -'[?]', # 0x7d -'[?]', # 0x7e -'[?]', # 0x7f +None, # 0x00 +None, # 0x01 +None, # 0x02 +None, # 0x03 +None, # 0x04 +None, # 0x05 +None, # 0x06 +None, # 0x07 +None, # 0x08 +None, # 0x09 +None, # 0x0a +None, # 0x0b +None, # 0x0c +None, # 0x0d +None, # 0x0e +None, # 0x0f +None, # 0x10 +None, # 0x11 +None, # 0x12 +None, # 0x13 +None, # 0x14 +None, # 0x15 +None, # 0x16 +None, # 0x17 +None, # 0x18 +None, # 0x19 +None, # 0x1a +None, # 0x1b +None, # 0x1c +None, # 0x1d +None, # 0x1e +None, # 0x1f +None, # 0x20 +None, # 0x21 +None, # 0x22 +None, # 0x23 +None, # 0x24 +None, # 0x25 +None, # 0x26 +None, # 0x27 +None, # 0x28 +None, # 0x29 +None, # 0x2a +None, # 0x2b +None, # 0x2c +None, # 0x2d +None, # 0x2e +None, # 0x2f +None, # 0x30 +None, # 0x31 +None, # 0x32 +None, # 0x33 +None, # 0x34 +None, # 0x35 +None, # 0x36 +None, # 0x37 +None, # 0x38 +None, # 0x39 +None, # 0x3a +None, # 0x3b +None, # 0x3c +None, # 0x3d +None, # 0x3e +None, # 0x3f +None, # 0x40 +None, # 0x41 +None, # 0x42 +None, # 0x43 +None, # 0x44 +None, # 0x45 +None, # 0x46 +None, # 0x47 +None, # 0x48 +None, # 0x49 +None, # 0x4a +None, # 0x4b +None, # 0x4c +None, # 0x4d +None, # 0x4e +None, # 0x4f +None, # 0x50 +None, # 0x51 +None, # 0x52 +None, # 0x53 +None, # 0x54 +None, # 0x55 +None, # 0x56 +None, # 0x57 +None, # 0x58 +None, # 0x59 +None, # 0x5a +None, # 0x5b +None, # 0x5c +None, # 0x5d +None, # 0x5e +None, # 0x5f +None, # 0x60 +None, # 0x61 +None, # 0x62 +None, # 0x63 +None, # 0x64 +None, # 0x65 +None, # 0x66 +None, # 0x67 +None, # 0x68 +None, # 0x69 +None, # 0x6a +None, # 0x6b +None, # 0x6c +None, # 0x6d +None, # 0x6e +None, # 0x6f +None, # 0x70 +None, # 0x71 +None, # 0x72 +None, # 0x73 +None, # 0x74 +None, # 0x75 +None, # 0x76 +None, # 0x77 +None, # 0x78 +None, # 0x79 +None, # 0x7a +None, # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f 'k', # 0x80 'kh', # 0x81 'g', # 0x82 @@ -220,9 +220,9 @@ data = ( ' /// ', # 0xda 'KR', # 0xdb '\'', # 0xdc -'[?]', # 0xdd -'[?]', # 0xde -'[?]', # 0xdf +None, # 0xdd +None, # 0xde +None, # 0xdf '0', # 0xe0 '1', # 0xe1 '2', # 0xe2 @@ -233,25 +233,25 @@ data = ( '7', # 0xe7 '8', # 0xe8 '9', # 0xe9 -'[?]', # 0xea -'[?]', # 0xeb -'[?]', # 0xec -'[?]', # 0xed -'[?]', # 0xee -'[?]', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x018.py b/libs/common/unidecode/x018.py index 3162a010..1b52c8aa 100644 --- a/libs/common/unidecode/x018.py +++ b/libs/common/unidecode/x018.py @@ -14,7 +14,7 @@ data = ( '', # 0x0c '', # 0x0d '', # 0x0e -'[?]', # 0x0f +None, # 0x0f '0', # 0x10 '1', # 0x11 '2', # 0x12 @@ -25,12 +25,12 @@ data = ( '7', # 0x17 '8', # 0x18 '9', # 0x19 -'[?]', # 0x1a -'[?]', # 0x1b -'[?]', # 0x1c -'[?]', # 0x1d -'[?]', # 0x1e -'[?]', # 0x1f +None, # 0x1a +None, # 0x1b +None, # 0x1c +None, # 0x1d +None, # 0x1e +None, # 0x1f 'a', # 0x20 'e', # 0x21 'i', # 0x22 @@ -119,15 +119,15 @@ data = ( 'r', # 0x75 'f', # 0x76 'zh', # 0x77 -'[?]', # 0x78 -'[?]', # 0x79 -'[?]', # 0x7a -'[?]', # 0x7b -'[?]', # 0x7c -'[?]', # 0x7d -'[?]', # 0x7e -'[?]', # 0x7f -'[?]', # 0x80 +None, # 0x78 +None, # 0x79 +None, # 0x7a +None, # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f +None, # 0x80 'H', # 0x81 'X', # 0x82 'W', # 0x83 @@ -169,89 +169,89 @@ data = ( 'y', # 0xa7 'bh', # 0xa8 '\'', # 0xa9 -'[?]', # 0xaa -'[?]', # 0xab -'[?]', # 0xac -'[?]', # 0xad -'[?]', # 0xae -'[?]', # 0xaf -'[?]', # 0xb0 -'[?]', # 0xb1 -'[?]', # 0xb2 -'[?]', # 0xb3 -'[?]', # 0xb4 -'[?]', # 0xb5 -'[?]', # 0xb6 -'[?]', # 0xb7 -'[?]', # 0xb8 -'[?]', # 0xb9 -'[?]', # 0xba -'[?]', # 0xbb -'[?]', # 0xbc -'[?]', # 0xbd -'[?]', # 0xbe -'[?]', # 0xbf -'[?]', # 0xc0 -'[?]', # 0xc1 -'[?]', # 0xc2 -'[?]', # 0xc3 -'[?]', # 0xc4 -'[?]', # 0xc5 -'[?]', # 0xc6 -'[?]', # 0xc7 -'[?]', # 0xc8 -'[?]', # 0xc9 -'[?]', # 0xca -'[?]', # 0xcb -'[?]', # 0xcc -'[?]', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf -'[?]', # 0xd0 -'[?]', # 0xd1 -'[?]', # 0xd2 -'[?]', # 0xd3 -'[?]', # 0xd4 -'[?]', # 0xd5 -'[?]', # 0xd6 -'[?]', # 0xd7 -'[?]', # 0xd8 -'[?]', # 0xd9 -'[?]', # 0xda -'[?]', # 0xdb -'[?]', # 0xdc -'[?]', # 0xdd -'[?]', # 0xde -'[?]', # 0xdf -'[?]', # 0xe0 -'[?]', # 0xe1 -'[?]', # 0xe2 -'[?]', # 0xe3 -'[?]', # 0xe4 -'[?]', # 0xe5 -'[?]', # 0xe6 -'[?]', # 0xe7 -'[?]', # 0xe8 -'[?]', # 0xe9 -'[?]', # 0xea -'[?]', # 0xeb -'[?]', # 0xec -'[?]', # 0xed -'[?]', # 0xee -'[?]', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xaa +None, # 0xab +None, # 0xac +None, # 0xad +None, # 0xae +None, # 0xaf +None, # 0xb0 +None, # 0xb1 +None, # 0xb2 +None, # 0xb3 +None, # 0xb4 +None, # 0xb5 +None, # 0xb6 +None, # 0xb7 +None, # 0xb8 +None, # 0xb9 +None, # 0xba +None, # 0xbb +None, # 0xbc +None, # 0xbd +None, # 0xbe +None, # 0xbf +None, # 0xc0 +None, # 0xc1 +None, # 0xc2 +None, # 0xc3 +None, # 0xc4 +None, # 0xc5 +None, # 0xc6 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x01e.py b/libs/common/unidecode/x01e.py index 606576b6..540786a8 100644 --- a/libs/common/unidecode/x01e.py +++ b/libs/common/unidecode/x01e.py @@ -155,10 +155,10 @@ data = ( 'y', # 0x99 'a', # 0x9a 'S', # 0x9b -'[?]', # 0x9c -'[?]', # 0x9d +None, # 0x9c +None, # 0x9d 'Ss', # 0x9e -'[?]', # 0x9f +None, # 0x9f 'A', # 0xa0 'a', # 0xa1 'A', # 0xa2 @@ -249,9 +249,9 @@ data = ( 'y', # 0xf7 'Y', # 0xf8 'y', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x01f.py b/libs/common/unidecode/x01f.py index bcd2dec5..f65fc6bc 100644 --- a/libs/common/unidecode/x01f.py +++ b/libs/common/unidecode/x01f.py @@ -21,16 +21,16 @@ data = ( 'e', # 0x13 'e', # 0x14 'e', # 0x15 -'[?]', # 0x16 -'[?]', # 0x17 +None, # 0x16 +None, # 0x17 'E', # 0x18 'E', # 0x19 'E', # 0x1a 'E', # 0x1b 'E', # 0x1c 'E', # 0x1d -'[?]', # 0x1e -'[?]', # 0x1f +None, # 0x1e +None, # 0x1f 'e', # 0x20 'e', # 0x21 'e', # 0x22 @@ -69,16 +69,16 @@ data = ( 'o', # 0x43 'o', # 0x44 'o', # 0x45 -'[?]', # 0x46 -'[?]', # 0x47 +None, # 0x46 +None, # 0x47 'O', # 0x48 'O', # 0x49 'O', # 0x4a 'O', # 0x4b 'O', # 0x4c 'O', # 0x4d -'[?]', # 0x4e -'[?]', # 0x4f +None, # 0x4e +None, # 0x4f 'u', # 0x50 'u', # 0x51 'u', # 0x52 @@ -87,13 +87,13 @@ data = ( 'u', # 0x55 'u', # 0x56 'u', # 0x57 -'[?]', # 0x58 +None, # 0x58 'U', # 0x59 -'[?]', # 0x5a +None, # 0x5a 'U', # 0x5b -'[?]', # 0x5c +None, # 0x5c 'U', # 0x5d -'[?]', # 0x5e +None, # 0x5e 'U', # 0x5f 'o', # 0x60 'o', # 0x61 @@ -125,8 +125,8 @@ data = ( 'u', # 0x7b 'o', # 0x7c 'o', # 0x7d -'[?]', # 0x7e -'[?]', # 0x7f +None, # 0x7e +None, # 0x7f 'a', # 0x80 'a', # 0x81 'a', # 0x82 @@ -180,7 +180,7 @@ data = ( 'a', # 0xb2 'a', # 0xb3 'a', # 0xb4 -'[?]', # 0xb5 +None, # 0xb5 'a', # 0xb6 'a', # 0xb7 'A', # 0xb8 @@ -196,7 +196,7 @@ data = ( 'e', # 0xc2 'e', # 0xc3 'e', # 0xc4 -'[?]', # 0xc5 +None, # 0xc5 'e', # 0xc6 'e', # 0xc7 'E', # 0xc8 @@ -211,15 +211,15 @@ data = ( 'i', # 0xd1 'i', # 0xd2 'i', # 0xd3 -'[?]', # 0xd4 -'[?]', # 0xd5 +None, # 0xd4 +None, # 0xd5 'i', # 0xd6 'i', # 0xd7 'I', # 0xd8 'I', # 0xd9 'I', # 0xda 'I', # 0xdb -'[?]', # 0xdc +None, # 0xdc '`\'', # 0xdd '`\'', # 0xde '`~', # 0xdf @@ -239,12 +239,12 @@ data = ( '"`', # 0xed '"\'', # 0xee '`', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 +None, # 0xf0 +None, # 0xf1 'o', # 0xf2 'o', # 0xf3 'o', # 0xf4 -'[?]', # 0xf5 +None, # 0xf5 'o', # 0xf6 'o', # 0xf7 'O', # 0xf8 diff --git a/libs/common/unidecode/x020.py b/libs/common/unidecode/x020.py index 46425bf0..60ff9616 100644 --- a/libs/common/unidecode/x020.py +++ b/libs/common/unidecode/x020.py @@ -73,38 +73,42 @@ data = ( '??', # 0x47 '?!', # 0x48 '!?', # 0x49 -'7', # 0x4a + +# Tironian note standing for Latin "et". Still used as an ampersand +# in modern Irish. See https://github.com/avian2/unidecode/issues/57 +'&', # 0x4a + 'PP', # 0x4b '(]', # 0x4c '[)', # 0x4d '*', # 0x4e -'[?]', # 0x4f -'[?]', # 0x50 -'[?]', # 0x51 +None, # 0x4f +None, # 0x50 +None, # 0x51 '%', # 0x52 '~', # 0x53 -'[?]', # 0x54 -'[?]', # 0x55 -'[?]', # 0x56 +None, # 0x54 +None, # 0x55 +None, # 0x56 "''''", # 0x57 -'[?]', # 0x58 -'[?]', # 0x59 -'[?]', # 0x5a -'[?]', # 0x5b -'[?]', # 0x5c -'[?]', # 0x5d -'[?]', # 0x5e +None, # 0x58 +None, # 0x59 +None, # 0x5a +None, # 0x5b +None, # 0x5c +None, # 0x5d +None, # 0x5e ' ', # 0x5f '', # 0x60 -'[?]', # 0x61 -'[?]', # 0x62 -'[?]', # 0x63 -'[?]', # 0x64 -'[?]', # 0x65 -'[?]', # 0x66 -'[?]', # 0x67 -'[?]', # 0x68 -'[?]', # 0x69 +None, # 0x61 +None, # 0x62 +None, # 0x63 +None, # 0x64 +None, # 0x65 +None, # 0x66 +None, # 0x67 +None, # 0x68 +None, # 0x69 '', # 0x6a '', # 0x6b '', # 0x6c @@ -142,12 +146,12 @@ data = ( '=', # 0x8c '(', # 0x8d ')', # 0x8e -'[?]', # 0x8f +None, # 0x8f 'a', # 0x90 'e', # 0x91 'o', # 0x92 'x', # 0x93 -'[?]', # 0x94 +None, # 0x94 'h', # 0x95 'k', # 0x96 'l', # 0x97 @@ -156,9 +160,9 @@ data = ( 'p', # 0x9a 's', # 0x9b 't', # 0x9c -'[?]', # 0x9d -'[?]', # 0x9e -'[?]', # 0x9f +None, # 0x9d +None, # 0x9e +None, # 0x9f 'ECU', # 0xa0 'CL', # 0xa1 'Cr', # 0xa2 @@ -191,22 +195,22 @@ data = ( 'R', # 0xbd 'l', # 0xbe 'BTC', # 0xbf -'[?]', # 0xc0 -'[?]', # 0xc1 -'[?]', # 0xc2 -'[?]', # 0xc3 -'[?]', # 0xc4 -'[?]', # 0xc5 -'[?]', # 0xc6 -'[?]', # 0xc7 -'[?]', # 0xc8 -'[?]', # 0xc9 -'[?]', # 0xca -'[?]', # 0xcb -'[?]', # 0xcc -'[?]', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf +None, # 0xc0 +None, # 0xc1 +None, # 0xc2 +None, # 0xc3 +None, # 0xc4 +None, # 0xc5 +None, # 0xc6 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf '', # 0xd0 '', # 0xd1 '', # 0xd2 @@ -227,31 +231,31 @@ data = ( '', # 0xe1 '', # 0xe2 '', # 0xe3 -'[?]', # 0xe4 +None, # 0xe4 '', # 0xe5 -'[?]', # 0xe6 -'[?]', # 0xe7 -'[?]', # 0xe8 -'[?]', # 0xe9 -'[?]', # 0xea -'[?]', # 0xeb -'[?]', # 0xec -'[?]', # 0xed -'[?]', # 0xee -'[?]', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x021.py b/libs/common/unidecode/x021.py index 29f05fd4..6bd37f7e 100644 --- a/libs/common/unidecode/x021.py +++ b/libs/common/unidecode/x021.py @@ -22,7 +22,7 @@ data = ( '', # 0x14 'N', # 0x15 'No. ', # 0x16 -'', # 0x17 +'(p)', # 0x17 '', # 0x18 'P', # 0x19 'Q', # 0x1a @@ -37,7 +37,7 @@ data = ( '', # 0x23 'Z', # 0x24 '', # 0x25 -'', # 0x26 +'ohm', # 0x26 '', # 0x27 'Z', # 0x28 '', # 0x29 @@ -63,22 +63,22 @@ data = ( '', # 0x3d '', # 0x3e '', # 0x3f -'[?]', # 0x40 -'[?]', # 0x41 -'[?]', # 0x42 -'[?]', # 0x43 -'[?]', # 0x44 +None, # 0x40 +None, # 0x41 +None, # 0x42 +None, # 0x43 +None, # 0x44 'D', # 0x45 'd', # 0x46 'e', # 0x47 'i', # 0x48 'j', # 0x49 -'[?]', # 0x4a -'[?]', # 0x4b -'[?]', # 0x4c -'[?]', # 0x4d +None, # 0x4a +None, # 0x4b +None, # 0x4c +None, # 0x4d 'F', # 0x4e -'[?]', # 0x4f +None, # 0x4f ' 1/7 ', # 0x50 ' 1/9 ', # 0x51 ' 1/10 ', # 0x52 @@ -131,18 +131,18 @@ data = ( 'D)', # 0x81 '((|))', # 0x82 ')', # 0x83 -'[?]', # 0x84 -'[?]', # 0x85 -'[?]', # 0x86 -'[?]', # 0x87 -'[?]', # 0x88 +None, # 0x84 +None, # 0x85 +None, # 0x86 +None, # 0x87 +None, # 0x88 ' 0/3 ', # 0x89 -'[?]', # 0x8a -'[?]', # 0x8b -'[?]', # 0x8c -'[?]', # 0x8d -'[?]', # 0x8e -'[?]', # 0x8f +None, # 0x8a +None, # 0x8b +None, # 0x8c +None, # 0x8d +None, # 0x8e +None, # 0x8f '-', # 0x90 '|', # 0x91 '-', # 0x92 @@ -243,15 +243,15 @@ data = ( '\\', # 0xf1 '\\', # 0xf2 '|', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x022.py b/libs/common/unidecode/x022.py index e38fb5cc..f2db33f0 100644 --- a/libs/common/unidecode/x022.py +++ b/libs/common/unidecode/x022.py @@ -1,257 +1,257 @@ data = ( -'[?]', # 0x00 -'[?]', # 0x01 -'[?]', # 0x02 -'[?]', # 0x03 -'[?]', # 0x04 -'[?]', # 0x05 -'[?]', # 0x06 -'[?]', # 0x07 -'[?]', # 0x08 -'[?]', # 0x09 -'[?]', # 0x0a -'[?]', # 0x0b -'[?]', # 0x0c -'[?]', # 0x0d -'[?]', # 0x0e -'[?]', # 0x0f -'[?]', # 0x10 -'[?]', # 0x11 +None, # 0x00 +None, # 0x01 +None, # 0x02 +None, # 0x03 +None, # 0x04 +None, # 0x05 +None, # 0x06 +None, # 0x07 +None, # 0x08 +None, # 0x09 +None, # 0x0a +None, # 0x0b +None, # 0x0c +None, # 0x0d +None, # 0x0e +None, # 0x0f +None, # 0x10 +None, # 0x11 '-', # 0x12 -'[?]', # 0x13 -'[?]', # 0x14 +None, # 0x13 +None, # 0x14 '/', # 0x15 '\\', # 0x16 '*', # 0x17 -'[?]', # 0x18 -'[?]', # 0x19 -'[?]', # 0x1a -'[?]', # 0x1b -'[?]', # 0x1c -'[?]', # 0x1d -'[?]', # 0x1e -'[?]', # 0x1f -'[?]', # 0x20 -'[?]', # 0x21 -'[?]', # 0x22 +None, # 0x18 +None, # 0x19 +None, # 0x1a +None, # 0x1b +None, # 0x1c +None, # 0x1d +None, # 0x1e +None, # 0x1f +None, # 0x20 +None, # 0x21 +None, # 0x22 '|', # 0x23 -'[?]', # 0x24 -'[?]', # 0x25 -'[?]', # 0x26 -'[?]', # 0x27 -'[?]', # 0x28 -'[?]', # 0x29 -'[?]', # 0x2a -'[?]', # 0x2b -'[?]', # 0x2c -'[?]', # 0x2d -'[?]', # 0x2e -'[?]', # 0x2f -'[?]', # 0x30 -'[?]', # 0x31 -'[?]', # 0x32 -'[?]', # 0x33 -'[?]', # 0x34 -'[?]', # 0x35 +None, # 0x24 +None, # 0x25 +None, # 0x26 +None, # 0x27 +None, # 0x28 +None, # 0x29 +None, # 0x2a +None, # 0x2b +None, # 0x2c +None, # 0x2d +None, # 0x2e +None, # 0x2f +None, # 0x30 +None, # 0x31 +None, # 0x32 +None, # 0x33 +None, # 0x34 +None, # 0x35 ':', # 0x36 -'[?]', # 0x37 -'[?]', # 0x38 -'[?]', # 0x39 -'[?]', # 0x3a -'[?]', # 0x3b +None, # 0x37 +None, # 0x38 +None, # 0x39 +None, # 0x3a +None, # 0x3b '~', # 0x3c -'[?]', # 0x3d -'[?]', # 0x3e -'[?]', # 0x3f -'[?]', # 0x40 -'[?]', # 0x41 -'[?]', # 0x42 -'[?]', # 0x43 -'[?]', # 0x44 -'[?]', # 0x45 -'[?]', # 0x46 -'[?]', # 0x47 -'[?]', # 0x48 -'[?]', # 0x49 -'[?]', # 0x4a -'[?]', # 0x4b -'[?]', # 0x4c -'[?]', # 0x4d -'[?]', # 0x4e -'[?]', # 0x4f -'[?]', # 0x50 -'[?]', # 0x51 -'[?]', # 0x52 -'[?]', # 0x53 -'[?]', # 0x54 -'[?]', # 0x55 -'[?]', # 0x56 -'[?]', # 0x57 -'[?]', # 0x58 -'[?]', # 0x59 -'[?]', # 0x5a -'[?]', # 0x5b -'[?]', # 0x5c -'[?]', # 0x5d -'[?]', # 0x5e -'[?]', # 0x5f -'[?]', # 0x60 -'[?]', # 0x61 -'[?]', # 0x62 -'[?]', # 0x63 +None, # 0x3d +None, # 0x3e +None, # 0x3f +None, # 0x40 +None, # 0x41 +None, # 0x42 +None, # 0x43 +None, # 0x44 +None, # 0x45 +None, # 0x46 +None, # 0x47 +None, # 0x48 +None, # 0x49 +None, # 0x4a +None, # 0x4b +None, # 0x4c +None, # 0x4d +None, # 0x4e +None, # 0x4f +None, # 0x50 +None, # 0x51 +None, # 0x52 +None, # 0x53 +None, # 0x54 +None, # 0x55 +None, # 0x56 +None, # 0x57 +None, # 0x58 +None, # 0x59 +None, # 0x5a +None, # 0x5b +None, # 0x5c +None, # 0x5d +None, # 0x5e +None, # 0x5f +None, # 0x60 +None, # 0x61 +None, # 0x62 +None, # 0x63 '<=', # 0x64 '>=', # 0x65 '<=', # 0x66 '>=', # 0x67 -'[?]', # 0x68 -'[?]', # 0x69 -'[?]', # 0x6a -'[?]', # 0x6b -'[?]', # 0x6c -'[?]', # 0x6d -'[?]', # 0x6e -'[?]', # 0x6f -'[?]', # 0x70 -'[?]', # 0x71 -'[?]', # 0x72 -'[?]', # 0x73 -'[?]', # 0x74 -'[?]', # 0x75 -'[?]', # 0x76 -'[?]', # 0x77 -'[?]', # 0x78 -'[?]', # 0x79 -'[?]', # 0x7a -'[?]', # 0x7b -'[?]', # 0x7c -'[?]', # 0x7d -'[?]', # 0x7e -'[?]', # 0x7f -'[?]', # 0x80 -'[?]', # 0x81 -'[?]', # 0x82 -'[?]', # 0x83 -'[?]', # 0x84 -'[?]', # 0x85 -'[?]', # 0x86 -'[?]', # 0x87 -'[?]', # 0x88 -'[?]', # 0x89 -'[?]', # 0x8a -'[?]', # 0x8b -'[?]', # 0x8c -'[?]', # 0x8d -'[?]', # 0x8e -'[?]', # 0x8f -'[?]', # 0x90 -'[?]', # 0x91 -'[?]', # 0x92 -'[?]', # 0x93 -'[?]', # 0x94 -'[?]', # 0x95 -'[?]', # 0x96 -'[?]', # 0x97 -'[?]', # 0x98 -'[?]', # 0x99 -'[?]', # 0x9a -'[?]', # 0x9b -'[?]', # 0x9c -'[?]', # 0x9d -'[?]', # 0x9e -'[?]', # 0x9f -'[?]', # 0xa0 -'[?]', # 0xa1 -'[?]', # 0xa2 -'[?]', # 0xa3 -'[?]', # 0xa4 -'[?]', # 0xa5 -'[?]', # 0xa6 -'[?]', # 0xa7 -'[?]', # 0xa8 -'[?]', # 0xa9 -'[?]', # 0xaa -'[?]', # 0xab -'[?]', # 0xac -'[?]', # 0xad -'[?]', # 0xae -'[?]', # 0xaf -'[?]', # 0xb0 -'[?]', # 0xb1 -'[?]', # 0xb2 -'[?]', # 0xb3 -'[?]', # 0xb4 -'[?]', # 0xb5 -'[?]', # 0xb6 -'[?]', # 0xb7 -'[?]', # 0xb8 -'[?]', # 0xb9 -'[?]', # 0xba -'[?]', # 0xbb -'[?]', # 0xbc -'[?]', # 0xbd -'[?]', # 0xbe -'[?]', # 0xbf -'[?]', # 0xc0 -'[?]', # 0xc1 -'[?]', # 0xc2 -'[?]', # 0xc3 -'[?]', # 0xc4 -'[?]', # 0xc5 -'[?]', # 0xc6 -'[?]', # 0xc7 -'[?]', # 0xc8 -'[?]', # 0xc9 -'[?]', # 0xca -'[?]', # 0xcb -'[?]', # 0xcc -'[?]', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf -'[?]', # 0xd0 -'[?]', # 0xd1 -'[?]', # 0xd2 -'[?]', # 0xd3 -'[?]', # 0xd4 -'[?]', # 0xd5 -'[?]', # 0xd6 -'[?]', # 0xd7 -'[?]', # 0xd8 -'[?]', # 0xd9 -'[?]', # 0xda -'[?]', # 0xdb -'[?]', # 0xdc -'[?]', # 0xdd -'[?]', # 0xde -'[?]', # 0xdf -'[?]', # 0xe0 -'[?]', # 0xe1 -'[?]', # 0xe2 -'[?]', # 0xe3 -'[?]', # 0xe4 -'[?]', # 0xe5 -'[?]', # 0xe6 -'[?]', # 0xe7 -'[?]', # 0xe8 -'[?]', # 0xe9 -'[?]', # 0xea -'[?]', # 0xeb -'[?]', # 0xec -'[?]', # 0xed -'[?]', # 0xee -'[?]', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0x68 +None, # 0x69 +None, # 0x6a +None, # 0x6b +None, # 0x6c +None, # 0x6d +None, # 0x6e +None, # 0x6f +None, # 0x70 +None, # 0x71 +None, # 0x72 +None, # 0x73 +None, # 0x74 +None, # 0x75 +None, # 0x76 +None, # 0x77 +None, # 0x78 +None, # 0x79 +None, # 0x7a +None, # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f +None, # 0x80 +None, # 0x81 +None, # 0x82 +None, # 0x83 +None, # 0x84 +None, # 0x85 +None, # 0x86 +None, # 0x87 +None, # 0x88 +None, # 0x89 +None, # 0x8a +None, # 0x8b +None, # 0x8c +None, # 0x8d +None, # 0x8e +None, # 0x8f +None, # 0x90 +None, # 0x91 +None, # 0x92 +None, # 0x93 +None, # 0x94 +None, # 0x95 +None, # 0x96 +None, # 0x97 +None, # 0x98 +None, # 0x99 +None, # 0x9a +None, # 0x9b +None, # 0x9c +None, # 0x9d +None, # 0x9e +None, # 0x9f +None, # 0xa0 +None, # 0xa1 +None, # 0xa2 +None, # 0xa3 +None, # 0xa4 +None, # 0xa5 +None, # 0xa6 +None, # 0xa7 +None, # 0xa8 +None, # 0xa9 +None, # 0xaa +None, # 0xab +None, # 0xac +None, # 0xad +None, # 0xae +None, # 0xaf +None, # 0xb0 +None, # 0xb1 +None, # 0xb2 +None, # 0xb3 +None, # 0xb4 +None, # 0xb5 +None, # 0xb6 +None, # 0xb7 +None, # 0xb8 +None, # 0xb9 +None, # 0xba +None, # 0xbb +None, # 0xbc +None, # 0xbd +None, # 0xbe +None, # 0xbf +None, # 0xc0 +None, # 0xc1 +None, # 0xc2 +None, # 0xc3 +None, # 0xc4 +None, # 0xc5 +None, # 0xc6 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x023.py b/libs/common/unidecode/x023.py index 3c4462e2..cb798848 100644 --- a/libs/common/unidecode/x023.py +++ b/libs/common/unidecode/x023.py @@ -1,257 +1,257 @@ data = ( -'[?]', # 0x00 -'[?]', # 0x01 -'[?]', # 0x02 +None, # 0x00 +None, # 0x01 +None, # 0x02 '^', # 0x03 -'[?]', # 0x04 -'[?]', # 0x05 -'[?]', # 0x06 -'[?]', # 0x07 -'[?]', # 0x08 -'[?]', # 0x09 -'[?]', # 0x0a -'[?]', # 0x0b -'[?]', # 0x0c -'[?]', # 0x0d -'[?]', # 0x0e -'[?]', # 0x0f -'[?]', # 0x10 -'[?]', # 0x11 -'[?]', # 0x12 -'[?]', # 0x13 -'[?]', # 0x14 -'[?]', # 0x15 -'[?]', # 0x16 -'[?]', # 0x17 -'[?]', # 0x18 -'[?]', # 0x19 -'[?]', # 0x1a -'[?]', # 0x1b -'[?]', # 0x1c -'[?]', # 0x1d -'[?]', # 0x1e -'[?]', # 0x1f -'[?]', # 0x20 -'[?]', # 0x21 -'[?]', # 0x22 -'[?]', # 0x23 -'[?]', # 0x24 -'[?]', # 0x25 -'[?]', # 0x26 -'[?]', # 0x27 -'[?]', # 0x28 +None, # 0x04 +None, # 0x05 +None, # 0x06 +None, # 0x07 +None, # 0x08 +None, # 0x09 +None, # 0x0a +None, # 0x0b +None, # 0x0c +None, # 0x0d +None, # 0x0e +None, # 0x0f +None, # 0x10 +None, # 0x11 +None, # 0x12 +None, # 0x13 +None, # 0x14 +None, # 0x15 +None, # 0x16 +None, # 0x17 +None, # 0x18 +None, # 0x19 +None, # 0x1a +None, # 0x1b +None, # 0x1c +None, # 0x1d +None, # 0x1e +None, # 0x1f +None, # 0x20 +None, # 0x21 +None, # 0x22 +None, # 0x23 +None, # 0x24 +None, # 0x25 +None, # 0x26 +None, # 0x27 +None, # 0x28 '<', # 0x29 '> ', # 0x2a -'[?]', # 0x2b -'[?]', # 0x2c -'[?]', # 0x2d -'[?]', # 0x2e -'[?]', # 0x2f -'[?]', # 0x30 -'[?]', # 0x31 -'[?]', # 0x32 -'[?]', # 0x33 -'[?]', # 0x34 -'[?]', # 0x35 -'[?]', # 0x36 -'[?]', # 0x37 -'[?]', # 0x38 -'[?]', # 0x39 -'[?]', # 0x3a -'[?]', # 0x3b -'[?]', # 0x3c -'[?]', # 0x3d -'[?]', # 0x3e -'[?]', # 0x3f -'[?]', # 0x40 -'[?]', # 0x41 -'[?]', # 0x42 -'[?]', # 0x43 -'[?]', # 0x44 -'[?]', # 0x45 -'[?]', # 0x46 -'[?]', # 0x47 -'[?]', # 0x48 -'[?]', # 0x49 -'[?]', # 0x4a -'[?]', # 0x4b -'[?]', # 0x4c -'[?]', # 0x4d -'[?]', # 0x4e -'[?]', # 0x4f -'[?]', # 0x50 -'[?]', # 0x51 -'[?]', # 0x52 -'[?]', # 0x53 -'[?]', # 0x54 -'[?]', # 0x55 -'[?]', # 0x56 -'[?]', # 0x57 -'[?]', # 0x58 -'[?]', # 0x59 -'[?]', # 0x5a -'[?]', # 0x5b -'[?]', # 0x5c -'[?]', # 0x5d -'[?]', # 0x5e -'[?]', # 0x5f -'[?]', # 0x60 -'[?]', # 0x61 -'[?]', # 0x62 -'[?]', # 0x63 -'[?]', # 0x64 -'[?]', # 0x65 -'[?]', # 0x66 -'[?]', # 0x67 -'[?]', # 0x68 -'[?]', # 0x69 -'[?]', # 0x6a -'[?]', # 0x6b -'[?]', # 0x6c -'[?]', # 0x6d -'[?]', # 0x6e -'[?]', # 0x6f -'[?]', # 0x70 -'[?]', # 0x71 -'[?]', # 0x72 -'[?]', # 0x73 -'[?]', # 0x74 -'[?]', # 0x75 -'[?]', # 0x76 -'[?]', # 0x77 -'[?]', # 0x78 -'[?]', # 0x79 -'[?]', # 0x7a -'[?]', # 0x7b -'[?]', # 0x7c -'[?]', # 0x7d -'[?]', # 0x7e -'[?]', # 0x7f -'[?]', # 0x80 -'[?]', # 0x81 -'[?]', # 0x82 -'[?]', # 0x83 -'[?]', # 0x84 -'[?]', # 0x85 -'[?]', # 0x86 -'[?]', # 0x87 -'[?]', # 0x88 -'[?]', # 0x89 -'[?]', # 0x8a -'[?]', # 0x8b -'[?]', # 0x8c -'[?]', # 0x8d -'[?]', # 0x8e -'[?]', # 0x8f -'[?]', # 0x90 -'[?]', # 0x91 -'[?]', # 0x92 -'[?]', # 0x93 -'[?]', # 0x94 -'[?]', # 0x95 -'[?]', # 0x96 -'[?]', # 0x97 -'[?]', # 0x98 -'[?]', # 0x99 -'[?]', # 0x9a -'[?]', # 0x9b -'[?]', # 0x9c -'[?]', # 0x9d -'[?]', # 0x9e -'[?]', # 0x9f -'[?]', # 0xa0 -'[?]', # 0xa1 -'[?]', # 0xa2 -'[?]', # 0xa3 -'[?]', # 0xa4 -'[?]', # 0xa5 -'[?]', # 0xa6 -'[?]', # 0xa7 -'[?]', # 0xa8 -'[?]', # 0xa9 -'[?]', # 0xaa -'[?]', # 0xab -'[?]', # 0xac -'[?]', # 0xad -'[?]', # 0xae -'[?]', # 0xaf -'[?]', # 0xb0 -'[?]', # 0xb1 -'[?]', # 0xb2 -'[?]', # 0xb3 -'[?]', # 0xb4 -'[?]', # 0xb5 -'[?]', # 0xb6 -'[?]', # 0xb7 -'[?]', # 0xb8 -'[?]', # 0xb9 -'[?]', # 0xba -'[?]', # 0xbb -'[?]', # 0xbc -'[?]', # 0xbd -'[?]', # 0xbe -'[?]', # 0xbf -'[?]', # 0xc0 -'[?]', # 0xc1 -'[?]', # 0xc2 -'[?]', # 0xc3 -'[?]', # 0xc4 -'[?]', # 0xc5 -'[?]', # 0xc6 -'[?]', # 0xc7 -'[?]', # 0xc8 -'[?]', # 0xc9 -'[?]', # 0xca -'[?]', # 0xcb -'[?]', # 0xcc -'[?]', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf -'[?]', # 0xd0 -'[?]', # 0xd1 -'[?]', # 0xd2 -'[?]', # 0xd3 -'[?]', # 0xd4 -'[?]', # 0xd5 -'[?]', # 0xd6 -'[?]', # 0xd7 -'[?]', # 0xd8 -'[?]', # 0xd9 -'[?]', # 0xda -'[?]', # 0xdb -'[?]', # 0xdc -'[?]', # 0xdd -'[?]', # 0xde -'[?]', # 0xdf -'[?]', # 0xe0 -'[?]', # 0xe1 -'[?]', # 0xe2 -'[?]', # 0xe3 -'[?]', # 0xe4 -'[?]', # 0xe5 -'[?]', # 0xe6 -'[?]', # 0xe7 -'[?]', # 0xe8 -'[?]', # 0xe9 -'[?]', # 0xea -'[?]', # 0xeb -'[?]', # 0xec -'[?]', # 0xed -'[?]', # 0xee -'[?]', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0x2b +None, # 0x2c +None, # 0x2d +None, # 0x2e +None, # 0x2f +None, # 0x30 +None, # 0x31 +None, # 0x32 +None, # 0x33 +None, # 0x34 +None, # 0x35 +None, # 0x36 +None, # 0x37 +None, # 0x38 +None, # 0x39 +None, # 0x3a +None, # 0x3b +None, # 0x3c +None, # 0x3d +None, # 0x3e +None, # 0x3f +None, # 0x40 +None, # 0x41 +None, # 0x42 +None, # 0x43 +None, # 0x44 +None, # 0x45 +None, # 0x46 +None, # 0x47 +None, # 0x48 +None, # 0x49 +None, # 0x4a +None, # 0x4b +None, # 0x4c +None, # 0x4d +None, # 0x4e +None, # 0x4f +None, # 0x50 +None, # 0x51 +None, # 0x52 +None, # 0x53 +None, # 0x54 +None, # 0x55 +None, # 0x56 +None, # 0x57 +None, # 0x58 +None, # 0x59 +None, # 0x5a +None, # 0x5b +None, # 0x5c +None, # 0x5d +None, # 0x5e +None, # 0x5f +None, # 0x60 +None, # 0x61 +None, # 0x62 +None, # 0x63 +None, # 0x64 +None, # 0x65 +None, # 0x66 +None, # 0x67 +None, # 0x68 +None, # 0x69 +None, # 0x6a +None, # 0x6b +None, # 0x6c +None, # 0x6d +None, # 0x6e +None, # 0x6f +None, # 0x70 +None, # 0x71 +None, # 0x72 +None, # 0x73 +None, # 0x74 +None, # 0x75 +None, # 0x76 +None, # 0x77 +None, # 0x78 +None, # 0x79 +None, # 0x7a +None, # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f +None, # 0x80 +None, # 0x81 +None, # 0x82 +None, # 0x83 +None, # 0x84 +None, # 0x85 +None, # 0x86 +None, # 0x87 +None, # 0x88 +None, # 0x89 +None, # 0x8a +None, # 0x8b +None, # 0x8c +None, # 0x8d +None, # 0x8e +None, # 0x8f +None, # 0x90 +None, # 0x91 +None, # 0x92 +None, # 0x93 +None, # 0x94 +None, # 0x95 +None, # 0x96 +None, # 0x97 +None, # 0x98 +None, # 0x99 +None, # 0x9a +None, # 0x9b +None, # 0x9c +None, # 0x9d +None, # 0x9e +None, # 0x9f +None, # 0xa0 +None, # 0xa1 +None, # 0xa2 +None, # 0xa3 +None, # 0xa4 +None, # 0xa5 +None, # 0xa6 +None, # 0xa7 +None, # 0xa8 +None, # 0xa9 +None, # 0xaa +None, # 0xab +None, # 0xac +None, # 0xad +None, # 0xae +None, # 0xaf +None, # 0xb0 +None, # 0xb1 +None, # 0xb2 +None, # 0xb3 +None, # 0xb4 +None, # 0xb5 +None, # 0xb6 +None, # 0xb7 +None, # 0xb8 +None, # 0xb9 +None, # 0xba +None, # 0xbb +None, # 0xbc +None, # 0xbd +None, # 0xbe +None, # 0xbf +None, # 0xc0 +None, # 0xc1 +None, # 0xc2 +None, # 0xc3 +None, # 0xc4 +None, # 0xc5 +None, # 0xc6 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x024.py b/libs/common/unidecode/x024.py index 231b0ca1..57126178 100644 --- a/libs/common/unidecode/x024.py +++ b/libs/common/unidecode/x024.py @@ -38,31 +38,31 @@ data = ( '', # 0x24 '', # 0x25 '', # 0x26 -'[?]', # 0x27 -'[?]', # 0x28 -'[?]', # 0x29 -'[?]', # 0x2a -'[?]', # 0x2b -'[?]', # 0x2c -'[?]', # 0x2d -'[?]', # 0x2e -'[?]', # 0x2f -'[?]', # 0x30 -'[?]', # 0x31 -'[?]', # 0x32 -'[?]', # 0x33 -'[?]', # 0x34 -'[?]', # 0x35 -'[?]', # 0x36 -'[?]', # 0x37 -'[?]', # 0x38 -'[?]', # 0x39 -'[?]', # 0x3a -'[?]', # 0x3b -'[?]', # 0x3c -'[?]', # 0x3d -'[?]', # 0x3e -'[?]', # 0x3f +None, # 0x27 +None, # 0x28 +None, # 0x29 +None, # 0x2a +None, # 0x2b +None, # 0x2c +None, # 0x2d +None, # 0x2e +None, # 0x2f +None, # 0x30 +None, # 0x31 +None, # 0x32 +None, # 0x33 +None, # 0x34 +None, # 0x35 +None, # 0x36 +None, # 0x37 +None, # 0x38 +None, # 0x39 +None, # 0x3a +None, # 0x3b +None, # 0x3c +None, # 0x3d +None, # 0x3e +None, # 0x3f '', # 0x40 '', # 0x41 '', # 0x42 @@ -74,27 +74,27 @@ data = ( '', # 0x48 '', # 0x49 '', # 0x4a -'[?]', # 0x4b -'[?]', # 0x4c -'[?]', # 0x4d -'[?]', # 0x4e -'[?]', # 0x4f -'[?]', # 0x50 -'[?]', # 0x51 -'[?]', # 0x52 -'[?]', # 0x53 -'[?]', # 0x54 -'[?]', # 0x55 -'[?]', # 0x56 -'[?]', # 0x57 -'[?]', # 0x58 -'[?]', # 0x59 -'[?]', # 0x5a -'[?]', # 0x5b -'[?]', # 0x5c -'[?]', # 0x5d -'[?]', # 0x5e -'[?]', # 0x5f +None, # 0x4b +None, # 0x4c +None, # 0x4d +None, # 0x4e +None, # 0x4f +None, # 0x50 +None, # 0x51 +None, # 0x52 +None, # 0x53 +None, # 0x54 +None, # 0x55 +None, # 0x56 +None, # 0x57 +None, # 0x58 +None, # 0x59 +None, # 0x5a +None, # 0x5b +None, # 0x5c +None, # 0x5d +None, # 0x5e +None, # 0x5f '1', # 0x60 '2', # 0x61 '3', # 0x62 diff --git a/libs/common/unidecode/x025.py b/libs/common/unidecode/x025.py index 5a62b10d..90eac33b 100644 --- a/libs/common/unidecode/x025.py +++ b/libs/common/unidecode/x025.py @@ -149,16 +149,16 @@ data = ( '#', # 0x93 '-', # 0x94 '|', # 0x95 -'[?]', # 0x96 -'[?]', # 0x97 -'[?]', # 0x98 -'[?]', # 0x99 -'[?]', # 0x9a -'[?]', # 0x9b -'[?]', # 0x9c -'[?]', # 0x9d -'[?]', # 0x9e -'[?]', # 0x9f +None, # 0x96 +None, # 0x97 +None, # 0x98 +None, # 0x99 +None, # 0x9a +None, # 0x9b +None, # 0x9c +None, # 0x9d +None, # 0x9e +None, # 0x9f '#', # 0xa0 '#', # 0xa1 '#', # 0xa2 @@ -247,11 +247,11 @@ data = ( '#', # 0xf5 '#', # 0xf6 '#', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x026.py b/libs/common/unidecode/x026.py index c575472c..380e84b9 100644 --- a/libs/common/unidecode/x026.py +++ b/libs/common/unidecode/x026.py @@ -19,11 +19,11 @@ data = ( '', # 0x11 '', # 0x12 '', # 0x13 -'[?]', # 0x14 -'[?]', # 0x15 -'[?]', # 0x16 -'[?]', # 0x17 -'[?]', # 0x18 +None, # 0x14 +None, # 0x15 +None, # 0x16 +None, # 0x17 +None, # 0x18 '', # 0x19 '', # 0x1a '', # 0x1b @@ -113,145 +113,145 @@ data = ( '#', # 0x6f '', # 0x70 '', # 0x71 -'[?]', # 0x72 -'[?]', # 0x73 -'[?]', # 0x74 -'[?]', # 0x75 -'[?]', # 0x76 -'[?]', # 0x77 -'[?]', # 0x78 -'[?]', # 0x79 -'[?]', # 0x7a -'[?]', # 0x7b -'[?]', # 0x7c -'[?]', # 0x7d -'[?]', # 0x7e -'[?]', # 0x7f -'[?]', # 0x80 -'[?]', # 0x81 -'[?]', # 0x82 -'[?]', # 0x83 -'[?]', # 0x84 -'[?]', # 0x85 -'[?]', # 0x86 -'[?]', # 0x87 -'[?]', # 0x88 -'[?]', # 0x89 -'[?]', # 0x8a -'[?]', # 0x8b -'[?]', # 0x8c -'[?]', # 0x8d -'[?]', # 0x8e -'[?]', # 0x8f -'[?]', # 0x90 -'[?]', # 0x91 -'[?]', # 0x92 -'[?]', # 0x93 -'[?]', # 0x94 -'[?]', # 0x95 -'[?]', # 0x96 -'[?]', # 0x97 -'[?]', # 0x98 -'[?]', # 0x99 -'[?]', # 0x9a -'[?]', # 0x9b -'[?]', # 0x9c -'[?]', # 0x9d -'[?]', # 0x9e -'[?]', # 0x9f -'[?]', # 0xa0 -'[?]', # 0xa1 -'[?]', # 0xa2 -'[?]', # 0xa3 -'[?]', # 0xa4 -'[?]', # 0xa5 -'[?]', # 0xa6 -'[?]', # 0xa7 -'[?]', # 0xa8 -'[?]', # 0xa9 -'[?]', # 0xaa -'[?]', # 0xab -'[?]', # 0xac -'[?]', # 0xad -'[?]', # 0xae -'[?]', # 0xaf -'[?]', # 0xb0 -'[?]', # 0xb1 -'[?]', # 0xb2 -'[?]', # 0xb3 -'[?]', # 0xb4 -'[?]', # 0xb5 -'[?]', # 0xb6 -'[?]', # 0xb7 -'[?]', # 0xb8 -'[?]', # 0xb9 -'[?]', # 0xba -'[?]', # 0xbb -'[?]', # 0xbc -'[?]', # 0xbd -'[?]', # 0xbe -'[?]', # 0xbf -'[?]', # 0xc0 -'[?]', # 0xc1 -'[?]', # 0xc2 -'[?]', # 0xc3 -'[?]', # 0xc4 -'[?]', # 0xc5 -'[?]', # 0xc6 -'[?]', # 0xc7 -'[?]', # 0xc8 -'[?]', # 0xc9 -'[?]', # 0xca -'[?]', # 0xcb -'[?]', # 0xcc -'[?]', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf -'[?]', # 0xd0 -'[?]', # 0xd1 -'[?]', # 0xd2 -'[?]', # 0xd3 -'[?]', # 0xd4 -'[?]', # 0xd5 -'[?]', # 0xd6 -'[?]', # 0xd7 -'[?]', # 0xd8 -'[?]', # 0xd9 -'[?]', # 0xda -'[?]', # 0xdb -'[?]', # 0xdc -'[?]', # 0xdd -'[?]', # 0xde -'[?]', # 0xdf -'[?]', # 0xe0 -'[?]', # 0xe1 -'[?]', # 0xe2 -'[?]', # 0xe3 -'[?]', # 0xe4 -'[?]', # 0xe5 -'[?]', # 0xe6 -'[?]', # 0xe7 -'[?]', # 0xe8 -'[?]', # 0xe9 -'[?]', # 0xea -'[?]', # 0xeb -'[?]', # 0xec -'[?]', # 0xed -'[?]', # 0xee -'[?]', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0x72 +None, # 0x73 +None, # 0x74 +None, # 0x75 +None, # 0x76 +None, # 0x77 +None, # 0x78 +None, # 0x79 +None, # 0x7a +None, # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f +None, # 0x80 +None, # 0x81 +None, # 0x82 +None, # 0x83 +None, # 0x84 +None, # 0x85 +None, # 0x86 +None, # 0x87 +None, # 0x88 +None, # 0x89 +None, # 0x8a +None, # 0x8b +None, # 0x8c +None, # 0x8d +None, # 0x8e +None, # 0x8f +None, # 0x90 +None, # 0x91 +None, # 0x92 +None, # 0x93 +None, # 0x94 +None, # 0x95 +None, # 0x96 +None, # 0x97 +None, # 0x98 +None, # 0x99 +None, # 0x9a +None, # 0x9b +None, # 0x9c +None, # 0x9d +None, # 0x9e +None, # 0x9f +None, # 0xa0 +None, # 0xa1 +None, # 0xa2 +None, # 0xa3 +None, # 0xa4 +None, # 0xa5 +None, # 0xa6 +None, # 0xa7 +None, # 0xa8 +None, # 0xa9 +None, # 0xaa +None, # 0xab +None, # 0xac +None, # 0xad +None, # 0xae +None, # 0xaf +None, # 0xb0 +None, # 0xb1 +None, # 0xb2 +None, # 0xb3 +None, # 0xb4 +None, # 0xb5 +None, # 0xb6 +None, # 0xb7 +None, # 0xb8 +None, # 0xb9 +None, # 0xba +None, # 0xbb +None, # 0xbc +None, # 0xbd +None, # 0xbe +None, # 0xbf +None, # 0xc0 +None, # 0xc1 +None, # 0xc2 +None, # 0xc3 +None, # 0xc4 +None, # 0xc5 +None, # 0xc6 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x027.py b/libs/common/unidecode/x027.py index 3c74c073..ab33ab95 100644 --- a/libs/common/unidecode/x027.py +++ b/libs/common/unidecode/x027.py @@ -1,5 +1,5 @@ data = ( -'[?]', # 0x00 +None, # 0x00 '', # 0x01 '', # 0x02 '', # 0x03 @@ -91,11 +91,11 @@ data = ( '', # 0x59 '', # 0x5a '', # 0x5b -'', # 0x5c -'', # 0x5d -'', # 0x5e -'[?]', # 0x5f -'[?]', # 0x60 +'\'', # 0x5c +'"', # 0x5d +'"', # 0x5e +',', # 0x5f +',,', # 0x60 '', # 0x61 '!', # 0x62 '', # 0x63 @@ -175,7 +175,7 @@ data = ( '', # 0xad '', # 0xae '', # 0xaf -'[?]', # 0xb0 +None, # 0xb0 '', # 0xb1 '', # 0xb2 '', # 0xb3 @@ -190,68 +190,68 @@ data = ( '', # 0xbc '', # 0xbd '', # 0xbe -'[?]', # 0xbf -'[?]', # 0xc0 -'[?]', # 0xc1 -'[?]', # 0xc2 -'[?]', # 0xc3 -'[?]', # 0xc4 -'[?]', # 0xc5 -'[?]', # 0xc6 -'[?]', # 0xc7 -'[?]', # 0xc8 -'[?]', # 0xc9 -'[?]', # 0xca -'[?]', # 0xcb -'[?]', # 0xcc -'[?]', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf -'[?]', # 0xd0 -'[?]', # 0xd1 -'[?]', # 0xd2 -'[?]', # 0xd3 -'[?]', # 0xd4 -'[?]', # 0xd5 -'[?]', # 0xd6 -'[?]', # 0xd7 -'[?]', # 0xd8 -'[?]', # 0xd9 -'[?]', # 0xda -'[?]', # 0xdb -'[?]', # 0xdc -'[?]', # 0xdd -'[?]', # 0xde -'[?]', # 0xdf -'[?]', # 0xe0 -'[?]', # 0xe1 -'[?]', # 0xe2 -'[?]', # 0xe3 -'[?]', # 0xe4 -'[?]', # 0xe5 +None, # 0xbf +None, # 0xc0 +None, # 0xc1 +None, # 0xc2 +None, # 0xc3 +None, # 0xc4 +None, # 0xc5 +None, # 0xc6 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 '[', # 0xe6 -'[?]', # 0xe7 +None, # 0xe7 '<', # 0xe8 '> ', # 0xe9 -'[?]', # 0xea -'[?]', # 0xeb -'[?]', # 0xec -'[?]', # 0xed -'[?]', # 0xee -'[?]', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x029.py b/libs/common/unidecode/x029.py index c2df2548..717e9031 100644 --- a/libs/common/unidecode/x029.py +++ b/libs/common/unidecode/x029.py @@ -1,257 +1,257 @@ data = ( -'', # 0x00 -'', # 0x01 -'', # 0x02 -'', # 0x03 -'', # 0x04 -'', # 0x05 -'', # 0x06 -'', # 0x07 -'', # 0x08 -'', # 0x09 -'', # 0x0a -'', # 0x0b -'', # 0x0c -'', # 0x0d -'', # 0x0e -'', # 0x0f -'', # 0x10 -'', # 0x11 -'', # 0x12 -'', # 0x13 -'', # 0x14 -'', # 0x15 -'', # 0x16 -'', # 0x17 -'', # 0x18 -'', # 0x19 -'', # 0x1a -'', # 0x1b -'', # 0x1c -'', # 0x1d -'', # 0x1e -'', # 0x1f -'', # 0x20 -'', # 0x21 -'', # 0x22 -'', # 0x23 -'', # 0x24 -'', # 0x25 -'', # 0x26 -'', # 0x27 -'', # 0x28 -'', # 0x29 -'', # 0x2a -'', # 0x2b -'', # 0x2c -'', # 0x2d -'', # 0x2e -'', # 0x2f -'', # 0x30 -'', # 0x31 -'', # 0x32 -'', # 0x33 -'', # 0x34 -'', # 0x35 -'', # 0x36 -'', # 0x37 -'', # 0x38 -'', # 0x39 -'', # 0x3a -'', # 0x3b -'', # 0x3c -'', # 0x3d -'', # 0x3e -'', # 0x3f -'', # 0x40 -'', # 0x41 -'', # 0x42 -'', # 0x43 -'', # 0x44 -'', # 0x45 -'', # 0x46 -'', # 0x47 -'', # 0x48 -'', # 0x49 -'', # 0x4a -'', # 0x4b -'', # 0x4c -'', # 0x4d -'', # 0x4e -'', # 0x4f -'', # 0x50 -'', # 0x51 -'', # 0x52 -'', # 0x53 -'', # 0x54 -'', # 0x55 -'', # 0x56 -'', # 0x57 -'', # 0x58 -'', # 0x59 -'', # 0x5a -'', # 0x5b -'', # 0x5c -'', # 0x5d -'', # 0x5e -'', # 0x5f -'', # 0x60 -'', # 0x61 -'', # 0x62 -'', # 0x63 -'', # 0x64 -'', # 0x65 -'', # 0x66 -'', # 0x67 -'', # 0x68 -'', # 0x69 -'', # 0x6a -'', # 0x6b -'', # 0x6c -'', # 0x6d -'', # 0x6e -'', # 0x6f -'', # 0x70 -'', # 0x71 -'', # 0x72 -'', # 0x73 -'', # 0x74 -'', # 0x75 -'', # 0x76 -'', # 0x77 -'', # 0x78 -'', # 0x79 -'', # 0x7a -'', # 0x7b -'', # 0x7c -'', # 0x7d -'', # 0x7e -'', # 0x7f -'', # 0x80 -'', # 0x81 -'', # 0x82 +None, # 0x00 +None, # 0x01 +None, # 0x02 +None, # 0x03 +None, # 0x04 +None, # 0x05 +None, # 0x06 +None, # 0x07 +None, # 0x08 +None, # 0x09 +None, # 0x0a +None, # 0x0b +None, # 0x0c +None, # 0x0d +None, # 0x0e +None, # 0x0f +None, # 0x10 +None, # 0x11 +None, # 0x12 +None, # 0x13 +None, # 0x14 +None, # 0x15 +None, # 0x16 +None, # 0x17 +None, # 0x18 +None, # 0x19 +None, # 0x1a +None, # 0x1b +None, # 0x1c +None, # 0x1d +None, # 0x1e +None, # 0x1f +None, # 0x20 +None, # 0x21 +None, # 0x22 +None, # 0x23 +None, # 0x24 +None, # 0x25 +None, # 0x26 +None, # 0x27 +None, # 0x28 +None, # 0x29 +None, # 0x2a +None, # 0x2b +None, # 0x2c +None, # 0x2d +None, # 0x2e +None, # 0x2f +None, # 0x30 +None, # 0x31 +None, # 0x32 +None, # 0x33 +None, # 0x34 +None, # 0x35 +None, # 0x36 +None, # 0x37 +None, # 0x38 +None, # 0x39 +None, # 0x3a +None, # 0x3b +None, # 0x3c +None, # 0x3d +None, # 0x3e +None, # 0x3f +None, # 0x40 +None, # 0x41 +None, # 0x42 +None, # 0x43 +None, # 0x44 +None, # 0x45 +None, # 0x46 +None, # 0x47 +None, # 0x48 +None, # 0x49 +None, # 0x4a +None, # 0x4b +None, # 0x4c +None, # 0x4d +None, # 0x4e +None, # 0x4f +None, # 0x50 +None, # 0x51 +None, # 0x52 +None, # 0x53 +None, # 0x54 +None, # 0x55 +None, # 0x56 +None, # 0x57 +None, # 0x58 +None, # 0x59 +None, # 0x5a +None, # 0x5b +None, # 0x5c +None, # 0x5d +None, # 0x5e +None, # 0x5f +None, # 0x60 +None, # 0x61 +None, # 0x62 +None, # 0x63 +None, # 0x64 +None, # 0x65 +None, # 0x66 +None, # 0x67 +None, # 0x68 +None, # 0x69 +None, # 0x6a +None, # 0x6b +None, # 0x6c +None, # 0x6d +None, # 0x6e +None, # 0x6f +None, # 0x70 +None, # 0x71 +None, # 0x72 +None, # 0x73 +None, # 0x74 +None, # 0x75 +None, # 0x76 +None, # 0x77 +None, # 0x78 +None, # 0x79 +None, # 0x7a +None, # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f +None, # 0x80 +None, # 0x81 +None, # 0x82 '{', # 0x83 '} ', # 0x84 -'', # 0x85 -'', # 0x86 -'', # 0x87 -'', # 0x88 -'', # 0x89 -'', # 0x8a -'', # 0x8b -'', # 0x8c -'', # 0x8d -'', # 0x8e -'', # 0x8f -'', # 0x90 -'', # 0x91 -'', # 0x92 -'', # 0x93 -'', # 0x94 -'', # 0x95 -'', # 0x96 -'', # 0x97 -'', # 0x98 -'', # 0x99 -'', # 0x9a -'', # 0x9b -'', # 0x9c -'', # 0x9d -'', # 0x9e -'', # 0x9f -'', # 0xa0 -'', # 0xa1 -'', # 0xa2 -'', # 0xa3 -'', # 0xa4 -'', # 0xa5 -'', # 0xa6 -'', # 0xa7 -'', # 0xa8 -'', # 0xa9 -'', # 0xaa -'', # 0xab -'', # 0xac -'', # 0xad -'', # 0xae -'', # 0xaf -'', # 0xb0 -'', # 0xb1 -'', # 0xb2 -'', # 0xb3 -'', # 0xb4 -'', # 0xb5 -'', # 0xb6 -'', # 0xb7 -'', # 0xb8 -'', # 0xb9 -'', # 0xba -'', # 0xbb -'', # 0xbc -'', # 0xbd -'', # 0xbe -'', # 0xbf -'', # 0xc0 -'', # 0xc1 -'', # 0xc2 -'', # 0xc3 -'', # 0xc4 -'', # 0xc5 -'', # 0xc6 -'', # 0xc7 -'', # 0xc8 -'', # 0xc9 -'', # 0xca -'', # 0xcb -'', # 0xcc -'', # 0xcd -'', # 0xce -'', # 0xcf -'', # 0xd0 -'', # 0xd1 -'', # 0xd2 -'', # 0xd3 -'', # 0xd4 -'', # 0xd5 -'', # 0xd6 -'', # 0xd7 -'', # 0xd8 -'', # 0xd9 -'', # 0xda -'', # 0xdb -'', # 0xdc -'', # 0xdd -'', # 0xde -'', # 0xdf -'', # 0xe0 -'', # 0xe1 -'', # 0xe2 -'', # 0xe3 -'', # 0xe4 -'', # 0xe5 -'', # 0xe6 -'', # 0xe7 -'', # 0xe8 -'', # 0xe9 -'', # 0xea -'', # 0xeb -'', # 0xec -'', # 0xed -'', # 0xee -'', # 0xef -'', # 0xf0 -'', # 0xf1 -'', # 0xf2 -'', # 0xf3 -'', # 0xf4 -'', # 0xf5 -'', # 0xf6 -'', # 0xf7 -'', # 0xf8 -'', # 0xf9 -'', # 0xfa -'', # 0xfb -'', # 0xfc -'', # 0xfd -'', # 0xfe +None, # 0x85 +None, # 0x86 +None, # 0x87 +None, # 0x88 +None, # 0x89 +None, # 0x8a +None, # 0x8b +None, # 0x8c +None, # 0x8d +None, # 0x8e +None, # 0x8f +None, # 0x90 +None, # 0x91 +None, # 0x92 +None, # 0x93 +None, # 0x94 +None, # 0x95 +None, # 0x96 +None, # 0x97 +None, # 0x98 +None, # 0x99 +None, # 0x9a +None, # 0x9b +None, # 0x9c +None, # 0x9d +None, # 0x9e +None, # 0x9f +None, # 0xa0 +None, # 0xa1 +None, # 0xa2 +None, # 0xa3 +None, # 0xa4 +None, # 0xa5 +None, # 0xa6 +None, # 0xa7 +None, # 0xa8 +None, # 0xa9 +None, # 0xaa +None, # 0xab +None, # 0xac +None, # 0xad +None, # 0xae +None, # 0xaf +None, # 0xb0 +None, # 0xb1 +None, # 0xb2 +None, # 0xb3 +None, # 0xb4 +None, # 0xb5 +None, # 0xb6 +None, # 0xb7 +None, # 0xb8 +None, # 0xb9 +None, # 0xba +None, # 0xbb +None, # 0xbc +None, # 0xbd +None, # 0xbe +None, # 0xbf +None, # 0xc0 +None, # 0xc1 +None, # 0xc2 +None, # 0xc3 +None, # 0xc4 +None, # 0xc5 +None, # 0xc6 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x02a.py b/libs/common/unidecode/x02a.py index b832ef35..4f574c14 100644 --- a/libs/common/unidecode/x02a.py +++ b/libs/common/unidecode/x02a.py @@ -1,257 +1,257 @@ data = ( -'', # 0x00 -'', # 0x01 -'', # 0x02 -'', # 0x03 -'', # 0x04 -'', # 0x05 -'', # 0x06 -'', # 0x07 -'', # 0x08 -'', # 0x09 -'', # 0x0a -'', # 0x0b -'', # 0x0c -'', # 0x0d -'', # 0x0e -'', # 0x0f -'', # 0x10 -'', # 0x11 -'', # 0x12 -'', # 0x13 -'', # 0x14 -'', # 0x15 -'', # 0x16 -'', # 0x17 -'', # 0x18 -'', # 0x19 -'', # 0x1a -'', # 0x1b -'', # 0x1c -'', # 0x1d -'', # 0x1e -'', # 0x1f -'', # 0x20 -'', # 0x21 -'', # 0x22 -'', # 0x23 -'', # 0x24 -'', # 0x25 -'', # 0x26 -'', # 0x27 -'', # 0x28 -'', # 0x29 -'', # 0x2a -'', # 0x2b -'', # 0x2c -'', # 0x2d -'', # 0x2e -'', # 0x2f -'', # 0x30 -'', # 0x31 -'', # 0x32 -'', # 0x33 -'', # 0x34 -'', # 0x35 -'', # 0x36 -'', # 0x37 -'', # 0x38 -'', # 0x39 -'', # 0x3a -'', # 0x3b -'', # 0x3c -'', # 0x3d -'', # 0x3e -'', # 0x3f -'', # 0x40 -'', # 0x41 -'', # 0x42 -'', # 0x43 -'', # 0x44 -'', # 0x45 -'', # 0x46 -'', # 0x47 -'', # 0x48 -'', # 0x49 -'', # 0x4a -'', # 0x4b -'', # 0x4c -'', # 0x4d -'', # 0x4e -'', # 0x4f -'', # 0x50 -'', # 0x51 -'', # 0x52 -'', # 0x53 -'', # 0x54 -'', # 0x55 -'', # 0x56 -'', # 0x57 -'', # 0x58 -'', # 0x59 -'', # 0x5a -'', # 0x5b -'', # 0x5c -'', # 0x5d -'', # 0x5e -'', # 0x5f -'', # 0x60 -'', # 0x61 -'', # 0x62 -'', # 0x63 -'', # 0x64 -'', # 0x65 -'', # 0x66 -'', # 0x67 -'', # 0x68 -'', # 0x69 -'', # 0x6a -'', # 0x6b -'', # 0x6c -'', # 0x6d -'', # 0x6e -'', # 0x6f -'', # 0x70 -'', # 0x71 -'', # 0x72 -'', # 0x73 +None, # 0x00 +None, # 0x01 +None, # 0x02 +None, # 0x03 +None, # 0x04 +None, # 0x05 +None, # 0x06 +None, # 0x07 +None, # 0x08 +None, # 0x09 +None, # 0x0a +None, # 0x0b +None, # 0x0c +None, # 0x0d +None, # 0x0e +None, # 0x0f +None, # 0x10 +None, # 0x11 +None, # 0x12 +None, # 0x13 +None, # 0x14 +None, # 0x15 +None, # 0x16 +None, # 0x17 +None, # 0x18 +None, # 0x19 +None, # 0x1a +None, # 0x1b +None, # 0x1c +None, # 0x1d +None, # 0x1e +None, # 0x1f +None, # 0x20 +None, # 0x21 +None, # 0x22 +None, # 0x23 +None, # 0x24 +None, # 0x25 +None, # 0x26 +None, # 0x27 +None, # 0x28 +None, # 0x29 +None, # 0x2a +None, # 0x2b +None, # 0x2c +None, # 0x2d +None, # 0x2e +None, # 0x2f +None, # 0x30 +None, # 0x31 +None, # 0x32 +None, # 0x33 +None, # 0x34 +None, # 0x35 +None, # 0x36 +None, # 0x37 +None, # 0x38 +None, # 0x39 +None, # 0x3a +None, # 0x3b +None, # 0x3c +None, # 0x3d +None, # 0x3e +None, # 0x3f +None, # 0x40 +None, # 0x41 +None, # 0x42 +None, # 0x43 +None, # 0x44 +None, # 0x45 +None, # 0x46 +None, # 0x47 +None, # 0x48 +None, # 0x49 +None, # 0x4a +None, # 0x4b +None, # 0x4c +None, # 0x4d +None, # 0x4e +None, # 0x4f +None, # 0x50 +None, # 0x51 +None, # 0x52 +None, # 0x53 +None, # 0x54 +None, # 0x55 +None, # 0x56 +None, # 0x57 +None, # 0x58 +None, # 0x59 +None, # 0x5a +None, # 0x5b +None, # 0x5c +None, # 0x5d +None, # 0x5e +None, # 0x5f +None, # 0x60 +None, # 0x61 +None, # 0x62 +None, # 0x63 +None, # 0x64 +None, # 0x65 +None, # 0x66 +None, # 0x67 +None, # 0x68 +None, # 0x69 +None, # 0x6a +None, # 0x6b +None, # 0x6c +None, # 0x6d +None, # 0x6e +None, # 0x6f +None, # 0x70 +None, # 0x71 +None, # 0x72 +None, # 0x73 '::=', # 0x74 '==', # 0x75 '===', # 0x76 -'', # 0x77 -'', # 0x78 -'', # 0x79 -'', # 0x7a -'', # 0x7b -'', # 0x7c -'', # 0x7d -'', # 0x7e -'', # 0x7f -'', # 0x80 -'', # 0x81 -'', # 0x82 -'', # 0x83 -'', # 0x84 -'', # 0x85 -'', # 0x86 -'', # 0x87 -'', # 0x88 -'', # 0x89 -'', # 0x8a -'', # 0x8b -'', # 0x8c -'', # 0x8d -'', # 0x8e -'', # 0x8f -'', # 0x90 -'', # 0x91 -'', # 0x92 -'', # 0x93 -'', # 0x94 -'', # 0x95 -'', # 0x96 -'', # 0x97 -'', # 0x98 -'', # 0x99 -'', # 0x9a -'', # 0x9b -'', # 0x9c -'', # 0x9d -'', # 0x9e -'', # 0x9f -'', # 0xa0 -'', # 0xa1 -'', # 0xa2 -'', # 0xa3 -'', # 0xa4 -'', # 0xa5 -'', # 0xa6 -'', # 0xa7 -'', # 0xa8 -'', # 0xa9 -'', # 0xaa -'', # 0xab -'', # 0xac -'', # 0xad -'', # 0xae -'', # 0xaf -'', # 0xb0 -'', # 0xb1 -'', # 0xb2 -'', # 0xb3 -'', # 0xb4 -'', # 0xb5 -'', # 0xb6 -'', # 0xb7 -'', # 0xb8 -'', # 0xb9 -'', # 0xba -'', # 0xbb -'', # 0xbc -'', # 0xbd -'', # 0xbe -'', # 0xbf -'', # 0xc0 -'', # 0xc1 -'', # 0xc2 -'', # 0xc3 -'', # 0xc4 -'', # 0xc5 -'', # 0xc6 -'', # 0xc7 -'', # 0xc8 -'', # 0xc9 -'', # 0xca -'', # 0xcb -'', # 0xcc -'', # 0xcd -'', # 0xce -'', # 0xcf -'', # 0xd0 -'', # 0xd1 -'', # 0xd2 -'', # 0xd3 -'', # 0xd4 -'', # 0xd5 -'', # 0xd6 -'', # 0xd7 -'', # 0xd8 -'', # 0xd9 -'', # 0xda -'', # 0xdb -'', # 0xdc -'', # 0xdd -'', # 0xde -'', # 0xdf -'', # 0xe0 -'', # 0xe1 -'', # 0xe2 -'', # 0xe3 -'', # 0xe4 -'', # 0xe5 -'', # 0xe6 -'', # 0xe7 -'', # 0xe8 -'', # 0xe9 -'', # 0xea -'', # 0xeb -'', # 0xec -'', # 0xed -'', # 0xee -'', # 0xef -'', # 0xf0 -'', # 0xf1 -'', # 0xf2 -'', # 0xf3 -'', # 0xf4 -'', # 0xf5 -'', # 0xf6 -'', # 0xf7 -'', # 0xf8 -'', # 0xf9 -'', # 0xfa -'', # 0xfb -'', # 0xfc -'', # 0xfd -'', # 0xfe +None, # 0x77 +None, # 0x78 +None, # 0x79 +None, # 0x7a +None, # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f +None, # 0x80 +None, # 0x81 +None, # 0x82 +None, # 0x83 +None, # 0x84 +None, # 0x85 +None, # 0x86 +None, # 0x87 +None, # 0x88 +None, # 0x89 +None, # 0x8a +None, # 0x8b +None, # 0x8c +None, # 0x8d +None, # 0x8e +None, # 0x8f +None, # 0x90 +None, # 0x91 +None, # 0x92 +None, # 0x93 +None, # 0x94 +None, # 0x95 +None, # 0x96 +None, # 0x97 +None, # 0x98 +None, # 0x99 +None, # 0x9a +None, # 0x9b +None, # 0x9c +None, # 0x9d +None, # 0x9e +None, # 0x9f +None, # 0xa0 +None, # 0xa1 +None, # 0xa2 +None, # 0xa3 +None, # 0xa4 +None, # 0xa5 +None, # 0xa6 +None, # 0xa7 +None, # 0xa8 +None, # 0xa9 +None, # 0xaa +None, # 0xab +None, # 0xac +None, # 0xad +None, # 0xae +None, # 0xaf +None, # 0xb0 +None, # 0xb1 +None, # 0xb2 +None, # 0xb3 +None, # 0xb4 +None, # 0xb5 +None, # 0xb6 +None, # 0xb7 +None, # 0xb8 +None, # 0xb9 +None, # 0xba +None, # 0xbb +None, # 0xbc +None, # 0xbd +None, # 0xbe +None, # 0xbf +None, # 0xc0 +None, # 0xc1 +None, # 0xc2 +None, # 0xc3 +None, # 0xc4 +None, # 0xc5 +None, # 0xc6 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x02c.py b/libs/common/unidecode/x02c.py index 0d05d069..f6e6d1f3 100644 --- a/libs/common/unidecode/x02c.py +++ b/libs/common/unidecode/x02c.py @@ -1,100 +1,100 @@ data = ( -'', # 0x00 -'', # 0x01 -'', # 0x02 -'', # 0x03 -'', # 0x04 -'', # 0x05 -'', # 0x06 -'', # 0x07 -'', # 0x08 -'', # 0x09 -'', # 0x0a -'', # 0x0b -'', # 0x0c -'', # 0x0d -'', # 0x0e -'', # 0x0f -'', # 0x10 -'', # 0x11 -'', # 0x12 -'', # 0x13 -'', # 0x14 -'', # 0x15 -'', # 0x16 -'', # 0x17 -'', # 0x18 -'', # 0x19 -'', # 0x1a -'', # 0x1b -'', # 0x1c -'', # 0x1d -'', # 0x1e -'', # 0x1f -'', # 0x20 -'', # 0x21 -'', # 0x22 -'', # 0x23 -'', # 0x24 -'', # 0x25 -'', # 0x26 -'', # 0x27 -'', # 0x28 -'', # 0x29 -'', # 0x2a -'', # 0x2b -'', # 0x2c -'', # 0x2d -'', # 0x2e -'', # 0x2f -'', # 0x30 -'', # 0x31 -'', # 0x32 -'', # 0x33 -'', # 0x34 -'', # 0x35 -'', # 0x36 -'', # 0x37 -'', # 0x38 -'', # 0x39 -'', # 0x3a -'', # 0x3b -'', # 0x3c -'', # 0x3d -'', # 0x3e -'', # 0x3f -'', # 0x40 -'', # 0x41 -'', # 0x42 -'', # 0x43 -'', # 0x44 -'', # 0x45 -'', # 0x46 -'', # 0x47 -'', # 0x48 -'', # 0x49 -'', # 0x4a -'', # 0x4b -'', # 0x4c -'', # 0x4d -'', # 0x4e -'', # 0x4f -'', # 0x50 -'', # 0x51 -'', # 0x52 -'', # 0x53 -'', # 0x54 -'', # 0x55 -'', # 0x56 -'', # 0x57 -'', # 0x58 -'', # 0x59 -'', # 0x5a -'', # 0x5b -'', # 0x5c -'', # 0x5d -'', # 0x5e -'', # 0x5f +None, # 0x00 +None, # 0x01 +None, # 0x02 +None, # 0x03 +None, # 0x04 +None, # 0x05 +None, # 0x06 +None, # 0x07 +None, # 0x08 +None, # 0x09 +None, # 0x0a +None, # 0x0b +None, # 0x0c +None, # 0x0d +None, # 0x0e +None, # 0x0f +None, # 0x10 +None, # 0x11 +None, # 0x12 +None, # 0x13 +None, # 0x14 +None, # 0x15 +None, # 0x16 +None, # 0x17 +None, # 0x18 +None, # 0x19 +None, # 0x1a +None, # 0x1b +None, # 0x1c +None, # 0x1d +None, # 0x1e +None, # 0x1f +None, # 0x20 +None, # 0x21 +None, # 0x22 +None, # 0x23 +None, # 0x24 +None, # 0x25 +None, # 0x26 +None, # 0x27 +None, # 0x28 +None, # 0x29 +None, # 0x2a +None, # 0x2b +None, # 0x2c +None, # 0x2d +None, # 0x2e +None, # 0x2f +None, # 0x30 +None, # 0x31 +None, # 0x32 +None, # 0x33 +None, # 0x34 +None, # 0x35 +None, # 0x36 +None, # 0x37 +None, # 0x38 +None, # 0x39 +None, # 0x3a +None, # 0x3b +None, # 0x3c +None, # 0x3d +None, # 0x3e +None, # 0x3f +None, # 0x40 +None, # 0x41 +None, # 0x42 +None, # 0x43 +None, # 0x44 +None, # 0x45 +None, # 0x46 +None, # 0x47 +None, # 0x48 +None, # 0x49 +None, # 0x4a +None, # 0x4b +None, # 0x4c +None, # 0x4d +None, # 0x4e +None, # 0x4f +None, # 0x50 +None, # 0x51 +None, # 0x52 +None, # 0x53 +None, # 0x54 +None, # 0x55 +None, # 0x56 +None, # 0x57 +None, # 0x58 +None, # 0x59 +None, # 0x5a +None, # 0x5b +None, # 0x5c +None, # 0x5d +None, # 0x5e +None, # 0x5f 'L', # 0x60 'l', # 0x61 'L', # 0x62 @@ -108,150 +108,150 @@ data = ( 'k', # 0x6a 'Z', # 0x6b 'z', # 0x6c -'', # 0x6d +None, # 0x6d 'M', # 0x6e 'A', # 0x6f -'', # 0x70 -'', # 0x71 -'', # 0x72 -'', # 0x73 -'', # 0x74 -'', # 0x75 -'', # 0x76 -'', # 0x77 -'', # 0x78 -'', # 0x79 -'', # 0x7a -'', # 0x7b -'', # 0x7c -'', # 0x7d -'', # 0x7e -'', # 0x7f -'', # 0x80 -'', # 0x81 -'', # 0x82 -'', # 0x83 -'', # 0x84 -'', # 0x85 -'', # 0x86 -'', # 0x87 -'', # 0x88 -'', # 0x89 -'', # 0x8a -'', # 0x8b -'', # 0x8c -'', # 0x8d -'', # 0x8e -'', # 0x8f -'', # 0x90 -'', # 0x91 -'', # 0x92 -'', # 0x93 -'', # 0x94 -'', # 0x95 -'', # 0x96 -'', # 0x97 -'', # 0x98 -'', # 0x99 -'', # 0x9a -'', # 0x9b -'', # 0x9c -'', # 0x9d -'', # 0x9e -'', # 0x9f -'', # 0xa0 -'', # 0xa1 -'', # 0xa2 -'', # 0xa3 -'', # 0xa4 -'', # 0xa5 -'', # 0xa6 -'', # 0xa7 -'', # 0xa8 -'', # 0xa9 -'', # 0xaa -'', # 0xab -'', # 0xac -'', # 0xad -'', # 0xae -'', # 0xaf -'', # 0xb0 -'', # 0xb1 -'', # 0xb2 -'', # 0xb3 -'', # 0xb4 -'', # 0xb5 -'', # 0xb6 -'', # 0xb7 -'', # 0xb8 -'', # 0xb9 -'', # 0xba -'', # 0xbb -'', # 0xbc -'', # 0xbd -'', # 0xbe -'', # 0xbf -'', # 0xc0 -'', # 0xc1 -'', # 0xc2 -'', # 0xc3 -'', # 0xc4 -'', # 0xc5 -'', # 0xc6 -'', # 0xc7 -'', # 0xc8 -'', # 0xc9 -'', # 0xca -'', # 0xcb -'', # 0xcc -'', # 0xcd -'', # 0xce -'', # 0xcf -'', # 0xd0 -'', # 0xd1 -'', # 0xd2 -'', # 0xd3 -'', # 0xd4 -'', # 0xd5 -'', # 0xd6 -'', # 0xd7 -'', # 0xd8 -'', # 0xd9 -'', # 0xda -'', # 0xdb -'', # 0xdc -'', # 0xdd -'', # 0xde -'', # 0xdf -'', # 0xe0 -'', # 0xe1 -'', # 0xe2 -'', # 0xe3 -'', # 0xe4 -'', # 0xe5 -'', # 0xe6 -'', # 0xe7 -'', # 0xe8 -'', # 0xe9 -'', # 0xea -'', # 0xeb -'', # 0xec -'', # 0xed -'', # 0xee -'', # 0xef -'', # 0xf0 -'', # 0xf1 -'', # 0xf2 -'', # 0xf3 -'', # 0xf4 -'', # 0xf5 -'', # 0xf6 -'', # 0xf7 -'', # 0xf8 -'', # 0xf9 -'', # 0xfa -'', # 0xfb -'', # 0xfc -'', # 0xfd -'', # 0xfe +None, # 0x70 +None, # 0x71 +None, # 0x72 +None, # 0x73 +None, # 0x74 +None, # 0x75 +None, # 0x76 +None, # 0x77 +None, # 0x78 +None, # 0x79 +None, # 0x7a +None, # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f +None, # 0x80 +None, # 0x81 +None, # 0x82 +None, # 0x83 +None, # 0x84 +None, # 0x85 +None, # 0x86 +None, # 0x87 +None, # 0x88 +None, # 0x89 +None, # 0x8a +None, # 0x8b +None, # 0x8c +None, # 0x8d +None, # 0x8e +None, # 0x8f +None, # 0x90 +None, # 0x91 +None, # 0x92 +None, # 0x93 +None, # 0x94 +None, # 0x95 +None, # 0x96 +None, # 0x97 +None, # 0x98 +None, # 0x99 +None, # 0x9a +None, # 0x9b +None, # 0x9c +None, # 0x9d +None, # 0x9e +None, # 0x9f +None, # 0xa0 +None, # 0xa1 +None, # 0xa2 +None, # 0xa3 +None, # 0xa4 +None, # 0xa5 +None, # 0xa6 +None, # 0xa7 +None, # 0xa8 +None, # 0xa9 +None, # 0xaa +None, # 0xab +None, # 0xac +None, # 0xad +None, # 0xae +None, # 0xaf +None, # 0xb0 +None, # 0xb1 +None, # 0xb2 +None, # 0xb3 +None, # 0xb4 +None, # 0xb5 +None, # 0xb6 +None, # 0xb7 +None, # 0xb8 +None, # 0xb9 +None, # 0xba +None, # 0xbb +None, # 0xbc +None, # 0xbd +None, # 0xbe +None, # 0xbf +None, # 0xc0 +None, # 0xc1 +None, # 0xc2 +None, # 0xc3 +None, # 0xc4 +None, # 0xc5 +None, # 0xc6 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x02e.py b/libs/common/unidecode/x02e.py index feaad8d3..32a45fcd 100644 --- a/libs/common/unidecode/x02e.py +++ b/libs/common/unidecode/x02e.py @@ -1,257 +1,257 @@ data = ( -'[?]', # 0x00 -'[?]', # 0x01 -'[?]', # 0x02 -'[?]', # 0x03 -'[?]', # 0x04 -'[?]', # 0x05 -'[?]', # 0x06 -'[?]', # 0x07 -'[?]', # 0x08 -'[?]', # 0x09 -'[?]', # 0x0a -'[?]', # 0x0b -'[?]', # 0x0c -'[?]', # 0x0d -'[?]', # 0x0e -'[?]', # 0x0f -'[?]', # 0x10 -'[?]', # 0x11 -'[?]', # 0x12 -'[?]', # 0x13 -'[?]', # 0x14 -'[?]', # 0x15 -'[?]', # 0x16 -'[?]', # 0x17 -'[?]', # 0x18 -'[?]', # 0x19 -'[?]', # 0x1a -'[?]', # 0x1b -'[?]', # 0x1c -'[?]', # 0x1d -'[?]', # 0x1e -'[?]', # 0x1f -'[?]', # 0x20 -'[?]', # 0x21 -'[?]', # 0x22 -'[?]', # 0x23 -'[?]', # 0x24 -'[?]', # 0x25 -'[?]', # 0x26 -'[?]', # 0x27 -'[?]', # 0x28 -'[?]', # 0x29 -'[?]', # 0x2a -'[?]', # 0x2b -'[?]', # 0x2c -'[?]', # 0x2d -'[?]', # 0x2e -'[?]', # 0x2f -'[?]', # 0x30 -'[?]', # 0x31 -'[?]', # 0x32 -'[?]', # 0x33 -'[?]', # 0x34 -'[?]', # 0x35 -'[?]', # 0x36 -'[?]', # 0x37 -'[?]', # 0x38 -'[?]', # 0x39 -'[?]', # 0x3a -'[?]', # 0x3b -'[?]', # 0x3c -'[?]', # 0x3d -'[?]', # 0x3e -'[?]', # 0x3f -'[?]', # 0x40 -'[?]', # 0x41 -'[?]', # 0x42 -'[?]', # 0x43 -'[?]', # 0x44 -'[?]', # 0x45 -'[?]', # 0x46 -'[?]', # 0x47 -'[?]', # 0x48 -'[?]', # 0x49 -'[?]', # 0x4a -'[?]', # 0x4b -'[?]', # 0x4c -'[?]', # 0x4d -'[?]', # 0x4e -'[?]', # 0x4f -'[?]', # 0x50 -'[?]', # 0x51 -'[?]', # 0x52 -'[?]', # 0x53 -'[?]', # 0x54 -'[?]', # 0x55 -'[?]', # 0x56 -'[?]', # 0x57 -'[?]', # 0x58 -'[?]', # 0x59 -'[?]', # 0x5a -'[?]', # 0x5b -'[?]', # 0x5c -'[?]', # 0x5d -'[?]', # 0x5e -'[?]', # 0x5f -'[?]', # 0x60 -'[?]', # 0x61 -'[?]', # 0x62 -'[?]', # 0x63 -'[?]', # 0x64 -'[?]', # 0x65 -'[?]', # 0x66 -'[?]', # 0x67 -'[?]', # 0x68 -'[?]', # 0x69 -'[?]', # 0x6a -'[?]', # 0x6b -'[?]', # 0x6c -'[?]', # 0x6d -'[?]', # 0x6e -'[?]', # 0x6f -'[?]', # 0x70 -'[?]', # 0x71 -'[?]', # 0x72 -'[?]', # 0x73 -'[?]', # 0x74 -'[?]', # 0x75 -'[?]', # 0x76 -'[?]', # 0x77 -'[?]', # 0x78 -'[?]', # 0x79 -'[?]', # 0x7a -'[?]', # 0x7b -'[?]', # 0x7c -'[?]', # 0x7d -'[?]', # 0x7e -'[?]', # 0x7f -'[?] ', # 0x80 -'[?] ', # 0x81 -'[?] ', # 0x82 -'[?] ', # 0x83 -'[?] ', # 0x84 -'[?] ', # 0x85 -'[?] ', # 0x86 -'[?] ', # 0x87 -'[?] ', # 0x88 -'[?] ', # 0x89 -'[?] ', # 0x8a -'[?] ', # 0x8b -'[?] ', # 0x8c -'[?] ', # 0x8d -'[?] ', # 0x8e -'[?] ', # 0x8f -'[?] ', # 0x90 -'[?] ', # 0x91 -'[?] ', # 0x92 -'[?] ', # 0x93 -'[?] ', # 0x94 -'[?] ', # 0x95 -'[?] ', # 0x96 -'[?] ', # 0x97 -'[?] ', # 0x98 -'[?] ', # 0x99 -'[?]', # 0x9a -'[?] ', # 0x9b -'[?] ', # 0x9c -'[?] ', # 0x9d -'[?] ', # 0x9e -'[?] ', # 0x9f -'[?] ', # 0xa0 -'[?] ', # 0xa1 -'[?] ', # 0xa2 -'[?] ', # 0xa3 -'[?] ', # 0xa4 -'[?] ', # 0xa5 -'[?] ', # 0xa6 -'[?] ', # 0xa7 -'[?] ', # 0xa8 -'[?] ', # 0xa9 -'[?] ', # 0xaa -'[?] ', # 0xab -'[?] ', # 0xac -'[?] ', # 0xad -'[?] ', # 0xae -'[?] ', # 0xaf -'[?] ', # 0xb0 -'[?] ', # 0xb1 -'[?] ', # 0xb2 -'[?] ', # 0xb3 -'[?] ', # 0xb4 -'[?] ', # 0xb5 -'[?] ', # 0xb6 -'[?] ', # 0xb7 -'[?] ', # 0xb8 -'[?] ', # 0xb9 -'[?] ', # 0xba -'[?] ', # 0xbb -'[?] ', # 0xbc -'[?] ', # 0xbd -'[?] ', # 0xbe -'[?] ', # 0xbf -'[?] ', # 0xc0 -'[?] ', # 0xc1 -'[?] ', # 0xc2 -'[?] ', # 0xc3 -'[?] ', # 0xc4 -'[?] ', # 0xc5 -'[?] ', # 0xc6 -'[?] ', # 0xc7 -'[?] ', # 0xc8 -'[?] ', # 0xc9 -'[?] ', # 0xca -'[?] ', # 0xcb -'[?] ', # 0xcc -'[?] ', # 0xcd -'[?] ', # 0xce -'[?] ', # 0xcf -'[?] ', # 0xd0 -'[?] ', # 0xd1 -'[?] ', # 0xd2 -'[?] ', # 0xd3 -'[?] ', # 0xd4 -'[?] ', # 0xd5 -'[?] ', # 0xd6 -'[?] ', # 0xd7 -'[?] ', # 0xd8 -'[?] ', # 0xd9 -'[?] ', # 0xda -'[?] ', # 0xdb -'[?] ', # 0xdc -'[?] ', # 0xdd -'[?] ', # 0xde -'[?] ', # 0xdf -'[?] ', # 0xe0 -'[?] ', # 0xe1 -'[?] ', # 0xe2 -'[?] ', # 0xe3 -'[?] ', # 0xe4 -'[?] ', # 0xe5 -'[?] ', # 0xe6 -'[?] ', # 0xe7 -'[?] ', # 0xe8 -'[?] ', # 0xe9 -'[?] ', # 0xea -'[?] ', # 0xeb -'[?] ', # 0xec -'[?] ', # 0xed -'[?] ', # 0xee -'[?] ', # 0xef -'[?] ', # 0xf0 -'[?] ', # 0xf1 -'[?] ', # 0xf2 -'[?] ', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +'r', # 0x00 +'r.', # 0x01 +None, # 0x02 +None, # 0x03 +None, # 0x04 +None, # 0x05 +'T', # 0x06 +'T.', # 0x07 +None, # 0x08 +'s', # 0x09 +None, # 0x0a +'[]', # 0x0b +'\\', # 0x0c +'/', # 0x0d +None, # 0x0e +'__', # 0x0f +None, # 0x10 +None, # 0x11 +'>', # 0x12 +'%', # 0x13 +None, # 0x14 +None, # 0x15 +'>', # 0x16 +'=', # 0x17 +None, # 0x18 +'/', # 0x19 +'-', # 0x1a +'~', # 0x1b +'\\', # 0x1c +'/', # 0x1d +'~', # 0x1e +'~', # 0x1f +'|-', # 0x20 +'-|', # 0x21 +None, # 0x22 +None, # 0x23 +None, # 0x24 +None, # 0x25 +'<=', # 0x26 +'=>', # 0x27 +'((', # 0x28 +'))', # 0x29 +None, # 0x2a +None, # 0x2b +'::', # 0x2c +None, # 0x2d +'?', # 0x2e +'\'', # 0x2f +'o', # 0x30 +'.', # 0x31 +',', # 0x32 +'.', # 0x33 +',', # 0x34 +';', # 0x35 +None, # 0x36 +None, # 0x37 +None, # 0x38 +None, # 0x39 +'----', # 0x3a +'------', # 0x3b +'x', # 0x3c +'|', # 0x3d +None, # 0x3e +None, # 0x3f +'=', # 0x40 +',', # 0x41 +'"', # 0x42 +'`--', # 0x43 +None, # 0x44 +None, # 0x45 +None, # 0x46 +None, # 0x47 +None, # 0x48 +None, # 0x49 +None, # 0x4a +None, # 0x4b +None, # 0x4c +None, # 0x4d +None, # 0x4e +None, # 0x4f +None, # 0x50 +None, # 0x51 +None, # 0x52 +None, # 0x53 +None, # 0x54 +None, # 0x55 +None, # 0x56 +None, # 0x57 +None, # 0x58 +None, # 0x59 +None, # 0x5a +None, # 0x5b +None, # 0x5c +None, # 0x5d +None, # 0x5e +None, # 0x5f +None, # 0x60 +None, # 0x61 +None, # 0x62 +None, # 0x63 +None, # 0x64 +None, # 0x65 +None, # 0x66 +None, # 0x67 +None, # 0x68 +None, # 0x69 +None, # 0x6a +None, # 0x6b +None, # 0x6c +None, # 0x6d +None, # 0x6e +None, # 0x6f +None, # 0x70 +None, # 0x71 +None, # 0x72 +None, # 0x73 +None, # 0x74 +None, # 0x75 +None, # 0x76 +None, # 0x77 +None, # 0x78 +None, # 0x79 +None, # 0x7a +None, # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f +None, # 0x80 +None, # 0x81 +None, # 0x82 +None, # 0x83 +None, # 0x84 +None, # 0x85 +None, # 0x86 +None, # 0x87 +None, # 0x88 +None, # 0x89 +None, # 0x8a +None, # 0x8b +None, # 0x8c +None, # 0x8d +None, # 0x8e +None, # 0x8f +None, # 0x90 +None, # 0x91 +None, # 0x92 +None, # 0x93 +None, # 0x94 +None, # 0x95 +None, # 0x96 +None, # 0x97 +None, # 0x98 +None, # 0x99 +None, # 0x9a +None, # 0x9b +None, # 0x9c +None, # 0x9d +None, # 0x9e +None, # 0x9f +None, # 0xa0 +None, # 0xa1 +None, # 0xa2 +None, # 0xa3 +None, # 0xa4 +None, # 0xa5 +None, # 0xa6 +None, # 0xa7 +None, # 0xa8 +None, # 0xa9 +None, # 0xaa +None, # 0xab +None, # 0xac +None, # 0xad +None, # 0xae +None, # 0xaf +None, # 0xb0 +None, # 0xb1 +None, # 0xb2 +None, # 0xb3 +None, # 0xb4 +None, # 0xb5 +None, # 0xb6 +None, # 0xb7 +None, # 0xb8 +None, # 0xb9 +None, # 0xba +None, # 0xbb +None, # 0xbc +None, # 0xbd +None, # 0xbe +None, # 0xbf +None, # 0xc0 +None, # 0xc1 +None, # 0xc2 +None, # 0xc3 +None, # 0xc4 +None, # 0xc5 +None, # 0xc6 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x02f.py b/libs/common/unidecode/x02f.py index 01f8b15b..eccead0c 100644 --- a/libs/common/unidecode/x02f.py +++ b/libs/common/unidecode/x02f.py @@ -1,257 +1,257 @@ data = ( -'[?] ', # 0x00 -'[?] ', # 0x01 -'[?] ', # 0x02 -'[?] ', # 0x03 -'[?] ', # 0x04 -'[?] ', # 0x05 -'[?] ', # 0x06 -'[?] ', # 0x07 -'[?] ', # 0x08 -'[?] ', # 0x09 -'[?] ', # 0x0a -'[?] ', # 0x0b -'[?] ', # 0x0c -'[?] ', # 0x0d -'[?] ', # 0x0e -'[?] ', # 0x0f -'[?] ', # 0x10 -'[?] ', # 0x11 -'[?] ', # 0x12 -'[?] ', # 0x13 -'[?] ', # 0x14 -'[?] ', # 0x15 -'[?] ', # 0x16 -'[?] ', # 0x17 -'[?] ', # 0x18 -'[?] ', # 0x19 -'[?] ', # 0x1a -'[?] ', # 0x1b -'[?] ', # 0x1c -'[?] ', # 0x1d -'[?] ', # 0x1e -'[?] ', # 0x1f -'[?] ', # 0x20 -'[?] ', # 0x21 -'[?] ', # 0x22 -'[?] ', # 0x23 -'[?] ', # 0x24 -'[?] ', # 0x25 -'[?] ', # 0x26 -'[?] ', # 0x27 -'[?] ', # 0x28 -'[?] ', # 0x29 -'[?] ', # 0x2a -'[?] ', # 0x2b -'[?] ', # 0x2c -'[?] ', # 0x2d -'[?] ', # 0x2e -'[?] ', # 0x2f -'[?] ', # 0x30 -'[?] ', # 0x31 -'[?] ', # 0x32 -'[?] ', # 0x33 -'[?] ', # 0x34 -'[?] ', # 0x35 -'[?] ', # 0x36 -'[?] ', # 0x37 -'[?] ', # 0x38 -'[?] ', # 0x39 -'[?] ', # 0x3a -'[?] ', # 0x3b -'[?] ', # 0x3c -'[?] ', # 0x3d -'[?] ', # 0x3e -'[?] ', # 0x3f -'[?] ', # 0x40 -'[?] ', # 0x41 -'[?] ', # 0x42 -'[?] ', # 0x43 -'[?] ', # 0x44 -'[?] ', # 0x45 -'[?] ', # 0x46 -'[?] ', # 0x47 -'[?] ', # 0x48 -'[?] ', # 0x49 -'[?] ', # 0x4a -'[?] ', # 0x4b -'[?] ', # 0x4c -'[?] ', # 0x4d -'[?] ', # 0x4e -'[?] ', # 0x4f -'[?] ', # 0x50 -'[?] ', # 0x51 -'[?] ', # 0x52 -'[?] ', # 0x53 -'[?] ', # 0x54 -'[?] ', # 0x55 -'[?] ', # 0x56 -'[?] ', # 0x57 -'[?] ', # 0x58 -'[?] ', # 0x59 -'[?] ', # 0x5a -'[?] ', # 0x5b -'[?] ', # 0x5c -'[?] ', # 0x5d -'[?] ', # 0x5e -'[?] ', # 0x5f -'[?] ', # 0x60 -'[?] ', # 0x61 -'[?] ', # 0x62 -'[?] ', # 0x63 -'[?] ', # 0x64 -'[?] ', # 0x65 -'[?] ', # 0x66 -'[?] ', # 0x67 -'[?] ', # 0x68 -'[?] ', # 0x69 -'[?] ', # 0x6a -'[?] ', # 0x6b -'[?] ', # 0x6c -'[?] ', # 0x6d -'[?] ', # 0x6e -'[?] ', # 0x6f -'[?] ', # 0x70 -'[?] ', # 0x71 -'[?] ', # 0x72 -'[?] ', # 0x73 -'[?] ', # 0x74 -'[?] ', # 0x75 -'[?] ', # 0x76 -'[?] ', # 0x77 -'[?] ', # 0x78 -'[?] ', # 0x79 -'[?] ', # 0x7a -'[?] ', # 0x7b -'[?] ', # 0x7c -'[?] ', # 0x7d -'[?] ', # 0x7e -'[?] ', # 0x7f -'[?] ', # 0x80 -'[?] ', # 0x81 -'[?] ', # 0x82 -'[?] ', # 0x83 -'[?] ', # 0x84 -'[?] ', # 0x85 -'[?] ', # 0x86 -'[?] ', # 0x87 -'[?] ', # 0x88 -'[?] ', # 0x89 -'[?] ', # 0x8a -'[?] ', # 0x8b -'[?] ', # 0x8c -'[?] ', # 0x8d -'[?] ', # 0x8e -'[?] ', # 0x8f -'[?] ', # 0x90 -'[?] ', # 0x91 -'[?] ', # 0x92 -'[?] ', # 0x93 -'[?] ', # 0x94 -'[?] ', # 0x95 -'[?] ', # 0x96 -'[?] ', # 0x97 -'[?] ', # 0x98 -'[?] ', # 0x99 -'[?] ', # 0x9a -'[?] ', # 0x9b -'[?] ', # 0x9c -'[?] ', # 0x9d -'[?] ', # 0x9e -'[?] ', # 0x9f -'[?] ', # 0xa0 -'[?] ', # 0xa1 -'[?] ', # 0xa2 -'[?] ', # 0xa3 -'[?] ', # 0xa4 -'[?] ', # 0xa5 -'[?] ', # 0xa6 -'[?] ', # 0xa7 -'[?] ', # 0xa8 -'[?] ', # 0xa9 -'[?] ', # 0xaa -'[?] ', # 0xab -'[?] ', # 0xac -'[?] ', # 0xad -'[?] ', # 0xae -'[?] ', # 0xaf -'[?] ', # 0xb0 -'[?] ', # 0xb1 -'[?] ', # 0xb2 -'[?] ', # 0xb3 -'[?] ', # 0xb4 -'[?] ', # 0xb5 -'[?] ', # 0xb6 -'[?] ', # 0xb7 -'[?] ', # 0xb8 -'[?] ', # 0xb9 -'[?] ', # 0xba -'[?] ', # 0xbb -'[?] ', # 0xbc -'[?] ', # 0xbd -'[?] ', # 0xbe -'[?] ', # 0xbf -'[?] ', # 0xc0 -'[?] ', # 0xc1 -'[?] ', # 0xc2 -'[?] ', # 0xc3 -'[?] ', # 0xc4 -'[?] ', # 0xc5 -'[?] ', # 0xc6 -'[?] ', # 0xc7 -'[?] ', # 0xc8 -'[?] ', # 0xc9 -'[?] ', # 0xca -'[?] ', # 0xcb -'[?] ', # 0xcc -'[?] ', # 0xcd -'[?] ', # 0xce -'[?] ', # 0xcf -'[?] ', # 0xd0 -'[?] ', # 0xd1 -'[?] ', # 0xd2 -'[?] ', # 0xd3 -'[?] ', # 0xd4 -'[?] ', # 0xd5 -'[?]', # 0xd6 -'[?]', # 0xd7 -'[?]', # 0xd8 -'[?]', # 0xd9 -'[?]', # 0xda -'[?]', # 0xdb -'[?]', # 0xdc -'[?]', # 0xdd -'[?]', # 0xde -'[?]', # 0xdf -'[?]', # 0xe0 -'[?]', # 0xe1 -'[?]', # 0xe2 -'[?]', # 0xe3 -'[?]', # 0xe4 -'[?]', # 0xe5 -'[?]', # 0xe6 -'[?]', # 0xe7 -'[?]', # 0xe8 -'[?]', # 0xe9 -'[?]', # 0xea -'[?]', # 0xeb -'[?]', # 0xec -'[?]', # 0xed -'[?]', # 0xee -'[?]', # 0xef -'[?] ', # 0xf0 -'[?] ', # 0xf1 -'[?] ', # 0xf2 -'[?] ', # 0xf3 -'[?] ', # 0xf4 -'[?] ', # 0xf5 -'[?] ', # 0xf6 -'[?] ', # 0xf7 -'[?] ', # 0xf8 -'[?] ', # 0xf9 -'[?] ', # 0xfa -'[?] ', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0x00 +None, # 0x01 +None, # 0x02 +None, # 0x03 +None, # 0x04 +None, # 0x05 +None, # 0x06 +None, # 0x07 +None, # 0x08 +None, # 0x09 +None, # 0x0a +None, # 0x0b +None, # 0x0c +None, # 0x0d +None, # 0x0e +None, # 0x0f +None, # 0x10 +None, # 0x11 +None, # 0x12 +None, # 0x13 +None, # 0x14 +None, # 0x15 +None, # 0x16 +None, # 0x17 +None, # 0x18 +None, # 0x19 +None, # 0x1a +None, # 0x1b +None, # 0x1c +None, # 0x1d +None, # 0x1e +None, # 0x1f +None, # 0x20 +None, # 0x21 +None, # 0x22 +None, # 0x23 +None, # 0x24 +None, # 0x25 +None, # 0x26 +None, # 0x27 +None, # 0x28 +None, # 0x29 +None, # 0x2a +None, # 0x2b +None, # 0x2c +None, # 0x2d +None, # 0x2e +None, # 0x2f +None, # 0x30 +None, # 0x31 +None, # 0x32 +None, # 0x33 +None, # 0x34 +None, # 0x35 +None, # 0x36 +None, # 0x37 +None, # 0x38 +None, # 0x39 +None, # 0x3a +None, # 0x3b +None, # 0x3c +None, # 0x3d +None, # 0x3e +None, # 0x3f +None, # 0x40 +None, # 0x41 +None, # 0x42 +None, # 0x43 +None, # 0x44 +None, # 0x45 +None, # 0x46 +None, # 0x47 +None, # 0x48 +None, # 0x49 +None, # 0x4a +None, # 0x4b +None, # 0x4c +None, # 0x4d +None, # 0x4e +None, # 0x4f +None, # 0x50 +None, # 0x51 +None, # 0x52 +None, # 0x53 +None, # 0x54 +None, # 0x55 +None, # 0x56 +None, # 0x57 +None, # 0x58 +None, # 0x59 +None, # 0x5a +None, # 0x5b +None, # 0x5c +None, # 0x5d +None, # 0x5e +None, # 0x5f +None, # 0x60 +None, # 0x61 +None, # 0x62 +None, # 0x63 +None, # 0x64 +None, # 0x65 +None, # 0x66 +None, # 0x67 +None, # 0x68 +None, # 0x69 +None, # 0x6a +None, # 0x6b +None, # 0x6c +None, # 0x6d +None, # 0x6e +None, # 0x6f +None, # 0x70 +None, # 0x71 +None, # 0x72 +None, # 0x73 +None, # 0x74 +None, # 0x75 +None, # 0x76 +None, # 0x77 +None, # 0x78 +None, # 0x79 +None, # 0x7a +None, # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f +None, # 0x80 +None, # 0x81 +None, # 0x82 +None, # 0x83 +None, # 0x84 +None, # 0x85 +None, # 0x86 +None, # 0x87 +None, # 0x88 +None, # 0x89 +None, # 0x8a +None, # 0x8b +None, # 0x8c +None, # 0x8d +None, # 0x8e +None, # 0x8f +None, # 0x90 +None, # 0x91 +None, # 0x92 +None, # 0x93 +None, # 0x94 +None, # 0x95 +None, # 0x96 +None, # 0x97 +None, # 0x98 +None, # 0x99 +None, # 0x9a +None, # 0x9b +None, # 0x9c +None, # 0x9d +None, # 0x9e +None, # 0x9f +None, # 0xa0 +None, # 0xa1 +None, # 0xa2 +None, # 0xa3 +None, # 0xa4 +None, # 0xa5 +None, # 0xa6 +None, # 0xa7 +None, # 0xa8 +None, # 0xa9 +None, # 0xaa +None, # 0xab +None, # 0xac +None, # 0xad +None, # 0xae +None, # 0xaf +None, # 0xb0 +None, # 0xb1 +None, # 0xb2 +None, # 0xb3 +None, # 0xb4 +None, # 0xb5 +None, # 0xb6 +None, # 0xb7 +None, # 0xb8 +None, # 0xb9 +None, # 0xba +None, # 0xbb +None, # 0xbc +None, # 0xbd +None, # 0xbe +None, # 0xbf +None, # 0xc0 +None, # 0xc1 +None, # 0xc2 +None, # 0xc3 +None, # 0xc4 +None, # 0xc5 +None, # 0xc6 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x030.py b/libs/common/unidecode/x030.py index d65ed4c5..549ee824 100644 --- a/libs/common/unidecode/x030.py +++ b/libs/common/unidecode/x030.py @@ -58,12 +58,12 @@ data = ( '+10+', # 0x38 '+20+', # 0x39 '+30+', # 0x3a -'[?]', # 0x3b -'[?]', # 0x3c -'[?]', # 0x3d +None, # 0x3b +None, # 0x3c +None, # 0x3d '', # 0x3e '', # 0x3f -'[?]', # 0x40 +None, # 0x40 'a', # 0x41 'a', # 0x42 'i', # 0x43 @@ -148,18 +148,18 @@ data = ( 'wo', # 0x92 'n', # 0x93 'vu', # 0x94 -'[?]', # 0x95 -'[?]', # 0x96 -'[?]', # 0x97 -'[?]', # 0x98 +None, # 0x95 +None, # 0x96 +None, # 0x97 +None, # 0x98 '', # 0x99 '', # 0x9a '', # 0x9b '', # 0x9c '"', # 0x9d '"', # 0x9e -'[?]', # 0x9f -'[?]', # 0xa0 +None, # 0x9f +None, # 0xa0 'a', # 0xa1 'a', # 0xa2 'i', # 0xa3 diff --git a/libs/common/unidecode/x031.py b/libs/common/unidecode/x031.py index f5576080..fe57d2fb 100644 --- a/libs/common/unidecode/x031.py +++ b/libs/common/unidecode/x031.py @@ -1,9 +1,9 @@ data = ( -'[?]', # 0x00 -'[?]', # 0x01 -'[?]', # 0x02 -'[?]', # 0x03 -'[?]', # 0x04 +None, # 0x00 +None, # 0x01 +None, # 0x02 +None, # 0x03 +None, # 0x04 'B', # 0x05 'P', # 0x06 'M', # 0x07 @@ -44,10 +44,10 @@ data = ( 'V', # 0x2a 'NG', # 0x2b 'GN', # 0x2c -'[?]', # 0x2d -'[?]', # 0x2e -'[?]', # 0x2f -'[?]', # 0x30 +None, # 0x2d +None, # 0x2e +None, # 0x2f +None, # 0x30 'g', # 0x31 'gg', # 0x32 'gs', # 0x33 @@ -142,7 +142,7 @@ data = ( 'yu-i', # 0x8c 'U', # 0x8d 'U-i', # 0x8e -'[?]', # 0x8f +None, # 0x8f '', # 0x90 '', # 0x91 '', # 0x92 @@ -183,75 +183,75 @@ data = ( 'T', # 0xb5 'K', # 0xb6 'H', # 0xb7 -'[?]', # 0xb8 -'[?]', # 0xb9 -'[?]', # 0xba -'[?]', # 0xbb -'[?]', # 0xbc -'[?]', # 0xbd -'[?]', # 0xbe -'[?]', # 0xbf -'[?]', # 0xc0 -'[?]', # 0xc1 -'[?]', # 0xc2 -'[?]', # 0xc3 -'[?]', # 0xc4 -'[?]', # 0xc5 -'[?]', # 0xc6 -'[?]', # 0xc7 -'[?]', # 0xc8 -'[?]', # 0xc9 -'[?]', # 0xca -'[?]', # 0xcb -'[?]', # 0xcc -'[?]', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf -'[?]', # 0xd0 -'[?]', # 0xd1 -'[?]', # 0xd2 -'[?]', # 0xd3 -'[?]', # 0xd4 -'[?]', # 0xd5 -'[?]', # 0xd6 -'[?]', # 0xd7 -'[?]', # 0xd8 -'[?]', # 0xd9 -'[?]', # 0xda -'[?]', # 0xdb -'[?]', # 0xdc -'[?]', # 0xdd -'[?]', # 0xde -'[?]', # 0xdf -'[?]', # 0xe0 -'[?]', # 0xe1 -'[?]', # 0xe2 -'[?]', # 0xe3 -'[?]', # 0xe4 -'[?]', # 0xe5 -'[?]', # 0xe6 -'[?]', # 0xe7 -'[?]', # 0xe8 -'[?]', # 0xe9 -'[?]', # 0xea -'[?]', # 0xeb -'[?]', # 0xec -'[?]', # 0xed -'[?]', # 0xee -'[?]', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xb8 +None, # 0xb9 +None, # 0xba +None, # 0xbb +None, # 0xbc +None, # 0xbd +None, # 0xbe +None, # 0xbf +None, # 0xc0 +None, # 0xc1 +None, # 0xc2 +None, # 0xc3 +None, # 0xc4 +None, # 0xc5 +None, # 0xc6 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x032.py b/libs/common/unidecode/x032.py index a0c21d11..2aa497a2 100644 --- a/libs/common/unidecode/x032.py +++ b/libs/common/unidecode/x032.py @@ -28,9 +28,9 @@ data = ( '(pa)', # 0x1a '(ha)', # 0x1b '(ju)', # 0x1c -'[?]', # 0x1d -'[?]', # 0x1e -'[?]', # 0x1f +None, # 0x1d +None, # 0x1e +None, # 0x1f '(1) ', # 0x20 '(2) ', # 0x21 '(3) ', # 0x22 @@ -67,19 +67,19 @@ data = ( '(Xiu) ', # 0x41 '<<', # 0x42 '>>', # 0x43 -'[?]', # 0x44 -'[?]', # 0x45 -'[?]', # 0x46 -'[?]', # 0x47 -'[?]', # 0x48 -'[?]', # 0x49 -'[?]', # 0x4a -'[?]', # 0x4b -'[?]', # 0x4c -'[?]', # 0x4d -'[?]', # 0x4e -'[?]', # 0x4f -'[?]', # 0x50 +None, # 0x44 +None, # 0x45 +None, # 0x46 +None, # 0x47 +None, # 0x48 +None, # 0x49 +None, # 0x4a +None, # 0x4b +None, # 0x4c +None, # 0x4d +None, # 0x4e +None, # 0x4f +None, # 0x50 '21', # 0x51 '22', # 0x52 '23', # 0x53 @@ -123,9 +123,9 @@ data = ( '(ta)', # 0x79 '(pa)', # 0x7a '(ha)', # 0x7b -'[?]', # 0x7c -'[?]', # 0x7d -'[?]', # 0x7e +None, # 0x7c +None, # 0x7d +None, # 0x7e 'KIS ', # 0x7f '(1) ', # 0x80 '(2) ', # 0x81 diff --git a/libs/common/unidecode/x04d.py b/libs/common/unidecode/x04d.py index b025461a..eccead0c 100644 --- a/libs/common/unidecode/x04d.py +++ b/libs/common/unidecode/x04d.py @@ -1,257 +1,257 @@ data = ( -'[?] ', # 0x00 -'[?] ', # 0x01 -'[?] ', # 0x02 -'[?] ', # 0x03 -'[?] ', # 0x04 -'[?] ', # 0x05 -'[?] ', # 0x06 -'[?] ', # 0x07 -'[?] ', # 0x08 -'[?] ', # 0x09 -'[?] ', # 0x0a -'[?] ', # 0x0b -'[?] ', # 0x0c -'[?] ', # 0x0d -'[?] ', # 0x0e -'[?] ', # 0x0f -'[?] ', # 0x10 -'[?] ', # 0x11 -'[?] ', # 0x12 -'[?] ', # 0x13 -'[?] ', # 0x14 -'[?] ', # 0x15 -'[?] ', # 0x16 -'[?] ', # 0x17 -'[?] ', # 0x18 -'[?] ', # 0x19 -'[?] ', # 0x1a -'[?] ', # 0x1b -'[?] ', # 0x1c -'[?] ', # 0x1d -'[?] ', # 0x1e -'[?] ', # 0x1f -'[?] ', # 0x20 -'[?] ', # 0x21 -'[?] ', # 0x22 -'[?] ', # 0x23 -'[?] ', # 0x24 -'[?] ', # 0x25 -'[?] ', # 0x26 -'[?] ', # 0x27 -'[?] ', # 0x28 -'[?] ', # 0x29 -'[?] ', # 0x2a -'[?] ', # 0x2b -'[?] ', # 0x2c -'[?] ', # 0x2d -'[?] ', # 0x2e -'[?] ', # 0x2f -'[?] ', # 0x30 -'[?] ', # 0x31 -'[?] ', # 0x32 -'[?] ', # 0x33 -'[?] ', # 0x34 -'[?] ', # 0x35 -'[?] ', # 0x36 -'[?] ', # 0x37 -'[?] ', # 0x38 -'[?] ', # 0x39 -'[?] ', # 0x3a -'[?] ', # 0x3b -'[?] ', # 0x3c -'[?] ', # 0x3d -'[?] ', # 0x3e -'[?] ', # 0x3f -'[?] ', # 0x40 -'[?] ', # 0x41 -'[?] ', # 0x42 -'[?] ', # 0x43 -'[?] ', # 0x44 -'[?] ', # 0x45 -'[?] ', # 0x46 -'[?] ', # 0x47 -'[?] ', # 0x48 -'[?] ', # 0x49 -'[?] ', # 0x4a -'[?] ', # 0x4b -'[?] ', # 0x4c -'[?] ', # 0x4d -'[?] ', # 0x4e -'[?] ', # 0x4f -'[?] ', # 0x50 -'[?] ', # 0x51 -'[?] ', # 0x52 -'[?] ', # 0x53 -'[?] ', # 0x54 -'[?] ', # 0x55 -'[?] ', # 0x56 -'[?] ', # 0x57 -'[?] ', # 0x58 -'[?] ', # 0x59 -'[?] ', # 0x5a -'[?] ', # 0x5b -'[?] ', # 0x5c -'[?] ', # 0x5d -'[?] ', # 0x5e -'[?] ', # 0x5f -'[?] ', # 0x60 -'[?] ', # 0x61 -'[?] ', # 0x62 -'[?] ', # 0x63 -'[?] ', # 0x64 -'[?] ', # 0x65 -'[?] ', # 0x66 -'[?] ', # 0x67 -'[?] ', # 0x68 -'[?] ', # 0x69 -'[?] ', # 0x6a -'[?] ', # 0x6b -'[?] ', # 0x6c -'[?] ', # 0x6d -'[?] ', # 0x6e -'[?] ', # 0x6f -'[?] ', # 0x70 -'[?] ', # 0x71 -'[?] ', # 0x72 -'[?] ', # 0x73 -'[?] ', # 0x74 -'[?] ', # 0x75 -'[?] ', # 0x76 -'[?] ', # 0x77 -'[?] ', # 0x78 -'[?] ', # 0x79 -'[?] ', # 0x7a -'[?] ', # 0x7b -'[?] ', # 0x7c -'[?] ', # 0x7d -'[?] ', # 0x7e -'[?] ', # 0x7f -'[?] ', # 0x80 -'[?] ', # 0x81 -'[?] ', # 0x82 -'[?] ', # 0x83 -'[?] ', # 0x84 -'[?] ', # 0x85 -'[?] ', # 0x86 -'[?] ', # 0x87 -'[?] ', # 0x88 -'[?] ', # 0x89 -'[?] ', # 0x8a -'[?] ', # 0x8b -'[?] ', # 0x8c -'[?] ', # 0x8d -'[?] ', # 0x8e -'[?] ', # 0x8f -'[?] ', # 0x90 -'[?] ', # 0x91 -'[?] ', # 0x92 -'[?] ', # 0x93 -'[?] ', # 0x94 -'[?] ', # 0x95 -'[?] ', # 0x96 -'[?] ', # 0x97 -'[?] ', # 0x98 -'[?] ', # 0x99 -'[?] ', # 0x9a -'[?] ', # 0x9b -'[?] ', # 0x9c -'[?] ', # 0x9d -'[?] ', # 0x9e -'[?] ', # 0x9f -'[?] ', # 0xa0 -'[?] ', # 0xa1 -'[?] ', # 0xa2 -'[?] ', # 0xa3 -'[?] ', # 0xa4 -'[?] ', # 0xa5 -'[?] ', # 0xa6 -'[?] ', # 0xa7 -'[?] ', # 0xa8 -'[?] ', # 0xa9 -'[?] ', # 0xaa -'[?] ', # 0xab -'[?] ', # 0xac -'[?] ', # 0xad -'[?] ', # 0xae -'[?] ', # 0xaf -'[?] ', # 0xb0 -'[?] ', # 0xb1 -'[?] ', # 0xb2 -'[?] ', # 0xb3 -'[?] ', # 0xb4 -'[?] ', # 0xb5 -'[?]', # 0xb6 -'[?]', # 0xb7 -'[?]', # 0xb8 -'[?]', # 0xb9 -'[?]', # 0xba -'[?]', # 0xbb -'[?]', # 0xbc -'[?]', # 0xbd -'[?]', # 0xbe -'[?]', # 0xbf -'[?]', # 0xc0 -'[?]', # 0xc1 -'[?]', # 0xc2 -'[?]', # 0xc3 -'[?]', # 0xc4 -'[?]', # 0xc5 -'[?]', # 0xc6 -'[?]', # 0xc7 -'[?]', # 0xc8 -'[?]', # 0xc9 -'[?]', # 0xca -'[?]', # 0xcb -'[?]', # 0xcc -'[?]', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf -'[?]', # 0xd0 -'[?]', # 0xd1 -'[?]', # 0xd2 -'[?]', # 0xd3 -'[?]', # 0xd4 -'[?]', # 0xd5 -'[?]', # 0xd6 -'[?]', # 0xd7 -'[?]', # 0xd8 -'[?]', # 0xd9 -'[?]', # 0xda -'[?]', # 0xdb -'[?]', # 0xdc -'[?]', # 0xdd -'[?]', # 0xde -'[?]', # 0xdf -'[?]', # 0xe0 -'[?]', # 0xe1 -'[?]', # 0xe2 -'[?]', # 0xe3 -'[?]', # 0xe4 -'[?]', # 0xe5 -'[?]', # 0xe6 -'[?]', # 0xe7 -'[?]', # 0xe8 -'[?]', # 0xe9 -'[?]', # 0xea -'[?]', # 0xeb -'[?]', # 0xec -'[?]', # 0xed -'[?]', # 0xee -'[?]', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0x00 +None, # 0x01 +None, # 0x02 +None, # 0x03 +None, # 0x04 +None, # 0x05 +None, # 0x06 +None, # 0x07 +None, # 0x08 +None, # 0x09 +None, # 0x0a +None, # 0x0b +None, # 0x0c +None, # 0x0d +None, # 0x0e +None, # 0x0f +None, # 0x10 +None, # 0x11 +None, # 0x12 +None, # 0x13 +None, # 0x14 +None, # 0x15 +None, # 0x16 +None, # 0x17 +None, # 0x18 +None, # 0x19 +None, # 0x1a +None, # 0x1b +None, # 0x1c +None, # 0x1d +None, # 0x1e +None, # 0x1f +None, # 0x20 +None, # 0x21 +None, # 0x22 +None, # 0x23 +None, # 0x24 +None, # 0x25 +None, # 0x26 +None, # 0x27 +None, # 0x28 +None, # 0x29 +None, # 0x2a +None, # 0x2b +None, # 0x2c +None, # 0x2d +None, # 0x2e +None, # 0x2f +None, # 0x30 +None, # 0x31 +None, # 0x32 +None, # 0x33 +None, # 0x34 +None, # 0x35 +None, # 0x36 +None, # 0x37 +None, # 0x38 +None, # 0x39 +None, # 0x3a +None, # 0x3b +None, # 0x3c +None, # 0x3d +None, # 0x3e +None, # 0x3f +None, # 0x40 +None, # 0x41 +None, # 0x42 +None, # 0x43 +None, # 0x44 +None, # 0x45 +None, # 0x46 +None, # 0x47 +None, # 0x48 +None, # 0x49 +None, # 0x4a +None, # 0x4b +None, # 0x4c +None, # 0x4d +None, # 0x4e +None, # 0x4f +None, # 0x50 +None, # 0x51 +None, # 0x52 +None, # 0x53 +None, # 0x54 +None, # 0x55 +None, # 0x56 +None, # 0x57 +None, # 0x58 +None, # 0x59 +None, # 0x5a +None, # 0x5b +None, # 0x5c +None, # 0x5d +None, # 0x5e +None, # 0x5f +None, # 0x60 +None, # 0x61 +None, # 0x62 +None, # 0x63 +None, # 0x64 +None, # 0x65 +None, # 0x66 +None, # 0x67 +None, # 0x68 +None, # 0x69 +None, # 0x6a +None, # 0x6b +None, # 0x6c +None, # 0x6d +None, # 0x6e +None, # 0x6f +None, # 0x70 +None, # 0x71 +None, # 0x72 +None, # 0x73 +None, # 0x74 +None, # 0x75 +None, # 0x76 +None, # 0x77 +None, # 0x78 +None, # 0x79 +None, # 0x7a +None, # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f +None, # 0x80 +None, # 0x81 +None, # 0x82 +None, # 0x83 +None, # 0x84 +None, # 0x85 +None, # 0x86 +None, # 0x87 +None, # 0x88 +None, # 0x89 +None, # 0x8a +None, # 0x8b +None, # 0x8c +None, # 0x8d +None, # 0x8e +None, # 0x8f +None, # 0x90 +None, # 0x91 +None, # 0x92 +None, # 0x93 +None, # 0x94 +None, # 0x95 +None, # 0x96 +None, # 0x97 +None, # 0x98 +None, # 0x99 +None, # 0x9a +None, # 0x9b +None, # 0x9c +None, # 0x9d +None, # 0x9e +None, # 0x9f +None, # 0xa0 +None, # 0xa1 +None, # 0xa2 +None, # 0xa3 +None, # 0xa4 +None, # 0xa5 +None, # 0xa6 +None, # 0xa7 +None, # 0xa8 +None, # 0xa9 +None, # 0xaa +None, # 0xab +None, # 0xac +None, # 0xad +None, # 0xae +None, # 0xaf +None, # 0xb0 +None, # 0xb1 +None, # 0xb2 +None, # 0xb3 +None, # 0xb4 +None, # 0xb5 +None, # 0xb6 +None, # 0xb7 +None, # 0xb8 +None, # 0xb9 +None, # 0xba +None, # 0xbb +None, # 0xbc +None, # 0xbd +None, # 0xbe +None, # 0xbf +None, # 0xc0 +None, # 0xc1 +None, # 0xc2 +None, # 0xc3 +None, # 0xc4 +None, # 0xc5 +None, # 0xc6 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x04e.py b/libs/common/unidecode/x04e.py index b472b855..6037a63e 100644 --- a/libs/common/unidecode/x04e.py +++ b/libs/common/unidecode/x04e.py @@ -5,7 +5,7 @@ data = ( 'Qi ', # 0x03 'Shang ', # 0x04 'Xia ', # 0x05 -'[?] ', # 0x06 +None, # 0x06 'Mo ', # 0x07 'Zhang ', # 0x08 'San ', # 0x09 @@ -73,7 +73,7 @@ data = ( 'Zhe ', # 0x47 'Yao ', # 0x48 'Yi ', # 0x49 -'[?] ', # 0x4a +None, # 0x4a 'Zhi ', # 0x4b 'Wu ', # 0x4c 'Zha ', # 0x4d @@ -90,7 +90,7 @@ data = ( 'Cheng ', # 0x58 'Yi ', # 0x59 'Yin ', # 0x5a -'[?] ', # 0x5b +None, # 0x5b 'Mie ', # 0x5c 'Jiu ', # 0x5d 'Qi ', # 0x5e @@ -100,7 +100,7 @@ data = ( 'Gai ', # 0x62 'Diu ', # 0x63 'Hal ', # 0x64 -'[?] ', # 0x65 +None, # 0x65 'Shu ', # 0x66 'Twul ', # 0x67 'Shi ', # 0x68 @@ -109,7 +109,7 @@ data = ( 'Jia ', # 0x6b 'Kel ', # 0x6c 'Shi ', # 0x6d -'[?] ', # 0x6e +None, # 0x6e 'Ol ', # 0x6f 'Mai ', # 0x70 'Luan ', # 0x71 @@ -124,7 +124,7 @@ data = ( 'Sol ', # 0x7a 'El ', # 0x7b 'Cwul ', # 0x7c -'[?] ', # 0x7d +None, # 0x7d 'Gan ', # 0x7e 'Chi ', # 0x7f 'Gui ', # 0x80 @@ -169,7 +169,7 @@ data = ( 'Chan ', # 0xa7 'Heng ', # 0xa8 'Mu ', # 0xa9 -'[?] ', # 0xaa +None, # 0xaa 'Xiang ', # 0xab 'Jing ', # 0xac 'Ting ', # 0xad @@ -232,7 +232,7 @@ data = ( 'Chao ', # 0xe6 'Chang ', # 0xe7 'Sa ', # 0xe8 -'[?] ', # 0xe9 +None, # 0xe9 'Yi ', # 0xea 'Mu ', # 0xeb 'Men ', # 0xec diff --git a/libs/common/unidecode/x04f.py b/libs/common/unidecode/x04f.py index 98c22910..3e372864 100644 --- a/libs/common/unidecode/x04f.py +++ b/libs/common/unidecode/x04f.py @@ -43,7 +43,7 @@ data = ( 'Xin ', # 0x29 'Wei ', # 0x2a 'Zhu ', # 0x2b -'[?] ', # 0x2c +None, # 0x2c 'Xuan ', # 0x2d 'Nu ', # 0x2e 'Bo ', # 0x2f @@ -101,9 +101,9 @@ data = ( 'Yong ', # 0x63 'Wa ', # 0x64 'Qian ', # 0x65 -'[?] ', # 0x66 +None, # 0x66 'Ka ', # 0x67 -'[?] ', # 0x68 +None, # 0x68 'Pei ', # 0x69 'Huai ', # 0x6a 'He ', # 0x6b @@ -230,12 +230,12 @@ data = ( 'Ti ', # 0xe4 'Che ', # 0xe5 'Chou ', # 0xe6 -'[?] ', # 0xe7 +None, # 0xe7 'Yan ', # 0xe8 'Lia ', # 0xe9 'Li ', # 0xea 'Lai ', # 0xeb -'[?] ', # 0xec +None, # 0xec 'Jian ', # 0xed 'Xiu ', # 0xee 'Fu ', # 0xef diff --git a/libs/common/unidecode/x050.py b/libs/common/unidecode/x050.py index 184b87fd..dbadd290 100644 --- a/libs/common/unidecode/x050.py +++ b/libs/common/unidecode/x050.py @@ -62,7 +62,7 @@ data = ( 'Zhi ', # 0x3c 'Sha ', # 0x3d 'Qing ', # 0x3e -'[?] ', # 0x3f +None, # 0x3f 'Ying ', # 0x40 'Cheng ', # 0x41 'Jian ', # 0x42 @@ -165,7 +165,7 @@ data = ( 'Dai ', # 0xa3 'Zai ', # 0xa4 'Tang ', # 0xa5 -'[?] ', # 0xa6 +None, # 0xa6 'Bin ', # 0xa7 'Chu ', # 0xa8 'Nuo ', # 0xa9 @@ -241,7 +241,7 @@ data = ( 'Lin ', # 0xef 'Bo ', # 0xf0 'Gu ', # 0xf1 -'[?] ', # 0xf2 +None, # 0xf2 'Su ', # 0xf3 'Xian ', # 0xf4 'Jiang ', # 0xf5 diff --git a/libs/common/unidecode/x051.py b/libs/common/unidecode/x051.py index c1928354..8dae426c 100644 --- a/libs/common/unidecode/x051.py +++ b/libs/common/unidecode/x051.py @@ -14,7 +14,7 @@ data = ( 'Jiao ', # 0x0c 'Sha ', # 0x0d 'Zai ', # 0x0e -'[?] ', # 0x0f +None, # 0x0f 'Bin ', # 0x10 'An ', # 0x11 'Ru ', # 0x12 @@ -110,7 +110,7 @@ data = ( 'Gong ', # 0x6c 'Liu ', # 0x6d 'Xi ', # 0x6e -'[?] ', # 0x6f +None, # 0x6f 'Lan ', # 0x70 'Gong ', # 0x71 'Tian ', # 0x72 diff --git a/libs/common/unidecode/x053.py b/libs/common/unidecode/x053.py index fa08b6ef..ab313b3c 100644 --- a/libs/common/unidecode/x053.py +++ b/libs/common/unidecode/x053.py @@ -6,7 +6,7 @@ data = ( 'Gai ', # 0x04 'Bao ', # 0x05 'Cong ', # 0x06 -'[?] ', # 0x07 +None, # 0x07 'Xiong ', # 0x08 'Peng ', # 0x09 'Ju ', # 0x0a @@ -128,7 +128,7 @@ data = ( 'E ', # 0x7e 'Qing ', # 0x7f 'Xi ', # 0x80 -'[?] ', # 0x81 +None, # 0x81 'Han ', # 0x82 'Zhan ', # 0x83 'E ', # 0x84 @@ -144,7 +144,7 @@ data = ( 'Zhi ', # 0x8e 'Zha ', # 0x8f 'Pang ', # 0x90 -'[?] ', # 0x91 +None, # 0x91 'He ', # 0x92 'Ya ', # 0x93 'Zhi ', # 0x94 @@ -253,6 +253,6 @@ data = ( 'Le ', # 0xfb 'Diao ', # 0xfc 'Ji ', # 0xfd -'[?] ', # 0xfe +None, # 0xfe 'Hong ', # 0xff ) diff --git a/libs/common/unidecode/x054.py b/libs/common/unidecode/x054.py index c014e0fe..b12b6242 100644 --- a/libs/common/unidecode/x054.py +++ b/libs/common/unidecode/x054.py @@ -89,7 +89,7 @@ data = ( 'Bai ', # 0x57 'Yuan ', # 0x58 'Kuai ', # 0x59 -'[?] ', # 0x5a +None, # 0x5a 'Qiang ', # 0x5b 'Wu ', # 0x5c 'E ', # 0x5d @@ -213,12 +213,12 @@ data = ( 'Xiao ', # 0xd3 'Bi ', # 0xd4 'Yue ', # 0xd5 -'[?] ', # 0xd6 +None, # 0xd6 'Hua ', # 0xd7 'Sasou ', # 0xd8 'Kuai ', # 0xd9 'Duo ', # 0xda -'[?] ', # 0xdb +None, # 0xdb 'Ji ', # 0xdc 'Nong ', # 0xdd 'Mou ', # 0xde diff --git a/libs/common/unidecode/x055.py b/libs/common/unidecode/x055.py index 26aea747..71be2089 100644 --- a/libs/common/unidecode/x055.py +++ b/libs/common/unidecode/x055.py @@ -120,7 +120,7 @@ data = ( 'Ding ', # 0x76 'Lang ', # 0x77 'Xiao ', # 0x78 -'[?] ', # 0x79 +None, # 0x79 'Tang ', # 0x7a 'Chi ', # 0x7b 'Ti ', # 0x7c @@ -243,7 +243,7 @@ data = ( 'Na ', # 0xf1 'Dia ', # 0xf2 'Ai ', # 0xf3 -'[?] ', # 0xf4 +None, # 0xf4 'Tong ', # 0xf5 'Bi ', # 0xf6 'Ao ', # 0xf7 diff --git a/libs/common/unidecode/x056.py b/libs/common/unidecode/x056.py index 30b7fa54..341e046f 100644 --- a/libs/common/unidecode/x056.py +++ b/libs/common/unidecode/x056.py @@ -144,8 +144,8 @@ data = ( 'Hao ', # 0x8e 'Ti ', # 0x8f 'Chang ', # 0x90 -'[?] ', # 0x91 -'[?] ', # 0x92 +None, # 0x91 +None, # 0x92 'Ca ', # 0x93 'Ti ', # 0x94 'Lu ', # 0x95 @@ -212,8 +212,8 @@ data = ( 'Lan ', # 0xd2 'Nie ', # 0xd3 'Nang ', # 0xd4 -'[?] ', # 0xd5 -'[?] ', # 0xd6 +None, # 0xd5 +None, # 0xd6 'Wei ', # 0xd7 'Hui ', # 0xd8 'Yin ', # 0xd9 diff --git a/libs/common/unidecode/x057.py b/libs/common/unidecode/x057.py index 9392fb86..fd6a5bfa 100644 --- a/libs/common/unidecode/x057.py +++ b/libs/common/unidecode/x057.py @@ -137,7 +137,7 @@ data = ( 'Ao ', # 0x87 'Tay ', # 0x88 'Pao ', # 0x89 -'[?] ', # 0x8a +None, # 0x8a 'Xing ', # 0x8b 'Dong ', # 0x8c 'Ji ', # 0x8d @@ -174,7 +174,7 @@ data = ( 'Hong ', # 0xac 'Wu ', # 0xad 'Kua ', # 0xae -'[?] ', # 0xaf +None, # 0xaf 'Tao ', # 0xb0 'Dang ', # 0xb1 'Kai ', # 0xb2 diff --git a/libs/common/unidecode/x058.py b/libs/common/unidecode/x058.py index 88057182..f23c1a6e 100644 --- a/libs/common/unidecode/x058.py +++ b/libs/common/unidecode/x058.py @@ -14,12 +14,12 @@ data = ( 'Gu ', # 0x0c 'Tu ', # 0x0d 'Leng ', # 0x0e -'[?] ', # 0x0f +None, # 0x0f 'Ya ', # 0x10 'Qian ', # 0x11 -'[?] ', # 0x12 +None, # 0x12 'An ', # 0x13 -'[?] ', # 0x14 +None, # 0x14 'Duo ', # 0x15 'Nao ', # 0x16 'Tu ', # 0x17 @@ -69,7 +69,7 @@ data = ( 'Huang ', # 0x43 'Leng ', # 0x44 'Duan ', # 0x45 -'[?] ', # 0x46 +None, # 0x46 'Xuan ', # 0x47 'Ji ', # 0x48 'Ji ', # 0x49 @@ -154,7 +154,7 @@ data = ( 'Qi ', # 0x98 'Qiang ', # 0x99 'Liang ', # 0x9a -'[?] ', # 0x9b +None, # 0x9b 'Zhui ', # 0x9c 'Qiao ', # 0x9d 'Zeng ', # 0x9e @@ -233,10 +233,10 @@ data = ( 'Yan ', # 0xe7 'Lei ', # 0xe8 'Ba ', # 0xe9 -'[?] ', # 0xea +None, # 0xea 'Shi ', # 0xeb 'Ren ', # 0xec -'[?] ', # 0xed +None, # 0xed 'Zhuang ', # 0xee 'Zhuang ', # 0xef 'Sheng ', # 0xf0 diff --git a/libs/common/unidecode/x059.py b/libs/common/unidecode/x059.py index 45966661..f850b0f4 100644 --- a/libs/common/unidecode/x059.py +++ b/libs/common/unidecode/x059.py @@ -16,7 +16,7 @@ data = ( 'Zuo ', # 0x0e 'Xia ', # 0x0f 'Xiong ', # 0x10 -'[?] ', # 0x11 +None, # 0x11 'Nao ', # 0x12 'Xia ', # 0x13 'Kui ', # 0x14 @@ -76,7 +76,7 @@ data = ( 'Xie ', # 0x4a 'Fen ', # 0x4b 'Dian ', # 0x4c -'[?] ', # 0x4d +None, # 0x4d 'Kui ', # 0x4e 'Zou ', # 0x4f 'Huan ', # 0x50 diff --git a/libs/common/unidecode/x05a.py b/libs/common/unidecode/x05a.py index be56e652..8e938646 100644 --- a/libs/common/unidecode/x05a.py +++ b/libs/common/unidecode/x05a.py @@ -50,7 +50,7 @@ data = ( 'Si ', # 0x30 'Yu ', # 0x31 'Wa ', # 0x32 -'[?] ', # 0x33 +None, # 0x33 'Xian ', # 0x34 'Ju ', # 0x35 'Qu ', # 0x36 @@ -170,7 +170,7 @@ data = ( 'Jiu ', # 0xa8 'Hu ', # 0xa9 'Ao ', # 0xaa -'[?] ', # 0xab +None, # 0xab 'Bou ', # 0xac 'Xu ', # 0xad 'Tou ', # 0xae diff --git a/libs/common/unidecode/x05b.py b/libs/common/unidecode/x05b.py index 1b167b3e..3d2ec495 100644 --- a/libs/common/unidecode/x05b.py +++ b/libs/common/unidecode/x05b.py @@ -102,7 +102,7 @@ data = ( 'Gu ', # 0x64 'Nu ', # 0x65 'Xue ', # 0x66 -'[?] ', # 0x67 +None, # 0x67 'Zhuan ', # 0x68 'Hai ', # 0x69 'Luan ', # 0x6a diff --git a/libs/common/unidecode/x05c.py b/libs/common/unidecode/x05c.py index 62957e87..bcb2df6d 100644 --- a/libs/common/unidecode/x05c.py +++ b/libs/common/unidecode/x05c.py @@ -32,7 +32,7 @@ data = ( 'Liao ', # 0x1e 'Xian ', # 0x1f 'Xian ', # 0x20 -'[?] ', # 0x21 +None, # 0x21 'Wang ', # 0x22 'Wang ', # 0x23 'You ', # 0x24 @@ -86,7 +86,7 @@ data = ( 'Ni ', # 0x54 'Zhan ', # 0x55 'Xi ', # 0x56 -'[?] ', # 0x57 +None, # 0x57 'Man ', # 0x58 'E ', # 0x59 'Lou ', # 0x5a @@ -113,12 +113,12 @@ data = ( 'Tun ', # 0x6f 'Ni ', # 0x70 'Shan ', # 0x71 -'[?] ', # 0x72 +None, # 0x72 'Xian ', # 0x73 'Li ', # 0x74 'Xue ', # 0x75 'Nata ', # 0x76 -'[?] ', # 0x77 +None, # 0x77 'Long ', # 0x78 'Yi ', # 0x79 'Qi ', # 0x7a @@ -130,7 +130,7 @@ data = ( 'Chu ', # 0x80 'Sui ', # 0x81 'Qi ', # 0x82 -'[?] ', # 0x83 +None, # 0x83 'Yue ', # 0x84 'Ban ', # 0x85 'Yao ', # 0x86 diff --git a/libs/common/unidecode/x05d.py b/libs/common/unidecode/x05d.py index c85032aa..6f43044c 100644 --- a/libs/common/unidecode/x05d.py +++ b/libs/common/unidecode/x05d.py @@ -47,7 +47,7 @@ data = ( 'Zhan ', # 0x2d 'Gu ', # 0x2e 'Yin ', # 0x2f -'[?] ', # 0x30 +None, # 0x30 'Ze ', # 0x31 'Huang ', # 0x32 'Yu ', # 0x33 @@ -116,7 +116,7 @@ data = ( 'Nie ', # 0x72 'Cuo ', # 0x73 'Ji ', # 0x74 -'[?] ', # 0x75 +None, # 0x75 'Tao ', # 0x76 'Song ', # 0x77 'Zong ', # 0x78 @@ -181,7 +181,7 @@ data = ( 'Di ', # 0xb3 'Ao ', # 0xb4 'Zui ', # 0xb5 -'[?] ', # 0xb6 +None, # 0xb6 'Ni ', # 0xb7 'Rong ', # 0xb8 'Dao ', # 0xb9 @@ -190,7 +190,7 @@ data = ( 'Yu ', # 0xbc 'Yue ', # 0xbd 'Yin ', # 0xbe -'[?] ', # 0xbf +None, # 0xbf 'Jie ', # 0xc0 'Li ', # 0xc1 'Sui ', # 0xc2 @@ -212,7 +212,7 @@ data = ( 'Luan ', # 0xd2 'Dian ', # 0xd3 'Dian ', # 0xd4 -'[?] ', # 0xd5 +None, # 0xd5 'Yan ', # 0xd6 'Yan ', # 0xd7 'Yan ', # 0xd8 diff --git a/libs/common/unidecode/x05e.py b/libs/common/unidecode/x05e.py index af879280..4f587404 100644 --- a/libs/common/unidecode/x05e.py +++ b/libs/common/unidecode/x05e.py @@ -100,7 +100,7 @@ data = ( 'Chuang ', # 0x62 'Bi ', # 0x63 'Hei ', # 0x64 -'[?] ', # 0x65 +None, # 0x65 'Mi ', # 0x66 'Qiao ', # 0x67 'Chan ', # 0x68 @@ -145,7 +145,7 @@ data = ( 'Xu ', # 0x8f 'Lu ', # 0x90 'Wu ', # 0x91 -'[?] ', # 0x92 +None, # 0x92 'Ku ', # 0x93 'Ying ', # 0x94 'Di ', # 0x95 @@ -236,7 +236,7 @@ data = ( 'Lin ', # 0xea 'Liao ', # 0xeb 'Lu ', # 0xec -'[?] ', # 0xed +None, # 0xed 'Ying ', # 0xee 'Xian ', # 0xef 'Ting ', # 0xf0 diff --git a/libs/common/unidecode/x05f.py b/libs/common/unidecode/x05f.py index 032eab89..547d75db 100644 --- a/libs/common/unidecode/x05f.py +++ b/libs/common/unidecode/x05f.py @@ -147,13 +147,13 @@ data = ( 'Jing ', # 0x91 'Tu ', # 0x92 'Cong ', # 0x93 -'[?] ', # 0x94 +None, # 0x94 'Lai ', # 0x95 'Cong ', # 0x96 'De ', # 0x97 'Pai ', # 0x98 'Xi ', # 0x99 -'[?] ', # 0x9a +None, # 0x9a 'Qi ', # 0x9b 'Chang ', # 0x9c 'Zhi ', # 0x9d diff --git a/libs/common/unidecode/x060.py b/libs/common/unidecode/x060.py index ad3728f8..5487c560 100644 --- a/libs/common/unidecode/x060.py +++ b/libs/common/unidecode/x060.py @@ -60,7 +60,7 @@ data = ( 'Koraeru ', # 0x3a 'Zong ', # 0x3b 'Dui ', # 0x3c -'[?] ', # 0x3d +None, # 0x3d 'Ki ', # 0x3e 'Yi ', # 0x3f 'Chi ', # 0x40 diff --git a/libs/common/unidecode/x061.py b/libs/common/unidecode/x061.py index 6e8ab80d..39a3b4bb 100644 --- a/libs/common/unidecode/x061.py +++ b/libs/common/unidecode/x061.py @@ -36,7 +36,7 @@ data = ( 'Sai ', # 0x22 'Leng ', # 0x23 'Fen ', # 0x24 -'[?] ', # 0x25 +None, # 0x25 'Kui ', # 0x26 'Kui ', # 0x27 'Que ', # 0x28 @@ -79,7 +79,7 @@ data = ( 'Yun ', # 0x4d 'Shen ', # 0x4e 'Ming ', # 0x4f -'[?] ', # 0x50 +None, # 0x50 'She ', # 0x51 'Cong ', # 0x52 'Piao ', # 0x53 @@ -242,7 +242,7 @@ data = ( 'Liu ', # 0xf0 'Mie ', # 0xf1 'Cheng ', # 0xf2 -'[?] ', # 0xf3 +None, # 0xf3 'Chan ', # 0xf4 'Meng ', # 0xf5 'Lan ', # 0xf6 diff --git a/libs/common/unidecode/x062.py b/libs/common/unidecode/x062.py index 97979203..ffa55aef 100644 --- a/libs/common/unidecode/x062.py +++ b/libs/common/unidecode/x062.py @@ -162,7 +162,7 @@ data = ( 'Kou ', # 0xa0 'Lun ', # 0xa1 'Qiang ', # 0xa2 -'[?] ', # 0xa3 +None, # 0xa3 'Hu ', # 0xa4 'Bao ', # 0xa5 'Bing ', # 0xa6 @@ -227,7 +227,7 @@ data = ( 'Kuo ', # 0xe1 'Long ', # 0xe2 'Jian ', # 0xe3 -'[?] ', # 0xe4 +None, # 0xe4 'Yong ', # 0xe5 'Lan ', # 0xe6 'Ning ', # 0xe7 diff --git a/libs/common/unidecode/x063.py b/libs/common/unidecode/x063.py index 896cea25..3906e52a 100644 --- a/libs/common/unidecode/x063.py +++ b/libs/common/unidecode/x063.py @@ -99,7 +99,7 @@ data = ( 'Jian ', # 0x61 'Huan ', # 0x62 'Dao ', # 0x63 -'[?] ', # 0x64 +None, # 0x64 'Wan ', # 0x65 'Qin ', # 0x66 'Peng ', # 0x67 @@ -181,7 +181,7 @@ data = ( 'Lu ', # 0xb3 'Guo ', # 0xb4 'Haba ', # 0xb5 -'[?] ', # 0xb6 +None, # 0xb6 'Zhi ', # 0xb7 'Dan ', # 0xb8 'Mang ', # 0xb9 @@ -250,8 +250,8 @@ data = ( 'Zha ', # 0xf8 'Bei ', # 0xf9 'Yao ', # 0xfa -'[?] ', # 0xfb -'[?] ', # 0xfc +None, # 0xfb +None, # 0xfc 'Lan ', # 0xfd 'Wen ', # 0xfe 'Qin ', # 0xff diff --git a/libs/common/unidecode/x064.py b/libs/common/unidecode/x064.py index dc1514b6..d36ffd89 100644 --- a/libs/common/unidecode/x064.py +++ b/libs/common/unidecode/x064.py @@ -181,7 +181,7 @@ data = ( 'Qin ', # 0xb3 'Dun ', # 0xb4 'Nian ', # 0xb5 -'[?] ', # 0xb6 +None, # 0xb6 'Xie ', # 0xb7 'Lu ', # 0xb8 'Jiao ', # 0xb9 @@ -219,7 +219,7 @@ data = ( 'Ao ', # 0xd9 'Ju ', # 0xda 'Ye ', # 0xdb -'[?] ', # 0xdc +None, # 0xdc 'Mang ', # 0xdd 'Sou ', # 0xde 'Mi ', # 0xdf diff --git a/libs/common/unidecode/x065.py b/libs/common/unidecode/x065.py index ede51764..5390e96e 100644 --- a/libs/common/unidecode/x065.py +++ b/libs/common/unidecode/x065.py @@ -25,7 +25,7 @@ data = ( 'Mei ', # 0x17 'Rang ', # 0x18 'Chan ', # 0x19 -'[?] ', # 0x1a +None, # 0x1a 'Cuan ', # 0x1b 'Xi ', # 0x1c 'She ', # 0x1d @@ -142,7 +142,7 @@ data = ( 'Bin ', # 0x8c 'Jue ', # 0x8d 'Zhai ', # 0x8e -'[?] ', # 0x8f +None, # 0x8f 'Fei ', # 0x90 'Ban ', # 0x91 'Ban ', # 0x92 diff --git a/libs/common/unidecode/x066.py b/libs/common/unidecode/x066.py index 01898d55..08549f49 100644 --- a/libs/common/unidecode/x066.py +++ b/libs/common/unidecode/x066.py @@ -225,7 +225,7 @@ data = ( 'Chen ', # 0xdf 'Kuang ', # 0xe0 'Die ', # 0xe1 -'[?] ', # 0xe2 +None, # 0xe2 'Yan ', # 0xe3 'Huo ', # 0xe4 'Lu ', # 0xe5 diff --git a/libs/common/unidecode/x067.py b/libs/common/unidecode/x067.py index 2e863ae0..51cab591 100644 --- a/libs/common/unidecode/x067.py +++ b/libs/common/unidecode/x067.py @@ -16,7 +16,7 @@ data = ( 'Ling ', # 0x0e 'Fei ', # 0x0f 'Qu ', # 0x10 -'[?] ', # 0x11 +None, # 0x11 'Nu ', # 0x12 'Tiao ', # 0x13 'Shuo ', # 0x14 @@ -36,7 +36,7 @@ data = ( 'Wang ', # 0x22 'Tong ', # 0x23 'Lang ', # 0x24 -'[?] ', # 0x25 +None, # 0x25 'Meng ', # 0x26 'Long ', # 0x27 'Mu ', # 0x28 @@ -47,7 +47,7 @@ data = ( 'Zha ', # 0x2d 'Zhu ', # 0x2e 'Zhu ', # 0x2f -'[?] ', # 0x30 +None, # 0x30 'Zhu ', # 0x31 'Ren ', # 0x32 'Ba ', # 0x33 @@ -163,7 +163,7 @@ data = ( 'Dou ', # 0xa1 'Shu ', # 0xa2 'Zao ', # 0xa3 -'[?] ', # 0xa4 +None, # 0xa4 'Li ', # 0xa5 'Haze ', # 0xa6 'Jian ', # 0xa7 diff --git a/libs/common/unidecode/x068.py b/libs/common/unidecode/x068.py index c562311c..822ec5f9 100644 --- a/libs/common/unidecode/x068.py +++ b/libs/common/unidecode/x068.py @@ -5,7 +5,7 @@ data = ( 'Hoy ', # 0x03 'Rong ', # 0x04 'Zha ', # 0x05 -'[?] ', # 0x06 +None, # 0x06 'Biao ', # 0x07 'Zhan ', # 0x08 'Jie ', # 0x09 @@ -93,7 +93,7 @@ data = ( 'Kasei ', # 0x5b 'Ying ', # 0x5c 'Masu ', # 0x5d -'[?] ', # 0x5e +None, # 0x5e 'Zhan ', # 0x5f 'Ya ', # 0x60 'Nao ', # 0x61 diff --git a/libs/common/unidecode/x069.py b/libs/common/unidecode/x069.py index 7fa8c7de..ed13a6ad 100644 --- a/libs/common/unidecode/x069.py +++ b/libs/common/unidecode/x069.py @@ -40,10 +40,10 @@ data = ( 'Ken ', # 0x26 'Myeng ', # 0x27 'Tafu ', # 0x28 -'[?] ', # 0x29 +None, # 0x29 'Peng ', # 0x2a 'Zhan ', # 0x2b -'[?] ', # 0x2c +None, # 0x2c 'Tuo ', # 0x2d 'Sen ', # 0x2e 'Duo ', # 0x2f @@ -138,7 +138,7 @@ data = ( 'Lu ', # 0x88 'Ju ', # 0x89 'Sakaki ', # 0x8a -'[?] ', # 0x8b +None, # 0x8b 'Pi ', # 0x8c 'Xie ', # 0x8d 'Jia ', # 0x8e @@ -224,7 +224,7 @@ data = ( 'Ori ', # 0xde 'Bin ', # 0xdf 'Zhu ', # 0xe0 -'[?] ', # 0xe1 +None, # 0xe1 'Xi ', # 0xe2 'Qi ', # 0xe3 'Lian ', # 0xe4 diff --git a/libs/common/unidecode/x06a.py b/libs/common/unidecode/x06a.py index 12fcabd5..5f5abdf5 100644 --- a/libs/common/unidecode/x06a.py +++ b/libs/common/unidecode/x06a.py @@ -44,7 +44,7 @@ data = ( 'Heng ', # 0x2a 'Jian ', # 0x2b 'Cong ', # 0x2c -'[?] ', # 0x2d +None, # 0x2d 'Hokuso ', # 0x2e 'Qiang ', # 0x2f 'Tara ', # 0x30 @@ -121,8 +121,8 @@ data = ( 'Dou ', # 0x77 'Shou ', # 0x78 'Lu ', # 0x79 -'[?] ', # 0x7a -'[?] ', # 0x7b +None, # 0x7a +None, # 0x7b 'Yuan ', # 0x7c 'Ta ', # 0x7d 'Shu ', # 0x7e @@ -201,7 +201,7 @@ data = ( 'Po ', # 0xc7 'Deng ', # 0xc8 'Chu ', # 0xc9 -'[?] ', # 0xca +None, # 0xca 'Mian ', # 0xcb 'You ', # 0xcc 'Zhi ', # 0xcd @@ -229,7 +229,7 @@ data = ( 'Lian ', # 0xe3 'Tamo ', # 0xe4 'Chu ', # 0xe5 -'[?] ', # 0xe6 +None, # 0xe6 'Zhu ', # 0xe7 'Lu ', # 0xe8 'Yan ', # 0xe9 @@ -244,7 +244,7 @@ data = ( 'Yu ', # 0xf2 'Long ', # 0xf3 'Lai ', # 0xf4 -'[?] ', # 0xf5 +None, # 0xf5 'Xian ', # 0xf6 'Kwi ', # 0xf7 'Ju ', # 0xf8 diff --git a/libs/common/unidecode/x06b.py b/libs/common/unidecode/x06b.py index 56aa7c65..417056c5 100644 --- a/libs/common/unidecode/x06b.py +++ b/libs/common/unidecode/x06b.py @@ -12,7 +12,7 @@ data = ( 'Quan ', # 0x0a 'Qu ', # 0x0b 'Cang ', # 0x0c -'[?] ', # 0x0d +None, # 0x0d 'Yu ', # 0x0e 'Luo ', # 0x0f 'Li ', # 0x10 @@ -219,8 +219,8 @@ data = ( 'Bi ', # 0xd9 'Chan ', # 0xda 'Mao ', # 0xdb -'[?] ', # 0xdc -'[?] ', # 0xdd +None, # 0xdc +None, # 0xdd 'Pu ', # 0xde 'Mushiru ', # 0xdf 'Jia ', # 0xe0 @@ -245,7 +245,7 @@ data = ( 'Cui ', # 0xf3 'Bi ', # 0xf4 'San ', # 0xf5 -'[?] ', # 0xf6 +None, # 0xf6 'Mao ', # 0xf7 'Sui ', # 0xf8 'Yu ', # 0xf9 diff --git a/libs/common/unidecode/x06c.py b/libs/common/unidecode/x06c.py index a1534e74..db7650ab 100644 --- a/libs/common/unidecode/x06c.py +++ b/libs/common/unidecode/x06c.py @@ -29,7 +29,7 @@ data = ( 'Fen ', # 0x1b 'Ri ', # 0x1c 'Nei ', # 0x1d -'[?] ', # 0x1e +None, # 0x1e 'Fu ', # 0x1f 'Shen ', # 0x20 'Dong ', # 0x21 @@ -98,7 +98,7 @@ data = ( 'Chi ', # 0x60 'Wu ', # 0x61 'Tsuchi ', # 0x62 -'[?] ', # 0x63 +None, # 0x63 'Tang ', # 0x64 'Zhi ', # 0x65 'Chi ', # 0x66 @@ -248,7 +248,7 @@ data = ( 'Xue ', # 0xf6 'Long ', # 0xf7 'Lu ', # 0xf8 -'[?] ', # 0xf9 +None, # 0xf9 'Bo ', # 0xfa 'Xie ', # 0xfb 'Po ', # 0xfc diff --git a/libs/common/unidecode/x06d.py b/libs/common/unidecode/x06d.py index a9113465..da29aabc 100644 --- a/libs/common/unidecode/x06d.py +++ b/libs/common/unidecode/x06d.py @@ -27,7 +27,7 @@ data = ( 'Zhu ', # 0x19 'Jiang ', # 0x1a 'Luo ', # 0x1b -'[?] ', # 0x1c +None, # 0x1c 'An ', # 0x1d 'Dong ', # 0x1e 'Yi ', # 0x1f @@ -164,7 +164,7 @@ data = ( 'Yun ', # 0xa2 'Huan ', # 0xa3 'Di ', # 0xa4 -'[?] ', # 0xa5 +None, # 0xa5 'Run ', # 0xa6 'Jian ', # 0xa7 'Zhang ', # 0xa8 diff --git a/libs/common/unidecode/x06e.py b/libs/common/unidecode/x06e.py index d4698fd4..e12e6598 100644 --- a/libs/common/unidecode/x06e.py +++ b/libs/common/unidecode/x06e.py @@ -14,7 +14,7 @@ data = ( 'Lu ', # 0x0c 'Zi ', # 0x0d 'Du ', # 0x0e -'[?] ', # 0x0f +None, # 0x0f 'Jian ', # 0x10 'Min ', # 0x11 'Pi ', # 0x12 @@ -131,14 +131,14 @@ data = ( 'Ying ', # 0x81 'Ratsu ', # 0x82 'Kui ', # 0x83 -'[?] ', # 0x84 +None, # 0x84 'Jian ', # 0x85 'Xu ', # 0x86 'Lu ', # 0x87 'Gui ', # 0x88 'Gai ', # 0x89 -'[?] ', # 0x8a -'[?] ', # 0x8b +None, # 0x8a +None, # 0x8b 'Po ', # 0x8c 'Jin ', # 0x8d 'Gui ', # 0x8e @@ -230,7 +230,7 @@ data = ( 'Lu ', # 0xe4 'Lan ', # 0xe5 'Luan ', # 0xe6 -'[?] ', # 0xe7 +None, # 0xe7 'Bin ', # 0xe8 'Tan ', # 0xe9 'Yu ', # 0xea diff --git a/libs/common/unidecode/x06f.py b/libs/common/unidecode/x06f.py index 36bf2a26..822a628a 100644 --- a/libs/common/unidecode/x06f.py +++ b/libs/common/unidecode/x06f.py @@ -71,8 +71,8 @@ data = ( 'Guan ', # 0x45 'Ying ', # 0x46 'Xiao ', # 0x47 -'[?] ', # 0x48 -'[?] ', # 0x49 +None, # 0x48 +None, # 0x49 'Xu ', # 0x4a 'Lian ', # 0x4b 'Zhi ', # 0x4c @@ -154,9 +154,9 @@ data = ( 'Shan ', # 0x98 'Xi ', # 0x99 'Oki ', # 0x9a -'[?] ', # 0x9b +None, # 0x9b 'Lan ', # 0x9c -'[?] ', # 0x9d +None, # 0x9d 'Yu ', # 0x9e 'Lin ', # 0x9f 'Min ', # 0xa0 @@ -206,7 +206,7 @@ data = ( 'Ta ', # 0xcc 'Song ', # 0xcd 'Ding ', # 0xce -'[?] ', # 0xcf +None, # 0xcf 'Zhu ', # 0xd0 'Lai ', # 0xd1 'Bin ', # 0xd2 @@ -247,7 +247,7 @@ data = ( 'Hama ', # 0xf5 'Kuo ', # 0xf6 'Fei ', # 0xf7 -'[?] ', # 0xf8 +None, # 0xf8 'Boku ', # 0xf9 'Jian ', # 0xfa 'Wei ', # 0xfb diff --git a/libs/common/unidecode/x070.py b/libs/common/unidecode/x070.py index b12567ff..4330f273 100644 --- a/libs/common/unidecode/x070.py +++ b/libs/common/unidecode/x070.py @@ -44,8 +44,8 @@ data = ( 'Fan ', # 0x2a 'Hu ', # 0x2b 'Lai ', # 0x2c -'[?] ', # 0x2d -'[?] ', # 0x2e +None, # 0x2d +None, # 0x2e 'Ying ', # 0x2f 'Mi ', # 0x30 'Ji ', # 0x31 @@ -91,7 +91,7 @@ data = ( 'Dang ', # 0x59 'Jiao ', # 0x5a 'Chan ', # 0x5b -'[?] ', # 0x5c +None, # 0x5c 'Hao ', # 0x5d 'Ba ', # 0x5e 'Zhu ', # 0x5f @@ -157,7 +157,7 @@ data = ( 'Guang ', # 0x9b 'Wei ', # 0x9c 'Qiang ', # 0x9d -'[?] ', # 0x9e +None, # 0x9e 'Da ', # 0x9f 'Xia ', # 0xa0 'Zheng ', # 0xa1 @@ -190,7 +190,7 @@ data = ( 'Lian ', # 0xbc 'Chi ', # 0xbd 'Huang ', # 0xbe -'[?] ', # 0xbf +None, # 0xbf 'Hu ', # 0xc0 'Shuo ', # 0xc1 'Lan ', # 0xc2 @@ -228,16 +228,16 @@ data = ( 'Zhe ', # 0xe2 'Hui ', # 0xe3 'Kao ', # 0xe4 -'[?] ', # 0xe5 +None, # 0xe5 'Fan ', # 0xe6 'Shao ', # 0xe7 'Ye ', # 0xe8 'Hui ', # 0xe9 -'[?] ', # 0xea +None, # 0xea 'Tang ', # 0xeb 'Jin ', # 0xec 'Re ', # 0xed -'[?] ', # 0xee +None, # 0xee 'Xi ', # 0xef 'Fu ', # 0xf0 'Jiong ', # 0xf1 diff --git a/libs/common/unidecode/x071.py b/libs/common/unidecode/x071.py index bad8f7ea..5d0b73f0 100644 --- a/libs/common/unidecode/x071.py +++ b/libs/common/unidecode/x071.py @@ -16,8 +16,8 @@ data = ( 'Xie ', # 0x0e 'Ji ', # 0x0f 'Wu ', # 0x10 -'[?] ', # 0x11 -'[?] ', # 0x12 +None, # 0x11 +None, # 0x12 'Han ', # 0x13 'Yan ', # 0x14 'Huan ', # 0x15 @@ -56,14 +56,14 @@ data = ( 'Ran ', # 0x36 'Pi ', # 0x37 'Gu ', # 0x38 -'[?] ', # 0x39 +None, # 0x39 'Sheng ', # 0x3a 'Chang ', # 0x3b 'Shao ', # 0x3c -'[?] ', # 0x3d -'[?] ', # 0x3e -'[?] ', # 0x3f -'[?] ', # 0x40 +None, # 0x3d +None, # 0x3e +None, # 0x3f +None, # 0x40 'Chen ', # 0x41 'He ', # 0x42 'Kui ', # 0x43 @@ -117,8 +117,8 @@ data = ( 'Hu ', # 0x73 'Yun ', # 0x74 'Xia ', # 0x75 -'[?] ', # 0x76 -'[?] ', # 0x77 +None, # 0x76 +None, # 0x77 'Bian ', # 0x78 'Gou ', # 0x79 'Tui ', # 0x7a @@ -149,7 +149,7 @@ data = ( 'Wen ', # 0x93 'Rong ', # 0x94 'Oozutsu ', # 0x95 -'[?] ', # 0x96 +None, # 0x96 'Qiang ', # 0x97 'Liu ', # 0x98 'Xi ', # 0x99 @@ -179,7 +179,7 @@ data = ( 'Re ', # 0xb1 'Jiong ', # 0xb2 'Man ', # 0xb3 -'[?] ', # 0xb4 +None, # 0xb4 'Shang ', # 0xb5 'Cuan ', # 0xb6 'Zeng ', # 0xb7 @@ -220,8 +220,8 @@ data = ( 'Yi ', # 0xda 'Jing ', # 0xdb 'Men ', # 0xdc -'[?] ', # 0xdd -'[?] ', # 0xde +None, # 0xdd +None, # 0xde 'Ying ', # 0xdf 'Yu ', # 0xe0 'Yi ', # 0xe1 diff --git a/libs/common/unidecode/x072.py b/libs/common/unidecode/x072.py index c91c93cb..eb48d956 100644 --- a/libs/common/unidecode/x072.py +++ b/libs/common/unidecode/x072.py @@ -13,7 +13,7 @@ data = ( 'Xun ', # 0x0b 'Kuang ', # 0x0c 'Shuo ', # 0x0d -'[?] ', # 0x0e +None, # 0x0e 'Li ', # 0x0f 'Lu ', # 0x10 'Jue ', # 0x11 @@ -23,7 +23,7 @@ data = ( 'Xie ', # 0x15 'Long ', # 0x16 'Ye ', # 0x17 -'[?] ', # 0x18 +None, # 0x18 'Rang ', # 0x19 'Yue ', # 0x1a 'Lan ', # 0x1b @@ -31,13 +31,13 @@ data = ( 'Jue ', # 0x1d 'Tong ', # 0x1e 'Guan ', # 0x1f -'[?] ', # 0x20 +None, # 0x20 'Che ', # 0x21 'Mi ', # 0x22 'Tang ', # 0x23 'Lan ', # 0x24 'Zhu ', # 0x25 -'[?] ', # 0x26 +None, # 0x26 'Ling ', # 0x27 'Cuan ', # 0x28 'Yu ', # 0x29 @@ -50,7 +50,7 @@ data = ( 'Yuan ', # 0x30 'Ai ', # 0x31 'Wei ', # 0x32 -'[?] ', # 0x33 +None, # 0x33 'Jue ', # 0x34 'Jue ', # 0x35 'Fu ', # 0x36 @@ -86,7 +86,7 @@ data = ( 'Bo ', # 0x54 'Chuang ', # 0x55 'You ', # 0x56 -'[?] ', # 0x57 +None, # 0x57 'Du ', # 0x58 'Ya ', # 0x59 'Cheng ', # 0x5a @@ -157,7 +157,7 @@ data = ( 'Li ', # 0x9b 'Dun ', # 0x9c 'Tong ', # 0x9d -'[?] ', # 0x9e +None, # 0x9e 'Jiang ', # 0x9f 'Ikenie ', # 0xa0 'Li ', # 0xa1 diff --git a/libs/common/unidecode/x073.py b/libs/common/unidecode/x073.py index 4cf0176d..8e294413 100644 --- a/libs/common/unidecode/x073.py +++ b/libs/common/unidecode/x073.py @@ -70,7 +70,7 @@ data = ( 'Yu ', # 0x44 'Shi ', # 0x45 'Hao ', # 0x46 -'[?] ', # 0x47 +None, # 0x47 'Yi ', # 0x48 'Zhen ', # 0x49 'Chuang ', # 0x4a @@ -238,7 +238,7 @@ data = ( 'Xu ', # 0xec 'Ban ', # 0xed 'Pei ', # 0xee -'[?] ', # 0xef +None, # 0xef 'Dang ', # 0xf0 'Ei ', # 0xf1 'Hun ', # 0xf2 diff --git a/libs/common/unidecode/x074.py b/libs/common/unidecode/x074.py index 312fc646..c44a3a08 100644 --- a/libs/common/unidecode/x074.py +++ b/libs/common/unidecode/x074.py @@ -17,7 +17,7 @@ data = ( 'Lian ', # 0x0f 'Suo ', # 0x10 'Chiisai ', # 0x11 -'[?] ', # 0x12 +None, # 0x12 'Wan ', # 0x13 'Dian ', # 0x14 'Pin ', # 0x15 @@ -58,7 +58,7 @@ data = ( 'Zhuo ', # 0x38 'Qin ', # 0x39 'Fa ', # 0x3a -'[?] ', # 0x3b +None, # 0x3b 'Qiong ', # 0x3c 'Du ', # 0x3d 'Jie ', # 0x3e @@ -140,7 +140,7 @@ data = ( 'Man ', # 0x8a 'Zhang ', # 0x8b 'Yin ', # 0x8c -'[?] ', # 0x8d +None, # 0x8d 'Ying ', # 0x8e 'Zhi ', # 0x8f 'Lu ', # 0x90 @@ -163,7 +163,7 @@ data = ( 'Jin ', # 0xa1 'Liu ', # 0xa2 'Ji ', # 0xa3 -'[?] ', # 0xa4 +None, # 0xa4 'Jing ', # 0xa5 'Ai ', # 0xa6 'Bi ', # 0xa7 @@ -179,7 +179,7 @@ data = ( 'Se ', # 0xb1 'Sui ', # 0xb2 'Tian ', # 0xb3 -'[?] ', # 0xb4 +None, # 0xb4 'Yu ', # 0xb5 'Jin ', # 0xb6 'Lu ', # 0xb7 diff --git a/libs/common/unidecode/x076.py b/libs/common/unidecode/x076.py index fc8b1672..29667f62 100644 --- a/libs/common/unidecode/x076.py +++ b/libs/common/unidecode/x076.py @@ -101,7 +101,7 @@ data = ( 'Xian ', # 0x63 'Jie ', # 0x64 'Zheng ', # 0x65 -'[?] ', # 0x66 +None, # 0x66 'Li ', # 0x67 'Huo ', # 0x68 'Lai ', # 0x69 @@ -118,7 +118,7 @@ data = ( 'Luan ', # 0x74 'Luan ', # 0x75 'Bo ', # 0x76 -'[?] ', # 0x77 +None, # 0x77 'Gui ', # 0x78 'Po ', # 0x79 'Fa ', # 0x7a diff --git a/libs/common/unidecode/x077.py b/libs/common/unidecode/x077.py index 3ed6a36b..347afbdc 100644 --- a/libs/common/unidecode/x077.py +++ b/libs/common/unidecode/x077.py @@ -162,7 +162,7 @@ data = ( 'Cheng ', # 0xa0 'Ji ', # 0xa1 'Meng ', # 0xa2 -'[?] ', # 0xa3 +None, # 0xa3 'Run ', # 0xa4 'Pie ', # 0xa5 'Xi ', # 0xa6 diff --git a/libs/common/unidecode/x078.py b/libs/common/unidecode/x078.py index 23d677de..e07980e1 100644 --- a/libs/common/unidecode/x078.py +++ b/libs/common/unidecode/x078.py @@ -26,7 +26,7 @@ data = ( 'Dun ', # 0x18 'Pan ', # 0x19 'Yan ', # 0x1a -'[?] ', # 0x1b +None, # 0x1b 'Feng ', # 0x1c 'Fa ', # 0x1d 'Mo ', # 0x1e @@ -60,7 +60,7 @@ data = ( 'Li ', # 0x3a 'Long ', # 0x3b 'Tong ', # 0x3c -'[?] ', # 0x3d +None, # 0x3d 'Li ', # 0x3e 'Aragane ', # 0x3f 'Chu ', # 0x40 @@ -82,15 +82,15 @@ data = ( 'Tong ', # 0x50 'Peng ', # 0x51 'Xi ', # 0x52 -'[?] ', # 0x53 +None, # 0x53 'Hong ', # 0x54 'Shuo ', # 0x55 'Xia ', # 0x56 'Qiao ', # 0x57 -'[?] ', # 0x58 +None, # 0x58 'Wei ', # 0x59 'Qiao ', # 0x5a -'[?] ', # 0x5b +None, # 0x5b 'Keng ', # 0x5c 'Xiao ', # 0x5d 'Que ', # 0x5e @@ -114,7 +114,7 @@ data = ( 'Sha ', # 0x70 'Kun ', # 0x71 'Yu ', # 0x72 -'[?] ', # 0x73 +None, # 0x73 'Kaki ', # 0x74 'Lu ', # 0x75 'Chen ', # 0x76 @@ -182,7 +182,7 @@ data = ( 'Cha ', # 0xb4 'Seki ', # 0xb5 'Qi ', # 0xb6 -'[?] ', # 0xb7 +None, # 0xb7 'Feng ', # 0xb8 'Xuan ', # 0xb9 'Que ', # 0xba @@ -214,7 +214,7 @@ data = ( 'Zhe ', # 0xd4 'Ke ', # 0xd5 'La ', # 0xd6 -'[?] ', # 0xd7 +None, # 0xd7 'Qing ', # 0xd8 'Gun ', # 0xd9 'Zhuan ', # 0xda @@ -237,7 +237,7 @@ data = ( 'Zong ', # 0xeb 'Qing ', # 0xec 'Chuo ', # 0xed -'[?] ', # 0xee +None, # 0xee 'Ji ', # 0xef 'Shan ', # 0xf0 'Lao ', # 0xf1 diff --git a/libs/common/unidecode/x079.py b/libs/common/unidecode/x079.py index ed1c5143..130ed608 100644 --- a/libs/common/unidecode/x079.py +++ b/libs/common/unidecode/x079.py @@ -1,7 +1,7 @@ data = ( 'Tani ', # 0x00 'Jiao ', # 0x01 -'[?] ', # 0x02 +None, # 0x02 'Zhang ', # 0x03 'Qiao ', # 0x04 'Dun ', # 0x05 @@ -32,8 +32,8 @@ data = ( 'Meng ', # 0x1e 'Pao ', # 0x1f 'Ci ', # 0x20 -'[?] ', # 0x21 -'[?] ', # 0x22 +None, # 0x21 +None, # 0x22 'Mie ', # 0x23 'Ca ', # 0x24 'Xian ', # 0x25 @@ -76,7 +76,7 @@ data = ( 'Beng ', # 0x4a 'Dui ', # 0x4b 'Zhong ', # 0x4c -'[?] ', # 0x4d +None, # 0x4d 'Yi ', # 0x4e 'Shi ', # 0x4f 'You ', # 0x50 @@ -152,7 +152,7 @@ data = ( 'Mei ', # 0x96 'Si ', # 0x97 'Di ', # 0x98 -'[?] ', # 0x99 +None, # 0x99 'Zhuo ', # 0x9a 'Zhen ', # 0x9b 'Yong ', # 0x9c @@ -162,7 +162,7 @@ data = ( 'Si ', # 0xa0 'Ma ', # 0xa1 'Ta ', # 0xa2 -'[?] ', # 0xa3 +None, # 0xa3 'Xuan ', # 0xa4 'Qi ', # 0xa5 'Yu ', # 0xa6 diff --git a/libs/common/unidecode/x07a.py b/libs/common/unidecode/x07a.py index b6d512cd..856b1cc1 100644 --- a/libs/common/unidecode/x07a.py +++ b/libs/common/unidecode/x07a.py @@ -36,7 +36,7 @@ data = ( 'Yu ', # 0x22 'Su ', # 0x23 'Lue ', # 0x24 -'[?] ', # 0x25 +None, # 0x25 'Yi ', # 0x26 'Xi ', # 0x27 'Bian ', # 0x28 @@ -81,7 +81,7 @@ data = ( 'Wen ', # 0x4f 'Qiu ', # 0x50 'Se ', # 0x51 -'[?] ', # 0x52 +None, # 0x52 'Yi ', # 0x53 'Huang ', # 0x54 'Qie ', # 0x55 @@ -110,7 +110,7 @@ data = ( 'Gong ', # 0x6c 'Lu ', # 0x6d 'Biao ', # 0x6e -'[?] ', # 0x6f +None, # 0x6f 'Rang ', # 0x70 'Zhuo ', # 0x71 'Li ', # 0x72 @@ -166,7 +166,7 @@ data = ( 'Guan ', # 0xa4 'Kui ', # 0xa5 'Dou ', # 0xa6 -'[?] ', # 0xa7 +None, # 0xa7 'Yin ', # 0xa8 'Wo ', # 0xa9 'Wa ', # 0xaa @@ -188,7 +188,7 @@ data = ( 'Kui ', # 0xba 'Chuang ', # 0xbb 'Zhao ', # 0xbc -'[?] ', # 0xbd +None, # 0xbd 'Kuan ', # 0xbe 'Long ', # 0xbf 'Cheng ', # 0xc0 diff --git a/libs/common/unidecode/x07b.py b/libs/common/unidecode/x07b.py index c904395c..e3aa7a6b 100644 --- a/libs/common/unidecode/x07b.py +++ b/libs/common/unidecode/x07b.py @@ -207,7 +207,7 @@ data = ( 'Qiu ', # 0xcd 'Miao ', # 0xce 'Qian ', # 0xcf -'[?] ', # 0xd0 +None, # 0xd0 'Kui ', # 0xd1 'Sik ', # 0xd2 'Lou ', # 0xd3 diff --git a/libs/common/unidecode/x07c.py b/libs/common/unidecode/x07c.py index 3379947a..f4580504 100644 --- a/libs/common/unidecode/x07c.py +++ b/libs/common/unidecode/x07c.py @@ -46,7 +46,7 @@ data = ( 'Du ', # 0x2c 'Shi ', # 0x2d 'Zan ', # 0x2e -'[?] ', # 0x2f +None, # 0x2f 'Pai ', # 0x30 'Hata ', # 0x31 'Pai ', # 0x32 @@ -65,7 +65,7 @@ data = ( 'Bo ', # 0x3f 'Zhou ', # 0x40 'Lai ', # 0x41 -'[?] ', # 0x42 +None, # 0x42 'Lan ', # 0x43 'Kui ', # 0x44 'Yu ', # 0x45 @@ -77,7 +77,7 @@ data = ( 'Mi ', # 0x4b 'Chou ', # 0x4c 'Ji ', # 0x4d -'[?] ', # 0x4e +None, # 0x4e 'Hata ', # 0x4f 'Teng ', # 0x50 'Zhuan ', # 0x51 @@ -127,7 +127,7 @@ data = ( 'Zi ', # 0x7d 'Ni ', # 0x7e 'Cun ', # 0x7f -'[?] ', # 0x80 +None, # 0x80 'Qian ', # 0x81 'Kume ', # 0x82 'Bi ', # 0x83 @@ -139,7 +139,7 @@ data = ( 'Fen ', # 0x89 'Bi ', # 0x8a 'Cui ', # 0x8b -'[?] ', # 0x8c +None, # 0x8c 'Li ', # 0x8d 'Chi ', # 0x8e 'Nukamiso ', # 0x8f @@ -168,10 +168,10 @@ data = ( 'Lin ', # 0xa6 'Zhuang ', # 0xa7 'Bai ', # 0xa8 -'[?] ', # 0xa9 +None, # 0xa9 'Fen ', # 0xaa 'Ji ', # 0xab -'[?] ', # 0xac +None, # 0xac 'Sukumo ', # 0xad 'Liang ', # 0xae 'Xian ', # 0xaf @@ -235,7 +235,7 @@ data = ( 'Kuai ', # 0xe9 'Bo ', # 0xea 'Huan ', # 0xeb -'[?] ', # 0xec +None, # 0xec 'Zong ', # 0xed 'Xian ', # 0xee 'Nuo ', # 0xef diff --git a/libs/common/unidecode/x07e.py b/libs/common/unidecode/x07e.py index 131ef35c..e302f2a8 100644 --- a/libs/common/unidecode/x07e.py +++ b/libs/common/unidecode/x07e.py @@ -235,7 +235,7 @@ data = ( 'Ji ', # 0xe9 'Xu ', # 0xea 'Ling ', # 0xeb -'[?] ', # 0xec +None, # 0xec 'Xu ', # 0xed 'Qi ', # 0xee 'Fei ', # 0xef diff --git a/libs/common/unidecode/x07f.py b/libs/common/unidecode/x07f.py index 0a708d6d..994f4701 100644 --- a/libs/common/unidecode/x07f.py +++ b/libs/common/unidecode/x07f.py @@ -63,7 +63,7 @@ data = ( 'Bo ', # 0x3d 'Ping ', # 0x3e 'Hou ', # 0x3f -'[?] ', # 0x40 +None, # 0x40 'Gang ', # 0x41 'Ying ', # 0x42 'Ying ', # 0x43 @@ -85,7 +85,7 @@ data = ( 'Gang ', # 0x53 'Wang ', # 0x54 'Han ', # 0x55 -'[?] ', # 0x56 +None, # 0x56 'Luo ', # 0x57 'Fu ', # 0x58 'Mi ', # 0x59 @@ -245,7 +245,7 @@ data = ( 'Yi ', # 0xf3 'Lian ', # 0xf4 'Qu ', # 0xf5 -'[?] ', # 0xf6 +None, # 0xf6 'Lin ', # 0xf7 'Pen ', # 0xf8 'Qiao ', # 0xf9 diff --git a/libs/common/unidecode/x080.py b/libs/common/unidecode/x080.py index 11f324b4..145ab01a 100644 --- a/libs/common/unidecode/x080.py +++ b/libs/common/unidecode/x080.py @@ -1,7 +1,7 @@ data = ( 'Yao ', # 0x00 'Lao ', # 0x01 -'[?] ', # 0x02 +None, # 0x02 'Kao ', # 0x03 'Mao ', # 0x04 'Zhe ', # 0x05 @@ -64,7 +64,7 @@ data = ( 'Hong ', # 0x3e 'Geng ', # 0x3f 'Zhi ', # 0x40 -'[?] ', # 0x41 +None, # 0x41 'Nie ', # 0x42 'Dan ', # 0x43 'Zhen ', # 0x44 @@ -82,7 +82,7 @@ data = ( 'Ya ', # 0x50 'Die ', # 0x51 'Gua ', # 0x52 -'[?] ', # 0x53 +None, # 0x53 'Lian ', # 0x54 'Hao ', # 0x55 'Sheng ', # 0x56 @@ -98,7 +98,7 @@ data = ( 'Ping ', # 0x60 'Cong ', # 0x61 'Shikato ', # 0x62 -'[?] ', # 0x63 +None, # 0x63 'Ting ', # 0x64 'Yu ', # 0x65 'Cong ', # 0x66 diff --git a/libs/common/unidecode/x081.py b/libs/common/unidecode/x081.py index 01ca95d3..38817811 100644 --- a/libs/common/unidecode/x081.py +++ b/libs/common/unidecode/x081.py @@ -11,7 +11,7 @@ data = ( 'Mai ', # 0x09 'Ji ', # 0x0a 'Obiyaakasu ', # 0x0b -'[?] ', # 0x0c +None, # 0x0c 'Kuai ', # 0x0d 'Sa ', # 0x0e 'Zang ', # 0x0f diff --git a/libs/common/unidecode/x082.py b/libs/common/unidecode/x082.py index daea2e23..890f63ea 100644 --- a/libs/common/unidecode/x082.py +++ b/libs/common/unidecode/x082.py @@ -35,7 +35,7 @@ data = ( 'Gang ', # 0x21 'Shan ', # 0x22 'Yi ', # 0x23 -'[?] ', # 0x24 +None, # 0x24 'Pa ', # 0x25 'Tai ', # 0x26 'Fan ', # 0x27 @@ -62,7 +62,7 @@ data = ( 'Hong ', # 0x3c 'Pang ', # 0x3d 'Xi ', # 0x3e -'[?] ', # 0x3f +None, # 0x3f 'Fu ', # 0x40 'Zao ', # 0x41 'Feng ', # 0x42 @@ -71,7 +71,7 @@ data = ( 'Yu ', # 0x45 'Lang ', # 0x46 'Ting ', # 0x47 -'[?] ', # 0x48 +None, # 0x48 'Wei ', # 0x49 'Bo ', # 0x4a 'Meng ', # 0x4b @@ -83,7 +83,7 @@ data = ( 'Bian ', # 0x51 'Mao ', # 0x52 'Die ', # 0x53 -'[?] ', # 0x54 +None, # 0x54 'Bang ', # 0x55 'Cha ', # 0x56 'Yi ', # 0x57 diff --git a/libs/common/unidecode/x083.py b/libs/common/unidecode/x083.py index 672cd5d8..703a215c 100644 --- a/libs/common/unidecode/x083.py +++ b/libs/common/unidecode/x083.py @@ -15,9 +15,9 @@ data = ( 'Ji ', # 0x0d 'Jing ', # 0x0e 'Long ', # 0x0f -'[?] ', # 0x10 +None, # 0x10 'Niao ', # 0x11 -'[?] ', # 0x12 +None, # 0x12 'Xue ', # 0x13 'Ying ', # 0x14 'Qiong ', # 0x15 @@ -61,7 +61,7 @@ data = ( 'Mang ', # 0x3b 'Tong ', # 0x3c 'Zhong ', # 0x3d -'[?] ', # 0x3e +None, # 0x3e 'Zhu ', # 0x3f 'Xun ', # 0x40 'Huan ', # 0x41 @@ -97,7 +97,7 @@ data = ( 'Hui ', # 0x5f 'Qi ', # 0x60 'Dang ', # 0x61 -'[?] ', # 0x62 +None, # 0x62 'Rong ', # 0x63 'Hun ', # 0x64 'Ying ', # 0x65 diff --git a/libs/common/unidecode/x084.py b/libs/common/unidecode/x084.py index 571a3607..703936cb 100644 --- a/libs/common/unidecode/x084.py +++ b/libs/common/unidecode/x084.py @@ -29,12 +29,12 @@ data = ( 'Jiu ', # 0x1b 'Tie ', # 0x1c 'Luo ', # 0x1d -'[?] ', # 0x1e -'[?] ', # 0x1f +None, # 0x1e +None, # 0x1f 'Meng ', # 0x20 -'[?] ', # 0x21 +None, # 0x21 'Yaji ', # 0x22 -'[?] ', # 0x23 +None, # 0x23 'Ying ', # 0x24 'Ying ', # 0x25 'Ying ', # 0x26 @@ -137,12 +137,12 @@ data = ( 'Chan ', # 0x87 'Kai ', # 0x88 'Kui ', # 0x89 -'[?] ', # 0x8a +None, # 0x8a 'Jiang ', # 0x8b 'Lou ', # 0x8c 'Wei ', # 0x8d 'Pai ', # 0x8e -'[?] ', # 0x8f +None, # 0x8f 'Sou ', # 0x90 'Yin ', # 0x91 'Shi ', # 0x92 @@ -221,13 +221,13 @@ data = ( 'Ce ', # 0xdb 'Hai ', # 0xdc 'Lan ', # 0xdd -'[?] ', # 0xde +None, # 0xde 'Ji ', # 0xdf 'Li ', # 0xe0 'Can ', # 0xe1 'Lang ', # 0xe2 'Yu ', # 0xe3 -'[?] ', # 0xe4 +None, # 0xe4 'Ying ', # 0xe5 'Mo ', # 0xe6 'Diao ', # 0xe7 diff --git a/libs/common/unidecode/x085.py b/libs/common/unidecode/x085.py index c11c513b..5b3bfa0a 100644 --- a/libs/common/unidecode/x085.py +++ b/libs/common/unidecode/x085.py @@ -112,7 +112,7 @@ data = ( 'Xi ', # 0x6e 'Long ', # 0x6f 'Yun ', # 0x70 -'[?] ', # 0x71 +None, # 0x71 'Qi ', # 0x72 'Jian ', # 0x73 'Yun ', # 0x74 @@ -211,7 +211,7 @@ data = ( 'Qiong ', # 0xd1 'Qie ', # 0xd2 'Xian ', # 0xd3 -'[?] ', # 0xd4 +None, # 0xd4 'Ou ', # 0xd5 'Xian ', # 0xd6 'Su ', # 0xd7 @@ -241,10 +241,10 @@ data = ( 'Wei ', # 0xef 'Liu ', # 0xf0 'Hui ', # 0xf1 -'[?] ', # 0xf2 +None, # 0xf2 'Gao ', # 0xf3 'Yun ', # 0xf4 -'[?] ', # 0xf5 +None, # 0xf5 'Li ', # 0xf6 'Shu ', # 0xf7 'Chu ', # 0xf8 diff --git a/libs/common/unidecode/x086.py b/libs/common/unidecode/x086.py index 38784a61..bd094e1a 100644 --- a/libs/common/unidecode/x086.py +++ b/libs/common/unidecode/x086.py @@ -20,7 +20,7 @@ data = ( 'Hagi ', # 0x12 'Su ', # 0x13 'Jiong ', # 0x14 -'[?] ', # 0x15 +None, # 0x15 'Nie ', # 0x16 'Bo ', # 0x17 'Rang ', # 0x18 @@ -68,7 +68,7 @@ data = ( 'Lu ', # 0x42 'Jian ', # 0x43 'San ', # 0x44 -'[?] ', # 0x45 +None, # 0x45 'Lei ', # 0x46 'Quan ', # 0x47 'Xiao ', # 0x48 @@ -113,7 +113,7 @@ data = ( 'Qiu ', # 0x6f 'Cheng ', # 0x70 'Shi ', # 0x71 -'[?] ', # 0x72 +None, # 0x72 'Di ', # 0x73 'Zhe ', # 0x74 'She ', # 0x75 diff --git a/libs/common/unidecode/x087.py b/libs/common/unidecode/x087.py index 2a2b79aa..6875ac8f 100644 --- a/libs/common/unidecode/x087.py +++ b/libs/common/unidecode/x087.py @@ -73,7 +73,7 @@ data = ( 'Ying ', # 0x47 'Guo ', # 0x48 'Chan ', # 0x49 -'[?] ', # 0x4a +None, # 0x4a 'La ', # 0x4b 'Ke ', # 0x4c 'Ji ', # 0x4d @@ -128,7 +128,7 @@ data = ( 'Rong ', # 0x7e 'Ying ', # 0x7f 'Jiang ', # 0x80 -'[?] ', # 0x81 +None, # 0x81 'Lang ', # 0x82 'Pang ', # 0x83 'Si ', # 0x84 @@ -168,7 +168,7 @@ data = ( 'So ', # 0xa6 'Ebi ', # 0xa7 'Man ', # 0xa8 -'[?] ', # 0xa9 +None, # 0xa9 'Shang ', # 0xaa 'Zhe ', # 0xab 'Cao ', # 0xac @@ -244,7 +244,7 @@ data = ( 'Chong ', # 0xf2 'Xun ', # 0xf3 'Si ', # 0xf4 -'[?] ', # 0xf5 +None, # 0xf5 'Cheng ', # 0xf6 'Dang ', # 0xf7 'Li ', # 0xf8 diff --git a/libs/common/unidecode/x088.py b/libs/common/unidecode/x088.py index f907ce92..bca16854 100644 --- a/libs/common/unidecode/x088.py +++ b/libs/common/unidecode/x088.py @@ -51,7 +51,7 @@ data = ( 'Gu ', # 0x31 'Juan ', # 0x32 'Ying ', # 0x33 -'[?] ', # 0x34 +None, # 0x34 'Xi ', # 0x35 'Can ', # 0x36 'Qu ', # 0x37 @@ -79,7 +79,7 @@ data = ( 'Yan ', # 0x4d 'Kan ', # 0x4e 'Yuan ', # 0x4f -'[?] ', # 0x50 +None, # 0x50 'Ling ', # 0x51 'Xuan ', # 0x52 'Shu ', # 0x53 @@ -198,7 +198,7 @@ data = ( 'Yuki ', # 0xc4 'Zhuang ', # 0xc5 'Dang ', # 0xc6 -'[?] ', # 0xc7 +None, # 0xc7 'Kun ', # 0xc8 'Ken ', # 0xc9 'Niao ', # 0xca diff --git a/libs/common/unidecode/x089.py b/libs/common/unidecode/x089.py index d23abc3c..e223cad0 100644 --- a/libs/common/unidecode/x089.py +++ b/libs/common/unidecode/x089.py @@ -103,7 +103,7 @@ data = ( 'Pu ', # 0x65 'Ru ', # 0x66 'Zhi ', # 0x67 -'[?] ', # 0x68 +None, # 0x68 'Shu ', # 0x69 'Wa ', # 0x6a 'Shi ', # 0x6b @@ -131,7 +131,7 @@ data = ( 'Yao ', # 0x81 'Feng ', # 0x82 'Tan ', # 0x83 -'[?] ', # 0x84 +None, # 0x84 'Biao ', # 0x85 'Fu ', # 0x86 'Ba ', # 0x87 diff --git a/libs/common/unidecode/x08b.py b/libs/common/unidecode/x08b.py index b89c37a2..f3c7d374 100644 --- a/libs/common/unidecode/x08b.py +++ b/libs/common/unidecode/x08b.py @@ -2,7 +2,7 @@ data = ( 'Mou ', # 0x00 'Ye ', # 0x01 'Wei ', # 0x02 -'[?] ', # 0x03 +None, # 0x03 'Teng ', # 0x04 'Zou ', # 0x05 'Shan ', # 0x06 @@ -33,7 +33,7 @@ data = ( 'Tao ', # 0x1f 'Yao ', # 0x20 'Yao ', # 0x21 -'[?] ', # 0x22 +None, # 0x22 'Yu ', # 0x23 'Biao ', # 0x24 'Cong ', # 0x25 diff --git a/libs/common/unidecode/x08c.py b/libs/common/unidecode/x08c.py index 514c446b..3735ef71 100644 --- a/libs/common/unidecode/x08c.py +++ b/libs/common/unidecode/x08c.py @@ -109,7 +109,7 @@ data = ( 'Yu ', # 0x6b 'Zhu ', # 0x6c 'Jia ', # 0x6d -'[?] ', # 0x6e +None, # 0x6e 'Xi ', # 0x6f 'Bo ', # 0x70 'Wen ', # 0x71 diff --git a/libs/common/unidecode/x08d.py b/libs/common/unidecode/x08d.py index ae63f226..f6cbadde 100644 --- a/libs/common/unidecode/x08d.py +++ b/libs/common/unidecode/x08d.py @@ -11,7 +11,7 @@ data = ( 'Tan ', # 0x09 'Zan ', # 0x0a 'Yan ', # 0x0b -'[?] ', # 0x0c +None, # 0x0c 'Shan ', # 0x0d 'Wan ', # 0x0e 'Ying ', # 0x0f @@ -23,7 +23,7 @@ data = ( 'Du ', # 0x15 'Shu ', # 0x16 'Yan ', # 0x17 -'[?] ', # 0x18 +None, # 0x18 'Xuan ', # 0x19 'Long ', # 0x1a 'Gan ', # 0x1b @@ -175,7 +175,7 @@ data = ( 'Yao ', # 0xad 'Zao ', # 0xae 'Ti ', # 0xaf -'[?] ', # 0xb0 +None, # 0xb0 'Zan ', # 0xb1 'Zan ', # 0xb2 'Zu ', # 0xb3 diff --git a/libs/common/unidecode/x08e.py b/libs/common/unidecode/x08e.py index 015ed1ea..628faf2b 100644 --- a/libs/common/unidecode/x08e.py +++ b/libs/common/unidecode/x08e.py @@ -13,7 +13,7 @@ data = ( 'Jiao ', # 0x0b 'Chou ', # 0x0c 'Qiao ', # 0x0d -'[?] ', # 0x0e +None, # 0x0e 'Ta ', # 0x0f 'Jian ', # 0x10 'Qi ', # 0x11 @@ -187,7 +187,7 @@ data = ( 'Ju ', # 0xb9 'Tang ', # 0xba 'Utsuke ', # 0xbb -'[?] ', # 0xbc +None, # 0xbc 'Yan ', # 0xbd 'Shitsuke ', # 0xbe 'Kang ', # 0xbf diff --git a/libs/common/unidecode/x08f.py b/libs/common/unidecode/x08f.py index 39d04a9c..d52164bc 100644 --- a/libs/common/unidecode/x08f.py +++ b/libs/common/unidecode/x08f.py @@ -169,7 +169,7 @@ data = ( 'Bian ', # 0xa7 'Bian ', # 0xa8 'Bian ', # 0xa9 -'[?] ', # 0xaa +None, # 0xaa 'Bian ', # 0xab 'Ban ', # 0xac 'Ci ', # 0xad diff --git a/libs/common/unidecode/x090.py b/libs/common/unidecode/x090.py index ade65069..e8b1eee1 100644 --- a/libs/common/unidecode/x090.py +++ b/libs/common/unidecode/x090.py @@ -99,7 +99,7 @@ data = ( 'Su ', # 0x61 'Ta ', # 0x62 'Qian ', # 0x63 -'[?] ', # 0x64 +None, # 0x64 'Yao ', # 0x65 'Guan ', # 0x66 'Zhang ', # 0x67 @@ -155,7 +155,7 @@ data = ( 'Mang ', # 0x99 'Ru ', # 0x9a 'Qiong ', # 0x9b -'[?] ', # 0x9c +None, # 0x9c 'Kuang ', # 0x9d 'Fu ', # 0x9e 'Kang ', # 0x9f diff --git a/libs/common/unidecode/x091.py b/libs/common/unidecode/x091.py index fa3dd766..764e3e4d 100644 --- a/libs/common/unidecode/x091.py +++ b/libs/common/unidecode/x091.py @@ -91,7 +91,7 @@ data = ( 'Zhen ', # 0x59 'Fen ', # 0x5a 'Sakenomoto ', # 0x5b -'[?] ', # 0x5c +None, # 0x5c 'Yun ', # 0x5d 'Tai ', # 0x5e 'Tian ', # 0x5f diff --git a/libs/common/unidecode/x092.py b/libs/common/unidecode/x092.py index e752f4fe..da9cccd2 100644 --- a/libs/common/unidecode/x092.py +++ b/libs/common/unidecode/x092.py @@ -42,7 +42,7 @@ data = ( 'Habaki ', # 0x28 'Irori ', # 0x29 'Ngaak ', # 0x2a -'[?] ', # 0x2b +None, # 0x2b 'Duo ', # 0x2c 'Zi ', # 0x2d 'Ni ', # 0x2e @@ -243,7 +243,7 @@ data = ( 'Te ', # 0xf1 'Pyeng ', # 0xf2 'Zhu ', # 0xf3 -'[?] ', # 0xf4 +None, # 0xf4 'Tu ', # 0xf5 'Liu ', # 0xf6 'Zui ', # 0xf7 diff --git a/libs/common/unidecode/x093.py b/libs/common/unidecode/x093.py index 82857e9a..eabec27f 100644 --- a/libs/common/unidecode/x093.py +++ b/libs/common/unidecode/x093.py @@ -62,13 +62,13 @@ data = ( 'Nai ', # 0x3c 'Wan ', # 0x3d 'Zan ', # 0x3e -'[?] ', # 0x3f +None, # 0x3f 'De ', # 0x40 'Xian ', # 0x41 -'[?] ', # 0x42 +None, # 0x42 'Huo ', # 0x43 'Liang ', # 0x44 -'[?] ', # 0x45 +None, # 0x45 'Men ', # 0x46 'Kai ', # 0x47 'Ying ', # 0x48 @@ -133,7 +133,7 @@ data = ( 'Pai ', # 0x83 'Ai ', # 0x84 'Jie ', # 0x85 -'[?] ', # 0x86 +None, # 0x86 'Mei ', # 0x87 'Chuo ', # 0x88 'Ta ', # 0x89 @@ -187,9 +187,9 @@ data = ( 'Kasugai ', # 0xb9 'Habaki ', # 0xba 'Suo ', # 0xbb -'[?] ', # 0xbc -'[?] ', # 0xbd -'[?] ', # 0xbe +None, # 0xbc +None, # 0xbd +None, # 0xbe 'Na ', # 0xbf 'Lu ', # 0xc0 'Suo ', # 0xc1 @@ -238,10 +238,10 @@ data = ( 'Xia ', # 0xec 'Xi ', # 0xed 'Kang ', # 0xee -'[?] ', # 0xef +None, # 0xef 'Beng ', # 0xf0 -'[?] ', # 0xf1 -'[?] ', # 0xf2 +None, # 0xf1 +None, # 0xf2 'Zheng ', # 0xf3 'Lu ', # 0xf4 'Hua ', # 0xf5 diff --git a/libs/common/unidecode/x094.py b/libs/common/unidecode/x094.py index 17eb9ddb..990ab60c 100644 --- a/libs/common/unidecode/x094.py +++ b/libs/common/unidecode/x094.py @@ -33,8 +33,8 @@ data = ( 'Ti ', # 0x1f 'Pu ', # 0x20 'Tie ', # 0x21 -'[?] ', # 0x22 -'[?] ', # 0x23 +None, # 0x22 +None, # 0x23 'Ding ', # 0x24 'Shan ', # 0x25 'Kai ', # 0x26 @@ -101,8 +101,8 @@ data = ( 'Biao ', # 0x63 'Bao ', # 0x64 'Lu ', # 0x65 -'[?] ', # 0x66 -'[?] ', # 0x67 +None, # 0x66 +None, # 0x67 'Long ', # 0x68 'E ', # 0x69 'Lu ', # 0x6a diff --git a/libs/common/unidecode/x095.py b/libs/common/unidecode/x095.py index 4b363942..71b7e4ba 100644 --- a/libs/common/unidecode/x095.py +++ b/libs/common/unidecode/x095.py @@ -205,7 +205,7 @@ data = ( 'Que ', # 0xcb 'Lan ', # 0xcc 'Du ', # 0xcd -'[?] ', # 0xce +None, # 0xce 'Phwung ', # 0xcf 'Tian ', # 0xd0 'Nie ', # 0xd1 @@ -230,7 +230,7 @@ data = ( 'Huan ', # 0xe4 'Ta ', # 0xe5 'Wen ', # 0xe6 -'[?] ', # 0xe7 +None, # 0xe7 'Men ', # 0xe8 'Shuan ', # 0xe9 'Shan ', # 0xea diff --git a/libs/common/unidecode/x096.py b/libs/common/unidecode/x096.py index 738a4ea3..6aaa359d 100644 --- a/libs/common/unidecode/x096.py +++ b/libs/common/unidecode/x096.py @@ -160,7 +160,7 @@ data = ( 'Ao ', # 0x9e 'Xi ', # 0x9f 'Yin ', # 0xa0 -'[?] ', # 0xa1 +None, # 0xa1 'Rao ', # 0xa2 'Lin ', # 0xa3 'Tui ', # 0xa4 diff --git a/libs/common/unidecode/x097.py b/libs/common/unidecode/x097.py index 7255f0f8..66e08e90 100644 --- a/libs/common/unidecode/x097.py +++ b/libs/common/unidecode/x097.py @@ -22,7 +22,7 @@ data = ( 'Chou ', # 0x14 'Tun ', # 0x15 'Lin ', # 0x16 -'[?] ', # 0x17 +None, # 0x17 'Dong ', # 0x18 'Ying ', # 0x19 'Wu ', # 0x1a @@ -58,7 +58,7 @@ data = ( 'Ba ', # 0x38 'Pi ', # 0x39 'Wei ', # 0x3a -'[?] ', # 0x3b +None, # 0x3b 'Xi ', # 0x3c 'Ji ', # 0x3d 'Mai ', # 0x3e @@ -76,7 +76,7 @@ data = ( 'Feng ', # 0x4a 'Li ', # 0x4b 'Bao ', # 0x4c -'[?] ', # 0x4d +None, # 0x4d 'He ', # 0x4e 'He ', # 0x4f 'Bing ', # 0x50 @@ -212,7 +212,7 @@ data = ( 'Qiao ', # 0xd2 'Han ', # 0xd3 'Chang ', # 0xd4 -'[?] ', # 0xd5 +None, # 0xd5 'Rou ', # 0xd6 'Xun ', # 0xd7 'She ', # 0xd8 @@ -222,7 +222,7 @@ data = ( 'Tao ', # 0xdc 'Gou ', # 0xdd 'Yun ', # 0xde -'[?] ', # 0xdf +None, # 0xdf 'Bi ', # 0xe0 'Wei ', # 0xe1 'Hui ', # 0xe2 diff --git a/libs/common/unidecode/x098.py b/libs/common/unidecode/x098.py index 98160e77..298a1fbd 100644 --- a/libs/common/unidecode/x098.py +++ b/libs/common/unidecode/x098.py @@ -201,7 +201,7 @@ data = ( 'Biao ', # 0xc7 'Biao ', # 0xc8 'Liao ', # 0xc9 -'[?] ', # 0xca +None, # 0xca 'Se ', # 0xcb 'Feng ', # 0xcc 'Biao ', # 0xcd diff --git a/libs/common/unidecode/x099.py b/libs/common/unidecode/x099.py index 2adf3de8..60a68014 100644 --- a/libs/common/unidecode/x099.py +++ b/libs/common/unidecode/x099.py @@ -198,7 +198,7 @@ data = ( 'Tuo ', # 0xc4 'Yi ', # 0xc5 'Qu ', # 0xc6 -'[?] ', # 0xc7 +None, # 0xc7 'Qu ', # 0xc8 'Jiong ', # 0xc9 'Bo ', # 0xca diff --git a/libs/common/unidecode/x09b.py b/libs/common/unidecode/x09b.py index 7fd0a2db..24dfb99e 100644 --- a/libs/common/unidecode/x09b.py +++ b/libs/common/unidecode/x09b.py @@ -151,7 +151,7 @@ data = ( 'Gu ', # 0x95 'Kajika ', # 0x96 'Tong ', # 0x97 -'[?] ', # 0x98 +None, # 0x98 'Ta ', # 0x99 'Jie ', # 0x9a 'Shu ', # 0x9b diff --git a/libs/common/unidecode/x09d.py b/libs/common/unidecode/x09d.py index 99d5859c..c23f21a8 100644 --- a/libs/common/unidecode/x09d.py +++ b/libs/common/unidecode/x09d.py @@ -144,7 +144,7 @@ data = ( 'Kikuitadaki ', # 0x8e 'Ji ', # 0x8f 'Shu ', # 0x90 -'[?] ', # 0x91 +None, # 0x91 'Chi ', # 0x92 'Miao ', # 0x93 'Rou ', # 0x94 diff --git a/libs/common/unidecode/x09e.py b/libs/common/unidecode/x09e.py index 8a392a23..5ddf6812 100644 --- a/libs/common/unidecode/x09e.py +++ b/libs/common/unidecode/x09e.py @@ -181,7 +181,7 @@ data = ( 'Lai ', # 0xb3 'Qu ', # 0xb4 'Mian ', # 0xb5 -'[?] ', # 0xb6 +None, # 0xb6 'Feng ', # 0xb7 'Fu ', # 0xb8 'Qu ', # 0xb9 diff --git a/libs/common/unidecode/x09f.py b/libs/common/unidecode/x09f.py index acd59a69..ea8e372b 100644 --- a/libs/common/unidecode/x09f.py +++ b/libs/common/unidecode/x09f.py @@ -165,93 +165,93 @@ data = ( 'Jue ', # 0xa3 'Xie ', # 0xa4 'Yu ', # 0xa5 -'[?]', # 0xa6 -'[?]', # 0xa7 -'[?]', # 0xa8 -'[?]', # 0xa9 -'[?]', # 0xaa -'[?]', # 0xab -'[?]', # 0xac -'[?]', # 0xad -'[?]', # 0xae -'[?]', # 0xaf -'[?]', # 0xb0 -'[?]', # 0xb1 -'[?]', # 0xb2 -'[?]', # 0xb3 -'[?]', # 0xb4 -'[?]', # 0xb5 -'[?]', # 0xb6 -'[?]', # 0xb7 -'[?]', # 0xb8 -'[?]', # 0xb9 -'[?]', # 0xba -'[?]', # 0xbb -'[?]', # 0xbc -'[?]', # 0xbd -'[?]', # 0xbe -'[?]', # 0xbf -'[?]', # 0xc0 -'[?]', # 0xc1 -'[?]', # 0xc2 -'[?]', # 0xc3 -'[?]', # 0xc4 -'[?]', # 0xc5 -'[?]', # 0xc6 -'[?]', # 0xc7 -'[?]', # 0xc8 -'[?]', # 0xc9 -'[?]', # 0xca -'[?]', # 0xcb -'[?]', # 0xcc -'[?]', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf -'[?]', # 0xd0 -'[?]', # 0xd1 -'[?]', # 0xd2 -'[?]', # 0xd3 -'[?]', # 0xd4 -'[?]', # 0xd5 -'[?]', # 0xd6 -'[?]', # 0xd7 -'[?]', # 0xd8 -'[?]', # 0xd9 -'[?]', # 0xda -'[?]', # 0xdb -'[?]', # 0xdc -'[?]', # 0xdd -'[?]', # 0xde -'[?]', # 0xdf -'[?]', # 0xe0 -'[?]', # 0xe1 -'[?]', # 0xe2 -'[?]', # 0xe3 -'[?]', # 0xe4 -'[?]', # 0xe5 -'[?]', # 0xe6 -'[?]', # 0xe7 -'[?]', # 0xe8 -'[?]', # 0xe9 -'[?]', # 0xea -'[?]', # 0xeb -'[?]', # 0xec -'[?]', # 0xed -'[?]', # 0xee -'[?]', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xa6 +None, # 0xa7 +None, # 0xa8 +None, # 0xa9 +None, # 0xaa +None, # 0xab +None, # 0xac +None, # 0xad +None, # 0xae +None, # 0xaf +None, # 0xb0 +None, # 0xb1 +None, # 0xb2 +None, # 0xb3 +None, # 0xb4 +None, # 0xb5 +None, # 0xb6 +None, # 0xb7 +None, # 0xb8 +None, # 0xb9 +None, # 0xba +None, # 0xbb +None, # 0xbc +None, # 0xbd +None, # 0xbe +None, # 0xbf +None, # 0xc0 +None, # 0xc1 +None, # 0xc2 +None, # 0xc3 +None, # 0xc4 +None, # 0xc5 +None, # 0xc6 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x0a4.py b/libs/common/unidecode/x0a4.py index 4fb6b264..334030e1 100644 --- a/libs/common/unidecode/x0a4.py +++ b/libs/common/unidecode/x0a4.py @@ -140,9 +140,9 @@ data = ( 'yyp', # 0x8a 'yyrx', # 0x8b 'yyr', # 0x8c -'[?]', # 0x8d -'[?]', # 0x8e -'[?]', # 0x8f +None, # 0x8d +None, # 0x8e +None, # 0x8f 'Qot', # 0x90 'Li', # 0x91 'Kit', # 0x92 @@ -161,8 +161,8 @@ data = ( 'Hxuo', # 0x9f 'Tat', # 0xa0 'Ga', # 0xa1 -'[?]', # 0xa2 -'[?]', # 0xa3 +None, # 0xa2 +None, # 0xa3 'Ddur', # 0xa4 'Bur', # 0xa5 'Gguo', # 0xa6 @@ -179,7 +179,7 @@ data = ( 'Vep', # 0xb1 'Za', # 0xb2 'Jo', # 0xb3 -'[?]', # 0xb4 +None, # 0xb4 'Jjy', # 0xb5 'Got', # 0xb6 'Jjie', # 0xb7 @@ -192,66 +192,66 @@ data = ( 'Cip', # 0xbe 'Hxop', # 0xbf 'Shat', # 0xc0 -'[?]', # 0xc1 +None, # 0xc1 'Shop', # 0xc2 'Che', # 0xc3 'Zziet', # 0xc4 -'[?]', # 0xc5 +None, # 0xc5 'Ke', # 0xc6 -'[?]', # 0xc7 -'[?]', # 0xc8 -'[?]', # 0xc9 -'[?]', # 0xca -'[?]', # 0xcb -'[?]', # 0xcc -'[?]', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf -'[?]', # 0xd0 -'[?]', # 0xd1 -'[?]', # 0xd2 -'[?]', # 0xd3 -'[?]', # 0xd4 -'[?]', # 0xd5 -'[?]', # 0xd6 -'[?]', # 0xd7 -'[?]', # 0xd8 -'[?]', # 0xd9 -'[?]', # 0xda -'[?]', # 0xdb -'[?]', # 0xdc -'[?]', # 0xdd -'[?]', # 0xde -'[?]', # 0xdf -'[?]', # 0xe0 -'[?]', # 0xe1 -'[?]', # 0xe2 -'[?]', # 0xe3 -'[?]', # 0xe4 -'[?]', # 0xe5 -'[?]', # 0xe6 -'[?]', # 0xe7 -'[?]', # 0xe8 -'[?]', # 0xe9 -'[?]', # 0xea -'[?]', # 0xeb -'[?]', # 0xec -'[?]', # 0xed -'[?]', # 0xee -'[?]', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x0d7.py b/libs/common/unidecode/x0d7.py index 1dfb7296..5036de25 100644 --- a/libs/common/unidecode/x0d7.py +++ b/libs/common/unidecode/x0d7.py @@ -163,95 +163,95 @@ data = ( 'hit', # 0xa1 'hip', # 0xa2 'hih', # 0xa3 -'[?]', # 0xa4 -'[?]', # 0xa5 -'[?]', # 0xa6 -'[?]', # 0xa7 -'[?]', # 0xa8 -'[?]', # 0xa9 -'[?]', # 0xaa -'[?]', # 0xab -'[?]', # 0xac -'[?]', # 0xad -'[?]', # 0xae -'[?]', # 0xaf -'[?]', # 0xb0 -'[?]', # 0xb1 -'[?]', # 0xb2 -'[?]', # 0xb3 -'[?]', # 0xb4 -'[?]', # 0xb5 -'[?]', # 0xb6 -'[?]', # 0xb7 -'[?]', # 0xb8 -'[?]', # 0xb9 -'[?]', # 0xba -'[?]', # 0xbb -'[?]', # 0xbc -'[?]', # 0xbd -'[?]', # 0xbe -'[?]', # 0xbf -'[?]', # 0xc0 -'[?]', # 0xc1 -'[?]', # 0xc2 -'[?]', # 0xc3 -'[?]', # 0xc4 -'[?]', # 0xc5 -'[?]', # 0xc6 -'[?]', # 0xc7 -'[?]', # 0xc8 -'[?]', # 0xc9 -'[?]', # 0xca -'[?]', # 0xcb -'[?]', # 0xcc -'[?]', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf -'[?]', # 0xd0 -'[?]', # 0xd1 -'[?]', # 0xd2 -'[?]', # 0xd3 -'[?]', # 0xd4 -'[?]', # 0xd5 -'[?]', # 0xd6 -'[?]', # 0xd7 -'[?]', # 0xd8 -'[?]', # 0xd9 -'[?]', # 0xda -'[?]', # 0xdb -'[?]', # 0xdc -'[?]', # 0xdd -'[?]', # 0xde -'[?]', # 0xdf -'[?]', # 0xe0 -'[?]', # 0xe1 -'[?]', # 0xe2 -'[?]', # 0xe3 -'[?]', # 0xe4 -'[?]', # 0xe5 -'[?]', # 0xe6 -'[?]', # 0xe7 -'[?]', # 0xe8 -'[?]', # 0xe9 -'[?]', # 0xea -'[?]', # 0xeb -'[?]', # 0xec -'[?]', # 0xed -'[?]', # 0xee -'[?]', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xa4 +None, # 0xa5 +None, # 0xa6 +None, # 0xa7 +None, # 0xa8 +None, # 0xa9 +None, # 0xaa +None, # 0xab +None, # 0xac +None, # 0xad +None, # 0xae +None, # 0xaf +None, # 0xb0 +None, # 0xb1 +None, # 0xb2 +None, # 0xb3 +None, # 0xb4 +None, # 0xb5 +None, # 0xb6 +None, # 0xb7 +None, # 0xb8 +None, # 0xb9 +None, # 0xba +None, # 0xbb +None, # 0xbc +None, # 0xbd +None, # 0xbe +None, # 0xbf +None, # 0xc0 +None, # 0xc1 +None, # 0xc2 +None, # 0xc3 +None, # 0xc4 +None, # 0xc5 +None, # 0xc6 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x0fa.py b/libs/common/unidecode/x0fa.py index 3d4e705c..24ff84d1 100644 --- a/libs/common/unidecode/x0fa.py +++ b/libs/common/unidecode/x0fa.py @@ -13,13 +13,13 @@ data = ( 'Hwak ', # 0x0b 'Wu ', # 0x0c 'Huo ', # 0x0d -'[?] ', # 0x0e -'[?] ', # 0x0f +None, # 0x0e +None, # 0x0f 'Zhong ', # 0x10 -'[?] ', # 0x11 +None, # 0x11 'Qing ', # 0x12 -'[?] ', # 0x13 -'[?] ', # 0x14 +None, # 0x13 +None, # 0x14 'Xi ', # 0x15 'Zhu ', # 0x16 'Yi ', # 0x17 @@ -30,228 +30,228 @@ data = ( 'Jing ', # 0x1c 'Jing ', # 0x1d 'Yu ', # 0x1e -'[?] ', # 0x1f +None, # 0x1f 'Hagi ', # 0x20 -'[?] ', # 0x21 +None, # 0x21 'Zhu ', # 0x22 -'[?] ', # 0x23 -'[?] ', # 0x24 +None, # 0x23 +None, # 0x24 'Yi ', # 0x25 'Du ', # 0x26 -'[?] ', # 0x27 -'[?] ', # 0x28 -'[?] ', # 0x29 +None, # 0x27 +None, # 0x28 +None, # 0x29 'Fan ', # 0x2a 'Si ', # 0x2b 'Guan ', # 0x2c -'[?]', # 0x2d -'[?]', # 0x2e -'[?]', # 0x2f -'[?]', # 0x30 -'[?]', # 0x31 -'[?]', # 0x32 -'[?]', # 0x33 -'[?]', # 0x34 -'[?]', # 0x35 -'[?]', # 0x36 -'[?]', # 0x37 -'[?]', # 0x38 -'[?]', # 0x39 -'[?]', # 0x3a -'[?]', # 0x3b -'[?]', # 0x3c -'[?]', # 0x3d -'[?]', # 0x3e -'[?]', # 0x3f -'[?]', # 0x40 -'[?]', # 0x41 -'[?]', # 0x42 -'[?]', # 0x43 -'[?]', # 0x44 -'[?]', # 0x45 -'[?]', # 0x46 -'[?]', # 0x47 -'[?]', # 0x48 -'[?]', # 0x49 -'[?]', # 0x4a -'[?]', # 0x4b -'[?]', # 0x4c -'[?]', # 0x4d -'[?]', # 0x4e -'[?]', # 0x4f -'[?]', # 0x50 -'[?]', # 0x51 -'[?]', # 0x52 -'[?]', # 0x53 -'[?]', # 0x54 -'[?]', # 0x55 -'[?]', # 0x56 -'[?]', # 0x57 -'[?]', # 0x58 -'[?]', # 0x59 -'[?]', # 0x5a -'[?]', # 0x5b -'[?]', # 0x5c -'[?]', # 0x5d -'[?]', # 0x5e -'[?]', # 0x5f -'[?]', # 0x60 -'[?]', # 0x61 -'[?]', # 0x62 -'[?]', # 0x63 -'[?]', # 0x64 -'[?]', # 0x65 -'[?]', # 0x66 -'[?]', # 0x67 -'[?]', # 0x68 -'[?]', # 0x69 -'[?]', # 0x6a -'[?]', # 0x6b -'[?]', # 0x6c -'[?]', # 0x6d -'[?]', # 0x6e -'[?]', # 0x6f -'[?]', # 0x70 -'[?]', # 0x71 -'[?]', # 0x72 -'[?]', # 0x73 -'[?]', # 0x74 -'[?]', # 0x75 -'[?]', # 0x76 -'[?]', # 0x77 -'[?]', # 0x78 -'[?]', # 0x79 -'[?]', # 0x7a -'[?]', # 0x7b -'[?]', # 0x7c -'[?]', # 0x7d -'[?]', # 0x7e -'[?]', # 0x7f -'[?]', # 0x80 -'[?]', # 0x81 -'[?]', # 0x82 -'[?]', # 0x83 -'[?]', # 0x84 -'[?]', # 0x85 -'[?]', # 0x86 -'[?]', # 0x87 -'[?]', # 0x88 -'[?]', # 0x89 -'[?]', # 0x8a -'[?]', # 0x8b -'[?]', # 0x8c -'[?]', # 0x8d -'[?]', # 0x8e -'[?]', # 0x8f -'[?]', # 0x90 -'[?]', # 0x91 -'[?]', # 0x92 -'[?]', # 0x93 -'[?]', # 0x94 -'[?]', # 0x95 -'[?]', # 0x96 -'[?]', # 0x97 -'[?]', # 0x98 -'[?]', # 0x99 -'[?]', # 0x9a -'[?]', # 0x9b -'[?]', # 0x9c -'[?]', # 0x9d -'[?]', # 0x9e -'[?]', # 0x9f -'[?]', # 0xa0 -'[?]', # 0xa1 -'[?]', # 0xa2 -'[?]', # 0xa3 -'[?]', # 0xa4 -'[?]', # 0xa5 -'[?]', # 0xa6 -'[?]', # 0xa7 -'[?]', # 0xa8 -'[?]', # 0xa9 -'[?]', # 0xaa -'[?]', # 0xab -'[?]', # 0xac -'[?]', # 0xad -'[?]', # 0xae -'[?]', # 0xaf -'[?]', # 0xb0 -'[?]', # 0xb1 -'[?]', # 0xb2 -'[?]', # 0xb3 -'[?]', # 0xb4 -'[?]', # 0xb5 -'[?]', # 0xb6 -'[?]', # 0xb7 -'[?]', # 0xb8 -'[?]', # 0xb9 -'[?]', # 0xba -'[?]', # 0xbb -'[?]', # 0xbc -'[?]', # 0xbd -'[?]', # 0xbe -'[?]', # 0xbf -'[?]', # 0xc0 -'[?]', # 0xc1 -'[?]', # 0xc2 -'[?]', # 0xc3 -'[?]', # 0xc4 -'[?]', # 0xc5 -'[?]', # 0xc6 -'[?]', # 0xc7 -'[?]', # 0xc8 -'[?]', # 0xc9 -'[?]', # 0xca -'[?]', # 0xcb -'[?]', # 0xcc -'[?]', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf -'[?]', # 0xd0 -'[?]', # 0xd1 -'[?]', # 0xd2 -'[?]', # 0xd3 -'[?]', # 0xd4 -'[?]', # 0xd5 -'[?]', # 0xd6 -'[?]', # 0xd7 -'[?]', # 0xd8 -'[?]', # 0xd9 -'[?]', # 0xda -'[?]', # 0xdb -'[?]', # 0xdc -'[?]', # 0xdd -'[?]', # 0xde -'[?]', # 0xdf -'[?]', # 0xe0 -'[?]', # 0xe1 -'[?]', # 0xe2 -'[?]', # 0xe3 -'[?]', # 0xe4 -'[?]', # 0xe5 -'[?]', # 0xe6 -'[?]', # 0xe7 -'[?]', # 0xe8 -'[?]', # 0xe9 -'[?]', # 0xea -'[?]', # 0xeb -'[?]', # 0xec -'[?]', # 0xed -'[?]', # 0xee -'[?]', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 -'[?]', # 0xf9 -'[?]', # 0xfa -'[?]', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0x2d +None, # 0x2e +None, # 0x2f +None, # 0x30 +None, # 0x31 +None, # 0x32 +None, # 0x33 +None, # 0x34 +None, # 0x35 +None, # 0x36 +None, # 0x37 +None, # 0x38 +None, # 0x39 +None, # 0x3a +None, # 0x3b +None, # 0x3c +None, # 0x3d +None, # 0x3e +None, # 0x3f +None, # 0x40 +None, # 0x41 +None, # 0x42 +None, # 0x43 +None, # 0x44 +None, # 0x45 +None, # 0x46 +None, # 0x47 +None, # 0x48 +None, # 0x49 +None, # 0x4a +None, # 0x4b +None, # 0x4c +None, # 0x4d +None, # 0x4e +None, # 0x4f +None, # 0x50 +None, # 0x51 +None, # 0x52 +None, # 0x53 +None, # 0x54 +None, # 0x55 +None, # 0x56 +None, # 0x57 +None, # 0x58 +None, # 0x59 +None, # 0x5a +None, # 0x5b +None, # 0x5c +None, # 0x5d +None, # 0x5e +None, # 0x5f +None, # 0x60 +None, # 0x61 +None, # 0x62 +None, # 0x63 +None, # 0x64 +None, # 0x65 +None, # 0x66 +None, # 0x67 +None, # 0x68 +None, # 0x69 +None, # 0x6a +None, # 0x6b +None, # 0x6c +None, # 0x6d +None, # 0x6e +None, # 0x6f +None, # 0x70 +None, # 0x71 +None, # 0x72 +None, # 0x73 +None, # 0x74 +None, # 0x75 +None, # 0x76 +None, # 0x77 +None, # 0x78 +None, # 0x79 +None, # 0x7a +None, # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f +None, # 0x80 +None, # 0x81 +None, # 0x82 +None, # 0x83 +None, # 0x84 +None, # 0x85 +None, # 0x86 +None, # 0x87 +None, # 0x88 +None, # 0x89 +None, # 0x8a +None, # 0x8b +None, # 0x8c +None, # 0x8d +None, # 0x8e +None, # 0x8f +None, # 0x90 +None, # 0x91 +None, # 0x92 +None, # 0x93 +None, # 0x94 +None, # 0x95 +None, # 0x96 +None, # 0x97 +None, # 0x98 +None, # 0x99 +None, # 0x9a +None, # 0x9b +None, # 0x9c +None, # 0x9d +None, # 0x9e +None, # 0x9f +None, # 0xa0 +None, # 0xa1 +None, # 0xa2 +None, # 0xa3 +None, # 0xa4 +None, # 0xa5 +None, # 0xa6 +None, # 0xa7 +None, # 0xa8 +None, # 0xa9 +None, # 0xaa +None, # 0xab +None, # 0xac +None, # 0xad +None, # 0xae +None, # 0xaf +None, # 0xb0 +None, # 0xb1 +None, # 0xb2 +None, # 0xb3 +None, # 0xb4 +None, # 0xb5 +None, # 0xb6 +None, # 0xb7 +None, # 0xb8 +None, # 0xb9 +None, # 0xba +None, # 0xbb +None, # 0xbc +None, # 0xbd +None, # 0xbe +None, # 0xbf +None, # 0xc0 +None, # 0xc1 +None, # 0xc2 +None, # 0xc3 +None, # 0xc4 +None, # 0xc5 +None, # 0xc6 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x0fb.py b/libs/common/unidecode/x0fb.py index bf69dbb9..5e098bc9 100644 --- a/libs/common/unidecode/x0fb.py +++ b/libs/common/unidecode/x0fb.py @@ -6,79 +6,79 @@ data = ( 'ffl', # 0x04 'st', # 0x05 'st', # 0x06 -'[?]', # 0x07 -'[?]', # 0x08 -'[?]', # 0x09 -'[?]', # 0x0a -'[?]', # 0x0b -'[?]', # 0x0c -'[?]', # 0x0d -'[?]', # 0x0e -'[?]', # 0x0f -'[?]', # 0x10 -'[?]', # 0x11 -'[?]', # 0x12 +None, # 0x07 +None, # 0x08 +None, # 0x09 +None, # 0x0a +None, # 0x0b +None, # 0x0c +None, # 0x0d +None, # 0x0e +None, # 0x0f +None, # 0x10 +None, # 0x11 +None, # 0x12 'mn', # 0x13 'me', # 0x14 'mi', # 0x15 'vn', # 0x16 'mkh', # 0x17 -'[?]', # 0x18 -'[?]', # 0x19 -'[?]', # 0x1a -'[?]', # 0x1b -'[?]', # 0x1c -'yi', # 0x1d +None, # 0x18 +None, # 0x19 +None, # 0x1a +None, # 0x1b +None, # 0x1c +'i', # 0x1d '', # 0x1e -'ay', # 0x1f +'AY', # 0x1f '`', # 0x20 -'', # 0x21 +'A', # 0x21 'd', # 0x22 'h', # 0x23 -'k', # 0x24 +'KH', # 0x24 'l', # 0x25 'm', # 0x26 -'m', # 0x27 +'r', # 0x27 't', # 0x28 '+', # 0x29 -'sh', # 0x2a -'s', # 0x2b -'sh', # 0x2c -'s', # 0x2d +'SH', # 0x2a +'S', # 0x2b +'SH', # 0x2c +'S', # 0x2d 'a', # 0x2e 'a', # 0x2f -'', # 0x30 +'A', # 0x30 'b', # 0x31 'g', # 0x32 'd', # 0x33 'h', # 0x34 'v', # 0x35 'z', # 0x36 -'[?]', # 0x37 +None, # 0x37 't', # 0x38 'y', # 0x39 -'k', # 0x3a -'k', # 0x3b +'KH', # 0x3a +'KH', # 0x3b 'l', # 0x3c -'[?]', # 0x3d -'l', # 0x3e -'[?]', # 0x3f +None, # 0x3d +'m', # 0x3e +None, # 0x3f 'n', # 0x40 -'n', # 0x41 -'[?]', # 0x42 +'s', # 0x41 +None, # 0x42 'p', # 0x43 'p', # 0x44 -'[?]', # 0x45 -'ts', # 0x46 -'ts', # 0x47 +None, # 0x45 +'TS', # 0x46 +'k', # 0x47 'r', # 0x48 -'sh', # 0x49 +'SH', # 0x49 't', # 0x4a -'vo', # 0x4b -'b', # 0x4c -'k', # 0x4d -'p', # 0x4e -'l', # 0x4f +'o', # 0x4b +'v', # 0x4c +'KH', # 0x4d +'f', # 0x4e +'EL', # 0x4f '', # 0x50 '', # 0x51 '', # 0x52 @@ -177,39 +177,39 @@ data = ( '', # 0xaf '', # 0xb0 '', # 0xb1 -'[?]', # 0xb2 -'[?]', # 0xb3 -'[?]', # 0xb4 -'[?]', # 0xb5 -'[?]', # 0xb6 -'[?]', # 0xb7 -'[?]', # 0xb8 -'[?]', # 0xb9 -'[?]', # 0xba -'[?]', # 0xbb -'[?]', # 0xbc -'[?]', # 0xbd -'[?]', # 0xbe -'[?]', # 0xbf -'[?]', # 0xc0 -'[?]', # 0xc1 -'[?]', # 0xc2 -'[?]', # 0xc3 -'[?]', # 0xc4 -'[?]', # 0xc5 -'[?]', # 0xc6 -'[?]', # 0xc7 -'[?]', # 0xc8 -'[?]', # 0xc9 -'[?]', # 0xca -'[?]', # 0xcb -'[?]', # 0xcc -'[?]', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf -'[?]', # 0xd0 -'[?]', # 0xd1 -'[?]', # 0xd2 +None, # 0xb2 +None, # 0xb3 +None, # 0xb4 +None, # 0xb5 +None, # 0xb6 +None, # 0xb7 +None, # 0xb8 +None, # 0xb9 +None, # 0xba +None, # 0xbb +None, # 0xbc +None, # 0xbd +None, # 0xbe +None, # 0xbf +None, # 0xc0 +None, # 0xc1 +None, # 0xc2 +None, # 0xc3 +None, # 0xc4 +None, # 0xc5 +None, # 0xc6 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 '', # 0xd3 '', # 0xd4 '', # 0xd5 diff --git a/libs/common/unidecode/x0fd.py b/libs/common/unidecode/x0fd.py index 892bcb05..be0c0f95 100644 --- a/libs/common/unidecode/x0fd.py +++ b/libs/common/unidecode/x0fd.py @@ -63,22 +63,22 @@ data = ( '', # 0x3d '', # 0x3e '', # 0x3f -'[?]', # 0x40 -'[?]', # 0x41 -'[?]', # 0x42 -'[?]', # 0x43 -'[?]', # 0x44 -'[?]', # 0x45 -'[?]', # 0x46 -'[?]', # 0x47 -'[?]', # 0x48 -'[?]', # 0x49 -'[?]', # 0x4a -'[?]', # 0x4b -'[?]', # 0x4c -'[?]', # 0x4d -'[?]', # 0x4e -'[?]', # 0x4f +None, # 0x40 +None, # 0x41 +None, # 0x42 +None, # 0x43 +None, # 0x44 +None, # 0x45 +None, # 0x46 +None, # 0x47 +None, # 0x48 +None, # 0x49 +None, # 0x4a +None, # 0x4b +None, # 0x4c +None, # 0x4d +None, # 0x4e +None, # 0x4f '', # 0x50 '', # 0x51 '', # 0x52 @@ -143,8 +143,8 @@ data = ( '', # 0x8d '', # 0x8e '', # 0x8f -'[?]', # 0x90 -'[?]', # 0x91 +None, # 0x90 +None, # 0x91 '', # 0x92 '', # 0x93 '', # 0x94 @@ -199,46 +199,46 @@ data = ( '', # 0xc5 '', # 0xc6 '', # 0xc7 -'[?]', # 0xc8 -'[?]', # 0xc9 -'[?]', # 0xca -'[?]', # 0xcb -'[?]', # 0xcc -'[?]', # 0xcd -'[?]', # 0xce -'[?]', # 0xcf -'[?]', # 0xd0 -'[?]', # 0xd1 -'[?]', # 0xd2 -'[?]', # 0xd3 -'[?]', # 0xd4 -'[?]', # 0xd5 -'[?]', # 0xd6 -'[?]', # 0xd7 -'[?]', # 0xd8 -'[?]', # 0xd9 -'[?]', # 0xda -'[?]', # 0xdb -'[?]', # 0xdc -'[?]', # 0xdd -'[?]', # 0xde -'[?]', # 0xdf -'[?]', # 0xe0 -'[?]', # 0xe1 -'[?]', # 0xe2 -'[?]', # 0xe3 -'[?]', # 0xe4 -'[?]', # 0xe5 -'[?]', # 0xe6 -'[?]', # 0xe7 -'[?]', # 0xe8 -'[?]', # 0xe9 -'[?]', # 0xea -'[?]', # 0xeb -'[?]', # 0xec -'[?]', # 0xed -'[?]', # 0xee -'[?]', # 0xef +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef '', # 0xf0 '', # 0xf1 '', # 0xf2 @@ -251,7 +251,7 @@ data = ( '', # 0xf9 '', # 0xfa '', # 0xfb -'[?]', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xfc +None, # 0xfd +None, # 0xfe ) diff --git a/libs/common/unidecode/x0fe.py b/libs/common/unidecode/x0fe.py index 60e86179..04dbd93f 100644 --- a/libs/common/unidecode/x0fe.py +++ b/libs/common/unidecode/x0fe.py @@ -1,52 +1,52 @@ data = ( -'[?]', # 0x00 -'[?]', # 0x01 -'[?]', # 0x02 -'[?]', # 0x03 -'[?]', # 0x04 -'[?]', # 0x05 -'[?]', # 0x06 -'[?]', # 0x07 -'[?]', # 0x08 -'[?]', # 0x09 -'[?]', # 0x0a -'[?]', # 0x0b -'[?]', # 0x0c -'[?]', # 0x0d -'[?]', # 0x0e -'[?]', # 0x0f -'[?]', # 0x10 -'[?]', # 0x11 -'[?]', # 0x12 -'[?]', # 0x13 -'[?]', # 0x14 -'[?]', # 0x15 -'[?]', # 0x16 -'[?]', # 0x17 -'[?]', # 0x18 -'[?]', # 0x19 -'[?]', # 0x1a -'[?]', # 0x1b -'[?]', # 0x1c -'[?]', # 0x1d -'[?]', # 0x1e -'[?]', # 0x1f +None, # 0x00 +None, # 0x01 +None, # 0x02 +None, # 0x03 +None, # 0x04 +None, # 0x05 +None, # 0x06 +None, # 0x07 +None, # 0x08 +None, # 0x09 +None, # 0x0a +None, # 0x0b +None, # 0x0c +None, # 0x0d +None, # 0x0e +None, # 0x0f +None, # 0x10 +None, # 0x11 +None, # 0x12 +None, # 0x13 +None, # 0x14 +None, # 0x15 +None, # 0x16 +None, # 0x17 +None, # 0x18 +None, # 0x19 +None, # 0x1a +None, # 0x1b +None, # 0x1c +None, # 0x1d +None, # 0x1e +None, # 0x1f '', # 0x20 '', # 0x21 '', # 0x22 '~', # 0x23 -'[?]', # 0x24 -'[?]', # 0x25 -'[?]', # 0x26 -'[?]', # 0x27 -'[?]', # 0x28 -'[?]', # 0x29 -'[?]', # 0x2a -'[?]', # 0x2b -'[?]', # 0x2c -'[?]', # 0x2d -'[?]', # 0x2e -'[?]', # 0x2f +None, # 0x24 +None, # 0x25 +None, # 0x26 +None, # 0x27 +None, # 0x28 +None, # 0x29 +None, # 0x2a +None, # 0x2b +None, # 0x2c +None, # 0x2d +None, # 0x2e +None, # 0x2f '..', # 0x30 '--', # 0x31 '-', # 0x32 @@ -68,10 +68,10 @@ data = ( '] ', # 0x42 '{', # 0x43 '}', # 0x44 -'[?]', # 0x45 -'[?]', # 0x46 -'[?]', # 0x47 -'[?]', # 0x48 +None, # 0x45 +None, # 0x46 +None, # 0x47 +None, # 0x48 '', # 0x49 '', # 0x4a '', # 0x4b @@ -107,16 +107,16 @@ data = ( '$', # 0x69 '%', # 0x6a '@', # 0x6b -'[?]', # 0x6c -'[?]', # 0x6d -'[?]', # 0x6e -'[?]', # 0x6f +None, # 0x6c +None, # 0x6d +None, # 0x6e +None, # 0x6f '', # 0x70 '', # 0x71 '', # 0x72 -'[?]', # 0x73 +None, # 0x73 '', # 0x74 -'[?]', # 0x75 +None, # 0x75 '', # 0x76 '', # 0x77 '', # 0x78 @@ -252,7 +252,7 @@ data = ( '', # 0xfa '', # 0xfb '', # 0xfc -'[?]', # 0xfd -'[?]', # 0xfe +None, # 0xfd +None, # 0xfe '', # 0xff ) diff --git a/libs/common/unidecode/x0ff.py b/libs/common/unidecode/x0ff.py index 04410151..557462c6 100644 --- a/libs/common/unidecode/x0ff.py +++ b/libs/common/unidecode/x0ff.py @@ -1,5 +1,5 @@ data = ( -'[?]', # 0x00 +None, # 0x00 '!', # 0x01 '"', # 0x02 '#', # 0x03 @@ -94,8 +94,8 @@ data = ( '|', # 0x5c '}', # 0x5d '~', # 0x5e -'[?]', # 0x5f -'[?]', # 0x60 +None, # 0x5f +None, # 0x60 '.', # 0x61 '[', # 0x62 ']', # 0x63 @@ -190,39 +190,39 @@ data = ( 't', # 0xbc 'p', # 0xbd 'h', # 0xbe -'[?]', # 0xbf -'[?]', # 0xc0 -'[?]', # 0xc1 +None, # 0xbf +None, # 0xc0 +None, # 0xc1 'a', # 0xc2 'ae', # 0xc3 'ya', # 0xc4 'yae', # 0xc5 'eo', # 0xc6 'e', # 0xc7 -'[?]', # 0xc8 -'[?]', # 0xc9 +None, # 0xc8 +None, # 0xc9 'yeo', # 0xca 'ye', # 0xcb 'o', # 0xcc 'wa', # 0xcd 'wae', # 0xce 'oe', # 0xcf -'[?]', # 0xd0 -'[?]', # 0xd1 +None, # 0xd0 +None, # 0xd1 'yo', # 0xd2 'u', # 0xd3 'weo', # 0xd4 'we', # 0xd5 'wi', # 0xd6 'yu', # 0xd7 -'[?]', # 0xd8 -'[?]', # 0xd9 +None, # 0xd8 +None, # 0xd9 'eu', # 0xda 'yi', # 0xdb 'i', # 0xdc -'[?]', # 0xdd -'[?]', # 0xde -'[?]', # 0xdf +None, # 0xdd +None, # 0xde +None, # 0xdf '/C', # 0xe0 'PS', # 0xe1 '!', # 0xe2 @@ -230,7 +230,7 @@ data = ( '|', # 0xe4 'Y=', # 0xe5 'W=', # 0xe6 -'[?]', # 0xe7 +None, # 0xe7 '|', # 0xe8 '-', # 0xe9 '|', # 0xea @@ -238,16 +238,16 @@ data = ( '|', # 0xec '#', # 0xed 'O', # 0xee -'[?]', # 0xef -'[?]', # 0xf0 -'[?]', # 0xf1 -'[?]', # 0xf2 -'[?]', # 0xf3 -'[?]', # 0xf4 -'[?]', # 0xf5 -'[?]', # 0xf6 -'[?]', # 0xf7 -'[?]', # 0xf8 +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 '{', # 0xf9 '|', # 0xfa '}', # 0xfb diff --git a/libs/common/unidecode/x1d4.py b/libs/common/unidecode/x1d4.py index 53795931..7d23fd47 100644 --- a/libs/common/unidecode/x1d4.py +++ b/libs/common/unidecode/x1d4.py @@ -84,7 +84,7 @@ data = ( 'e', # 0x52 'f', # 0x53 'g', # 0x54 -'', # 0x55 +None, # 0x55 'i', # 0x56 'j', # 0x57 'k', # 0x58 @@ -156,23 +156,23 @@ data = ( 'y', # 0x9a 'z', # 0x9b 'A', # 0x9c -'', # 0x9d +None, # 0x9d 'C', # 0x9e 'D', # 0x9f -'', # 0xa0 -'', # 0xa1 +None, # 0xa0 +None, # 0xa1 'G', # 0xa2 -'', # 0xa3 -'', # 0xa4 +None, # 0xa3 +None, # 0xa4 'J', # 0xa5 'K', # 0xa6 -'', # 0xa7 -'', # 0xa8 +None, # 0xa7 +None, # 0xa8 'N', # 0xa9 'O', # 0xaa 'P', # 0xab 'Q', # 0xac -'', # 0xad +None, # 0xad 'S', # 0xae 'T', # 0xaf 'U', # 0xb0 @@ -185,9 +185,9 @@ data = ( 'b', # 0xb7 'c', # 0xb8 'd', # 0xb9 -'', # 0xba +None, # 0xba 'f', # 0xbb -'', # 0xbc +None, # 0xbc 'h', # 0xbd 'i', # 0xbe 'j', # 0xbf @@ -195,7 +195,7 @@ data = ( 'l', # 0xc1 'm', # 0xc2 'n', # 0xc3 -'', # 0xc4 +None, # 0xc4 'p', # 0xc5 'q', # 0xc6 'r', # 0xc7 diff --git a/libs/common/unidecode/x1d5.py b/libs/common/unidecode/x1d5.py index 6ec605ba..40e6935d 100644 --- a/libs/common/unidecode/x1d5.py +++ b/libs/common/unidecode/x1d5.py @@ -5,13 +5,13 @@ data = ( 'z', # 0x03 'A', # 0x04 'B', # 0x05 -'', # 0x06 +None, # 0x06 'D', # 0x07 'E', # 0x08 'F', # 0x09 'G', # 0x0a -'', # 0x0b -'', # 0x0c +None, # 0x0b +None, # 0x0c 'J', # 0x0d 'K', # 0x0e 'L', # 0x0f @@ -20,7 +20,7 @@ data = ( 'O', # 0x12 'P', # 0x13 'Q', # 0x14 -'', # 0x15 +None, # 0x15 'S', # 0x16 'T', # 0x17 'U', # 0x18 @@ -28,7 +28,7 @@ data = ( 'W', # 0x1a 'X', # 0x1b 'Y', # 0x1c -'', # 0x1d +None, # 0x1d 'a', # 0x1e 'b', # 0x1f 'c', # 0x20 @@ -57,22 +57,22 @@ data = ( 'z', # 0x37 'A', # 0x38 'B', # 0x39 -'', # 0x3a +None, # 0x3a 'D', # 0x3b 'E', # 0x3c 'F', # 0x3d 'G', # 0x3e -'', # 0x3f +None, # 0x3f 'I', # 0x40 'J', # 0x41 'K', # 0x42 'L', # 0x43 'M', # 0x44 -'', # 0x45 +None, # 0x45 'O', # 0x46 -'', # 0x47 -'', # 0x48 -'', # 0x49 +None, # 0x47 +None, # 0x48 +None, # 0x49 'S', # 0x4a 'T', # 0x4b 'U', # 0x4c @@ -80,7 +80,7 @@ data = ( 'W', # 0x4e 'X', # 0x4f 'Y', # 0x50 -'', # 0x51 +None, # 0x51 'a', # 0x52 'b', # 0x53 'c', # 0x54 diff --git a/libs/common/unidecode/x1d6.py b/libs/common/unidecode/x1d6.py index c06a855d..2be1a7fb 100644 --- a/libs/common/unidecode/x1d6.py +++ b/libs/common/unidecode/x1d6.py @@ -165,8 +165,8 @@ data = ( 'z', # 0xa3 'i', # 0xa4 'j', # 0xa5 -'', # 0xa6 -'', # 0xa7 +None, # 0xa6 +None, # 0xa7 'Alpha', # 0xa8 'Beta', # 0xa9 'Gamma', # 0xaa @@ -218,41 +218,41 @@ data = ( 'chi', # 0xd8 'psi', # 0xd9 'omega', # 0xda -'', # 0xdb -'', # 0xdc -'', # 0xdd -'', # 0xde -'', # 0xdf -'', # 0xe0 -'', # 0xe1 -'', # 0xe2 -'', # 0xe3 -'', # 0xe4 -'', # 0xe5 -'', # 0xe6 -'', # 0xe7 -'', # 0xe8 -'', # 0xe9 -'', # 0xea -'', # 0xeb -'', # 0xec -'', # 0xed -'', # 0xee -'', # 0xef -'', # 0xf0 -'', # 0xf1 -'', # 0xf2 -'', # 0xf3 -'', # 0xf4 -'', # 0xf5 -'', # 0xf6 -'', # 0xf7 -'', # 0xf8 -'', # 0xf9 -'', # 0xfa -'', # 0xfb -'', # 0xfc -'', # 0xfd -'', # 0xfe -'', # 0xff +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe +None, # 0xff ) diff --git a/libs/common/unidecode/x1d7.py b/libs/common/unidecode/x1d7.py index 6943bf45..46242ecd 100644 --- a/libs/common/unidecode/x1d7.py +++ b/libs/common/unidecode/x1d7.py @@ -1,210 +1,210 @@ data = ( -'', # 0x00 -'', # 0x01 -'', # 0x02 -'', # 0x03 -'', # 0x04 -'', # 0x05 -'', # 0x06 -'', # 0x07 -'', # 0x08 -'', # 0x09 -'', # 0x0a -'', # 0x0b -'', # 0x0c -'', # 0x0d -'', # 0x0e -'', # 0x0f -'', # 0x10 -'', # 0x11 -'', # 0x12 -'', # 0x13 -'', # 0x14 -'', # 0x15 -'', # 0x16 -'', # 0x17 -'', # 0x18 -'', # 0x19 -'', # 0x1a -'', # 0x1b -'', # 0x1c -'', # 0x1d -'', # 0x1e -'', # 0x1f -'', # 0x20 -'', # 0x21 -'', # 0x22 -'', # 0x23 -'', # 0x24 -'', # 0x25 -'', # 0x26 -'', # 0x27 -'', # 0x28 -'', # 0x29 -'', # 0x2a -'', # 0x2b -'', # 0x2c -'', # 0x2d -'', # 0x2e -'', # 0x2f -'', # 0x30 -'', # 0x31 -'', # 0x32 -'', # 0x33 -'', # 0x34 -'', # 0x35 -'', # 0x36 -'', # 0x37 -'', # 0x38 -'', # 0x39 -'', # 0x3a -'', # 0x3b -'', # 0x3c -'', # 0x3d -'', # 0x3e -'', # 0x3f -'', # 0x40 -'', # 0x41 -'', # 0x42 -'', # 0x43 -'', # 0x44 -'', # 0x45 -'', # 0x46 -'', # 0x47 -'', # 0x48 -'', # 0x49 -'', # 0x4a -'', # 0x4b -'', # 0x4c -'', # 0x4d -'', # 0x4e -'', # 0x4f -'', # 0x50 -'', # 0x51 -'', # 0x52 -'', # 0x53 -'', # 0x54 -'', # 0x55 -'', # 0x56 -'', # 0x57 -'', # 0x58 -'', # 0x59 -'', # 0x5a -'', # 0x5b -'', # 0x5c -'', # 0x5d -'', # 0x5e -'', # 0x5f -'', # 0x60 -'', # 0x61 -'', # 0x62 -'', # 0x63 -'', # 0x64 -'', # 0x65 -'', # 0x66 -'', # 0x67 -'', # 0x68 -'', # 0x69 -'', # 0x6a -'', # 0x6b -'', # 0x6c -'', # 0x6d -'', # 0x6e -'', # 0x6f -'', # 0x70 -'', # 0x71 -'', # 0x72 -'', # 0x73 -'', # 0x74 -'', # 0x75 -'', # 0x76 -'', # 0x77 -'', # 0x78 -'', # 0x79 -'', # 0x7a -'', # 0x7b -'', # 0x7c -'', # 0x7d -'', # 0x7e -'', # 0x7f -'', # 0x80 -'', # 0x81 -'', # 0x82 -'', # 0x83 -'', # 0x84 -'', # 0x85 -'', # 0x86 -'', # 0x87 -'', # 0x88 -'', # 0x89 -'', # 0x8a -'', # 0x8b -'', # 0x8c -'', # 0x8d -'', # 0x8e -'', # 0x8f -'', # 0x90 -'', # 0x91 -'', # 0x92 -'', # 0x93 -'', # 0x94 -'', # 0x95 -'', # 0x96 -'', # 0x97 -'', # 0x98 -'', # 0x99 -'', # 0x9a -'', # 0x9b -'', # 0x9c -'', # 0x9d -'', # 0x9e -'', # 0x9f -'', # 0xa0 -'', # 0xa1 -'', # 0xa2 -'', # 0xa3 -'', # 0xa4 -'', # 0xa5 -'', # 0xa6 -'', # 0xa7 -'', # 0xa8 -'', # 0xa9 -'', # 0xaa -'', # 0xab -'', # 0xac -'', # 0xad -'', # 0xae -'', # 0xaf -'', # 0xb0 -'', # 0xb1 -'', # 0xb2 -'', # 0xb3 -'', # 0xb4 -'', # 0xb5 -'', # 0xb6 -'', # 0xb7 -'', # 0xb8 -'', # 0xb9 -'', # 0xba -'', # 0xbb -'', # 0xbc -'', # 0xbd -'', # 0xbe -'', # 0xbf -'', # 0xc0 -'', # 0xc1 -'', # 0xc2 -'', # 0xc3 -'', # 0xc4 -'', # 0xc5 -'', # 0xc6 -'', # 0xc7 -'', # 0xc8 -'', # 0xc9 -'', # 0xca -'', # 0xcb -'', # 0xcc -'', # 0xcd +None, # 0x00 +None, # 0x01 +None, # 0x02 +None, # 0x03 +None, # 0x04 +None, # 0x05 +None, # 0x06 +None, # 0x07 +None, # 0x08 +None, # 0x09 +None, # 0x0a +None, # 0x0b +None, # 0x0c +None, # 0x0d +None, # 0x0e +None, # 0x0f +None, # 0x10 +None, # 0x11 +None, # 0x12 +None, # 0x13 +None, # 0x14 +None, # 0x15 +None, # 0x16 +None, # 0x17 +None, # 0x18 +None, # 0x19 +None, # 0x1a +None, # 0x1b +None, # 0x1c +None, # 0x1d +None, # 0x1e +None, # 0x1f +None, # 0x20 +None, # 0x21 +None, # 0x22 +None, # 0x23 +None, # 0x24 +None, # 0x25 +None, # 0x26 +None, # 0x27 +None, # 0x28 +None, # 0x29 +None, # 0x2a +None, # 0x2b +None, # 0x2c +None, # 0x2d +None, # 0x2e +None, # 0x2f +None, # 0x30 +None, # 0x31 +None, # 0x32 +None, # 0x33 +None, # 0x34 +None, # 0x35 +None, # 0x36 +None, # 0x37 +None, # 0x38 +None, # 0x39 +None, # 0x3a +None, # 0x3b +None, # 0x3c +None, # 0x3d +None, # 0x3e +None, # 0x3f +None, # 0x40 +None, # 0x41 +None, # 0x42 +None, # 0x43 +None, # 0x44 +None, # 0x45 +None, # 0x46 +None, # 0x47 +None, # 0x48 +None, # 0x49 +None, # 0x4a +None, # 0x4b +None, # 0x4c +None, # 0x4d +None, # 0x4e +None, # 0x4f +None, # 0x50 +None, # 0x51 +None, # 0x52 +None, # 0x53 +None, # 0x54 +None, # 0x55 +None, # 0x56 +None, # 0x57 +None, # 0x58 +None, # 0x59 +None, # 0x5a +None, # 0x5b +None, # 0x5c +None, # 0x5d +None, # 0x5e +None, # 0x5f +None, # 0x60 +None, # 0x61 +None, # 0x62 +None, # 0x63 +None, # 0x64 +None, # 0x65 +None, # 0x66 +None, # 0x67 +None, # 0x68 +None, # 0x69 +None, # 0x6a +None, # 0x6b +None, # 0x6c +None, # 0x6d +None, # 0x6e +None, # 0x6f +None, # 0x70 +None, # 0x71 +None, # 0x72 +None, # 0x73 +None, # 0x74 +None, # 0x75 +None, # 0x76 +None, # 0x77 +None, # 0x78 +None, # 0x79 +None, # 0x7a +None, # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f +None, # 0x80 +None, # 0x81 +None, # 0x82 +None, # 0x83 +None, # 0x84 +None, # 0x85 +None, # 0x86 +None, # 0x87 +None, # 0x88 +None, # 0x89 +None, # 0x8a +None, # 0x8b +None, # 0x8c +None, # 0x8d +None, # 0x8e +None, # 0x8f +None, # 0x90 +None, # 0x91 +None, # 0x92 +None, # 0x93 +None, # 0x94 +None, # 0x95 +None, # 0x96 +None, # 0x97 +None, # 0x98 +None, # 0x99 +None, # 0x9a +None, # 0x9b +None, # 0x9c +None, # 0x9d +None, # 0x9e +None, # 0x9f +None, # 0xa0 +None, # 0xa1 +None, # 0xa2 +None, # 0xa3 +None, # 0xa4 +None, # 0xa5 +None, # 0xa6 +None, # 0xa7 +None, # 0xa8 +None, # 0xa9 +None, # 0xaa +None, # 0xab +None, # 0xac +None, # 0xad +None, # 0xae +None, # 0xaf +None, # 0xb0 +None, # 0xb1 +None, # 0xb2 +None, # 0xb3 +None, # 0xb4 +None, # 0xb5 +None, # 0xb6 +None, # 0xb7 +None, # 0xb8 +None, # 0xb9 +None, # 0xba +None, # 0xbb +None, # 0xbc +None, # 0xbd +None, # 0xbe +None, # 0xbf +None, # 0xc0 +None, # 0xc1 +None, # 0xc2 +None, # 0xc3 +None, # 0xc4 +None, # 0xc5 +None, # 0xc6 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd '0', # 0xce '1', # 0xcf '2', # 0xd0 diff --git a/libs/common/unidecode/x1f1.py b/libs/common/unidecode/x1f1.py index ba0481fc..2f78cf01 100644 --- a/libs/common/unidecode/x1f1.py +++ b/libs/common/unidecode/x1f1.py @@ -10,11 +10,11 @@ data = ( '7,', # 0x08 '8,', # 0x09 '9,', # 0x0a -'', # 0x0b -'', # 0x0c -'', # 0x0d -'', # 0x0e -'', # 0x0f +None, # 0x0b +None, # 0x0c +None, # 0x0d +None, # 0x0e +None, # 0x0f '(A)', # 0x10 '(B)', # 0x11 '(C)', # 0x12 @@ -41,218 +41,218 @@ data = ( '(X)', # 0x27 '(Y)', # 0x28 '(Z)', # 0x29 -'', # 0x2a -'', # 0x2b -'', # 0x2c -'', # 0x2d -'', # 0x2e -'', # 0x2f -'', # 0x30 -'', # 0x31 -'', # 0x32 -'', # 0x33 -'', # 0x34 -'', # 0x35 -'', # 0x36 -'', # 0x37 -'', # 0x38 -'', # 0x39 -'', # 0x3a -'', # 0x3b -'', # 0x3c -'', # 0x3d -'', # 0x3e -'', # 0x3f -'', # 0x40 -'', # 0x41 -'', # 0x42 -'', # 0x43 -'', # 0x44 -'', # 0x45 -'', # 0x46 -'', # 0x47 -'', # 0x48 -'', # 0x49 -'', # 0x4a -'', # 0x4b -'', # 0x4c -'', # 0x4d -'', # 0x4e -'', # 0x4f -'', # 0x50 -'', # 0x51 -'', # 0x52 -'', # 0x53 -'', # 0x54 -'', # 0x55 -'', # 0x56 -'', # 0x57 -'', # 0x58 -'', # 0x59 -'', # 0x5a -'', # 0x5b -'', # 0x5c -'', # 0x5d -'', # 0x5e -'', # 0x5f -'', # 0x60 -'', # 0x61 -'', # 0x62 -'', # 0x63 -'', # 0x64 -'', # 0x65 -'', # 0x66 -'', # 0x67 -'', # 0x68 -'', # 0x69 -'', # 0x6a -'', # 0x6b -'', # 0x6c -'', # 0x6d -'', # 0x6e -'', # 0x6f -'', # 0x70 -'', # 0x71 -'', # 0x72 -'', # 0x73 -'', # 0x74 -'', # 0x75 -'', # 0x76 -'', # 0x77 -'', # 0x78 -'', # 0x79 -'', # 0x7a -'', # 0x7b -'', # 0x7c -'', # 0x7d -'', # 0x7e -'', # 0x7f -'', # 0x80 -'', # 0x81 -'', # 0x82 -'', # 0x83 -'', # 0x84 -'', # 0x85 -'', # 0x86 -'', # 0x87 -'', # 0x88 -'', # 0x89 -'', # 0x8a -'', # 0x8b -'', # 0x8c -'', # 0x8d -'', # 0x8e -'', # 0x8f -'', # 0x90 -'', # 0x91 -'', # 0x92 -'', # 0x93 -'', # 0x94 -'', # 0x95 -'', # 0x96 -'', # 0x97 -'', # 0x98 -'', # 0x99 -'', # 0x9a -'', # 0x9b -'', # 0x9c -'', # 0x9d -'', # 0x9e -'', # 0x9f -'', # 0xa0 -'', # 0xa1 -'', # 0xa2 -'', # 0xa3 -'', # 0xa4 -'', # 0xa5 -'', # 0xa6 -'', # 0xa7 -'', # 0xa8 -'', # 0xa9 -'', # 0xaa -'', # 0xab -'', # 0xac -'', # 0xad -'', # 0xae -'', # 0xaf -'', # 0xb0 -'', # 0xb1 -'', # 0xb2 -'', # 0xb3 -'', # 0xb4 -'', # 0xb5 -'', # 0xb6 -'', # 0xb7 -'', # 0xb8 -'', # 0xb9 -'', # 0xba -'', # 0xbb -'', # 0xbc -'', # 0xbd -'', # 0xbe -'', # 0xbf -'', # 0xc0 -'', # 0xc1 -'', # 0xc2 -'', # 0xc3 -'', # 0xc4 -'', # 0xc5 -'', # 0xc6 -'', # 0xc7 -'', # 0xc8 -'', # 0xc9 -'', # 0xca -'', # 0xcb -'', # 0xcc -'', # 0xcd -'', # 0xce -'', # 0xcf -'', # 0xd0 -'', # 0xd1 -'', # 0xd2 -'', # 0xd3 -'', # 0xd4 -'', # 0xd5 -'', # 0xd6 -'', # 0xd7 -'', # 0xd8 -'', # 0xd9 -'', # 0xda -'', # 0xdb -'', # 0xdc -'', # 0xdd -'', # 0xde -'', # 0xdf -'', # 0xe0 -'', # 0xe1 -'', # 0xe2 -'', # 0xe3 -'', # 0xe4 -'', # 0xe5 -'', # 0xe6 -'', # 0xe7 -'', # 0xe8 -'', # 0xe9 -'', # 0xea -'', # 0xeb -'', # 0xec -'', # 0xed -'', # 0xee -'', # 0xef -'', # 0xf0 -'', # 0xf1 -'', # 0xf2 -'', # 0xf3 -'', # 0xf4 -'', # 0xf5 -'', # 0xf6 -'', # 0xf7 -'', # 0xf8 -'', # 0xf9 -'', # 0xfa -'', # 0xfb -'', # 0xfc -'', # 0xfd -'', # 0xfe -'', # 0xff +'', # 0x2a +'(C)', # 0x2b +'(R)', # 0x2c +None, # 0x2d +None, # 0x2e +None, # 0x2f +'[A]', # 0x30 +'[B]', # 0x31 +'[C]', # 0x32 +'[D]', # 0x33 +'[E]', # 0x34 +'[F]', # 0x35 +'[G]', # 0x36 +'[H]', # 0x37 +'[I]', # 0x38 +'[J]', # 0x39 +'[K]', # 0x3a +'[L]', # 0x3b +'[M]', # 0x3c +'[N]', # 0x3d +'[O]', # 0x3e +'[P]', # 0x3f +'[Q]', # 0x40 +'[R]', # 0x41 +'[S]', # 0x42 +'[T]', # 0x43 +'[U]', # 0x44 +'[V]', # 0x45 +'[W]', # 0x46 +'[X]', # 0x47 +'[Y]', # 0x48 +'[Z]', # 0x49 +None, # 0x4a +None, # 0x4b +None, # 0x4c +None, # 0x4d +None, # 0x4e +None, # 0x4f +'(A)', # 0x50 +'(B)', # 0x51 +'(C)', # 0x52 +'(D)', # 0x53 +'(E)', # 0x54 +'(F)', # 0x55 +'(G)', # 0x56 +'(H)', # 0x57 +'(I)', # 0x58 +'(J)', # 0x59 +'(K)', # 0x5a +'(L)', # 0x5b +'(M)', # 0x5c +'(N)', # 0x5d +'(O)', # 0x5e +'(P)', # 0x5f +'(Q)', # 0x60 +'(R)', # 0x61 +'(S)', # 0x62 +'(T)', # 0x63 +'(U)', # 0x64 +'(V)', # 0x65 +'(W)', # 0x66 +'(X)', # 0x67 +'(Y)', # 0x68 +'(Z)', # 0x69 +None, # 0x6a +None, # 0x6b +None, # 0x6c +None, # 0x6d +None, # 0x6e +None, # 0x6f +'[A]', # 0x70 +'[B]', # 0x71 +'[C]', # 0x72 +'[D]', # 0x73 +'[E]', # 0x74 +'[F]', # 0x75 +'[G]', # 0x76 +'[H]', # 0x77 +'[I]', # 0x78 +'[J]', # 0x79 +'[K]', # 0x7a +'[L]', # 0x7b +'[M]', # 0x7c +'[N]', # 0x7d +'[O]', # 0x7e +'[P]', # 0x7f +'[Q]', # 0x80 +'[R]', # 0x81 +'[S]', # 0x82 +'[T]', # 0x83 +'[U]', # 0x84 +'[V]', # 0x85 +'[W]', # 0x86 +'[X]', # 0x87 +'[Y]', # 0x88 +'[Z]', # 0x89 +None, # 0x8a +None, # 0x8b +None, # 0x8c +None, # 0x8d +None, # 0x8e +None, # 0x8f +'DJ', # 0x90 +'[CL]', # 0x91 +'[COOL]', # 0x92 +'[FREE]', # 0x93 +'[ID]', # 0x94 +'[NEW]', # 0x95 +'[NG]', # 0x96 +'[OK]', # 0x97 +'[SOS]', # 0x98 +'[UP!]', # 0x99 +'[VS]', # 0x9a +'[3D]', # 0x9b +'[2nc-Scr]', # 0x9c +'[2K]', # 0x9d +'[4K]', # 0x9e +'[8K]', # 0x9f +'[5.1]', # 0xa0 +'[7.1]', # 0xa1 +'[22.2]', # 0xa2 +'[60P]', # 0xa3 +'[120P]', # 0xa4 +'[d]', # 0xa5 +'[HC]', # 0xa6 +'[HDR]', # 0xa7 +'[Hi-res]', # 0xa8 +'[Loss-less]', # 0xa9 +'[SHV]', # 0xaa +'[UHD]', # 0xab +'[VOD]', # 0xac +'(m)', # 0xad +None, # 0xae +None, # 0xaf +None, # 0xb0 +None, # 0xb1 +None, # 0xb2 +None, # 0xb3 +None, # 0xb4 +None, # 0xb5 +None, # 0xb6 +None, # 0xb7 +None, # 0xb8 +None, # 0xb9 +None, # 0xba +None, # 0xbb +None, # 0xbc +None, # 0xbd +None, # 0xbe +None, # 0xbf +None, # 0xc0 +None, # 0xc1 +None, # 0xc2 +None, # 0xc3 +None, # 0xc4 +None, # 0xc5 +None, # 0xc6 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe +None, # 0xff ) diff --git a/libs/common/unidecode/x1f6.py b/libs/common/unidecode/x1f6.py new file mode 100644 index 00000000..47eeada4 --- /dev/null +++ b/libs/common/unidecode/x1f6.py @@ -0,0 +1,258 @@ +data = ( +None, # 0x00 +None, # 0x01 +None, # 0x02 +None, # 0x03 +None, # 0x04 +None, # 0x05 +None, # 0x06 +None, # 0x07 +None, # 0x08 +None, # 0x09 +None, # 0x0a +None, # 0x0b +None, # 0x0c +None, # 0x0d +None, # 0x0e +None, # 0x0f +None, # 0x10 +None, # 0x11 +None, # 0x12 +None, # 0x13 +None, # 0x14 +None, # 0x15 +None, # 0x16 +None, # 0x17 +None, # 0x18 +None, # 0x19 +None, # 0x1a +None, # 0x1b +None, # 0x1c +None, # 0x1d +None, # 0x1e +None, # 0x1f +None, # 0x20 +None, # 0x21 +None, # 0x22 +None, # 0x23 +None, # 0x24 +None, # 0x25 +None, # 0x26 +None, # 0x27 +None, # 0x28 +None, # 0x29 +None, # 0x2a +None, # 0x2b +None, # 0x2c +None, # 0x2d +None, # 0x2e +None, # 0x2f +None, # 0x30 +None, # 0x31 +None, # 0x32 +None, # 0x33 +None, # 0x34 +None, # 0x35 +None, # 0x36 +None, # 0x37 +None, # 0x38 +None, # 0x39 +None, # 0x3a +None, # 0x3b +None, # 0x3c +None, # 0x3d +None, # 0x3e +None, # 0x3f +None, # 0x40 +None, # 0x41 +None, # 0x42 +None, # 0x43 +None, # 0x44 +None, # 0x45 +None, # 0x46 +None, # 0x47 +None, # 0x48 +None, # 0x49 +None, # 0x4a +None, # 0x4b +None, # 0x4c +None, # 0x4d +None, # 0x4e +None, # 0x4f +None, # 0x50 +None, # 0x51 +None, # 0x52 +None, # 0x53 +None, # 0x54 +None, # 0x55 +None, # 0x56 +None, # 0x57 +None, # 0x58 +None, # 0x59 +None, # 0x5a +None, # 0x5b +None, # 0x5c +None, # 0x5d +None, # 0x5e +None, # 0x5f +None, # 0x60 +None, # 0x61 +None, # 0x62 +None, # 0x63 +None, # 0x64 +None, # 0x65 +None, # 0x66 +None, # 0x67 +None, # 0x68 +None, # 0x69 +None, # 0x6a +None, # 0x6b +None, # 0x6c +None, # 0x6d +None, # 0x6e +None, # 0x6f +'et', # 0x70 +'et', # 0x71 +'et', # 0x72 +'et', # 0x73 +'&', # 0x74 +'&', # 0x75 +'"', # 0x76 +'"', # 0x77 +',,', # 0x78 +'!?', # 0x79 +'!?', # 0x7a +'!?', # 0x7b +None, # 0x7c +None, # 0x7d +None, # 0x7e +None, # 0x7f +None, # 0x80 +None, # 0x81 +None, # 0x82 +None, # 0x83 +None, # 0x84 +None, # 0x85 +None, # 0x86 +None, # 0x87 +None, # 0x88 +None, # 0x89 +None, # 0x8a +None, # 0x8b +None, # 0x8c +None, # 0x8d +None, # 0x8e +None, # 0x8f +None, # 0x90 +None, # 0x91 +None, # 0x92 +None, # 0x93 +None, # 0x94 +None, # 0x95 +None, # 0x96 +None, # 0x97 +None, # 0x98 +None, # 0x99 +None, # 0x9a +None, # 0x9b +None, # 0x9c +None, # 0x9d +None, # 0x9e +None, # 0x9f +None, # 0xa0 +None, # 0xa1 +None, # 0xa2 +None, # 0xa3 +None, # 0xa4 +None, # 0xa5 +None, # 0xa6 +None, # 0xa7 +None, # 0xa8 +None, # 0xa9 +None, # 0xaa +None, # 0xab +None, # 0xac +None, # 0xad +None, # 0xae +None, # 0xaf +None, # 0xb0 +None, # 0xb1 +None, # 0xb2 +None, # 0xb3 +None, # 0xb4 +None, # 0xb5 +None, # 0xb6 +None, # 0xb7 +None, # 0xb8 +None, # 0xb9 +None, # 0xba +None, # 0xbb +None, # 0xbc +None, # 0xbd +None, # 0xbe +None, # 0xbf +None, # 0xc0 +None, # 0xc1 +None, # 0xc2 +None, # 0xc3 +None, # 0xc4 +None, # 0xc5 +None, # 0xc6 +None, # 0xc7 +None, # 0xc8 +None, # 0xc9 +None, # 0xca +None, # 0xcb +None, # 0xcc +None, # 0xcd +None, # 0xce +None, # 0xcf +None, # 0xd0 +None, # 0xd1 +None, # 0xd2 +None, # 0xd3 +None, # 0xd4 +None, # 0xd5 +None, # 0xd6 +None, # 0xd7 +None, # 0xd8 +None, # 0xd9 +None, # 0xda +None, # 0xdb +None, # 0xdc +None, # 0xdd +None, # 0xde +None, # 0xdf +None, # 0xe0 +None, # 0xe1 +None, # 0xe2 +None, # 0xe3 +None, # 0xe4 +None, # 0xe5 +None, # 0xe6 +None, # 0xe7 +None, # 0xe8 +None, # 0xe9 +None, # 0xea +None, # 0xeb +None, # 0xec +None, # 0xed +None, # 0xee +None, # 0xef +None, # 0xf0 +None, # 0xf1 +None, # 0xf2 +None, # 0xf3 +None, # 0xf4 +None, # 0xf5 +None, # 0xf6 +None, # 0xf7 +None, # 0xf8 +None, # 0xf9 +None, # 0xfa +None, # 0xfb +None, # 0xfc +None, # 0xfd +None, # 0xfe +None, # 0xff +) diff --git a/libs/common/yaml/__init__.py b/libs/common/yaml/__init__.py index 9e35fe29..465041dc 100644 --- a/libs/common/yaml/__init__.py +++ b/libs/common/yaml/__init__.py @@ -8,7 +8,7 @@ from .nodes import * from .loader import * from .dumper import * -__version__ = '3.13' +__version__ = '6.0' try: from .cyaml import * __with_libyaml__ = True @@ -17,6 +17,15 @@ except ImportError: import io +#------------------------------------------------------------------------------ +# XXX "Warnings control" is now deprecated. Leaving in the API function to not +# break code that uses it. +#------------------------------------------------------------------------------ +def warnings(settings=None): + if settings is None: + return {} + +#------------------------------------------------------------------------------ def scan(stream, Loader=Loader): """ Scan a YAML stream and produce scanning tokens. @@ -62,7 +71,7 @@ def compose_all(stream, Loader=Loader): finally: loader.dispose() -def load(stream, Loader=Loader): +def load(stream, Loader): """ Parse the first YAML document in a stream and produce the corresponding Python object. @@ -73,7 +82,7 @@ def load(stream, Loader=Loader): finally: loader.dispose() -def load_all(stream, Loader=Loader): +def load_all(stream, Loader): """ Parse all YAML documents in a stream and produce corresponding Python objects. @@ -85,11 +94,33 @@ def load_all(stream, Loader=Loader): finally: loader.dispose() +def full_load(stream): + """ + Parse the first YAML document in a stream + and produce the corresponding Python object. + + Resolve all tags except those known to be + unsafe on untrusted input. + """ + return load(stream, FullLoader) + +def full_load_all(stream): + """ + Parse all YAML documents in a stream + and produce corresponding Python objects. + + Resolve all tags except those known to be + unsafe on untrusted input. + """ + return load_all(stream, FullLoader) + def safe_load(stream): """ Parse the first YAML document in a stream and produce the corresponding Python object. - Resolve only basic YAML tags. + + Resolve only basic YAML tags. This is known + to be safe for untrusted input. """ return load(stream, SafeLoader) @@ -97,10 +128,32 @@ def safe_load_all(stream): """ Parse all YAML documents in a stream and produce corresponding Python objects. - Resolve only basic YAML tags. + + Resolve only basic YAML tags. This is known + to be safe for untrusted input. """ return load_all(stream, SafeLoader) +def unsafe_load(stream): + """ + Parse the first YAML document in a stream + and produce the corresponding Python object. + + Resolve all tags, even those known to be + unsafe on untrusted input. + """ + return load(stream, UnsafeLoader) + +def unsafe_load_all(stream): + """ + Parse all YAML documents in a stream + and produce corresponding Python objects. + + Resolve all tags, even those known to be + unsafe on untrusted input. + """ + return load_all(stream, UnsafeLoader) + def emit(events, stream=None, Dumper=Dumper, canonical=None, indent=None, width=None, allow_unicode=None, line_break=None): @@ -160,11 +213,11 @@ def serialize(node, stream=None, Dumper=Dumper, **kwds): return serialize_all([node], stream, Dumper=Dumper, **kwds) def dump_all(documents, stream=None, Dumper=Dumper, - default_style=None, default_flow_style=None, + default_style=None, default_flow_style=False, canonical=None, indent=None, width=None, allow_unicode=None, line_break=None, encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None): + version=None, tags=None, sort_keys=True): """ Serialize a sequence of Python objects into a YAML stream. If stream is None, return the produced string instead. @@ -181,7 +234,7 @@ def dump_all(documents, stream=None, Dumper=Dumper, canonical=canonical, indent=indent, width=width, allow_unicode=allow_unicode, line_break=line_break, encoding=encoding, version=version, tags=tags, - explicit_start=explicit_start, explicit_end=explicit_end) + explicit_start=explicit_start, explicit_end=explicit_end, sort_keys=sort_keys) try: dumper.open() for data in documents: @@ -216,42 +269,62 @@ def safe_dump(data, stream=None, **kwds): return dump_all([data], stream, Dumper=SafeDumper, **kwds) def add_implicit_resolver(tag, regexp, first=None, - Loader=Loader, Dumper=Dumper): + Loader=None, Dumper=Dumper): """ Add an implicit scalar detector. If an implicit scalar value matches the given regexp, the corresponding tag is assigned to the scalar. first is a sequence of possible initial characters or None. """ - Loader.add_implicit_resolver(tag, regexp, first) + if Loader is None: + loader.Loader.add_implicit_resolver(tag, regexp, first) + loader.FullLoader.add_implicit_resolver(tag, regexp, first) + loader.UnsafeLoader.add_implicit_resolver(tag, regexp, first) + else: + Loader.add_implicit_resolver(tag, regexp, first) Dumper.add_implicit_resolver(tag, regexp, first) -def add_path_resolver(tag, path, kind=None, Loader=Loader, Dumper=Dumper): +def add_path_resolver(tag, path, kind=None, Loader=None, Dumper=Dumper): """ Add a path based resolver for the given tag. A path is a list of keys that forms a path to a node in the representation tree. Keys can be string values, integers, or None. """ - Loader.add_path_resolver(tag, path, kind) + if Loader is None: + loader.Loader.add_path_resolver(tag, path, kind) + loader.FullLoader.add_path_resolver(tag, path, kind) + loader.UnsafeLoader.add_path_resolver(tag, path, kind) + else: + Loader.add_path_resolver(tag, path, kind) Dumper.add_path_resolver(tag, path, kind) -def add_constructor(tag, constructor, Loader=Loader): +def add_constructor(tag, constructor, Loader=None): """ Add a constructor for the given tag. Constructor is a function that accepts a Loader instance and a node object and produces the corresponding Python object. """ - Loader.add_constructor(tag, constructor) + if Loader is None: + loader.Loader.add_constructor(tag, constructor) + loader.FullLoader.add_constructor(tag, constructor) + loader.UnsafeLoader.add_constructor(tag, constructor) + else: + Loader.add_constructor(tag, constructor) -def add_multi_constructor(tag_prefix, multi_constructor, Loader=Loader): +def add_multi_constructor(tag_prefix, multi_constructor, Loader=None): """ Add a multi-constructor for the given tag prefix. Multi-constructor is called for a node if its tag starts with tag_prefix. Multi-constructor accepts a Loader instance, a tag suffix, and a node object and produces the corresponding Python object. """ - Loader.add_multi_constructor(tag_prefix, multi_constructor) + if Loader is None: + loader.Loader.add_multi_constructor(tag_prefix, multi_constructor) + loader.FullLoader.add_multi_constructor(tag_prefix, multi_constructor) + loader.UnsafeLoader.add_multi_constructor(tag_prefix, multi_constructor) + else: + Loader.add_multi_constructor(tag_prefix, multi_constructor) def add_representer(data_type, representer, Dumper=Dumper): """ @@ -278,7 +351,12 @@ class YAMLObjectMetaclass(type): def __init__(cls, name, bases, kwds): super(YAMLObjectMetaclass, cls).__init__(name, bases, kwds) if 'yaml_tag' in kwds and kwds['yaml_tag'] is not None: - cls.yaml_loader.add_constructor(cls.yaml_tag, cls.from_yaml) + if isinstance(cls.yaml_loader, list): + for loader in cls.yaml_loader: + loader.add_constructor(cls.yaml_tag, cls.from_yaml) + else: + cls.yaml_loader.add_constructor(cls.yaml_tag, cls.from_yaml) + cls.yaml_dumper.add_representer(cls, cls.to_yaml) class YAMLObject(metaclass=YAMLObjectMetaclass): @@ -289,7 +367,7 @@ class YAMLObject(metaclass=YAMLObjectMetaclass): __slots__ = () # no direct instantiation, so allow immutable subclasses - yaml_loader = Loader + yaml_loader = [Loader, FullLoader, UnsafeLoader] yaml_dumper = Dumper yaml_tag = None diff --git a/libs/common/yaml/_yaml.cp37-win_amd64.pyd b/libs/common/yaml/_yaml.cp37-win_amd64.pyd new file mode 100644 index 0000000000000000000000000000000000000000..52e813be3c82134d511fe7191f274c4ea0e00797 GIT binary patch literal 264192 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\'\"%@`': + if ch in '#,[]{}&*!|>\'\"%@`': flow_indicators = True block_indicators = True if ch in '?:': @@ -689,7 +689,7 @@ class Emitter: flow_indicators = True if followed_by_whitespace: block_indicators = True - if ch == '#' and preceeded_by_whitespace: + if ch == '#' and preceded_by_whitespace: flow_indicators = True block_indicators = True @@ -698,7 +698,8 @@ class Emitter: line_breaks = True if not (ch == '\n' or '\x20' <= ch <= '\x7E'): if (ch == '\x85' or '\xA0' <= ch <= '\uD7FF' - or '\uE000' <= ch <= '\uFFFD') and ch != '\uFEFF': + or '\uE000' <= ch <= '\uFFFD' + or '\U00010000' <= ch < '\U0010ffff') and ch != '\uFEFF': unicode_characters = True if not self.allow_unicode: special_characters = True @@ -730,7 +731,7 @@ class Emitter: # Prepare for the next character. index += 1 - preceeded_by_whitespace = (ch in '\0 \t\r\n\x85\u2028\u2029') + preceded_by_whitespace = (ch in '\0 \t\r\n\x85\u2028\u2029') followed_by_whitespace = (index+1 >= len(scalar) or scalar[index+1] in '\0 \t\r\n\x85\u2028\u2029') @@ -1134,4 +1135,3 @@ class Emitter: spaces = (ch == ' ') breaks = (ch in '\n\x85\u2028\u2029') end += 1 - diff --git a/libs/common/yaml/loader.py b/libs/common/yaml/loader.py index 08c8f01b..e90c1122 100644 --- a/libs/common/yaml/loader.py +++ b/libs/common/yaml/loader.py @@ -1,5 +1,5 @@ -__all__ = ['BaseLoader', 'SafeLoader', 'Loader'] +__all__ = ['BaseLoader', 'FullLoader', 'SafeLoader', 'Loader', 'UnsafeLoader'] from .reader import * from .scanner import * @@ -18,6 +18,16 @@ class BaseLoader(Reader, Scanner, Parser, Composer, BaseConstructor, BaseResolve BaseConstructor.__init__(self) BaseResolver.__init__(self) +class FullLoader(Reader, Scanner, Parser, Composer, FullConstructor, Resolver): + + def __init__(self, stream): + Reader.__init__(self, stream) + Scanner.__init__(self) + Parser.__init__(self) + Composer.__init__(self) + FullConstructor.__init__(self) + Resolver.__init__(self) + class SafeLoader(Reader, Scanner, Parser, Composer, SafeConstructor, Resolver): def __init__(self, stream): @@ -38,3 +48,16 @@ class Loader(Reader, Scanner, Parser, Composer, Constructor, Resolver): Constructor.__init__(self) Resolver.__init__(self) +# UnsafeLoader is the same as Loader (which is and was always unsafe on +# untrusted input). Use of either Loader or UnsafeLoader should be rare, since +# FullLoad should be able to load almost all YAML safely. Loader is left intact +# to ensure backwards compatibility. +class UnsafeLoader(Reader, Scanner, Parser, Composer, Constructor, Resolver): + + def __init__(self, stream): + Reader.__init__(self, stream) + Scanner.__init__(self) + Parser.__init__(self) + Composer.__init__(self) + Constructor.__init__(self) + Resolver.__init__(self) diff --git a/libs/common/yaml/reader.py b/libs/common/yaml/reader.py index f70e920f..774b0219 100644 --- a/libs/common/yaml/reader.py +++ b/libs/common/yaml/reader.py @@ -134,7 +134,7 @@ class Reader(object): self.encoding = 'utf-8' self.update(1) - NON_PRINTABLE = re.compile('[^\x09\x0A\x0D\x20-\x7E\x85\xA0-\uD7FF\uE000-\uFFFD]') + NON_PRINTABLE = re.compile('[^\x09\x0A\x0D\x20-\x7E\x85\xA0-\uD7FF\uE000-\uFFFD\U00010000-\U0010ffff]') def check_printable(self, data): match = self.NON_PRINTABLE.search(data) if match: @@ -183,10 +183,3 @@ class Reader(object): self.stream_pointer += len(data) if not data: self.eof = True - -#try: -# import psyco -# psyco.bind(Reader) -#except ImportError: -# pass - diff --git a/libs/common/yaml/representer.py b/libs/common/yaml/representer.py index b9e65c51..808ca06d 100644 --- a/libs/common/yaml/representer.py +++ b/libs/common/yaml/representer.py @@ -5,7 +5,7 @@ __all__ = ['BaseRepresenter', 'SafeRepresenter', 'Representer', from .error import * from .nodes import * -import datetime, sys, copyreg, types, base64, collections +import datetime, copyreg, types, base64, collections class RepresenterError(YAMLError): pass @@ -15,8 +15,9 @@ class BaseRepresenter: yaml_representers = {} yaml_multi_representers = {} - def __init__(self, default_style=None, default_flow_style=None): + def __init__(self, default_style=None, default_flow_style=False, sort_keys=True): self.default_style = default_style + self.sort_keys = sort_keys self.default_flow_style = default_flow_style self.represented_objects = {} self.object_keeper = [] @@ -107,10 +108,11 @@ class BaseRepresenter: best_style = True if hasattr(mapping, 'items'): mapping = list(mapping.items()) - try: - mapping = sorted(mapping) - except TypeError: - pass + if self.sort_keys: + try: + mapping = sorted(mapping) + except TypeError: + pass for item_key, item_value in mapping: node_key = self.represent_data(item_key) node_value = self.represent_data(item_value) @@ -226,7 +228,7 @@ class SafeRepresenter(BaseRepresenter): return self.represent_mapping(tag, state, flow_style=flow_style) def represent_undefined(self, data): - raise RepresenterError("cannot represent an object: %s" % data) + raise RepresenterError("cannot represent an object", data) SafeRepresenter.add_representer(type(None), SafeRepresenter.represent_none) @@ -316,7 +318,7 @@ class Representer(SafeRepresenter): elif hasattr(data, '__reduce__'): reduce = data.__reduce__() else: - raise RepresenterError("cannot represent object: %r" % data) + raise RepresenterError("cannot represent an object", data) reduce = (list(reduce)+[None]*5)[:5] function, args, state, listitems, dictitems = reduce args = list(args) @@ -367,7 +369,7 @@ Representer.add_representer(complex, Representer.add_representer(tuple, Representer.represent_tuple) -Representer.add_representer(type, +Representer.add_multi_representer(type, Representer.represent_name) Representer.add_representer(collections.OrderedDict, diff --git a/libs/common/yaml/resolver.py b/libs/common/yaml/resolver.py index 02b82e73..3522bdaa 100644 --- a/libs/common/yaml/resolver.py +++ b/libs/common/yaml/resolver.py @@ -146,8 +146,8 @@ class BaseResolver: resolvers = self.yaml_implicit_resolvers.get('', []) else: resolvers = self.yaml_implicit_resolvers.get(value[0], []) - resolvers += self.yaml_implicit_resolvers.get(None, []) - for tag, regexp in resolvers: + wildcard_resolvers = self.yaml_implicit_resolvers.get(None, []) + for tag, regexp in resolvers + wildcard_resolvers: if regexp.match(value): return tag implicit = implicit[1] @@ -177,7 +177,7 @@ Resolver.add_implicit_resolver( Resolver.add_implicit_resolver( 'tag:yaml.org,2002:float', re.compile(r'''^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+][0-9]+)? - |\.[0-9_]+(?:[eE][-+][0-9]+)? + |\.[0-9][0-9_]*(?:[eE][-+][0-9]+)? |[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]* |[-+]?\.(?:inf|Inf|INF) |\.(?:nan|NaN|NAN))$''', re.X), diff --git a/libs/common/yaml/scanner.py b/libs/common/yaml/scanner.py index c8d127b8..de925b07 100644 --- a/libs/common/yaml/scanner.py +++ b/libs/common/yaml/scanner.py @@ -124,10 +124,13 @@ class Scanner: def peek_token(self): # Return the next token, but do not delete if from the queue. + # Return None if no more tokens. while self.need_more_tokens(): self.fetch_more_tokens() if self.tokens: return self.tokens[0] + else: + return None def get_token(self): # Return the next token. @@ -329,7 +332,7 @@ class Scanner: ## } #if self.flow_level and self.indent > column: # raise ScannerError(None, None, - # "invalid intendation or unclosed '[' or '{'", + # "invalid indentation or unclosed '[' or '{'", # self.get_mark()) # In the flow context, indentation is ignored. We make the scanner less @@ -367,7 +370,7 @@ class Scanner: def fetch_stream_end(self): - # Set the current intendation to -1. + # Set the current indentation to -1. self.unwind_indent(-1) # Reset simple keys. @@ -386,7 +389,7 @@ class Scanner: def fetch_directive(self): - # Set the current intendation to -1. + # Set the current indentation to -1. self.unwind_indent(-1) # Reset simple keys. @@ -404,7 +407,7 @@ class Scanner: def fetch_document_indicator(self, TokenClass): - # Set the current intendation to -1. + # Set the current indentation to -1. self.unwind_indent(-1) # Reset simple keys. Note that there could not be a block collection @@ -516,7 +519,7 @@ class Scanner: # Block context needs additional checks. if not self.flow_level: - # Are we allowed to start a key (not nessesary a simple)? + # Are we allowed to start a key (not necessary a simple)? if not self.allow_simple_key: raise ScannerError(None, None, "mapping keys are not allowed here", @@ -564,7 +567,7 @@ class Scanner: else: # Block context needs additional checks. - # (Do we really need them? They will be catched by the parser + # (Do we really need them? They will be caught by the parser # anyway.) if not self.flow_level: @@ -897,7 +900,7 @@ class Scanner: # The specification does not restrict characters for anchors and # aliases. This may lead to problems, for instance, the document: # [ *alias, value ] - # can be interpteted in two ways, as + # can be interpreted in two ways, as # [ "value" ] # and # [ *alias , "value" ] @@ -1166,6 +1169,7 @@ class Scanner: ' ': '\x20', '\"': '\"', '\\': '\\', + '/': '/', 'N': '\x85', '_': '\xA0', 'L': '\u2028', @@ -1207,7 +1211,7 @@ class Scanner: for k in range(length): if self.peek(k) not in '0123456789ABCDEFabcdef': raise ScannerError("while scanning a double-quoted scalar", start_mark, - "expected escape sequence of %d hexdecimal numbers, but found %r" % + "expected escape sequence of %d hexadecimal numbers, but found %r" % (length, self.peek(k)), self.get_mark()) code = int(self.prefix(length), 16) chunks.append(chr(code)) @@ -1266,7 +1270,7 @@ class Scanner: def scan_plain(self): # See the specification for details. # We add an additional restriction for the flow context: - # plain scalars in the flow context cannot contain ',', ':' and '?'. + # plain scalars in the flow context cannot contain ',' or '?'. # We also keep track of the `allow_simple_key` flag here. # Indentation rules are loosed for the flow context. chunks = [] @@ -1285,18 +1289,12 @@ class Scanner: while True: ch = self.peek(length) if ch in '\0 \t\r\n\x85\u2028\u2029' \ - or (not self.flow_level and ch == ':' and - self.peek(length+1) in '\0 \t\r\n\x85\u2028\u2029') \ - or (self.flow_level and ch in ',:?[]{}'): + or (ch == ':' and + self.peek(length+1) in '\0 \t\r\n\x85\u2028\u2029' + + (u',[]{}' if self.flow_level else u''))\ + or (self.flow_level and ch in ',?[]{}'): break length += 1 - # It's not clear what we should do with ':' in the flow context. - if (self.flow_level and ch == ':' - and self.peek(length+1) not in '\0 \t\r\n\x85\u2028\u2029,[]{}'): - self.forward(length) - raise ScannerError("while scanning a plain scalar", start_mark, - "found unexpected ':'", self.get_mark(), - "Please check http://pyyaml.org/wiki/YAMLColonInFlowContext for details.") if length == 0: break self.allow_simple_key = False @@ -1405,7 +1403,7 @@ class Scanner: for k in range(2): if self.peek(k) not in '0123456789ABCDEFabcdef': raise ScannerError("while scanning a %s" % name, start_mark, - "expected URI escape sequence of 2 hexdecimal numbers, but found %r" + "expected URI escape sequence of 2 hexadecimal numbers, but found %r" % self.peek(k), self.get_mark()) codes.append(int(self.prefix(2), 16)) self.forward(2) @@ -1435,10 +1433,3 @@ class Scanner: self.forward() return ch return '' - -#try: -# import psyco -# psyco.bind(Scanner) -#except ImportError: -# pass -