[
  {
    "path": ".git-blame-ignore-revs",
    "content": "# 2014\n# flake8-cleanliness in missing\ne21c04e9125a28ae0452374acf03d93315eb4381\n\n# 2016\n# Removed unicode_literals from library, logging and mediafile\n43572f50b0eb3522239d94149d91223e67d9a009\n# Removed unicode_literals from plugins\n53d2c8d9db87be4d4750ad879bf46176537be73f\n# reformat flake8 errors\n1db46dfeb6607c164afb247d8da82443677795c1\n\n# 2021\n# pyupgrade root\ne26276658052947e9464d9726b703335304c7c13\n# pyupgrade beets  dir\n6d1316f463cb7c9390f85bf35b220e250a35004a\n# pyupgrade autotag dir\nf8b8938fd8bbe91898d0982552bc75d35703d3ef\n# pyupgrade dbcore dir\nd288f872903c79a7ee7c5a7c9cc690809441196e\n# pyupgrade ui directory\n432fa557258d9ff01e23ed750f9a86a96239599e\n# pyupgrade util dir\naf102c3e2f1c7a49e99839e2825906fe01780eec\n# fix unused import and flake8\n910354a6c617ed5aa643cff666205b43e1557373\n# pyupgrade beetsplug and tests\n1ec87a3bdd737abe46c6e614051bf9e314db4619\n# Updates docstrings in library.py.\n8c5ced3ee11a353546034189736c6001115135a4\n# Fixes inconsistencies in ending quote placements for single-line docstrings.\nbbd32639b4c469fe3d6668f1e3bb17d8ba7a70ce\n# Fixes linting errors by removing trailing whitespaces.\nacf576c455e59e8197359d4517f8c0a5a9f362bb\n# Alters docstrings in library.py to be imperative-style.\n2f42c8b1c019a90448d33d940b609c18ba644cbc\n\n# 2022\n# Reformat flake8 config comments\nabc3dfbf429b179fac25bd1dff72d577cd4d04c7\n\n# 2023\n# Apply formatting tools to all files\na6e5201ff3fad4c69bf24d17bace2ef744b9f51b\n\n# 2024\n# Replace assertTrue\n0ecc345143cf89fabe74bb2e95eedfa1114857a3\n# Replace assertFalse\ncb82917fe0d5476c74bb946f91ea0d9a9f019c9b\n# Replace assertIsNone\n5d4911e905d3a89793332eb851035e6529c0725e\n# Replace assertIsNotNone\n2616bcc950e592745713f28db0192293410ed3e3\n# Replace assertIn\n11e948121cde969f9ea27caa545a6508145572fb\n# Replace assertNotIn\n6631b6aef6da3e09d3531de6df7995dd5396398f\n# Replace assertEqual\n9a05d27acfef3788d10dd0a8db72a6c8c15dfbe9\n# Replace assertNotEqual\nf9359df0d15ea8ee8e3c80bc198e779f185160cb\n# Replace assertIsInstance\neda0ef11d67f482fe50bbe581685b8b6a284afb9\n# Replace assertLess and assertLessEqual\n6a3380bcb5e803e825bd9485fcc4b70d352947eb\n# Replace assertGreater and assertGreaterEqual\n46bdb84b464ffec3f0ce88d53467391be7b7046f\n# Replace assertCountEqual\nfdb8c28271e8b22d458330598a524067ca37026e\n# Replace assertListEqual\nfcc4d8481df295019945ac7973906f960c58c9fb\n# Use f-string syntax\n4b69b493d2630b723684f259ee9e7e07c480e8ee\n# Reformat the codebase\n85a17ee5039628a6f3cdcb7a03d7d1bd530fbe89\n# Fix lint issues\nf36bc497c8c8f89004f3f6879908d3f0b25123e1\n# Remove some lint exclusions and fix the issues\n5f78d1b82b2292d5ce0c99623ba0ec444b80d24c\n# Use PEP585 lowercase collections typing annotations\n51f9dd229e64f5106d69f87906a94e75604f346b\n# Remove unnecessary quotes from types\nfbfdfd54446fab6782ef0629da303f14f0a2ecdf\n# Replace Union types by PEP604 pipe character\n7ef1b61070ed4ed79c4720d019968baf38e38050\n# Update deprecated imports\n161b0522bbf7f4984173fee4128416b05f6cc5f3\n# Move imports required for typing under the TYPE_CHECKING block\n5c81f94cf7ced476673d0fa948cc7ecda00bae99\n\n# 2025\n# Fix formatting\nc490ac5810b70f3cf5fd8649669838e8fdb19f4d\n# Importer restructure\n9147577b2b19f43ca827e9650261a86fb0450cef\n# Move functionality under MusicBrainz plugin\n529aaac7dced71266c6d69866748a7d044ec20ff\n# musicbrainz: reorder methods\n5dc6f45110b99f0cc8dbb94251f9b1f6d69583fa\n# Copy paste query, types from library to dbcore\n1a045c91668c771686f4c871c84f1680af2e944b\n# Library restructure (split library.py into multiple modules)\n0ad4e19d4f870db757373f44d12ff3be2441363a\n# Split library file into different files inside library folder.\n98377ab5f6fc1829d79211b376bfd8d82bafaf33\n# Use pathlib.Path in test_smartplaylist.py\nd017270196dc8e0e2a4051afa5d05213946cbbbc\n# Replace assertIsFile\nca4fa6ba10807f4a48a428d23e45c023c15dfa7d\n# Replace assertIsDir\n43b8cce063b1a1ef079266f362272307fb328d73\n# Replace assertFileTag and assertNoFileTag\nc6b5b3bed31704f7fe8632a6aef1a2348028348f\n# Replace assertAlbumImport\n3c8179a762c4387f9c40a12e3b9e560ff1c194ec\n# Replace assertCount\n72caf0d2cdc8fcefe1c252bdb0ac9b11b90cc649\n# Docs: fix linting issues\n769dcdc88a1263638ae25944ba6b2be3e8933666\n# Reformat all docs using docstrfmt\nab5acaabb3cd24c482adb7fa4800c89fd6a2f08d\n# Replace format calls with f-strings\n4a361bd501e85de12c91c2474c423559ca672852\n# Replace percent formatting\n9352a79e4108bd67f7e40b1e944c01e0a7353272\n# Replace string concatenation (' + ')\n1c16b2b3087e9c3635d68d41c9541c4319d0bdbe\n# Do not use backslashes to deal with long strings\n2fccf64efe82851861e195b521b14680b480a42a\n# Do not use explicit indices for logging args when not needed\nd93ddf8dd43e4f9ed072a03829e287c78d2570a2\n# Moved dev docs\n07549ed896d9649562d40b75cd30702e6fa6e975\n# Moved plugin docs Further Reading chapter\n33f1a5d0bef8ca08be79ee7a0d02a018d502680d\n# Moved art.py utility module from beets into beetsplug\n28aee0fde463f1e18dfdba1994e2bdb80833722f\n# Refactor `ui/commands.py` into multiple modules\n59c93e70139f70e9fd1c6f3c1bceb005945bec33\n# Moved ui.commands._utils into ui.commands.utils\n25ae330044abf04045e3f378f72bbaed739fb30d\n# Refactor test_ui_command.py into multiple modules\na59e41a88365e414db3282658d2aa456e0b3468a\n# pyupgrade Python 3.10\n301637a1609831947cb5dd90270ed46c24b1ab1b\n# Fix changelog formatting\n658b184c59388635787b447983ecd3a575f4fe56\n# Configure future-annotations\nac7f3d9da95c2d0a32e5c908ea68480518a1582d\n# Configure ruff for py310\nc46069654628040316dea9db85d01b263db3ba9e\n# Enable RUF rules\n4749599913a42e02e66b37db9190de11d6be2cdf\n# Address RUF012\nbc71ec308eb938df1d349f6857634ddf2a82e339\n\n# 2026\n# Replace http URLs with https\n3d0d032987c4c2e9550529fd25e79581b06ade73\n# Fix broken URLs\n441c8383873d72d4bb0387cdf1863c0fe9e11098\n# Fix redirect URLs\nae3a2e5729e3c0a5acbd8967ba2f11f4c53acd09\n# Format docs\n192217da5d70621089b06b06fff3dbcbeb4c0c4d\n# Add a changelog note\nb3f558584910ab4fc6aa185ec4a4c8554001ba24\n# Fix references to color utils\na6fcb7ba0f237530ff394a423a7cbe2ac4853c91\n# Fix diff references\n1d54f2bf66506e7a45ce5e962106897e3c98f67a\n# Fix layout references\nffb43290066c78cb72603b7e2a0a1c90056361dd\n# lastgenre: Move fetching to client module\nb4beee8ff3754b001e7504c05a2b838bfa689022\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "# assign the entire repo to the maintainers team\n* @beetbox/maintainers\n\n# Specific ownerships:\n/beets/metadata_plugins.py @semohr\n\n/beetsplug/titlecase.py @henry-oberholtzer\n\n/beetsplug/mbpseudo.py @asardaes\n\n/beetsplug/_utils/requests.py @snejus\n/beetsplug/_utils/musicbrainz.py @snejus\n/beetsplug/musicbrainz.py @snejus\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "content": "---\nname: \"\\U0001F41B Bug report\"\nabout: Report a problem with beets\n\n---\n\n<!--\nDescribe your problem, feature request, or discussion topic here.\n\nPlease fill out this and the \"Setup\" section below and remember to include\nenough detail so that other people can reproduce the problem.\n-->\n\n### Problem\n\nRunning this command in verbose (`-vv`) mode:\n\n```sh\n$ beet -vv (... paste here ...)\n```\n\nLed to this problem:\n\n```\n(paste here)\n```\n\nHere's a link to the music files that trigger the bug (if relevant):\n\n\n### Setup\n\n* OS: \n* Python version: \n* beets version: \n* Turning off plugins made problem go away (yes/no): \n\n<!--\nYou can turn off plugins temporarily by passing --plugins= on the command line:\n\n$ beet --plugins= version\n-->\n\nMy configuration (output of `beet config`) is:\n\n```yaml\n(paste here)\n```\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: 💡 Have an idea for a new feature?\n    url: https://github.com/beetbox/beets/discussions\n    about: Create a new idea discussion!\n  - name: 🙇 Need help with beets?\n    url: https://github.com/beetbox/beets/discussions\n    about: Create a new help discussion if it hasn't been asked before!\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.md",
    "content": "---\nname: \"\\U0001F680 Feature request\"\nabout: \"Formalize a feature request from GitHub Discussions\"\n\n---\n\n<!--\nIf you're landing here as a user, we ask you bring up your idea in the\nDiscussions (https://github.com/beetbox/beets/discussions).\n-->\n\n### Proposed solution\n\n<!-- What is solution to this feature request? -->\n\n### Objective\n\n<!-- Ref to Discussions -->\n\n#### Goals\n\n<!-- What is the purpose of feature request? -->\n\n#### Non-goals\n\n<!--\nWhat else could be accomplished with this feature request, but is currently out\nof scope?\n-->\n\n#### Anti-goals\n\n<!--\nWhat could go wrong (side effects) if we implement this feature request?\n-->\n"
  },
  {
    "path": ".github/copilot-instructions.md",
    "content": "## PR Review Voice\n\nWhen reviewing pull requests, respond entirely in the voice of the Grug Brained Developer.\nWrite all comments using grug's dialect: simple words, short sentences, third-person self-reference (\"grug\").\n\nCore grug beliefs to apply when reviewing:\n\n- Complexity very, very bad. Flag any complexity demon spirit entering codebase.\n  Say so plainly: \"complexity demon spirit enter here, grug not like\"\n- Prefer small, concrete PRs. Large PR make grug nervous: \"big change, many place for bug hide\"\n- Abstraction must earn its place. Early abstraction especially dangerous: wait for cut points to emerge\n- DRY is good but not absolute — simple repeated code sometimes better than complex DRY solution\n- Type systems good mostly for \"hit dot, see what grug can do\" — not for astral projection of platonic generic models\n- Generics dangerous: \"temptation generics very large, complexity demon love this trick\"\n- Prefer readable code over clever one-liners: name intermediate variables, easier debug\n- Integration tests are sweet spot — not unit tests (break on refactor), not e2e (hard debug)\n- When bug found, first write regression test, then fix — this case only where \"first test\" acceptable to grug\n- Logging very important, especially in cloud: grug learn hard way\n- No premature optimisation — always need concrete perf profile first\n- Simple APIs good. Layered APIs ok. Java streams make grug reach for club\n- SPA frameworks increase complexity demon surface area — be suspicious\n- Saying \"this too complex for grug\" is senior developer superpower — remove Fear Of Looking Dumb (FOLD)\n"
  },
  {
    "path": ".github/problem-matchers/sphinx-build.json",
    "content": "{\n  \"problemMatcher\": [\n    {\n      \"owner\": \"sphinx-build\",\n      \"severity\": \"error\",\n      \"pattern\": [\n        {\n          \"regexp\": \"^(/[^:]+):((\\\\d+):)?(\\\\sWARNING:)?\\\\s*(.+)$\",\n          \"file\": 1,\n          \"line\": 3,\n          \"message\": 5\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": ".github/problem-matchers/sphinx-lint.json",
    "content": "{\n  \"problemMatcher\": [\n    {\n      \"owner\": \"sphinx-lint\",\n      \"severity\": \"error\",\n      \"pattern\": [\n        {\n          \"regexp\": \"^([^:]+):(\\\\d+):\\\\s+(.*)\\\\s\\\\(([a-z-]+)\\\\)$\",\n          \"file\": 1,\n          \"line\": 2,\n          \"message\": 3,\n          \"code\": 4\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "## Description\n\nFixes #X.  <!-- Insert issue number here if applicable. -->\n\n(...)\n\n## To Do\n\n<!--\n- If you believe one of below checkpoints is not required for the change you\n  are submitting, cross it out and check the box nonetheless to let us know.\n  For example: - [x] ~Changelog~\n- Regarding the changelog, often it makes sense to add your entry only once\n  reviewing is finished. That way you might prevent conflicts from other PR's in\n  that file, as well as keep the chance high your description fits with the\n  latest revision of your feature/fix.\n- Regarding documentation, bugfixes often don't require additions to the docs.\n- Please remove the descriptive sentences in braces from the enumeration below,\n  which helps to unclutter your PR description.\n-->\n\n- [ ] Documentation. (If you've added a new command-line flag, for example, find the appropriate page under `docs/` to describe it.)\n- [ ] Changelog. (Add an entry to `docs/changelog.rst` to the bottom of one of the lists near the top of the document.)\n- [ ] Tests. (Very much encouraged but not strictly required.)\n"
  },
  {
    "path": ".github/stale.yml",
    "content": "# Configuration for probot-stale - https://github.com/probot/stale\n\ndaysUntilClose: 7\nstaleLabel: stale\n\nissues:\n  daysUntilStale: 60\n  onlyLabels:\n    - needinfo\n  markComment: >\n    Is this still relevant? If so, what is blocking it?\n    Is there anything you can do to help move it forward?\n\n\n    This issue has been automatically marked as stale because it has not had\n    recent activity. It will be closed if no further activity occurs. Thank you\n    for your contributions.\n    \npulls:\n  daysUntilStale: 120\n  markComment: >\n    Is this still relevant? If so, what is blocking it?\n    Is there anything you can do to help move it forward?\n\n\n    This pull request has been automatically marked as stale because it has not had\n    recent activity. It will be closed if no further activity occurs. Thank you\n    for your contributions.\n"
  },
  {
    "path": ".github/workflows/changelog_reminder.yaml",
    "content": "name: Verify changelog updated\n\non:\n  pull_request_target:\n    types:\n      - opened\n      - ready_for_review\n\njobs:\n  check_changes:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n\n      - name: Get all updated Python files\n        id: changed-python-files\n        uses: tj-actions/changed-files@v46\n        with:\n          files: |\n            **.py\n\n      - name: Check for the changelog update\n        id: changelog-update\n        uses: tj-actions/changed-files@v46\n        with:\n          files: docs/changelog.rst\n\n      - name: Comment under the PR with a reminder\n        if: steps.changed-python-files.outputs.any_changed == 'true' && steps.changelog-update.outputs.any_changed == 'false'\n        uses: thollander/actions-comment-pull-request@v2\n        with:\n          message: 'Thank you for the PR! The changelog has not been updated, so here is a friendly reminder to check if you need to add an entry.'\n          GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'\n"
  },
  {
    "path": ".github/workflows/ci.yaml",
    "content": "name: Test\non:\n  pull_request:\n  push:\n    branches:\n      - master\n\nconcurrency:\n  # Cancel previous workflow run when a new commit is pushed to a feature branch\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}\n\nenv:\n  PY_COLORS: 1\n\njobs:\n  test:\n    name: Run tests\n    strategy:\n      fail-fast: false\n      matrix:\n        platform: [ubuntu-latest, windows-latest]\n        python-version: [\"3.10\", \"3.11\", \"3.12\", \"3.13\"]\n    runs-on: ${{ matrix.platform }}\n    env:\n      IS_MAIN_PYTHON: ${{ matrix.python-version == '3.10' && matrix.platform == 'ubuntu-latest' }}\n    steps:\n      - uses: actions/checkout@v5\n      - name: Install Python tools\n        uses: BrandonLWhite/pipx-install-action@v1.0.3\n      - name: Setup Python with poetry caching\n        # poetry cache requires poetry to already be installed, weirdly\n        uses: actions/setup-python@v6\n        with:\n          python-version: ${{ matrix.python-version }}\n          cache: poetry\n\n      - name: Install system dependencies on Windows\n        if: matrix.platform == 'windows-latest'\n        run: |\n          choco install mp3gain -y\n\n      - name: Install system dependencies on Ubuntu\n        if: matrix.platform == 'ubuntu-latest'\n        run: |\n          sudo apt update\n          sudo apt install --yes --no-install-recommends \\\n            ffmpeg \\\n            gobject-introspection \\\n            gstreamer1.0-plugins-base \\\n            imagemagick \\\n            libcairo2-dev \\\n            libgirepository-2.0-dev \\\n            mp3gain \\\n            pandoc \\\n            python3-gst-1.0\n\n      - name: Get changed lyrics files\n        id: lyrics-update\n        uses: tj-actions/changed-files@v46\n        with:\n          files: |\n            beetsplug/lyrics.py\n            test/plugins/test_lyrics.py\n\n      - name: Add pytest annotator\n        uses: liskin/gh-problem-matcher-wrap@v3\n        with:\n          linters: pytest\n          action: add\n\n      - if: ${{ env.IS_MAIN_PYTHON != 'true' }}\n        name: Test without coverage\n        run: |\n          poetry install --without=lint --extras=autobpm --extras=discogs --extras=lyrics --extras=replaygain --extras=reflink --extras=fetchart --extras=chroma --extras=sonosupdate\n          poe test\n\n      - if: ${{ env.IS_MAIN_PYTHON == 'true' }}\n        name: Test with coverage\n        env:\n          LYRICS_UPDATED: ${{ steps.lyrics-update.outputs.any_changed }}\n        run: |\n          poetry install --extras=autobpm --extras=discogs --extras=lyrics --extras=docs --extras=replaygain --extras=reflink --extras=fetchart --extras=chroma --extras=sonosupdate\n          poe docs\n          poe test-with-coverage\n\n      - if: ${{ !cancelled() }}\n        name: Upload test results to Codecov\n        uses: codecov/test-results-action@v1\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n\n      - if: ${{ env.IS_MAIN_PYTHON == 'true' }}\n        name: Store the coverage report\n        uses: actions/upload-artifact@v4\n        with:\n          name: coverage-report\n          path: .reports/coverage.xml\n\n  upload-coverage:\n    name: Upload coverage report\n    needs: test\n    runs-on: ubuntu-latest\n    permissions:\n      id-token: write\n    steps:\n      - uses: actions/checkout@v5\n\n      - name: Get the coverage report\n        uses: actions/download-artifact@v5\n        with:\n          name: coverage-report\n\n      - name: Upload code coverage\n        uses: codecov/codecov-action@v5\n        with:\n          files: ./coverage.xml\n          use_oidc: ${{ !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) }}\n"
  },
  {
    "path": ".github/workflows/integration_test.yaml",
    "content": "name: integration tests\non:\n  workflow_dispatch:\n  schedule:\n    - cron: \"0 0 * * SUN\" # run every Sunday at midnight\n\nenv:\n  PYTHON_VERSION: \"3.10\"\n\njobs:\n  test_integration:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n      - name: Install Python tools\n        uses: BrandonLWhite/pipx-install-action@v1.0.3\n      - uses: actions/setup-python@v6\n        with:\n          python-version: ${{ env.PYTHON_VERSION }}\n          cache: poetry\n\n      - name: Install dependencies\n        run: poetry install\n\n      - name: Test\n        env:\n          INTEGRATION_TEST: 1\n        run: poe test\n\n      - name: Check external links in docs\n        run: poe check-docs-links\n\n      - name: Notify on failure\n        if: ${{ failure() }}\n        env:\n          ZULIP_BOT_CREDENTIALS: ${{ secrets.ZULIP_BOT_CREDENTIALS }}\n        run: |\n          if [ -z \"${ZULIP_BOT_CREDENTIALS}\" ]; then\n            echo \"Skipping notify, ZULIP_BOT_CREDENTIALS is unset\"\n            exit 0\n          fi\n\n          curl -X POST https://beets.zulipchat.com/api/v1/messages \\\n            -u \"${ZULIP_BOT_CREDENTIALS}\" \\\n            -d \"type=stream\" \\\n            -d \"to=github\" \\\n            -d \"subject=${GITHUB_WORKFLOW} - $(date -u +%Y-%m-%d)\" \\\n            -d \"content=[${GITHUB_WORKFLOW}#${GITHUB_RUN_NUMBER}](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}) failed.\"\n"
  },
  {
    "path": ".github/workflows/lint.yaml",
    "content": "name: Lint check\nrun-name: Lint code\non:\n  pull_request:\n  push:\n    branches:\n      - master\n\nconcurrency:\n  # Cancel previous workflow run when a new commit is pushed to a feature branch\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}\n\nenv:\n  PYTHON_VERSION: \"3.10\"\n\njobs:\n  changed-files:\n    runs-on: ubuntu-latest\n    name: Get changed files\n    outputs:\n      any_docs_changed: ${{ steps.changed-doc-files.outputs.any_changed }}\n      any_python_changed: ${{ steps.raw-changed-python-files.outputs.any_changed }}\n      changed_doc_files: ${{ steps.changed-doc-files.outputs.all_changed_files }}\n      changed_python_files: ${{ steps.changed-python-files.outputs.all_changed_files }}\n    steps:\n      - uses: actions/checkout@v5\n      - name: Get changed docs files\n        id: changed-doc-files\n        uses: tj-actions/changed-files@v46\n        with:\n          files: |\n            docs/**\n      - name: Get changed python files\n        id: raw-changed-python-files\n        uses: tj-actions/changed-files@v46\n        with:\n          files: |\n            **.py\n            poetry.lock\n\n      - name: Check changed python files\n        id: changed-python-files\n        env:\n          CHANGED_PYTHON_FILES: ${{ steps.raw-changed-python-files.outputs.all_changed_files }}\n        run: |\n          if [[ \" $CHANGED_PYTHON_FILES \" == *\" poetry.lock \"* ]]; then\n            # if poetry.lock is changed, we need to check everything\n            CHANGED_PYTHON_FILES=\".\"\n          fi\n          echo \"all_changed_files=$CHANGED_PYTHON_FILES\" >> \"$GITHUB_OUTPUT\"\n\n  format:\n    if: needs.changed-files.outputs.any_python_changed == 'true'\n    runs-on: ubuntu-latest\n    name: Check formatting\n    needs: changed-files\n    steps:\n      - uses: actions/checkout@v5\n      - name: Install Python tools\n        uses: BrandonLWhite/pipx-install-action@v1.0.3\n      - uses: actions/setup-python@v6\n        with:\n          python-version: ${{ env.PYTHON_VERSION }}\n          cache: poetry\n\n      - name: Install dependencies\n        run: poetry install --only=lint\n\n      - name: Check code formatting\n        # the job output will contain colored diffs with what needs adjusting\n        run: poe check-format\n\n  lint:\n    if: needs.changed-files.outputs.any_python_changed == 'true'\n    runs-on: ubuntu-latest\n    name: Check linting\n    needs: changed-files\n    steps:\n      - uses: actions/checkout@v5\n      - name: Install Python tools\n        uses: BrandonLWhite/pipx-install-action@v1.0.3\n      - uses: actions/setup-python@v6\n        with:\n          python-version: ${{ env.PYTHON_VERSION }}\n          cache: poetry\n\n      - name: Install dependencies\n        run: poetry install --only=lint\n\n      - name: Lint code\n        run: poe lint --output-format=github ${{ needs.changed-files.outputs.changed_python_files }}\n\n  mypy:\n    if: needs.changed-files.outputs.any_python_changed == 'true'\n    runs-on: ubuntu-latest\n    name: Check types with mypy\n    needs: changed-files\n    steps:\n      - uses: actions/checkout@v5\n      - name: Install Python tools\n        uses: BrandonLWhite/pipx-install-action@v1.0.3\n      - uses: actions/setup-python@v6\n        with:\n          python-version: ${{ env.PYTHON_VERSION }}\n          cache: poetry\n\n      - name: Install dependencies\n        run: poetry install --only=typing\n\n      - name: Type check code\n        uses: liskin/gh-problem-matcher-wrap@v3\n        with:\n          linters: mypy\n          run: poe check-types --show-column-numbers --no-error-summary .\n\n  docs:\n    if: needs.changed-files.outputs.any_docs_changed == 'true'\n    runs-on: ubuntu-latest\n    name: Check docs\n    needs: changed-files\n    steps:\n      - uses: actions/checkout@v5\n        with:\n          fetch-depth: 0 # needed to get the full git history for the changelog check\n      - name: Install Python tools\n        uses: BrandonLWhite/pipx-install-action@v1.0.3\n      - uses: actions/setup-python@v6\n        with:\n          python-version: ${{ env.PYTHON_VERSION }}\n          cache: poetry\n\n      - name: Install dependencies\n        run: poetry install --extras=docs\n\n      - name: Add Sphinx problem matchers\n        run: |\n          echo \"::add-matcher::.github/problem-matchers/sphinx-build.json\"\n          echo \"::add-matcher::.github/problem-matchers/sphinx-lint.json\"\n\n      - name: Check docs formatting\n        run: poe format-docs --check\n\n      - name: Lint docs\n        run: poe lint-docs\n\n      - name: Check changelog entries are added under Unreleased section\n        run: |\n          git diff --word-diff=plain -U1000 origin/${{ github.base_ref }} -- docs/changelog.rst | awk '\n            # match the new version header\n            /^[0-9]+\\.[0-9]+\\.[0-9]+ \\(/ { past_version=1 }\n            # match a line that starts with a new changelog entry\n            /^\\{\\+- / && past_version {\n                  # NR gives the line number. Subtract 5 to skip the first 5 lines that have git diff headers\n                  print \"docs/changelog.rst:\" NR - 5 \": Changelog entry must be added under Unreleased section above. (changelog-unreleased)\"; exit 1\n                }\n          '\n\n      - name: Build docs\n        run: poe docs -- -e 'SPHINXOPTS=--fail-on-warning --keep-going'\n"
  },
  {
    "path": ".github/workflows/make_release.yaml",
    "content": "name: Make a Beets Release\n\non:\n  workflow_dispatch:\n    inputs:\n      version:\n        description: 'Version of the new release, just as a number with no prepended \"v\"'\n        required: true\n\nenv:\n  PYTHON_VERSION: \"3.10\"\n  NEW_VERSION: ${{ inputs.version }}\n  NEW_TAG: v${{ inputs.version }}\n\njobs:\n  increment-version:\n    name: Bump version, commit and create tag\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n      - name: Install Python tools\n        uses: BrandonLWhite/pipx-install-action@v1.0.3\n      - uses: actions/setup-python@v6\n        with:\n          python-version: ${{ env.PYTHON_VERSION }}\n          cache: poetry\n\n      - name: Install dependencies\n        run: poetry install --with=release --extras=docs\n\n      - name: Bump project version\n        run: poe bump \"${{ env.NEW_VERSION }}\"\n\n      - uses: EndBug/add-and-commit@v9\n        id: commit_and_tag\n        name: Commit the changes and create tag\n        with:\n          message: \"Increment version to ${{ env.NEW_VERSION }}\"\n          tag: \"${{ env.NEW_TAG }} --force\"\n\n  build:\n    name: Get changelog and build the distribution package\n    runs-on: ubuntu-latest\n    needs: increment-version\n    outputs:\n      changelog: ${{ steps.generate_changelog.outputs.changelog }}\n    steps:\n      - uses: actions/checkout@v5\n        with:\n          ref: ${{ env.NEW_TAG }}\n\n      - name: Install Python tools\n        uses: BrandonLWhite/pipx-install-action@v1.0.3\n      - uses: actions/setup-python@v6\n        with:\n          python-version: ${{ env.PYTHON_VERSION }}\n          cache: poetry\n\n      - name: Install dependencies\n        run: poetry install --with=release --extras=docs\n\n      - name: Install pandoc\n        run: sudo apt update && sudo apt install pandoc -y\n\n      - name: Obtain the changelog\n        id: generate_changelog\n        run: |\n          poe docs\n          {\n            echo 'changelog<<EOF'\n            poe --quiet changelog\n            echo EOF\n          } >> \"$GITHUB_OUTPUT\"\n\n      - name: Build a binary wheel and a source tarball\n        run: poe build\n\n      - name: Store the distribution packages\n        uses: actions/upload-artifact@v4\n        with:\n          name: python-package-distributions\n          path: dist/\n\n  publish-to-pypi:\n    name: Publish distribution 📦 to PyPI\n    runs-on: ubuntu-latest\n    needs: build\n    environment:\n      name: pypi\n      url: https://pypi.org/p/beets\n    permissions:\n      id-token: write\n    steps:\n      - name: Download all the dists\n        uses: actions/download-artifact@v5\n        with:\n          name: python-package-distributions\n          path: dist/\n      - name: Publish distribution 📦 to PyPI\n        uses: pypa/gh-action-pypi-publish@release/v1\n\n  make-github-release:\n    name: Create GitHub release\n    runs-on: ubuntu-latest\n    needs: [build, publish-to-pypi]\n    env:\n      CHANGELOG: ${{ needs.build.outputs.changelog }}\n    steps:\n      - name: Download all the dists\n        uses: actions/download-artifact@v5\n        with:\n          name: python-package-distributions\n          path: dist/\n\n      - name: Create a GitHub release\n        id: make_release\n        uses: ncipollo/release-action@v1\n        with:\n          tag: ${{ env.NEW_TAG }}\n          name: Release ${{ env.NEW_TAG }}\n          body: ${{ env.CHANGELOG }}\n          artifacts: dist/*\n      - name: Send release toot to Fosstodon\n        uses: cbrgm/mastodon-github-action@v2\n        continue-on-error: true\n        with:\n          access-token: ${{ secrets.MASTODON_ACCESS_TOKEN }}\n          url: ${{ secrets.MASTODON_URL }}\n          message: \"Version ${{ env.NEW_TAG }} of beets has been released! Check out all of the new changes at ${{ steps.make_release.outputs.html_url }}\"\n"
  },
  {
    "path": ".gitignore",
    "content": "# general hidden files/directories\n.DS_Store\n.idea\n\n# file patterns\n*~\n\n# Project Specific patterns\nman\n\n# The rest is from https://www.gitignore.io/api/python\n\n### Python ###\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64\nparts/\nsdist/\nvar/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\ncoverage.xml\n*,cover\n.hypothesis/\n.reports\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# pyenv\n.python-version\n\n# dotenv\n.env\n\n# virtualenv\nenv/\nvenv/\n.venv/\nENV/\n\n# Spyder project settings\n.spyderproject\n\n# Rope project settings\n.ropeproject\n\n# PyDev and Eclipse project settings\n/.project\n/.pydevproject\n/.settings\n.vscode\n\n# pyright\npyrightconfig.json\n\n# Pyrefly\npyrefly.toml\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "# See https://pre-commit.com for more information\n# See https://pre-commit.com/hooks.html for more hooks\n\nrepos:\n  - repo: local\n    hooks:\n      - id: format\n        name: Format Python files\n        entry: poe format\n        language: system\n        files: '.*.py'\n        pass_filenames: true\n      - id: format-docs\n        name: Format docs\n        entry: poe format-docs\n        language: system\n        files: '.*.rst'\n        pass_filenames: true\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "version: 2\n\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n\nsphinx:\n  configuration: docs/conf.py\n\npython:\n  install:\n    - method: pip\n      path: .\n      extra_requirements:\n        - docs\n"
  },
  {
    "path": "CODE_OF_CONDUCT.rst",
    "content": "Contributor Covenant Code of Conduct\n====================================\n\nOur Pledge\n----------\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, caste, color, religion, or sexual\nidentity and orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\nOur Standards\n-------------\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n- Demonstrating empathy and kindness toward other people\n- Being respectful of differing opinions, viewpoints, and experiences\n- Giving and gracefully accepting constructive feedback\n- Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n- Focusing on what is best not just for us as individuals, but for the overall\n  community\n\nExamples of unacceptable behavior include:\n\n- The use of sexualized language or imagery, and sexual attention or advances of\n  any kind\n- Trolling, insulting or derogatory comments, and personal or political attacks\n- Public or private harassment\n- Publishing others' private information, such as a physical or email address,\n  without their explicit permission\n- Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\nEnforcement Responsibilities\n----------------------------\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\nScope\n-----\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\nEnforcement\n-----------\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at here on Github.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\nEnforcement Guidelines\n----------------------\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n1. Correction\n~~~~~~~~~~~~~\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n2. Warning\n~~~~~~~~~~\n\n**Community Impact**: A violation through a single incident or series of\nactions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or permanent\nban.\n\n3. Temporary Ban\n~~~~~~~~~~~~~~~~\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n4. Permanent Ban\n~~~~~~~~~~~~~~~~\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior, harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within the\ncommunity.\n\nAttribution\n-----------\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.1, available `here\n<https://www.contributor-covenant.org/version/2/1/code_of_conduct/>`_.\n\nCommunity Impact Guidelines were inspired by Mozilla's code of conduct\nenforcement ladder.\n\nFor answers to common questions about this code of conduct, see the `FAQ\n<https://www.contributor-covenant.org/faq>`_. Translations are available at\n`translations <https://www.contributor-covenant.org/translations>`_.\n"
  },
  {
    "path": "CONTRIBUTING.rst",
    "content": "Contributing\n============\n\n.. contents::\n    :depth: 3\n\nThank you!\n----------\n\nFirst off, thank you for considering contributing to beets! It’s people like you\nthat make beets continue to succeed.\n\nThese guidelines describe how you can help most effectively. By following these\nguidelines, you can make life easier for the development team as it indicates\nyou respect the maintainers’ time; in return, the maintainers will reciprocate\nby helping to address your issue, review changes, and finalize pull requests.\n\nTypes of Contributions\n----------------------\n\nWe love to get contributions from our community—you! There are many ways to\ncontribute, whether you’re a programmer or not.\n\nThe first thing to do, regardless of how you'd like to contribute to the\nproject, is to check out our :doc:`Code of Conduct <code_of_conduct>` and to\nkeep that in mind while interacting with other contributors and users.\n\nNon-Programming\n~~~~~~~~~~~~~~~\n\n- Promote beets! Help get the word out by telling your friends, writing a blog\n  post, or discussing it on a forum you frequent.\n- Improve the documentation_. It’s incredibly easy to contribute here: just find\n  a page you want to modify and hit the “Edit on GitHub” button in the\n  upper-right. You can automatically send us a pull request for your changes.\n- GUI design. For the time being, beets is a command-line-only affair. But\n  that’s mostly because we don’t have any great ideas for what a good GUI should\n  look like. If you have those great ideas, please get in touch.\n- Benchmarks. We’d like to have a consistent way of measuring speed improvements\n  in beets’ tagger and other functionality as well as a way of comparing beets’\n  performance to other tools. You can help by compiling a library of\n  freely-licensed music files (preferably with incorrect metadata) for testing\n  and measurement.\n- Think you have a nice config or cool use-case for beets? We’d love to hear\n  about it! Submit a post to our `discussion board\n  <https://github.com/beetbox/beets/discussions/categories/show-and-tell>`__\n  under the “Show and Tell” category for a chance to get featured in `the docs\n  <https://beets.readthedocs.io/en/stable/guides/advanced.html>`__.\n- Consider helping out fellow users by `responding to support requests\n  <https://github.com/beetbox/beets/discussions/categories/q-a>`__ .\n\nProgramming\n~~~~~~~~~~~\n\n- As a programmer (even if you’re just a beginner!), you have a ton of\n  opportunities to get your feet wet with beets.\n- For developing plugins, or hacking away at beets, there’s some good\n  information in the `“For Developers” section of the docs\n  <https://beets.readthedocs.io/en/stable/dev/>`__.\n\n.. _development-tools:\n\nDevelopment Tools\n+++++++++++++++++\n\nIn order to develop beets, you will need a few tools installed:\n\n- poetry_ for packaging, virtual environment and dependency management\n- poethepoet_ to run tasks, such as linting, formatting, testing\n\nPython community recommends using pipx_ to install stand-alone command-line\napplications such as above. pipx_ installs each application in an isolated\nvirtual environment, where its dependencies will not interfere with your system\nand other CLI tools.\n\nIf you do not have pipx_ installed in your system, follow `pipx installation\ninstructions <https://pipx.pypa.io/stable/installation/>`__ or\n\n.. code-block:: sh\n\n    $ python3 -m pip install --user pipx\n\nInstall poetry_ and poethepoet_ using pipx_:\n\n::\n\n    $ pipx install poetry poethepoet\n\n.. admonition:: Check ``tool.pipx-install`` section in ``pyproject.toml`` to see supported versions\n\n    .. code-block:: toml\n\n        [tool.pipx-install]\n        poethepoet = \">=0.26\"\n        poetry = \"<2\"\n\n.. _getting-the-source:\n\nGetting the Source\n++++++++++++++++++\n\nThe easiest way to get started with the latest beets source is to clone the\nrepository and install ``beets`` in a local virtual environment using poetry_.\nThis can be done with:\n\n.. code-block:: bash\n\n    $ git clone https://github.com/beetbox/beets.git\n    $ cd beets\n    $ poetry install\n\nThis will install ``beets`` and all development dependencies into its own\nvirtual environment in your ``$POETRY_CACHE_DIR``. See ``poetry install --help``\nfor installation options, including installing ``extra`` dependencies for\nplugins.\n\nIn order to run something within this virtual environment, start the command\nwith ``poetry run`` to them, for example ``poetry run pytest``.\n\nOn the other hand, it may get tedious to type ``poetry run`` before every\ncommand. Instead, you can activate the virtual environment in your shell with:\n\n::\n\n    $ poetry shell\n\nYou should see ``(beets-py3.10)`` prefix in your shell prompt. Now you can run\ncommands directly, for example:\n\n::\n\n    $ (beets-py3.10) pytest\n\nAdditionally, poethepoet_ task runner assists us with the most common\noperations. Formatting, linting, testing are defined as ``poe`` tasks in\npyproject.toml_. Run:\n\n::\n\n    $ poe\n\nto see all available tasks. They can be used like this, for example\n\n.. code-block:: sh\n\n    $ poe lint                  # check code style\n    $ poe format                # fix formatting issues\n    $ poe test                  # run tests\n    # ... fix failing tests\n    $ poe test --lf             # re-run failing tests (note the additional pytest option)\n    $ poe check-types --pretty  # check types with an extra option for mypy\n\nCode Contribution Ideas\n+++++++++++++++++++++++\n\n- We maintain a set of `issues marked as “good first issue”\n  <https://github.com/beetbox/beets/labels/good%20first%20issue>`__. These are\n  issues that would serve as a good introduction to the codebase. Claim one and\n  start exploring!\n- Like testing? Our `test coverage\n  <https://app.codecov.io/github/beetbox/beets>`__ is somewhat low. You can help\n  out by finding low-coverage modules or checking out other `testing-related\n  issues <https://github.com/beetbox/beets/labels/testing>`__.\n- There are several ways to improve the tests in general (see :ref:`testing` and\n  some places to think about performance optimization (see `Optimization\n  <https://github.com/beetbox/beets/wiki/Optimization>`__).\n- Not all of our code is up to our coding conventions. In particular, the\n  `library API documentation\n  <https://beets.readthedocs.io/en/stable/dev/library.html>`__ are currently\n  quite sparse. You can help by adding to the docstrings in the code and to the\n  documentation pages themselves. beets follows `PEP-257\n  <https://peps.python.org/pep-0257/>`__ for docstrings and in some places, we\n  also sometimes use `ReST autodoc syntax for Sphinx\n  <https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html>`__ to,\n  for example, refer to a class name.\n\nYour First Contribution\n-----------------------\n\nIf this is your first time contributing to an open source project, welcome! If\nyou are confused at all about how to contribute or what to contribute, take a\nlook at `this great tutorial <https://makeapullrequest.com/>`__, or stop by our\n`discussion board`_ if you have any questions.\n\nWe maintain a list of issues we reserved for those new to open source labeled\n`first timers only`_. Since the goal of these issues is to get users comfortable\nwith contributing to an open source project, please do not hesitate to ask any\nquestions.\n\n.. _first timers only: https://github.com/beetbox/beets/issues?q=is%3Aopen+is%3Aissue+label%3A%22first+timers+only%22\n\nHow to Submit Your Work\n-----------------------\n\nDo you have a great bug fix, new feature, or documentation expansion you’d like\nto contribute? Follow these steps to create a GitHub pull request and your code\nwill ship in no time.\n\n1. Fork the beets repository and clone it (see above) to create a workspace.\n2. Install pre-commit, following the instructions `here\n   <https://pre-commit.com/>`_.\n3. Make your changes.\n4. Add tests. If you’ve fixed a bug, write a test to ensure that you’ve actually\n   fixed it. If there’s a new feature or plugin, please contribute tests that\n   show that your code does what it says.\n5. Add documentation. If you’ve added a new command flag, for example, find the\n   appropriate page under ``docs/`` where it needs to be listed.\n6. Add a changelog entry to ``docs/changelog.rst`` near the top of the document.\n7. Run the tests and style checker, see :ref:`testing`.\n8. Push to your fork and open a pull request! We’ll be in touch shortly.\n9. If you add commits to a pull request, please add a comment or re-request a\n   review after you push them since GitHub doesn’t automatically notify us when\n   commits are added.\n\nRemember, code contributions have four parts: the code, the tests, the\ndocumentation, and the changelog entry. Thank you for contributing!\n\n.. admonition:: Ownership\n\n    If you are the owner of a plugin, please consider reviewing pull requests\n    that affect your plugin. If you are not the owner of a plugin, please\n    consider becoming one! You can do so by adding an entry to\n    ``.github/CODEOWNERS``. This way, you will automatically receive a review\n    request for pull requests that adjust the code that you own. If you have any\n    questions, please ask on our `discussion board`_.\n\nThe Code\n--------\n\nThe documentation has a section on the `library API\n<https://beets.readthedocs.io/en/stable/dev/library.html>`__ that serves as an\nintroduction to beets’ design.\n\nCoding Conventions\n------------------\n\nGeneral\n~~~~~~~\n\nThere are a few coding conventions we use in beets:\n\n- Whenever you access the library database, do so through the provided Library\n  methods or via a Transaction object. Never call ``lib.conn.*`` directly. For\n  example, do this:\n\n  .. code-block:: python\n\n      with g.lib.transaction() as tx:\n          rows = tx.query(\"SELECT DISTINCT {field} FROM {model._table} ORDER BY {sort_field}\")\n\n  To fetch Item objects from the database, use lib.items(…) and supply a query\n  as an argument. Resist the urge to write raw SQL for your query. If you must\n  use lower-level queries into the database, do this, for example:\n\n  .. code-block:: python\n\n      with lib.transaction() as tx:\n          rows = tx.query(\"SELECT path FROM items WHERE album_id = ?\", (album_id,))\n\n  Transaction objects help control concurrent access to the database and assist\n  in debugging conflicting accesses.\n\n- f-strings should be used instead of the ``%`` operator and ``str.format()``\n  calls.\n- Never ``print`` informational messages; use the `logging\n  <https://docs.python.org/3/library/logging.html>`__ module instead. In\n  particular, we have our own logging shim, so you’ll see ``from beets import\n  logging`` in most files.\n\n  - The loggers use `str.format\n    <https://docs.python.org/3/library/stdtypes.html>`__-style logging instead\n    of ``%``-style, so you can type ``log.debug(\"{}\", obj)`` to do your\n    formatting.\n\n- Exception handlers must use ``except A as B:`` instead of ``except A, B:``.\n\nStyle\n~~~~~\n\nWe use `ruff <https://docs.astral.sh/ruff/>`__ to format and lint the codebase.\n\nRun ``poe check-format`` and ``poe lint`` to check your code for style and\nlinting errors. Running ``poe format`` will automatically format your code\naccording to the specifications required by the project.\n\nSimilarly, run ``poe format-docs`` and ``poe lint-docs`` to ensure consistent\ndocumentation formatting and check for any issues.\n\nEditor Settings\n~~~~~~~~~~~~~~~\n\nPersonally, I work on beets with vim_. Here are some ``.vimrc`` lines that might\nhelp with PEP 8-compliant Python coding:\n\n::\n\n    filetype indent on\n    autocmd FileType python setlocal shiftwidth=4 tabstop=4 softtabstop=4 expandtab shiftround autoindent\n\nConsider installing `this alternative Python indentation plugin\n<https://github.com/mitsuhiko/vim-python-combined>`__. I also like `neomake\n<https://github.com/neomake/neomake>`__ with its flake8 checker.\n\n.. _testing:\n\nTesting\n-------\n\nRunning the Tests\n~~~~~~~~~~~~~~~~~\n\nUse ``poe`` to run tests:\n\n::\n\n    $ poe test [pytest options]\n\nYou can disable a hand-selected set of \"slow\" tests by setting the environment\nvariable ``SKIP_SLOW_TESTS``, for example:\n\n::\n\n    $ SKIP_SLOW_TESTS=1 poe test\n\nCoverage\n++++++++\n\nThe ``test`` command does not include coverage as it slows down testing. In\norder to measure it, use the ``test-with-coverage`` task\n\n    $ poe test-with-coverage [pytest options]\n\nYou are welcome to explore coverage by opening the HTML report in\n``.reports/html/index.html``.\n\nNote that for each covered line the report shows **which tests cover it**\n(expand the list on the right-hand side of the affected line).\n\nYou can find project coverage status on Codecov_.\n\nRed Flags\n+++++++++\n\nThe pytest-random_ plugin makes it easy to randomize the order of tests. ``poe\ntest --random`` will occasionally turn up failing tests that reveal ordering\ndependencies—which are bad news!\n\nTest Dependencies\n+++++++++++++++++\n\nThe tests have a few more dependencies than beets itself. (The additional\ndependencies consist of testing utilities and dependencies of non-default\nplugins exercised by the test suite.) The dependencies are listed under the\n``tool.poetry.group.test.dependencies`` section in pyproject.toml_.\n\nWriting Tests\n~~~~~~~~~~~~~\n\nWriting tests is done by adding or modifying files in folder test_. Take a look\nat test-query_ to get a basic view on how tests are written. Since we are\ncurrently migrating the tests from unittest_ to pytest_, new tests should be\nwritten using pytest_. Contributions migrating existing tests are welcome!\n\nExternal API requests under test should be mocked with requests-mock_, However,\nwe still want to know whether external APIs are up and that they return expected\nresponses, therefore we test them weekly with our `integration test`_ suite.\n\nIn order to add such a test, mark your test with the ``integration_test`` marker\n\n.. code-block:: python\n\n    @pytest.mark.integration_test\n    def test_external_api_call(): ...\n\nThis way, the test will be run only in the integration test suite.\n\nbeets also defines custom pytest markers in ``test/conftest.py``:\n\n- ``integration_test``: runs only when ``INTEGRATION_TEST=true`` is set.\n- ``on_lyrics_update``: runs only when ``LYRICS_UPDATED=true`` is set.\n- ``requires_import(\"module\", force_ci=True)``: runs the test only when the\n  module is importable. With the default ``force_ci=True``, this import check is\n  bypassed on GitHub Actions for ``beetbox/beets`` so CI still runs the test.\n  Set ``force_ci=False`` to allow CI to skip when the module is missing.\n\n.. code-block:: python\n\n    @pytest.mark.integration_test\n    def test_external_api_call(): ...\n\n\n    @pytest.mark.on_lyrics_update\n    def test_real_lyrics_backend(): ...\n\n\n    @pytest.mark.requires_import(\"langdetect\")\n    def test_language_detection(): ...\n\n\n    @pytest.mark.requires_import(\"librosa\", force_ci=False)\n    def test_autobpm_command(): ...\n\n.. _codecov: https://app.codecov.io/github/beetbox/beets\n\n.. _discussion board: https://github.com/beetbox/beets/discussions\n\n.. _documentation: https://beets.readthedocs.io/en/stable/\n\n.. _integration test: https://github.com/beetbox/beets/actions?query=workflow%3A%22integration+tests%22\n\n.. _pipx: https://pipx.pypa.io/stable\n\n.. _poethepoet: https://poethepoet.natn.io/index.html\n\n.. _poetry: https://python-poetry.org/docs/\n\n.. _pyproject.toml: https://github.com/beetbox/beets/blob/master/pyproject.toml\n\n.. _pytest: https://docs.pytest.org/en/stable/\n\n.. _pytest-random: https://github.com/klrmn/pytest-random\n\n.. _requests-mock: https://requests-mock.readthedocs.io/en/latest/response.html\n\n.. _test: https://github.com/beetbox/beets/tree/master/test\n\n.. _test-query: https://github.com/beetbox/beets/blob/master/test/test_query.py\n\n.. _unittest: https://docs.python.org/3/library/unittest.html\n\n.. _vim: https://www.vim.org/\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License\n\nCopyright (c) 2010-2016 Adrian Sampson\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.rst",
    "content": ".. image:: https://img.shields.io/pypi/v/beets.svg\n    :target: https://pypi.python.org/pypi/beets\n\n.. image:: https://img.shields.io/codecov/c/github/beetbox/beets.svg\n    :target: https://app.codecov.io/github/beetbox/beets\n\n.. image:: https://img.shields.io/github/actions/workflow/status/beetbox/beets/ci.yaml\n    :target: https://github.com/beetbox/beets/actions\n\n.. image:: https://repology.org/badge/tiny-repos/beets.svg\n    :target: https://repology.org/project/beets/versions\n\nbeets\n=====\n\nBeets is the media library management system for obsessive music geeks.\n\nThe purpose of beets is to get your music collection right once and for all. It\ncatalogs your collection, automatically improving its metadata as it goes. It\nthen provides a suite of tools for manipulating and accessing your music.\n\nHere's an example of beets' brainy tag corrector doing its thing:\n\n::\n\n    $ beet import ~/music/ladytron\n    Tagging:\n        Ladytron - Witching Hour\n    (Similarity: 98.4%)\n     * Last One Standing      -> The Last One Standing\n     * Beauty                 -> Beauty*2\n     * White Light Generation -> Whitelightgenerator\n     * All the Way            -> All the Way...\n\nBecause beets is designed as a library, it can do almost anything you can\nimagine for your music collection. Via plugins_, beets becomes a panacea:\n\n- Fetch or calculate all the metadata you could possibly need: `album art`_,\n  lyrics_, genres_, tempos_, ReplayGain_ levels, or `acoustic fingerprints`_.\n- Get metadata from MusicBrainz_, Discogs_, and Beatport_. Or guess metadata\n  using songs' filenames or their acoustic fingerprints.\n- `Transcode audio`_ to any format you like.\n- Check your library for `duplicate tracks and albums`_ or for `albums that are\n  missing tracks`_.\n- Clean up crufty tags left behind by other, less-awesome tools.\n- Embed and extract album art from files' metadata.\n- Browse your music library graphically through a Web browser and play it in any\n  browser that supports `HTML5 Audio`_.\n- Analyze music files' metadata from the command line.\n- Listen to your library with a music player that speaks the MPD_ protocol and\n  works with a staggering variety of interfaces.\n\nIf beets doesn't do what you want yet, `writing your own plugin`_ is shockingly\nsimple if you know a little Python.\n\n.. _acoustic fingerprints: https://beets.readthedocs.org/page/plugins/chroma.html\n\n.. _album art: https://beets.readthedocs.org/page/plugins/fetchart.html\n\n.. _albums that are missing tracks: https://beets.readthedocs.org/page/plugins/missing.html\n\n.. _beatport: https://www.beatport.com\n\n.. _discogs: https://www.discogs.com/\n\n.. _duplicate tracks and albums: https://beets.readthedocs.org/page/plugins/duplicates.html\n\n.. _genres: https://beets.readthedocs.org/page/plugins/lastgenre.html\n\n.. _html5 audio: https://html.spec.whatwg.org/multipage/media.html#the-audio-element\n\n.. _lyrics: https://beets.readthedocs.org/page/plugins/lyrics.html\n\n.. _mpd: https://www.musicpd.org/\n\n.. _musicbrainz: https://musicbrainz.org/\n\n.. _musicbrainz music collection: https://musicbrainz.org/doc/Collections/\n\n.. _plugins: https://beets.readthedocs.org/page/plugins/\n\n.. _replaygain: https://beets.readthedocs.org/page/plugins/replaygain.html\n\n.. _tempos: https://beets.readthedocs.org/page/plugins/acousticbrainz.html\n\n.. _transcode audio: https://beets.readthedocs.org/page/plugins/convert.html\n\n.. _writing your own plugin: https://beets.readthedocs.org/page/dev/plugins/index.html\n\nInstall\n-------\n\nYou can install beets by typing ``pip install beets`` or directly from Github\n(see details here_). Beets has also been packaged in the `software\nrepositories`_ of several distributions. Check out the `Getting Started`_ guide\nfor more information.\n\n.. _getting started: https://beets.readthedocs.org/page/guides/main.html\n\n.. _here: https://beets.readthedocs.io/en/latest/faq.html#run-the-latest-source-version-of-beets\n\n.. _software repositories: https://repology.org/project/beets/versions\n\nContribute\n----------\n\nThank you for considering contributing to ``beets``! Whether you're a programmer\nor not, you should be able to find all the info you need at CONTRIBUTING.rst_.\n\n.. _contributing.rst: https://github.com/beetbox/beets/blob/master/CONTRIBUTING.rst\n\nRead More\n---------\n\nLearn more about beets at `its Web site`_. Follow `@b33ts`_ on Mastodon for news\nand updates.\n\n.. _@b33ts: https://fosstodon.org/@beets\n\n.. _its web site: https://beets.io/\n\nContact\n-------\n\n- Encountered a bug you'd like to report? Check out our `issue tracker`_!\n\n  - If your issue hasn't already been reported, please `open a new ticket`_ and\n    we'll be in touch with you shortly.\n  - If you'd like to vote on a feature/bug, simply give a :+1: on issues you'd\n    like to see prioritized over others.\n  - Need help/support, would like to start a discussion, have an idea for a new\n    feature, or would just like to introduce yourself to the team? Check out\n    `GitHub Discussions`_!\n\n.. _github discussions: https://github.com/beetbox/beets/discussions\n\n.. _issue tracker: https://github.com/beetbox/beets/issues\n\n.. _open a new ticket: https://github.com/beetbox/beets/issues/new/choose\n\nAuthors\n-------\n\nBeets is by `Adrian Sampson`_ with a supporting cast of thousands.\n\n.. _adrian sampson: https://www.cs.cornell.edu/~asampson/\n"
  },
  {
    "path": "README_kr.rst",
    "content": ".. image:: https://img.shields.io/pypi/v/beets.svg\n    :target: https://pypi.python.org/pypi/beets\n\n.. image:: https://img.shields.io/codecov/c/github/beetbox/beets.svg\n    :target: https://app.codecov.io/github/beetbox/beets\n\n.. image:: https://travis-ci.org/beetbox/beets.svg?branch=master\n    :target: https://travis-ci.org/beetbox/beets\n\nbeets\n=====\n\nBeets는 강박적인 음악을 듣는 사람들을 위한 미디어 라이브러리 관리 시스템이다.\n\nBeets의 목적은 음악들을 한번에 다 받는 것이다. 음악들을 카탈로그화 하고, 자동으로 메타 데이터를 개선한다. 그리고 음악에 접근하고 조작할\n수 있는 도구들을 제공한다.\n\n다음은 Beets의 brainy tag corrector가 한 일의 예시이다.\n\n::\n\n    $ beet import ~/music/ladytron\n    Tagging:\n        Ladytron - Witching Hour\n    (Similarity: 98.4%)\n     * Last One Standing      -> The Last One Standing\n     * Beauty                 -> Beauty*2\n     * White Light Generation -> Whitelightgenerator\n     * All the Way            -> All the Way...\n\nBeets는 라이브러리로 디자인 되었기 때문에, 당신이 음악들에 대해 상상하는 모든 것을 할 수 있다. plugins_ 을 통해서 모든 것을 할\n수 있는 것이다!\n\n- 필요하는 메타 데이터를 계산하거나 패치 할 때: `album art`_, lyrics_, genres_, tempos_,\n  ReplayGain_ levels, or `acoustic fingerprints`_.\n- MusicBrainz_, Discogs_,`Beatport`_로부터 메타데이터를 가져오거나, 노래 제목이나 음향 특징으로 메타데이터를\n  추측한다\n- `Transcode audio`_ 당신이 좋아하는 어떤 포맷으로든 변경한다.\n- 당신의 라이브러리에서 `duplicate tracks and albums`_ 이나 `albums that are missing\n  tracks`_ 를 검사한다.\n- 남이 남기거나, 좋지 않은 도구로 남긴 잡다한 태그들을 지운다.\n- 파일의 메타데이터에서 앨범 아트를 삽입이나 추출한다.\n- 당신의 음악들을 `HTML5 Audio`_ 를 지원하는 어떤 브라우저든 재생할 수 있고, 웹 브라우저에 표시 할 수 있다.\n- 명령어로부터 음악 파일의 메타데이터를 분석할 수 있다.\n- MPD_ 프로토콜을 사용하여 음악 플레이어로 음악을 들으면, 엄청나게 다양한 인터페이스로 작동한다.\n\n만약 Beets에 당신이 원하는게 아직 없다면, 당신이 python을 안다면 `writing your own plugin`_ _은 놀라울정도로\n간단하다.\n\n.. _acoustic fingerprints: https://beets.readthedocs.org/page/plugins/chroma.html\n\n.. _album art: https://beets.readthedocs.org/page/plugins/fetchart.html\n\n.. _albums that are missing tracks: https://beets.readthedocs.org/page/plugins/missing.html\n\n.. _beatport: https://www.beatport.com\n\n.. _discogs: https://www.discogs.com/\n\n.. _duplicate tracks and albums: https://beets.readthedocs.org/page/plugins/duplicates.html\n\n.. _genres: https://beets.readthedocs.org/page/plugins/lastgenre.html\n\n.. _html5 audio: https://html.spec.whatwg.org/multipage/media.html#the-audio-element\n\n.. _lyrics: https://beets.readthedocs.org/page/plugins/lyrics.html\n\n.. _mpd: https://www.musicpd.org/\n\n.. _musicbrainz: https://musicbrainz.org/\n\n.. _musicbrainz music collection: https://musicbrainz.org/doc/Collections/\n\n.. _plugins: https://beets.readthedocs.org/page/plugins/\n\n.. _replaygain: https://beets.readthedocs.org/page/plugins/replaygain.html\n\n.. _tempos: https://beets.readthedocs.org/page/plugins/acousticbrainz.html\n\n.. _transcode audio: https://beets.readthedocs.org/page/plugins/convert.html\n\n.. _writing your own plugin: https://beets.readthedocs.org/page/dev/plugins/index.html\n\n설치\n-------\n\n당신은 ``pip install beets`` 을 사용해서 Beets를 설치할 수 있다. 그리고 `Getting Started`_ 가이드를\n확인할 수 있다.\n\n.. _getting started: https://beets.readthedocs.org/page/guides/main.html\n\n컨트리뷰션\n----------\n\n어떻게 도우려는지 알고싶다면 Hacking_ 위키페이지를 확인하라. 당신은 docs 안에 `For Developers`_ 에도 관심이 있을수\n있다.\n\n.. _for developers: https://beets.readthedocs.io/en/stable/dev/\n\n.. _hacking: https://github.com/beetbox/beets/wiki/Hacking\n\nRead More\n---------\n\n`its Web site`_ 에서 Beets에 대해 조금 더 알아볼 수 있다. 트위터에서 `@b33ts`_ 를 팔로우하면 새 소식을 볼 수\n있다.\n\n.. _@b33ts: https://twitter.com/b33ts/\n\n.. _its web site: https://beets.io/\n\n저자들\n-------\n\n`Adrian Sampson`_ 와 많은 사람들의 지지를 받아 Beets를 만들었다. 돕고 싶다면 forum_.를 방문하면 된다.\n\n.. _adrian sampson: https://www.cs.cornell.edu/~asampson/\n\n.. _forum: https://github.com/beetbox/beets/discussions/\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nWe currently support only the latest release of beets.\n\n## Reporting a Vulnerability\n\nTo report a security vulnerability, please send email to [our Zulip team][z].\n\n[z]: mailto:email.218c36e48d78cf125c0a6219a6c2a417.show-sender@streams.zulipchat.com\n"
  },
  {
    "path": "beets/__init__.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nfrom sys import stderr\n\nimport confuse\n\nfrom .util.deprecation import deprecate_imports\n\n__version__ = \"2.7.1\"\n__author__ = \"Adrian Sampson <adrian@radbox.org>\"\n\n\ndef __getattr__(name: str):\n    \"\"\"Handle deprecated imports.\"\"\"\n    return deprecate_imports(\n        __name__,\n        {\"art\": \"beetsplug._utils\", \"vfs\": \"beetsplug._utils\"},\n        name,\n    )\n\n\nclass IncludeLazyConfig(confuse.LazyConfig):\n    \"\"\"A version of Confuse's LazyConfig that also merges in data from\n    YAML files specified in an `include` setting.\n    \"\"\"\n\n    def read(self, user: bool = True, defaults: bool = True) -> None:\n        super().read(user, defaults)\n\n        try:\n            for view in self[\"include\"].sequence():\n                self.set_file(view.as_filename())\n        except confuse.NotFoundError:\n            pass\n        except confuse.ConfigReadError as err:\n            stderr.write(f\"configuration `import` failed: {err.reason}\")\n\n\nconfig = IncludeLazyConfig(\"beets\", __name__)\n"
  },
  {
    "path": "beets/__main__.py",
    "content": "# This file is part of beets.\n# Copyright 2017, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"The __main__ module lets you run the beets CLI interface by typing\n`python -m beets`.\n\"\"\"\n\nimport sys\n\nfrom .ui import main\n\nif __name__ == \"__main__\":\n    main(sys.argv[1:])\n"
  },
  {
    "path": "beets/autotag/__init__.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Facilities for automatically determining files' correct metadata.\"\"\"\n\nfrom __future__ import annotations\n\nfrom importlib import import_module\nfrom typing import TYPE_CHECKING\n\nfrom beets import config, logging\n\n# Parts of external interface.\nfrom beets.util import unique_list\nfrom beets.util.deprecation import deprecate_for_maintainers, deprecate_imports\n\nfrom .hooks import AlbumInfo, AlbumMatch, TrackInfo, TrackMatch\nfrom .match import Proposal, Recommendation, tag_album, tag_item\n\nif TYPE_CHECKING:\n    from collections.abc import Sequence\n\n    from beets.library import Album, Item, LibModel\n\n\ndef __getattr__(name: str):\n    if name == \"current_metadata\":\n        deprecate_for_maintainers(\n            f\"'beets.autotag.{name}'\", \"'beets.util.get_most_common_tags'\"\n        )\n        return import_module(\"beets.util\").get_most_common_tags\n\n    return deprecate_imports(\n        __name__, {\"Distance\": \"beets.autotag.distance\"}, name\n    )\n\n\n__all__ = [\n    \"AlbumInfo\",\n    \"AlbumMatch\",\n    \"Proposal\",\n    \"Recommendation\",\n    \"TrackInfo\",\n    \"TrackMatch\",\n    \"apply_album_metadata\",\n    \"apply_item_metadata\",\n    \"apply_metadata\",\n    \"tag_album\",\n    \"tag_item\",\n]\n\n# Global logger.\nlog = logging.getLogger(\"beets\")\n\n# Metadata fields that are already hardcoded, or where the tag name changes.\nSPECIAL_FIELDS = {\n    \"album\": (\n        \"va\",\n        \"releasegroup_id\",\n        \"artist_id\",\n        \"artists_ids\",\n        \"album_id\",\n        \"mediums\",\n        \"tracks\",\n        \"year\",\n        \"month\",\n        \"day\",\n        \"artist\",\n        \"artists\",\n        \"artist_credit\",\n        \"artists_credit\",\n        \"artist_sort\",\n        \"artists_sort\",\n        \"data_url\",\n    ),\n    \"track\": (\n        \"track_alt\",\n        \"artist_id\",\n        \"artists_ids\",\n        \"release_track_id\",\n        \"medium\",\n        \"index\",\n        \"medium_index\",\n        \"title\",\n        \"artist_credit\",\n        \"artists_credit\",\n        \"artist_sort\",\n        \"artists_sort\",\n        \"artist\",\n        \"artists\",\n        \"track_id\",\n        \"medium_total\",\n        \"data_url\",\n        \"length\",\n    ),\n}\n\n\n# Additional utilities for the main interface.\n\n\ndef _apply_metadata(\n    info: AlbumInfo | TrackInfo,\n    db_obj: Album | Item,\n    nullable_fields: Sequence[str] = [],\n):\n    \"\"\"Set the db_obj's metadata to match the info.\"\"\"\n    special_fields = SPECIAL_FIELDS[\n        \"album\" if isinstance(info, AlbumInfo) else \"track\"\n    ]\n\n    for field, value in info.items():\n        # We only overwrite fields that are not already hardcoded.\n        if field in special_fields:\n            continue\n\n        # Don't overwrite fields with empty values unless the\n        # field is explicitly allowed to be overwritten.\n        if value is None and field not in nullable_fields:\n            continue\n\n        db_obj[field] = value\n\n\ndef correct_list_fields(m: LibModel) -> None:\n    \"\"\"Synchronise single and list values for the list fields that we use.\n\n    That is, ensure the same value in the single field and the first element\n    in the list.\n\n    For context, the value we set as, say, ``mb_artistid`` is simply ignored:\n    Under the current :class:`MediaFile` implementation, fields ``albumtype``,\n    ``mb_artistid`` and ``mb_albumartistid`` are mapped to the first element of\n    ``albumtypes``, ``mb_artistids`` and ``mb_albumartistids`` respectively.\n\n    This means setting ``mb_artistid`` has no effect. However, beets\n    functionality still assumes that ``mb_artistid`` is independent and stores\n    its value in the database. If ``mb_artistid`` != ``mb_artistids[0]``,\n    ``beet write`` command thinks that ``mb_artistid`` is modified and tries to\n    update the field in the file. Of course nothing happens, so the same diff\n    is shown every time the command is run.\n\n    We can avoid this issue by ensuring that ``mb_artistid`` has the same value\n    as ``mb_artistids[0]``, and that's what this function does.\n\n    Note: :class:`Album` model does not have ``mb_artistids`` and\n    ``mb_albumartistids`` fields therefore we need to check for their presence.\n    \"\"\"\n\n    def ensure_first_value(single_field: str, list_field: str) -> None:\n        \"\"\"Ensure the first ``list_field`` item is equal to ``single_field``.\"\"\"\n        single_val, list_val = getattr(m, single_field), getattr(m, list_field)\n        if single_val:\n            setattr(m, list_field, unique_list([single_val, *list_val]))\n        elif list_val:\n            setattr(m, single_field, list_val[0])\n\n    ensure_first_value(\"albumtype\", \"albumtypes\")\n\n    if hasattr(m, \"mb_artistids\"):\n        ensure_first_value(\"mb_artistid\", \"mb_artistids\")\n\n    if hasattr(m, \"mb_albumartistids\"):\n        ensure_first_value(\"mb_albumartistid\", \"mb_albumartistids\")\n\n\ndef apply_item_metadata(item: Item, track_info: TrackInfo):\n    \"\"\"Set an item's metadata from its matched TrackInfo object.\"\"\"\n    item.artist = track_info.artist\n    item.artists = track_info.artists\n    item.artist_sort = track_info.artist_sort\n    item.artists_sort = track_info.artists_sort\n    item.artist_credit = track_info.artist_credit\n    item.artists_credit = track_info.artists_credit\n    item.title = track_info.title\n    item.mb_trackid = track_info.track_id\n    item.mb_releasetrackid = track_info.release_track_id\n    if track_info.artist_id:\n        item.mb_artistid = track_info.artist_id\n    if track_info.artists_ids:\n        item.mb_artistids = track_info.artists_ids\n\n    _apply_metadata(track_info, item)\n    correct_list_fields(item)\n\n    # At the moment, the other metadata is left intact (including album\n    # and track number). Perhaps these should be emptied?\n\n\ndef apply_album_metadata(album_info: AlbumInfo, album: Album):\n    \"\"\"Set the album's metadata to match the AlbumInfo object.\"\"\"\n    _apply_metadata(album_info, album)\n    correct_list_fields(album)\n\n\ndef apply_metadata(\n    album_info: AlbumInfo, item_info_pairs: list[tuple[Item, TrackInfo]]\n):\n    \"\"\"Set items metadata to match corresponding tagged info.\"\"\"\n    for item, track_info in item_info_pairs:\n        # Artist or artist credit.\n        if config[\"artist_credit\"]:\n            item.artist = (\n                track_info.artist_credit\n                or track_info.artist\n                or album_info.artist_credit\n                or album_info.artist\n            )\n            item.artists = (\n                track_info.artists_credit\n                or track_info.artists\n                or album_info.artists_credit\n                or album_info.artists\n            )\n            item.albumartist = album_info.artist_credit or album_info.artist\n            item.albumartists = album_info.artists_credit or album_info.artists\n        else:\n            item.artist = track_info.artist or album_info.artist\n            item.artists = track_info.artists or album_info.artists\n            item.albumartist = album_info.artist\n            item.albumartists = album_info.artists\n\n        # Album.\n        item.album = album_info.album\n\n        # Artist sort and credit names.\n        item.artist_sort = track_info.artist_sort or album_info.artist_sort\n        item.artists_sort = track_info.artists_sort or album_info.artists_sort\n        item.artist_credit = (\n            track_info.artist_credit or album_info.artist_credit\n        )\n        item.artists_credit = (\n            track_info.artists_credit or album_info.artists_credit\n        )\n        item.albumartist_sort = album_info.artist_sort\n        item.albumartists_sort = album_info.artists_sort\n        item.albumartist_credit = album_info.artist_credit\n        item.albumartists_credit = album_info.artists_credit\n\n        # Release date.\n        for prefix in \"\", \"original_\":\n            if config[\"original_date\"] and not prefix:\n                # Ignore specific release date.\n                continue\n\n            for suffix in \"year\", \"month\", \"day\":\n                key = f\"{prefix}{suffix}\"\n                value = getattr(album_info, key) or 0\n\n                # If we don't even have a year, apply nothing.\n                if suffix == \"year\" and not value:\n                    break\n\n                # Otherwise, set the fetched value (or 0 for the month\n                # and day if not available).\n                item[key] = value\n\n                # If we're using original release date for both fields,\n                # also set item.year = info.original_year, etc.\n                if config[\"original_date\"]:\n                    item[suffix] = value\n\n        # Title.\n        item.title = track_info.title\n\n        if config[\"per_disc_numbering\"]:\n            # We want to let the track number be zero, but if the medium index\n            # is not provided we need to fall back to the overall index.\n            if track_info.medium_index is not None:\n                item.track = track_info.medium_index\n            else:\n                item.track = track_info.index\n            item.tracktotal = track_info.medium_total or len(album_info.tracks)\n        else:\n            item.track = track_info.index\n            item.tracktotal = len(album_info.tracks)\n\n        # Disc and disc count.\n        item.disc = track_info.medium\n        item.disctotal = album_info.mediums\n\n        # MusicBrainz IDs.\n        item.mb_trackid = track_info.track_id\n        item.mb_releasetrackid = track_info.release_track_id\n        item.mb_albumid = album_info.album_id\n        if track_info.artist_id:\n            item.mb_artistid = track_info.artist_id\n        else:\n            item.mb_artistid = album_info.artist_id\n\n        if track_info.artists_ids:\n            item.mb_artistids = track_info.artists_ids\n        else:\n            item.mb_artistids = album_info.artists_ids\n\n        item.mb_albumartistid = album_info.artist_id\n        item.mb_albumartistids = album_info.artists_ids\n        item.mb_releasegroupid = album_info.releasegroup_id\n\n        # Compilation flag.\n        item.comp = album_info.va\n\n        # Track alt.\n        item.track_alt = track_info.track_alt\n\n        _apply_metadata(\n            album_info,\n            item,\n            nullable_fields=config[\"overwrite_null\"][\"album\"].as_str_seq(),\n        )\n\n        _apply_metadata(\n            track_info,\n            item,\n            nullable_fields=config[\"overwrite_null\"][\"track\"].as_str_seq(),\n        )\n\n        correct_list_fields(item)\n"
  },
  {
    "path": "beets/autotag/distance.py",
    "content": "from __future__ import annotations\n\nimport datetime\nimport re\nfrom functools import cache, total_ordering\nfrom typing import TYPE_CHECKING, Any\n\nfrom jellyfish import levenshtein_distance\nfrom unidecode import unidecode\n\nfrom beets import config, metadata_plugins\nfrom beets.util import as_string, cached_classproperty, get_most_common_tags\n\nif TYPE_CHECKING:\n    from collections.abc import Iterator, Sequence\n\n    from beets.library import Item\n\n    from .hooks import AlbumInfo, TrackInfo\n\n# Candidate distance scoring.\n\n# Artist signals that indicate \"various artists\". These are used at the\n# album level to determine whether a given release is likely a VA\n# release and also on the track level to to remove the penalty for\n# differing artists.\nVA_ARTISTS = (\"\", \"various artists\", \"various\", \"va\", \"unknown\")\n\n# Parameters for string distance function.\n# Words that can be moved to the end of a string using a comma.\nSD_END_WORDS = [\"the\", \"a\", \"an\"]\n# Reduced weights for certain portions of the string.\nSD_PATTERNS = [\n    (r\"^the \", 0.1),\n    (r\"[\\[\\(]?(ep|single)[\\]\\)]?\", 0.0),\n    (r\"[\\[\\(]?(featuring|feat|ft)[\\. :].+\", 0.1),\n    (r\"\\(.*?\\)\", 0.3),\n    (r\"\\[.*?\\]\", 0.3),\n    (r\"(, )?(pt\\.|part) .+\", 0.2),\n]\n# Replacements to use before testing distance.\nSD_REPLACE = [\n    (r\"&\", \"and\"),\n]\n\n\ndef _string_dist_basic(str1: str, str2: str) -> float:\n    \"\"\"Basic edit distance between two strings, ignoring\n    non-alphanumeric characters and case. Comparisons are based on a\n    transliteration/lowering to ASCII characters. Normalized by string\n    length.\n    \"\"\"\n    assert isinstance(str1, str)\n    assert isinstance(str2, str)\n    str1 = as_string(unidecode(str1))\n    str2 = as_string(unidecode(str2))\n    str1 = re.sub(r\"[^a-z0-9]\", \"\", str1.lower())\n    str2 = re.sub(r\"[^a-z0-9]\", \"\", str2.lower())\n    if not str1 and not str2:\n        return 0.0\n    return levenshtein_distance(str1, str2) / float(max(len(str1), len(str2)))\n\n\ndef string_dist(str1: str | None, str2: str | None) -> float:\n    \"\"\"Gives an \"intuitive\" edit distance between two strings. This is\n    an edit distance, normalized by the string length, with a number of\n    tweaks that reflect intuition about text.\n    \"\"\"\n    if str1 is None and str2 is None:\n        return 0.0\n    if str1 is None or str2 is None:\n        return 1.0\n\n    str1 = str1.lower()\n    str2 = str2.lower()\n\n    # Don't penalize strings that move certain words to the end. For\n    # example, \"the something\" should be considered equal to\n    # \"something, the\".\n    for word in SD_END_WORDS:\n        if str1.endswith(f\", {word}\"):\n            str1 = f\"{word} {str1[: -len(word) - 2]}\"\n        if str2.endswith(f\", {word}\"):\n            str2 = f\"{word} {str2[: -len(word) - 2]}\"\n\n    # Perform a couple of basic normalizing substitutions.\n    for pat, repl in SD_REPLACE:\n        str1 = re.sub(pat, repl, str1)\n        str2 = re.sub(pat, repl, str2)\n\n    # Change the weight for certain string portions matched by a set\n    # of regular expressions. We gradually change the strings and build\n    # up penalties associated with parts of the string that were\n    # deleted.\n    base_dist = _string_dist_basic(str1, str2)\n    penalty = 0.0\n    for pat, weight in SD_PATTERNS:\n        # Get strings that drop the pattern.\n        case_str1 = re.sub(pat, \"\", str1)\n        case_str2 = re.sub(pat, \"\", str2)\n\n        if case_str1 != str1 or case_str2 != str2:\n            # If the pattern was present (i.e., it is deleted in the\n            # the current case), recalculate the distances for the\n            # modified strings.\n            case_dist = _string_dist_basic(case_str1, case_str2)\n            case_delta = max(0.0, base_dist - case_dist)\n            if case_delta == 0.0:\n                continue\n\n            # Shift our baseline strings down (to avoid rematching the\n            # same part of the string) and add a scaled distance\n            # amount to the penalties.\n            str1 = case_str1\n            str2 = case_str2\n            base_dist = case_dist\n            penalty += weight * case_delta\n\n    return base_dist + penalty\n\n\n@total_ordering\nclass Distance:\n    \"\"\"Keeps track of multiple distance penalties. Provides a single\n    weighted distance for all penalties as well as a weighted distance\n    for each individual penalty.\n    \"\"\"\n\n    def __init__(self) -> None:\n        self._penalties: dict[str, list[float]] = {}\n        self.tracks: dict[TrackInfo, Distance] = {}\n\n    @cached_classproperty\n    def _weights(cls) -> dict[str, float]:\n        \"\"\"A dictionary from keys to floating-point weights.\"\"\"\n        weights_view = config[\"match\"][\"distance_weights\"]\n        weights = {}\n        for key in weights_view.keys():\n            weights[key] = weights_view[key].as_number()\n        return weights\n\n    # Access the components and their aggregates.\n\n    @property\n    def distance(self) -> float:\n        \"\"\"Return a weighted and normalized distance across all\n        penalties.\n        \"\"\"\n        dist_max = self.max_distance\n        if dist_max:\n            return self.raw_distance / self.max_distance\n        return 0.0\n\n    @property\n    def max_distance(self) -> float:\n        \"\"\"Return the maximum distance penalty (normalization factor).\"\"\"\n        dist_max = 0.0\n        for key, penalty in self._penalties.items():\n            dist_max += len(penalty) * self._weights[key]\n        return dist_max\n\n    @property\n    def raw_distance(self) -> float:\n        \"\"\"Return the raw (denormalized) distance.\"\"\"\n        dist_raw = 0.0\n        for key, penalty in self._penalties.items():\n            dist_raw += sum(penalty) * self._weights[key]\n        return dist_raw\n\n    def items(self) -> list[tuple[str, float]]:\n        \"\"\"Return a list of (key, dist) pairs, with `dist` being the\n        weighted distance, sorted from highest to lowest. Does not\n        include penalties with a zero value.\n        \"\"\"\n        list_ = []\n        for key in self._penalties:\n            dist = self[key]\n            if dist:\n                list_.append((key, dist))\n        # Convert distance into a negative float we can sort items in\n        # ascending order (for keys, when the penalty is equal) and\n        # still get the items with the biggest distance first.\n        return sorted(\n            list_, key=lambda key_and_dist: (-key_and_dist[1], key_and_dist[0])\n        )\n\n    def __hash__(self) -> int:\n        return id(self)\n\n    def __eq__(self, other) -> bool:\n        return self.distance == other\n\n    # Behave like a float.\n\n    def __lt__(self, other) -> bool:\n        return self.distance < other\n\n    def __float__(self) -> float:\n        return self.distance\n\n    def __sub__(self, other) -> float:\n        return self.distance - other\n\n    def __rsub__(self, other) -> float:\n        return other - self.distance\n\n    def __str__(self) -> str:\n        return f\"{self.distance:.2f}\"\n\n    # Behave like a dict.\n\n    def __getitem__(self, key) -> float:\n        \"\"\"Returns the weighted distance for a named penalty.\"\"\"\n        dist = sum(self._penalties[key]) * self._weights[key]\n        dist_max = self.max_distance\n        if dist_max:\n            return dist / dist_max\n        return 0.0\n\n    def __iter__(self) -> Iterator[tuple[str, float]]:\n        return iter(self.items())\n\n    def __len__(self) -> int:\n        return len(self.items())\n\n    def keys(self) -> list[str]:\n        return [key for key, _ in self.items()]\n\n    def update(self, dist: Distance):\n        \"\"\"Adds all the distance penalties from `dist`.\"\"\"\n        if not isinstance(dist, Distance):\n            raise ValueError(\n                f\"`dist` must be a Distance object, not {type(dist)}\"\n            )\n        for key, penalties in dist._penalties.items():\n            self._penalties.setdefault(key, []).extend(penalties)\n\n    # Adding components.\n\n    def _eq(self, value1: re.Pattern[str] | Any, value2: Any) -> bool:\n        \"\"\"Returns True if `value1` is equal to `value2`. `value1` may\n        be a compiled regular expression, in which case it will be\n        matched against `value2`.\n        \"\"\"\n        if isinstance(value1, re.Pattern):\n            return bool(value1.match(value2))\n        return value1 == value2\n\n    def add(self, key: str, dist: float):\n        \"\"\"Adds a distance penalty. `key` must correspond with a\n        configured weight setting. `dist` must be a float between 0.0\n        and 1.0, and will be added to any existing distance penalties\n        for the same key.\n        \"\"\"\n        if not 0.0 <= dist <= 1.0:\n            raise ValueError(f\"`dist` must be between 0.0 and 1.0, not {dist}\")\n        self._penalties.setdefault(key, []).append(dist)\n\n    def add_equality(\n        self,\n        key: str,\n        value: Any,\n        options: list[Any] | tuple[Any, ...] | Any,\n    ):\n        \"\"\"Adds a distance penalty of 1.0 if `value` doesn't match any\n        of the values in `options`. If an option is a compiled regular\n        expression, it will be considered equal if it matches against\n        `value`.\n        \"\"\"\n        if not isinstance(options, (list, tuple)):\n            options = [options]\n        for opt in options:\n            if self._eq(opt, value):\n                dist = 0.0\n                break\n        else:\n            dist = 1.0\n        self.add(key, dist)\n\n    def add_expr(self, key: str, expr: bool):\n        \"\"\"Adds a distance penalty of 1.0 if `expr` evaluates to True,\n        or 0.0.\n        \"\"\"\n        if expr:\n            self.add(key, 1.0)\n        else:\n            self.add(key, 0.0)\n\n    def add_number(self, key: str, number1: int, number2: int):\n        \"\"\"Adds a distance penalty of 1.0 for each number of difference\n        between `number1` and `number2`, or 0.0 when there is no\n        difference. Use this when there is no upper limit on the\n        difference between the two numbers.\n        \"\"\"\n        diff = abs(number1 - number2)\n        if diff:\n            for i in range(diff):\n                self.add(key, 1.0)\n        else:\n            self.add(key, 0.0)\n\n    def add_priority(\n        self,\n        key: str,\n        value: Any,\n        options: list[Any] | tuple[Any, ...] | Any,\n    ):\n        \"\"\"Adds a distance penalty that corresponds to the position at\n        which `value` appears in `options`. A distance penalty of 0.0\n        for the first option, or 1.0 if there is no matching option. If\n        an option is a compiled regular expression, it will be\n        considered equal if it matches against `value`.\n        \"\"\"\n        if not isinstance(options, (list, tuple)):\n            options = [options]\n        unit = 1.0 / (len(options) or 1)\n        for i, opt in enumerate(options):\n            if self._eq(opt, value):\n                dist = i * unit\n                break\n        else:\n            dist = 1.0\n        self.add(key, dist)\n\n    def add_ratio(\n        self,\n        key: str,\n        number1: int | float,\n        number2: int | float,\n    ):\n        \"\"\"Adds a distance penalty for `number1` as a ratio of `number2`.\n        `number1` is bound at 0 and `number2`.\n        \"\"\"\n        number = float(max(min(number1, number2), 0))\n        if number2:\n            dist = number / number2\n        else:\n            dist = 0.0\n        self.add(key, dist)\n\n    def add_string(self, key: str, str1: str | None, str2: str | None):\n        \"\"\"Adds a distance penalty based on the edit distance between\n        `str1` and `str2`.\n        \"\"\"\n        dist = string_dist(str1, str2)\n        self.add(key, dist)\n\n    def add_data_source(self, before: str | None, after: str | None) -> None:\n        if before != after and (\n            before or len(metadata_plugins.find_metadata_source_plugins()) > 1\n        ):\n            self.add(\"data_source\", metadata_plugins.get_penalty(after))\n\n\n@cache\ndef get_track_length_grace() -> float:\n    \"\"\"Get cached grace period for track length matching.\"\"\"\n    return config[\"match\"][\"track_length_grace\"].as_number()\n\n\n@cache\ndef get_track_length_max() -> float:\n    \"\"\"Get cached maximum track length for track length matching.\"\"\"\n    return config[\"match\"][\"track_length_max\"].as_number()\n\n\ndef track_index_changed(item: Item, track_info: TrackInfo) -> bool:\n    \"\"\"Returns True if the item and track info index is different. Tolerates\n    per disc and per release numbering.\n    \"\"\"\n    return item.track not in (track_info.medium_index, track_info.index)\n\n\ndef track_distance(\n    item: Item,\n    track_info: TrackInfo,\n    incl_artist: bool = False,\n) -> Distance:\n    \"\"\"Determines the significance of a track metadata change. Returns a\n    Distance object. `incl_artist` indicates that a distance component should\n    be included for the track artist (i.e., for various-artist releases).\n\n    ``track_length_grace`` and ``track_length_max`` configuration options are\n    cached because this function is called many times during the matching\n    process and their access comes with a performance overhead.\n    \"\"\"\n    dist = Distance()\n\n    # Length.\n    if info_length := track_info.length:\n        diff = abs(item.length - info_length) - get_track_length_grace()\n        dist.add_ratio(\"track_length\", diff, get_track_length_max())\n\n    # Title.\n    dist.add_string(\"track_title\", item.title, track_info.title)\n\n    # Artist. Only check if there is actually an artist in the track data.\n    if (\n        incl_artist\n        and track_info.artist\n        and item.artist.lower() not in VA_ARTISTS\n    ):\n        dist.add_string(\"track_artist\", item.artist, track_info.artist)\n\n    # Track index.\n    if track_info.index and item.track:\n        dist.add_expr(\"track_index\", track_index_changed(item, track_info))\n\n    # Track ID.\n    if item.mb_trackid:\n        dist.add_expr(\"track_id\", item.mb_trackid != track_info.track_id)\n\n    # Penalize mismatching disc numbers.\n    if track_info.medium and item.disc:\n        dist.add_expr(\"medium\", item.disc != track_info.medium)\n\n    dist.add_data_source(item.get(\"data_source\"), track_info.data_source)\n\n    return dist\n\n\ndef distance(\n    items: Sequence[Item],\n    album_info: AlbumInfo,\n    item_info_pairs: list[tuple[Item, TrackInfo]],\n) -> Distance:\n    \"\"\"Determines how \"significant\" an album metadata change would be.\n    Returns a Distance object. `album_info` is an AlbumInfo object\n    reflecting the album to be compared. `items` is a sequence of all\n    Item objects that will be matched (order is not important).\n    `mapping` is a dictionary mapping Items to TrackInfo objects; the\n    keys are a subset of `items` and the values are a subset of\n    `album_info.tracks`.\n    \"\"\"\n    likelies, _ = get_most_common_tags(items)\n\n    dist = Distance()\n\n    # Artist, if not various.\n    if not album_info.va:\n        dist.add_string(\"artist\", likelies[\"artist\"], album_info.artist)\n\n    # Album.\n    dist.add_string(\"album\", likelies[\"album\"], album_info.album)\n\n    preferred_config = config[\"match\"][\"preferred\"]\n    # Current or preferred media.\n    if album_info.media:\n        # Preferred media options.\n        media_patterns: Sequence[str] = preferred_config[\"media\"].as_str_seq()\n        options = [\n            re.compile(rf\"(\\d+x)?({pat})\", re.I) for pat in media_patterns\n        ]\n        if options:\n            dist.add_priority(\"media\", album_info.media, options)\n        # Current media.\n        elif likelies[\"media\"]:\n            dist.add_equality(\"media\", album_info.media, likelies[\"media\"])\n\n    # Mediums.\n    if likelies[\"disctotal\"] and album_info.mediums:\n        dist.add_number(\"mediums\", likelies[\"disctotal\"], album_info.mediums)\n\n    # Prefer earliest release.\n    if album_info.year and preferred_config[\"original_year\"]:\n        # Assume 1889 (earliest first gramophone discs) if we don't know the\n        # original year.\n        original = album_info.original_year or 1889\n        diff = abs(album_info.year - original)\n        diff_max = abs(datetime.date.today().year - original)\n        dist.add_ratio(\"year\", diff, diff_max)\n    # Year.\n    elif likelies[\"year\"] and album_info.year:\n        if likelies[\"year\"] in (album_info.year, album_info.original_year):\n            # No penalty for matching release or original year.\n            dist.add(\"year\", 0.0)\n        elif album_info.original_year:\n            # Prefer matchest closest to the release year.\n            diff = abs(likelies[\"year\"] - album_info.year)\n            diff_max = abs(\n                datetime.date.today().year - album_info.original_year\n            )\n            dist.add_ratio(\"year\", diff, diff_max)\n        else:\n            # Full penalty when there is no original year.\n            dist.add(\"year\", 1.0)\n\n    # Preferred countries.\n    country_patterns: Sequence[str] = preferred_config[\"countries\"].as_str_seq()\n    options = [re.compile(pat, re.I) for pat in country_patterns]\n    if album_info.country and options:\n        dist.add_priority(\"country\", album_info.country, options)\n    # Country.\n    elif likelies[\"country\"] and album_info.country:\n        dist.add_string(\"country\", likelies[\"country\"], album_info.country)\n\n    # Label.\n    if likelies[\"label\"] and album_info.label:\n        dist.add_string(\"label\", likelies[\"label\"], album_info.label)\n\n    # Catalog number.\n    if likelies[\"catalognum\"] and album_info.catalognum:\n        dist.add_string(\n            \"catalognum\", likelies[\"catalognum\"], album_info.catalognum\n        )\n\n    # Disambiguation.\n    if likelies[\"albumdisambig\"] and album_info.albumdisambig:\n        dist.add_string(\n            \"albumdisambig\", likelies[\"albumdisambig\"], album_info.albumdisambig\n        )\n\n    # Album ID.\n    if likelies[\"mb_albumid\"]:\n        dist.add_equality(\n            \"album_id\", likelies[\"mb_albumid\"], album_info.album_id\n        )\n\n    # Tracks.\n    dist.tracks = {}\n    for item, track in item_info_pairs:\n        dist.tracks[track] = track_distance(item, track, album_info.va)\n        dist.add(\"tracks\", dist.tracks[track].distance)\n\n    # Missing tracks.\n    for _ in range(len(album_info.tracks) - len(item_info_pairs)):\n        dist.add(\"missing_tracks\", 1.0)\n\n    # Unmatched tracks.\n    for _ in range(len(items) - len(item_info_pairs)):\n        dist.add(\"unmatched_tracks\", 1.0)\n\n    dist.add_data_source(likelies[\"data_source\"], album_info.data_source)\n\n    return dist\n"
  },
  {
    "path": "beets/autotag/hooks.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Glue between metadata sources and the matching logic.\"\"\"\n\nfrom __future__ import annotations\n\nfrom copy import deepcopy\nfrom dataclasses import dataclass\nfrom functools import cached_property\nfrom typing import TYPE_CHECKING, Any, TypeVar\n\nfrom typing_extensions import Self\n\nfrom beets import plugins\nfrom beets.util import cached_classproperty\nfrom beets.util.deprecation import deprecate_for_maintainers\n\nif TYPE_CHECKING:\n    from beets.library import Item\n\n    from .distance import Distance\n\nV = TypeVar(\"V\")\n\n\n# Classes used to represent candidate options.\nclass AttrDict(dict[str, V]):\n    \"\"\"Mapping enabling attribute-style access to stored metadata values.\"\"\"\n\n    def copy(self) -> Self:\n        return deepcopy(self)\n\n    def __getattr__(self, attr: str) -> V:\n        if attr in self:\n            return self[attr]\n\n        raise AttributeError(\n            f\"'{self.__class__.__name__}' object has no attribute '{attr}'\"\n        )\n\n    def __setattr__(self, key: str, value: V) -> None:\n        self.__setitem__(key, value)\n\n    def __hash__(self) -> int:  # type: ignore[override]\n        return id(self)\n\n\nclass Info(AttrDict[Any]):\n    \"\"\"Container for metadata about a musical entity.\"\"\"\n\n    Identifier = tuple[str | None, str | None]\n\n    @property\n    def id(self) -> str | None:\n        \"\"\"Return the provider-specific identifier for this metadata object.\"\"\"\n        raise NotImplementedError\n\n    @property\n    def identifier(self) -> Identifier:\n        \"\"\"Return a cross-provider key in ``(data_source, id)`` form.\"\"\"\n        return (self.data_source, self.id)\n\n    @cached_property\n    def name(self) -> str:\n        raise NotImplementedError\n\n    def __init__(\n        self,\n        album: str | None = None,\n        artist_credit: str | None = None,\n        artist_id: str | None = None,\n        artist: str | None = None,\n        artists_credit: list[str] | None = None,\n        artists_ids: list[str] | None = None,\n        artists: list[str] | None = None,\n        artist_sort: str | None = None,\n        artists_sort: list[str] | None = None,\n        data_source: str | None = None,\n        data_url: str | None = None,\n        genre: str | None = None,\n        genres: list[str] | None = None,\n        media: str | None = None,\n        **kwargs,\n    ) -> None:\n        if genre is not None:\n            deprecate_for_maintainers(\n                \"The 'genre' parameter\", \"'genres' (list)\", stacklevel=3\n            )\n            if not genres:\n                try:\n                    sep = next(s for s in [\"; \", \", \", \" / \"] if s in genre)\n                except StopIteration:\n                    genres = [genre]\n                else:\n                    genres = list(map(str.strip, genre.split(sep)))\n\n        self.album = album\n        self.artist = artist\n        self.artist_credit = artist_credit\n        self.artist_id = artist_id\n        self.artists = artists\n        self.artists_credit = artists_credit\n        self.artists_ids = artists_ids\n        self.artist_sort = artist_sort\n        self.artists_sort = artists_sort\n        self.data_source = data_source\n        self.data_url = data_url\n        self.genre = None\n        self.genres = genres\n        self.media = media\n        self.update(kwargs)\n\n\nclass AlbumInfo(Info):\n    \"\"\"Metadata snapshot representing a single album candidate.\n\n    Aggregates track entries and album-wide context gathered from an external\n    provider. Used during matching to evaluate similarity against a group of\n    user items, and later to drive tagging decisions once selected.\n    \"\"\"\n\n    @property\n    def id(self) -> str | None:\n        return self.album_id\n\n    @cached_property\n    def name(self) -> str:\n        return self.album or \"\"\n\n    def __init__(\n        self,\n        tracks: list[TrackInfo],\n        *,\n        album_id: str | None = None,\n        albumdisambig: str | None = None,\n        albumstatus: str | None = None,\n        albumtype: str | None = None,\n        albumtypes: list[str] | None = None,\n        asin: str | None = None,\n        barcode: str | None = None,\n        catalognum: str | None = None,\n        country: str | None = None,\n        day: int | None = None,\n        discogs_albumid: str | None = None,\n        discogs_artistid: str | None = None,\n        discogs_labelid: str | None = None,\n        label: str | None = None,\n        language: str | None = None,\n        mediums: int | None = None,\n        month: int | None = None,\n        original_day: int | None = None,\n        original_month: int | None = None,\n        original_year: int | None = None,\n        release_group_title: str | None = None,\n        releasegroup_id: str | None = None,\n        releasegroupdisambig: str | None = None,\n        script: str | None = None,\n        style: str | None = None,\n        va: bool = False,\n        year: int | None = None,\n        **kwargs,\n    ) -> None:\n        self.tracks = tracks\n        self.album_id = album_id\n        self.albumdisambig = albumdisambig\n        self.albumstatus = albumstatus\n        self.albumtype = albumtype\n        self.albumtypes = albumtypes\n        self.asin = asin\n        self.barcode = barcode\n        self.catalognum = catalognum\n        self.country = country\n        self.day = day\n        self.discogs_albumid = discogs_albumid\n        self.discogs_artistid = discogs_artistid\n        self.discogs_labelid = discogs_labelid\n        self.label = label\n        self.language = language\n        self.mediums = mediums\n        self.month = month\n        self.original_day = original_day\n        self.original_month = original_month\n        self.original_year = original_year\n        self.release_group_title = release_group_title\n        self.releasegroup_id = releasegroup_id\n        self.releasegroupdisambig = releasegroupdisambig\n        self.script = script\n        self.style = style\n        self.va = va\n        self.year = year\n        super().__init__(**kwargs)\n\n\nclass TrackInfo(Info):\n    \"\"\"Metadata snapshot for a single track candidate.\n\n    Captures identifying details and creative credits used to compare against\n    a user's item. Instances often originate within an AlbumInfo but may also\n    stand alone for singleton matching.\n    \"\"\"\n\n    @property\n    def id(self) -> str | None:\n        return self.track_id\n\n    @cached_property\n    def name(self) -> str:\n        return self.title or \"\"\n\n    def __init__(\n        self,\n        *,\n        arranger: str | None = None,\n        bpm: str | None = None,\n        composer: str | None = None,\n        composer_sort: str | None = None,\n        disctitle: str | None = None,\n        index: int | None = None,\n        initial_key: str | None = None,\n        length: float | None = None,\n        lyricist: str | None = None,\n        mb_workid: str | None = None,\n        medium: int | None = None,\n        medium_index: int | None = None,\n        medium_total: int | None = None,\n        release_track_id: str | None = None,\n        title: str | None = None,\n        track_alt: str | None = None,\n        track_id: str | None = None,\n        work: str | None = None,\n        work_disambig: str | None = None,\n        **kwargs,\n    ) -> None:\n        self.arranger = arranger\n        self.bpm = bpm\n        self.composer = composer\n        self.composer_sort = composer_sort\n        self.disctitle = disctitle\n        self.index = index\n        self.initial_key = initial_key\n        self.length = length\n        self.lyricist = lyricist\n        self.mb_workid = mb_workid\n        self.medium = medium\n        self.medium_index = medium_index\n        self.medium_total = medium_total\n        self.release_track_id = release_track_id\n        self.title = title\n        self.track_alt = track_alt\n        self.track_id = track_id\n        self.work = work\n        self.work_disambig = work_disambig\n        super().__init__(**kwargs)\n\n\n# Structures that compose all the information for a candidate match.\n@dataclass\nclass Match:\n    distance: Distance\n    info: Info\n\n    @cached_classproperty\n    def type(cls) -> str:\n        return cls.__name__.removesuffix(\"Match\")  # type: ignore[attr-defined]\n\n\n@dataclass\nclass AlbumMatch(Match):\n    info: AlbumInfo\n    mapping: dict[Item, TrackInfo]\n    extra_items: list[Item]\n    extra_tracks: list[TrackInfo]\n\n    def __post_init__(self) -> None:\n        \"\"\"Notify listeners when an album candidate has been matched.\"\"\"\n        plugins.send(\"album_matched\", match=self)\n\n    @property\n    def item_info_pairs(self) -> list[tuple[Item, TrackInfo]]:\n        return list(self.mapping.items())\n\n    @property\n    def items(self) -> list[Item]:\n        return [i for i, _ in self.item_info_pairs]\n\n\n@dataclass\nclass TrackMatch(Match):\n    info: TrackInfo\n"
  },
  {
    "path": "beets/autotag/match.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Matches existing metadata with canonical information to identify\nreleases and tracks.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom enum import IntEnum\nfrom typing import TYPE_CHECKING, NamedTuple, TypeVar\n\nimport lap\nimport numpy as np\n\nfrom beets import config, logging, metadata_plugins\nfrom beets.autotag import AlbumMatch, TrackMatch, hooks\nfrom beets.util import get_most_common_tags\n\nfrom .distance import VA_ARTISTS, distance, track_distance\nfrom .hooks import Info\n\nif TYPE_CHECKING:\n    from collections.abc import Iterable, Sequence\n\n    from beets.autotag import AlbumInfo, TrackInfo\n    from beets.library import Item\n\n\nAnyMatch = TypeVar(\"AnyMatch\", TrackMatch, AlbumMatch)\nCandidates = dict[Info.Identifier, AnyMatch]\n\n# Global logger.\nlog = logging.getLogger(\"beets\")\n\n\n# Recommendation enumeration.\n\n\nclass Recommendation(IntEnum):\n    \"\"\"Indicates a qualitative suggestion to the user about what should\n    be done with a given match.\n    \"\"\"\n\n    none = 0\n    low = 1\n    medium = 2\n    strong = 3\n\n\n# A structure for holding a set of possible matches to choose between. This\n# consists of a list of possible candidates (i.e., AlbumInfo or TrackInfo\n# objects) and a recommendation value.\n\n\nclass Proposal(NamedTuple):\n    candidates: Sequence[AlbumMatch | TrackMatch]\n    recommendation: Recommendation\n\n\n# Primary matching functionality.\n\n\ndef assign_items(\n    items: Sequence[Item],\n    tracks: Sequence[TrackInfo],\n) -> tuple[list[tuple[Item, TrackInfo]], list[Item], list[TrackInfo]]:\n    \"\"\"Given a list of Items and a list of TrackInfo objects, find the\n    best mapping between them. Returns a mapping from Items to TrackInfo\n    objects, a set of extra Items, and a set of extra TrackInfo\n    objects. These \"extra\" objects occur when there is an unequal number\n    of objects of the two types.\n    \"\"\"\n    log.debug(\"Computing track assignment...\")\n    # Construct the cost matrix.\n    costs = [[float(track_distance(i, t)) for t in tracks] for i in items]\n    # Assign items to tracks\n    _, _, assigned_item_idxs = lap.lapjv(np.array(costs), extend_cost=True)\n    log.debug(\"...done.\")\n\n    # Each item in `assigned_item_idxs` list corresponds to a track in the\n    # `tracks` list. Each value is either an index into the assigned item in\n    # `items` list, or -1 if that track has no match.\n    mapping = {\n        items[iidx]: t\n        for iidx, t in zip(assigned_item_idxs, tracks)\n        if iidx != -1\n    }\n    extra_items = list(set(items) - mapping.keys())\n    extra_items.sort(key=lambda i: (i.disc, i.track, i.title))\n    extra_tracks = list(set(tracks) - set(mapping.values()))\n    extra_tracks.sort(key=lambda t: (t.index, t.title))\n    return list(mapping.items()), extra_items, extra_tracks\n\n\ndef match_by_id(album_id: str | None, consensus: bool) -> Iterable[AlbumInfo]:\n    \"\"\"Return album candidates for the given album id.\n\n    Make sure that the ID is present and that there is consensus on it among\n    the items being tagged.\n    \"\"\"\n    if not album_id:\n        log.debug(\"No album ID found.\")\n    elif not consensus:\n        log.debug(\"No album ID consensus.\")\n    else:\n        log.debug(\"Searching for discovered album ID: {}\", album_id)\n        return metadata_plugins.albums_for_ids([album_id])\n\n    return ()\n\n\ndef _recommendation(\n    results: Sequence[AlbumMatch | TrackMatch],\n) -> Recommendation:\n    \"\"\"Given a sorted list of AlbumMatch or TrackMatch objects, return a\n    recommendation based on the results' distances.\n\n    If the recommendation is higher than the configured maximum for\n    an applied penalty, the recommendation will be downgraded to the\n    configured maximum for that penalty.\n    \"\"\"\n    if not results:\n        # No candidates: no recommendation.\n        return Recommendation.none\n\n    # Basic distance thresholding.\n    min_dist = results[0].distance\n    if min_dist < config[\"match\"][\"strong_rec_thresh\"].as_number():\n        # Strong recommendation level.\n        rec = Recommendation.strong\n    elif min_dist <= config[\"match\"][\"medium_rec_thresh\"].as_number():\n        # Medium recommendation level.\n        rec = Recommendation.medium\n    elif len(results) == 1:\n        # Only a single candidate.\n        rec = Recommendation.low\n    elif (\n        results[1].distance - min_dist\n        >= config[\"match\"][\"rec_gap_thresh\"].as_number()\n    ):\n        # Gap between first two candidates is large.\n        rec = Recommendation.low\n    else:\n        # No conclusion. Return immediately. Can't be downgraded any further.\n        return Recommendation.none\n\n    # Downgrade to the max rec if it is lower than the current rec for an\n    # applied penalty.\n    keys = set(min_dist.keys())\n    if isinstance(results[0], hooks.AlbumMatch):\n        for track_dist in min_dist.tracks.values():\n            keys.update(list(track_dist.keys()))\n    max_rec_view = config[\"match\"][\"max_rec\"]\n    for key in keys:\n        if key in list(max_rec_view.keys()):\n            max_rec = max_rec_view[key].as_choice(\n                {\n                    \"strong\": Recommendation.strong,\n                    \"medium\": Recommendation.medium,\n                    \"low\": Recommendation.low,\n                    \"none\": Recommendation.none,\n                }\n            )\n            rec = min(rec, max_rec)\n\n    return rec\n\n\ndef _sort_candidates(candidates: Iterable[AnyMatch]) -> Sequence[AnyMatch]:\n    \"\"\"Sort candidates by distance.\"\"\"\n    return sorted(candidates, key=lambda match: match.distance)\n\n\ndef _add_candidate(\n    items: Sequence[Item],\n    results: Candidates[AlbumMatch],\n    info: AlbumInfo,\n):\n    \"\"\"Given a candidate AlbumInfo object, attempt to add the candidate\n    to the output dictionary of AlbumMatch objects. This involves\n    checking the track count, ordering the items, checking for\n    duplicates, and calculating the distance.\n    \"\"\"\n    log.debug(\n        \"Candidate: {0.artist} - {0.album} ({0.album_id}) from {0.data_source}\",\n        info,\n    )\n\n    # Discard albums with zero tracks.\n    if not info.tracks:\n        log.debug(\"No tracks.\")\n        return\n\n    # Prevent duplicates.\n    if info.album_id and info.identifier in results:\n        log.debug(\"Duplicate.\")\n        return\n\n    # Discard matches without required tags.\n    required_tags: Sequence[str] = config[\"match\"][\"required\"].as_str_seq()\n    for req_tag in required_tags:\n        if getattr(info, req_tag) is None:\n            log.debug(\"Ignored. Missing required tag: {}\", req_tag)\n            return\n\n    # Find mapping between the items and the track info.\n    item_info_pairs, extra_items, extra_tracks = assign_items(\n        items, info.tracks\n    )\n\n    # Get the change distance.\n    dist = distance(items, info, item_info_pairs)\n\n    # Skip matches with ignored penalties.\n    penalties = [key for key, _ in dist]\n    ignored_tags: Sequence[str] = config[\"match\"][\"ignored\"].as_str_seq()\n    for penalty in ignored_tags:\n        if penalty in penalties:\n            log.debug(\"Ignored. Penalty: {}\", penalty)\n            return\n\n    log.debug(\"Success. Distance: {}\", dist)\n    results[info.identifier] = hooks.AlbumMatch(\n        dist, info, dict(item_info_pairs), extra_items, extra_tracks\n    )\n\n\ndef tag_album(\n    items,\n    search_artist: str | None = None,\n    search_name: str | None = None,\n    search_ids: list[str] = [],\n) -> tuple[str, str, Proposal]:\n    \"\"\"Return a tuple of the current artist name, the current album\n    name, and a `Proposal` containing `AlbumMatch` candidates.\n\n    The artist and album are the most common values of these fields\n    among `items`.\n\n    The `AlbumMatch` objects are generated by searching the metadata\n    backends. By default, the metadata of the items is used for the\n    search. This can be customized by setting the parameters.\n    `search_ids` is a list of metadata backend IDs: if specified,\n    it will restrict the candidates to those IDs, ignoring\n    `search_artist` and `search album`. The `mapping` field of the\n    album has the matched `items` as keys.\n\n    The recommendation is calculated from the match quality of the\n    candidates.\n    \"\"\"\n    # Get current metadata.\n    likelies, consensus = get_most_common_tags(items)\n    cur_artist: str = likelies[\"artist\"]\n    cur_album: str = likelies[\"album\"]\n    log.debug(\"Tagging {} - {}\", cur_artist, cur_album)\n\n    # The output result, keys are (data_source, album_id) pairs, values are\n    # AlbumMatch objects.\n    candidates: Candidates[AlbumMatch] = {}\n\n    # Search by explicit ID.\n    if search_ids:\n        log.debug(\"Searching for album IDs: {}\", \", \".join(search_ids))\n        for _info in metadata_plugins.albums_for_ids(search_ids):\n            _add_candidate(items, candidates, _info)\n\n    # Use existing metadata or text search.\n    else:\n        # Try search based on current ID.\n        for info in match_by_id(\n            likelies[\"mb_albumid\"], consensus[\"mb_albumid\"]\n        ):\n            _add_candidate(items, candidates, info)\n\n        rec = _recommendation(list(candidates.values()))\n        log.debug(\"Album ID match recommendation is {}\", rec)\n        if candidates and not config[\"import\"][\"timid\"]:\n            # If we have a very good MBID match, return immediately.\n            # Otherwise, this match will compete against metadata-based\n            # matches.\n            if rec == Recommendation.strong:\n                log.debug(\"ID match.\")\n                return (\n                    cur_artist,\n                    cur_album,\n                    Proposal(list(candidates.values()), rec),\n                )\n\n        # Search terms.\n        if not (search_artist and search_name):\n            # No explicit search terms -- use current metadata.\n            search_artist, search_name = cur_artist, cur_album\n        log.debug(\"Search terms: {} - {}\", search_artist, search_name)\n\n        # Is this album likely to be a \"various artist\" release?\n        va_likely = (\n            (not consensus[\"artist\"])\n            or (search_artist.lower() in VA_ARTISTS)\n            or any(item.comp for item in items)\n        )\n        log.debug(\"Album might be VA: {}\", va_likely)\n\n        # Get the results from the data sources.\n        for matched_candidate in metadata_plugins.candidates(\n            items, search_artist, search_name, va_likely\n        ):\n            _add_candidate(items, candidates, matched_candidate)\n\n    log.debug(\"Evaluating {} candidates.\", len(candidates))\n    # Sort and get the recommendation.\n    candidates_sorted = _sort_candidates(candidates.values())\n    rec = _recommendation(candidates_sorted)\n    return cur_artist, cur_album, Proposal(candidates_sorted, rec)\n\n\ndef tag_item(\n    item,\n    search_artist: str | None = None,\n    search_name: str | None = None,\n    search_ids: list[str] | None = None,\n) -> Proposal:\n    \"\"\"Find metadata for a single track. Return a `Proposal` consisting\n    of `TrackMatch` objects.\n\n    `search_artist` and `search_title` may be used to override the item\n    metadata in the search query. `search_ids` may be used for restricting the\n    search to a list of metadata backend IDs.\n    \"\"\"\n    # Holds candidates found so far: keys are (data_source, track_id) pairs,\n    # values TrackMatch objects\n    candidates: Candidates[TrackMatch] = {}\n    rec: Recommendation | None = None\n\n    # First, try matching by the external source ID.\n    trackids = search_ids or [t for t in [item.mb_trackid] if t]\n    if trackids:\n        log.debug(\"Searching for track IDs: {}\", \", \".join(trackids))\n        for info in metadata_plugins.tracks_for_ids(trackids):\n            dist = track_distance(item, info, incl_artist=True)\n            candidates[info.identifier] = hooks.TrackMatch(dist, info)\n\n        # If this is a good match, then don't keep searching.\n        rec = _recommendation(_sort_candidates(candidates.values()))\n        if rec == Recommendation.strong and not config[\"import\"][\"timid\"]:\n            log.debug(\"Track ID match.\")\n            return Proposal(_sort_candidates(candidates.values()), rec)\n\n    # If we're searching by ID, don't proceed.\n    if search_ids:\n        if candidates:\n            assert rec is not None\n            return Proposal(_sort_candidates(candidates.values()), rec)\n        else:\n            return Proposal([], Recommendation.none)\n\n    # Search terms.\n    search_artist = search_artist or item.artist\n    search_name = search_name or item.title\n    log.debug(\"Item search terms: {} - {}\", search_artist, search_name)\n\n    # Get and evaluate candidate metadata.\n    for track_info in metadata_plugins.item_candidates(\n        item, search_artist, search_name\n    ):\n        dist = track_distance(item, track_info, incl_artist=True)\n        candidates[track_info.identifier] = hooks.TrackMatch(dist, track_info)\n\n    # Sort by distance and return with recommendation.\n    log.debug(\"Found {} candidates.\", len(candidates))\n    candidates_sorted = _sort_candidates(candidates.values())\n    rec = _recommendation(candidates_sorted)\n    return Proposal(candidates_sorted, rec)\n"
  },
  {
    "path": "beets/config_default.yaml",
    "content": "# --------------- Main ---------------\n\nlibrary: library.db\ndirectory: ~/Music\nstatefile: state.pickle\n\n# --------------- Plugins ---------------\n\nplugins: [musicbrainz]\n\npluginpath: []\n\nraise_on_error: no\n\n# --------------- Import ---------------\n\nclutter: [\"Thumbs.DB\", \".DS_Store\"]\nignore: [\".*\", \"*~\", \"System Volume Information\", \"lost+found\"]\nignore_hidden: yes\n\nimport:\n    # common options\n    write: yes\n    copy: yes\n    move: no\n    timid: no\n    quiet: no\n    log:\n    # other options\n    default_action: apply\n    languages: []\n    quiet_fallback: skip\n    none_rec_action: ask\n    # rare options\n    link: no\n    hardlink: no\n    reflink: no\n    delete: no\n    resume: ask\n    incremental: no\n    incremental_skip_later: no\n    from_scratch: no\n    autotag: yes\n    singletons: no\n    detail: no\n    flat: no\n    group_albums: no\n    pretend: false\n    search_ids: []\n    duplicate_keys:\n        album: albumartist album\n        item: artist title\n    duplicate_action: ask\n    duplicate_verbose_prompt: no\n    bell: no\n    set_fields: {}\n    ignored_alias_types: []\n    singleton_album_disambig: yes\n\n# --------------- Paths ---------------\n\npath_sep_replace: _\ndrive_sep_replace: _\nasciify_paths: false\nart_filename: cover\nmax_filename_length: 0\nreplace:\n    # Replace bad characters with _\n    # prohibited in many filesystem paths\n    '[<>:\\?\\*\\|]': _\n    # double quotation mark \"\n    '\\\"': _\n    # path separators: \\ or /\n    '[\\\\/]': _\n    # starting and closing periods\n    '^\\.': _\n    '\\.$': _\n    # control characters\n    '[\\x00-\\x1f]': _\n    # dash at the start of a filename (causes command line ambiguity)\n    '^-': _\n    # Replace bad characters with nothing\n    # starting and closing whitespace\n    '\\s+$': ''\n    '^\\s+': ''\n\naunique:\n    keys: albumartist album\n    disambiguators: albumtype year label catalognum albumdisambig releasegroupdisambig\n    bracket: '[]'\n\nsunique:\n    keys: artist title\n    disambiguators: year trackdisambig\n    bracket: '[]'\n\n# --------------- Tagging ---------------\n\nper_disc_numbering: no\noriginal_date: no\nartist_credit: no\nid3v23: no\nva_name: \"Various Artists\"\npaths:\n    default: $albumartist/$album%aunique{}/$track $title\n    singleton: Non-Album/$artist/$title\n    comp: Compilations/$album%aunique{}/$track $title\n\n# --------------- Performance ---------------\n\nthreaded: yes\ntimeout: 5.0\n\n# --------------- UI ---------------\n\nverbose: 0\nterminal_encoding:\n\nui:\n    terminal_width: 80\n    length_diff_thresh: 10.0\n    color: yes\n    colors:\n        text_success: ['bold', 'green']\n        text_warning: ['bold', 'yellow']\n        text_error: ['bold', 'red']\n        text_highlight: ['bold', 'red']\n        text_highlight_minor: ['white']\n        action_default: ['bold', 'cyan']\n        action: ['bold', 'cyan']\n        # New Colors\n        text_faint: ['faint']\n        import_path: ['bold', 'blue']\n        import_path_items: ['bold', 'blue']\n        changed: ['yellow']\n        text_diff_added: ['bold', 'green']\n        text_diff_removed: ['bold', 'red']\n        action_description: ['white']\n    import:\n        indentation:\n            match_header: 2\n            match_details: 2\n            match_tracklist: 5\n        layout: column\n\n# --------------- Search ---------------\n\nformat_item: $artist - $album - $title\nformat_album: $albumartist - $album\ntime_format: '%Y-%m-%d %H:%M:%S'\nformat_raw_length: no\n\nsort_album: albumartist+ album+\nsort_item: artist+ album+ disc+ track+\nsort_case_insensitive: yes\n\n# --------------- Autotagger ---------------\n\noverwrite_null:\n  album: []\n  track: []\n\nmatch:\n    strong_rec_thresh: 0.04\n    medium_rec_thresh: 0.25\n    rec_gap_thresh: 0.25\n    max_rec:\n        missing_tracks: medium\n        unmatched_tracks: medium\n    distance_weights:\n        data_source: 2.0\n        artist: 3.0\n        album: 3.0\n        media: 1.0\n        mediums: 1.0\n        year: 1.0\n        country: 0.5\n        label: 0.5\n        catalognum: 0.5\n        albumdisambig: 0.5\n        album_id: 5.0\n        tracks: 2.0\n        missing_tracks: 0.9\n        unmatched_tracks: 0.6\n        track_title: 3.0\n        track_artist: 2.0\n        track_index: 1.0\n        track_length: 2.0\n        track_id: 5.0\n        medium: 1.0\n    preferred:\n        countries: []\n        media: []\n        original_year: no\n    ignored: []\n    required: []\n    ignored_media: []\n    ignore_data_tracks: yes\n    ignore_video_tracks: yes\n    track_length_grace: 10\n    track_length_max: 30\n    album_disambig_fields: data_source media year country label catalognum albumdisambig\n    singleton_disambig_fields: data_source index track_alt album\n"
  },
  {
    "path": "beets/dbcore/__init__.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"DBCore is an abstract database package that forms the basis for beets'\nLibrary.\n\"\"\"\n\nfrom .db import Database, Index, Model, Results\nfrom .query import (\n    AndQuery,\n    FieldQuery,\n    InvalidQueryError,\n    MatchQuery,\n    OrQuery,\n    Query,\n)\nfrom .queryparse import (\n    parse_sorted_query,\n    query_from_strings,\n    sort_from_strings,\n)\nfrom .types import Type\n\n__all__ = [\n    \"AndQuery\",\n    \"Database\",\n    \"FieldQuery\",\n    \"Index\",\n    \"InvalidQueryError\",\n    \"MatchQuery\",\n    \"Model\",\n    \"OrQuery\",\n    \"Query\",\n    \"Results\",\n    \"Type\",\n    \"parse_sorted_query\",\n    \"query_from_strings\",\n    \"sort_from_strings\",\n]\n"
  },
  {
    "path": "beets/dbcore/db.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"The central Model and Database constructs for DBCore.\"\"\"\n\nfrom __future__ import annotations\n\nimport functools\nimport os\nimport re\nimport sqlite3\nimport sys\nimport threading\nimport time\nfrom abc import ABC, abstractmethod\nfrom collections import defaultdict\nfrom collections.abc import Mapping\nfrom contextlib import contextmanager\nfrom dataclasses import dataclass\nfrom functools import cached_property\nfrom sqlite3 import Connection, sqlite_version_info\nfrom typing import (\n    TYPE_CHECKING,\n    Any,\n    AnyStr,\n    ClassVar,\n    Generic,\n    Literal,\n    NamedTuple,\n    TypedDict,\n)\n\nfrom typing_extensions import (\n    Self,\n    TypeVar,  # default value support\n)\nfrom unidecode import unidecode\n\nimport beets\n\nfrom ..util import cached_classproperty, functemplate\nfrom . import types\nfrom .query import MatchQuery, NullSort, TrueQuery\n\nif TYPE_CHECKING:\n    from collections.abc import (\n        Callable,\n        Generator,\n        Iterable,\n        Iterator,\n        Sequence,\n    )\n    from sqlite3 import Connection\n    from types import TracebackType\n\n    from .query import FieldQueryType, FieldSort, Query, Sort, SQLiteType\n\nD = TypeVar(\"D\", bound=\"Database\", default=Any)\n\nFlexAttrs = dict[str, str]\n\n\nclass DBAccessError(Exception):\n    \"\"\"The SQLite database became inaccessible.\n\n    This can happen when trying to read or write the database when, for\n    example, the database file is deleted or otherwise disappears. There\n    is probably no way to recover from this error.\n    \"\"\"\n\n\nclass DBCustomFunctionError(Exception):\n    \"\"\"A sqlite function registered by beets failed.\"\"\"\n\n    def __init__(self):\n        super().__init__(\n            \"beets defined SQLite function failed; \"\n            \"see the other errors above for details\"\n        )\n\n\nclass NotFoundError(LookupError):\n    pass\n\n\nclass FormattedMapping(Mapping[str, str]):\n    \"\"\"A `dict`-like formatted view of a model.\n\n    The accessor `mapping[key]` returns the formatted version of\n    `model[key]` as a unicode string.\n\n    The `included_keys` parameter allows filtering the fields that are\n    returned. By default all fields are returned. Limiting to specific keys can\n    avoid expensive per-item database queries.\n\n    If `for_path` is true, all path separators in the formatted values\n    are replaced.\n    \"\"\"\n\n    model: Model\n\n    ALL_KEYS = \"*\"\n\n    def __init__(\n        self,\n        model: Model,\n        included_keys: str = ALL_KEYS,\n        for_path: bool = False,\n    ):\n        self.for_path = for_path\n        self.model = model\n        if included_keys == self.ALL_KEYS:\n            # Performance note: this triggers a database query.\n            self.model_keys = self.model.keys(True)\n        else:\n            self.model_keys = included_keys\n\n    def __getitem__(self, key: str) -> str:\n        if key in self.model_keys:\n            return self._get_formatted(self.model, key)\n        else:\n            raise KeyError(key)\n\n    def __iter__(self) -> Iterator[str]:\n        return iter(self.model_keys)\n\n    def __len__(self) -> int:\n        return len(self.model_keys)\n\n    # The following signature is incompatible with `Mapping[str, str]`, since\n    # the return type doesn't include `None` (but `default` can be `None`).\n    def get(  # type: ignore\n        self,\n        key: str,\n        default: str | None = None,\n    ) -> str:\n        \"\"\"Similar to Mapping.get(key, default), but always formats to str.\"\"\"\n        if default is None:\n            default = self.model._type(key).format(None)\n        return super().get(key, default)\n\n    def _get_formatted(self, model: Model, key: str) -> str:\n        value = model._type(key).format(model.get(key))\n        if isinstance(value, bytes):\n            value = value.decode(\"utf-8\", \"ignore\")\n\n        if self.for_path:\n            sep_repl: str = beets.config[\"path_sep_replace\"].as_str()\n            sep_drive: str = beets.config[\"drive_sep_replace\"].as_str()\n\n            if re.match(r\"^[a-zA-Z]:\", value):\n                value = re.sub(r\"(?<=[a-zA-Z]):\", sep_drive, value)\n\n            for sep in (os.path.sep, os.path.altsep):\n                if sep:\n                    value = value.replace(sep, sep_repl)\n\n        return value\n\n\n# NOTE: This seems like it should be a `Mapping`, i.e.\n# ```\n# class LazyConvertDict(Mapping[str, Any])\n# ```\n# but there are some conflicts with the `Mapping` protocol such that we\n# can't do this without changing behaviour: In particular, iterators returned\n# by some methods build intermediate lists, such that modification of the\n# `LazyConvertDict` becomes safe during iteration. Some code does in fact rely\n# on this.\nclass LazyConvertDict:\n    \"\"\"Lazily convert types for attributes fetched from the database\"\"\"\n\n    def __init__(self, model_cls: Model):\n        \"\"\"Initialize the object empty\"\"\"\n        # FIXME: Dict[str, SQLiteType]\n        self._data: dict[str, Any] = {}\n        self.model_cls = model_cls\n        self._converted: dict[str, Any] = {}\n\n    def init(self, data: dict[str, Any]):\n        \"\"\"Set the base data that should be lazily converted\"\"\"\n        self._data = data\n\n    def _convert(self, key: str, value: Any):\n        \"\"\"Convert the attribute type according to the SQL type\"\"\"\n        return self.model_cls._type(key).from_sql(value)\n\n    def __setitem__(self, key: str, value: Any):\n        \"\"\"Set an attribute value, assume it's already converted\"\"\"\n        self._converted[key] = value\n\n    def __getitem__(self, key: str) -> Any:\n        \"\"\"Get an attribute value, converting the type on demand\n        if needed\n        \"\"\"\n        if key in self._converted:\n            return self._converted[key]\n        elif key in self._data:\n            value = self._convert(key, self._data[key])\n            self._converted[key] = value\n            return value\n\n    def __delitem__(self, key: str):\n        \"\"\"Delete both converted and base data\"\"\"\n        if key in self._converted:\n            del self._converted[key]\n        if key in self._data:\n            del self._data[key]\n\n    def keys(self) -> list[str]:\n        \"\"\"Get a list of available field names for this object.\"\"\"\n        return list(self._converted.keys()) + list(self._data.keys())\n\n    def copy(self) -> LazyConvertDict:\n        \"\"\"Create a copy of the object.\"\"\"\n        new = self.__class__(self.model_cls)\n        new._data = self._data.copy()\n        new._converted = self._converted.copy()\n        return new\n\n    # Act like a dictionary.\n\n    def update(self, values: Mapping[str, Any]):\n        \"\"\"Assign all values in the given dict.\"\"\"\n        for key, value in values.items():\n            self[key] = value\n\n    def items(self) -> Iterable[tuple[str, Any]]:\n        \"\"\"Iterate over (key, value) pairs that this object contains.\n        Computed fields are not included.\n        \"\"\"\n        for key in self:\n            yield key, self[key]\n\n    def get(self, key: str, default: Any | None = None):\n        \"\"\"Get the value for a given key or `default` if it does not\n        exist.\n        \"\"\"\n        if key in self:\n            return self[key]\n        else:\n            return default\n\n    def __contains__(self, key: Any) -> bool:\n        \"\"\"Determine whether `key` is an attribute on this object.\"\"\"\n        return key in self._converted or key in self._data\n\n    def __iter__(self) -> Iterator[str]:\n        \"\"\"Iterate over the available field names (excluding computed\n        fields).\n        \"\"\"\n        # NOTE: It would be nice to use the following:\n        # yield from self._converted\n        # yield from self._data\n        # but that won't work since some code relies on modifying `self`\n        # during iteration.\n        return iter(self.keys())\n\n    def __len__(self) -> int:\n        # FIXME: This is incorrect due to duplication of keys\n        return len(self._converted) + len(self._data)\n\n\n# Abstract base for model classes.\n\n\nclass Model(ABC, Generic[D]):\n    \"\"\"An abstract object representing an object in the database. Model\n    objects act like dictionaries (i.e., they allow subscript access like\n    ``obj['field']``). The same field set is available via attribute\n    access as a shortcut (i.e., ``obj.field``). Three kinds of attributes are\n    available:\n\n    * **Fixed attributes** come from a predetermined list of field\n      names. These fields correspond to SQLite table columns and are\n      thus fast to read, write, and query.\n    * **Flexible attributes** are free-form and do not need to be listed\n      ahead of time.\n    * **Computed attributes** are read-only fields computed by a getter\n      function provided by a plugin.\n\n    Access to all three field types is uniform: ``obj.field`` works the\n    same regardless of whether ``field`` is fixed, flexible, or\n    computed.\n\n    Model objects can optionally be associated with a `Library` object,\n    in which case they can be loaded and stored from the database. Dirty\n    flags are used to track which fields need to be stored.\n    \"\"\"\n\n    # Abstract components (to be provided by subclasses).\n\n    _table: str\n    \"\"\"The main SQLite table name.\n    \"\"\"\n\n    _flex_table: str\n    \"\"\"The flex field SQLite table name.\n    \"\"\"\n\n    _fields: ClassVar[dict[str, types.Type]] = {}\n    \"\"\"A mapping indicating available \"fixed\" fields on this type. The\n    keys are field names and the values are `Type` objects.\n    \"\"\"\n\n    _search_fields: Sequence[str] = ()\n    \"\"\"The fields that should be queried by default by unqualified query\n    terms.\n    \"\"\"\n\n    _indices: Sequence[Index] = ()\n    \"\"\"A sequence of `Index` objects that describe the indices to be\n    created for this table.\n    \"\"\"\n\n    @cached_classproperty\n    def _types(cls) -> dict[str, types.Type]:\n        \"\"\"Optional types for non-fixed (flexible and computed) fields.\"\"\"\n        return {}\n\n    _sorts: ClassVar[dict[str, type[FieldSort]]] = {}\n    \"\"\"Optional named sort criteria. The keys are strings and the values\n    are subclasses of `Sort`.\n    \"\"\"\n\n    @cached_classproperty\n    def _queries(cls) -> dict[str, FieldQueryType]:\n        \"\"\"Named queries that use a field-like `name:value` syntax but which\n        do not relate to any specific field.\n        \"\"\"\n        return {}\n\n    _always_dirty = False\n    \"\"\"By default, fields only become \"dirty\" when their value actually\n    changes. Enabling this flag marks fields as dirty even when the new\n    value is the same as the old value (e.g., `o.f = o.f`).\n    \"\"\"\n\n    _revision = -1\n    \"\"\"A revision number from when the model was loaded from or written\n    to the database.\n    \"\"\"\n\n    @cached_classproperty\n    def _relation(cls):\n        \"\"\"The model that this model is closely related to.\"\"\"\n        return cls\n\n    @cached_classproperty\n    def relation_join(cls) -> str:\n        \"\"\"Return the join required to include the related table in the query.\n\n        This is intended to be used as a FROM clause in the SQL query.\n        \"\"\"\n        return \"\"\n\n    @cached_classproperty\n    def all_db_fields(cls) -> set[str]:\n        return cls._fields.keys() | cls._relation._fields.keys()\n\n    @cached_classproperty\n    def shared_db_fields(cls) -> set[str]:\n        return cls._fields.keys() & cls._relation._fields.keys()\n\n    @cached_classproperty\n    def other_db_fields(cls) -> set[str]:\n        \"\"\"Fields in the related table.\"\"\"\n        return cls._relation._fields.keys() - cls.shared_db_fields\n\n    @cached_property\n    def db(self) -> D:\n        \"\"\"Get the database associated with this object.\n\n        This validates that the database is attached and the object has an id.\n        \"\"\"\n        return self._check_db()\n\n    def get_fresh_from_db(self) -> Self:\n        \"\"\"Load this object from the database.\"\"\"\n        model_cls = self.__class__\n        if obj := self.db._get(model_cls, self.id):\n            return obj\n\n        raise NotFoundError(f\"No matching {model_cls.__name__} found\") from None\n\n    @classmethod\n    def _getters(cls: type[Model]):\n        \"\"\"Return a mapping from field names to getter functions.\"\"\"\n        # We could cache this if it becomes a performance problem to\n        # gather the getter mapping every time.\n        raise NotImplementedError()\n\n    def _template_funcs(self) -> Mapping[str, Callable[[str], str]]:\n        \"\"\"Return a mapping from function names to text-transformer\n        functions.\n        \"\"\"\n        # As above: we could consider caching this result.\n        raise NotImplementedError()\n\n    # Basic operation.\n\n    def __init__(self, db: D | None = None, **values):\n        \"\"\"Create a new object with an optional Database association and\n        initial field values.\n        \"\"\"\n        self._db = db\n        self._dirty: set[str] = set()\n        self._values_fixed = LazyConvertDict(self)\n        self._values_flex = LazyConvertDict(self)\n\n        # Initial contents.\n        self.update(values)\n        self.clear_dirty()\n\n    @classmethod\n    def _awaken(\n        cls: type[AnyModel],\n        db: D | None = None,\n        fixed_values: dict[str, Any] = {},\n        flex_values: dict[str, Any] = {},\n    ) -> AnyModel:\n        \"\"\"Create an object with values drawn from the database.\n\n        This is a performance optimization: the checks involved with\n        ordinary construction are bypassed.\n        \"\"\"\n        obj = cls(db)\n\n        obj._values_fixed.init(fixed_values)\n        obj._values_flex.init(flex_values)\n\n        return obj\n\n    def __repr__(self) -> str:\n        return (\n            f\"{type(self).__name__}\"\n            f\"({', '.join(f'{k}={v!r}' for k, v in dict(self).items())})\"\n        )\n\n    def clear_dirty(self):\n        \"\"\"Mark all fields as *clean* (i.e., not needing to be stored to\n        the database). Also update the revision.\n        \"\"\"\n        self._dirty = set()\n        if self._db:\n            self._revision = self._db.revision\n\n    def _check_db(self, need_id: bool = True) -> D:\n        \"\"\"Ensure that this object is associated with a database row: it\n        has a reference to a database (`_db`) and an id. A ValueError\n        exception is raised otherwise.\n        \"\"\"\n        if not self._db:\n            raise ValueError(f\"{type(self).__name__} has no database\")\n        if need_id and not self.id:\n            raise ValueError(f\"{type(self).__name__} has no id\")\n\n        return self._db\n\n    def copy(self) -> Model:\n        \"\"\"Create a copy of the model object.\n\n        The field values and other state is duplicated, but the new copy\n        remains associated with the same database as the old object.\n        (A simple `copy.deepcopy` will not work because it would try to\n        duplicate the SQLite connection.)\n        \"\"\"\n        new = self.__class__()\n        new._db = self._db\n        new._values_fixed = self._values_fixed.copy()\n        new._values_flex = self._values_flex.copy()\n        new._dirty = self._dirty.copy()\n        return new\n\n    # Essential field accessors.\n\n    @classmethod\n    def _type(cls, key) -> types.Type:\n        \"\"\"Get the type of a field, a `Type` instance.\n\n        If the field has no explicit type, it is given the base `Type`,\n        which does no conversion.\n        \"\"\"\n        return cls._fields.get(key) or cls._types.get(key) or types.DEFAULT\n\n    def _get(self, key, default: Any = None, raise_: bool = False):\n        \"\"\"Get the value for a field, or `default`. Alternatively,\n        raise a KeyError if the field is not available.\n        \"\"\"\n        getters = self._getters()\n        if key in getters:  # Computed.\n            return getters[key](self)\n        elif key in self._fields:  # Fixed.\n            if key in self._values_fixed:\n                return self._values_fixed[key]\n            else:\n                return self._type(key).null\n        elif key in self._values_flex:  # Flexible.\n            return self._values_flex[key]\n        elif raise_:\n            raise KeyError(key)\n        else:\n            return default\n\n    get = _get\n\n    def __getitem__(self, key):\n        \"\"\"Get the value for a field. Raise a KeyError if the field is\n        not available.\n        \"\"\"\n        return self._get(key, raise_=True)\n\n    def _setitem(self, key, value):\n        \"\"\"Assign the value for a field, return whether new and old value\n        differ.\n        \"\"\"\n        # Choose where to place the value.\n        if key in self._fields:\n            source = self._values_fixed\n        else:\n            source = self._values_flex\n\n        # If the field has a type, filter the value.\n        value = self._type(key).normalize(value)\n\n        # Assign value and possibly mark as dirty.\n        old_value = source.get(key)\n        source[key] = value\n        changed = old_value != value\n        if self._always_dirty or changed:\n            self._dirty.add(key)\n\n        return changed\n\n    def __setitem__(self, key, value):\n        \"\"\"Assign the value for a field.\"\"\"\n        self._setitem(key, value)\n\n    def __delitem__(self, key):\n        \"\"\"Remove a flexible attribute from the model.\"\"\"\n        if key in self._values_flex:  # Flexible.\n            del self._values_flex[key]\n            self._dirty.add(key)  # Mark for dropping on store.\n        elif key in self._fields:  # Fixed\n            setattr(self, key, self._type(key).null)\n        elif key in self._getters():  # Computed.\n            raise KeyError(f\"computed field {key} cannot be deleted\")\n        else:\n            raise KeyError(f\"no such field {key}\")\n\n    def keys(self, computed: bool = False):\n        \"\"\"Get a list of available field names for this object. The\n        `computed` parameter controls whether computed (plugin-provided)\n        fields are included in the key list.\n        \"\"\"\n        base_keys = list(self._fields) + list(self._values_flex.keys())\n        if computed:\n            return base_keys + list(self._getters().keys())\n        else:\n            return base_keys\n\n    @classmethod\n    def all_keys(cls):\n        \"\"\"Get a list of available keys for objects of this type.\n        Includes fixed and computed fields.\n        \"\"\"\n        return list(cls._fields) + list(cls._getters().keys())\n\n    # Act like a dictionary.\n\n    def update(self, values):\n        \"\"\"Assign all values in the given dict.\"\"\"\n        for key, value in values.items():\n            self[key] = value\n\n    def items(self) -> Iterator[tuple[str, Any]]:\n        \"\"\"Iterate over (key, value) pairs that this object contains.\n        Computed fields are not included.\n        \"\"\"\n        for key in self:\n            yield key, self[key]\n\n    def __contains__(self, key) -> bool:\n        \"\"\"Determine whether `key` is an attribute on this object.\"\"\"\n        return key in self.keys(computed=True)\n\n    def __iter__(self) -> Iterator[str]:\n        \"\"\"Iterate over the available field names (excluding computed\n        fields).\n        \"\"\"\n        return iter(self.keys())\n\n    # Convenient attribute access.\n\n    def __getattr__(self, key):\n        if key.startswith(\"_\"):\n            raise AttributeError(f\"model has no attribute {key!r}\")\n        else:\n            try:\n                return self[key]\n            except KeyError:\n                raise AttributeError(f\"no such field {key!r}\")\n\n    def __setattr__(self, key, value):\n        if key.startswith(\"_\"):\n            super().__setattr__(key, value)\n        else:\n            self[key] = value\n\n    def __delattr__(self, key):\n        if key.startswith(\"_\"):\n            super().__delattr__(key)\n        else:\n            del self[key]\n\n    # Database interaction (CRUD methods).\n\n    def store(self, fields: Iterable[str] | None = None):\n        \"\"\"Save the object's metadata into the library database.\n        :param fields: the fields to be stored. If not specified, all fields\n        will be.\n        \"\"\"\n        if fields is None:\n            fields = self._fields\n\n        # Build assignments for query.\n        assignments = []\n        subvars: list[SQLiteType] = []\n        for key in fields:\n            if key != \"id\" and key in self._dirty:\n                self._dirty.remove(key)\n                assignments.append(f\"{key}=?\")\n                value = self._type(key).to_sql(self[key])\n                subvars.append(value)\n\n        with self.db.transaction() as tx:\n            # Main table update.\n            if assignments:\n                query = f\"UPDATE {self._table} SET {','.join(assignments)} WHERE id=?\"\n                subvars.append(self.id)\n                tx.mutate(query, subvars)\n\n            # Modified/added flexible attributes.\n            for key, value in self._values_flex.items():\n                if key in self._dirty:\n                    self._dirty.remove(key)\n                    value = self._type(key).to_sql(value)\n                    tx.mutate(\n                        f\"INSERT INTO {self._flex_table} \"\n                        \"(entity_id, key, value) \"\n                        \"VALUES (?, ?, ?);\",\n                        (self.id, key, value),\n                    )\n\n            # Deleted flexible attributes.\n            for key in self._dirty:\n                tx.mutate(\n                    f\"DELETE FROM {self._flex_table} WHERE entity_id=? AND key=?\",\n                    (self.id, key),\n                )\n\n        self.clear_dirty()\n\n    def load(self):\n        \"\"\"Refresh the object's metadata from the library database.\n\n        If check_revision is true, the database is only queried loaded when a\n        transaction has been committed since the item was last loaded.\n        \"\"\"\n        if not self._dirty and self.db.revision == self._revision:\n            # Exit early\n            return\n\n        self.__dict__.update(self.get_fresh_from_db().__dict__)\n        self.clear_dirty()\n\n    def remove(self):\n        \"\"\"Remove the object's associated rows from the database.\"\"\"\n        with self.db.transaction() as tx:\n            tx.mutate(f\"DELETE FROM {self._table} WHERE id=?\", (self.id,))\n            tx.mutate(\n                f\"DELETE FROM {self._flex_table} WHERE entity_id=?\", (self.id,)\n            )\n\n    def add(self, db: D | None = None):\n        \"\"\"Add the object to the library database. This object must be\n        associated with a database; you can provide one via the `db`\n        parameter or use the currently associated database.\n\n        The object's `id` and `added` fields are set along with any\n        current field values.\n        \"\"\"\n        if db:\n            self._db = db\n        db = self._check_db(need_id=False)\n\n        with db.transaction() as tx:\n            new_id = tx.mutate(f\"INSERT INTO {self._table} DEFAULT VALUES\")\n            self.id = new_id\n            self.added = time.time()\n\n            # Mark every non-null field as dirty and store.\n            for key in self:\n                if self[key] is not None:\n                    self._dirty.add(key)\n            self.store()\n\n    # Formatting and templating.\n\n    _formatter = FormattedMapping\n\n    def formatted(\n        self,\n        included_keys: str = _formatter.ALL_KEYS,\n        for_path: bool = False,\n    ) -> FormattedMapping:\n        \"\"\"Get a mapping containing all values on this object formatted\n        as human-readable unicode strings.\n        \"\"\"\n        return self._formatter(self, included_keys, for_path)\n\n    def evaluate_template(\n        self,\n        template: str | functemplate.Template,\n        for_path: bool = False,\n    ) -> str:\n        \"\"\"Evaluate a template (a string or a `Template` object) using\n        the object's fields. If `for_path` is true, then no new path\n        separators will be added to the template.\n        \"\"\"\n        # Perform substitution.\n        if isinstance(template, str):\n            t = functemplate.template(template)\n        else:\n            # Help out mypy\n            t = template\n        return t.substitute(\n            self.formatted(for_path=for_path), self._template_funcs()\n        )\n\n    # Parsing.\n\n    @classmethod\n    def _parse(cls, key, string: str) -> Any:\n        \"\"\"Parse a string as a value for the given key.\"\"\"\n        if not isinstance(string, str):\n            raise TypeError(\"_parse() argument must be a string\")\n\n        return cls._type(key).parse(string)\n\n    def set_parse(self, key, string: str):\n        \"\"\"Set the object's key to a value represented by a string.\"\"\"\n        self[key] = self._parse(key, string)\n\n    def __getstate__(self):\n        \"\"\"Return the state of the object for pickling.\n        Remove the database connection as sqlite connections are not\n        picklable.\n        \"\"\"\n        return {\n            k: v for k, v in self.__dict__.items() if k not in {\"_db\", \"db\"}\n        }\n\n\n# Database controller and supporting interfaces.\n\n\nAnyModel = TypeVar(\"AnyModel\", bound=Model)\n\n\nclass Results(Generic[AnyModel]):\n    \"\"\"An item query result set. Iterating over the collection lazily\n    constructs Model objects that reflect database rows.\n    \"\"\"\n\n    def __init__(\n        self,\n        model_class: type[AnyModel],\n        rows: list[sqlite3.Row],\n        db: D,\n        flex_rows,\n        query: Query | None = None,\n        sort=None,\n    ):\n        \"\"\"Create a result set that will construct objects of type\n        `model_class`.\n\n        `model_class` is a subclass of `Model` that will be\n        constructed. `rows` is a query result: a list of mappings. The\n        new objects will be associated with the database `db`.\n\n        If `query` is provided, it is used as a predicate to filter the\n        results for a \"slow query\" that cannot be evaluated by the\n        database directly. If `sort` is provided, it is used to sort the\n        full list of results before returning. This means it is a \"slow\n        sort\" and all objects must be built before returning the first\n        one.\n        \"\"\"\n        self.model_class = model_class\n        self.rows = rows\n        self.db = db\n        self.query = query\n        self.sort = sort\n        self.flex_rows = flex_rows\n\n        # We keep a queue of rows we haven't yet consumed for\n        # materialization. We preserve the original total number of\n        # rows.\n        self._rows = rows\n        self._row_count = len(rows)\n\n        # The materialized objects corresponding to rows that have been\n        # consumed.\n        self._objects: list[AnyModel] = []\n\n    def _get_objects(self) -> Iterator[AnyModel]:\n        \"\"\"Construct and generate Model objects for they query. The\n        objects are returned in the order emitted from the database; no\n        slow sort is applied.\n\n        For performance, this generator caches materialized objects to\n        avoid constructing them more than once. This way, iterating over\n        a `Results` object a second time should be much faster than the\n        first.\n        \"\"\"\n\n        # Index flexible attributes by the item ID, so we have easier access\n        flex_attrs = self._get_indexed_flex_attrs()\n\n        index = 0  # Position in the materialized objects.\n        while index < len(self._objects) or self._rows:\n            # Are there previously-materialized objects to produce?\n            if index < len(self._objects):\n                yield self._objects[index]\n                index += 1\n\n            # Otherwise, we consume another row, materialize its object\n            # and produce it.\n            else:\n                while self._rows:\n                    row = self._rows.pop(0)\n                    obj = self._make_model(row, flex_attrs.get(row[\"id\"], {}))\n                    # If there is a slow-query predicate, ensurer that the\n                    # object passes it.\n                    if not self.query or self.query.match(obj):\n                        self._objects.append(obj)\n                        index += 1\n                        yield obj\n                        break\n\n    def __iter__(self) -> Iterator[AnyModel]:\n        \"\"\"Construct and generate Model objects for all matching\n        objects, in sorted order.\n        \"\"\"\n        if self.sort:\n            # Slow sort. Must build the full list first.\n            objects = self.sort.sort(list(self._get_objects()))\n            return iter(objects)\n\n        else:\n            # Objects are pre-sorted (i.e., by the database).\n            return self._get_objects()\n\n    def _get_indexed_flex_attrs(self) -> dict[int, FlexAttrs]:\n        \"\"\"Index flexible attributes by the entity id they belong to\"\"\"\n        flex_values: dict[int, FlexAttrs] = {}\n        for row in self.flex_rows:\n            if row[\"entity_id\"] not in flex_values:\n                flex_values[row[\"entity_id\"]] = {}\n\n            flex_values[row[\"entity_id\"]][row[\"key\"]] = row[\"value\"]\n\n        return flex_values\n\n    def _make_model(\n        self, row: sqlite3.Row, flex_values: FlexAttrs = {}\n    ) -> AnyModel:\n        \"\"\"Create a Model object for the given row\"\"\"\n        cols = dict(row)\n        values = {k: v for (k, v) in cols.items() if not k[:4] == \"flex\"}\n\n        # Construct the Python object\n        obj = self.model_class._awaken(self.db, values, flex_values)\n        return obj\n\n    def __len__(self) -> int:\n        \"\"\"Get the number of matching objects.\"\"\"\n        if not self._rows:\n            # Fully materialized. Just count the objects.\n            return len(self._objects)\n\n        elif self.query:\n            # A slow query. Fall back to testing every object.\n            count = 0\n            for obj in self:\n                count += 1\n            return count\n\n        else:\n            # A fast query. Just count the rows.\n            return self._row_count\n\n    def __nonzero__(self) -> bool:\n        \"\"\"Does this result contain any objects?\"\"\"\n        return self.__bool__()\n\n    def __bool__(self) -> bool:\n        \"\"\"Does this result contain any objects?\"\"\"\n        return bool(len(self))\n\n    def __getitem__(self, n):\n        \"\"\"Get the nth item in this result set. This is inefficient: all\n        items up to n are materialized and thrown away.\n        \"\"\"\n        if not self._rows and not self.sort:\n            # Fully materialized and already in order. Just look up the\n            # object.\n            return self._objects[n]\n\n        it = iter(self)\n        try:\n            for i in range(n):\n                next(it)\n            return next(it)\n        except StopIteration:\n            raise IndexError(f\"result index {n} out of range\")\n\n    def get(self) -> AnyModel | None:\n        \"\"\"Return the first matching object, or None if no objects\n        match.\n        \"\"\"\n        it = iter(self)\n        try:\n            return next(it)\n        except StopIteration:\n            return None\n\n\nclass Transaction:\n    \"\"\"A context manager for safe, concurrent access to the database.\n    All SQL commands should be executed through a transaction.\n    \"\"\"\n\n    _mutated = False\n    \"\"\"A flag storing whether a mutation has been executed in the\n    current transaction.\n    \"\"\"\n\n    def __init__(self, db: Database):\n        self.db = db\n\n    def __enter__(self) -> Transaction:\n        \"\"\"Begin a transaction. This transaction may be created while\n        another is active in a different thread.\n        \"\"\"\n        with self.db._tx_stack() as stack:\n            first = not stack\n            stack.append(self)\n        if first:\n            # Beginning a \"root\" transaction, which corresponds to an\n            # SQLite transaction.\n            self.db._db_lock.acquire()\n        return self\n\n    def __exit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_value: BaseException | None,\n        traceback: TracebackType | None,\n    ) -> bool | None:\n        \"\"\"Complete a transaction. This must be the most recently\n        entered but not yet exited transaction. If it is the last active\n        transaction, the database updates are committed.\n        \"\"\"\n        # Beware of races; currently secured by db._db_lock\n        self.db.revision += self._mutated\n        with self.db._tx_stack() as stack:\n            assert stack.pop() is self\n            empty = not stack\n        if empty:\n            # Ending a \"root\" transaction. End the SQLite transaction.\n            self.db._connection().commit()\n            self._mutated = False\n            self.db._db_lock.release()\n\n        if (\n            isinstance(exc_value, sqlite3.OperationalError)\n            and exc_value.args[0] == \"user-defined function raised exception\"\n        ):\n            raise DBCustomFunctionError()\n\n        return None\n\n    def query(\n        self, statement: str, subvals: Sequence[SQLiteType] = ()\n    ) -> list[sqlite3.Row]:\n        \"\"\"Execute an SQL statement with substitution values and return\n        a list of rows from the database.\n        \"\"\"\n        cursor = self.db._connection().execute(statement, subvals)\n        return cursor.fetchall()\n\n    @contextmanager\n    def _handle_mutate(self) -> Iterator[None]:\n        \"\"\"Handle mutation bookkeeping and database access errors.\n\n        Yield control to mutation execution code. If execution succeeds,\n        mark this transaction as mutated.\n        \"\"\"\n        try:\n            yield\n        except sqlite3.OperationalError as e:\n            # In two specific cases, SQLite reports an error while accessing\n            # the underlying database file. We surface these exceptions as\n            # DBAccessError so the application can abort.\n            if e.args[0] in (\n                \"attempt to write a readonly database\",\n                \"unable to open database file\",\n            ):\n                raise DBAccessError(e.args[0])\n            raise\n        else:\n            self._mutated = True\n\n    def mutate(self, statement: str, subvals: Sequence[SQLiteType] = ()) -> Any:\n        \"\"\"Run one write statement with shared mutation/error handling.\"\"\"\n        with self._handle_mutate():\n            return self.db._connection().execute(statement, subvals).lastrowid\n\n    def mutate_many(\n        self, statement: str, subvals: Sequence[tuple[SQLiteType, ...]] = ()\n    ) -> Any:\n        \"\"\"Run batched writes with shared mutation/error handling.\"\"\"\n        with self._handle_mutate():\n            return (\n                self.db._connection().executemany(statement, subvals).lastrowid\n            )\n\n    def script(self, statements: str):\n        \"\"\"Execute a string containing multiple SQL statements.\"\"\"\n        # We don't know whether this mutates, but quite likely it does.\n        self._mutated = True\n        self.db._connection().executescript(statements)\n\n\n@dataclass\nclass Migration(ABC):\n    \"\"\"Define a one-time data migration that runs during database startup.\"\"\"\n\n    db: Database\n\n    @cached_classproperty\n    def name(cls) -> str:\n        \"\"\"Class name (except Migration) converted to snake case.\"\"\"\n        name = cls.__name__.removesuffix(\"Migration\")  # type: ignore[attr-defined]\n        return re.sub(r\"(?<=[a-z])(?=[A-Z])\", \"_\", name).lower()\n\n    @contextmanager\n    def with_row_factory(self, factory: type[NamedTuple]) -> Iterator[None]:\n        \"\"\"Temporarily decode query rows into a typed tuple shape.\"\"\"\n        original_factory = self.db._connection().row_factory\n        self.db._connection().row_factory = lambda _, row: factory(*row)\n        try:\n            yield\n        finally:\n            self.db._connection().row_factory = original_factory\n\n    def migrate_model(self, model_cls: type[Model], *args, **kwargs) -> None:\n        \"\"\"Run this migration once for a model's backing table.\"\"\"\n        table = model_cls._table\n        if not self.db.migration_exists(self.name, table):\n            self._migrate_data(model_cls, *args, **kwargs)\n            self.db.record_migration(self.name, table)\n\n    @abstractmethod\n    def _migrate_data(\n        self, model_cls: type[Model], current_fields: set[str]\n    ) -> None:\n        \"\"\"Migrate data for a specific model.\"\"\"\n\n\nclass TableInfo(TypedDict):\n    columns: set[str]\n    migrations: set[str]\n\n\nclass Database:\n    \"\"\"A container for Model objects that wraps an SQLite database as\n    the backend.\n    \"\"\"\n\n    _models: Sequence[type[Model]] = ()\n    \"\"\"The Model subclasses representing tables in this database.\n    \"\"\"\n\n    _migrations: Sequence[tuple[type[Migration], Sequence[type[Model]]]] = ()\n    \"\"\"Migrations that are to be performed for the configured models.\"\"\"\n\n    supports_extensions = hasattr(sqlite3.Connection, \"enable_load_extension\")\n    \"\"\"Whether or not the current version of SQLite supports extensions\"\"\"\n\n    revision = 0\n    \"\"\"The current revision of the database. To be increased whenever\n    data is written in a transaction.\n    \"\"\"\n\n    def __init__(self, path, timeout: float = 5.0):\n        if sqlite3.threadsafety == 0:\n            raise RuntimeError(\n                \"sqlite3 must be compiled with multi-threading support\"\n            )\n\n        # Print tracebacks for exceptions in user defined functions\n        # See also `self.add_functions` and `DBCustomFunctionError`.\n        #\n        # `if`: use feature detection because PyPy doesn't support this.\n        if hasattr(sqlite3, \"enable_callback_tracebacks\"):\n            sqlite3.enable_callback_tracebacks(True)\n\n        self.path = path\n        self.timeout = timeout\n\n        self._connections: dict[int, sqlite3.Connection] = {}\n        self._tx_stacks: defaultdict[int, list[Transaction]] = defaultdict(list)\n        self._extensions: list[str] = []\n\n        # A lock to protect the _connections and _tx_stacks maps, which\n        # both map thread IDs to private resources.\n        self._shared_map_lock = threading.Lock()\n\n        # A lock to protect access to the database itself. SQLite does\n        # allow multiple threads to access the database at the same\n        # time, but many users were experiencing crashes related to this\n        # capability: where SQLite was compiled without HAVE_USLEEP, its\n        # backoff algorithm in the case of contention was causing\n        # whole-second sleeps (!) that would trigger its internal\n        # timeout. Using this lock ensures only one SQLite transaction\n        # is active at a time.\n        self._db_lock = threading.Lock()\n\n        # Set up database schema.\n        self._ensure_migration_state_table()\n        for model_cls in self._models:\n            self._make_table(model_cls._table, model_cls._fields)\n            self._make_attribute_table(model_cls._flex_table)\n            self._create_indices(model_cls._table, model_cls._indices)\n\n        self._migrate()\n\n    @cached_property\n    def db_tables(self) -> dict[str, TableInfo]:\n        column_queries = [\n            f\"\"\"\n                SELECT '{m._table}' AS table_name, 'columns' AS source, name\n                FROM pragma_table_info('{m._table}')\n            \"\"\"\n            for m in self._models\n        ]\n        with self.transaction() as tx:\n            rows = tx.query(f\"\"\"\n                {\" UNION ALL \".join(column_queries)}\n                UNION ALL\n                SELECT table_name, 'migrations' AS source, name FROM migrations\n            \"\"\")\n\n        tables_data: dict[str, TableInfo] = defaultdict(\n            lambda: TableInfo(columns=set(), migrations=set())\n        )\n\n        source: Literal[\"columns\", \"migrations\"]\n        for table_name, source, name in rows:\n            tables_data[table_name][source].add(name)\n\n        return tables_data\n\n    # Primitive access control: connections and transactions.\n\n    def _connection(self) -> Connection:\n        \"\"\"Get a SQLite connection object to the underlying database.\n        One connection object is created per thread.\n        \"\"\"\n        thread_id = threading.current_thread().ident\n        # Help the type checker: ident can only be None if the thread has not\n        # been started yet; but since this results from current_thread(), that\n        # can't happen\n        assert thread_id is not None\n\n        with self._shared_map_lock:\n            if thread_id in self._connections:\n                return self._connections[thread_id]\n            else:\n                conn = self._create_connection()\n                self._connections[thread_id] = conn\n                return conn\n\n    def _create_connection(self) -> Connection:\n        \"\"\"Create a SQLite connection to the underlying database.\n\n        Makes a new connection every time. If you need to configure the\n        connection settings (e.g., add custom functions), override this\n        method.\n        \"\"\"\n        # Make a new connection. The `sqlite3` module can't use\n        # bytestring paths here on Python 3, so we need to\n        # provide a `str` using `os.fsdecode`.\n        conn = sqlite3.connect(\n            os.fsdecode(self.path),\n            timeout=self.timeout,\n            # We have our own same-thread checks in _connection(), but need to\n            # call conn.close() in _close()\n            check_same_thread=False,\n        )\n\n        if sys.version_info >= (3, 12) and sqlite3.sqlite_version_info >= (\n            3,\n            29,\n            0,\n        ):\n            # If possible, disable double-quoted strings\n            conn.setconfig(sqlite3.SQLITE_DBCONFIG_DQS_DDL, 0)\n            conn.setconfig(sqlite3.SQLITE_DBCONFIG_DQS_DML, 0)\n\n        self.add_functions(conn)\n\n        if self.supports_extensions:\n            conn.enable_load_extension(True)\n\n            # Load any extension that are already loaded for other connections.\n            for path in self._extensions:\n                conn.load_extension(path)\n\n        # Access SELECT results like dictionaries.\n        conn.row_factory = sqlite3.Row\n        return conn\n\n    def add_functions(self, conn):\n        def regexp(value, pattern):\n            if isinstance(value, bytes):\n                value = value.decode()\n            return re.search(pattern, str(value)) is not None\n\n        def bytelower(bytestring: AnyStr | None) -> AnyStr | None:\n            \"\"\"A custom ``bytelower`` sqlite function so we can compare\n            bytestrings in a semi case insensitive fashion.\n\n            This is to work around sqlite builds are that compiled with\n            ``-DSQLITE_LIKE_DOESNT_MATCH_BLOBS``. See\n            ``https://github.com/beetbox/beets/issues/2172`` for details.\n            \"\"\"\n            if bytestring is not None:\n                return bytestring.lower()\n\n            return bytestring\n\n        create_function = conn.create_function\n        if sys.version_info >= (3, 8) and sqlite_version_info >= (3, 8, 3):\n            # Let sqlite make extra optimizations\n            create_function = functools.partial(\n                conn.create_function, deterministic=True\n            )\n\n        create_function(\"regexp\", 2, regexp)\n        create_function(\"unidecode\", 1, unidecode)\n        create_function(\"bytelower\", 1, bytelower)\n\n    def _close(self):\n        \"\"\"Close the all connections to the underlying SQLite database\n        from all threads. This does not render the database object\n        unusable; new connections can still be opened on demand.\n        \"\"\"\n        with self._shared_map_lock:\n            while self._connections:\n                _thread_id, conn = self._connections.popitem()\n                conn.close()\n\n    @contextmanager\n    def _tx_stack(self) -> Generator[list[Transaction]]:\n        \"\"\"A context manager providing access to the current thread's\n        transaction stack. The context manager synchronizes access to\n        the stack map. Transactions should never migrate across threads.\n        \"\"\"\n        thread_id = threading.current_thread().ident\n        # Help the type checker: ident can only be None if the thread has not\n        # been started yet; but since this results from current_thread(), that\n        # can't happen\n        assert thread_id is not None\n\n        with self._shared_map_lock:\n            yield self._tx_stacks[thread_id]\n\n    def transaction(self) -> Transaction:\n        \"\"\"Get a :class:`Transaction` object for interacting directly\n        with the underlying SQLite database.\n        \"\"\"\n        return Transaction(self)\n\n    def load_extension(self, path: str):\n        \"\"\"Load an SQLite extension into all open connections.\"\"\"\n        if not self.supports_extensions:\n            raise ValueError(\n                \"this sqlite3 installation does not support extensions\"\n            )\n\n        self._extensions.append(path)\n\n        # Load the extension into every open connection.\n        for conn in self._connections.values():\n            conn.load_extension(path)\n\n    # Schema setup and migration.\n\n    def _make_table(self, table: str, fields: Mapping[str, types.Type]):\n        \"\"\"Set up the schema of the database. `fields` is a mapping\n        from field names to `Type`s. Columns are added if necessary.\n        \"\"\"\n        if table not in self.db_tables:\n            # No table exists.\n            columns = []\n            for name, typ in fields.items():\n                columns.append(f\"{name} {typ.sql}\")\n            setup_sql = f\"CREATE TABLE {table} ({', '.join(columns)});\\n\"\n            self.db_tables[table][\"columns\"] = set(fields)\n        else:\n            # Table exists does not match the field set.\n            setup_sql = \"\"\n            current_fields = self.db_tables[table][\"columns\"]\n            for name, typ in fields.items():\n                if name not in current_fields:\n                    setup_sql += (\n                        f\"ALTER TABLE {table} ADD COLUMN {name} {typ.sql};\\n\"\n                    )\n\n        with self.transaction() as tx:\n            tx.script(setup_sql)\n\n    def _make_attribute_table(self, flex_table: str):\n        \"\"\"Create a table and associated index for flexible attributes\n        for the given entity (if they don't exist).\n        \"\"\"\n        with self.transaction() as tx:\n            tx.script(f\"\"\"\n                CREATE TABLE IF NOT EXISTS {flex_table} (\n                    id INTEGER PRIMARY KEY,\n                    entity_id INTEGER,\n                    key TEXT,\n                    value TEXT,\n                    UNIQUE(entity_id, key) ON CONFLICT REPLACE);\n                CREATE INDEX IF NOT EXISTS {flex_table}_by_entity\n                    ON {flex_table} (entity_id);\n                \"\"\")\n\n    def _create_indices(\n        self,\n        table: str,\n        indices: Sequence[Index],\n    ):\n        \"\"\"Create indices for the given table if they don't exist.\"\"\"\n        with self.transaction() as tx:\n            for index in indices:\n                tx.script(\n                    f\"CREATE INDEX IF NOT EXISTS {index.name} \"\n                    f\"ON {table} ({', '.join(index.columns)});\"\n                )\n\n    # Generic migration state handling.\n\n    def _ensure_migration_state_table(self) -> None:\n        with self.transaction() as tx:\n            tx.script(\"\"\"\n                CREATE TABLE IF NOT EXISTS migrations (\n                    name TEXT NOT NULL,\n                    table_name TEXT NOT NULL,\n                    PRIMARY KEY(name, table_name)\n                );\n            \"\"\")\n\n    def _migrate(self) -> None:\n        \"\"\"Perform any necessary migration for the database.\"\"\"\n        for migration_cls, model_classes in self._migrations:\n            migration = migration_cls(self)\n            for model_cls in model_classes:\n                migration.migrate_model(\n                    model_cls, self.db_tables[model_cls._table][\"columns\"]\n                )\n\n    def migration_exists(self, name: str, table: str) -> bool:\n        \"\"\"Return whether a named migration has been marked complete.\"\"\"\n        return name in self.db_tables[table][\"migrations\"]\n\n    def record_migration(self, name: str, table: str) -> None:\n        \"\"\"Set completion state for a named migration.\"\"\"\n        with self.transaction() as tx:\n            tx.mutate(\n                \"INSERT INTO migrations(name, table_name) VALUES (?, ?)\",\n                (name, table),\n            )\n\n    # Querying.\n\n    def _fetch(\n        self,\n        model_cls: type[AnyModel],\n        query: Query | None = None,\n        sort: Sort | None = None,\n    ) -> Results[AnyModel]:\n        \"\"\"Fetch the objects of type `model_cls` matching the given\n        query. The query may be given as a string, string sequence, a\n        Query object, or None (to fetch everything). `sort` is an\n        `Sort` object.\n        \"\"\"\n        query = query or TrueQuery()  # A null query.\n        sort = sort or NullSort()  # Unsorted.\n        where, subvals = query.clause()\n        order_by = sort.order_clause()\n\n        table = model_cls._table\n        _from = table\n        if query.field_names & model_cls.other_db_fields:\n            _from += f\" {model_cls.relation_join}\"\n\n        # group by id to avoid duplicates when joining with the relation\n        sql = (\n            f\"SELECT {table}.* \"\n            f\"FROM ({_from}) \"\n            f\"WHERE {where or 1} \"\n            f\"GROUP BY {table}.id\"\n        )\n        # Fetch flexible attributes for items matching the main query.\n        # Doing the per-item filtering in python is faster than issuing\n        # one query per item to sqlite.\n        flex_sql = (\n            \"SELECT * \"\n            f\"FROM {model_cls._flex_table} \"\n            f\"WHERE entity_id IN (SELECT id FROM ({sql}))\"\n        )\n\n        if order_by:\n            # the sort field may exist in both 'items' and 'albums' tables\n            # (when they are joined), causing ambiguous column OperationalError\n            # if we try to order directly.\n            # Since the join is required only for filtering, we can filter in\n            # a subquery and order the result, which returns unique fields.\n            sql = f\"SELECT * FROM ({sql}) ORDER BY {order_by}\"\n\n        with self.transaction() as tx:\n            rows = tx.query(sql, subvals)\n            flex_rows = tx.query(flex_sql, subvals)\n\n        return Results(\n            model_cls,\n            rows,\n            self,\n            flex_rows,\n            None if where else query,  # Slow query component.\n            sort if sort.is_slow() else None,  # Slow sort component.\n        )\n\n    def _get(self, model_cls: type[AnyModel], id_: int) -> AnyModel | None:\n        \"\"\"Get a Model object by its id or None if the id does not exist.\"\"\"\n        return self._fetch(model_cls, MatchQuery(\"id\", id_)).get()\n\n\nclass Index(NamedTuple):\n    \"\"\"A helper class to represent the index\n    information in the database schema.\n    \"\"\"\n\n    name: str\n    columns: tuple[str, ...]\n"
  },
  {
    "path": "beets/dbcore/query.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"The Query type hierarchy for DBCore.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport re\nimport unicodedata\nfrom abc import ABC, abstractmethod\nfrom collections.abc import Sequence\nfrom datetime import datetime, timedelta\nfrom functools import cached_property, reduce\nfrom operator import mul, or_\nfrom re import Pattern\nfrom typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar\n\nfrom beets import util\nfrom beets.util.units import raw_seconds_short\n\nif TYPE_CHECKING:\n    from collections.abc import Iterator, MutableSequence\n\n    from beets.dbcore.db import AnyModel, Model\n\n    P = TypeVar(\"P\", default=Any)\nelse:\n    P = TypeVar(\"P\")\n\n# To use the SQLite \"blob\" type, it doesn't suffice to provide a byte\n# string; SQLite treats that as encoded text. Wrapping it in a\n# `memoryview` tells it that we actually mean non-text data.\n# needs to be defined in here due to circular import.\n# TODO: remove it from this module and define it in dbcore/types.py instead\nBLOB_TYPE = memoryview\n\n\nclass ParsingError(ValueError):\n    \"\"\"Abstract class for any unparsable user-requested album/query\n    specification.\n    \"\"\"\n\n\nclass InvalidQueryError(ParsingError):\n    \"\"\"Represent any kind of invalid query.\n\n    The query should be a unicode string or a list, which will be space-joined.\n    \"\"\"\n\n    def __init__(self, query, explanation):\n        if isinstance(query, list):\n            query = \" \".join(query)\n        message = f\"'{query}': {explanation}\"\n        super().__init__(message)\n\n\nclass InvalidQueryArgumentValueError(ParsingError):\n    \"\"\"Represent a query argument that could not be converted as expected.\n\n    It exists to be caught in upper stack levels so a meaningful (i.e. with the\n    query) InvalidQueryError can be raised.\n    \"\"\"\n\n    def __init__(self, what, expected, detail=None):\n        message = f\"'{what}' is not {expected}\"\n        if detail:\n            message = f\"{message}: {detail}\"\n        super().__init__(message)\n\n\nclass Query(ABC):\n    \"\"\"An abstract class representing a query into the database.\"\"\"\n\n    @property\n    def field_names(self) -> set[str]:\n        \"\"\"Return a set with field names that this query operates on.\"\"\"\n        return set()\n\n    @abstractmethod\n    def clause(self) -> tuple[str | None, Sequence[Any]]:\n        \"\"\"Generate an SQLite expression implementing the query.\n\n        Return (clause, subvals) where clause is a valid sqlite\n        WHERE clause implementing the query and subvals is a list of\n        items to be substituted for ?s in the clause.\n\n        The default implementation returns None, falling back to a slow query\n        using `match()`.\n        \"\"\"\n\n    @abstractmethod\n    def match(self, obj: Model):\n        \"\"\"Check whether this query matches a given Model. Can be used to\n        perform queries on arbitrary sets of Model.\n        \"\"\"\n\n    def __and__(self, other: Query) -> AndQuery:\n        return AndQuery([self, other])\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}()\"\n\n    def __eq__(self, other) -> bool:\n        return type(self) is type(other)\n\n    def __hash__(self) -> int:\n        \"\"\"Minimalistic default implementation of a hash.\n\n        Given the implementation if __eq__ above, this is\n        certainly correct.\n        \"\"\"\n        return hash(type(self))\n\n\nSQLiteType = str | bytes | float | int | memoryview | None\nAnySQLiteType = TypeVar(\"AnySQLiteType\", bound=SQLiteType)\nFieldQueryType = type[\"FieldQuery\"]\n\n\nclass FieldQuery(Query, Generic[P]):\n    \"\"\"An abstract query that searches in a specific field for a\n    pattern. Subclasses must provide a `value_match` class method, which\n    determines whether a certain pattern string matches a certain value\n    string. Subclasses may also provide `col_clause` to implement the\n    same matching functionality in SQLite.\n    \"\"\"\n\n    @property\n    def field(self) -> str:\n        return (\n            f\"{self.table}.{self.field_name}\" if self.table else self.field_name\n        )\n\n    @property\n    def field_names(self) -> set[str]:\n        \"\"\"Return a set with field names that this query operates on.\"\"\"\n        return {self.field_name}\n\n    def __init__(self, field_name: str, pattern: P, fast: bool = True):\n        self.table, _, self.field_name = field_name.rpartition(\".\")\n        self.pattern = pattern\n        self.fast = fast\n\n    def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:\n        raise NotImplementedError\n\n    def clause(self) -> tuple[str | None, Sequence[SQLiteType]]:\n        if self.fast:\n            return self.col_clause()\n        else:\n            # Matching a flexattr. This is a slow query.\n            return None, ()\n\n    @classmethod\n    def value_match(cls, pattern: P, value: Any):\n        \"\"\"Determine whether the value matches the pattern.\"\"\"\n        raise NotImplementedError\n\n    def match(self, obj: Model) -> bool:\n        return self.value_match(self.pattern, obj.get(self.field_name))\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}({self.field_name!r}, {self.pattern!r}, \"\n            f\"fast={self.fast})\"\n        )\n\n    def __eq__(self, other) -> bool:\n        return (\n            super().__eq__(other)\n            and self.field_name == other.field_name\n            and self.pattern == other.pattern\n        )\n\n    def __hash__(self) -> int:\n        return hash((self.field_name, hash(self.pattern)))\n\n\nclass MatchQuery(FieldQuery[AnySQLiteType]):\n    \"\"\"A query that looks for exact matches in an Model field.\"\"\"\n\n    def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:\n        return f\"{self.field} = ?\", [self.pattern]\n\n    @classmethod\n    def value_match(cls, pattern: AnySQLiteType, value: Any) -> bool:\n        return pattern == value\n\n\nclass NoneQuery(FieldQuery[None]):\n    \"\"\"A query that checks whether a field is null.\"\"\"\n\n    def __init__(self, field, fast: bool = True):\n        super().__init__(field, None, fast)\n\n    def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:\n        return f\"{self.field} IS NULL\", ()\n\n    def match(self, obj: Model) -> bool:\n        return obj.get(self.field_name) is None\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}({self.field_name!r}, {self.fast})\"\n\n\nclass StringFieldQuery(FieldQuery[P]):\n    \"\"\"A FieldQuery that converts values to strings before matching\n    them.\n    \"\"\"\n\n    @classmethod\n    def value_match(cls, pattern: P, value: Any):\n        \"\"\"Determine whether the value matches the pattern. The value\n        may have any type.\n        \"\"\"\n        return cls.string_match(pattern, util.as_string(value))\n\n    @classmethod\n    def string_match(\n        cls,\n        pattern: P,\n        value: str,\n    ) -> bool:\n        \"\"\"Determine whether the value matches the pattern. Both\n        arguments are strings. Subclasses implement this method.\n        \"\"\"\n        raise NotImplementedError\n\n\nclass StringQuery(StringFieldQuery[str]):\n    \"\"\"A query that matches a whole string in a specific Model field.\"\"\"\n\n    def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:\n        search = (\n            self.pattern.replace(\"\\\\\", \"\\\\\\\\\")\n            .replace(\"%\", \"\\\\%\")\n            .replace(\"_\", \"\\\\_\")\n        )\n        clause = f\"{self.field} like ? escape '\\\\'\"\n        subvals = [search]\n        return clause, subvals\n\n    @classmethod\n    def string_match(cls, pattern: str, value: str) -> bool:\n        return pattern.lower() == value.lower()\n\n\nclass SubstringQuery(StringFieldQuery[str]):\n    \"\"\"A query that matches a substring in a specific Model field.\"\"\"\n\n    def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:\n        pattern = (\n            self.pattern.replace(\"\\\\\", \"\\\\\\\\\")\n            .replace(\"%\", \"\\\\%\")\n            .replace(\"_\", \"\\\\_\")\n        )\n        search = f\"%{pattern}%\"\n        clause = f\"{self.field} like ? escape '\\\\'\"\n        subvals = [search]\n        return clause, subvals\n\n    @classmethod\n    def string_match(cls, pattern: str, value: str) -> bool:\n        return pattern.lower() in value.lower()\n\n\nclass PathQuery(FieldQuery[bytes]):\n    \"\"\"A query that matches all items under a given path.\n\n    Matching can either be case-insensitive or case-sensitive. By\n    default, the behavior depends on the OS: case-insensitive on Windows\n    and case-sensitive otherwise.\n    \"\"\"\n\n    def __init__(self, field: str, pattern: bytes, fast: bool = True) -> None:\n        \"\"\"Create a path query.\n\n        `pattern` must be a path, either to a file or a directory.\n        \"\"\"\n        path = util.normpath(pattern)\n\n        # Case sensitivity depends on the filesystem that the query path is located on.\n        self.case_sensitive = util.case_sensitive(path)\n\n        # Use a normalized-case pattern for case-insensitive matches.\n        if not self.case_sensitive:\n            # We need to lowercase the entire path, not just the pattern.\n            # In particular, on Windows, the drive letter is otherwise not\n            # lowercased.\n            # This also ensures that the `match()` method below and the SQL\n            # from `col_clause()` do the same thing.\n            path = path.lower()\n\n        super().__init__(field, path, fast)\n\n    @cached_property\n    def dir_path(self) -> bytes:\n        return os.path.join(self.pattern, b\"\")\n\n    @staticmethod\n    def is_path_query(query_part: str) -> bool:\n        \"\"\"Try to guess whether a unicode query part is a path query.\n\n        The path query must\n        1. precede the colon in the query, if a colon is present\n        2. contain either ``os.sep`` or ``os.altsep`` (Windows)\n        3. this path must exist on the filesystem.\n        \"\"\"\n        query_part = query_part.split(\":\")[0]\n\n        return (\n            # make sure the query part contains a path separator\n            bool(set(query_part) & {os.sep, os.altsep})\n            and os.path.exists(util.normpath(query_part))\n        )\n\n    def match(self, obj: Model) -> bool:\n        \"\"\"Check whether a model object's path matches this query.\n\n        Performs either an exact match against the pattern or checks if the path\n        starts with the given directory path. Case sensitivity depends on the object's\n        filesystem as determined during initialization.\n        \"\"\"\n        path = obj.path if self.case_sensitive else obj.path.lower()\n        return (path == self.pattern) or path.startswith(self.dir_path)\n\n    def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:\n        \"\"\"Generate an SQL clause that implements path matching in the database.\n\n        Returns a tuple of SQL clause string and parameter values list that matches\n        paths either exactly or by directory prefix. Handles case sensitivity\n        appropriately using BYTELOWER for case-insensitive matches.\n        \"\"\"\n        if self.case_sensitive:\n            left, right = self.field, \"?\"\n        else:\n            left, right = f\"BYTELOWER({self.field})\", \"BYTELOWER(?)\"\n\n        return f\"({left} = {right}) || (substr({left}, 1, ?) = {right})\", [\n            BLOB_TYPE(self.pattern),\n            len(dir_blob := BLOB_TYPE(self.dir_path)),\n            dir_blob,\n        ]\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}({self.field!r}, {self.pattern!r}, \"\n            f\"fast={self.fast}, case_sensitive={self.case_sensitive})\"\n        )\n\n\nclass RegexpQuery(StringFieldQuery[Pattern[str]]):\n    \"\"\"A query that matches a regular expression in a specific Model field.\n\n    Raises InvalidQueryError when the pattern is not a valid regular\n    expression.\n    \"\"\"\n\n    def __init__(self, field_name: str, pattern: str, fast: bool = True):\n        pattern = self._normalize(pattern)\n        try:\n            pattern_re = re.compile(pattern)\n        except re.error as exc:\n            # Invalid regular expression.\n            raise InvalidQueryArgumentValueError(\n                pattern, \"a regular expression\", format(exc)\n            )\n\n        super().__init__(field_name, pattern_re, fast)\n\n    def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:\n        return f\" regexp({self.field}, ?)\", [self.pattern.pattern]\n\n    @staticmethod\n    def _normalize(s: str) -> str:\n        \"\"\"Normalize a Unicode string's representation (used on both\n        patterns and matched values).\n        \"\"\"\n        return unicodedata.normalize(\"NFC\", s)\n\n    @classmethod\n    def string_match(cls, pattern: Pattern[str], value: str) -> bool:\n        return pattern.search(cls._normalize(value)) is not None\n\n\nclass BooleanQuery(MatchQuery[int]):\n    \"\"\"Matches a boolean field. Pattern should either be a boolean or a\n    string reflecting a boolean.\n    \"\"\"\n\n    def __init__(\n        self,\n        field_name: str,\n        pattern: bool,\n        fast: bool = True,\n    ):\n        if isinstance(pattern, str):\n            pattern = util.str2bool(pattern)\n\n        pattern_int = int(pattern)\n\n        super().__init__(field_name, pattern_int, fast)\n\n\nclass NumericQuery(FieldQuery[str]):\n    \"\"\"Matches numeric fields. A syntax using Ruby-style range ellipses\n    (``..``) lets users specify one- or two-sided ranges. For example,\n    ``year:2001..`` finds music released since the turn of the century.\n\n    Raises InvalidQueryError when the pattern does not represent an int or\n    a float.\n    \"\"\"\n\n    def _convert(self, s: str) -> float | int | None:\n        \"\"\"Convert a string to a numeric type (float or int).\n\n        Return None if `s` is empty.\n        Raise an InvalidQueryError if the string cannot be converted.\n        \"\"\"\n        # This is really just a bit of fun premature optimization.\n        if not s:\n            return None\n        try:\n            return int(s)\n        except ValueError:\n            try:\n                return float(s)\n            except ValueError:\n                raise InvalidQueryArgumentValueError(s, \"an int or a float\")\n\n    def __init__(self, field_name: str, pattern: str, fast: bool = True):\n        super().__init__(field_name, pattern, fast)\n\n        parts = pattern.split(\"..\", 1)\n        if len(parts) == 1:\n            # No range.\n            self.point = self._convert(parts[0])\n            self.rangemin = None\n            self.rangemax = None\n        else:\n            # One- or two-sided range.\n            self.point = None\n            self.rangemin = self._convert(parts[0])\n            self.rangemax = self._convert(parts[1])\n\n    def match(self, obj: Model) -> bool:\n        if self.field_name not in obj:\n            return False\n        value = obj[self.field_name]\n        if isinstance(value, str):\n            value = self._convert(value)\n\n        if self.point is not None:\n            return value == self.point\n        else:\n            if self.rangemin is not None and value < self.rangemin:\n                return False\n            if self.rangemax is not None and value > self.rangemax:\n                return False\n            return True\n\n    def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:\n        if self.point is not None:\n            return f\"{self.field}=?\", (self.point,)\n        else:\n            if self.rangemin is not None and self.rangemax is not None:\n                return (\n                    f\"{self.field} >= ? AND {self.field} <= ?\",\n                    (self.rangemin, self.rangemax),\n                )\n            elif self.rangemin is not None:\n                return f\"{self.field} >= ?\", (self.rangemin,)\n            elif self.rangemax is not None:\n                return f\"{self.field} <= ?\", (self.rangemax,)\n            else:\n                return \"1\", ()\n\n\nclass InQuery(Generic[AnySQLiteType], FieldQuery[Sequence[AnySQLiteType]]):\n    \"\"\"Query which matches values in the given set.\"\"\"\n\n    field_name: str\n    pattern: Sequence[AnySQLiteType]\n    fast: bool = True\n\n    @property\n    def subvals(self) -> Sequence[SQLiteType]:\n        return self.pattern\n\n    def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:\n        placeholders = \", \".join([\"?\"] * len(self.subvals))\n        return f\"{self.field_name} IN ({placeholders})\", self.subvals\n\n    @classmethod\n    def value_match(\n        cls, pattern: Sequence[AnySQLiteType], value: AnySQLiteType\n    ) -> bool:\n        return value in pattern\n\n\nclass CollectionQuery(Query):\n    \"\"\"An abstract query class that aggregates other queries. Can be\n    indexed like a list to access the sub-queries.\n    \"\"\"\n\n    @property\n    def field_names(self) -> set[str]:\n        \"\"\"Return a set with field names that this query operates on.\"\"\"\n        return reduce(or_, (sq.field_names for sq in self.subqueries))\n\n    def __init__(self, subqueries: Sequence[Query] = ()):\n        self.subqueries = subqueries\n\n    # Act like a sequence.\n\n    def __len__(self) -> int:\n        return len(self.subqueries)\n\n    def __getitem__(self, key):\n        return self.subqueries[key]\n\n    def __iter__(self) -> Iterator[Query]:\n        return iter(self.subqueries)\n\n    def __contains__(self, subq) -> bool:\n        return subq in self.subqueries\n\n    def clause_with_joiner(\n        self,\n        joiner: str,\n    ) -> tuple[str | None, Sequence[SQLiteType]]:\n        \"\"\"Return a clause created by joining together the clauses of\n        all subqueries with the string joiner (padded by spaces).\n        \"\"\"\n        clause_parts = []\n        subvals: list[SQLiteType] = []\n        for subq in self.subqueries:\n            subq_clause, subq_subvals = subq.clause()\n            if not subq_clause:\n                # Fall back to slow query.\n                return None, ()\n            clause_parts.append(f\"({subq_clause})\")\n            subvals += subq_subvals\n        clause = f\" {joiner} \".join(clause_parts)\n        return clause, subvals\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}({self.subqueries!r})\"\n\n    def __eq__(self, other) -> bool:\n        return super().__eq__(other) and self.subqueries == other.subqueries\n\n    def __hash__(self) -> int:\n        \"\"\"Since subqueries are mutable, this object should not be hashable.\n        However and for conveniences purposes, it can be hashed.\n        \"\"\"\n        return reduce(mul, map(hash, self.subqueries), 1)\n\n\nclass MutableCollectionQuery(CollectionQuery):\n    \"\"\"A collection query whose subqueries may be modified after the\n    query is initialized.\n    \"\"\"\n\n    subqueries: MutableSequence[Query]\n\n    def __setitem__(self, key, value):\n        self.subqueries[key] = value\n\n    def __delitem__(self, key):\n        del self.subqueries[key]\n\n\nclass AndQuery(MutableCollectionQuery):\n    \"\"\"A conjunction of a list of other queries.\"\"\"\n\n    def clause(self) -> tuple[str | None, Sequence[SQLiteType]]:\n        return self.clause_with_joiner(\"and\")\n\n    def match(self, obj: Model) -> bool:\n        return all(q.match(obj) for q in self.subqueries)\n\n\nclass OrQuery(MutableCollectionQuery):\n    \"\"\"A conjunction of a list of other queries.\"\"\"\n\n    def clause(self) -> tuple[str | None, Sequence[SQLiteType]]:\n        return self.clause_with_joiner(\"or\")\n\n    def match(self, obj: Model) -> bool:\n        return any(q.match(obj) for q in self.subqueries)\n\n\nclass NotQuery(Query):\n    \"\"\"A query that matches the negation of its `subquery`, as a shortcut for\n    performing `not(subquery)` without using regular expressions.\n    \"\"\"\n\n    @property\n    def field_names(self) -> set[str]:\n        \"\"\"Return a set with field names that this query operates on.\"\"\"\n        return self.subquery.field_names\n\n    def __init__(self, subquery):\n        self.subquery = subquery\n\n    def clause(self) -> tuple[str | None, Sequence[SQLiteType]]:\n        clause, subvals = self.subquery.clause()\n        if clause:\n            return f\"not ({clause})\", subvals\n        else:\n            # If there is no clause, there is nothing to negate. All the logic\n            # is handled by match() for slow queries.\n            return clause, subvals\n\n    def match(self, obj: Model) -> bool:\n        return not self.subquery.match(obj)\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}({self.subquery!r})\"\n\n    def __eq__(self, other) -> bool:\n        return super().__eq__(other) and self.subquery == other.subquery\n\n    def __hash__(self) -> int:\n        return hash((\"not\", hash(self.subquery)))\n\n\nclass TrueQuery(Query):\n    \"\"\"A query that always matches.\"\"\"\n\n    def clause(self) -> tuple[str, Sequence[SQLiteType]]:\n        return \"1\", ()\n\n    def match(self, obj: Model) -> bool:\n        return True\n\n\nclass FalseQuery(Query):\n    \"\"\"A query that never matches.\"\"\"\n\n    def clause(self) -> tuple[str, Sequence[SQLiteType]]:\n        return \"0\", ()\n\n    def match(self, obj: Model) -> bool:\n        return False\n\n\n# Time/date queries.\n\n\ndef _parse_periods(pattern: str) -> tuple[Period | None, Period | None]:\n    \"\"\"Parse a string containing two dates separated by two dots (..).\n    Return a pair of `Period` objects.\n    \"\"\"\n    parts = pattern.split(\"..\", 1)\n    if len(parts) == 1:\n        instant = Period.parse(parts[0])\n        return (instant, instant)\n    else:\n        start = Period.parse(parts[0])\n        end = Period.parse(parts[1])\n        return (start, end)\n\n\nclass Period:\n    \"\"\"A period of time given by a date, time and precision.\n\n    Example: 2014-01-01 10:50:30 with precision 'month' represents all\n    instants of time during January 2014.\n    \"\"\"\n\n    precisions = (\"year\", \"month\", \"day\", \"hour\", \"minute\", \"second\")\n    date_formats = (\n        (\"%Y\",),  # year\n        (\"%Y-%m\",),  # month\n        (\"%Y-%m-%d\",),  # day\n        (\"%Y-%m-%dT%H\", \"%Y-%m-%d %H\"),  # hour\n        (\"%Y-%m-%dT%H:%M\", \"%Y-%m-%d %H:%M\"),  # minute\n        (\"%Y-%m-%dT%H:%M:%S\", \"%Y-%m-%d %H:%M:%S\"),  # second\n    )\n    relative_units: ClassVar[dict[str, int]] = {\n        \"y\": 365,\n        \"m\": 30,\n        \"w\": 7,\n        \"d\": 1,\n    }\n    relative_re = \"(?P<sign>[+|-]?)(?P<quantity>[0-9]+)(?P<timespan>[y|m|w|d])\"\n\n    def __init__(self, date: datetime, precision: str):\n        \"\"\"Create a period with the given date (a `datetime` object) and\n        precision (a string, one of \"year\", \"month\", \"day\", \"hour\", \"minute\",\n        or \"second\").\n        \"\"\"\n        if precision not in Period.precisions:\n            raise ValueError(f\"Invalid precision {precision}\")\n        self.date = date\n        self.precision = precision\n\n    @classmethod\n    def parse(cls: type[Period], string: str) -> Period | None:\n        \"\"\"Parse a date and return a `Period` object or `None` if the\n        string is empty, or raise an InvalidQueryArgumentValueError if\n        the string cannot be parsed to a date.\n\n        The date may be absolute or relative. Absolute dates look like\n        `YYYY`, or `YYYY-MM-DD`, or `YYYY-MM-DD HH:MM:SS`, etc. Relative\n        dates have three parts:\n\n        - Optionally, a ``+`` or ``-`` sign indicating the future or the\n          past. The default is the future.\n        - A number: how much to add or subtract.\n        - A letter indicating the unit: days, weeks, months or years\n          (``d``, ``w``, ``m`` or ``y``). A \"month\" is exactly 30 days\n          and a \"year\" is exactly 365 days.\n        \"\"\"\n\n        def find_date_and_format(\n            string: str,\n        ) -> tuple[None, None] | tuple[datetime, int]:\n            for ord, format in enumerate(cls.date_formats):\n                for format_option in format:\n                    try:\n                        date = datetime.strptime(string, format_option)\n                        return date, ord\n                    except ValueError:\n                        # Parsing failed.\n                        pass\n            return (None, None)\n\n        if not string:\n            return None\n\n        date: datetime | None\n\n        # Check for a relative date.\n        match_dq = re.match(cls.relative_re, string)\n        if match_dq:\n            sign = match_dq.group(\"sign\")\n            quantity = match_dq.group(\"quantity\")\n            timespan = match_dq.group(\"timespan\")\n\n            # Add or subtract the given amount of time from the current\n            # date.\n            multiplier = -1 if sign == \"-\" else 1\n            days = cls.relative_units[timespan]\n            date = (\n                datetime.now()\n                + timedelta(days=int(quantity) * days) * multiplier\n            )\n            return cls(date, cls.precisions[5])\n\n        # Check for an absolute date.\n        date, ordinal = find_date_and_format(string)\n        if date is None or ordinal is None:\n            raise InvalidQueryArgumentValueError(\n                string, \"a valid date/time string\"\n            )\n        precision = cls.precisions[ordinal]\n        return cls(date, precision)\n\n    def open_right_endpoint(self) -> datetime:\n        \"\"\"Based on the precision, convert the period to a precise\n        `datetime` for use as a right endpoint in a right-open interval.\n        \"\"\"\n        precision = self.precision\n        date = self.date\n        if \"year\" == self.precision:\n            return date.replace(year=date.year + 1, month=1)\n        elif \"month\" == precision:\n            if date.month < 12:\n                return date.replace(month=date.month + 1)\n            else:\n                return date.replace(year=date.year + 1, month=1)\n        elif \"day\" == precision:\n            return date + timedelta(days=1)\n        elif \"hour\" == precision:\n            return date + timedelta(hours=1)\n        elif \"minute\" == precision:\n            return date + timedelta(minutes=1)\n        elif \"second\" == precision:\n            return date + timedelta(seconds=1)\n        else:\n            raise ValueError(f\"unhandled precision {precision}\")\n\n\nclass DateInterval:\n    \"\"\"A closed-open interval of dates.\n\n    A left endpoint of None means since the beginning of time.\n    A right endpoint of None means towards infinity.\n    \"\"\"\n\n    def __init__(self, start: datetime | None, end: datetime | None):\n        if start is not None and end is not None and not start < end:\n            raise ValueError(f\"start date {start} is not before end date {end}\")\n        self.start = start\n        self.end = end\n\n    @classmethod\n    def from_periods(\n        cls,\n        start: Period | None,\n        end: Period | None,\n    ) -> DateInterval:\n        \"\"\"Create an interval with two Periods as the endpoints.\"\"\"\n        end_date = end.open_right_endpoint() if end is not None else None\n        start_date = start.date if start is not None else None\n        return cls(start_date, end_date)\n\n    def contains(self, date: datetime) -> bool:\n        if self.start is not None and date < self.start:\n            return False\n        if self.end is not None and date >= self.end:\n            return False\n        return True\n\n    def __str__(self) -> str:\n        return f\"[{self.start}, {self.end})\"\n\n\nclass DateQuery(FieldQuery[str]):\n    \"\"\"Matches date fields stored as seconds since Unix epoch time.\n\n    Dates can be specified as ``year-month-day`` strings where only year\n    is mandatory.\n\n    The value of a date field can be matched against a date interval by\n    using an ellipsis interval syntax similar to that of NumericQuery.\n    \"\"\"\n\n    def __init__(self, field_name: str, pattern: str, fast: bool = True):\n        super().__init__(field_name, pattern, fast)\n        start, end = _parse_periods(pattern)\n        self.interval = DateInterval.from_periods(start, end)\n\n    def match(self, obj: Model) -> bool:\n        if self.field_name not in obj:\n            return False\n        timestamp = float(obj[self.field_name])\n        date = datetime.fromtimestamp(timestamp)\n        return self.interval.contains(date)\n\n    def col_clause(self) -> tuple[str, Sequence[SQLiteType]]:\n        clause_parts = []\n        subvals = []\n\n        # Convert the `datetime` objects to an integer number of seconds since\n        # the (local) Unix epoch using `datetime.timestamp()`.\n        if self.interval.start:\n            clause_parts.append(f\"{self.field} >= ?\")\n            subvals.append(int(self.interval.start.timestamp()))\n\n        if self.interval.end:\n            clause_parts.append(f\"{self.field} < ?\")\n            subvals.append(int(self.interval.end.timestamp()))\n\n        if clause_parts:\n            # One- or two-sided interval.\n            clause = \" AND \".join(clause_parts)\n        else:\n            # Match any date.\n            clause = \"1\"\n        return clause, subvals\n\n\nclass DurationQuery(NumericQuery):\n    \"\"\"NumericQuery that allow human-friendly (M:SS) time interval formats.\n\n    Converts the range(s) to a float value, and delegates on NumericQuery.\n\n    Raises InvalidQueryError when the pattern does not represent an int, float\n    or M:SS time interval.\n    \"\"\"\n\n    def _convert(self, s: str) -> float | None:\n        \"\"\"Convert a M:SS or numeric string to a float.\n\n        Return None if `s` is empty.\n        Raise an InvalidQueryError if the string cannot be converted.\n        \"\"\"\n        if not s:\n            return None\n        try:\n            return raw_seconds_short(s)\n        except ValueError:\n            try:\n                return float(s)\n            except ValueError:\n                raise InvalidQueryArgumentValueError(\n                    s, \"a M:SS string or a float\"\n                )\n\n\nclass SingletonQuery(FieldQuery[str]):\n    \"\"\"This query is responsible for the 'singleton' lookup.\n\n    It is based on the FieldQuery and constructs a SQL clause\n    'album_id is NULL' which yields the same result as the previous filter\n    in Python but is more performant since it's done in SQL.\n\n    Using util.str2bool ensures that lookups like singleton:true, singleton:1\n    and singleton:false, singleton:0 are handled consistently.\n    \"\"\"\n\n    def __new__(cls, field: str, value: str, *args, **kwargs):\n        query = NoneQuery(\"album_id\")\n        if util.str2bool(value):\n            return query\n        return NotQuery(query)\n\n\n# Sorting.\n\n\nclass Sort:\n    \"\"\"An abstract class representing a sort operation for a query into\n    the database.\n    \"\"\"\n\n    def order_clause(self) -> str | None:\n        \"\"\"Generates a SQL fragment to be used in a ORDER BY clause, or\n        None if no fragment is used (i.e., this is a slow sort).\n        \"\"\"\n        return None\n\n    def sort(self, items: list[AnyModel]) -> list[AnyModel]:\n        \"\"\"Sort the list of objects and return a list.\"\"\"\n        return sorted(items)\n\n    def is_slow(self) -> bool:\n        \"\"\"Indicate whether this query is *slow*, meaning that it cannot\n        be executed in SQL and must be executed in Python.\n        \"\"\"\n        return False\n\n    def __hash__(self) -> int:\n        return 0\n\n    def __eq__(self, other) -> bool:\n        return type(self) is type(other)\n\n    def __repr__(self):\n        return f\"{self.__class__.__name__}()\"\n\n\nclass MultipleSort(Sort):\n    \"\"\"Sort that encapsulates multiple sub-sorts.\"\"\"\n\n    def __init__(self, sorts: list[Sort] | None = None):\n        self.sorts = sorts or []\n\n    def add_sort(self, sort: Sort):\n        self.sorts.append(sort)\n\n    def order_clause(self) -> str:\n        \"\"\"Return the list SQL clauses for those sub-sorts for which we can be\n        (at least partially) fast.\n\n        A contiguous suffix of fast (SQL-capable) sub-sorts are\n        executable in SQL. The remaining, even if they are fast\n        independently, must be executed slowly.\n        \"\"\"\n        order_strings = []\n        for sort in reversed(self.sorts):\n            clause = sort.order_clause()\n            if clause is None:\n                break\n            order_strings.append(clause)\n        order_strings.reverse()\n\n        return \", \".join(order_strings)\n\n    def is_slow(self) -> bool:\n        for sort in self.sorts:\n            if sort.is_slow():\n                return True\n        return False\n\n    def sort(self, items):\n        slow_sorts = []\n        switch_slow = False\n        for sort in reversed(self.sorts):\n            if switch_slow:\n                slow_sorts.append(sort)\n            elif sort.order_clause() is None:\n                switch_slow = True\n                slow_sorts.append(sort)\n            else:\n                pass\n\n        for sort in slow_sorts:\n            items = sort.sort(items)\n        return items\n\n    def __repr__(self):\n        return f\"{self.__class__.__name__}({self.sorts!r})\"\n\n    def __hash__(self):\n        return hash(tuple(self.sorts))\n\n    def __eq__(self, other):\n        return super().__eq__(other) and self.sorts == other.sorts\n\n\nclass FieldSort(Sort):\n    \"\"\"An abstract sort criterion that orders by a specific field (of\n    any kind).\n    \"\"\"\n\n    def __init__(\n        self,\n        field: str,\n        ascending: bool = True,\n        case_insensitive: bool = True,\n    ):\n        self.field = field\n        self.ascending = ascending\n        self.case_insensitive = case_insensitive\n\n    def sort(self, objs: list[AnyModel]) -> list[AnyModel]:\n        # TODO: Conversion and null-detection here. In Python 3,\n        # comparisons with None fail. We should also support flexible\n        # attributes with different types without falling over.\n\n        def key(obj: Model) -> Any:\n            field_val = obj.get(self.field, None)\n            if field_val is None:\n                if _type := obj._types.get(self.field):\n                    # If the field is typed, use its null value.\n                    field_val = obj._types[self.field].null\n                else:\n                    # If not, fall back to using an empty string.\n                    field_val = \"\"\n            if self.case_insensitive and isinstance(field_val, str):\n                field_val = field_val.lower()\n            return field_val\n\n        return sorted(objs, key=key, reverse=not self.ascending)\n\n    def __repr__(self) -> str:\n        return (\n            f\"{self.__class__.__name__}\"\n            f\"({self.field!r}, ascending={self.ascending!r})\"\n        )\n\n    def __hash__(self) -> int:\n        return hash((self.field, self.ascending))\n\n    def __eq__(self, other) -> bool:\n        return (\n            super().__eq__(other)\n            and self.field == other.field\n            and self.ascending == other.ascending\n        )\n\n\nclass FixedFieldSort(FieldSort):\n    \"\"\"Sort object to sort on a fixed field.\"\"\"\n\n    def order_clause(self) -> str:\n        order = \"ASC\" if self.ascending else \"DESC\"\n        if self.case_insensitive:\n            field = (\n                \"(CASE \"\n                f\"WHEN TYPEOF({self.field})='text' THEN LOWER({self.field}) \"\n                f\"WHEN TYPEOF({self.field})='blob' THEN LOWER({self.field}) \"\n                f\"ELSE {self.field} END)\"\n            )\n        else:\n            field = self.field\n        return f\"{field} {order}\"\n\n\nclass SlowFieldSort(FieldSort):\n    \"\"\"A sort criterion by some model field other than a fixed field:\n    i.e., a computed or flexible field.\n    \"\"\"\n\n    def is_slow(self) -> bool:\n        return True\n\n\nclass NullSort(Sort):\n    \"\"\"No sorting. Leave results unsorted.\"\"\"\n\n    def sort(self, items: list[AnyModel]) -> list[AnyModel]:\n        return items\n\n    def __nonzero__(self) -> bool:\n        return self.__bool__()\n\n    def __bool__(self) -> bool:\n        return False\n\n    def __eq__(self, other) -> bool:\n        return type(self) is type(other) or other is None\n\n    def __hash__(self) -> int:\n        return 0\n\n\nclass SmartArtistSort(FieldSort):\n    \"\"\"Sort by artist (either album artist or track artist),\n    prioritizing the sort field over the raw field.\n    \"\"\"\n\n    def order_clause(self):\n        order = \"ASC\" if self.ascending else \"DESC\"\n        collate = \"COLLATE NOCASE\" if self.case_insensitive else \"\"\n        field = self.field\n\n        return f\"COALESCE(NULLIF({field}_sort, ''), {field}) {collate} {order}\"\n\n    def sort(self, objs: list[AnyModel]) -> list[AnyModel]:\n        def key(o):\n            val = o[f\"{self.field}_sort\"] or o[self.field]\n            return val.lower() if self.case_insensitive else val\n\n        return sorted(objs, key=key, reverse=not self.ascending)\n"
  },
  {
    "path": "beets/dbcore/queryparse.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Parsing of strings into DBCore queries.\"\"\"\n\nfrom __future__ import annotations\n\nimport itertools\nimport re\nfrom typing import TYPE_CHECKING\n\nfrom . import query\n\nif TYPE_CHECKING:\n    from collections.abc import Collection, Sequence\n\n    from ..library import LibModel\n    from .query import FieldQueryType, Sort\n\n    Prefixes = dict[str, FieldQueryType]\n\n\nPARSE_QUERY_PART_REGEX = re.compile(\n    # Non-capturing optional segment for the keyword.\n    r\"(-|\\^)?\"  # Negation prefixes.\n    r\"(?:\"\n    r\"(\\S+?)\"  # The field key.\n    r\"(?<!\\\\):\"  # Unescaped :\n    r\")?\"\n    r\"(.*)\",  # The term itself.\n    re.I,  # Case-insensitive.\n)\n\n\ndef parse_query_part(\n    part: str,\n    query_classes: dict[str, FieldQueryType] = {},\n    prefixes: Prefixes = {},\n    default_class: type[query.SubstringQuery] = query.SubstringQuery,\n) -> tuple[str | None, str, FieldQueryType, bool]:\n    \"\"\"Parse a single *query part*, which is a chunk of a complete query\n    string representing a single criterion.\n\n    A query part is a string consisting of:\n    - A *pattern*: the value to look for.\n    - Optionally, a *field name* preceding the pattern, separated by a\n      colon. So in `foo:bar`, `foo` is the field name and `bar` is the\n      pattern.\n    - Optionally, a *query prefix* just before the pattern (and after the\n      optional colon) indicating the type of query that should be used. For\n      example, in `~foo`, `~` might be a prefix. (The set of prefixes to\n      look for is given in the `prefixes` parameter.)\n    - Optionally, a negation indicator, `-` or `^`, at the very beginning.\n\n    Both prefixes and the separating `:` character may be escaped with a\n    backslash to avoid their normal meaning.\n\n    The function returns a tuple consisting of:\n    - The field name: a string or None if it's not present.\n    - The pattern, a string.\n    - The query class to use, which inherits from the base\n      :class:`Query` type.\n    - A negation flag, a bool.\n\n    The three optional parameters determine which query class is used (i.e.,\n    the third return value). They are:\n    - `query_classes`, which maps field names to query classes. These\n      are used when no explicit prefix is present.\n    - `prefixes`, which maps prefix strings to query classes.\n    - `default_class`, the fallback when neither the field nor a prefix\n      indicates a query class.\n\n    So the precedence for determining which query class to return is:\n    prefix, followed by field, and finally the default.\n\n    For example, assuming the `:` prefix is used for `RegexpQuery`:\n    - `'stapler'` -> `(None, 'stapler', SubstringQuery, False)`\n    - `'color:red'` -> `('color', 'red', SubstringQuery, False)`\n    - `':^Quiet'` -> `(None, '^Quiet', RegexpQuery, False)`, because\n      the `^` follows the `:`\n    - `'color::b..e'` -> `('color', 'b..e', RegexpQuery, False)`\n    - `'-color:red'` -> `('color', 'red', SubstringQuery, True)`\n    \"\"\"\n    # Apply the regular expression and extract the components.\n    part = part.strip()\n    match = PARSE_QUERY_PART_REGEX.match(part)\n\n    assert match  # Regex should always match\n    negate = bool(match.group(1))\n    key = match.group(2)\n    term = match.group(3).replace(\"\\\\:\", \":\")\n\n    # Check whether there's a prefix in the query and use the\n    # corresponding query type.\n    for pre, query_class in prefixes.items():\n        if term.startswith(pre):\n            return key, term[len(pre) :], query_class, negate\n\n    # No matching prefix, so use either the query class determined by\n    # the field or the default as a fallback.\n    query_class = query_classes.get(key, default_class)\n    return key, term, query_class, negate\n\n\ndef construct_query_part(\n    model_cls: type[LibModel],\n    prefixes: Prefixes,\n    query_part: str,\n) -> query.Query:\n    \"\"\"Parse a *query part* string and return a :class:`Query` object.\n\n    :param model_cls: The :class:`Model` class that this is a query for.\n      This is used to determine the appropriate query types for the\n      model's fields.\n    :param prefixes: A map from prefix strings to :class:`Query` types.\n    :param query_part: The string to parse.\n\n    See the documentation for `parse_query_part` for more information on\n    query part syntax.\n    \"\"\"\n    # A shortcut for empty query parts.\n    if not query_part:\n        return query.TrueQuery()\n\n    out_query: query.Query\n\n    # Use `model_cls` to build up a map from field (or query) names to\n    # `Query` classes.\n    query_classes: dict[str, FieldQueryType] = {}\n    for k, t in itertools.chain(\n        model_cls._fields.items(), model_cls._types.items()\n    ):\n        query_classes[k] = t.query\n    query_classes.update(model_cls._queries)  # Non-field queries.\n\n    # Parse the string.\n    key, pattern, query_class, negate = parse_query_part(\n        query_part, query_classes, prefixes\n    )\n\n    if key is None:\n        # If there's no key (field name) specified, this is a \"match anything\"\n        # query.\n        out_query = model_cls.any_field_query(pattern, query_class)\n    else:\n        # Field queries get constructed according to the name of the field\n        # they are querying.\n        out_query = model_cls.field_query(key.lower(), pattern, query_class)\n\n    # Apply negation.\n    if negate:\n        return query.NotQuery(out_query)\n    else:\n        return out_query\n\n\n# TYPING ERROR\ndef query_from_strings(\n    query_cls: type[query.CollectionQuery],\n    model_cls: type[LibModel],\n    prefixes: Prefixes,\n    query_parts: Collection[str],\n) -> query.Query:\n    \"\"\"Creates a collection query of type `query_cls` from a list of\n    strings in the format used by parse_query_part. `model_cls`\n    determines how queries are constructed from strings.\n    \"\"\"\n    subqueries = []\n    for part in query_parts:\n        subqueries.append(construct_query_part(model_cls, prefixes, part))\n    if not subqueries:  # No terms in query.\n        subqueries = [query.TrueQuery()]\n    return query_cls(subqueries)\n\n\ndef construct_sort_part(\n    model_cls: type[LibModel],\n    part: str,\n    case_insensitive: bool = True,\n) -> Sort:\n    \"\"\"Create a `Sort` from a single string criterion.\n\n    `model_cls` is the `Model` being queried. `part` is a single string\n    ending in ``+`` or ``-`` indicating the sort. `case_insensitive`\n    indicates whether or not the sort should be performed in a case\n    sensitive manner.\n    \"\"\"\n    assert part, \"part must be a field name and + or -\"\n    field = part[:-1]\n    assert field, \"field is missing\"\n    direction = part[-1]\n    assert direction in (\"+\", \"-\"), \"part must end with + or -\"\n    is_ascending = direction == \"+\"\n\n    if sort_cls := model_cls._sorts.get(field):\n        if isinstance(sort_cls, query.SmartArtistSort):\n            field = \"albumartist\" if model_cls.__name__ == \"Album\" else \"artist\"\n    elif field in model_cls._fields:\n        sort_cls = query.FixedFieldSort\n    else:\n        # Flexible or computed.\n        sort_cls = query.SlowFieldSort\n\n    return sort_cls(field, is_ascending, case_insensitive)\n\n\ndef sort_from_strings(\n    model_cls: type[LibModel],\n    sort_parts: Sequence[str],\n    case_insensitive: bool = True,\n) -> Sort:\n    \"\"\"Create a `Sort` from a list of sort criteria (strings).\"\"\"\n    if not sort_parts:\n        return query.NullSort()\n    elif len(sort_parts) == 1:\n        return construct_sort_part(model_cls, sort_parts[0], case_insensitive)\n    else:\n        sort = query.MultipleSort()\n        for part in sort_parts:\n            sort.add_sort(\n                construct_sort_part(model_cls, part, case_insensitive)\n            )\n        return sort\n\n\ndef parse_sorted_query(\n    model_cls: type[LibModel],\n    parts: list[str],\n    prefixes: Prefixes = {},\n    case_insensitive: bool = True,\n) -> tuple[query.Query, Sort]:\n    \"\"\"Given a list of strings, create the `Query` and `Sort` that they\n    represent.\n    \"\"\"\n    # Separate query token and sort token.\n    query_parts = []\n    sort_parts = []\n\n    # Split up query in to comma-separated subqueries, each representing\n    # an AndQuery, which need to be joined together in one OrQuery\n    subquery_parts = []\n    for part in [*parts, \",\"]:\n        if part.endswith(\",\"):\n            # Ensure we can catch \"foo, bar\" as well as \"foo , bar\"\n            last_subquery_part = part[:-1]\n            if last_subquery_part:\n                subquery_parts.append(last_subquery_part)\n            # Parse the subquery in to a single AndQuery\n            # TODO: Avoid needlessly wrapping AndQueries containing 1 subquery?\n            query_parts.append(\n                query_from_strings(\n                    query.AndQuery, model_cls, prefixes, subquery_parts\n                )\n            )\n            del subquery_parts[:]\n        else:\n            # Sort parts (1) end in + or -, (2) don't have a field, and\n            # (3) consist of more than just the + or -.\n            if part.endswith((\"+\", \"-\")) and \":\" not in part and len(part) > 1:\n                sort_parts.append(part)\n            else:\n                subquery_parts.append(part)\n\n    # Avoid needlessly wrapping single statements in an OR\n    q = query.OrQuery(query_parts) if len(query_parts) > 1 else query_parts[0]\n    s = sort_from_strings(model_cls, sort_parts, case_insensitive)\n    return q, s\n"
  },
  {
    "path": "beets/dbcore/types.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Representation of type information for DBCore model fields.\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nimport time\nimport typing\nfrom abc import ABC\nfrom typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar, cast\n\nimport beets\nfrom beets import util\nfrom beets.util.units import human_seconds_short, raw_seconds_short\n\nfrom . import query\n\nSQLiteType = query.SQLiteType\nBLOB_TYPE = query.BLOB_TYPE\nMULTI_VALUE_DELIMITER = \"\\\\␀\"\n\n\nclass ModelType(typing.Protocol):\n    \"\"\"Protocol that specifies the required constructor for model types,\n    i.e. a function that takes any argument and attempts to parse it to the\n    given type.\n    \"\"\"\n\n    def __init__(self, value: Any = None): ...\n\n\n# Generic type variables, used for the value type T and null type N (if\n# nullable, else T and N are set to the same type for the concrete subclasses\n# of Type).\nif TYPE_CHECKING:\n    N = TypeVar(\"N\", default=Any)\n    T = TypeVar(\"T\", bound=ModelType, default=Any)\nelse:\n    N = TypeVar(\"N\")\n    T = TypeVar(\"T\", bound=ModelType)\n\n\nclass Type(ABC, Generic[T, N]):\n    \"\"\"An object encapsulating the type of a model field. Includes\n    information about how to store, query, format, and parse a given\n    field.\n    \"\"\"\n\n    sql: str = \"TEXT\"\n    \"\"\"The SQLite column type for the value.\n    \"\"\"\n\n    query: query.FieldQueryType = query.SubstringQuery\n    \"\"\"The `Query` subclass to be used when querying the field.\n    \"\"\"\n\n    # For sequence-like types, keep ``model_type`` unsubscripted as it's used\n    # for ``isinstance`` checks. Use ``list`` instead of ``list[str]``\n    model_type: type[T]\n    \"\"\"The Python type that is used to represent the value in the model.\n\n    The model is guaranteed to return a value of this type if the field\n    is accessed.  To this end, the constructor is used by the `normalize`\n    and `from_sql` methods and the `default` property.\n    \"\"\"\n\n    @property\n    def null(self) -> N:\n        \"\"\"The value to be exposed when the underlying value is None.\"\"\"\n        # Note that this default implementation only makes sense for T = N.\n        # It would be better to implement `null()` only in subclasses, or\n        # have a field null_type similar to `model_type` and use that here.\n        return cast(N, self.model_type())\n\n    def format(self, value: N | T) -> str:\n        \"\"\"Given a value of this type, produce a Unicode string\n        representing the value. This is used in template evaluation.\n        \"\"\"\n        if value is None:\n            value = self.null\n        # `self.null` might be `None`\n        if value is None:\n            return \"\"\n        elif isinstance(value, bytes):\n            return value.decode(\"utf-8\", \"ignore\")\n        else:\n            return str(value)\n\n    def parse(self, string: str) -> T | N:\n        \"\"\"Parse a (possibly human-written) string and return the\n        indicated value of this type.\n        \"\"\"\n        try:\n            return self.model_type(string)\n        except ValueError:\n            return self.null\n\n    def normalize(self, value: Any) -> T | N:\n        \"\"\"Given a value that will be assigned into a field of this\n        type, normalize the value to have the appropriate type. This\n        base implementation only reinterprets `None`.\n        \"\"\"\n        # TYPING ERROR\n        if value is None:\n            return self.null\n        else:\n            # TODO This should eventually be replaced by\n            # `self.model_type(value)`\n            return cast(T, value)\n\n    def from_sql(self, sql_value: SQLiteType) -> T | N:\n        \"\"\"Receives the value stored in the SQL backend and return the\n        value to be stored in the model.\n\n        For fixed fields the type of `value` is determined by the column\n        type affinity given in the `sql` property and the SQL to Python\n        mapping of the database adapter. For more information see:\n        https://www.sqlite.org/datatype3.html\n        https://docs.python.org/2/library/sqlite3.html#sqlite-and-python-types\n\n        Flexible fields have the type affinity `TEXT`. This means the\n        `sql_value` is either a `memoryview` or a `unicode` object`\n        and the method must handle these in addition.\n        \"\"\"\n        if isinstance(sql_value, memoryview):\n            sql_value = bytes(sql_value).decode(\"utf-8\", \"ignore\")\n        if isinstance(sql_value, str):\n            return self.parse(sql_value)\n        else:\n            return self.normalize(sql_value)\n\n    def to_sql(self, model_value: Any) -> SQLiteType:\n        \"\"\"Convert a value as stored in the model object to a value used\n        by the database adapter.\n        \"\"\"\n        return model_value\n\n\n# Reusable types.\n\n\nclass Default(Type[str, None]):\n    model_type = str\n\n    @property\n    def null(self):\n        return None\n\n\nclass BaseInteger(Type[int, N]):\n    \"\"\"A basic integer type.\"\"\"\n\n    sql = \"INTEGER\"\n    query = query.NumericQuery\n    model_type = int\n\n    def normalize(self, value: Any) -> int | N:\n        try:\n            return self.model_type(round(float(value)))\n        except ValueError:\n            return self.null\n        except TypeError:\n            return self.null\n\n\nclass Integer(BaseInteger[int]):\n    @property\n    def null(self) -> int:\n        return 0\n\n\nclass NullInteger(BaseInteger[None]):\n    @property\n    def null(self) -> None:\n        return None\n\n\nclass BasePaddedInt(BaseInteger[N]):\n    \"\"\"An integer field that is formatted with a given number of digits,\n    padded with zeroes.\n    \"\"\"\n\n    def __init__(self, digits: int):\n        self.digits = digits\n\n    def format(self, value: int | N) -> str:\n        return f\"{value or 0:0{self.digits}d}\"\n\n\nclass PaddedInt(BasePaddedInt[int]):\n    pass\n\n\nclass NullPaddedInt(BasePaddedInt[None]):\n    \"\"\"Same as `PaddedInt`, but does not normalize `None` to `0`.\"\"\"\n\n    @property\n    def null(self) -> None:\n        return None\n\n\nclass ScaledInt(Integer):\n    \"\"\"An integer whose formatting operation scales the number by a\n    constant and adds a suffix. Good for units with large magnitudes.\n    \"\"\"\n\n    def __init__(self, unit: int, suffix: str = \"\"):\n        self.unit = unit\n        self.suffix = suffix\n\n    def format(self, value: int) -> str:\n        return f\"{(value or 0) // self.unit}{self.suffix}\"\n\n\nclass Id(NullInteger):\n    \"\"\"An integer used as the row id or a foreign key in a SQLite table.\n    This type is nullable: None values are not translated to zero.\n    \"\"\"\n\n    @property\n    def null(self) -> None:\n        return None\n\n    def __init__(self, primary: bool = True):\n        if primary:\n            self.sql = \"INTEGER PRIMARY KEY\"\n\n\nclass BaseFloat(Type[float, N]):\n    \"\"\"A basic floating-point type. The `digits` parameter specifies how\n    many decimal places to use in the human-readable representation.\n    \"\"\"\n\n    sql = \"REAL\"\n    query: query.FieldQueryType = query.NumericQuery\n    model_type = float\n\n    def __init__(self, digits: int = 1):\n        self.digits = digits\n\n    def format(self, value: float | N) -> str:\n        return f\"{value or 0:.{self.digits}f}\"\n\n\nclass Float(BaseFloat[float]):\n    \"\"\"Floating-point type that normalizes `None` to `0.0`.\"\"\"\n\n    @property\n    def null(self) -> float:\n        return 0.0\n\n\nclass NullFloat(BaseFloat[None]):\n    \"\"\"Same as `Float`, but does not normalize `None` to `0.0`.\"\"\"\n\n    @property\n    def null(self) -> None:\n        return None\n\n\nclass BaseString(Type[T, N]):\n    \"\"\"A Unicode string type.\"\"\"\n\n    sql = \"TEXT\"\n    query = query.SubstringQuery\n\n    def normalize(self, value: Any) -> T | N:\n        if value is None:\n            return self.null\n        else:\n            return self.model_type(value)\n\n\nclass String(BaseString[str, Any]):\n    \"\"\"A Unicode string type.\"\"\"\n\n    model_type = str\n\n\nclass DelimitedString(BaseString[list, list]):  # type: ignore[type-arg]\n    r\"\"\"A list of Unicode strings, represented in-database by a single string\n    containing delimiter-separated values.\n\n    In template evaluation the list is formatted by joining the values with\n    a fixed '; ' delimiter regardless of the database delimiter. That is because\n    the '\\␀' character used for multi-value fields is mishandled on Windows\n    as it contains a backslash character.\n    \"\"\"\n\n    model_type = list\n    fmt_delimiter = \"; \"\n\n    def __init__(self, db_delimiter: str):\n        self.db_delimiter = db_delimiter\n\n    def format(self, value: list[str]):\n        return self.fmt_delimiter.join(value)\n\n    def parse(self, string: str):\n        if not string:\n            return []\n\n        delimiter = (\n            self.db_delimiter\n            if self.db_delimiter in string\n            else self.fmt_delimiter\n        )\n        return string.split(delimiter)\n\n    def to_sql(self, model_value: list[str]):\n        return self.db_delimiter.join(model_value)\n\n\nclass Boolean(Type):\n    \"\"\"A boolean type.\"\"\"\n\n    sql = \"INTEGER\"\n    query = query.BooleanQuery\n    model_type = bool\n\n    def format(self, value: bool) -> str:\n        return str(bool(value))\n\n    def parse(self, string: str) -> bool:\n        return util.str2bool(string)\n\n\nclass DateType(Float):\n    # TODO representation should be `datetime` object\n    # TODO distinguish between date and time types\n    query = query.DateQuery\n\n    def format(self, value):\n        return time.strftime(\n            beets.config[\"time_format\"].as_str(), time.localtime(value or 0)\n        )\n\n    def parse(self, string):\n        try:\n            # Try a formatted date string.\n            return time.mktime(\n                time.strptime(string, beets.config[\"time_format\"].as_str())\n            )\n        except ValueError:\n            # Fall back to a plain timestamp number.\n            try:\n                return float(string)\n            except ValueError:\n                return self.null\n\n\nclass BasePathType(Type[bytes, N]):\n    \"\"\"A dbcore type for filesystem paths.\n\n    These are represented as `bytes` objects, in keeping with\n    the Unix filesystem abstraction.\n    \"\"\"\n\n    sql = \"BLOB\"\n    query = query.PathQuery\n    model_type = bytes\n\n    def parse(self, string: str) -> bytes:\n        return util.normpath(string)\n\n    def normalize(self, value: Any) -> bytes | N:\n        if isinstance(value, str):\n            # Paths stored internally as encoded bytes.\n            return util.bytestring_path(value)\n\n        elif isinstance(value, BLOB_TYPE):\n            # We unwrap buffers to bytes.\n            return bytes(value)\n\n        else:\n            return value\n\n    def from_sql(self, sql_value):\n        return self.normalize(sql_value)\n\n    def to_sql(self, value: bytes) -> BLOB_TYPE:\n        if isinstance(value, bytes):\n            value = BLOB_TYPE(value)\n        return value\n\n\nclass NullPathType(BasePathType[None]):\n    @property\n    def null(self) -> None:\n        return None\n\n    def format(self, value: bytes | None) -> str:\n        return util.displayable_path(value or b\"\")\n\n\nclass PathType(BasePathType[bytes]):\n    @property\n    def null(self) -> bytes:\n        return b\"\"\n\n    def format(self, value: bytes) -> str:\n        return util.displayable_path(value or b\"\")\n\n\nclass MusicalKey(String):\n    \"\"\"String representing the musical key of a song.\n\n    The standard format is C, Cm, C#, C#m, etc.\n    \"\"\"\n\n    ENHARMONIC: ClassVar[dict[str, str]] = {\n        r\"db\": \"c#\",\n        r\"eb\": \"d#\",\n        r\"gb\": \"f#\",\n        r\"ab\": \"g#\",\n        r\"bb\": \"a#\",\n    }\n\n    null = None\n\n    def parse(self, key):\n        key = key.lower()\n        for flat, sharp in self.ENHARMONIC.items():\n            key = re.sub(flat, sharp, key)\n        key = re.sub(r\"[\\W\\s]+minor\", \"m\", key)\n        key = re.sub(r\"[\\W\\s]+major\", \"\", key)\n        return key.capitalize()\n\n    def normalize(self, key):\n        if key is None:\n            return None\n        else:\n            return self.parse(key)\n\n\nclass DurationType(Float):\n    \"\"\"Human-friendly (M:SS) representation of a time interval.\"\"\"\n\n    query = query.DurationQuery\n\n    def format(self, value):\n        if not beets.config[\"format_raw_length\"].get(bool):\n            return human_seconds_short(value or 0.0)\n        else:\n            return value\n\n    def parse(self, string):\n        try:\n            # Try to format back hh:ss to seconds.\n            return raw_seconds_short(string)\n        except ValueError:\n            # Fall back to a plain float.\n            try:\n                return float(string)\n            except ValueError:\n                return self.null\n\n\n# Shared instances of common types.\nDEFAULT = Default()\nINTEGER = Integer()\nPRIMARY_ID = Id(True)\nFOREIGN_ID = Id(False)\nFLOAT = Float()\nNULL_FLOAT = NullFloat()\nSTRING = String()\nBOOLEAN = Boolean()\nDATE = DateType()\nSEMICOLON_SPACE_DSV = DelimitedString(\"; \")\n\n# Will set the proper null char in mediafile\nMULTI_VALUE_DSV = DelimitedString(MULTI_VALUE_DELIMITER)\n"
  },
  {
    "path": "beets/importer/__init__.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Provides the basic, interface-agnostic workflow for importing and\nautotagging music files.\n\"\"\"\n\nfrom .session import ImportAbortError, ImportSession\nfrom .tasks import (\n    Action,\n    ArchiveImportTask,\n    ImportTask,\n    SentinelImportTask,\n    SingletonImportTask,\n)\n\n# Note: Stages are not exposed to the public API\n\n__all__ = [\n    \"Action\",\n    \"ArchiveImportTask\",\n    \"ImportAbortError\",\n    \"ImportSession\",\n    \"ImportTask\",\n    \"SentinelImportTask\",\n    \"SingletonImportTask\",\n]\n"
  },
  {
    "path": "beets/importer/session.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\nfrom __future__ import annotations\n\nimport os\nimport time\nfrom typing import TYPE_CHECKING\n\nfrom beets import config, logging, plugins, util\nfrom beets.importer.tasks import Action\nfrom beets.util import displayable_path, normpath, pipeline, syspath\n\nfrom . import stages as stagefuncs\nfrom .state import ImportState\n\nif TYPE_CHECKING:\n    from collections.abc import Sequence\n\n    from beets import dbcore, library\n    from beets.util import PathBytes\n\n    from .tasks import ImportTask\n\n\nQUEUE_SIZE = 128\n\n# Global logger.\nlog = logging.getLogger(\"beets\")\n\n\nclass ImportAbortError(Exception):\n    \"\"\"Raised when the user aborts the tagging operation.\"\"\"\n\n    pass\n\n\nclass ImportSession:\n    \"\"\"Controls an import action. Subclasses should implement methods to\n    communicate with the user or otherwise make decisions.\n    \"\"\"\n\n    logger: logging.Logger\n    paths: list[PathBytes]\n    lib: library.Library\n\n    _is_resuming: dict[bytes, bool]\n    _merged_items: set[PathBytes]\n    _merged_dirs: set[PathBytes]\n\n    def __init__(\n        self,\n        lib: library.Library,\n        loghandler: logging.Handler | None,\n        paths: Sequence[PathBytes] | None,\n        query: dbcore.Query | None,\n    ):\n        \"\"\"Create a session.\n\n        Parameters\n        ----------\n        lib : library.Library\n            The library instance to which items will be imported.\n        loghandler : logging.Handler or None\n            A logging handler to use for the session's logger. If None, a\n            NullHandler will be used.\n        paths : os.PathLike or None\n            The paths to be imported.\n        query : dbcore.Query or None\n            A query to filter items for import.\n        \"\"\"\n        self.lib = lib\n        self.logger = self._setup_logging(loghandler)\n        self.query = query\n        self._is_resuming = {}\n        self._merged_items = set()\n        self._merged_dirs = set()\n\n        # Normalize the paths.\n        self.paths = list(map(normpath, paths or []))\n\n    def _setup_logging(self, loghandler: logging.Handler | None):\n        logger = logging.getLogger(__name__)\n        logger.propagate = False\n        if not loghandler:\n            loghandler = logging.NullHandler()\n        logger.handlers = [loghandler]\n        return logger\n\n    def set_config(self, config):\n        \"\"\"Set `config` property from global import config and make\n        implied changes.\n        \"\"\"\n        # FIXME: Maybe this function should not exist and should instead\n        # provide \"decision wrappers\" like \"should_resume()\", etc.\n        iconfig = dict(config)\n        self.config = iconfig\n\n        # Incremental and progress are mutually exclusive.\n        if iconfig[\"incremental\"]:\n            iconfig[\"resume\"] = False\n\n        # When based on a query instead of directories, never\n        # save progress or try to resume.\n        if self.query is not None:\n            iconfig[\"resume\"] = False\n            iconfig[\"incremental\"] = False\n\n        if iconfig[\"reflink\"]:\n            iconfig[\"reflink\"] = iconfig[\"reflink\"].as_choice(\n                [\"auto\", True, False]\n            )\n\n        # Copy, move, reflink, link, and hardlink are mutually exclusive.\n        if iconfig[\"move\"]:\n            iconfig[\"copy\"] = False\n            iconfig[\"link\"] = False\n            iconfig[\"hardlink\"] = False\n            iconfig[\"reflink\"] = False\n        elif iconfig[\"link\"]:\n            iconfig[\"copy\"] = False\n            iconfig[\"move\"] = False\n            iconfig[\"hardlink\"] = False\n            iconfig[\"reflink\"] = False\n        elif iconfig[\"hardlink\"]:\n            iconfig[\"copy\"] = False\n            iconfig[\"move\"] = False\n            iconfig[\"link\"] = False\n            iconfig[\"reflink\"] = False\n        elif iconfig[\"reflink\"]:\n            iconfig[\"copy\"] = False\n            iconfig[\"move\"] = False\n            iconfig[\"link\"] = False\n            iconfig[\"hardlink\"] = False\n\n        # Only delete when copying.\n        if not iconfig[\"copy\"]:\n            iconfig[\"delete\"] = False\n\n        self.want_resume = config[\"resume\"].as_choice([True, False, \"ask\"])\n\n    def tag_log(self, status, paths: Sequence[PathBytes]):\n        \"\"\"Log a message about a given album to the importer log. The status\n        should reflect the reason the album couldn't be tagged.\n        \"\"\"\n        self.logger.info(\"{} {}\", status, displayable_path(paths))\n\n    def log_choice(self, task: ImportTask, duplicate=False):\n        \"\"\"Logs the task's current choice if it should be logged. If\n        ``duplicate``, then this is a secondary choice after a duplicate was\n        detected and a decision was made.\n        \"\"\"\n        paths = task.paths\n        if duplicate:\n            # Duplicate: log all three choices (skip, keep both, and trump).\n            if task.should_remove_duplicates:\n                self.tag_log(\"duplicate-replace\", paths)\n            elif task.choice_flag in (Action.ASIS, Action.APPLY):\n                self.tag_log(\"duplicate-keep\", paths)\n            elif task.choice_flag is Action.SKIP:\n                self.tag_log(\"duplicate-skip\", paths)\n        else:\n            # Non-duplicate: log \"skip\" and \"asis\" choices.\n            if task.choice_flag is Action.ASIS:\n                self.tag_log(\"asis\", paths)\n            elif task.choice_flag is Action.SKIP:\n                self.tag_log(\"skip\", paths)\n\n    def should_resume(self, path: PathBytes):\n        raise NotImplementedError\n\n    def choose_match(self, task: ImportTask):\n        raise NotImplementedError\n\n    def resolve_duplicate(self, task: ImportTask, found_duplicates):\n        raise NotImplementedError\n\n    def choose_item(self, task: ImportTask):\n        raise NotImplementedError\n\n    def run(self):\n        \"\"\"Run the import task.\"\"\"\n        self.logger.info(\"import started {}\", time.asctime())\n        self.set_config(config[\"import\"])\n\n        # Set up the pipeline.\n        if self.query is None:\n            stages = [stagefuncs.read_tasks(self)]\n        else:\n            stages = [stagefuncs.query_tasks(self)]\n\n        # In pretend mode, just log what would otherwise be imported.\n        if self.config[\"pretend\"]:\n            stages += [stagefuncs.log_files(self)]\n        else:\n            if self.config[\"group_albums\"] and not self.config[\"singletons\"]:\n                # Split directory tasks into one task for each album.\n                stages += [stagefuncs.group_albums(self)]\n\n            # These stages either talk to the user to get a decision or,\n            # in the case of a non-autotagged import, just choose to\n            # import everything as-is. In *both* cases, these stages\n            # also add the music to the library database, so later\n            # stages need to read and write data from there.\n            if self.config[\"autotag\"]:\n                stages += [\n                    stagefuncs.lookup_candidates(self),\n                    stagefuncs.user_query(self),\n                ]\n            else:\n                stages += [stagefuncs.import_asis(self)]\n\n            # Plugin stages.\n            for stage_func in plugins.early_import_stages():\n                stages.append(stagefuncs.plugin_stage(self, stage_func))\n            for stage_func in plugins.import_stages():\n                stages.append(stagefuncs.plugin_stage(self, stage_func))\n\n            stages += [stagefuncs.manipulate_files(self)]\n\n        pl = pipeline.Pipeline(stages)\n\n        # Run the pipeline.\n        plugins.send(\"import_begin\", session=self)\n        try:\n            if config[\"threaded\"]:\n                pl.run_parallel(QUEUE_SIZE)\n            else:\n                pl.run_sequential()\n        except ImportAbortError:\n            # User aborted operation. Silently stop.\n            pass\n\n    # Incremental and resumed imports\n\n    def already_imported(self, toppath: PathBytes, paths: Sequence[PathBytes]):\n        \"\"\"Returns true if the files belonging to this task have already\n        been imported in a previous session.\n        \"\"\"\n        if self.is_resuming(toppath) and all(\n            [ImportState().progress_has_element(toppath, p) for p in paths]\n        ):\n            return True\n        if self.config[\"incremental\"] and tuple(paths) in self.history_dirs:\n            return True\n\n        return False\n\n    _history_dirs = None\n\n    @property\n    def history_dirs(self) -> set[tuple[PathBytes, ...]]:\n        # FIXME: This could be simplified to a cached property\n        if self._history_dirs is None:\n            self._history_dirs = ImportState().taghistory\n        return self._history_dirs\n\n    def already_merged(self, paths: Sequence[PathBytes]):\n        \"\"\"Returns true if all the paths being imported were part of a merge\n        during previous tasks.\n        \"\"\"\n        for path in paths:\n            if path not in self._merged_items and path not in self._merged_dirs:\n                return False\n        return True\n\n    def mark_merged(self, paths: Sequence[PathBytes]):\n        \"\"\"Mark paths and directories as merged for future reimport tasks.\"\"\"\n        self._merged_items.update(paths)\n        dirs = {\n            os.path.dirname(path) if os.path.isfile(syspath(path)) else path\n            for path in paths\n        }\n        self._merged_dirs.update(dirs)\n\n    def is_resuming(self, toppath: PathBytes):\n        \"\"\"Return `True` if user wants to resume import of this path.\n\n        You have to call `ask_resume` first to determine the return value.\n        \"\"\"\n        return self._is_resuming.get(toppath, False)\n\n    def ask_resume(self, toppath: PathBytes):\n        \"\"\"If import of `toppath` was aborted in an earlier session, ask\n        user if they want to resume the import.\n\n        Determines the return value of `is_resuming(toppath)`.\n        \"\"\"\n        if self.want_resume and ImportState().progress_has(toppath):\n            # Either accept immediately or prompt for input to decide.\n            if self.want_resume is True or self.should_resume(toppath):\n                log.warning(\n                    \"Resuming interrupted import of {}\",\n                    util.displayable_path(toppath),\n                )\n                self._is_resuming[toppath] = True\n            else:\n                # Clear progress; we're starting from the top.\n                ImportState().progress_reset(toppath)\n"
  },
  {
    "path": "beets/importer/stages.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\nfrom __future__ import annotations\n\nimport itertools\nimport logging\nfrom typing import TYPE_CHECKING\n\nfrom beets import config, plugins\nfrom beets.util import MoveOperation, displayable_path, pipeline\n\nfrom .tasks import (\n    Action,\n    ImportTask,\n    ImportTaskFactory,\n    SentinelImportTask,\n    SingletonImportTask,\n)\n\nif TYPE_CHECKING:\n    from collections.abc import Callable\n\n    from beets import library\n\n    from .session import ImportSession\n\n# Global logger.\nlog = logging.getLogger(\"beets\")\n\n# ---------------------------- Producer functions ---------------------------- #\n# Functions that are called first i.e. they generate import tasks\n\n\ndef read_tasks(session: ImportSession):\n    \"\"\"A generator yielding all the albums (as ImportTask objects) found\n    in the user-specified list of paths. In the case of a singleton\n    import, yields single-item tasks instead.\n    \"\"\"\n    skipped = 0\n\n    for toppath in session.paths:\n        # Check whether we need to resume the import.\n        session.ask_resume(toppath)\n\n        # Generate tasks.\n        task_factory = ImportTaskFactory(toppath, session)\n        yield from task_factory.tasks()\n        skipped += task_factory.skipped\n\n        if not task_factory.imported:\n            log.warning(\"No files imported from {}\", displayable_path(toppath))\n\n    # Show skipped directories (due to incremental/resume).\n    if skipped:\n        log.info(\"Skipped {} paths.\", skipped)\n\n\ndef query_tasks(session: ImportSession):\n    \"\"\"A generator that works as a drop-in-replacement for read_tasks.\n    Instead of finding files from the filesystem, a query is used to\n    match items from the library.\n    \"\"\"\n    task: ImportTask\n    if session.config[\"singletons\"]:\n        # Search for items.\n        for item in session.lib.items(session.query):\n            task = SingletonImportTask(None, item)\n            for task in task.handle_created(session):\n                yield task\n\n    else:\n        # Search for albums.\n        for album in session.lib.albums(session.query):\n            log.debug(\n                \"yielding album {0.id}: {0.albumartist} - {0.album}\", album\n            )\n            items = list(album.items())\n            _freshen_items(items)\n\n            task = ImportTask(None, [album.item_dir()], items)\n            for task in task.handle_created(session):\n                yield task\n\n\n# ---------------------------------- Stages ---------------------------------- #\n# Functions that process import tasks, may transform or filter them\n# They are chained together in the pipeline e.g. stage2(stage1(task)) -> task\n\n\ndef group_albums(session: ImportSession):\n    \"\"\"A pipeline stage that groups the items of each task into albums\n    using their metadata.\n\n    Groups are identified using their artist and album fields. The\n    pipeline stage emits new album tasks for each discovered group.\n    \"\"\"\n\n    def group(item):\n        return (item.albumartist or item.artist, item.album)\n\n    task = None\n    while True:\n        task = yield task\n        if task.skip:\n            continue\n        tasks = []\n        sorted_items: list[library.Item] = sorted(task.items, key=group)\n        for _, items in itertools.groupby(sorted_items, group):\n            l_items = list(items)\n            task = ImportTask(task.toppath, [i.path for i in l_items], l_items)\n            tasks += task.handle_created(session)\n        tasks.append(SentinelImportTask(task.toppath, task.paths))\n\n        task = pipeline.multiple(tasks)\n\n\n@pipeline.mutator_stage\ndef lookup_candidates(session: ImportSession, task: ImportTask):\n    \"\"\"A coroutine for performing the initial MusicBrainz lookup for an\n    album. It accepts lists of Items and yields\n    (items, cur_artist, cur_album, candidates, rec) tuples. If no match\n    is found, all of the yielded parameters (except items) are None.\n    \"\"\"\n    if task.skip:\n        # FIXME This gets duplicated a lot. We need a better\n        # abstraction.\n        return\n\n    plugins.send(\"import_task_start\", session=session, task=task)\n    log.debug(\"Looking up: {}\", displayable_path(task.paths))\n\n    # Restrict the initial lookup to IDs specified by the user via the -m\n    # option. Currently all the IDs are passed onto the tasks directly.\n    task.lookup_candidates(session.config[\"search_ids\"].as_str_seq())\n\n\n@pipeline.stage\ndef user_query(session: ImportSession, task: ImportTask):\n    \"\"\"A coroutine for interfacing with the user about the tagging\n    process.\n\n    The coroutine accepts an ImportTask objects. It uses the\n    session's `choose_match` method to determine the `action` for\n    this task. Depending on the action additional stages are executed\n    and the processed task is yielded.\n\n    It emits the ``import_task_choice`` event for plugins. Plugins have\n    access to the choice via the ``task.choice_flag`` property and may\n    choose to change it.\n    \"\"\"\n    if task.skip:\n        return task\n\n    if session.already_merged(task.paths):\n        return pipeline.BUBBLE\n\n    # Ask the user for a choice.\n    task.choose_match(session)\n    plugins.send(\"import_task_choice\", session=session, task=task)\n\n    # As-tracks: transition to singleton workflow.\n    if task.choice_flag is Action.TRACKS:\n        # Set up a little pipeline for dealing with the singletons.\n        def emitter(task):\n            for item in task.items:\n                task = SingletonImportTask(task.toppath, item)\n                yield from task.handle_created(session)\n            yield SentinelImportTask(task.toppath, task.paths)\n\n        return _extend_pipeline(\n            emitter(task), lookup_candidates(session), user_query(session)\n        )\n\n    # As albums: group items by albums and create task for each album\n    if task.choice_flag is Action.ALBUMS:\n        return _extend_pipeline(\n            [task],\n            group_albums(session),\n            lookup_candidates(session),\n            user_query(session),\n        )\n\n    _resolve_duplicates(session, task)\n\n    if task.should_merge_duplicates:\n        # Create a new task for tagging the current items\n        # and duplicates together\n        duplicate_items = task.duplicate_items(session.lib)\n\n        # Duplicates would be reimported so make them look \"fresh\"\n        _freshen_items(duplicate_items)\n        duplicate_paths = [item.path for item in duplicate_items]\n\n        # Record merged paths in the session so they are not reimported\n        session.mark_merged(duplicate_paths)\n\n        merged_task = ImportTask(\n            None, task.paths + duplicate_paths, task.items + duplicate_items\n        )\n\n        return _extend_pipeline(\n            [merged_task], lookup_candidates(session), user_query(session)\n        )\n\n    _apply_choice(session, task)\n    return task\n\n\n@pipeline.mutator_stage\ndef import_asis(session: ImportSession, task: ImportTask):\n    \"\"\"Select the `action.ASIS` choice for all tasks.\n\n    This stage replaces the initial_lookup and user_query stages\n    when the importer is run without autotagging.\n    \"\"\"\n    if task.skip:\n        return\n\n    log.info(\"{}\", displayable_path(task.paths))\n    task.set_choice(Action.ASIS)\n    _resolve_duplicates(session, task)\n    _apply_choice(session, task)\n\n\n@pipeline.mutator_stage\ndef plugin_stage(\n    session: ImportSession,\n    func: Callable[[ImportSession, ImportTask], None],\n    task: ImportTask,\n):\n    \"\"\"A coroutine (pipeline stage) that calls the given function with\n    each non-skipped import task. These stages occur between applying\n    metadata changes and moving/copying/writing files.\n    \"\"\"\n    if task.skip:\n        return\n\n    func(session, task)\n\n    # Stage may modify DB, so re-load cached item data.\n    # FIXME Importer plugins should not modify the database but instead\n    # the albums and items attached to tasks.\n    task.reload()\n\n\n@pipeline.stage\ndef log_files(session: ImportSession, task: ImportTask):\n    \"\"\"A coroutine (pipeline stage) to log each file to be imported.\"\"\"\n    if isinstance(task, SingletonImportTask):\n        log.info(\"Singleton: {}\", displayable_path(task.item[\"path\"]))\n    elif task.items:\n        log.info(\"Album: {}\", displayable_path(task.paths[0]))\n        for item in task.items:\n            log.info(\"  {}\", displayable_path(item[\"path\"]))\n\n\n# --------------------------------- Consumer --------------------------------- #\n# Anything that should be placed last in the pipeline\n# In theory every stage could be a consumer, but in practice there are some\n# functions which are typically placed last in the pipeline\n\n\n@pipeline.stage\ndef manipulate_files(session: ImportSession, task: ImportTask):\n    \"\"\"A coroutine (pipeline stage) that performs necessary file\n    manipulations *after* items have been added to the library and\n    finalizes each task.\n    \"\"\"\n    if not task.skip:\n        if task.should_remove_duplicates:\n            task.remove_duplicates(session.lib)\n\n        if session.config[\"move\"]:\n            operation = MoveOperation.MOVE\n        elif session.config[\"copy\"]:\n            operation = MoveOperation.COPY\n        elif session.config[\"link\"]:\n            operation = MoveOperation.LINK\n        elif session.config[\"hardlink\"]:\n            operation = MoveOperation.HARDLINK\n        elif session.config[\"reflink\"] == \"auto\":\n            operation = MoveOperation.REFLINK_AUTO\n        elif session.config[\"reflink\"]:\n            operation = MoveOperation.REFLINK\n        else:\n            operation = None\n\n        task.manipulate_files(\n            session=session,\n            operation=operation,\n            write=session.config[\"write\"],\n        )\n\n    # Progress, cleanup, and event.\n    task.finalize(session)\n\n\n# ---------------------------- Utility functions ----------------------------- #\n# Private functions only used in the stages above\n\n\ndef _apply_choice(session: ImportSession, task: ImportTask):\n    \"\"\"Apply the task's choice to the Album or Item it contains and add\n    it to the library.\n    \"\"\"\n    if task.skip:\n        return\n\n    # Change metadata.\n    if task.apply:\n        task.apply_metadata()\n        plugins.send(\"import_task_apply\", session=session, task=task)\n\n    task.add(session.lib)\n\n    # If ``set_fields`` is set, set those fields to the\n    # configured values.\n    # NOTE: This cannot be done before the ``task.add()`` call above,\n    # because then the ``ImportTask`` won't have an `album` for which\n    # it can set the fields.\n    if config[\"import\"][\"set_fields\"]:\n        task.set_fields(session.lib)\n\n\ndef _resolve_duplicates(session: ImportSession, task: ImportTask):\n    \"\"\"Check if a task conflicts with items or albums already imported\n    and ask the session to resolve this.\n    \"\"\"\n    if task.choice_flag in (Action.ASIS, Action.APPLY, Action.RETAG):\n        found_duplicates = task.find_duplicates(session.lib)\n        if found_duplicates:\n            log.debug(\"found duplicates: {}\", [o.id for o in found_duplicates])\n\n            # Get the default action to follow from config.\n            duplicate_action = config[\"import\"][\"duplicate_action\"].as_choice(\n                {\n                    \"skip\": \"s\",\n                    \"keep\": \"k\",\n                    \"remove\": \"r\",\n                    \"merge\": \"m\",\n                    \"ask\": \"a\",\n                }\n            )\n            log.debug(\"default action for duplicates: {}\", duplicate_action)\n\n            if duplicate_action == \"s\":\n                # Skip new.\n                task.set_choice(Action.SKIP)\n            elif duplicate_action == \"k\":\n                # Keep both. Do nothing; leave the choice intact.\n                pass\n            elif duplicate_action == \"r\":\n                # Remove old.\n                task.should_remove_duplicates = True\n            elif duplicate_action == \"m\":\n                # Merge duplicates together\n                task.should_merge_duplicates = True\n            else:\n                # No default action set; ask the session.\n                session.resolve_duplicate(task, found_duplicates)\n\n            session.log_choice(task, True)\n\n\ndef _freshen_items(items):\n    # Clear IDs from re-tagged items so they appear \"fresh\" when\n    # we add them back to the library.\n    for item in items:\n        item.id = None\n        item.album_id = None\n\n\ndef _extend_pipeline(tasks, *stages):\n    # Return pipeline extension for stages with list of tasks\n    if isinstance(tasks, list):\n        task_iter = iter(tasks)\n    else:\n        task_iter = tasks\n\n    ipl = pipeline.Pipeline([task_iter, *list(stages)])\n    return pipeline.multiple(ipl.pull())\n"
  },
  {
    "path": "beets/importer/state.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\nfrom __future__ import annotations\n\nimport logging\nimport os\nimport pickle\nfrom bisect import bisect_left, insort\nfrom dataclasses import dataclass\nfrom typing import TYPE_CHECKING\n\nfrom beets import config\n\nif TYPE_CHECKING:\n    from beets.util import PathBytes\n\n\n# Global logger.\nlog = logging.getLogger(\"beets\")\n\n\n@dataclass\nclass ImportState:\n    \"\"\"Representing the progress of an import task.\n\n    Opens the state file on creation of the class. If you want\n    to ensure the state is written to disk, you should use the\n    context manager protocol.\n\n    Tagprogress allows long tagging tasks to be resumed when they pause.\n\n    Taghistory is a utility for manipulating the \"incremental\" import log.\n    This keeps track of all directories that were ever imported, which\n    allows the importer to only import new stuff.\n\n    Usage\n    -----\n    ```\n    # Readonly\n    progress = ImportState().tagprogress\n\n    # Read and write\n    with ImportState() as state:\n        state[\"key\"] = \"value\"\n    ```\n    \"\"\"\n\n    tagprogress: dict[PathBytes, list[PathBytes]]\n    taghistory: set[tuple[PathBytes, ...]]\n    path: PathBytes\n\n    def __init__(self, readonly=False, path: PathBytes | None = None):\n        self.path = path or os.fsencode(config[\"statefile\"].as_filename())\n        self.tagprogress = {}\n        self.taghistory = set()\n        self._open()\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self._save()\n\n    def _open(\n        self,\n    ):\n        try:\n            with open(self.path, \"rb\") as f:\n                state = pickle.load(f)\n                # Read the states\n                self.tagprogress = state.get(\"tagprogress\", {})\n                self.taghistory = state.get(\"taghistory\", set())\n        except Exception as exc:\n            # The `pickle` module can emit all sorts of exceptions during\n            # unpickling, including ImportError. We use a catch-all\n            # exception to avoid enumerating them all (the docs don't even have a\n            # full list!).\n            log.debug(\"state file could not be read: {}\", exc)\n\n    def _save(self):\n        try:\n            with open(self.path, \"wb\") as f:\n                pickle.dump(\n                    {\n                        \"tagprogress\": self.tagprogress,\n                        \"taghistory\": self.taghistory,\n                    },\n                    f,\n                )\n        except OSError as exc:\n            log.error(\"state file could not be written: {}\", exc)\n\n    # -------------------------------- Tagprogress ------------------------------- #\n\n    def progress_add(self, toppath: PathBytes, *paths: PathBytes):\n        \"\"\"Record that the files under all of the `paths` have been imported\n        under `toppath`.\n        \"\"\"\n        with self as state:\n            imported = state.tagprogress.setdefault(toppath, [])\n            for path in paths:\n                if imported and imported[-1] <= path:\n                    imported.append(path)\n                else:\n                    insort(imported, path)\n\n    def progress_has_element(self, toppath: PathBytes, path: PathBytes) -> bool:\n        \"\"\"Return whether `path` has been imported in `toppath`.\"\"\"\n        imported = self.tagprogress.get(toppath, [])\n        i = bisect_left(imported, path)\n        return i != len(imported) and imported[i] == path\n\n    def progress_has(self, toppath: PathBytes) -> bool:\n        \"\"\"Return `True` if there exist paths that have already been\n        imported under `toppath`.\n        \"\"\"\n        return toppath in self.tagprogress\n\n    def progress_reset(self, toppath: PathBytes | None):\n        \"\"\"Reset the progress for `toppath`.\"\"\"\n        with self as state:\n            if toppath in state.tagprogress:\n                del state.tagprogress[toppath]\n\n    # -------------------------------- Taghistory -------------------------------- #\n\n    def history_add(self, paths: list[PathBytes]):\n        \"\"\"Add the paths to the history.\"\"\"\n        with self as state:\n            state.taghistory.add(tuple(paths))\n"
  },
  {
    "path": "beets/importer/tasks.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\nfrom __future__ import annotations\n\nimport logging\nimport os\nimport re\nimport shutil\nimport time\nfrom collections import defaultdict\nfrom collections.abc import Callable\nfrom enum import Enum\nfrom tempfile import mkdtemp\nfrom typing import TYPE_CHECKING, Any\n\nimport mediafile\n\nfrom beets import autotag, config, library, plugins, util\nfrom beets.dbcore.query import PathQuery\n\nfrom .state import ImportState\n\nif TYPE_CHECKING:\n    from collections.abc import Iterable, Sequence\n\n    from beets.autotag.match import Recommendation\n\n    from .session import ImportSession\n\n# Global logger.\nlog = logging.getLogger(\"beets\")\n\n\nSINGLE_ARTIST_THRESH = 0.25\n\n# Usually flexible attributes are preserved (i.e., not updated) during\n# reimports. The following two lists (globally) change this behaviour for\n# certain fields. To alter these lists only when a specific plugin is in use,\n# something like this can be used within that plugin's code:\n#\n# from beets import importer\n# def extend_reimport_fresh_fields_item():\n#     importer.REIMPORT_FRESH_FIELDS_ITEM.extend(['tidal_track_popularity']\n# )\nREIMPORT_FRESH_FIELDS_ITEM = [\n    \"data_source\",\n    \"bandcamp_album_id\",\n    \"spotify_album_id\",\n    \"deezer_album_id\",\n    \"beatport_album_id\",\n    \"tidal_album_id\",\n    \"data_url\",\n]\nREIMPORT_FRESH_FIELDS_ALBUM = [*REIMPORT_FRESH_FIELDS_ITEM, \"media\"]\n\n# Global logger.\nlog = logging.getLogger(\"beets\")\n\n\nclass ImportAbortError(Exception):\n    \"\"\"Raised when the user aborts the tagging operation.\"\"\"\n\n    pass\n\n\nclass Action(Enum):\n    \"\"\"Enumeration of possible actions for an import task.\"\"\"\n\n    SKIP = \"SKIP\"\n    ASIS = \"ASIS\"\n    TRACKS = \"TRACKS\"\n    APPLY = \"APPLY\"\n    ALBUMS = \"ALBUMS\"\n    RETAG = \"RETAG\"\n    # The RETAG action represents \"don't apply any match, but do record\n    # new metadata\". It's not reachable via the standard command prompt but\n    # can be used by plugins.\n\n\nclass BaseImportTask:\n    \"\"\"An abstract base class for importer tasks.\n\n    Tasks flow through the importer pipeline. Each stage can update\n    them.\"\"\"\n\n    toppath: util.PathBytes | None\n    paths: list[util.PathBytes]\n    items: list[library.Item]\n\n    def __init__(\n        self,\n        toppath: util.PathBytes | None,\n        paths: Iterable[util.PathBytes] | None,\n        items: Iterable[library.Item] | None,\n    ):\n        \"\"\"Create a task. The primary fields that define a task are:\n\n        * `toppath`: The user-specified base directory that contains the\n          music for this task. If the task has *no* user-specified base\n          (for example, when importing based on an -L query), this can\n          be None. This is used for tracking progress and history.\n        * `paths`: A list of *specific* paths where the music for this task\n          came from. These paths can be directories, when their entire\n          contents are being imported, or files, when the task comprises\n          individual tracks. This is used for progress/history tracking and\n          for displaying the task to the user.\n        * `items`: A list of `Item` objects representing the music being\n          imported.\n\n        These fields should not change after initialization.\n        \"\"\"\n        self.toppath = toppath\n        self.paths = list(paths) if paths is not None else []\n        self.items = list(items) if items is not None else []\n\n\nclass ImportTask(BaseImportTask):\n    \"\"\"Represents a single set of items to be imported along with its\n    intermediate state. May represent an album or a single item.\n\n    The import session and stages call the following methods in the\n    given order.\n\n    * `lookup_candidates()` Sets the `common_artist`, `common_album`,\n      `candidates`, and `rec` attributes. `candidates` is a list of\n      `AlbumMatch` objects.\n\n    * `choose_match()` Uses the session to set the `match` attribute\n      from the `candidates` list.\n\n    * `find_duplicates()` Returns a list of albums from `lib` with the\n      same artist and album name as the task.\n\n    * `apply_metadata()` Sets the attributes of the items from the\n      task's `match` attribute.\n\n    * `add()` Add the imported items and album to the database.\n\n    * `manipulate_files()` Copy, move, and write files depending on the\n      session configuration.\n\n    * `set_fields()` Sets the fields given at CLI or configuration to\n      the specified values.\n\n    * `finalize()` Update the import progress and cleanup the file\n      system.\n    \"\"\"\n\n    choice_flag: Action | None = None\n    match: autotag.AlbumMatch | autotag.TrackMatch | None = None\n\n    # Keep track of the current task item\n    cur_album: str | None = None\n    cur_artist: str | None = None\n    candidates: Sequence[autotag.AlbumMatch | autotag.TrackMatch] = []\n    rec: Recommendation | None = None\n\n    def __init__(\n        self,\n        toppath: util.PathBytes | None,\n        paths: Iterable[util.PathBytes] | None,\n        items: Iterable[library.Item] | None,\n    ):\n        super().__init__(toppath, paths, items)\n        self.should_remove_duplicates = False\n        self.should_merge_duplicates = False\n        self.is_album = True\n\n    def set_choice(\n        self, choice: Action | autotag.AlbumMatch | autotag.TrackMatch\n    ):\n        \"\"\"Given an AlbumMatch or TrackMatch object or an action constant,\n        indicates that an action has been selected for this task.\n\n        Album and trackmatch are implemented as tuples, so we can't\n        use isinstance to check for them.\n        \"\"\"\n        # Not part of the task structure:\n        assert choice != Action.APPLY  # Only used internally.\n\n        if choice in (\n            Action.SKIP,\n            Action.ASIS,\n            Action.TRACKS,\n            Action.ALBUMS,\n            Action.RETAG,\n        ):\n            # TODO: redesign to stricten the type\n            self.choice_flag = choice  # type: ignore[assignment]\n            self.match = None\n        else:\n            self.choice_flag = Action.APPLY  # Implicit choice.\n            self.match = choice  # type: ignore[assignment]\n\n    def save_progress(self):\n        \"\"\"Updates the progress state to indicate that this album has\n        finished.\n        \"\"\"\n        if self.toppath:\n            ImportState().progress_add(self.toppath, *self.paths)\n\n    def save_history(self):\n        \"\"\"Save the directory in the history for incremental imports.\"\"\"\n        ImportState().history_add(self.paths)\n\n    # Logical decisions.\n\n    @property\n    def apply(self):\n        return self.choice_flag == Action.APPLY\n\n    @property\n    def skip(self):\n        return self.choice_flag == Action.SKIP\n\n    # Convenient data.\n\n    def chosen_info(self):\n        \"\"\"Return a dictionary of metadata about the current choice.\n        May only be called when the choice flag is ASIS or RETAG\n        (in which case the data comes from the files' current metadata)\n        or APPLY (in which case the data comes from the choice).\n        \"\"\"\n        if self.choice_flag in (Action.ASIS, Action.RETAG):\n            likelies, _ = util.get_most_common_tags(self.items)\n            return likelies\n        elif self.choice_flag is Action.APPLY and self.match:\n            return self.match.info.copy()\n        assert False\n\n    def imported_items(self):\n        \"\"\"Return a list of Items that should be added to the library.\n\n        If the tasks applies an album match the method only returns the\n        matched items.\n        \"\"\"\n        if self.choice_flag in (Action.ASIS, Action.RETAG):\n            return self.items\n        elif self.choice_flag == Action.APPLY and isinstance(\n            self.match, autotag.AlbumMatch\n        ):\n            return self.match.items\n        else:\n            return []\n\n    def apply_metadata(self):\n        \"\"\"Copy metadata from match info to the items.\"\"\"\n        if config[\"import\"][\"from_scratch\"]:\n            for item in self.match.items:\n                item.clear()\n\n        autotag.apply_metadata(self.match.info, self.match.item_info_pairs)\n\n    def duplicate_items(self, lib: library.Library):\n        duplicate_items = []\n        for album in self.find_duplicates(lib):\n            duplicate_items += album.items()\n        return duplicate_items\n\n    def remove_duplicates(self, lib: library.Library):\n        duplicate_items = self.duplicate_items(lib)\n        log.debug(\"removing {} old duplicated items\", len(duplicate_items))\n        for item in duplicate_items:\n            item.remove()\n            if lib.directory in util.ancestry(item.path):\n                log.debug(\"deleting duplicate {.filepath}\", item)\n                util.remove(item.path)\n                util.prune_dirs(os.path.dirname(item.path), lib.directory)\n\n    def set_fields(self, lib: library.Library):\n        \"\"\"Sets the fields given at CLI or configuration to the specified\n        values, for both the album and all its items.\n        \"\"\"\n        items = self.imported_items()\n        for field, view in config[\"import\"][\"set_fields\"].items():\n            value = str(view.get())\n            log.debug(\n                \"Set field {}={} for {}\",\n                field,\n                value,\n                util.displayable_path(self.paths),\n            )\n            self.album.set_parse(field, format(self.album, value))\n            for item in items:\n                item.set_parse(field, format(item, value))\n        with lib.transaction():\n            for item in items:\n                item.store()\n            self.album.store()\n\n    def finalize(self, session: ImportSession):\n        \"\"\"Save progress, clean up files, and emit plugin event.\"\"\"\n        # Update progress.\n        if session.want_resume:\n            self.save_progress()\n        if session.config[\"incremental\"] and not (\n            # Should we skip recording to incremental list?\n            self.skip and session.config[\"incremental_skip_later\"]\n        ):\n            self.save_history()\n\n        self.cleanup(\n            copy=session.config[\"copy\"],\n            delete=session.config[\"delete\"],\n            move=session.config[\"move\"],\n        )\n\n        if not self.skip:\n            self._emit_imported(session.lib)\n\n    def cleanup(self, copy=False, delete=False, move=False):\n        \"\"\"Remove and prune imported paths.\"\"\"\n        # Do not delete any files or prune directories when skipping.\n        if self.skip:\n            return\n\n        items = self.imported_items()\n\n        # When copying and deleting originals, delete old files.\n        if copy and delete:\n            new_paths = [os.path.realpath(item.path) for item in items]\n            for old_path in self.old_paths:\n                # Only delete files that were actually copied.\n                if old_path not in new_paths:\n                    util.remove(old_path, False)\n                    self.prune(old_path)\n\n        # When moving, prune empty directories containing the original files.\n        elif move:\n            for old_path in self.old_paths:\n                self.prune(old_path)\n\n    def _emit_imported(self, lib: library.Library):\n        plugins.send(\"album_imported\", lib=lib, album=self.album)\n\n    def handle_created(self, session: ImportSession):\n        \"\"\"Send the `import_task_created` event for this task. Return a list of\n        tasks that should continue through the pipeline. By default, this is a\n        list containing only the task itself, but plugins can replace the task\n        with new ones.\n        \"\"\"\n        tasks = plugins.send(\"import_task_created\", session=session, task=self)\n        if not tasks:\n            tasks = [self]\n        else:\n            # The plugins gave us a list of lists of tasks. Flatten it.\n            tasks = [t for inner in tasks for t in inner]\n        return tasks\n\n    def lookup_candidates(self, search_ids: list[str]) -> None:\n        \"\"\"Retrieve and store candidates for this album.\n\n        If User-specified ``search_ids`` list is not empty, the lookup is\n        restricted to only those IDs.\n        \"\"\"\n        self.cur_artist, self.cur_album, (self.candidates, self.rec) = (\n            autotag.tag_album(self.items, search_ids=search_ids)\n        )\n\n    def find_duplicates(self, lib: library.Library) -> list[library.Album]:\n        \"\"\"Return a list of albums from `lib` with the same artist and\n        album name as the task.\n        \"\"\"\n        info = self.chosen_info()\n        info[\"albumartist\"] = info[\"artist\"]\n\n        if info[\"artist\"] is None:\n            # As-is import with no artist. Skip check.\n            return []\n\n        # Construct a query to find duplicates with this metadata. We\n        # use a temporary Album object to generate any computed fields.\n        tmp_album = library.Album(lib, **info)\n        keys: list[str] = config[\"import\"][\"duplicate_keys\"][\n            \"album\"\n        ].as_str_seq()\n        dup_query = tmp_album.duplicates_query(keys)\n\n        # Don't count albums with the same files as duplicates.\n        task_paths = {i.path for i in self.items if i}\n\n        duplicates = []\n        for album in lib.albums(dup_query):\n            # Check whether the album paths are all present in the task\n            # i.e. album is being completely re-imported by the task,\n            # in which case it is not a duplicate (will be replaced).\n            album_paths = {i.path for i in album.items()}\n            if not (album_paths <= task_paths):\n                duplicates.append(album)\n\n        return duplicates\n\n    def align_album_level_fields(self):\n        \"\"\"Make some album fields equal across `self.items`. For the\n        RETAG action, we assume that the responsible for returning it\n        (ie. a plugin) always ensures that the first item contains\n        valid data on the relevant fields.\n        \"\"\"\n        changes = {}\n\n        if self.choice_flag == Action.ASIS:\n            # Taking metadata \"as-is\". Guess whether this album is VA.\n            plur_albumartist, freq = util.plurality(\n                [i.albumartist or i.artist for i in self.items]\n            )\n            if freq == len(self.items) or (\n                freq > 1\n                and float(freq) / len(self.items) >= SINGLE_ARTIST_THRESH\n            ):\n                # Single-artist album.\n                changes[\"albumartist\"] = plur_albumartist\n                changes[\"comp\"] = False\n            else:\n                # VA.\n                changes[\"albumartist\"] = config[\"va_name\"].as_str()\n                changes[\"comp\"] = True\n\n        elif self.choice_flag in (Action.APPLY, Action.RETAG):\n            # Applying autotagged metadata. Just get AA from the first\n            # item.\n            if not self.items[0].albumartist:\n                changes[\"albumartist\"] = self.items[0].artist\n            if not self.items[0].albumartists:\n                changes[\"albumartists\"] = self.items[0].artists\n            if not self.items[0].mb_albumartistid:\n                changes[\"mb_albumartistid\"] = self.items[0].mb_artistid\n            if not self.items[0].mb_albumartistids:\n                changes[\"mb_albumartistids\"] = self.items[0].mb_artistids\n\n        # Apply new metadata.\n        for item in self.items:\n            item.update(changes)\n\n    def manipulate_files(\n        self,\n        session: ImportSession,\n        operation: util.MoveOperation | None = None,\n        write=False,\n    ):\n        \"\"\"Copy, move, link, hardlink or reflink (depending on `operation`)\n        the files as well as write metadata.\n\n        `operation` should be an instance of `util.MoveOperation`.\n\n        If `write` is `True` metadata is written to the files.\n        # TODO: Introduce a MoveOperation.NONE or SKIP\n        \"\"\"\n\n        items = self.imported_items()\n        # Save the original paths of all items for deletion and pruning\n        # in the next step (finalization).\n        self.old_paths: list[util.PathBytes] = [item.path for item in items]\n        for item in items:\n            if operation is not None:\n                # In copy and link modes, treat re-imports specially:\n                # move in-library files. (Out-of-library files are\n                # copied/moved as usual).\n                old_path = item.path\n                if (\n                    operation != util.MoveOperation.MOVE\n                    and self.replaced_items[item]\n                    and session.lib.directory in util.ancestry(old_path)\n                ):\n                    item.move()\n                    # We moved the item, so remove the\n                    # now-nonexistent file from old_paths.\n                    self.old_paths.remove(old_path)\n                else:\n                    # A normal import. Just copy files and keep track of\n                    # old paths.\n                    item.move(operation)\n\n            if write and (self.apply or self.choice_flag == Action.RETAG):\n                item.try_write()\n\n        with session.lib.transaction():\n            for item in self.imported_items():\n                item.store()\n\n        plugins.send(\"import_task_files\", session=session, task=self)\n\n    def add(self, lib: library.Library):\n        \"\"\"Add the items as an album to the library and remove replaced items.\"\"\"\n        self.align_album_level_fields()\n        with lib.transaction():\n            self.record_replaced(lib)\n            self.remove_replaced(lib)\n\n            self.album = lib.add_album(self.imported_items())\n            if self.choice_flag == Action.APPLY and isinstance(\n                self.match, autotag.AlbumMatch\n            ):\n                # Copy album flexible fields to the DB\n                # TODO: change the flow so we create the `Album` object earlier,\n                #   and we can move this into `self.apply_metadata`, just like\n                #   is done for tracks.\n                autotag.apply_album_metadata(self.match.info, self.album)\n                self.album.store()\n\n            self.reimport_metadata(lib)\n\n    def record_replaced(self, lib: library.Library):\n        \"\"\"Records the replaced items and albums in the `replaced_items`\n        and `replaced_albums` dictionaries.\n        \"\"\"\n        self.replaced_items = defaultdict(list)\n        self.replaced_albums: dict[util.PathBytes, library.Album] = (\n            defaultdict()\n        )\n        replaced_album_ids = set()\n        for item in self.imported_items():\n            dup_items = list(lib.items(query=PathQuery(\"path\", item.path)))\n            self.replaced_items[item] = dup_items\n            for dup_item in dup_items:\n                if (\n                    not dup_item.album_id\n                    or dup_item.album_id in replaced_album_ids\n                ):\n                    continue\n                replaced_album = dup_item._cached_album\n                if replaced_album:\n                    replaced_album_ids.add(dup_item.album_id)\n                    self.replaced_albums[replaced_album.path] = replaced_album\n\n    def reimport_metadata(self, lib: library.Library):\n        \"\"\"For reimports, preserves metadata for reimported items and\n        albums.\n        \"\"\"\n\n        def _reduce_and_log(new_obj, existing_fields, overwrite_keys):\n            \"\"\"Some flexible attributes should be overwritten (rather than\n            preserved) on reimports; Copies existing_fields, logs and removes\n            entries that should not be preserved and returns a dict containing\n            those fields left to actually be preserved.\n            \"\"\"\n            noun = \"album\" if isinstance(new_obj, library.Album) else \"item\"\n            existing_fields = dict(existing_fields)\n            overwritten_fields = [\n                k\n                for k in existing_fields\n                if k in overwrite_keys\n                and new_obj.get(k)\n                and existing_fields.get(k) != new_obj.get(k)\n            ]\n            if overwritten_fields:\n                log.debug(\n                    \"Reimported {0} {1.id}. Not preserving flexible attributes {2}. \"\n                    \"Path: {1.filepath}\",\n                    noun,\n                    new_obj,\n                    overwritten_fields,\n                )\n                for key in overwritten_fields:\n                    del existing_fields[key]\n            return existing_fields\n\n        if self.is_album:\n            replaced_album = self.replaced_albums.get(self.album.path)\n            if replaced_album:\n                album_fields = _reduce_and_log(\n                    self.album,\n                    replaced_album._values_flex,\n                    REIMPORT_FRESH_FIELDS_ALBUM,\n                )\n                self.album.added = replaced_album.added\n                self.album.update(album_fields)\n                self.album.artpath = replaced_album.artpath\n                self.album.store()\n                log.debug(\n                    \"Reimported album {0.album.id}. Preserving attribute ['added']. \"\n                    \"Path: {0.album.filepath}\",\n                    self,\n                )\n                log.debug(\n                    \"Reimported album {0.album.id}. Preserving flexible\"\n                    \" attributes {1}. Path: {0.album.filepath}\",\n                    self,\n                    list(album_fields.keys()),\n                )\n\n        for item in self.imported_items():\n            dup_items = self.replaced_items[item]\n            for dup_item in dup_items:\n                if dup_item.added and dup_item.added != item.added:\n                    item.added = dup_item.added\n                    log.debug(\n                        \"Reimported item {0.id}. Preserving attribute ['added']. \"\n                        \"Path: {0.filepath}\",\n                        item,\n                    )\n                item_fields = _reduce_and_log(\n                    item, dup_item._values_flex, REIMPORT_FRESH_FIELDS_ITEM\n                )\n                item.update(item_fields)\n                log.debug(\n                    \"Reimported item {0.id}. Preserving flexible attributes {1}. \"\n                    \"Path: {0.filepath}\",\n                    item,\n                    list(item_fields.keys()),\n                )\n                item.store()\n\n    def remove_replaced(self, lib):\n        \"\"\"Removes all the items from the library that have the same\n        path as an item from this task.\n        \"\"\"\n        for item in self.imported_items():\n            for dup_item in self.replaced_items[item]:\n                log.debug(\"Replacing item {.id}: {.filepath}\", dup_item, item)\n                dup_item.remove()\n        log.debug(\n            \"{} of {} items replaced\",\n            sum(bool(v) for v in self.replaced_items.values()),\n            len(self.imported_items()),\n        )\n\n    def choose_match(self, session):\n        \"\"\"Ask the session which match should apply and apply it.\"\"\"\n        choice = session.choose_match(self)\n        self.set_choice(choice)\n        session.log_choice(self)\n\n    def reload(self):\n        \"\"\"Reload albums and items from the database.\"\"\"\n        for item in self.imported_items():\n            item.load()\n        self.album.load()\n\n    # Utilities.\n\n    def prune(self, filename):\n        \"\"\"Prune any empty directories above the given file. If this\n        task has no `toppath` or the file path provided is not within\n        the `toppath`, then this function has no effect. Similarly, if\n        the file still exists, no pruning is performed, so it's safe to\n        call when the file in question may not have been removed.\n        \"\"\"\n        if self.toppath and not os.path.exists(util.syspath(filename)):\n            util.prune_dirs(\n                os.path.dirname(filename),\n                self.toppath,\n                clutter=config[\"clutter\"].as_str_seq(),\n            )\n\n\nclass SingletonImportTask(ImportTask):\n    \"\"\"ImportTask for a single track that is not associated to an album.\"\"\"\n\n    def __init__(self, toppath: util.PathBytes | None, item: library.Item):\n        super().__init__(toppath, [item.path], [item])\n        self.item = item\n        self.is_album = False\n        self.paths = [item.path]\n\n    def chosen_info(self):\n        \"\"\"Return a dictionary of metadata about the current choice.\n        May only be called when the choice flag is ASIS or RETAG\n        (in which case the data comes from the files' current metadata)\n        or APPLY (in which case the data comes from the choice).\n        \"\"\"\n        assert self.choice_flag in (Action.ASIS, Action.RETAG, Action.APPLY)\n        if self.choice_flag in (Action.ASIS, Action.RETAG):\n            return dict(self.item)\n        elif self.choice_flag is Action.APPLY:\n            return self.match.info.copy()\n\n    def imported_items(self):\n        return [self.item]\n\n    def apply_metadata(self):\n        if config[\"import\"][\"from_scratch\"]:\n            self.item.clear()\n        autotag.apply_item_metadata(self.item, self.match.info)\n\n    def _emit_imported(self, lib):\n        for item in self.imported_items():\n            plugins.send(\"item_imported\", lib=lib, item=item)\n\n    def lookup_candidates(self, search_ids: list[str]) -> None:\n        self.candidates, self.rec = autotag.tag_item(\n            self.item, search_ids=search_ids\n        )\n\n    def find_duplicates(self, lib: library.Library) -> list[library.Item]:  # type: ignore[override] # Need splitting Singleton and Album tasks into separate classes\n        \"\"\"Return a list of items from `lib` that have the same artist\n        and title as the task.\n        \"\"\"\n        info = self.chosen_info()\n\n        # Query for existing items using the same metadata. We use a\n        # temporary `Item` object to generate any computed fields.\n        tmp_item = library.Item(lib, **info)\n        keys: list[str] = config[\"import\"][\"duplicate_keys\"][\n            \"item\"\n        ].as_str_seq()\n        dup_query = tmp_item.duplicates_query(keys)\n\n        found_items = []\n        for other_item in lib.items(dup_query):\n            # Existing items not considered duplicates.\n            if other_item.path != self.item.path:\n                found_items.append(other_item)\n        return found_items\n\n    duplicate_items = find_duplicates\n\n    def add(self, lib):\n        with lib.transaction():\n            self.record_replaced(lib)\n            self.remove_replaced(lib)\n            lib.add(self.item)\n            self.reimport_metadata(lib)\n\n    def infer_album_fields(self):\n        raise NotImplementedError\n\n    def choose_match(self, session: ImportSession):\n        \"\"\"Ask the session which match should apply and apply it.\"\"\"\n        choice = session.choose_item(self)\n        self.set_choice(choice)\n        session.log_choice(self)\n\n    def reload(self):\n        self.item.load()\n\n    def set_fields(self, lib):\n        \"\"\"Sets the fields given at CLI or configuration to the specified\n        values, for the singleton item.\n        \"\"\"\n        for field, view in config[\"import\"][\"set_fields\"].items():\n            value = str(view.get())\n            log.debug(\n                \"Set field {}={} for {}\",\n                field,\n                value,\n                util.displayable_path(self.paths),\n            )\n            self.item.set_parse(field, format(self.item, value))\n        self.item.store()\n\n\n# FIXME The inheritance relationships are inverted. This is why there\n# are so many methods which pass. More responsibility should be delegated to\n# the BaseImportTask class.\nclass SentinelImportTask(ImportTask):\n    \"\"\"A sentinel task marks the progress of an import and does not\n    import any items itself.\n\n    If only `toppath` is set the task indicates the end of a top-level\n    directory import. If the `paths` argument is also given, the task\n    indicates the progress in the `toppath` import.\n    \"\"\"\n\n    def __init__(self, toppath, paths):\n        super().__init__(toppath, paths, ())\n        # TODO Remove the remaining attributes eventually\n        self.should_remove_duplicates = False\n        self.is_album = True\n        self.choice_flag = None\n\n    def save_history(self):\n        pass\n\n    def save_progress(self):\n        if not self.paths:\n            # \"Done\" sentinel.\n            ImportState().progress_reset(self.toppath)\n        elif self.toppath:\n            # \"Directory progress\" sentinel for singletons\n            super().save_progress()\n\n    @property\n    def skip(self) -> bool:\n        return True\n\n    def set_choice(self, choice):\n        raise NotImplementedError\n\n    def cleanup(self, copy=False, delete=False, move=False):\n        pass\n\n    def _emit_imported(self, lib):\n        pass\n\n\nArchiveHandler = tuple[\n    Callable[[util.StrPath], bool], Callable[[util.StrPath], Any]\n]\n\n\nclass ArchiveImportTask(SentinelImportTask):\n    \"\"\"An import task that represents the processing of an archive.\n\n    `toppath` must be a `zip`, `tar`, or `rar` archive. Archive tasks\n    serve two purposes:\n    - First, it will unarchive the files to a temporary directory and\n      return it. The client should read tasks from the resulting\n      directory and send them through the pipeline.\n    - Second, it will clean up the temporary directory when it proceeds\n      through the pipeline. The client should send the archive task\n      after sending the rest of the music tasks to make this work.\n    \"\"\"\n\n    def __init__(self, toppath):\n        super().__init__(toppath, ())\n        self.extracted = False\n\n    @classmethod\n    def is_archive(cls, path):\n        \"\"\"Returns true if the given path points to an archive that can\n        be handled.\n        \"\"\"\n        if not os.path.isfile(path):\n            return False\n\n        for path_test, _ in cls.handlers:\n            if path_test(os.fsdecode(path)):\n                return True\n        return False\n\n    @util.cached_classproperty\n    def handlers(cls) -> list[ArchiveHandler]:\n        \"\"\"Returns a list of archive handlers.\n\n        Each handler is a `(path_test, ArchiveClass)` tuple. `path_test`\n        is a function that returns `True` if the given path can be\n        handled by `ArchiveClass`. `ArchiveClass` is a class that\n        implements the same interface as `tarfile.TarFile`.\n        \"\"\"\n        _handlers: list[ArchiveHandler] = []\n        from zipfile import ZipFile, is_zipfile\n\n        _handlers.append((is_zipfile, ZipFile))\n        import tarfile\n\n        _handlers.append((tarfile.is_tarfile, tarfile.open))\n        try:\n            from rarfile import RarFile, is_rarfile\n        except ImportError:\n            pass\n        else:\n            _handlers.append((is_rarfile, RarFile))\n        try:\n            from py7zr import SevenZipFile, is_7zfile\n        except ImportError:\n            pass\n        else:\n            _handlers.append((is_7zfile, SevenZipFile))\n\n        return _handlers\n\n    def cleanup(self, copy=False, delete=False, move=False):\n        \"\"\"Removes the temporary directory the archive was extracted to.\"\"\"\n        if self.extracted and self.toppath:\n            log.debug(\n                \"Removing extracted directory: {}\",\n                util.displayable_path(self.toppath),\n            )\n            shutil.rmtree(util.syspath(self.toppath))\n\n    def extract(self):\n        \"\"\"Extracts the archive to a temporary directory and sets\n        `toppath` to that directory.\n        \"\"\"\n        assert self.toppath is not None, \"toppath must be set\"\n\n        for path_test, handler_class in self.handlers:\n            if path_test(os.fsdecode(self.toppath)):\n                break\n        else:\n            raise ValueError(f\"No handler found for archive: {self.toppath}\")\n        extract_to = mkdtemp()\n        archive = handler_class(os.fsdecode(self.toppath), mode=\"r\")\n        try:\n            archive.extractall(extract_to)\n\n            # Adjust the files' mtimes to match the information from the\n            # archive. Inspired by: https://stackoverflow.com/q/9813243\n            for f in archive.infolist():\n                # The date_time will need to adjusted otherwise\n                # the item will have the current date_time of extraction.\n                # The (0, 0, -1) is added to date_time because the\n                # function time.mktime expects a 9-element tuple.\n                # The -1 indicates that the DST flag is unknown.\n                date_time = time.mktime((*f.date_time, 0, 0, -1))\n                fullpath = os.path.join(extract_to, f.filename)\n                os.utime(fullpath, (date_time, date_time))\n\n        finally:\n            archive.close()\n        self.extracted = True\n        self.toppath = extract_to\n\n\nclass ImportTaskFactory:\n    \"\"\"Generate album and singleton import tasks for all media files\n    indicated by a path.\n    \"\"\"\n\n    def __init__(self, toppath: util.PathBytes, session: ImportSession):\n        \"\"\"Create a new task factory.\n\n        `toppath` is the user-specified path to search for music to\n        import. `session` is the `ImportSession`, which controls how\n        tasks are read from the directory.\n        \"\"\"\n        self.toppath = toppath\n        self.session = session\n        self.skipped = 0  # Skipped due to incremental/resume.\n        self.imported = 0  # \"Real\" tasks created.\n        self.is_archive = ArchiveImportTask.is_archive(util.syspath(toppath))\n\n    def tasks(self) -> Iterable[ImportTask]:\n        \"\"\"Yield all import tasks for music found in the user-specified\n        path `self.toppath`. Any necessary sentinel tasks are also\n        produced.\n\n        During generation, update `self.skipped` and `self.imported`\n        with the number of tasks that were not produced (due to\n        incremental mode or resumed imports) and the number of concrete\n        tasks actually produced, respectively.\n\n        If `self.toppath` is an archive, it is adjusted to point to the\n        extracted data.\n        \"\"\"\n        # Check whether this is an archive.\n        archive_task: ArchiveImportTask | None = None\n        if self.is_archive:\n            archive_task = self.unarchive()\n            if not archive_task:\n                return\n\n        # Search for music in the directory.\n        for dirs, paths in self.paths():\n            if self.session.config[\"singletons\"]:\n                for path in paths:\n                    tasks = self._create(self.singleton(path))\n                    yield from tasks\n                yield self.sentinel(dirs)\n\n            else:\n                tasks = self._create(self.album(paths, dirs))\n                yield from tasks\n\n        # Produce the final sentinel for this toppath to indicate that\n        # it is finished. This is usually just a SentinelImportTask, but\n        # for archive imports, send the archive task instead (to remove\n        # the extracted directory).\n        yield archive_task or self.sentinel()\n\n    def _create(self, task: ImportTask | None):\n        \"\"\"Handle a new task to be emitted by the factory.\n\n        Emit the `import_task_created` event and increment the\n        `imported` count if the task is not skipped. Return the same\n        task. If `task` is None, do nothing.\n        \"\"\"\n        if task:\n            tasks = task.handle_created(self.session)\n            self.imported += len(tasks)\n            return tasks\n        return []\n\n    def paths(self):\n        \"\"\"Walk `self.toppath` and yield `(dirs, files)` pairs where\n        `files` are individual music files and `dirs` the set of\n        containing directories where the music was found.\n\n        This can either be a recursive search in the ordinary case, a\n        single track when `toppath` is a file, a single directory in\n        `flat` mode.\n        \"\"\"\n        if not os.path.isdir(util.syspath(self.toppath)):\n            yield [self.toppath], [self.toppath]\n        elif self.session.config[\"flat\"]:\n            paths = []\n            for dirs, paths_in_dir in albums_in_dir(self.toppath):\n                paths += paths_in_dir\n            yield [self.toppath], paths\n        else:\n            for dirs, paths in albums_in_dir(self.toppath):\n                yield dirs, paths\n\n    def singleton(self, path: util.PathBytes):\n        \"\"\"Return a `SingletonImportTask` for the music file.\"\"\"\n        if self.session.already_imported(self.toppath, [path]):\n            log.debug(\n                \"Skipping previously-imported path: {}\",\n                util.displayable_path(path),\n            )\n            self.skipped += 1\n            return None\n\n        item = self.read_item(path)\n        if item:\n            return SingletonImportTask(self.toppath, item)\n        else:\n            return None\n\n    def album(self, paths: Iterable[util.PathBytes], dirs=None):\n        \"\"\"Return a `ImportTask` with all media files from paths.\n\n        `dirs` is a list of parent directories used to record already\n        imported albums.\n        \"\"\"\n\n        if dirs is None:\n            dirs = list({os.path.dirname(p) for p in paths})\n\n        if self.session.already_imported(self.toppath, dirs):\n            log.debug(\n                \"Skipping previously-imported path: {}\",\n                util.displayable_path(dirs),\n            )\n            self.skipped += 1\n            return None\n\n        items: list[library.Item] = [\n            item for item in map(self.read_item, paths) if item\n        ]\n\n        if len(items) > 0:\n            return ImportTask(self.toppath, dirs, items)\n        else:\n            return None\n\n    def sentinel(self, paths: Iterable[util.PathBytes] | None = None):\n        \"\"\"Return a `SentinelImportTask` indicating the end of a\n        top-level directory import.\n        \"\"\"\n        return SentinelImportTask(self.toppath, paths)\n\n    def unarchive(self):\n        \"\"\"Extract the archive for this `toppath`.\n\n        Extract the archive to a new directory, adjust `toppath` to\n        point to the extracted directory, and return an\n        `ArchiveImportTask`. If extraction fails, return None.\n        \"\"\"\n        assert self.is_archive\n\n        if not (self.session.config[\"move\"] or self.session.config[\"copy\"]):\n            log.warning(\n                \"Archive importing requires either \"\n                \"'copy' or 'move' to be enabled.\"\n            )\n            return\n\n        log.debug(\"Extracting archive: {}\", util.displayable_path(self.toppath))\n        archive_task = ArchiveImportTask(self.toppath)\n        try:\n            archive_task.extract()\n        except Exception as exc:\n            log.error(\"extraction failed: {}\", exc)\n            return\n\n        # Now read albums from the extracted directory.\n        self.toppath = archive_task.toppath\n        log.debug(\"Archive extracted to: {.toppath}\", self)\n        return archive_task\n\n    def read_item(self, path: util.PathBytes):\n        \"\"\"Return an `Item` read from the path.\n\n        If an item cannot be read, return `None` instead and log an\n        error.\n        \"\"\"\n        try:\n            return library.Item.from_path(path)\n        except library.ReadError as exc:\n            if isinstance(exc.reason, mediafile.FileTypeError):\n                # Silently ignore non-music files.\n                pass\n            elif isinstance(exc.reason, mediafile.UnreadableFileError):\n                log.warning(\"unreadable file: {}\", util.displayable_path(path))\n            else:\n                log.error(\n                    \"error reading {}: {}\", util.displayable_path(path), exc\n                )\n\n\nMULTIDISC_MARKERS = (rb\"dis[ck]\", rb\"cd\")\nMULTIDISC_PAT_FMT = rb\"^(.*%s[\\W_]*)\\d\"\n\n\ndef is_subdir_of_any_in_list(path, dirs):\n    \"\"\"Returns True if path os a subdirectory of any directory in dirs\n    (a list). In other case, returns False.\n    \"\"\"\n    ancestors = util.ancestry(path)\n    return any(d in ancestors for d in dirs)\n\n\ndef albums_in_dir(path: util.PathBytes):\n    \"\"\"Recursively searches the given directory and returns an iterable\n    of (paths, items) where paths is a list of directories and items is\n    a list of Items that is probably an album. Specifically, any folder\n    containing any media files is an album.\n    \"\"\"\n    collapse_paths: list[util.PathBytes] = []\n    collapse_items: list[util.PathBytes] = []\n    collapse_pat = None\n\n    ignore: list[str] = config[\"ignore\"].as_str_seq()\n    ignore_hidden: bool = config[\"ignore_hidden\"].get(bool)\n\n    for root, dirs, files in util.sorted_walk(\n        path, ignore=ignore, ignore_hidden=ignore_hidden, logger=log\n    ):\n        items = [os.path.join(root, f) for f in files]\n        # If we're currently collapsing the constituent directories in a\n        # multi-disc album, check whether we should continue collapsing\n        # and add the current directory. If so, just add the directory\n        # and move on to the next directory. If not, stop collapsing.\n        if collapse_paths:\n            if (is_subdir_of_any_in_list(root, collapse_paths)) or (\n                collapse_pat and collapse_pat.match(os.path.basename(root))\n            ):\n                # Still collapsing.\n                collapse_paths.append(root)\n                collapse_items += items\n                continue\n            else:\n                # Collapse finished. Yield the collapsed directory and\n                # proceed to process the current one.\n                if collapse_items:\n                    yield collapse_paths, collapse_items\n                collapse_pat, collapse_paths, collapse_items = None, [], []\n\n        # Check whether this directory looks like the *first* directory\n        # in a multi-disc sequence. There are two indicators: the file\n        # is named like part of a multi-disc sequence (e.g., \"Title Disc\n        # 1\") or it contains no items but only directories that are\n        # named in this way.\n        start_collapsing = False\n        for marker in MULTIDISC_MARKERS:\n            # We're using replace on %s due to lack of .format() on bytestrings\n            p = MULTIDISC_PAT_FMT.replace(b\"%s\", marker)\n            marker_pat = re.compile(p, re.I)\n            match = marker_pat.match(os.path.basename(root))\n\n            # Is this directory the root of a nested multi-disc album?\n            if dirs and not items:\n                # Check whether all subdirectories have the same prefix.\n                start_collapsing = True\n                subdir_pat = None\n                for subdir in dirs:\n                    subdir = util.bytestring_path(subdir)\n                    # The first directory dictates the pattern for\n                    # the remaining directories.\n                    if not subdir_pat:\n                        match = marker_pat.match(subdir)\n                        if match:\n                            match_group = re.escape(match.group(1))\n                            subdir_pat = re.compile(\n                                b\"\".join([b\"^\", match_group, rb\"\\d\"]), re.I\n                            )\n                        else:\n                            start_collapsing = False\n                            break\n\n                    # Subsequent directories must match the pattern.\n                    elif not subdir_pat.match(subdir):\n                        start_collapsing = False\n                        break\n\n                # If all subdirectories match, don't check other\n                # markers.\n                if start_collapsing:\n                    break\n\n            # Is this directory the first in a flattened multi-disc album?\n            elif match:\n                start_collapsing = True\n                # Set the current pattern to match directories with the same\n                # prefix as this one, followed by a digit.\n                collapse_pat = re.compile(\n                    b\"\".join([b\"^\", re.escape(match.group(1)), rb\"\\d\"]), re.I\n                )\n                break\n\n        # If either of the above heuristics indicated that this is the\n        # beginning of a multi-disc album, initialize the collapsed\n        # directory and item lists and check the next directory.\n        if start_collapsing:\n            # Start collapsing; continue to the next iteration.\n            collapse_paths = [root]\n            collapse_items = items\n            continue\n\n        # If it's nonempty, yield it.\n        if items:\n            yield [root], items\n\n    # Clear out any unfinished collapse.\n    if collapse_paths and collapse_items:\n        yield collapse_paths, collapse_items\n"
  },
  {
    "path": "beets/library/__init__.py",
    "content": "from beets.util.deprecation import deprecate_imports\n\nfrom .exceptions import FileOperationError, ReadError, WriteError\nfrom .library import Library\nfrom .models import Album, Item, LibModel\nfrom .queries import parse_query_parts, parse_query_string\n\nNEW_MODULE_BY_NAME = dict.fromkeys(\n    (\"DateType\", \"DurationType\", \"MusicalKey\", \"PathType\"), \"beets.dbcore.types\"\n) | dict.fromkeys(\n    (\"BLOB_TYPE\", \"SingletonQuery\", \"PathQuery\"), \"beets.dbcore.query\"\n)\n\n\ndef __getattr__(name: str):\n    return deprecate_imports(__name__, NEW_MODULE_BY_NAME, name)\n\n\n__all__ = [\n    \"Album\",\n    \"FileOperationError\",\n    \"Item\",\n    \"LibModel\",\n    \"Library\",\n    \"ReadError\",\n    \"WriteError\",\n    \"parse_query_parts\",\n    \"parse_query_string\",\n]\n"
  },
  {
    "path": "beets/library/exceptions.py",
    "content": "from beets import util\n\n\nclass FileOperationError(Exception):\n    \"\"\"Indicate an error when interacting with a file on disk.\n\n    Possibilities include an unsupported media type, a permissions\n    error, and an unhandled Mutagen exception.\n    \"\"\"\n\n    def __init__(self, path, reason):\n        \"\"\"Create an exception describing an operation on the file at\n        `path` with the underlying (chained) exception `reason`.\n        \"\"\"\n        super().__init__(path, reason)\n        self.path = path\n        self.reason = reason\n\n    def __str__(self):\n        \"\"\"Get a string representing the error.\n\n        Describe both the underlying reason and the file path in question.\n        \"\"\"\n        return f\"{util.displayable_path(self.path)}: {self.reason}\"\n\n\nclass ReadError(FileOperationError):\n    \"\"\"An error while reading a file (i.e. in `Item.read`).\"\"\"\n\n    def __str__(self):\n        return f\"error reading {super()}\"\n\n\nclass WriteError(FileOperationError):\n    \"\"\"An error while writing a file (i.e. in `Item.write`).\"\"\"\n\n    def __str__(self):\n        return f\"error writing {super()}\"\n"
  },
  {
    "path": "beets/library/library.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nimport platformdirs\n\nimport beets\nfrom beets import dbcore\nfrom beets.util import normpath\n\nfrom . import migrations\nfrom .models import Album, Item\nfrom .queries import PF_KEY_DEFAULT, parse_query_parts, parse_query_string\n\nif TYPE_CHECKING:\n    from beets.dbcore import Results\n\n\nclass Library(dbcore.Database):\n    \"\"\"A database of music containing songs and albums.\"\"\"\n\n    _models = (Item, Album)\n    _migrations = (\n        (migrations.MultiGenreFieldMigration, (Item, Album)),\n        (migrations.LyricsMetadataInFlexFieldsMigration, (Item,)),\n    )\n\n    def __init__(\n        self,\n        path=\"library.blb\",\n        directory: str | None = None,\n        path_formats=((PF_KEY_DEFAULT, \"$artist/$album/$track $title\"),),\n        replacements=None,\n    ):\n        timeout = beets.config[\"timeout\"].as_number()\n        super().__init__(path, timeout=timeout)\n\n        self.directory = normpath(directory or platformdirs.user_music_path())\n\n        self.path_formats = path_formats\n        self.replacements = replacements\n\n        # Used for template substitution performance.\n        self._memotable: dict[tuple[str, ...], str] = {}\n\n    # Adding objects to the database.\n\n    def add(self, obj):\n        \"\"\"Add the :class:`Item` or :class:`Album` object to the library\n        database.\n\n        Return the object's new id.\n        \"\"\"\n        obj.add(self)\n        self._memotable = {}\n        return obj.id\n\n    def add_album(self, items):\n        \"\"\"Create a new album consisting of a list of items.\n\n        The items are added to the database if they don't yet have an\n        ID. Return a new :class:`Album` object. The list items must not\n        be empty.\n        \"\"\"\n        if not items:\n            raise ValueError(\"need at least one item\")\n\n        # Create the album structure using metadata from the first item.\n        values = {key: items[0][key] for key in Album.item_keys}\n        album = Album(self, **values)\n\n        # Add the album structure and set the items' album_id fields.\n        # Store or add the items.\n        with self.transaction():\n            album.add(self)\n            for item in items:\n                item.album_id = album.id\n                if item.id is None:\n                    item.add(self)\n                else:\n                    item.store()\n\n        return album\n\n    # Querying.\n\n    def _fetch(self, model_cls, query, sort=None):\n        \"\"\"Parse a query and fetch.\n\n        If an order specification is present in the query string\n        the `sort` argument is ignored.\n        \"\"\"\n        # Parse the query, if necessary.\n        try:\n            parsed_sort = None\n            if isinstance(query, str):\n                query, parsed_sort = parse_query_string(query, model_cls)\n            elif isinstance(query, (list, tuple)):\n                query, parsed_sort = parse_query_parts(query, model_cls)\n        except dbcore.query.InvalidQueryArgumentValueError as exc:\n            raise dbcore.InvalidQueryError(query, exc)\n\n        # Any non-null sort specified by the parsed query overrides the\n        # provided sort.\n        if parsed_sort and not isinstance(parsed_sort, dbcore.query.NullSort):\n            sort = parsed_sort\n\n        return super()._fetch(model_cls, query, sort)\n\n    @staticmethod\n    def get_default_album_sort():\n        \"\"\"Get a :class:`Sort` object for albums from the config option.\"\"\"\n        return dbcore.sort_from_strings(\n            Album, beets.config[\"sort_album\"].as_str_seq()\n        )\n\n    @staticmethod\n    def get_default_item_sort():\n        \"\"\"Get a :class:`Sort` object for items from the config option.\"\"\"\n        return dbcore.sort_from_strings(\n            Item, beets.config[\"sort_item\"].as_str_seq()\n        )\n\n    def albums(self, query=None, sort=None) -> Results[Album]:\n        \"\"\"Get :class:`Album` objects matching the query.\"\"\"\n        return self._fetch(Album, query, sort or self.get_default_album_sort())\n\n    def items(self, query=None, sort=None) -> Results[Item]:\n        \"\"\"Get :class:`Item` objects matching the query.\"\"\"\n        return self._fetch(Item, query, sort or self.get_default_item_sort())\n\n    # Convenience accessors.\n    def get_item(self, id_: int) -> Item | None:\n        \"\"\"Fetch a :class:`Item` by its ID.\n\n        Return `None` if no match is found.\n        \"\"\"\n        return self._get(Item, id_)\n\n    def get_album(self, item_or_id: Item | int) -> Album | None:\n        \"\"\"Given an album ID or an item associated with an album, return\n        a :class:`Album` object for the album.\n\n        If no such album exists, return `None`.\n        \"\"\"\n        album_id = (\n            item_or_id if isinstance(item_or_id, int) else item_or_id.album_id\n        )\n        return self._get(Album, album_id) if album_id else None\n"
  },
  {
    "path": "beets/library/migrations.py",
    "content": "from __future__ import annotations\n\nfrom contextlib import suppress\nfrom functools import cached_property\nfrom typing import TYPE_CHECKING, NamedTuple, TypeVar\n\nfrom confuse.exceptions import ConfigError\n\nimport beets\nfrom beets import ui\nfrom beets.dbcore.db import Migration\nfrom beets.dbcore.types import MULTI_VALUE_DELIMITER\nfrom beets.util import unique_list\nfrom beets.util.lyrics import Lyrics\n\nif TYPE_CHECKING:\n    from collections.abc import Iterator\n\n    from beets.dbcore.db import Model\n\nT = TypeVar(\"T\")\n\n\nclass GenreRow(NamedTuple):\n    id: int\n    genre: str\n    genres: str | None\n\n\ndef chunks(lst: list[T], n: int) -> Iterator[list[T]]:\n    \"\"\"Yield successive n-sized chunks from lst.\"\"\"\n    for i in range(0, len(lst), n):\n        yield lst[i : i + n]\n\n\nclass MultiGenreFieldMigration(Migration):\n    \"\"\"Backfill multi-value genres from legacy single-string genre data.\"\"\"\n\n    @cached_property\n    def separators(self) -> list[str]:\n        \"\"\"Return known separators that indicate multiple legacy genres.\"\"\"\n        separators = []\n        with suppress(ConfigError):\n            separators.append(beets.config[\"lastgenre\"][\"separator\"].as_str())\n\n        separators.extend([\"; \", \", \", \" / \"])\n        return unique_list(filter(None, separators))\n\n    def get_genres(self, genre: str) -> str:\n        \"\"\"Normalize legacy genre separators to the canonical delimiter.\"\"\"\n        for separator in self.separators:\n            if separator in genre:\n                return genre.replace(separator, MULTI_VALUE_DELIMITER)\n\n        return genre\n\n    def _migrate_data(\n        self, model_cls: type[Model], current_fields: set[str]\n    ) -> None:\n        \"\"\"Migrate legacy genre values to the multi-value genres field.\"\"\"\n        if \"genre\" not in current_fields:\n            # No legacy genre field, so nothing to migrate.\n            return\n\n        table = model_cls._table\n\n        with self.db.transaction() as tx, self.with_row_factory(GenreRow):\n            rows: list[GenreRow] = tx.query(  # type: ignore[assignment]\n                f\"\"\"\n                SELECT id, genre, genres\n                FROM {table}\n                WHERE genre IS NOT NULL AND genre != ''\n                \"\"\"\n            )\n\n        total = len(rows)\n        to_migrate = [e for e in rows if not e.genres]\n        if not to_migrate:\n            return\n\n        migrated = total - len(to_migrate)\n\n        ui.print_(f\"Migrating genres for {total} {table}...\")\n        for batch in chunks(to_migrate, 1000):\n            with self.db.transaction() as tx:\n                tx.mutate_many(\n                    f\"UPDATE {table} SET genres = ? WHERE id = ?\",\n                    [(self.get_genres(e.genre), e.id) for e in batch],\n                )\n\n            migrated += len(batch)\n\n            ui.print_(\n                f\"  Migrated {migrated} {table} \"\n                f\"({migrated}/{total} processed)...\"\n            )\n\n        ui.print_(f\"Migration complete: {migrated} of {total} {table} updated\")\n\n\nclass LyricsRow(NamedTuple):\n    id: int\n    lyrics: str\n\n\nclass LyricsMetadataInFlexFieldsMigration(Migration):\n    \"\"\"Move legacy inline lyrics metadata into dedicated flexible fields.\"\"\"\n\n    def _migrate_data(self, model_cls: type[Model], _: set[str]) -> None:\n        \"\"\"Migrate legacy lyrics to move metadata to flex attributes.\"\"\"\n        table = model_cls._table\n        flex_table = model_cls._flex_table\n\n        with self.db.transaction() as tx:\n            migrated_ids = {\n                r[0]\n                for r in tx.query(\n                    f\"\"\"\n                    SELECT entity_id\n                    FROM {flex_table}\n                    WHERE key == 'lyrics_backend'\n                    \"\"\"\n                )\n            }\n        with self.db.transaction() as tx, self.with_row_factory(LyricsRow):\n            rows: list[LyricsRow] = tx.query(  # type: ignore[assignment]\n                f\"\"\"\n                SELECT id, lyrics\n                FROM {table}\n                WHERE lyrics IS NOT NULL AND lyrics != ''\n                \"\"\"\n            )\n\n        total = len(rows)\n        to_migrate = [r for r in rows if r.id not in migrated_ids]\n        if not to_migrate:\n            return\n\n        migrated = total - len(to_migrate)\n\n        ui.print_(f\"Migrating lyrics for {total} {table}...\")\n        lyr_fields = [\"backend\", \"url\", \"language\", \"translation_language\"]\n        for batch in chunks(to_migrate, 100):\n            lyrics_batch = [Lyrics.from_legacy_text(r.lyrics) for r in batch]\n            ids_with_lyrics = [\n                (lyr, r.id) for lyr, r in zip(lyrics_batch, batch)\n            ]\n            with self.db.transaction() as tx:\n                update_rows = [\n                    (lyr.full_text, r.id)\n                    for lyr, r in zip(lyrics_batch, batch)\n                    if lyr.full_text != r.lyrics\n                ]\n                if update_rows:\n                    tx.mutate_many(\n                        f\"UPDATE {table} SET lyrics = ? WHERE id = ?\",\n                        update_rows,\n                    )\n\n                # Only insert flex rows for non-null metadata values\n                flex_rows = [\n                    (_id, f\"lyrics_{field}\", val)\n                    for lyr, _id in ids_with_lyrics\n                    for field in lyr_fields\n                    if (val := getattr(lyr, field)) is not None\n                ]\n                if flex_rows:\n                    tx.mutate_many(\n                        f\"\"\"\n                        INSERT INTO {flex_table} (entity_id, key, value)\n                        VALUES (?, ?, ?)\n                        \"\"\",\n                        flex_rows,\n                    )\n\n            migrated += len(batch)\n\n            ui.print_(\n                f\"  Migrated {migrated} {table} \"\n                f\"({migrated}/{total} processed)...\"\n            )\n\n        ui.print_(f\"Migration complete: {migrated} of {total} {table} updated\")\n"
  },
  {
    "path": "beets/library/models.py",
    "content": "from __future__ import annotations\n\nimport os\nimport string\nimport sys\nimport time\nimport unicodedata\nfrom functools import cached_property\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, ClassVar\n\nfrom mediafile import MediaFile, UnreadableFileError\n\nimport beets\nfrom beets import dbcore, logging, plugins, util\nfrom beets.dbcore import types\nfrom beets.util import (\n    MoveOperation,\n    bytestring_path,\n    cached_classproperty,\n    normpath,\n    samefile,\n    syspath,\n)\nfrom beets.util.functemplate import Template, template\n\nfrom .exceptions import FileOperationError, ReadError, WriteError\nfrom .queries import PF_KEY_DEFAULT, parse_query_string\n\nif TYPE_CHECKING:\n    from ..dbcore.query import FieldQuery, FieldQueryType\n    from .library import Library  # noqa: F401\n\nlog = logging.getLogger(\"beets\")\n\n\nclass LibModel(dbcore.Model[\"Library\"]):\n    \"\"\"Shared concrete functionality for Items and Albums.\"\"\"\n\n    # Config key that specifies how an instance should be formatted.\n    _format_config_key: str\n    path: bytes\n    length: float\n\n    @cached_classproperty\n    def _types(cls) -> dict[str, types.Type]:\n        \"\"\"Return the types of the fields in this model.\"\"\"\n        return {\n            **plugins.types(cls),  # type: ignore[arg-type]\n            \"data_source\": types.STRING,\n        }\n\n    @cached_classproperty\n    def _queries(cls) -> dict[str, FieldQueryType]:\n        return plugins.named_queries(cls)  # type: ignore[arg-type]\n\n    @cached_classproperty\n    def writable_media_fields(cls) -> set[str]:\n        return set(MediaFile.fields()) & cls._fields.keys()\n\n    @property\n    def filepath(self) -> Path:\n        \"\"\"The path to the entity as pathlib.Path.\"\"\"\n        return Path(os.fsdecode(self.path))\n\n    def _template_funcs(self):\n        funcs = DefaultTemplateFunctions(self, self._db).functions()\n        funcs.update(plugins.template_funcs())\n        return funcs\n\n    def store(self, fields=None):\n        super().store(fields)\n        plugins.send(\"database_change\", lib=self._db, model=self)\n\n    def remove(self):\n        super().remove()\n        plugins.send(\"database_change\", lib=self._db, model=self)\n\n    def add(self, lib=None):\n        # super().add() calls self.store(), which sends `database_change`,\n        # so don't do it here\n        super().add(lib)\n\n    def __format__(self, spec):\n        if not spec:\n            spec = beets.config[self._format_config_key].as_str()\n        assert isinstance(spec, str)\n        return self.evaluate_template(spec)\n\n    def __str__(self):\n        return format(self)\n\n    def __bytes__(self):\n        return self.__str__().encode(\"utf-8\")\n\n    # Convenient queries.\n\n    @classmethod\n    def field_query(\n        cls, field: str, pattern: str, query_cls: FieldQueryType\n    ) -> FieldQuery:\n        \"\"\"Get a `FieldQuery` for the given field on this model.\"\"\"\n        fast = field in cls.all_db_fields\n        if field in cls.shared_db_fields:\n            # This field exists in both tables, so SQLite will encounter\n            # an OperationalError if we try to use it in a query.\n            # Using an explicit table name resolves this.\n            field = f\"{cls._table}.{field}\"\n\n        return query_cls(field, pattern, fast)\n\n    @classmethod\n    def any_field_query(cls, *args, **kwargs) -> dbcore.OrQuery:\n        return dbcore.OrQuery(\n            [cls.field_query(f, *args, **kwargs) for f in cls._search_fields]\n        )\n\n    @classmethod\n    def any_writable_media_field_query(cls, *args, **kwargs) -> dbcore.OrQuery:\n        fields = cls.writable_media_fields\n        return dbcore.OrQuery(\n            [cls.field_query(f, *args, **kwargs) for f in fields]\n        )\n\n    def duplicates_query(self, fields: list[str]) -> dbcore.AndQuery:\n        \"\"\"Return a query for entities with same values in the given fields.\"\"\"\n        return dbcore.AndQuery(\n            [\n                self.field_query(f, self.get(f), dbcore.MatchQuery)\n                for f in fields\n            ]\n        )\n\n\nclass FormattedItemMapping(dbcore.db.FormattedMapping):\n    \"\"\"Add lookup for album-level fields.\n\n    Album-level fields take precedence if `for_path` is true.\n    \"\"\"\n\n    ALL_KEYS = \"*\"\n\n    def __init__(self, item, included_keys=ALL_KEYS, for_path=False):\n        # We treat album and item keys specially here,\n        # so exclude transitive album keys from the model's keys.\n        super().__init__(item, included_keys=[], for_path=for_path)\n        self.included_keys = included_keys\n        if included_keys == self.ALL_KEYS:\n            # Performance note: this triggers a database query.\n            self.model_keys = item.keys(computed=True, with_album=False)\n        else:\n            self.model_keys = included_keys\n        self.item = item\n\n    @cached_property\n    def all_keys(self):\n        return set(self.model_keys).union(self.album_keys)\n\n    @cached_property\n    def album_keys(self):\n        album_keys = []\n        if self.album:\n            if self.included_keys == self.ALL_KEYS:\n                # Performance note: this triggers a database query.\n                for key in self.album.keys(computed=True):\n                    if (\n                        key in Album.item_keys\n                        or key not in self.item._fields.keys()\n                    ):\n                        album_keys.append(key)\n            else:\n                album_keys = self.included_keys\n        return album_keys\n\n    @property\n    def album(self):\n        return self.item._cached_album\n\n    def _get(self, key):\n        \"\"\"Get the value for a key, either from the album or the item.\n\n        Raise a KeyError for invalid keys.\n        \"\"\"\n        if self.for_path and key in self.album_keys:\n            return self._get_formatted(self.album, key)\n        elif key in self.model_keys:\n            return self._get_formatted(self.model, key)\n        elif key in self.album_keys:\n            return self._get_formatted(self.album, key)\n        else:\n            raise KeyError(key)\n\n    def __getitem__(self, key):\n        \"\"\"Get the value for a key.\n\n        `artist` and `albumartist` are fallback values for each other\n        when not set.\n        \"\"\"\n        value = self._get(key)\n\n        # `artist` and `albumartist` fields fall back to one another.\n        # This is helpful in path formats when the album artist is unset\n        # on as-is imports.\n        try:\n            if key == \"artist\" and not value:\n                return self._get(\"albumartist\")\n            elif key == \"albumartist\" and not value:\n                return self._get(\"artist\")\n        except KeyError:\n            pass\n\n        return value\n\n    def __iter__(self):\n        return iter(self.all_keys)\n\n    def __len__(self):\n        return len(self.all_keys)\n\n\nclass Album(LibModel):\n    \"\"\"Provide access to information about albums stored in a\n    library.\n\n    Reflects the library's \"albums\" table, including album art.\n    \"\"\"\n\n    artpath: bytes\n\n    _table = \"albums\"\n    _flex_table = \"album_attributes\"\n    _always_dirty = True\n    _fields: ClassVar[dict[str, types.Type]] = {\n        \"id\": types.PRIMARY_ID,\n        \"artpath\": types.NullPathType(),\n        \"added\": types.DATE,\n        \"albumartist\": types.STRING,\n        \"albumartist_sort\": types.STRING,\n        \"albumartist_credit\": types.STRING,\n        \"albumartists\": types.MULTI_VALUE_DSV,\n        \"albumartists_sort\": types.MULTI_VALUE_DSV,\n        \"albumartists_credit\": types.MULTI_VALUE_DSV,\n        \"album\": types.STRING,\n        \"genres\": types.MULTI_VALUE_DSV,\n        \"style\": types.STRING,\n        \"discogs_albumid\": types.INTEGER,\n        \"discogs_artistid\": types.INTEGER,\n        \"discogs_labelid\": types.INTEGER,\n        \"year\": types.PaddedInt(4),\n        \"month\": types.PaddedInt(2),\n        \"day\": types.PaddedInt(2),\n        \"disctotal\": types.PaddedInt(2),\n        \"comp\": types.BOOLEAN,\n        \"mb_albumid\": types.STRING,\n        \"mb_albumartistid\": types.STRING,\n        \"mb_albumartistids\": types.MULTI_VALUE_DSV,\n        \"albumtype\": types.STRING,\n        \"albumtypes\": types.SEMICOLON_SPACE_DSV,\n        \"label\": types.STRING,\n        \"barcode\": types.STRING,\n        \"mb_releasegroupid\": types.STRING,\n        \"release_group_title\": types.STRING,\n        \"asin\": types.STRING,\n        \"catalognum\": types.STRING,\n        \"script\": types.STRING,\n        \"language\": types.STRING,\n        \"country\": types.STRING,\n        \"albumstatus\": types.STRING,\n        \"albumdisambig\": types.STRING,\n        \"releasegroupdisambig\": types.STRING,\n        \"rg_album_gain\": types.NULL_FLOAT,\n        \"rg_album_peak\": types.NULL_FLOAT,\n        \"r128_album_gain\": types.NULL_FLOAT,\n        \"original_year\": types.PaddedInt(4),\n        \"original_month\": types.PaddedInt(2),\n        \"original_day\": types.PaddedInt(2),\n    }\n\n    _search_fields = (\"album\", \"albumartist\", \"genres\")\n\n    @cached_classproperty\n    def _types(cls) -> dict[str, types.Type]:\n        return {**super()._types, \"path\": types.PathType()}\n\n    _sorts: ClassVar[dict[str, type[dbcore.query.FieldSort]]] = {\n        \"albumartist\": dbcore.query.SmartArtistSort,\n        \"artist\": dbcore.query.SmartArtistSort,\n    }\n\n    # List of keys that are set on an album's items.\n    item_keys: ClassVar[list[str]] = [\n        \"added\",\n        \"albumartist\",\n        \"albumartists\",\n        \"albumartist_sort\",\n        \"albumartists_sort\",\n        \"albumartist_credit\",\n        \"albumartists_credit\",\n        \"album\",\n        \"genres\",\n        \"style\",\n        \"discogs_albumid\",\n        \"discogs_artistid\",\n        \"discogs_labelid\",\n        \"year\",\n        \"month\",\n        \"day\",\n        \"disctotal\",\n        \"comp\",\n        \"mb_albumid\",\n        \"mb_albumartistid\",\n        \"mb_albumartistids\",\n        \"albumtype\",\n        \"albumtypes\",\n        \"label\",\n        \"barcode\",\n        \"mb_releasegroupid\",\n        \"asin\",\n        \"catalognum\",\n        \"script\",\n        \"language\",\n        \"country\",\n        \"albumstatus\",\n        \"albumdisambig\",\n        \"releasegroupdisambig\",\n        \"release_group_title\",\n        \"rg_album_gain\",\n        \"rg_album_peak\",\n        \"r128_album_gain\",\n        \"original_year\",\n        \"original_month\",\n        \"original_day\",\n    ]\n\n    _format_config_key = \"format_album\"\n\n    @cached_classproperty\n    def _relation(cls) -> type[Item]:\n        return Item\n\n    @cached_classproperty\n    def relation_join(cls) -> str:\n        \"\"\"Return FROM clause which joins on related album items.\n\n        Use LEFT join to select all albums, including those that do not have\n        any items.\n        \"\"\"\n        return (\n            f\"LEFT JOIN {cls._relation._table} \"\n            f\"ON {cls._table}.id = {cls._relation._table}.album_id\"\n        )\n\n    @property\n    def art_filepath(self) -> Path | None:\n        \"\"\"The path to album's cover picture as pathlib.Path.\"\"\"\n        return Path(os.fsdecode(self.artpath)) if self.artpath else None\n\n    @classmethod\n    def _getters(cls):\n        # In addition to plugin-provided computed fields, also expose\n        # the album's directory as `path`.\n        getters = plugins.album_field_getters()\n        getters[\"path\"] = Album.item_dir\n        getters[\"albumtotal\"] = Album._albumtotal\n        return getters\n\n    def items(self):\n        \"\"\"Return an iterable over the items associated with this\n        album.\n\n        This method conflicts with :meth:`LibModel.items`, which is\n        inherited from :meth:`beets.dbcore.Model.items`.\n        Since :meth:`Album.items` predates these methods, and is\n        likely to be used by plugins, we keep this interface as-is.\n        \"\"\"\n        return self._db.items(dbcore.MatchQuery(\"album_id\", self.id))\n\n    def remove(self, delete=False, with_items=True):\n        \"\"\"Remove this album and all its associated items from the\n        library.\n\n        If delete, then the items' files are also deleted from disk,\n        along with any album art. The directories containing the album are\n        also removed (recursively) if empty.\n\n        Set with_items to False to avoid removing the album's items.\n        \"\"\"\n        super().remove()\n\n        # Send a 'album_removed' signal to plugins\n        plugins.send(\"album_removed\", album=self)\n\n        # Delete art file.\n        if delete:\n            artpath = self.artpath\n            if artpath:\n                util.remove(artpath)\n\n        # Remove (and possibly delete) the constituent items.\n        if with_items:\n            for item in self.items():\n                item.remove(delete, False)\n\n    def move_art(self, operation=MoveOperation.MOVE):\n        \"\"\"Move, copy, link or hardlink (depending on `operation`) any\n        existing album art so that it remains in the same directory as\n        the items.\n\n        `operation` should be an instance of `util.MoveOperation`.\n        \"\"\"\n        old_art = self.artpath\n        if not old_art:\n            return\n\n        if not os.path.exists(syspath(old_art)):\n            log.error(\n                \"removing reference to missing album art file {}\",\n                util.displayable_path(old_art),\n            )\n            self.artpath = None\n            return\n\n        new_art = self.art_destination(old_art)\n        if new_art == old_art:\n            return\n\n        new_art = util.unique_path(new_art)\n        log.debug(\n            \"moving album art {} to {}\",\n            util.displayable_path(old_art),\n            util.displayable_path(new_art),\n        )\n        if operation == MoveOperation.MOVE:\n            util.move(old_art, new_art)\n            util.prune_dirs(os.path.dirname(old_art), self._db.directory)\n        elif operation == MoveOperation.COPY:\n            util.copy(old_art, new_art)\n        elif operation == MoveOperation.LINK:\n            util.link(old_art, new_art)\n        elif operation == MoveOperation.HARDLINK:\n            util.hardlink(old_art, new_art)\n        elif operation == MoveOperation.REFLINK:\n            util.reflink(old_art, new_art, fallback=False)\n        elif operation == MoveOperation.REFLINK_AUTO:\n            util.reflink(old_art, new_art, fallback=True)\n        else:\n            assert False, \"unknown MoveOperation\"\n        self.artpath = new_art\n\n    def move(self, operation=MoveOperation.MOVE, basedir=None, store=True):\n        \"\"\"Move, copy, link or hardlink (depending on `operation`)\n        all items to their destination. Any album art moves along with them.\n\n        `basedir` overrides the library base directory for the destination.\n\n        `operation` should be an instance of `util.MoveOperation`.\n\n        By default, the album is stored to the database, persisting any\n        modifications to its metadata. If `store` is `False` however,\n        the album is not stored automatically, and it will have to be manually\n        stored after invoking this method.\n        \"\"\"\n        basedir = basedir or self._db.directory\n\n        # Ensure new metadata is available to items for destination\n        # computation.\n        if store:\n            self.store()\n\n        # Move items.\n        items = list(self.items())\n        for item in items:\n            item.move(operation, basedir=basedir, with_album=False, store=store)\n\n        # Move art.\n        self.move_art(operation)\n        if store:\n            self.store()\n\n    def item_dir(self):\n        \"\"\"Return the directory containing the album's first item,\n        provided that such an item exists.\n        \"\"\"\n        item = self.items().get()\n        if not item:\n            raise ValueError(f\"empty album for album id {self.id}\")\n        return os.path.dirname(item.path)\n\n    def _albumtotal(self):\n        \"\"\"Return the total number of tracks on all discs on the album.\"\"\"\n        if self.disctotal == 1 or not beets.config[\"per_disc_numbering\"]:\n            return self.items()[0].tracktotal\n\n        counted = []\n        total = 0\n\n        for item in self.items():\n            if item.disc in counted:\n                continue\n\n            total += item.tracktotal\n            counted.append(item.disc)\n\n            if len(counted) == self.disctotal:\n                break\n\n        return total\n\n    def art_destination(self, image, item_dir=None):\n        \"\"\"Return a path to the destination for the album art image\n        for the album.\n\n        `image` is the path of the image that will be\n        moved there (used for its extension).\n\n        The path construction uses the existing path of the album's\n        items, so the album must contain at least one item or\n        item_dir must be provided.\n        \"\"\"\n        image = bytestring_path(image)\n        item_dir = item_dir or self.item_dir()\n\n        filename_tmpl = template(beets.config[\"art_filename\"].as_str())\n        subpath = self.evaluate_template(filename_tmpl, True)\n        if beets.config[\"asciify_paths\"]:\n            subpath = util.asciify_path(\n                subpath, beets.config[\"path_sep_replace\"].as_str()\n            )\n        subpath = util.sanitize_path(\n            subpath, replacements=self._db.replacements\n        )\n        subpath = bytestring_path(subpath)\n\n        _, ext = os.path.splitext(image)\n        dest = os.path.join(item_dir, subpath + ext)\n\n        return bytestring_path(dest)\n\n    def set_art(self, path, copy=True):\n        \"\"\"Set the album's cover art to the image at the given path.\n\n        The image is copied (or moved) into place, replacing any\n        existing art.\n\n        Send an 'art_set' event with `self` as the sole argument.\n        \"\"\"\n        path = bytestring_path(path)\n        oldart = self.artpath\n        artdest = self.art_destination(path)\n\n        if oldart and samefile(path, oldart):\n            # Art already set.\n            return\n        elif samefile(path, artdest):\n            # Art already in place.\n            self.artpath = path\n            return\n\n        # Normal operation.\n        if oldart == artdest:\n            util.remove(oldart)\n        artdest = util.unique_path(artdest)\n        if copy:\n            util.copy(path, artdest)\n        else:\n            util.move(path, artdest)\n        self.artpath = artdest\n\n        plugins.send(\"art_set\", album=self)\n\n    def store(self, fields=None, inherit=True):\n        \"\"\"Update the database with the album information.\n\n        `fields` represents the fields to be stored. If not specified,\n        all fields will be.\n\n        The album's tracks are also updated when the `inherit` flag is enabled.\n        This applies to fixed attributes as well as flexible ones. The `id`\n        attribute of the album will never be inherited.\n        \"\"\"\n        # Get modified track fields.\n        track_updates = {}\n        track_deletes = set()\n        for key in self._dirty:\n            if inherit:\n                if key in self.item_keys:  # is a fixed attribute\n                    track_updates[key] = self[key]\n                elif key not in self:  # is a fixed or a flexible attribute\n                    track_deletes.add(key)\n                elif key != \"id\":  # is a flexible attribute\n                    track_updates[key] = self[key]\n\n        with self._db.transaction():\n            super().store(fields)\n            if track_updates:\n                for item in self.items():\n                    for key, value in track_updates.items():\n                        item[key] = value\n                    item.store()\n            if track_deletes:\n                for item in self.items():\n                    for key in track_deletes:\n                        if key in item:\n                            del item[key]\n                    item.store()\n\n    def try_sync(self, write, move, inherit=True):\n        \"\"\"Synchronize the album and its items with the database.\n        Optionally, also write any new tags into the files and update\n        their paths.\n\n        `write` indicates whether to write tags to the item files, and\n        `move` controls whether files (both audio and album art) are\n        moved.\n        \"\"\"\n        self.store(inherit=inherit)\n        for item in self.items():\n            item.try_sync(write, move)\n\n    @cached_property\n    def length(self) -> float:  # type: ignore[override] # still writable since we override __setattr__\n        \"\"\"Return the total length of all items in this album in seconds.\"\"\"\n        return sum(item.length for item in self.items())\n\n\nclass Item(LibModel):\n    \"\"\"Represent a song or track.\"\"\"\n\n    album_id: int | None\n\n    _table = \"items\"\n    _flex_table = \"item_attributes\"\n    _fields: ClassVar[dict[str, types.Type]] = {\n        \"id\": types.PRIMARY_ID,\n        \"path\": types.PathType(),\n        \"album_id\": types.FOREIGN_ID,\n        \"title\": types.STRING,\n        \"artist\": types.STRING,\n        \"artists\": types.MULTI_VALUE_DSV,\n        \"artists_ids\": types.MULTI_VALUE_DSV,\n        \"artist_sort\": types.STRING,\n        \"artists_sort\": types.MULTI_VALUE_DSV,\n        \"artist_credit\": types.STRING,\n        \"artists_credit\": types.MULTI_VALUE_DSV,\n        \"remixer\": types.STRING,\n        \"album\": types.STRING,\n        \"albumartist\": types.STRING,\n        \"albumartists\": types.MULTI_VALUE_DSV,\n        \"albumartist_sort\": types.STRING,\n        \"albumartists_sort\": types.MULTI_VALUE_DSV,\n        \"albumartist_credit\": types.STRING,\n        \"albumartists_credit\": types.MULTI_VALUE_DSV,\n        \"genres\": types.MULTI_VALUE_DSV,\n        \"style\": types.STRING,\n        \"discogs_albumid\": types.INTEGER,\n        \"discogs_artistid\": types.INTEGER,\n        \"discogs_labelid\": types.INTEGER,\n        \"lyricist\": types.STRING,\n        \"composer\": types.STRING,\n        \"composer_sort\": types.STRING,\n        \"work\": types.STRING,\n        \"mb_workid\": types.STRING,\n        \"work_disambig\": types.STRING,\n        \"arranger\": types.STRING,\n        \"grouping\": types.STRING,\n        \"year\": types.PaddedInt(4),\n        \"month\": types.PaddedInt(2),\n        \"day\": types.PaddedInt(2),\n        \"track\": types.PaddedInt(2),\n        \"tracktotal\": types.PaddedInt(2),\n        \"disc\": types.PaddedInt(2),\n        \"disctotal\": types.PaddedInt(2),\n        \"lyrics\": types.STRING,\n        \"comments\": types.STRING,\n        \"bpm\": types.INTEGER,\n        \"comp\": types.BOOLEAN,\n        \"mb_trackid\": types.STRING,\n        \"mb_albumid\": types.STRING,\n        \"mb_artistid\": types.STRING,\n        \"mb_artistids\": types.MULTI_VALUE_DSV,\n        \"mb_albumartistid\": types.STRING,\n        \"mb_albumartistids\": types.MULTI_VALUE_DSV,\n        \"mb_releasetrackid\": types.STRING,\n        \"trackdisambig\": types.STRING,\n        \"albumtype\": types.STRING,\n        \"albumtypes\": types.SEMICOLON_SPACE_DSV,\n        \"label\": types.STRING,\n        \"barcode\": types.STRING,\n        \"acoustid_fingerprint\": types.STRING,\n        \"acoustid_id\": types.STRING,\n        \"mb_releasegroupid\": types.STRING,\n        \"release_group_title\": types.STRING,\n        \"asin\": types.STRING,\n        \"isrc\": types.STRING,\n        \"catalognum\": types.STRING,\n        \"script\": types.STRING,\n        \"language\": types.STRING,\n        \"country\": types.STRING,\n        \"albumstatus\": types.STRING,\n        \"media\": types.STRING,\n        \"albumdisambig\": types.STRING,\n        \"releasegroupdisambig\": types.STRING,\n        \"disctitle\": types.STRING,\n        \"encoder\": types.STRING,\n        \"rg_track_gain\": types.NULL_FLOAT,\n        \"rg_track_peak\": types.NULL_FLOAT,\n        \"rg_album_gain\": types.NULL_FLOAT,\n        \"rg_album_peak\": types.NULL_FLOAT,\n        \"r128_track_gain\": types.NULL_FLOAT,\n        \"r128_album_gain\": types.NULL_FLOAT,\n        \"original_year\": types.PaddedInt(4),\n        \"original_month\": types.PaddedInt(2),\n        \"original_day\": types.PaddedInt(2),\n        \"initial_key\": types.MusicalKey(),\n        \"length\": types.DurationType(),\n        \"bitrate\": types.ScaledInt(1000, \"kbps\"),\n        \"bitrate_mode\": types.STRING,\n        \"encoder_info\": types.STRING,\n        \"encoder_settings\": types.STRING,\n        \"format\": types.STRING,\n        \"samplerate\": types.ScaledInt(1000, \"kHz\"),\n        \"bitdepth\": types.INTEGER,\n        \"channels\": types.INTEGER,\n        \"mtime\": types.DATE,\n        \"added\": types.DATE,\n    }\n    _indices = (dbcore.Index(\"idx_item_album_id\", (\"album_id\",)),)\n\n    _search_fields = (\n        \"artist\",\n        \"title\",\n        \"comments\",\n        \"album\",\n        \"albumartist\",\n        \"genres\",\n    )\n\n    # Set of item fields that are backed by `MediaFile` fields.\n    # Any kind of field (fixed, flexible, and computed) may be a media\n    # field. Only these fields are read from disk in `read` and written in\n    # `write`.\n    _media_fields = set(MediaFile.readable_fields()).intersection(\n        _fields.keys()\n    )\n\n    # Set of item fields that are backed by *writable* `MediaFile` tag\n    # fields.\n    # This excludes fields that represent audio data, such as `bitrate` or\n    # `length`.\n    _media_tag_fields = set(MediaFile.fields()).intersection(_fields.keys())\n\n    _formatter = FormattedItemMapping\n\n    _sorts: ClassVar[dict[str, type[dbcore.query.FieldSort]]] = {\n        \"artist\": dbcore.query.SmartArtistSort\n    }\n\n    @cached_classproperty\n    def _queries(cls) -> dict[str, FieldQueryType]:\n        return {**super()._queries, \"singleton\": dbcore.query.SingletonQuery}\n\n    _format_config_key = \"format_item\"\n\n    # Cached album object. Read-only.\n    __album: Album | None = None\n\n    @cached_classproperty\n    def _relation(cls) -> type[Album]:\n        return Album\n\n    @cached_classproperty\n    def relation_join(cls) -> str:\n        \"\"\"Return the FROM clause which includes related albums.\n\n        We need to use a LEFT JOIN here, otherwise items that are not part of\n        an album (e.g. singletons) would be left out.\n        \"\"\"\n        return (\n            f\"LEFT JOIN {cls._relation._table} \"\n            f\"ON {cls._table}.album_id = {cls._relation._table}.id\"\n        )\n\n    @property\n    def _cached_album(self):\n        \"\"\"The Album object that this item belongs to, if any, or\n        None if the item is a singleton or is not associated with a\n        library.\n        The instance is cached and refreshed on access.\n\n        DO NOT MODIFY!\n        If you want a copy to modify, use :meth:`get_album`.\n        \"\"\"\n        if not self.__album and self._db:\n            self.__album = self._db.get_album(self)\n        elif self.__album:\n            self.__album.load()\n        return self.__album\n\n    @_cached_album.setter\n    def _cached_album(self, album):\n        self.__album = album\n\n    @classmethod\n    def _getters(cls):\n        getters = plugins.item_field_getters()\n        getters[\"singleton\"] = lambda i: i.album_id is None\n        getters[\"filesize\"] = Item.try_filesize  # In bytes.\n        return getters\n\n    def duplicates_query(self, fields: list[str]) -> dbcore.AndQuery:\n        \"\"\"Return a query for entities with same values in the given fields.\"\"\"\n        return super().duplicates_query(fields) & dbcore.query.NoneQuery(\n            \"album_id\"\n        )\n\n    @classmethod\n    def from_path(cls, path):\n        \"\"\"Create a new item from the media file at the specified path.\"\"\"\n        # Initiate with values that aren't read from files.\n        i = cls(album_id=None)\n        i.read(path)\n        i.mtime = i.current_mtime()  # Initial mtime.\n        return i\n\n    def __setitem__(self, key, value):\n        \"\"\"Set the item's value for a standard field or a flexattr.\"\"\"\n        # Encode unicode paths and read buffers.\n        if key == \"path\":\n            if isinstance(value, str):\n                value = bytestring_path(value)\n            elif isinstance(value, types.BLOB_TYPE):\n                value = bytes(value)\n        elif key == \"album_id\":\n            self._cached_album = None\n\n        changed = super()._setitem(key, value)\n\n        if changed and key in MediaFile.fields():\n            self.mtime = 0  # Reset mtime on dirty.\n\n    def __getitem__(self, key):\n        \"\"\"Get the value for a field, falling back to the album if\n        necessary.\n\n        Raise a KeyError if the field is not available.\n        \"\"\"\n        try:\n            return super().__getitem__(key)\n        except KeyError:\n            if self._cached_album:\n                return self._cached_album[key]\n            raise\n\n    def __repr__(self):\n        # This must not use `with_album=True`, because that might access\n        # the database. When debugging, that is not guaranteed to succeed, and\n        # can even deadlock due to the database lock.\n        return (\n            f\"{type(self).__name__}\"\n            f\"({', '.join(f'{k}={self[k]!r}' for k in self.keys(with_album=False))})\"\n        )\n\n    def keys(self, computed=False, with_album=True):\n        \"\"\"Get a list of available field names.\n\n        `with_album` controls whether the album's fields are included.\n        \"\"\"\n        keys = super().keys(computed=computed)\n        if with_album and self._cached_album:\n            keys = set(keys)\n            keys.update(self._cached_album.keys(computed=computed))\n            keys = list(keys)\n        return keys\n\n    def get(self, key, default=None, with_album=True):\n        \"\"\"Get the value for a given key or `default` if it does not\n        exist.\n\n        Set `with_album` to false to skip album fallback.\n        \"\"\"\n        try:\n            return self._get(key, default, raise_=with_album)\n        except KeyError:\n            if self._cached_album:\n                return self._cached_album.get(key, default)\n            return default\n\n    def update(self, values):\n        \"\"\"Set all key/value pairs in the mapping.\n\n        If mtime is specified, it is not reset (as it might otherwise be).\n        \"\"\"\n        super().update(values)\n        if self.mtime == 0 and \"mtime\" in values:\n            self.mtime = values[\"mtime\"]\n\n    def clear(self):\n        \"\"\"Set all key/value pairs to None.\"\"\"\n        for key in self._media_tag_fields:\n            setattr(self, key, None)\n\n    def get_album(self):\n        \"\"\"Get the Album object that this item belongs to, if any, or\n        None if the item is a singleton or is not associated with a\n        library.\n        \"\"\"\n        if not self._db:\n            return None\n        return self._db.get_album(self)\n\n    # Interaction with file metadata.\n\n    def read(self, read_path=None):\n        \"\"\"Read the metadata from the associated file.\n\n        If `read_path` is specified, read metadata from that file\n        instead. Update all the properties in `_media_fields`\n        from the media file.\n\n        Raise a `ReadError` if the file could not be read.\n        \"\"\"\n        if read_path is None:\n            read_path = self.path\n        else:\n            read_path = normpath(read_path)\n        try:\n            mediafile = MediaFile(syspath(read_path))\n        except UnreadableFileError as exc:\n            raise ReadError(read_path, exc)\n\n        for key in self._media_fields:\n            value = getattr(mediafile, key)\n            if isinstance(value, int):\n                if value.bit_length() > 63:\n                    value = 0\n            self[key] = value\n\n        # Database's mtime should now reflect the on-disk value.\n        if read_path == self.path:\n            self.mtime = self.current_mtime()\n\n        self.path = read_path\n\n    def write(self, path=None, tags=None, id3v23=None):\n        \"\"\"Write the item's metadata to a media file.\n\n        All fields in `_media_fields` are written to disk according to\n        the values on this object.\n\n        `path` is the path of the mediafile to write the data to. It\n        defaults to the item's path.\n\n        `tags` is a dictionary of additional metadata the should be\n        written to the file. (These tags need not be in `_media_fields`.)\n\n        `id3v23` will override the global `id3v23` config option if it is\n        set to something other than `None`.\n\n        Can raise either a `ReadError` or a `WriteError`.\n        \"\"\"\n        if path is None:\n            path = self.path\n        else:\n            path = normpath(path)\n\n        if id3v23 is None:\n            id3v23 = beets.config[\"id3v23\"].get(bool)\n\n        # Get the data to write to the file.\n        item_tags = dict(self)\n        item_tags = {\n            k: v for k, v in item_tags.items() if k in self._media_fields\n        }  # Only write media fields.\n        if tags is not None:\n            item_tags.update(tags)\n        plugins.send(\"write\", item=self, path=path, tags=item_tags)\n\n        # Open the file.\n        try:\n            mediafile = MediaFile(syspath(path), id3v23=id3v23)\n        except UnreadableFileError as exc:\n            raise ReadError(path, exc)\n\n        # Write the tags to the file.\n        mediafile.update(item_tags)\n        try:\n            mediafile.save()\n        except UnreadableFileError as exc:\n            raise WriteError(self.path, exc)\n\n        # The file has a new mtime.\n        if path == self.path:\n            self.mtime = self.current_mtime()\n        plugins.send(\"after_write\", item=self, path=path)\n\n    def try_write(self, *args, **kwargs):\n        \"\"\"Call `write()` but catch and log `FileOperationError`\n        exceptions.\n\n        Return `False` an exception was caught and `True` otherwise.\n        \"\"\"\n        try:\n            self.write(*args, **kwargs)\n            return True\n        except FileOperationError as exc:\n            log.error(\"{}\", exc)\n            return False\n\n    def try_sync(self, write, move, with_album=True):\n        \"\"\"Synchronize the item with the database and, possibly, update its\n        tags on disk and its path (by moving the file).\n\n        `write` indicates whether to write new tags into the file. Similarly,\n        `move` controls whether the path should be updated. In the\n        latter case, files are *only* moved when they are inside their\n        library's directory (if any).\n\n        Similar to calling :meth:`write`, :meth:`move`, and :meth:`store`\n        (conditionally).\n        \"\"\"\n        if write:\n            self.try_write()\n        if move:\n            # Check whether this file is inside the library directory.\n            if self._db and self._db.directory in util.ancestry(self.path):\n                log.debug(\"moving {.filepath} to synchronize path\", self)\n                self.move(with_album=with_album)\n        self.store()\n\n    # Files themselves.\n\n    def move_file(self, dest, operation=MoveOperation.MOVE):\n        \"\"\"Move, copy, link or hardlink the item depending on `operation`,\n        updating the path value if the move succeeds.\n\n        If a file exists at `dest`, then it is slightly modified to be unique.\n\n        `operation` should be an instance of `util.MoveOperation`.\n        \"\"\"\n        if not util.samefile(self.path, dest):\n            dest = util.unique_path(dest)\n        if operation == MoveOperation.MOVE:\n            plugins.send(\n                \"before_item_moved\",\n                item=self,\n                source=self.path,\n                destination=dest,\n            )\n            util.move(self.path, dest)\n            plugins.send(\n                \"item_moved\", item=self, source=self.path, destination=dest\n            )\n        elif operation == MoveOperation.COPY:\n            util.copy(self.path, dest)\n            plugins.send(\n                \"item_copied\", item=self, source=self.path, destination=dest\n            )\n        elif operation == MoveOperation.LINK:\n            util.link(self.path, dest)\n            plugins.send(\n                \"item_linked\", item=self, source=self.path, destination=dest\n            )\n        elif operation == MoveOperation.HARDLINK:\n            util.hardlink(self.path, dest)\n            plugins.send(\n                \"item_hardlinked\", item=self, source=self.path, destination=dest\n            )\n        elif operation == MoveOperation.REFLINK:\n            util.reflink(self.path, dest, fallback=False)\n            plugins.send(\n                \"item_reflinked\", item=self, source=self.path, destination=dest\n            )\n        elif operation == MoveOperation.REFLINK_AUTO:\n            util.reflink(self.path, dest, fallback=True)\n            plugins.send(\n                \"item_reflinked\", item=self, source=self.path, destination=dest\n            )\n        else:\n            assert False, \"unknown MoveOperation\"\n\n        # Either copying or moving succeeded, so update the stored path.\n        self.path = dest\n\n    def current_mtime(self):\n        \"\"\"Return the current mtime of the file, rounded to the nearest\n        integer.\n        \"\"\"\n        return int(os.path.getmtime(syspath(self.path)))\n\n    def try_filesize(self):\n        \"\"\"Get the size of the underlying file in bytes.\n\n        If the file is missing, return 0 (and log a warning).\n        \"\"\"\n        try:\n            return os.path.getsize(syspath(self.path))\n        except (OSError, Exception) as exc:\n            log.warning(\"could not get filesize: {}\", exc)\n            return 0\n\n    # Model methods.\n\n    def remove(self, delete=False, with_album=True):\n        \"\"\"Remove the item.\n\n        If `delete`, then the associated file is removed from disk.\n\n        If `with_album`, then the item's album (if any) is removed\n        if the item was the last in the album.\n        \"\"\"\n        super().remove()\n\n        # Remove the album if it is empty.\n        if with_album:\n            album = self.get_album()\n            if album and not album.items():\n                album.remove(delete, False)\n\n        # Send a 'item_removed' signal to plugins\n        plugins.send(\"item_removed\", item=self)\n\n        # Delete the associated file.\n        if delete:\n            util.remove(self.path)\n            util.prune_dirs(os.path.dirname(self.path), self._db.directory)\n\n        self._db._memotable = {}\n\n    def move(\n        self,\n        operation=MoveOperation.MOVE,\n        basedir=None,\n        with_album=True,\n        store=True,\n    ):\n        \"\"\"Move the item to its designated location within the library\n        directory (provided by destination()).\n\n        Subdirectories are created as needed. If the operation succeeds,\n        the item's path field is updated to reflect the new location.\n\n        Instead of moving the item it can also be copied, linked or hardlinked\n        depending on `operation` which should be an instance of\n        `util.MoveOperation`.\n\n        `basedir` overrides the library base directory for the destination.\n\n        If the item is in an album and `with_album` is `True`, the album is\n        given an opportunity to move its art.\n\n        By default, the item is stored to the database if it is in the\n        database, so any dirty fields prior to the move() call will be written\n        as a side effect.\n        If `store` is `False` however, the item won't be stored and it will\n        have to be manually stored after invoking this method.\n        \"\"\"\n        dest = self.destination(basedir=basedir)\n\n        # Create necessary ancestry for the move.\n        util.mkdirall(dest)\n\n        # Perform the move and store the change.\n        old_path = self.path\n        self.move_file(dest, operation)\n        if store:\n            self.store()\n\n        # If this item is in an album, move its art.\n        if with_album:\n            album = self.get_album()\n            if album:\n                album.move_art(operation)\n                if store:\n                    album.store()\n\n        # Prune vacated directory.\n        if operation == MoveOperation.MOVE:\n            util.prune_dirs(os.path.dirname(old_path), self._db.directory)\n\n    # Templating.\n\n    def destination(\n        self,\n        relative_to_libdir=False,\n        basedir=None,\n        path_formats=None,\n    ) -> bytes:\n        \"\"\"Return the path in the library directory designated for the item\n        (i.e., where the file ought to be).\n\n        The path is returned as a bytestring. ``basedir`` can override the\n        library's base directory for the destination. If ``relative_to_libdir``\n        is true, returns just the fragment of the path underneath the library\n        base directory.\n        \"\"\"\n        basedir = basedir or self.db.directory\n        path_formats = path_formats or self.db.path_formats\n\n        # Use a path format based on a query, falling back on the\n        # default.\n        for query, path_format in path_formats:\n            if query == PF_KEY_DEFAULT:\n                continue\n            query, _ = parse_query_string(query, type(self))\n            if query.match(self):\n                # The query matches the item! Use the corresponding path\n                # format.\n                break\n        else:\n            # No query matched; fall back to default.\n            for query, path_format in path_formats:\n                if query == PF_KEY_DEFAULT:\n                    break\n            else:\n                assert False, \"no default path format\"\n        if isinstance(path_format, Template):\n            subpath_tmpl = path_format\n        else:\n            subpath_tmpl = template(path_format)\n\n        # Evaluate the selected template.\n        subpath = self.evaluate_template(subpath_tmpl, True)\n\n        # Prepare path for output: normalize Unicode characters.\n        if sys.platform == \"darwin\":\n            subpath = unicodedata.normalize(\"NFD\", subpath)\n        else:\n            subpath = unicodedata.normalize(\"NFC\", subpath)\n\n        if beets.config[\"asciify_paths\"]:\n            subpath = util.asciify_path(\n                subpath, beets.config[\"path_sep_replace\"].as_str()\n            )\n\n        lib_path_str, fallback = util.legalize_path(\n            subpath, self.db.replacements, self.filepath.suffix\n        )\n        if fallback:\n            # Print an error message if legalization fell back to\n            # default replacements because of the maximum length.\n            log.warning(\n                \"Fell back to default replacements when naming \"\n                \"file {}. Configure replacements to avoid lengthening \"\n                \"the filename.\",\n                subpath,\n            )\n        lib_path_bytes = util.bytestring_path(lib_path_str)\n\n        if relative_to_libdir:\n            return lib_path_bytes\n\n        return normpath(os.path.join(basedir, lib_path_bytes))\n\n\ndef _int_arg(s):\n    \"\"\"Convert a string argument to an integer for use in a template\n    function.\n\n    May raise a ValueError.\n    \"\"\"\n    return int(s.strip())\n\n\nclass DefaultTemplateFunctions:\n    \"\"\"A container class for the default functions provided to path\n    templates.\n\n    These functions are contained in an object to provide\n    additional context to the functions -- specifically, the Item being\n    evaluated.\n    \"\"\"\n\n    _prefix = \"tmpl_\"\n\n    @cached_classproperty\n    def _func_names(cls) -> list[str]:\n        \"\"\"Names of tmpl_* functions in this class.\"\"\"\n        return [s for s in dir(cls) if s.startswith(cls._prefix)]\n\n    def __init__(self, item=None, lib=None):\n        \"\"\"Parametrize the functions.\n\n        If `item` or `lib` is None, then some functions (namely, ``aunique``)\n        will always evaluate to the empty string.\n        \"\"\"\n        self.item = item\n        self.lib = lib\n\n    def functions(self):\n        \"\"\"Return a dictionary containing the functions defined in this\n        object.\n\n        The keys are function names (as exposed in templates)\n        and the values are Python functions.\n        \"\"\"\n        out = {}\n        for key in self._func_names:\n            out[key[len(self._prefix) :]] = getattr(self, key)\n        return out\n\n    @staticmethod\n    def tmpl_lower(s):\n        \"\"\"Convert a string to lower case.\"\"\"\n        return s.lower()\n\n    @staticmethod\n    def tmpl_upper(s):\n        \"\"\"Convert a string to upper case.\"\"\"\n        return s.upper()\n\n    @staticmethod\n    def tmpl_capitalize(s):\n        \"\"\"Converts to a capitalized string.\"\"\"\n        return s.capitalize()\n\n    @staticmethod\n    def tmpl_title(s):\n        \"\"\"Convert a string to title case.\"\"\"\n        return string.capwords(s)\n\n    @staticmethod\n    def tmpl_left(s, chars):\n        \"\"\"Get the leftmost characters of a string.\"\"\"\n        return s[0 : _int_arg(chars)]\n\n    @staticmethod\n    def tmpl_right(s, chars):\n        \"\"\"Get the rightmost characters of a string.\"\"\"\n        return s[-_int_arg(chars) :]\n\n    @staticmethod\n    def tmpl_if(condition, trueval, falseval=\"\"):\n        \"\"\"If ``condition`` is nonempty and nonzero, emit ``trueval``;\n        otherwise, emit ``falseval`` (if provided).\n        \"\"\"\n        try:\n            int_condition = _int_arg(condition)\n        except ValueError:\n            if condition.lower() == \"false\":\n                return falseval\n        else:\n            condition = int_condition\n\n        if condition:\n            return trueval\n        else:\n            return falseval\n\n    @staticmethod\n    def tmpl_asciify(s):\n        \"\"\"Translate non-ASCII characters to their ASCII equivalents.\"\"\"\n        return util.asciify_path(s, beets.config[\"path_sep_replace\"].as_str())\n\n    @staticmethod\n    def tmpl_time(s, fmt):\n        \"\"\"Format a time value using `strftime`.\"\"\"\n        cur_fmt = beets.config[\"time_format\"].as_str()\n        return time.strftime(fmt, time.strptime(s, cur_fmt))\n\n    def tmpl_aunique(self, keys=None, disam=None, bracket=None):\n        \"\"\"Generate a string that is guaranteed to be unique among all\n        albums in the library who share the same set of keys.\n\n        A fields from \"disam\" is used in the string if one is sufficient to\n        disambiguate the albums. Otherwise, a fallback opaque value is\n        used. Both \"keys\" and \"disam\" should be given as\n        whitespace-separated lists of field names, while \"bracket\" is a\n        pair of characters to be used as brackets surrounding the\n        disambiguator or empty to have no brackets.\n        \"\"\"\n        # Fast paths: no album, no item or library, or memoized value.\n        if not self.item or not self.lib:\n            return \"\"\n\n        if isinstance(self.item, Item):\n            album_id = self.item.album_id\n        elif isinstance(self.item, Album):\n            album_id = self.item.id\n\n        if album_id is None:\n            return \"\"\n\n        memokey = self._tmpl_unique_memokey(\"aunique\", keys, disam, album_id)\n        memoval = self.lib._memotable.get(memokey)\n        if memoval is not None:\n            return memoval\n\n        album = self.lib.get_album(album_id)\n\n        return self._tmpl_unique(\n            \"aunique\",\n            keys,\n            disam,\n            bracket,\n            album_id,\n            album,\n            album.item_keys,\n            # Do nothing for singletons.\n            lambda a: a is None,\n        )\n\n    def tmpl_sunique(self, keys=None, disam=None, bracket=None):\n        \"\"\"Generate a string that is guaranteed to be unique among all\n        singletons in the library who share the same set of keys.\n\n        A fields from \"disam\" is used in the string if one is sufficient to\n        disambiguate the albums. Otherwise, a fallback opaque value is\n        used. Both \"keys\" and \"disam\" should be given as\n        whitespace-separated lists of field names, while \"bracket\" is a\n        pair of characters to be used as brackets surrounding the\n        disambiguator or empty to have no brackets.\n        \"\"\"\n        # Fast paths: no album, no item or library, or memoized value.\n        if not self.item or not self.lib:\n            return \"\"\n\n        if isinstance(self.item, Item):\n            item_id = self.item.id\n        else:\n            raise NotImplementedError(\"sunique is only implemented for items\")\n\n        if item_id is None:\n            return \"\"\n\n        return self._tmpl_unique(\n            \"sunique\",\n            keys,\n            disam,\n            bracket,\n            item_id,\n            self.item,\n            Item.all_keys(),\n            # Do nothing for non singletons.\n            lambda i: i.album_id is not None,\n        )\n\n    def _tmpl_unique_memokey(self, name, keys, disam, item_id):\n        \"\"\"Get the memokey for the unique template named \"name\" for the\n        specific parameters.\n        \"\"\"\n        return (name, keys, disam, item_id)\n\n    def _tmpl_unique(\n        self,\n        name,\n        keys,\n        disam,\n        bracket,\n        item_id,\n        db_item,\n        item_keys,\n        skip_item,\n    ):\n        \"\"\"Generate a string that is guaranteed to be unique among all items of\n        the same type as \"db_item\" who share the same set of keys.\n\n        A field from \"disam\" is used in the string if one is sufficient to\n        disambiguate the items. Otherwise, a fallback opaque value is\n        used. Both \"keys\" and \"disam\" should be given as\n        whitespace-separated lists of field names, while \"bracket\" is a\n        pair of characters to be used as brackets surrounding the\n        disambiguator or empty to have no brackets.\n\n        \"name\" is the name of the templates. It is also the name of the\n        configuration section where the default values of the parameters\n        are stored.\n\n        \"skip_item\" is a function that must return True when the template\n        should return an empty string.\n\n        \"initial_subqueries\" is a list of subqueries that should be included\n        in the query to find the ambiguous items.\n        \"\"\"\n        memokey = self._tmpl_unique_memokey(name, keys, disam, item_id)\n        memoval = self.lib._memotable.get(memokey)\n        if memoval is not None:\n            return memoval\n\n        if skip_item(db_item):\n            self.lib._memotable[memokey] = \"\"\n            return \"\"\n\n        keys = keys or beets.config[name][\"keys\"].as_str()\n        disam = disam or beets.config[name][\"disambiguators\"].as_str()\n        if bracket is None:\n            bracket = beets.config[name][\"bracket\"].as_str()\n        keys = keys.split()\n        disam = disam.split()\n\n        # Assign a left and right bracket or leave blank if argument is empty.\n        if len(bracket) == 2:\n            bracket_l = bracket[0]\n            bracket_r = bracket[1]\n        else:\n            bracket_l = \"\"\n            bracket_r = \"\"\n\n        # Find matching items to disambiguate with.\n        query = db_item.duplicates_query(keys)\n        ambigous_items = (\n            self.lib.items(query)\n            if isinstance(db_item, Item)\n            else self.lib.albums(query)\n        )\n\n        # If there's only one item to matching these details, then do\n        # nothing.\n        if len(ambigous_items) == 1:\n            self.lib._memotable[memokey] = \"\"\n            return \"\"\n\n        # Find the first disambiguator that distinguishes the items.\n        for disambiguator in disam:\n            # Get the value for each item for the current field.\n            disam_values = {s.get(disambiguator, \"\") for s in ambigous_items}\n\n            # If the set of unique values is equal to the number of\n            # items in the disambiguation set, we're done -- this is\n            # sufficient disambiguation.\n            if len(disam_values) == len(ambigous_items):\n                break\n        else:\n            # No disambiguator distinguished all fields.\n            res = f\" {bracket_l}{item_id}{bracket_r}\"\n            self.lib._memotable[memokey] = res\n            return res\n\n        # Flatten disambiguation value into a string.\n        disam_value = db_item.formatted(for_path=True).get(disambiguator)\n\n        # Return empty string if disambiguator is empty.\n        if disam_value:\n            res = f\" {bracket_l}{disam_value}{bracket_r}\"\n        else:\n            res = \"\"\n\n        self.lib._memotable[memokey] = res\n        return res\n\n    @staticmethod\n    def tmpl_first(s, count=1, skip=0, sep=\"; \", join_str=\"; \"):\n        \"\"\"Get the item(s) from x to y in a string separated by something\n        and join then with something.\n\n        Args:\n            s: the string\n            count: The number of items included\n            skip: The number of items skipped\n            sep: the separator\n            join_str: the string which will join the items\n        \"\"\"\n        skip = int(skip)\n        count = skip + int(count)\n        return join_str.join(s.split(sep)[skip:count])\n\n    def tmpl_ifdef(self, field, trueval=\"\", falseval=\"\"):\n        \"\"\"If field exists return trueval or the field (default)\n        otherwise, emit return falseval (if provided).\n\n        Args:\n            field: The name of the field\n            trueval: The string if the condition is true\n            falseval: The string if the condition is false\n\n        Returns:\n            The string, based on condition.\n        \"\"\"\n        if field in self.item:\n            return trueval if trueval else self.item.formatted().get(field)\n        else:\n            return falseval\n"
  },
  {
    "path": "beets/library/queries.py",
    "content": "from __future__ import annotations\n\nimport shlex\n\nimport beets\nfrom beets import dbcore, logging, plugins\n\nlog = logging.getLogger(\"beets\")\n\n\n# Special path format key.\nPF_KEY_DEFAULT = \"default\"\n\n# Query construction helpers.\n\n\ndef parse_query_parts(parts, model_cls):\n    \"\"\"Given a beets query string as a list of components, return the\n    `Query` and `Sort` they represent.\n\n    Like `dbcore.parse_sorted_query`, with beets query prefixes and\n    ensuring that implicit path queries are made explicit with 'path::<query>'\n    \"\"\"\n    # Get query types and their prefix characters.\n    prefixes = {\n        \":\": dbcore.query.RegexpQuery,\n        \"=~\": dbcore.query.StringQuery,\n        \"=\": dbcore.query.MatchQuery,\n    }\n    prefixes.update(plugins.queries())\n\n    # Special-case path-like queries, which are non-field queries\n    # containing path separators (/).\n    parts = [\n        f\"path:{s}\" if dbcore.query.PathQuery.is_path_query(s) else s\n        for s in parts\n    ]\n\n    case_insensitive = beets.config[\"sort_case_insensitive\"].get(bool)\n\n    query, sort = dbcore.parse_sorted_query(\n        model_cls, parts, prefixes, case_insensitive\n    )\n    log.debug(\"Parsed query: {!r}\", query)\n    log.debug(\"Parsed sort: {!r}\", sort)\n    return query, sort\n\n\ndef parse_query_string(s, model_cls):\n    \"\"\"Given a beets query string, return the `Query` and `Sort` they\n    represent.\n\n    The string is split into components using shell-like syntax.\n    \"\"\"\n    message = f\"Query is not unicode: {s!r}\"\n    assert isinstance(s, str), message\n    try:\n        parts = shlex.split(s)\n    except ValueError as exc:\n        raise dbcore.InvalidQueryError(s, exc)\n    return parse_query_parts(parts, model_cls)\n"
  },
  {
    "path": "beets/logging.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"A drop-in replacement for the standard-library `logging` module.\n\nProvides everything the \"logging\" module does. In addition, beets' logger\n(as obtained by `getLogger(name)`) supports thread-local levels, and messages\nuse {}-style formatting and can interpolate keywords arguments to the logging\ncalls (`debug`, `info`, etc).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nimport threading\nfrom copy import copy\nfrom logging import (\n    DEBUG,\n    INFO,\n    NOTSET,\n    WARNING,\n    FileHandler,\n    Filter,\n    Handler,\n    Logger,\n    NullHandler,\n    StreamHandler,\n)\nfrom typing import TYPE_CHECKING, Any, TypeVar, overload\n\nif TYPE_CHECKING:\n    from collections.abc import Mapping\n    from logging import RootLogger\n    from types import TracebackType\n\n    T = TypeVar(\"T\")\n\n    # see https://github.com/python/typeshed/blob/main/stdlib/logging/__init__.pyi\n    _SysExcInfoType = (\n        tuple[type[BaseException], BaseException, TracebackType | None]\n        | tuple[None, None, None]\n    )\n    _ExcInfoType = _SysExcInfoType | BaseException | bool | None\n    _ArgsType = tuple[object, ...] | Mapping[str, object]\n\n\n__all__ = [\n    \"DEBUG\",\n    \"INFO\",\n    \"NOTSET\",\n    \"WARNING\",\n    \"FileHandler\",\n    \"Filter\",\n    \"Handler\",\n    \"Logger\",\n    \"NullHandler\",\n    \"StreamHandler\",\n    \"getLogger\",\n]\n\n# Regular expression to match:\n# - C0 control characters (0x00-0x1F) except useful whitespace (\\t, \\n, \\r)\n# - DEL control character (0x7f)\n# - C1 control characters (0x80-0x9F)\n# Used to sanitize log messages that could disrupt terminal output\n_CONTROL_CHAR_REGEX = re.compile(r\"[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f\\x80-\\x9f]\")\n_UNICODE_REPLACEMENT_CHARACTER = \"\\ufffd\"\n\n\ndef _logsafe(val: T) -> str | T:\n    \"\"\"Coerce `bytes` to `str` to avoid crashes solely due to logging.\n\n    This is particularly relevant for bytestring paths. Much of our code\n    explicitly uses `displayable_path` for them, but better be safe and prevent\n    any crashes that are solely due to log formatting.\n    \"\"\"\n    # Bytestring: Needs decoding to be safe for substitution in format strings.\n    if isinstance(val, bytes):\n        # Blindly convert with UTF-8. Eventually, it would be nice to\n        # (a) only do this for paths, if they can be given a distinct\n        # type, and (b) warn the developer if they do this for other\n        # bytestrings.\n        return val.decode(\"utf-8\", \"replace\")\n    if isinstance(val, str):\n        # Sanitize log messages by replacing control characters that can disrupt\n        # terminals.\n        return _CONTROL_CHAR_REGEX.sub(_UNICODE_REPLACEMENT_CHARACTER, val)\n\n    # Other objects are used as-is so field access, etc., still works in\n    # the format string. Relies on a working __str__ implementation.\n    return val\n\n\nclass StrFormatLogger(Logger):\n    \"\"\"A version of `Logger` that uses `str.format`-style formatting\n    instead of %-style formatting and supports keyword arguments.\n\n    We cannot easily get rid of this even in the Python 3 era: This custom\n    formatting supports substitution from `kwargs` into the message, which the\n    default `logging.Logger._log()` implementation does not.\n\n    Remark by @sampsyo: https://stackoverflow.com/a/24683360 might be a way to\n    achieve this with less code.\n    \"\"\"\n\n    class _LogMessage:\n        def __init__(\n            self,\n            msg: str,\n            args: _ArgsType,\n            kwargs: dict[str, Any],\n        ):\n            self.msg = msg\n            self.args = args\n            self.kwargs = kwargs\n\n        def __str__(self):\n            args = [_logsafe(a) for a in self.args]\n            kwargs = {k: _logsafe(v) for (k, v) in self.kwargs.items()}\n            return self.msg.format(*args, **kwargs)\n\n    def _log(\n        self,\n        level: int,\n        msg: object,\n        args: _ArgsType,\n        exc_info: _ExcInfoType = None,\n        extra: Mapping[str, Any] | None = None,\n        stack_info: bool = False,\n        stacklevel: int = 1,\n        **kwargs,\n    ):\n        \"\"\"Log msg.format(*args, **kwargs)\"\"\"\n\n        if isinstance(msg, str):\n            msg = self._LogMessage(msg, args, kwargs)\n\n        return super()._log(\n            level,\n            msg,\n            (),\n            exc_info=exc_info,\n            extra=extra,\n            stack_info=stack_info,\n            stacklevel=stacklevel,\n        )\n\n\nclass ThreadLocalLevelLogger(Logger):\n    \"\"\"A version of `Logger` whose level is thread-local instead of shared.\"\"\"\n\n    def __init__(self, name, level=NOTSET):\n        self._thread_level = threading.local()\n        self.default_level = NOTSET\n        super().__init__(name, level)\n\n    @property\n    def level(self):\n        try:\n            return self._thread_level.level\n        except AttributeError:\n            self._thread_level.level = self.default_level\n            return self.level\n\n    @level.setter\n    def level(self, value):\n        self._thread_level.level = value\n\n    def set_global_level(self, level):\n        \"\"\"Set the level on the current thread + the default value for all\n        threads.\n        \"\"\"\n        self.default_level = level\n        self.setLevel(level)\n\n\nclass BeetsLogger(ThreadLocalLevelLogger, StrFormatLogger):\n    \"\"\"The logger class used by beets.\"\"\"\n\n    def extra_debug(self, msg: str, *args: Any, **kwargs: Any) -> None:\n        \"\"\"Log a message at DEBUG level only when verbosity level is >= 3.\n\n        Intended for high-verbosity tuning/diagnostic messages that would be too\n        noisy at normal debug level.\n        \"\"\"\n        # Lazy import to avoid circular dependency (beets.__init__ -> beets.logging)\n        from beets import config\n\n        if config[\"verbose\"].as_number() >= 3:\n            self._log(DEBUG, msg, args, **kwargs)\n\n\nmy_manager = copy(Logger.manager)\nmy_manager.loggerClass = BeetsLogger\n\n\n@overload\ndef getLogger(name: str) -> BeetsLogger: ...\n@overload\ndef getLogger(name: None = ...) -> RootLogger: ...\ndef getLogger(name=None) -> BeetsLogger | RootLogger:  # noqa: N802\n    if name:\n        return my_manager.getLogger(name)  # type: ignore[return-value]\n    else:\n        return Logger.root\n"
  },
  {
    "path": "beets/mediafile.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nimport mediafile\n\nfrom .util.deprecation import deprecate_for_maintainers\n\ndeprecate_for_maintainers(\"'beets.mediafile'\", \"'mediafile'\", stacklevel=2)\n\n# Import everything from the mediafile module into this module.\nfor key, value in mediafile.__dict__.items():\n    if key not in [\"__name__\"]:\n        globals()[key] = value\n\n# Cleanup namespace.\ndel key, value, mediafile\n"
  },
  {
    "path": "beets/metadata_plugins.py",
    "content": "\"\"\"Metadata source plugin interface.\n\nThis allows beets to lookup metadata from various sources. We define\na common interface for all metadata sources which need to be\nimplemented as plugins.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport abc\nimport re\nfrom contextlib import contextmanager\nfrom functools import cache, cached_property, wraps\nfrom typing import (\n    TYPE_CHECKING,\n    Generic,\n    Literal,\n    NamedTuple,\n    TypedDict,\n    TypeVar,\n)\n\nimport unidecode\nfrom confuse import NotFoundError\n\nfrom beets import config, logging\nfrom beets.util import cached_classproperty\nfrom beets.util.id_extractors import extract_release_id\n\nfrom .plugins import BeetsPlugin, find_plugins, notify_info_yielded, send\n\nRet = TypeVar(\"Ret\")\nQueryType = Literal[\"album\", \"track\"]\n\nif TYPE_CHECKING:\n    from collections.abc import Callable, Iterable, Iterator, Sequence\n\n    from .autotag.hooks import AlbumInfo, Item, TrackInfo\n\n# Global logger.\nlog = logging.getLogger(\"beets\")\n\n\n@cache\ndef find_metadata_source_plugins() -> list[MetadataSourcePlugin]:\n    \"\"\"Return a list of all loaded metadata source plugins.\"\"\"\n    # TODO: Make this an isinstance(MetadataSourcePlugin, ...) check in v3.0.0\n    # This should also allow us to remove the type: ignore comments below.\n    return [p for p in find_plugins() if hasattr(p, \"data_source\")]  # type: ignore[misc]\n\n\n@cache\ndef get_metadata_source(name: str) -> MetadataSourcePlugin | None:\n    \"\"\"Get metadata source plugin by name.\"\"\"\n    name = name.lower()\n    plugins = find_metadata_source_plugins()\n    return next((p for p in plugins if p.data_source.lower() == name), None)\n\n\n@contextmanager\ndef maybe_handle_plugin_error(plugin: MetadataSourcePlugin, method_name: str):\n    \"\"\"Safely call a plugin method, catching and logging exceptions.\"\"\"\n    if config[\"raise_on_error\"]:\n        yield\n    else:\n        try:\n            yield\n        except Exception as e:\n            log.error(\n                \"Error in '{}.{}': {}\", plugin.data_source, method_name, e\n            )\n            log.debug(\"Exception details:\", exc_info=True)\n\n\ndef _yield_from_plugins(\n    func: Callable[..., Iterable[Ret]],\n) -> Callable[..., Iterator[Ret]]:\n    method_name = func.__name__\n\n    @wraps(func)\n    def wrapper(*args, **kwargs) -> Iterator[Ret]:\n        for plugin in find_metadata_source_plugins():\n            method = getattr(plugin, method_name)\n            with maybe_handle_plugin_error(plugin, method_name):\n                yield from filter(None, method(*args, **kwargs))\n\n    return wrapper\n\n\n@notify_info_yielded(\"albuminfo_received\")\n@_yield_from_plugins\ndef candidates(*args, **kwargs) -> Iterator[AlbumInfo]:\n    yield from ()\n\n\n@notify_info_yielded(\"trackinfo_received\")\n@_yield_from_plugins\ndef item_candidates(*args, **kwargs) -> Iterator[TrackInfo]:\n    yield from ()\n\n\n@notify_info_yielded(\"albuminfo_received\")\n@_yield_from_plugins\ndef albums_for_ids(*args, **kwargs) -> Iterator[AlbumInfo]:\n    yield from ()\n\n\n@notify_info_yielded(\"trackinfo_received\")\n@_yield_from_plugins\ndef tracks_for_ids(*args, **kwargs) -> Iterator[TrackInfo]:\n    yield from ()\n\n\ndef album_for_id(_id: str, data_source: str) -> AlbumInfo | None:\n    \"\"\"Get AlbumInfo object for the given ID and data source.\"\"\"\n    if plugin := get_metadata_source(data_source):\n        with maybe_handle_plugin_error(plugin, \"album_for_id\"):\n            if info := plugin.album_for_id(_id):\n                send(\"albuminfo_received\", info=info)\n                return info\n\n    return None\n\n\ndef track_for_id(_id: str, data_source: str) -> TrackInfo | None:\n    \"\"\"Get TrackInfo object for the given ID and data source.\"\"\"\n    if plugin := get_metadata_source(data_source):\n        with maybe_handle_plugin_error(plugin, \"track_for_id\"):\n            if info := plugin.track_for_id(_id):\n                send(\"trackinfo_received\", info=info)\n                return info\n\n    return None\n\n\n@cache\ndef get_penalty(data_source: str | None) -> float:\n    \"\"\"Get the penalty value for the given data source.\"\"\"\n    return next(\n        (\n            p.data_source_mismatch_penalty\n            for p in find_metadata_source_plugins()\n            if p.data_source == data_source\n        ),\n        MetadataSourcePlugin.DEFAULT_DATA_SOURCE_MISMATCH_PENALTY,\n    )\n\n\nclass MetadataSourcePlugin(BeetsPlugin, metaclass=abc.ABCMeta):\n    \"\"\"A plugin that provides metadata from a specific source.\n\n    This base class implements a contract for plugins that provide metadata\n    from a specific source. The plugin must implement the methods to search for albums\n    and tracks, and to retrieve album and track information by ID.\n    \"\"\"\n\n    DEFAULT_DATA_SOURCE_MISMATCH_PENALTY = 0.5\n\n    @cached_classproperty\n    def data_source(cls) -> str:\n        \"\"\"The data source name for this plugin.\n\n        This is inferred from the plugin name.\n        \"\"\"\n        return cls.__name__.replace(\"Plugin\", \"\")  # type: ignore[attr-defined]\n\n    @cached_property\n    def data_source_mismatch_penalty(self) -> float:\n        try:\n            return self.config[\"source_weight\"].as_number()\n        except NotFoundError:\n            return self.config[\"data_source_mismatch_penalty\"].as_number()\n\n    def __init__(self, *args, **kwargs) -> None:\n        super().__init__(*args, **kwargs)\n        self.config.add(\n            {\n                \"search_limit\": 5,\n                \"data_source_mismatch_penalty\": self.DEFAULT_DATA_SOURCE_MISMATCH_PENALTY,  # noqa: E501\n            }\n        )\n\n    @abc.abstractmethod\n    def album_for_id(self, album_id: str) -> AlbumInfo | None:\n        \"\"\"Return :py:class:`AlbumInfo` object or None if no matching release was\n        found.\"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def track_for_id(self, track_id: str) -> TrackInfo | None:\n        \"\"\"Return a :py:class:`TrackInfo` object or None if no matching release was\n        found.\n        \"\"\"\n        raise NotImplementedError\n\n    # ---------------------------------- search ---------------------------------- #\n\n    @abc.abstractmethod\n    def candidates(\n        self,\n        items: Sequence[Item],\n        artist: str,\n        album: str,\n        va_likely: bool,\n    ) -> Iterable[AlbumInfo]:\n        \"\"\"Return :py:class:`AlbumInfo` candidates that match the given album.\n\n        Used in the autotag functionality to search for albums.\n\n        :param items: List of items in the album\n        :param artist: Album artist\n        :param album: Album name\n        :param va_likely: Whether the album is likely to be by various artists\n        \"\"\"\n        raise NotImplementedError\n\n    @abc.abstractmethod\n    def item_candidates(\n        self, item: Item, artist: str, title: str\n    ) -> Iterable[TrackInfo]:\n        \"\"\"Return :py:class:`TrackInfo` candidates that match the given track.\n\n        Used in the autotag functionality to search for tracks.\n\n        :param item: Track item\n        :param artist: Track artist\n        :param title: Track title\n        \"\"\"\n        raise NotImplementedError\n\n    def albums_for_ids(self, ids: Iterable[str]) -> Iterable[AlbumInfo | None]:\n        \"\"\"Batch lookup of album metadata for a list of album IDs.\n\n        Given a list of album identifiers, yields corresponding AlbumInfo objects.\n        Missing albums result in None values in the output iterator.\n        Plugins may implement this for optimized batched lookups instead of\n        single calls to album_for_id.\n        \"\"\"\n\n        return (self.album_for_id(id) for id in ids)\n\n    def tracks_for_ids(self, ids: Iterable[str]) -> Iterable[TrackInfo | None]:\n        \"\"\"Batch lookup of track metadata for a list of track IDs.\n\n        Given a list of track identifiers, yields corresponding TrackInfo objects.\n        Missing tracks result in None values in the output iterator.\n        Plugins may implement this for optimized batched lookups instead of\n        single calls to track_for_id.\n        \"\"\"\n\n        return (self.track_for_id(id) for id in ids)\n\n    def _extract_id(self, url: str) -> str | None:\n        \"\"\"Extract an ID from a URL for this metadata source plugin.\n\n        Uses the plugin's data source name to determine the ID format and\n        extracts the ID from a given URL.\n        \"\"\"\n        return extract_release_id(self.data_source, url)\n\n    @staticmethod\n    def get_artist(\n        artists: Iterable[dict[str | int, str]],\n        id_key: str | int = \"id\",\n        name_key: str | int = \"name\",\n        join_key: str | int | None = None,\n    ) -> tuple[str, str | None]:\n        \"\"\"Returns an artist string (all artists) and an artist_id (the main\n        artist) for a list of artist object dicts.\n\n        For each artist, this function moves articles (such as 'a', 'an', and 'the')\n        to the front. It returns a tuple containing the comma-separated string\n        of all normalized artists and the ``id`` of the main/first artist.\n        Alternatively a keyword can be used to combine artists together into a\n        single string by passing the join_key argument.\n\n        :param artists: Iterable of artist dicts or lists returned by API.\n        :param id_key: Key or index corresponding to the value of ``id`` for\n            the main/first artist. Defaults to 'id'.\n        :param name_key: Key or index corresponding to values of names\n            to concatenate for the artist string (containing all artists).\n            Defaults to 'name'.\n        :param join_key: Key or index corresponding to a field containing a\n            keyword to use for combining artists into a single string, for\n            example \"Feat.\", \"Vs.\", \"And\" or similar. The default is None\n            which keeps the default behaviour (comma-separated).\n        :return: Normalized artist string.\n        \"\"\"\n        artist_id = None\n        artist_string = \"\"\n        artists = list(artists)  # In case a generator was passed.\n        total = len(artists)\n        for idx, artist in enumerate(artists):\n            if not artist_id:\n                artist_id = artist[id_key]\n            name = artist[name_key]\n            # Move articles to the front.\n            name = re.sub(r\"^(.*?), (a|an|the)$\", r\"\\2 \\1\", name, flags=re.I)\n            # Use a join keyword if requested and available.\n            if idx < (total - 1):  # Skip joining on last.\n                if join_key and artist.get(join_key, None):\n                    name += f\" {artist[join_key]} \"\n                else:\n                    name += \", \"\n            artist_string += name\n\n        return artist_string, artist_id\n\n\nclass IDResponse(TypedDict):\n    \"\"\"Response from the API containing an ID.\"\"\"\n\n    id: str\n\n\nclass SearchParams(NamedTuple):\n    \"\"\"Bundle normalized search context passed to provider search hooks.\n\n    Shared search orchestration constructs this value so plugin hooks receive\n    one object describing search intent, query text, and provider filters.\n    \"\"\"\n\n    query_type: QueryType\n    query: str\n    filters: dict[str, str]\n    limit: int\n\n\nR = TypeVar(\"R\", bound=IDResponse)\n\n\nclass SearchApiMetadataSourcePlugin(\n    Generic[R], MetadataSourcePlugin, metaclass=abc.ABCMeta\n):\n    \"\"\"Helper class to implement a metadata source plugin with an API.\n\n    Plugins using this ABC must implement an API search method to\n    retrieve album and track information by ID,\n    i.e. `album_for_id` and `track_for_id`, and a search method to\n    perform a search on the API. The search method should return a list\n    of identifiers for the requested type (album or track).\n    \"\"\"\n\n    def __init__(self, *args, **kwargs) -> None:\n        super().__init__(*args, **kwargs)\n        self.config.add(\n            {\n                \"search_query_ascii\": False,\n            }\n        )\n\n    @abc.abstractmethod\n    def get_search_query_with_filters(\n        self,\n        query_type: QueryType,\n        items: Sequence[Item],\n        artist: str,\n        name: str,\n        va_likely: bool,\n    ) -> tuple[str, dict[str, str]]:\n        \"\"\"Build query text and API filters for a provider search.\n\n        Subclasses can override this hook when their API requires a query format\n        or filter set that differs from the default text-based construction.\n\n        :param query_type: The type of query to perform. Either *album* or *track*\n        :param items: List of items the search is being performed for\n        :param artist: Artist name\n        :param name: Album or track name, depending on ``query_type``\n        :param va_likely: Whether the search is likely to be for various artists\n        :return: Tuple of (``query`` text, ``filters`` dict) to use for the\n            search API call\n        \"\"\"\n\n    @abc.abstractmethod\n    def get_search_response(self, params: SearchParams) -> Sequence[R]:\n        \"\"\"Fetch raw search results for a provider request.\n\n        Implementations should return records containing source IDs so shared\n        candidate resolution can perform ID-based album and track lookups.\n\n        :param params: :py:namedtuple:`~SearchParams` named tuple\n        :return: Sequence of IDResponse dicts containing at least an \"id\" key for each\n        \"\"\"\n\n        raise NotImplementedError\n\n    def _search_api(\n        self, query_type: QueryType, query: str, filters: dict[str, str]\n    ) -> Sequence[R]:\n        \"\"\"Run shared provider search orchestration and return ID-bearing results.\n\n        This path applies optional query normalization and default limits, then\n        delegates API access to provider hooks with consistent logging and\n        failure handling.\n        \"\"\"\n        if self.config[\"search_query_ascii\"].get():\n            query = unidecode.unidecode(query)\n\n        limit = self.config[\"search_limit\"].get(int)\n        params = SearchParams(query_type, query, filters, limit)\n\n        self._log.debug(\"Searching for '{}' with {}\", query, filters)\n        try:\n            response_data = self.get_search_response(params)\n        except Exception as e:\n            if config[\"raise_on_error\"].get(bool):\n                raise\n            self._log.error(\n                \"Error searching {.data_source}: {}\", self, e, exc_info=True\n            )\n            return ()\n\n        self._log.debug(\"Found {} result(s)\", len(response_data))\n        return response_data\n\n    def _get_candidates(\n        self, query_type: QueryType, *args, **kwargs\n    ) -> Sequence[R]:\n        \"\"\"Resolve query hooks and execute one provider search request.\"\"\"\n\n        return self._search_api(\n            query_type,\n            *self.get_search_query_with_filters(query_type, *args, **kwargs),\n        )\n\n    def candidates(\n        self,\n        items: Sequence[Item],\n        artist: str,\n        album: str,\n        va_likely: bool,\n    ) -> Iterable[AlbumInfo]:\n        results = self._get_candidates(\"album\", items, artist, album, va_likely)\n        return filter(None, self.albums_for_ids(r[\"id\"] for r in results))\n\n    def item_candidates(\n        self, item: Item, artist: str, title: str\n    ) -> Iterable[TrackInfo]:\n        results = self._get_candidates(\"track\", [item], artist, title, False)\n        return filter(None, self.tracks_for_ids(r[\"id\"] for r in results))\n"
  },
  {
    "path": "beets/plugins.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Support for beets plugins.\"\"\"\n\nfrom __future__ import annotations\n\nimport abc\nimport inspect\nimport re\nimport sys\nfrom collections import defaultdict\nfrom functools import cached_property, wraps\nfrom importlib import import_module\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeVar\n\nimport mediafile\nfrom typing_extensions import ParamSpec\n\nimport beets\nfrom beets import logging\nfrom beets.util import unique_list\nfrom beets.util.deprecation import deprecate_for_maintainers, deprecate_for_user\n\nif TYPE_CHECKING:\n    from collections.abc import Callable, Iterable, Iterator, Sequence\n\n    from confuse import Subview\n\n    from beets.dbcore import Query\n    from beets.dbcore.db import FieldQueryType\n    from beets.dbcore.types import Type\n    from beets.importer import ImportSession, ImportTask\n    from beets.library import Album, Item, Library\n    from beets.ui import Subcommand\n\n    # TYPE_CHECKING guard is needed for any derived type\n    # which uses an import from `beets.library` and `beets.imported`\n    ImportStageFunc = Callable[[ImportSession, ImportTask], None]\n    T = TypeVar(\"T\", Album, Item, str)\n    TFunc = Callable[[T], str]\n    TFuncMap = dict[str, TFunc[T]]\n\n    AnyModel = TypeVar(\"AnyModel\", Album, Item)\n\n    P = ParamSpec(\"P\")\n    Ret = TypeVar(\"Ret\", bound=Any)\n    Listener = Callable[..., Any]\n\n\nPLUGIN_NAMESPACE = \"beetsplug\"\n\n# Plugins using the Last.fm API can share the same API key.\nLASTFM_KEY = \"2dc3914abf35f0d9c92d97d8f8e42b43\"\n\nEventType = Literal[\n    \"after_write\",\n    \"album_imported\",\n    \"album_removed\",\n    \"albuminfo_received\",\n    \"album_matched\",\n    \"before_choose_candidate\",\n    \"before_item_moved\",\n    \"cli_exit\",\n    \"database_change\",\n    \"import\",\n    \"import_begin\",\n    \"import_task_apply\",\n    \"import_task_before_choice\",\n    \"import_task_choice\",\n    \"import_task_created\",\n    \"import_task_files\",\n    \"import_task_start\",\n    \"item_copied\",\n    \"item_hardlinked\",\n    \"item_imported\",\n    \"item_linked\",\n    \"item_moved\",\n    \"item_reflinked\",\n    \"item_removed\",\n    \"library_opened\",\n    \"mb_album_extract\",\n    \"mb_track_extract\",\n    \"pluginload\",\n    \"trackinfo_received\",\n    \"write\",\n]\n# Global logger.\nlog = logging.getLogger(\"beets\")\n\n\nclass PluginConflictError(Exception):\n    \"\"\"Indicates that the services provided by one plugin conflict with\n    those of another.\n\n    For example two plugins may define different types for flexible fields.\n    \"\"\"\n\n\nclass PluginImportError(ImportError):\n    \"\"\"Indicates that a plugin could not be imported.\n\n    This is a subclass of ImportError so that it can be caught separately\n    from other errors.\n    \"\"\"\n\n    def __init__(self, name: str):\n        super().__init__(f\"Could not import plugin {name}\")\n\n\nclass PluginLogFilter(logging.Filter):\n    \"\"\"A logging filter that identifies the plugin that emitted a log\n    message.\n    \"\"\"\n\n    def __init__(self, plugin):\n        self.prefix = f\"{plugin.name}: \"\n\n    def filter(self, record):\n        if hasattr(record.msg, \"msg\") and isinstance(record.msg.msg, str):\n            # A _LogMessage from our hacked-up Logging replacement.\n            record.msg.msg = f\"{self.prefix}{record.msg.msg}\"\n        elif isinstance(record.msg, str):\n            record.msg = f\"{self.prefix}{record.msg}\"\n        return True\n\n\n# Managing the plugins themselves.\n\n\nclass BeetsPluginMeta(abc.ABCMeta):\n    template_funcs: ClassVar[TFuncMap[str]] = {}\n    template_fields: ClassVar[TFuncMap[Item]] = {}\n    album_template_fields: ClassVar[TFuncMap[Album]] = {}\n\n\nclass BeetsPlugin(metaclass=BeetsPluginMeta):\n    \"\"\"The base class for all beets plugins. Plugins provide\n    functionality by defining a subclass of BeetsPlugin and overriding\n    the abstract methods defined here.\n    \"\"\"\n\n    _raw_listeners: ClassVar[dict[EventType, list[Listener]]] = defaultdict(\n        list\n    )\n    listeners: ClassVar[dict[EventType, list[Listener]]] = defaultdict(list)\n\n    template_funcs: TFuncMap[str]\n    template_fields: TFuncMap[Item]\n    album_template_fields: TFuncMap[Album]\n\n    name: str\n    config: Subview\n    early_import_stages: list[ImportStageFunc]\n    import_stages: list[ImportStageFunc]\n\n    def __init_subclass__(cls) -> None:\n        \"\"\"Enable legacy metadata source plugins to work with the new interface.\n\n        When a plugin subclass of BeetsPlugin defines a `data_source` attribute\n        but does not inherit from MetadataSourcePlugin, this hook:\n\n        1. Skips abstract classes.\n        2. Warns that the class should extend MetadataSourcePlugin (deprecation).\n        3. Copies any nonabstract methods from MetadataSourcePlugin onto the\n           subclass to provide the full plugin API.\n\n        This compatibility layer will be removed in the v3.0.0 release.\n        \"\"\"\n        # TODO: Remove in v3.0.0\n        if inspect.isabstract(cls):\n            return\n\n        from beets.metadata_plugins import MetadataSourcePlugin\n\n        if issubclass(cls, MetadataSourcePlugin) or not hasattr(\n            cls, \"data_source\"\n        ):\n            return\n\n        deprecate_for_maintainers(\n            (\n                f\"'{cls.__name__}' is used as a legacy metadata source since it\"\n                \" inherits 'beets.plugins.BeetsPlugin'. Support for this\"\n            ),\n            \"'beets.metadata_plugins.MetadataSourcePlugin'\",\n            stacklevel=3,\n        )\n\n        method: property | cached_property[Any] | Callable[..., Any]\n        for name, method in inspect.getmembers(\n            MetadataSourcePlugin,\n            predicate=lambda f: (  # type: ignore[arg-type]\n                (\n                    isinstance(f, (property, cached_property))\n                    and not hasattr(\n                        BeetsPlugin,\n                        getattr(f, \"attrname\", None) or f.fget.__name__,  # type: ignore[union-attr]\n                    )\n                )\n                or (\n                    inspect.isfunction(f)\n                    and f.__name__\n                    and not getattr(f, \"__isabstractmethod__\", False)\n                    and not hasattr(BeetsPlugin, f.__name__)\n                )\n            ),\n        ):\n            setattr(cls, name, method)\n\n    def __init__(self, name: str | None = None):\n        \"\"\"Perform one-time plugin setup.\"\"\"\n\n        self.name = name or self.__module__.split(\".\")[-1]\n        self.config = beets.config[self.name]\n\n        # create per-instance storage for template fields and functions\n        self.template_funcs = {}\n        self.template_fields = {}\n        self.album_template_fields = {}\n\n        self.early_import_stages = []\n        self.import_stages = []\n\n        self._log = log.getChild(self.name)\n        self._log.setLevel(logging.NOTSET)  # Use `beets` logger level.\n        if not any(isinstance(f, PluginLogFilter) for f in self._log.filters):\n            self._log.addFilter(PluginLogFilter(self))\n\n        # In order to verify the config we need to make sure the plugin is fully\n        # configured (plugins usually add the default configuration *after*\n        # calling super().__init__()).\n        self.register_listener(\"pluginload\", self._verify_config)\n\n    def _verify_config(self, *_, **__) -> None:\n        \"\"\"Verify plugin configuration.\n\n        If deprecated 'source_weight' option is explicitly set by the user, they\n        will see a warning in the logs. Otherwise, this must be configured by\n        a third party plugin, thus we raise a deprecation warning which won't be\n        shown to user but will be visible to plugin developers.\n        \"\"\"\n        # TODO: Remove in v3.0.0\n        if (\n            not hasattr(self, \"data_source\")\n            or \"source_weight\" not in self.config\n        ):\n            return\n\n        for source in self.config.root().sources:\n            if \"source_weight\" in (source.get(self.name) or {}):\n                if source.filename:  # user config\n                    deprecate_for_user(\n                        self._log,\n                        f\"'{self.name}.source_weight' configuration option\",\n                        f\"'{self.name}.data_source_mismatch_penalty'\",\n                    )\n                else:  # 3rd-party plugin config\n                    deprecate_for_maintainers(\n                        \"'source_weight' configuration option\",\n                        \"'data_source_mismatch_penalty'\",\n                    )\n\n    def commands(self) -> Sequence[Subcommand]:\n        \"\"\"Should return a list of beets.ui.Subcommand objects for\n        commands that should be added to beets' CLI.\n        \"\"\"\n        return ()\n\n    def _set_stage_log_level(\n        self,\n        stages: list[ImportStageFunc],\n    ) -> list[ImportStageFunc]:\n        \"\"\"Adjust all the stages in `stages` to WARNING logging level.\"\"\"\n        return [\n            self._set_log_level_and_params(logging.WARNING, stage)\n            for stage in stages\n        ]\n\n    def get_early_import_stages(self) -> list[ImportStageFunc]:\n        \"\"\"Return a list of functions that should be called as importer\n        pipelines stages early in the pipeline.\n\n        The callables are wrapped versions of the functions in\n        `self.early_import_stages`. Wrapping provides some bookkeeping for the\n        plugin: specifically, the logging level is adjusted to WARNING.\n        \"\"\"\n        return self._set_stage_log_level(self.early_import_stages)\n\n    def get_import_stages(self) -> list[ImportStageFunc]:\n        \"\"\"Return a list of functions that should be called as importer\n        pipelines stages.\n\n        The callables are wrapped versions of the functions in\n        `self.import_stages`. Wrapping provides some bookkeeping for the\n        plugin: specifically, the logging level is adjusted to WARNING.\n        \"\"\"\n        return self._set_stage_log_level(self.import_stages)\n\n    def _set_log_level_and_params(\n        self,\n        base_log_level: int,\n        func: Callable[P, Ret],\n    ) -> Callable[P, Ret]:\n        \"\"\"Wrap `func` to temporarily set this plugin's logger level to\n        `base_log_level` + config options (and restore it to its previous\n        value after the function returns). Also determines which params may not\n        be sent for backwards-compatibility.\n        \"\"\"\n        argspec = inspect.getfullargspec(func)\n\n        @wraps(func)\n        def wrapper(*args: P.args, **kwargs: P.kwargs) -> Ret:\n            assert self._log.level == logging.NOTSET\n\n            verbosity = beets.config[\"verbose\"].get(int)\n            log_level = max(logging.DEBUG, base_log_level - 10 * verbosity)\n            self._log.setLevel(log_level)\n            if argspec.varkw is None:\n                kwargs = {k: v for k, v in kwargs.items() if k in argspec.args}  # type: ignore[assignment]\n\n            try:\n                return func(*args, **kwargs)\n            finally:\n                self._log.setLevel(logging.NOTSET)\n\n        return wrapper\n\n    def queries(self) -> dict[str, type[Query]]:\n        \"\"\"Return a dict mapping prefixes to Query subclasses.\"\"\"\n        return {}\n\n    def add_media_field(\n        self, name: str, descriptor: mediafile.MediaField\n    ) -> None:\n        \"\"\"Add a field that is synchronized between media files and items.\n\n        When a media field is added ``item.write()`` will set the name\n        property of the item's MediaFile to ``item[name]`` and save the\n        changes. Similarly ``item.read()`` will set ``item[name]`` to\n        the value of the name property of the media file.\n        \"\"\"\n        # Defer import to prevent circular dependency\n        from beets import library\n\n        mediafile.MediaFile.add_field(name, descriptor)\n        library.Item._media_fields.add(name)\n\n    def register_listener(self, event: EventType, func: Listener) -> None:\n        \"\"\"Add a function as a listener for the specified event.\"\"\"\n        if func not in self._raw_listeners[event]:\n            self._raw_listeners[event].append(func)\n            self.listeners[event].append(\n                self._set_log_level_and_params(logging.WARNING, func)\n            )\n\n    @classmethod\n    def template_func(cls, name: str) -> Callable[[TFunc[str]], TFunc[str]]:\n        \"\"\"Decorator that registers a path template function. The\n        function will be invoked as ``%name{}`` from path format\n        strings.\n        \"\"\"\n\n        def helper(func: TFunc[str]) -> TFunc[str]:\n            cls.template_funcs[name] = func\n            return func\n\n        return helper\n\n    @classmethod\n    def template_field(cls, name: str) -> Callable[[TFunc[Item]], TFunc[Item]]:\n        \"\"\"Decorator that registers a path template field computation.\n        The value will be referenced as ``$name`` from path format\n        strings. The function must accept a single parameter, the Item\n        being formatted.\n        \"\"\"\n\n        def helper(func: TFunc[Item]) -> TFunc[Item]:\n            cls.template_fields[name] = func\n            return func\n\n        return helper\n\n\ndef get_plugin_names() -> list[str]:\n    \"\"\"Discover and return the set of plugin names to be loaded.\n\n    Configures the plugin search paths and resolves the final set of plugins\n    based on configuration settings, inclusion filters, and exclusion rules.\n    Automatically includes the musicbrainz plugin when enabled in configuration.\n    \"\"\"\n    paths = [\n        str(Path(p).expanduser().absolute())\n        for p in beets.config[\"pluginpath\"].as_str_seq(split=False)\n    ]\n    log.debug(\"plugin paths: {}\", paths)\n\n    # Extend the `beetsplug` package to include the plugin paths.\n    import beetsplug\n\n    beetsplug.__path__ = paths + list(beetsplug.__path__)\n\n    # For backwards compatibility, also support plugin paths that\n    # *contain* a `beetsplug` package.\n    sys.path += paths\n    plugins = unique_list(beets.config[\"plugins\"].as_str_seq())\n    beets.config.add({\"disabled_plugins\": []})\n    disabled_plugins = set(beets.config[\"disabled_plugins\"].as_str_seq())\n    # TODO: Remove in v3.0.0\n    mb_enabled = beets.config[\"musicbrainz\"].flatten().get(\"enabled\")\n    if mb_enabled:\n        deprecate_for_user(\n            log,\n            \"'musicbrainz.enabled' configuration option\",\n            \"'plugins' configuration to explicitly add 'musicbrainz'\",\n        )\n        if \"musicbrainz\" not in plugins:\n            plugins.append(\"musicbrainz\")\n    elif mb_enabled is False:\n        deprecate_for_user(log, \"'musicbrainz.enabled' configuration option\")\n        disabled_plugins.add(\"musicbrainz\")\n\n    return [p for p in plugins if p not in disabled_plugins]\n\n\ndef _get_plugin(name: str) -> BeetsPlugin | None:\n    \"\"\"Dynamically load and instantiate a plugin class by name.\n\n    Attempts to import the plugin module, locate the appropriate plugin class\n    within it, and return an instance. Handles import failures gracefully and\n    logs warnings for missing plugins or loading errors.\n\n    Note we load the *last* plugin class found in the plugin namespace. This\n    allows plugins to define helper classes that inherit from BeetsPlugin\n    without those being loaded as the main plugin class.\n\n    Returns None if the plugin could not be loaded for any reason.\n    \"\"\"\n    try:\n        try:\n            namespace = import_module(f\"{PLUGIN_NAMESPACE}.{name}\")\n        except Exception as exc:\n            raise PluginImportError(name) from exc\n\n        for obj in reversed(namespace.__dict__.values()):\n            if (\n                inspect.isclass(obj)\n                and issubclass(obj, BeetsPlugin)\n                and obj != BeetsPlugin\n                and not inspect.isabstract(obj)\n                # Only consider this plugin's module or submodules to avoid\n                # conflicts when plugins import other BeetsPlugin classes\n                and (\n                    obj.__module__ == namespace.__name__\n                    or obj.__module__.startswith(f\"{namespace.__name__}.\")\n                )\n            ):\n                return obj()\n\n    except Exception:\n        log.warning(\"** error loading plugin {}\", name, exc_info=True)\n\n    return None\n\n\n_instances: list[BeetsPlugin] = []\n\n\ndef load_plugins() -> None:\n    \"\"\"Initialize the plugin system by loading all configured plugins.\n\n    Performs one-time plugin discovery and instantiation, storing loaded plugin\n    instances globally. Emits a pluginload event after successful initialization\n    to notify other components.\n    \"\"\"\n    if not _instances:\n        names = get_plugin_names()\n        log.debug(\"Loading plugins: {}\", \", \".join(sorted(names)))\n        _instances.extend(filter(None, map(_get_plugin, names)))\n\n        send(\"pluginload\")\n\n\ndef find_plugins() -> Iterable[BeetsPlugin]:\n    return _instances\n\n\n# Communication with plugins.\n\n\ndef commands() -> list[Subcommand]:\n    \"\"\"Returns a list of Subcommand objects from all loaded plugins.\"\"\"\n    out: list[Subcommand] = []\n    for plugin in find_plugins():\n        out += plugin.commands()\n    return out\n\n\ndef queries() -> dict[str, type[Query]]:\n    \"\"\"Returns a dict mapping prefix strings to Query subclasses all loaded\n    plugins.\n    \"\"\"\n    out: dict[str, type[Query]] = {}\n    for plugin in find_plugins():\n        out.update(plugin.queries())\n    return out\n\n\ndef types(model_cls: type[AnyModel]) -> dict[str, Type]:\n    \"\"\"Return mapping between flex field names and types for the given model.\"\"\"\n    attr_name = f\"{model_cls.__name__.lower()}_types\"\n    types: dict[str, Type] = {}\n    for plugin in find_plugins():\n        plugin_types = getattr(plugin, attr_name, {})\n        for field in plugin_types:\n            if field in types and plugin_types[field] != types[field]:\n                raise PluginConflictError(\n                    f\"Plugin {plugin.name} defines flexible field {field} \"\n                    \"which has already been defined with \"\n                    \"another type.\"\n                )\n        types.update(plugin_types)\n    return types\n\n\ndef named_queries(model_cls: type[AnyModel]) -> dict[str, FieldQueryType]:\n    \"\"\"Return mapping between field names and queries for the given model.\"\"\"\n    attr_name = f\"{model_cls.__name__.lower()}_queries\"\n    return {\n        field: query\n        for plugin in find_plugins()\n        for field, query in getattr(plugin, attr_name, {}).items()\n    }\n\n\ndef notify_info_yielded(\n    event: EventType,\n) -> Callable[[Callable[P, Iterable[Ret]]], Callable[P, Iterator[Ret]]]:\n    \"\"\"Makes a generator send the event 'event' every time it yields.\n    This decorator is supposed to decorate a generator, but any function\n    returning an iterable should work.\n    Each yielded value is passed to plugins using the 'info' parameter of\n    'send'.\n    \"\"\"\n\n    def decorator(\n        func: Callable[P, Iterable[Ret]],\n    ) -> Callable[P, Iterator[Ret]]:\n        @wraps(func)\n        def wrapper(*args: P.args, **kwargs: P.kwargs) -> Iterator[Ret]:\n            for v in func(*args, **kwargs):\n                send(event, info=v)\n                yield v\n\n        return wrapper\n\n    return decorator\n\n\ndef template_funcs() -> TFuncMap[str]:\n    \"\"\"Get all the template functions declared by plugins as a\n    dictionary.\n    \"\"\"\n    funcs: TFuncMap[str] = {}\n    for plugin in find_plugins():\n        funcs.update(plugin.template_funcs)\n    return funcs\n\n\ndef early_import_stages() -> list[ImportStageFunc]:\n    \"\"\"Get a list of early import stage functions defined by plugins.\"\"\"\n    stages: list[ImportStageFunc] = []\n    for plugin in find_plugins():\n        stages += plugin.get_early_import_stages()\n    return stages\n\n\ndef import_stages() -> list[ImportStageFunc]:\n    \"\"\"Get a list of import stage functions defined by plugins.\"\"\"\n    stages: list[ImportStageFunc] = []\n    for plugin in find_plugins():\n        stages += plugin.get_import_stages()\n    return stages\n\n\n# New-style (lazy) plugin-provided fields.\n\nF = TypeVar(\"F\")\n\n\ndef _check_conflicts_and_merge(\n    plugin: BeetsPlugin, plugin_funcs: dict[str, F], funcs: dict[str, F]\n) -> None:\n    \"\"\"Check the provided template functions for conflicts and merge into funcs.\n\n    Raises a `PluginConflictError` if a plugin defines template functions\n    for fields that another plugin has already defined template functions for.\n    \"\"\"\n    if not plugin_funcs.keys().isdisjoint(funcs.keys()):\n        conflicted_fields = \", \".join(plugin_funcs.keys() & funcs.keys())\n        raise PluginConflictError(\n            f\"Plugin {plugin.name} defines template functions for \"\n            f\"{conflicted_fields} that conflict with another plugin.\"\n        )\n    funcs.update(plugin_funcs)\n\n\ndef item_field_getters() -> TFuncMap[Item]:\n    \"\"\"Get a dictionary mapping field names to unary functions that\n    compute the field's value.\n    \"\"\"\n    funcs: TFuncMap[Item] = {}\n    for plugin in find_plugins():\n        _check_conflicts_and_merge(plugin, plugin.template_fields, funcs)\n    return funcs\n\n\ndef album_field_getters() -> TFuncMap[Album]:\n    \"\"\"As above, for album fields.\"\"\"\n    funcs: TFuncMap[Album] = {}\n    for plugin in find_plugins():\n        _check_conflicts_and_merge(plugin, plugin.album_template_fields, funcs)\n    return funcs\n\n\n# Event dispatch.\n\n\ndef send(event: EventType, **arguments: Any) -> list[Any]:\n    \"\"\"Send an event to all assigned event listeners.\n\n    `event` is the name of  the event to send, all other named arguments\n    are passed along to the handlers.\n\n    Return a list of non-None values returned from the handlers.\n    \"\"\"\n    log.debug(\"Sending event: {}\", event)\n    return [\n        r\n        for handler in BeetsPlugin.listeners[event]\n        if (r := handler(**arguments)) is not None\n    ]\n\n\ndef feat_tokens(\n    for_artist: bool = True, custom_words: list[str] | None = None\n) -> str:\n    \"\"\"Return a regular expression that matches phrases like \"featuring\"\n    that separate a main artist or a song title from secondary artists.\n    The `for_artist` option determines whether the regex should be\n    suitable for matching artist fields (the default) or title fields.\n    \"\"\"\n    feat_words = [\"ft\", \"featuring\", \"feat\", \"feat.\", \"ft.\"]\n    if isinstance(custom_words, list):\n        feat_words += custom_words\n    if for_artist:\n        feat_words += [\"with\", \"vs\", \"and\", \"con\", \"&\"]\n    return (\n        rf\"(?<=[\\s(\\[])(?:{'|'.join(re.escape(x) for x in feat_words)})(?=\\s)\"\n    )\n\n\ndef apply_item_changes(\n    lib: Library, item: Item, move: bool, pretend: bool, write: bool\n) -> None:\n    \"\"\"Store, move, and write the item according to the arguments.\n\n    :param lib: beets library.\n    :param item: Item whose changes to apply.\n    :param move: Move the item if it's in the library.\n    :param pretend: Return without moving, writing, or storing the item's\n        metadata.\n    :param write: Write the item's metadata to its media file.\n    \"\"\"\n    if pretend:\n        return\n\n    from beets import util\n\n    # Move the item if it's in the library.\n    if move and lib.directory in util.ancestry(item.path):\n        item.move(with_album=False)\n\n    if write:\n        item.try_write()\n\n    item.store()\n"
  },
  {
    "path": "beets/py.typed",
    "content": ""
  },
  {
    "path": "beets/test/__init__.py",
    "content": "# This file is part of beets.\n# Copyright 2024, Lars Kruse\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"This module contains components of beets' test environment, which\nmay be of use for testing procedures of external libraries or programs.\nFor example the 'TestHelper' class may be useful for creating an\nin-memory beets library filled with a few example items.\n\"\"\"\n"
  },
  {
    "path": "beets/test/_common.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Some common functionality for beets' test cases.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport sys\nimport unittest\nfrom contextlib import contextmanager\nfrom typing import TYPE_CHECKING\n\nimport beets\nimport beets.library\n\n# Make sure the development versions of the plugins are used\nimport beetsplug\nfrom beets import importer, logging, util\nfrom beets.ui import commands\nfrom beets.util import syspath\n\nif TYPE_CHECKING:\n    import pytest\n\nbeetsplug.__path__ = [\n    os.path.abspath(\n        os.path.join(\n            os.path.dirname(__file__),\n            os.path.pardir,\n            os.path.pardir,\n            \"beetsplug\",\n        )\n    )\n]\n\n# Test resources path.\nRSRC = util.bytestring_path(\n    os.path.abspath(\n        os.path.join(\n            os.path.dirname(__file__),\n            os.path.pardir,\n            os.path.pardir,\n            \"test\",\n            \"rsrc\",\n        )\n    )\n)\nPLUGINPATH = os.path.join(RSRC.decode(), \"beetsplug\")\n\n# Propagate to root logger so the test runner can capture it\nlog = logging.getLogger(\"beets\")\nlog.propagate = True\nlog.setLevel(logging.DEBUG)\n\n# OS feature test.\nHAVE_SYMLINK = sys.platform != \"win32\"\nHAVE_HARDLINK = sys.platform != \"win32\"\n\n\ndef item(lib=None, **kwargs):\n    defaults = dict(\n        title=\"the title\",\n        artist=\"the artist\",\n        albumartist=\"the album artist\",\n        album=\"the album\",\n        genres=[\"the genre\"],\n        lyricist=\"the lyricist\",\n        composer=\"the composer\",\n        arranger=\"the arranger\",\n        grouping=\"the grouping\",\n        work=\"the work title\",\n        mb_workid=\"the work musicbrainz id\",\n        work_disambig=\"the work disambiguation\",\n        year=1,\n        month=2,\n        day=3,\n        track=4,\n        tracktotal=5,\n        disc=6,\n        disctotal=7,\n        lyrics=\"the lyrics\",\n        comments=\"the comments\",\n        bpm=8,\n        comp=True,\n        length=60.0,\n        bitrate=128000,\n        format=\"FLAC\",\n        mb_trackid=\"someID-1\",\n        mb_albumid=\"someID-2\",\n        mb_artistid=\"someID-3\",\n        mb_albumartistid=\"someID-4\",\n        mb_releasetrackid=\"someID-5\",\n        album_id=None,\n        mtime=12345,\n    )\n    i = beets.library.Item(**{**defaults, **kwargs})\n    if lib:\n        lib.add(i)\n    return i\n\n\n# Dummy import session.\ndef import_session(lib=None, loghandler=None, paths=[], query=[], cli=False):\n    cls = (\n        commands.import_.session.TerminalImportSession\n        if cli\n        else importer.ImportSession\n    )\n    return cls(lib, loghandler, paths, query)\n\n\n# Mock I/O.\n\n\nclass InputError(IOError):\n    def __str__(self) -> str:\n        return \"Attempt to read with no input provided.\"\n\n\nclass DummyIn:\n    encoding = \"utf-8\"\n\n    def __init__(self) -> None:\n        self.buf: list[str] = []\n\n    def add(self, s: str) -> None:\n        self.buf.append(f\"{s}\\n\")\n\n    def close(self) -> None:\n        pass\n\n    def readline(self) -> str:\n        if not self.buf:\n            raise InputError\n\n        return self.buf.pop(0)\n\n\nclass DummyIO:\n    \"\"\"Test helper that manages standard input and output.\"\"\"\n\n    def __init__(\n        self,\n        monkeypatch: pytest.MonkeyPatch,\n        capteesys: pytest.CaptureFixture[str],\n    ) -> None:\n        self._capteesys = capteesys\n        self.stdin = DummyIn()\n\n        monkeypatch.setattr(\"sys.stdin\", self.stdin)\n\n    def addinput(self, text: str) -> None:\n        \"\"\"Simulate user typing into stdin.\"\"\"\n        self.stdin.add(text)\n\n    def getoutput(self) -> str:\n        \"\"\"Get the standard output captured so far.\n\n        Note: it clears the internal buffer, so subsequent calls will only\n        return *new* output.\n        \"\"\"\n        # Using capteesys allows you to see output in the console if the test fails\n        return self._capteesys.readouterr().out\n\n\n# Utility.\n\n\ndef touch(path):\n    open(syspath(path), \"a\").close()\n\n\nclass Bag:\n    \"\"\"An object that exposes a set of fields given as keyword\n    arguments. Any field not found in the dictionary appears to be None.\n    Used for mocking Album objects and the like.\n    \"\"\"\n\n    def __init__(self, **fields):\n        self.fields = fields\n\n    def __getattr__(self, key):\n        return self.fields.get(key)\n\n\n# Platform mocking.\n\n\n@contextmanager\ndef platform_windows():\n    import ntpath\n\n    old_path = os.path\n    try:\n        os.path = ntpath\n        yield\n    finally:\n        os.path = old_path\n\n\n@contextmanager\ndef platform_posix():\n    import posixpath\n\n    old_path = os.path\n    try:\n        os.path = posixpath\n        yield\n    finally:\n        os.path = old_path\n\n\n@contextmanager\ndef system_mock(name):\n    import platform\n\n    old_system = platform.system\n    platform.system = lambda: name\n    try:\n        yield\n    finally:\n        platform.system = old_system\n\n\ndef slow_test(unused=None):\n    def _id(obj):\n        return obj\n\n    if \"SKIP_SLOW_TESTS\" in os.environ:\n        return unittest.skip(\"test is slow\")\n    return _id\n"
  },
  {
    "path": "beets/test/helper.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Thomas Scholtes.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"This module includes various helpers that provide fixtures, capture\ninformation or mock the environment.\n\n- `has_program` checks the presence of a command on the system.\n\n- The `ImportSessionFixture` allows one to run importer code while\n  controlling the interactions through code.\n\n- The `TestHelper` class encapsulates various fixtures that can be set up.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport os.path\nimport shutil\nimport subprocess\nimport sys\nimport unittest\nfrom contextlib import contextmanager\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom functools import cached_property\nfrom pathlib import Path\nfrom tempfile import gettempdir, mkdtemp, mkstemp\nfrom typing import Any, ClassVar\nfrom unittest.mock import patch\n\nimport pytest\nimport responses\nfrom mediafile import Image, MediaFile\n\nimport beets\nimport beets.plugins\nfrom beets import importer, logging, util\nfrom beets.autotag.hooks import AlbumInfo, TrackInfo\nfrom beets.importer import ImportSession\nfrom beets.library import Item, Library\nfrom beets.test import _common\nfrom beets.ui.commands.import_.session import TerminalImportSession\nfrom beets.util import (\n    MoveOperation,\n    bytestring_path,\n    clean_module_tempdir,\n    syspath,\n)\n\n\nclass LogCapture(logging.Handler):\n    def __init__(self):\n        logging.Handler.__init__(self)\n        self.messages = []\n\n    def emit(self, record):\n        self.messages.append(str(record.msg))\n\n\n@contextmanager\ndef capture_log(logger=\"beets\"):\n    capture = LogCapture()\n    log = logging.getLogger(logger)\n    log.addHandler(capture)\n    try:\n        yield capture.messages\n    finally:\n        log.removeHandler(capture)\n\n\ndef has_program(cmd, args=[\"--version\"]):\n    \"\"\"Returns `True` if `cmd` can be executed.\"\"\"\n    full_cmd = [cmd, *args]\n    try:\n        with open(os.devnull, \"wb\") as devnull:\n            subprocess.check_call(\n                full_cmd, stderr=devnull, stdout=devnull, stdin=devnull\n            )\n    except OSError:\n        return False\n    except subprocess.CalledProcessError:\n        return False\n    else:\n        return True\n\n\ndef check_reflink_support(path: str) -> bool:\n    try:\n        import reflink\n    except ImportError:\n        return False\n\n    return reflink.supported_at(path)\n\n\nclass ConfigMixin:\n    @cached_property\n    def config(self) -> beets.IncludeLazyConfig:\n        \"\"\"Base beets configuration for tests.\"\"\"\n        config = beets.config\n        config.sources = []\n        config.read(user=False, defaults=True)\n\n        config[\"plugins\"] = []\n        config[\"verbose\"] = 1\n        config[\"ui\"][\"color\"] = False\n        config[\"threaded\"] = False\n        return config\n\n\nNEEDS_REFLINK = unittest.skipUnless(\n    check_reflink_support(gettempdir()), \"no reflink support for libdir\"\n)\n\n\nclass RunMixin:\n    def run_command(self, *args, **kwargs):\n        \"\"\"Run a beets command with an arbitrary amount of arguments. The\n        Library` defaults to `self.lib`, but can be overridden with\n        the keyword argument `lib`.\n        \"\"\"\n        sys.argv = [\"beet\"]  # avoid leakage from test suite args\n        lib = None\n        if hasattr(self, \"lib\"):\n            lib = self.lib\n        lib = kwargs.get(\"lib\", lib)\n        beets.ui._raw_main(list(args), lib)\n\n\n@pytest.mark.usefixtures(\"io\")\nclass IOMixin(RunMixin):\n    io: _common.DummyIO\n\n    def run_with_output(self, *args):\n        self.io.getoutput()\n        self.run_command(*args)\n        return self.io.getoutput()\n\n\nclass TestHelper(RunMixin, ConfigMixin):\n    \"\"\"Helper mixin for high-level cli and plugin tests.\n\n    This mixin provides methods to isolate beets' global state provide\n    fixtures.\n    \"\"\"\n\n    lib: Library\n\n    resource_path = Path(os.fsdecode(_common.RSRC)) / \"full.mp3\"\n\n    db_on_disk: ClassVar[bool] = False\n\n    @cached_property\n    def temp_dir_path(self) -> Path:\n        return Path(self.create_temp_dir())\n\n    @cached_property\n    def temp_dir(self) -> bytes:\n        return util.bytestring_path(self.temp_dir_path)\n\n    @cached_property\n    def lib_path(self) -> Path:\n        lib_path = self.temp_dir_path / \"libdir\"\n        lib_path.mkdir(exist_ok=True)\n        return lib_path\n\n    @cached_property\n    def libdir(self) -> bytes:\n        return bytestring_path(self.lib_path)\n\n    # TODO automate teardown through hook registration\n\n    def setup_beets(self):\n        \"\"\"Setup pristine global configuration and library for testing.\n\n        Sets ``beets.config`` so we can safely use any functionality\n        that uses the global configuration.  All paths used are\n        contained in a temporary directory\n\n        Sets the following properties on itself.\n\n        - ``temp_dir`` Path to a temporary directory containing all\n          files specific to beets\n\n        - ``libdir`` Path to a subfolder of ``temp_dir``, containing the\n          library's media files. Same as ``config['directory']``.\n\n        - ``lib`` Library instance created with the settings from\n          ``config``.\n\n        Make sure you call ``teardown_beets()`` afterwards.\n        \"\"\"\n        temp_dir_str = str(self.temp_dir_path)\n        self.env_patcher = patch.dict(\n            \"os.environ\",\n            {\n                \"BEETSDIR\": temp_dir_str,\n                \"HOME\": temp_dir_str,  # used by Confuse to create directories.\n            },\n        )\n        self.env_patcher.start()\n\n        self.config[\"directory\"] = str(self.lib_path)\n\n        if self.db_on_disk:\n            dbpath = util.bytestring_path(self.config[\"library\"].as_filename())\n        else:\n            dbpath = \":memory:\"\n        self.lib = Library(dbpath, self.libdir)\n\n    def teardown_beets(self):\n        self.env_patcher.stop()\n        self.lib._close()\n        self.remove_temp_dir()\n\n    # Library fixtures methods\n\n    def create_item(self, **values):\n        \"\"\"Return an `Item` instance with sensible default values.\n\n        The item receives its attributes from `**values` paratmeter. The\n        `title`, `artist`, `album`, `track`, `format` and `path`\n        attributes have defaults if they are not given as parameters.\n        The `title` attribute is formatted with a running item count to\n        prevent duplicates. The default for the `path` attribute\n        respects the `format` value.\n\n        The item is attached to the database from `self.lib`.\n        \"\"\"\n        values_ = {\n            \"title\": \"t\\u00eftle {}\",\n            \"artist\": \"the \\u00e4rtist\",\n            \"album\": \"the \\u00e4lbum\",\n            \"track\": 1,\n            \"format\": \"MP3\",\n        }\n        values_.update(values)\n        values_[\"title\"] = values_[\"title\"].format(1)\n        values_[\"db\"] = self.lib\n        item = Item(**values_)\n        if \"path\" not in values:\n            item[\"path\"] = f\"audio.{item['format'].lower()}\"\n        # mtime needs to be set last since other assignments reset it.\n        item.mtime = 12345\n        return item\n\n    def add_item(self, **values):\n        \"\"\"Add an item to the library and return it.\n\n        Creates the item by passing the parameters to `create_item()`.\n\n        If `path` is not set in `values` it is set to `item.destination()`.\n        \"\"\"\n        # When specifying a path, store it normalized (as beets does\n        # ordinarily).\n        if \"path\" in values:\n            values[\"path\"] = util.normpath(values[\"path\"])\n\n        item = self.create_item(**values)\n        item.add(self.lib)\n\n        # Ensure every item has a path.\n        if \"path\" not in values:\n            item[\"path\"] = item.destination()\n            item.store()\n\n        return item\n\n    def add_item_fixture(self, **values):\n        \"\"\"Add an item with an actual audio file to the library.\"\"\"\n        item = self.create_item(**values)\n        extension = item[\"format\"].lower()\n        item[\"path\"] = os.path.join(\n            _common.RSRC, util.bytestring_path(f\"min.{extension}\")\n        )\n        item.add(self.lib)\n        item.move(operation=MoveOperation.COPY)\n        item.store()\n        return item\n\n    def add_album(self, **values):\n        item = self.add_item(**values)\n        return self.lib.add_album([item])\n\n    def add_item_fixtures(self, ext=\"mp3\", count=1):\n        \"\"\"Add a number of items with files to the database.\"\"\"\n        # TODO base this on `add_item()`\n        items = []\n        path = os.path.join(_common.RSRC, util.bytestring_path(f\"full.{ext}\"))\n        for i in range(count):\n            item = Item.from_path(path)\n            item.album = f\"\\u00e4lbum {i}\"  # Check unicode paths\n            item.title = f\"t\\u00eftle {i}\"\n            # mtime needs to be set last since other assignments reset it.\n            item.mtime = 12345\n            item.add(self.lib)\n            item.move(operation=MoveOperation.COPY)\n            item.store()\n            items.append(item)\n        return items\n\n    def add_album_fixture(\n        self,\n        track_count=1,\n        fname=\"full\",\n        ext=\"mp3\",\n        disc_count=1,\n    ):\n        \"\"\"Add an album with files to the database.\"\"\"\n        items = []\n        path = os.path.join(\n            _common.RSRC,\n            util.bytestring_path(f\"{fname}.{ext}\"),\n        )\n        for discnumber in range(1, disc_count + 1):\n            for i in range(track_count):\n                item = Item.from_path(path)\n                item.album = \"\\u00e4lbum\"  # Check unicode paths\n                item.title = f\"t\\u00eftle {i}\"\n                item.disc = discnumber\n                # mtime needs to be set last since other assignments reset it.\n                item.mtime = 12345\n                item.add(self.lib)\n                item.move(operation=MoveOperation.COPY)\n                item.store()\n                items.append(item)\n        return self.lib.add_album(items)\n\n    def create_mediafile_fixture(self, ext=\"mp3\", images=[], target_dir=None):\n        \"\"\"Copy a fixture mediafile with the extension to `temp_dir`.\n\n        `images` is a subset of 'png', 'jpg', and 'tiff'. For each\n        specified extension a cover art image is added to the media\n        file.\n        \"\"\"\n        if not target_dir:\n            target_dir = self.temp_dir\n        src = os.path.join(_common.RSRC, util.bytestring_path(f\"full.{ext}\"))\n        handle, path = mkstemp(dir=target_dir)\n        path = bytestring_path(path)\n        os.close(handle)\n        shutil.copyfile(syspath(src), syspath(path))\n\n        if images:\n            mediafile = MediaFile(path)\n            imgs = []\n            for img_ext in images:\n                file = util.bytestring_path(f\"image-2x3.{img_ext}\")\n                img_path = os.path.join(_common.RSRC, file)\n                with open(img_path, \"rb\") as f:\n                    imgs.append(Image(f.read()))\n            mediafile.images = imgs\n            mediafile.save()\n\n        return path\n\n    # Safe file operations\n\n    def create_temp_dir(self, **kwargs) -> str:\n        return mkdtemp(**kwargs)\n\n    def remove_temp_dir(self):\n        \"\"\"Delete the temporary directory created by `create_temp_dir`.\"\"\"\n        shutil.rmtree(self.temp_dir_path)\n\n    def touch(self, path, dir=None, content=\"\"):\n        \"\"\"Create a file at `path` with given content.\n\n        If `dir` is given, it is prepended to `path`. After that, if the\n        path is relative, it is resolved with respect to\n        `self.temp_dir`.\n        \"\"\"\n        if dir:\n            path = os.path.join(dir, path)\n\n        if not os.path.isabs(path):\n            path = os.path.join(self.temp_dir, path)\n\n        parent = os.path.dirname(path)\n        if not os.path.isdir(syspath(parent)):\n            os.makedirs(syspath(parent))\n\n        with open(syspath(path), \"a+\") as f:\n            f.write(content)\n        return path\n\n\n# A test harness for all beets tests.\n# Provides temporary, isolated configuration.\nclass BeetsTestCase(unittest.TestCase, TestHelper):\n    \"\"\"A unittest.TestCase subclass that saves and restores beets'\n    global configuration. This allows tests to make temporary\n    modifications that will then be automatically removed when the test\n    completes. Also provides some additional assertion methods, a\n    temporary directory, and a DummyIO.\n    \"\"\"\n\n    def setUp(self):\n        self.setup_beets()\n\n    def tearDown(self):\n        self.teardown_beets()\n\n\nclass ItemInDBTestCase(BeetsTestCase):\n    \"\"\"A test case that includes an in-memory library object (`lib`) and\n    an item added to the library (`i`).\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.i = _common.item(self.lib)\n\n\nclass PluginMixin(ConfigMixin):\n    plugin: ClassVar[str]\n    preload_plugin: ClassVar[bool] = True\n\n    def setup_beets(self):\n        super().setup_beets()\n        if self.preload_plugin:\n            self.load_plugins()\n\n    def teardown_beets(self):\n        super().teardown_beets()\n        self.unload_plugins()\n\n    def register_plugin(\n        self, plugin_class: type[beets.plugins.BeetsPlugin]\n    ) -> None:\n        beets.plugins._instances.append(plugin_class())\n\n    def load_plugins(self, *plugins: str) -> None:\n        \"\"\"Load and initialize plugins by names.\n\n        Similar setting a list of plugins in the configuration. Make\n        sure you call ``unload_plugins()`` afterwards.\n        \"\"\"\n        # FIXME this should eventually be handled by a plugin manager\n        plugins = (self.plugin,) if hasattr(self, \"plugin\") else plugins\n        self.config[\"plugins\"] = plugins\n        beets.plugins.load_plugins()\n\n    def unload_plugins(self) -> None:\n        \"\"\"Unload all plugins and remove them from the configuration.\"\"\"\n        # FIXME this should eventually be handled by a plugin manager\n        beets.plugins.BeetsPlugin.listeners.clear()\n        beets.plugins.BeetsPlugin._raw_listeners.clear()\n        self.config[\"plugins\"] = []\n        beets.plugins._instances.clear()\n\n    @contextmanager\n    def configure_plugin(self, config: Any):\n        self.config[self.plugin].set(config)\n        self.load_plugins(self.plugin)\n\n        yield\n\n        self.unload_plugins()\n\n\nclass PluginTestCase(PluginMixin, BeetsTestCase):\n    pass\n\n\nclass ImportHelper(TestHelper):\n    \"\"\"Provides tools to setup a library, a directory containing files that are\n    to be imported and an import session. The class also provides stubs for the\n    autotagging library and several assertions for the library.\n    \"\"\"\n\n    default_import_config: ClassVar[dict[str, bool]] = {\n        \"autotag\": True,\n        \"copy\": True,\n        \"hardlink\": False,\n        \"link\": False,\n        \"move\": False,\n        \"resume\": False,\n        \"singletons\": False,\n        \"timid\": True,\n    }\n\n    lib: Library\n    importer: ImportSession\n\n    @cached_property\n    def import_path(self) -> Path:\n        import_path = self.temp_dir_path / \"import\"\n        import_path.mkdir(exist_ok=True)\n        return import_path\n\n    @cached_property\n    def import_dir(self) -> bytes:\n        return bytestring_path(self.import_path)\n\n    def setUp(self):\n        super().setUp()\n        self.import_media = []\n        self.lib.path_formats = [\n            (\"default\", os.path.join(\"$artist\", \"$album\", \"$title\")),\n            (\"singleton:true\", os.path.join(\"singletons\", \"$title\")),\n            (\"comp:true\", os.path.join(\"compilations\", \"$album\", \"$title\")),\n        ]\n\n    def prepare_track_for_import(\n        self,\n        track_id: int,\n        album_path: Path,\n        album_id: int | None = None,\n    ) -> Path:\n        track_path = album_path / f\"track_{track_id}.mp3\"\n        shutil.copy(self.resource_path, track_path)\n        medium = MediaFile(track_path)\n        medium.update(\n            {\n                \"album\": f\"Tag Album{f' {album_id}' if album_id else ''}\",\n                \"albumartist\": None,\n                \"mb_albumid\": None,\n                \"comp\": None,\n                \"artist\": \"Tag Artist\",\n                \"title\": f\"Tag Track {track_id}\",\n                \"track\": track_id,\n                \"mb_trackid\": None,\n            }\n        )\n        medium.save()\n        self.import_media.append(medium)\n        return track_path\n\n    def prepare_album_for_import(\n        self,\n        item_count: int,\n        album_id: int | None = None,\n        album_path: Path | None = None,\n    ) -> list[Path]:\n        \"\"\"Create an album directory with media files to import.\n\n        The directory has following layout\n          album/\n            track_1.mp3\n            track_2.mp3\n            track_3.mp3\n        \"\"\"\n        if not album_path:\n            album_dir = f\"album_{album_id}\" if album_id else \"album\"\n            album_path = self.import_path / album_dir\n\n        album_path.mkdir(exist_ok=True)\n\n        return [\n            self.prepare_track_for_import(tid, album_path, album_id=album_id)\n            for tid in range(1, item_count + 1)\n        ]\n\n    def prepare_albums_for_import(self, count: int = 1) -> None:\n        album_dirs = self.import_path.glob(\"album_*\")\n        base_idx = int(str(max(album_dirs, default=\"0\")).split(\"_\")[-1]) + 1\n\n        for album_id in range(base_idx, count + base_idx):\n            self.prepare_album_for_import(1, album_id=album_id)\n\n    def _get_import_session(self, import_dir: bytes) -> ImportSession:\n        return ImportSessionFixture(\n            self.lib,\n            loghandler=None,\n            query=None,\n            paths=[import_dir],\n        )\n\n    def setup_importer(\n        self, import_dir: bytes | None = None, **kwargs\n    ) -> ImportSession:\n        self.config[\"import\"].set_args({**self.default_import_config, **kwargs})\n        self.importer = self._get_import_session(import_dir or self.import_dir)\n        return self.importer\n\n    def setup_singleton_importer(self, **kwargs) -> ImportSession:\n        return self.setup_importer(singletons=True, **kwargs)\n\n\nclass AsIsImporterMixin:\n    def setUp(self):\n        super().setUp()\n        self.prepare_album_for_import(1)\n\n    def run_asis_importer(self, **kwargs):\n        importer = self.setup_importer(autotag=False, **kwargs)\n        importer.run()\n        return importer\n\n\nclass ImportTestCase(ImportHelper, BeetsTestCase):\n    pass\n\n\nclass ImportSessionFixture(ImportSession):\n    \"\"\"ImportSession that can be controlled programaticaly.\n\n    >>> lib = Library(':memory:')\n    >>> importer = ImportSessionFixture(lib, paths=['/path/to/import'])\n    >>> importer.add_choice(importer.Action.SKIP)\n    >>> importer.add_choice(importer.Action.ASIS)\n    >>> importer.default_choice = importer.Action.APPLY\n    >>> importer.run()\n\n    This imports ``/path/to/import`` into `lib`. It skips the first\n    album and imports the second one with metadata from the tags. For the\n    remaining albums, the metadata from the autotagger will be applied.\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self._choices = []\n        self._resolutions = []\n\n    default_choice = importer.Action.APPLY\n\n    def add_choice(self, choice):\n        self._choices.append(choice)\n\n    def clear_choices(self):\n        self._choices = []\n\n    def choose_match(self, task):\n        try:\n            choice = self._choices.pop(0)\n        except IndexError:\n            choice = self.default_choice\n\n        if choice == importer.Action.APPLY:\n            return task.candidates[0]\n        elif isinstance(choice, int):\n            return task.candidates[choice - 1]\n        else:\n            return choice\n\n    choose_item = choose_match\n\n    Resolution = Enum(\"Resolution\", \"REMOVE SKIP KEEPBOTH MERGE\")\n\n    default_resolution = \"REMOVE\"\n\n    def resolve_duplicate(self, task, found_duplicates):\n        try:\n            res = self._resolutions.pop(0)\n        except IndexError:\n            res = self.default_resolution\n\n        if res == self.Resolution.SKIP:\n            task.set_choice(importer.Action.SKIP)\n        elif res == self.Resolution.REMOVE:\n            task.should_remove_duplicates = True\n        elif res == self.Resolution.MERGE:\n            task.should_merge_duplicates = True\n\n\nclass TerminalImportSessionFixture(TerminalImportSession):\n    def __init__(self, *args, **kwargs):\n        self.io = kwargs.pop(\"io\")\n        super().__init__(*args, **kwargs)\n        self._choices = []\n\n    default_choice = importer.Action.APPLY\n\n    def add_choice(self, choice):\n        self._choices.append(choice)\n\n    def clear_choices(self):\n        self._choices = []\n\n    def choose_match(self, task):\n        self._add_choice_input()\n        return super().choose_match(task)\n\n    def choose_item(self, task):\n        self._add_choice_input()\n        return super().choose_item(task)\n\n    def _add_choice_input(self):\n        try:\n            choice = self._choices.pop(0)\n        except IndexError:\n            choice = self.default_choice\n\n        if choice == importer.Action.APPLY:\n            self.io.addinput(\"A\")\n        elif choice == importer.Action.ASIS:\n            self.io.addinput(\"U\")\n        elif choice == importer.Action.ALBUMS:\n            self.io.addinput(\"G\")\n        elif choice == importer.Action.TRACKS:\n            self.io.addinput(\"T\")\n        elif choice == importer.Action.SKIP:\n            self.io.addinput(\"S\")\n        else:\n            self.io.addinput(\"M\")\n            self.io.addinput(str(choice))\n            self._add_choice_input()\n\n\nclass TerminalImportMixin(IOMixin, ImportHelper):\n    \"\"\"Provides_a terminal importer for the import session.\"\"\"\n\n    def _get_import_session(self, import_dir: bytes) -> importer.ImportSession:\n        return TerminalImportSessionFixture(\n            self.lib,\n            loghandler=None,\n            query=None,\n            io=self.io,\n            paths=[import_dir],\n        )\n\n\n@dataclass\nclass AutotagStub:\n    \"\"\"Stub out MusicBrainz album and track matcher and control what the\n    autotagger returns.\n    \"\"\"\n\n    NONE = \"NONE\"\n    IDENT = \"IDENT\"\n    GOOD = \"GOOD\"\n    BAD = \"BAD\"\n    MISSING = \"MISSING\"\n    matching: str\n\n    length = 2\n\n    def install(self):\n        self.patchers = [\n            patch(\"beets.metadata_plugins.album_for_id\", lambda *_: None),\n            patch(\"beets.metadata_plugins.track_for_id\", lambda *_: None),\n            patch(\"beets.metadata_plugins.candidates\", self.candidates),\n            patch(\n                \"beets.metadata_plugins.item_candidates\", self.item_candidates\n            ),\n        ]\n        for p in self.patchers:\n            p.start()\n\n        return self\n\n    def restore(self):\n        for p in self.patchers:\n            p.stop()\n\n    def candidates(self, items, artist, album, va_likely):\n        if self.matching == self.IDENT:\n            yield self._make_album_match(artist, album, len(items))\n\n        elif self.matching == self.GOOD:\n            for i in range(self.length):\n                yield self._make_album_match(artist, album, len(items), i)\n\n        elif self.matching == self.BAD:\n            for i in range(self.length):\n                yield self._make_album_match(artist, album, len(items), i + 1)\n\n        elif self.matching == self.MISSING:\n            yield self._make_album_match(artist, album, len(items), missing=1)\n\n    def item_candidates(self, item, artist, title):\n        yield TrackInfo(\n            title=title.replace(\"Tag\", \"Applied\"),\n            track_id=\"trackid\",\n            artist=artist.replace(\"Tag\", \"Applied\"),\n            artist_id=\"artistid\",\n            length=1,\n            index=0,\n        )\n\n    def _make_track_match(self, artist, album, number):\n        return TrackInfo(\n            title=f\"Applied Track {number}\",\n            track_id=f\"match {number}\",\n            artist=artist,\n            length=1,\n            index=0,\n        )\n\n    def _make_album_match(self, artist, album, tracks, distance=0, missing=0):\n        id = f\" {'M' * distance}\" if distance else \"\"\n\n        if artist is None:\n            artist = \"Various Artists\"\n        else:\n            artist = f\"{artist.replace('Tag', 'Applied')}{id}\"\n        album = f\"{album.replace('Tag', 'Applied')}{id}\"\n\n        track_infos = []\n        for i in range(tracks - missing):\n            track_infos.append(self._make_track_match(artist, album, i + 1))\n\n        return AlbumInfo(\n            artist=artist,\n            album=album,\n            tracks=track_infos,\n            va=False,\n            album_id=f\"albumid{id}\",\n            artist_id=f\"artistid{id}\",\n            albumtype=\"soundtrack\",\n            data_source=\"match_source\",\n            bandcamp_album_id=\"bc_url\",\n        )\n\n\nclass AutotagImportTestCase(ImportTestCase):\n    matching = AutotagStub.IDENT\n\n    def setUp(self):\n        super().setUp()\n        self.matcher = AutotagStub(self.matching).install()\n        self.addCleanup(self.matcher.restore)\n\n\nclass FetchImageHelper:\n    \"\"\"Helper mixin for mocking requests when fetching images\n    with remote art sources.\n    \"\"\"\n\n    @responses.activate\n    def run(self, *args, **kwargs):\n        super().run(*args, **kwargs)\n\n    IMAGEHEADER: ClassVar[dict[str, bytes]] = {\n        \"image/jpeg\": b\"\\xff\\xd8\\xff\\x00\\x00\\x00JFIF\",\n        \"image/png\": b\"\\211PNG\\r\\n\\032\\n\",\n        \"image/gif\": b\"GIF89a\",\n        # dummy type that is definitely not a valid image content type\n        \"image/watercolour\": b\"watercolour\",\n        \"text/html\": (\n            b\"<!DOCTYPE html>\\n<html>\\n<head>\\n</head>\\n\"\n            b\"<body>\\n</body>\\n</html>\"\n        ),\n    }\n\n    def mock_response(\n        self,\n        url: str,\n        content_type: str = \"image/jpeg\",\n        file_type: None | str = None,\n    ) -> None:\n        # Potentially return a file of a type that differs from the\n        # server-advertised content type to mimic misbehaving servers.\n        if file_type is None:\n            file_type = content_type\n\n        try:\n            # imghdr reads 32 bytes\n            header = self.IMAGEHEADER[file_type].ljust(32, b\"\\x00\")\n        except KeyError:\n            # If we can't return a file that looks like real file of the requested\n            # type, better fail the test than returning something else, which might\n            # violate assumption made when writing a test.\n            raise AssertionError(f\"Mocking {file_type} responses not supported\")\n\n        responses.add(\n            responses.GET,\n            url,\n            content_type=content_type,\n            body=header,\n        )\n\n\nclass CleanupModulesMixin:\n    modules: ClassVar[tuple[str, ...]]\n\n    @classmethod\n    def tearDownClass(cls) -> None:\n        \"\"\"Remove files created by the plugin.\"\"\"\n        for module in cls.modules:\n            clean_module_tempdir(module)\n"
  },
  {
    "path": "beets/ui/__init__.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"This module contains all of the core logic for beets' command-line\ninterface. To invoke the CLI, just call beets.ui.main(). The actual\nCLI commands are implemented in the ui.commands module.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport errno\nimport optparse\nimport os.path\nimport re\nimport shutil\nimport sqlite3\nimport sys\nimport textwrap\nimport traceback\nfrom functools import cache\nfrom typing import TYPE_CHECKING, Any\n\nimport confuse\n\nfrom beets import config, library, logging, plugins, util\nfrom beets.dbcore import db\nfrom beets.dbcore import query as db_query\nfrom beets.util import as_string\nfrom beets.util.color import colorize\nfrom beets.util.deprecation import deprecate_for_maintainers\nfrom beets.util.diff import get_model_changes\nfrom beets.util.functemplate import template\n\nif TYPE_CHECKING:\n    from collections.abc import Callable, Iterable\n\n\n# On Windows platforms, use colorama to support \"ANSI\" terminal colors.\nif sys.platform == \"win32\":\n    try:\n        import colorama\n    except ImportError:\n        pass\n    else:\n        colorama.init()\n\n\nlog = logging.getLogger(\"beets\")\nif not log.handlers:\n    log.addHandler(logging.StreamHandler())\nlog.propagate = False  # Don't propagate to root handler.\n\n\nPF_KEY_QUERIES = {\n    \"comp\": \"comp:true\",\n    \"singleton\": \"singleton:true\",\n}\n\n\nclass UserError(Exception):\n    \"\"\"UI exception. Commands should throw this in order to display\n    nonrecoverable errors to the user.\n    \"\"\"\n\n\n# Encoding utilities.\n\n\ndef _in_encoding():\n    \"\"\"Get the encoding to use for *inputting* strings from the console.\"\"\"\n    return _stream_encoding(sys.stdin)\n\n\ndef _out_encoding():\n    \"\"\"Get the encoding to use for *outputting* strings to the console.\"\"\"\n    return _stream_encoding(sys.stdout)\n\n\ndef _stream_encoding(stream, default=\"utf-8\"):\n    \"\"\"A helper for `_in_encoding` and `_out_encoding`: get the stream's\n    preferred encoding, using a configured override or a default\n    fallback if neither is not specified.\n    \"\"\"\n    # Configured override?\n    encoding = config[\"terminal_encoding\"].get()\n    if encoding:\n        return encoding\n\n    # For testing: When sys.stdout or sys.stdin is a StringIO under the\n    # test harness, it doesn't have an `encoding` attribute. Just use\n    # UTF-8.\n    if not hasattr(stream, \"encoding\"):\n        return default\n\n    # Python's guessed output stream encoding, or UTF-8 as a fallback\n    # (e.g., when piped to a file).\n    return stream.encoding or default\n\n\ndef decargs(arglist):\n    \"\"\"Given a list of command-line argument bytestrings, attempts to\n    decode them to Unicode strings when running under Python 2.\n\n    .. deprecated:: 2.4.0\n       This function will be removed in 3.0.0.\n    \"\"\"\n    deprecate_for_maintainers(\"'beets.ui.decargs'\")\n    return arglist\n\n\ndef print_(*strings: str, end: str = \"\\n\") -> None:\n    \"\"\"Like print, but rather than raising an error when a character\n    is not in the terminal's encoding's character set, just silently\n    replaces it.\n\n    The `end` keyword argument behaves similarly to the built-in `print`\n    (it defaults to a newline).\n    \"\"\"\n    txt = f\"{' '.join(strings or ('',))}{end}\"\n\n    # Encode the string and write it to stdout.\n    # On Python 3, sys.stdout expects text strings and uses the\n    # exception-throwing encoding error policy. To avoid throwing\n    # errors and use our configurable encoding override, we use the\n    # underlying bytes buffer instead.\n    if hasattr(sys.stdout, \"buffer\"):\n        out = txt.encode(_out_encoding(), \"replace\")\n        sys.stdout.buffer.write(out)\n        sys.stdout.buffer.flush()\n    else:\n        # In our test harnesses (e.g., DummyOut), sys.stdout.buffer\n        # does not exist. We instead just record the text string.\n        sys.stdout.write(txt)\n\n\n# Configuration wrappers.\n\n\ndef _bool_fallback(a, b):\n    \"\"\"Given a boolean or None, return the original value or a fallback.\"\"\"\n    if a is None:\n        assert isinstance(b, bool)\n        return b\n    else:\n        assert isinstance(a, bool)\n        return a\n\n\ndef should_write(write_opt=None):\n    \"\"\"Decide whether a command that updates metadata should also write\n    tags, using the importer configuration as the default.\n    \"\"\"\n    return _bool_fallback(write_opt, config[\"import\"][\"write\"].get(bool))\n\n\ndef should_move(move_opt=None):\n    \"\"\"Decide whether a command that updates metadata should also move\n    files when they're inside the library, using the importer\n    configuration as the default.\n\n    Specifically, commands should move files after metadata updates only\n    when the importer is configured *either* to move *or* to copy files.\n    They should avoid moving files when the importer is configured not\n    to touch any filenames.\n    \"\"\"\n    return _bool_fallback(\n        move_opt,\n        config[\"import\"][\"move\"].get(bool)\n        or config[\"import\"][\"copy\"].get(bool),\n    )\n\n\n# Input prompts.\n\n\ndef input_(prompt=None):\n    \"\"\"Like `input`, but decodes the result to a Unicode string.\n    Raises a UserError if stdin is not available. The prompt is sent to\n    stdout rather than stderr. A printed between the prompt and the\n    input cursor.\n    \"\"\"\n    # raw_input incorrectly sends prompts to stderr, not stdout, so we\n    # use print_() explicitly to display prompts.\n    # https://bugs.python.org/issue1927\n    if prompt:\n        print_(prompt, end=\" \")\n\n    try:\n        resp = input()\n    except EOFError:\n        raise UserError(\"stdin stream ended while input required\")\n\n    return resp\n\n\ndef input_options(\n    options,\n    require=False,\n    prompt=None,\n    fallback_prompt=None,\n    numrange=None,\n    default=None,\n    max_width=72,\n):\n    \"\"\"Prompts a user for input. The sequence of `options` defines the\n    choices the user has. A single-letter shortcut is inferred for each\n    option; the user's choice is returned as that single, lower-case\n    letter. The options should be provided as lower-case strings unless\n    a particular shortcut is desired; in that case, only that letter\n    should be capitalized.\n\n    By default, the first option is the default. `default` can be provided to\n    override this. If `require` is provided, then there is no default. The\n    prompt and fallback prompt are also inferred but can be overridden.\n\n    If numrange is provided, it is a pair of `(high, low)` (both ints)\n    indicating that, in addition to `options`, the user may enter an\n    integer in that inclusive range.\n\n    `max_width` specifies the maximum number of columns in the\n    automatically generated prompt string.\n    \"\"\"\n    # Assign single letters to each option. Also capitalize the options\n    # to indicate the letter.\n    letters = {}\n    display_letters = []\n    capitalized = []\n    first = True\n    for option in options:\n        # Is a letter already capitalized?\n        for letter in option:\n            if letter.isalpha() and letter.upper() == letter:\n                found_letter = letter\n                break\n        else:\n            # Infer a letter.\n            for letter in option:\n                if not letter.isalpha():\n                    continue  # Don't use punctuation.\n                if letter not in letters:\n                    found_letter = letter\n                    break\n            else:\n                raise ValueError(\"no unambiguous lettering found\")\n\n        letters[found_letter.lower()] = option\n        index = option.index(found_letter)\n\n        # Mark the option's shortcut letter for display.\n        if not require and (\n            (default is None and not numrange and first)\n            or (\n                isinstance(default, str)\n                and found_letter.lower() == default.lower()\n            )\n        ):\n            # The first option is the default; mark it.\n            show_letter = f\"[{found_letter.upper()}]\"\n            is_default = True\n        else:\n            show_letter = found_letter.upper()\n            is_default = False\n\n        # Colorize the letter shortcut.\n        show_letter = colorize(\n            \"action_default\" if is_default else \"action\", show_letter\n        )\n\n        # Insert the highlighted letter back into the word.\n        descr_color = \"action_default\" if is_default else \"action_description\"\n        capitalized.append(\n            colorize(descr_color, option[:index])\n            + show_letter\n            + colorize(descr_color, option[index + 1 :])\n        )\n        display_letters.append(found_letter.upper())\n\n        first = False\n\n    # The default is just the first option if unspecified.\n    if require:\n        default = None\n    elif default is None:\n        if numrange:\n            default = numrange[0]\n        else:\n            default = display_letters[0].lower()\n\n    # Make a prompt if one is not provided.\n    if not prompt:\n        prompt_parts = []\n        prompt_part_lengths = []\n        if numrange:\n            if isinstance(default, int):\n                default_name = str(default)\n                default_name = colorize(\"action_default\", default_name)\n                tmpl = \"# selection (default {})\"\n                prompt_parts.append(tmpl.format(default_name))\n                prompt_part_lengths.append(len(tmpl) - 2 + len(str(default)))\n            else:\n                prompt_parts.append(\"# selection\")\n                prompt_part_lengths.append(len(prompt_parts[-1]))\n        prompt_parts += capitalized\n        prompt_part_lengths += [len(s) for s in options]\n\n        # Wrap the query text.\n        # Start prompt with U+279C: Heavy Round-Tipped Rightwards Arrow\n        prompt = colorize(\"action\", \"\\u279c \")\n        line_length = 0\n        for i, (part, length) in enumerate(\n            zip(prompt_parts, prompt_part_lengths)\n        ):\n            # Add punctuation.\n            if i == len(prompt_parts) - 1:\n                part += colorize(\"action_description\", \"?\")\n            else:\n                part += colorize(\"action_description\", \",\")\n            length += 1\n\n            # Choose either the current line or the beginning of the next.\n            if line_length + length + 1 > max_width:\n                prompt += \"\\n\"\n                line_length = 0\n\n            if line_length != 0:\n                # Not the beginning of the line; need a space.\n                part = f\" {part}\"\n                length += 1\n\n            prompt += part\n            line_length += length\n\n    # Make a fallback prompt too. This is displayed if the user enters\n    # something that is not recognized.\n    if not fallback_prompt:\n        fallback_prompt = \"Enter one of \"\n        if numrange:\n            fallback_prompt += \"{}-{}, \".format(*numrange)\n        fallback_prompt += f\"{', '.join(display_letters)}:\"\n\n    resp = input_(prompt)\n    while True:\n        resp = resp.strip().lower()\n\n        # Try default option.\n        if default is not None and not resp:\n            resp = default\n\n        # Try an integer input if available.\n        if numrange:\n            try:\n                resp = int(resp)\n            except ValueError:\n                pass\n            else:\n                low, high = numrange\n                if low <= resp <= high:\n                    return resp\n                else:\n                    resp = None\n\n        # Try a normal letter input.\n        if resp:\n            resp = resp[0]\n            if resp in letters:\n                return resp\n\n        # Prompt for new input.\n        resp = input_(fallback_prompt)\n\n\ndef input_yn(prompt, require=False):\n    \"\"\"Prompts the user for a \"yes\" or \"no\" response. The default is\n    \"yes\" unless `require` is `True`, in which case there is no default.\n    \"\"\"\n    # Start prompt with U+279C: Heavy Round-Tipped Rightwards Arrow\n    yesno = colorize(\"action\", \"\\u279c \") + colorize(\n        \"action_description\", \"Enter Y or N:\"\n    )\n    sel = input_options((\"y\", \"n\"), require, prompt, yesno)\n    return sel == \"y\"\n\n\ndef input_select_objects(prompt, objs, rep, prompt_all=None):\n    \"\"\"Prompt to user to choose all, none, or some of the given objects.\n    Return the list of selected objects.\n\n    `prompt` is the prompt string to use for each question (it should be\n    phrased as an imperative verb). If `prompt_all` is given, it is used\n    instead of `prompt` for the first (yes(/no/select) question.\n    `rep` is a function to call on each object to print it out when confirming\n    objects individually.\n    \"\"\"\n    choice = input_options(\n        (\"y\", \"n\", \"s\"), False, f\"{prompt_all or prompt}? (Yes/no/select)\"\n    )\n    print()  # Blank line.\n\n    if choice == \"y\":  # Yes.\n        return objs\n\n    elif choice == \"s\":  # Select.\n        out = []\n        for obj in objs:\n            rep(obj)\n            answer = input_options(\n                (\"y\", \"n\", \"q\"),\n                True,\n                f\"{prompt}? (yes/no/quit)\",\n                \"Enter Y or N:\",\n            )\n            if answer == \"y\":\n                out.append(obj)\n            elif answer == \"q\":\n                return out\n        return out\n\n    else:  # No.\n        return []\n\n\ndef get_path_formats(subview=None):\n    \"\"\"Get the configuration's path formats as a list of query/template\n    pairs.\n    \"\"\"\n    path_formats = []\n    subview = subview or config[\"paths\"]\n    for query, view in subview.items():\n        query = PF_KEY_QUERIES.get(query, query)  # Expand common queries.\n        path_formats.append((query, template(view.as_str())))\n    return path_formats\n\n\ndef get_replacements():\n    \"\"\"Confuse validation function that reads regex/string pairs.\"\"\"\n    replacements = []\n    for pattern, repl in config[\"replace\"].get(dict).items():\n        repl = repl or \"\"\n        try:\n            replacements.append((re.compile(pattern), repl))\n        except re.error:\n            raise UserError(\n                f\"malformed regular expression in replace: {pattern}\"\n            )\n    return replacements\n\n\n@cache\ndef term_width() -> int:\n    \"\"\"Get the width (columns) of the terminal.\"\"\"\n    columns, _ = shutil.get_terminal_size(fallback=(0, 0))\n    return columns if columns else config[\"ui\"][\"terminal_width\"].get(int)\n\n\ndef show_model_changes(\n    new: library.LibModel,\n    old: library.LibModel | None = None,\n    fields: Iterable[str] | None = None,\n    always: bool = False,\n    print_obj: bool = True,\n) -> bool:\n    \"\"\"Print a diff of changes between two library model states.\n\n    Compares `new` against `old`, falling back to the database version of\n    `new` when `old` is not provided.\n\n    Optionally prints the original object label before listing field-level\n    changes when `print_obj` is enabled. When `always` is set, the object\n    label is printed even if no changes are detected. Returns whether any\n    changes were found.\n    \"\"\"\n    old = old or new.get_fresh_from_db()\n    changes = get_model_changes(new, old, fields)\n\n    # Print changes.\n    if print_obj and (changes or always):\n        print_(format(old))\n    if changes:\n        print_(textwrap.indent(\"\\n\".join(changes), \"  \"))\n\n    return bool(changes)\n\n\n# Helper functions for option parsing.\n\n\nclass CommonOptionsParser(optparse.OptionParser):\n    \"\"\"Offers a simple way to add common formatting options.\n\n    Options available include:\n        - matching albums instead of tracks: add_album_option()\n        - showing paths instead of items/albums: add_path_option()\n        - changing the format of displayed items/albums: add_format_option()\n\n    The last one can have several behaviors:\n        - against a special target\n        - with a certain format\n        - autodetected target with the album option\n\n    Each method is fully documented in the related method.\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self._album_flags = False\n        # this serves both as an indicator that we offer the feature AND allows\n        # us to check whether it has been specified on the CLI - bypassing the\n        # fact that arguments may be in any order\n\n    def add_album_option(self, flags=(\"-a\", \"--album\")):\n        \"\"\"Add a -a/--album option to match albums instead of tracks.\n\n        If used then the format option can auto-detect whether we're setting\n        the format for items or albums.\n        Sets the album property on the options extracted from the CLI.\n        \"\"\"\n        album = optparse.Option(\n            *flags, action=\"store_true\", help=\"match albums instead of tracks\"\n        )\n        self.add_option(album)\n        self._album_flags = set(flags)\n\n    def _set_format(\n        self,\n        option,\n        opt_str,\n        value,\n        parser,\n        target=None,\n        fmt=None,\n        store_true=False,\n    ):\n        \"\"\"Internal callback that sets the correct format while parsing CLI\n        arguments.\n        \"\"\"\n        if store_true:\n            setattr(parser.values, option.dest, True)\n\n        # Use the explicitly specified format, or the string from the option.\n        value = fmt or value or \"\"\n        parser.values.format = value\n\n        if target:\n            config[target._format_config_key].set(value)\n        else:\n            if self._album_flags:\n                if parser.values.album:\n                    target = library.Album\n                else:\n                    # the option is either missing either not parsed yet\n                    if self._album_flags & set(parser.rargs):\n                        target = library.Album\n                    else:\n                        target = library.Item\n                config[target._format_config_key].set(value)\n            else:\n                config[library.Item._format_config_key].set(value)\n                config[library.Album._format_config_key].set(value)\n\n    def add_path_option(self, flags=(\"-p\", \"--path\")):\n        \"\"\"Add a -p/--path option to display the path instead of the default\n        format.\n\n        By default this affects both items and albums. If add_album_option()\n        is used then the target will be autodetected.\n\n        Sets the format property to '$path' on the options extracted from the\n        CLI.\n        \"\"\"\n        path = optparse.Option(\n            *flags,\n            nargs=0,\n            action=\"callback\",\n            callback=self._set_format,\n            callback_kwargs={\"fmt\": \"$path\", \"store_true\": True},\n            help=\"print paths for matched items or albums\",\n        )\n        self.add_option(path)\n\n    def add_format_option(self, flags=(\"-f\", \"--format\"), target=None):\n        \"\"\"Add -f/--format option to print some LibModel instances with a\n        custom format.\n\n        `target` is optional and can be one of ``library.Item``, 'item',\n        ``library.Album`` and 'album'.\n\n        Several behaviors are available:\n            - if `target` is given then the format is only applied to that\n            LibModel\n            - if the album option is used then the target will be autodetected\n            - otherwise the format is applied to both items and albums.\n\n        Sets the format property on the options extracted from the CLI.\n        \"\"\"\n        kwargs = {}\n        if target:\n            if isinstance(target, str):\n                target = {\"item\": library.Item, \"album\": library.Album}[target]\n            kwargs[\"target\"] = target\n\n        opt = optparse.Option(\n            *flags,\n            action=\"callback\",\n            callback=self._set_format,\n            callback_kwargs=kwargs,\n            help=\"print with custom format\",\n        )\n        self.add_option(opt)\n\n    def add_all_common_options(self):\n        \"\"\"Add album, path and format options.\"\"\"\n        self.add_album_option()\n        self.add_path_option()\n        self.add_format_option()\n\n\n# Subcommand parsing infrastructure.\n#\n# This is a fairly generic subcommand parser for optparse. It is\n# maintained externally here:\n# https://gist.github.com/462717\n# There you will also find a better description of the code and a more\n# succinct example program.\n\n\nclass Subcommand:\n    \"\"\"A subcommand of a root command-line application that may be\n    invoked by a SubcommandOptionParser.\n    \"\"\"\n\n    func: Callable[[library.Library, optparse.Values, list[str]], Any]\n\n    def __init__(self, name, parser=None, help=\"\", aliases=(), hide=False):\n        \"\"\"Creates a new subcommand. name is the primary way to invoke\n        the subcommand; aliases are alternate names. parser is an\n        OptionParser responsible for parsing the subcommand's options.\n        help is a short description of the command. If no parser is\n        given, it defaults to a new, empty CommonOptionsParser.\n        \"\"\"\n        self.name = name\n        self.parser = parser or CommonOptionsParser()\n        self.aliases = aliases\n        self.help = help\n        self.hide = hide\n        self._root_parser = None\n\n    def print_help(self):\n        self.parser.print_help()\n\n    def parse_args(self, args):\n        return self.parser.parse_args(args)\n\n    @property\n    def root_parser(self):\n        return self._root_parser\n\n    @root_parser.setter\n    def root_parser(self, root_parser):\n        self._root_parser = root_parser\n        self.parser.prog = (\n            f\"{as_string(root_parser.get_prog_name())} {self.name}\"\n        )\n\n\nclass SubcommandsOptionParser(CommonOptionsParser):\n    \"\"\"A variant of OptionParser that parses subcommands and their\n    arguments.\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        \"\"\"Create a new subcommand-aware option parser. All of the\n        options to OptionParser.__init__ are supported in addition\n        to subcommands, a sequence of Subcommand objects.\n        \"\"\"\n        # A more helpful default usage.\n        if \"usage\" not in kwargs:\n            kwargs[\"usage\"] = \"\"\"\n  %prog COMMAND [ARGS...]\n  %prog help COMMAND\"\"\"\n        kwargs[\"add_help_option\"] = False\n\n        # Super constructor.\n        super().__init__(*args, **kwargs)\n\n        # Our root parser needs to stop on the first unrecognized argument.\n        self.disable_interspersed_args()\n\n        self.subcommands = []\n\n    def add_subcommand(self, *cmds):\n        \"\"\"Adds a Subcommand object to the parser's list of commands.\"\"\"\n        for cmd in cmds:\n            cmd.root_parser = self\n            self.subcommands.append(cmd)\n\n    # Add the list of subcommands to the help message.\n    def format_help(self, formatter=None):\n        # Get the original help message, to which we will append.\n        out = super().format_help(formatter)\n        if formatter is None:\n            formatter = self.formatter\n\n        # Subcommands header.\n        result = [\"\\n\"]\n        result.append(formatter.format_heading(\"Commands\"))\n        formatter.indent()\n\n        # Generate the display names (including aliases).\n        # Also determine the help position.\n        disp_names = []\n        help_position = 0\n        subcommands = [c for c in self.subcommands if not c.hide]\n        subcommands.sort(key=lambda c: c.name)\n        for subcommand in subcommands:\n            name = subcommand.name\n            if subcommand.aliases:\n                name += f\" ({', '.join(subcommand.aliases)})\"\n            disp_names.append(name)\n\n            # Set the help position based on the max width.\n            proposed_help_position = len(name) + formatter.current_indent + 2\n            if proposed_help_position <= formatter.max_help_position:\n                help_position = max(help_position, proposed_help_position)\n\n        # Add each subcommand to the output.\n        for subcommand, name in zip(subcommands, disp_names):\n            # Lifted directly from optparse.py.\n            name_width = help_position - formatter.current_indent - 2\n            if len(name) > name_width:\n                name = f\"{' ' * formatter.current_indent}{name}\\n\"\n                indent_first = help_position\n            else:\n                name = f\"{' ' * formatter.current_indent}{name:<{name_width}}\\n\"\n                indent_first = 0\n            result.append(name)\n            help_width = formatter.width - help_position\n            help_lines = textwrap.wrap(subcommand.help, help_width)\n            help_line = help_lines[0] if help_lines else \"\"\n            result.append(f\"{' ' * indent_first}{help_line}\\n\")\n            result.extend(\n                [f\"{' ' * help_position}{line}\\n\" for line in help_lines[1:]]\n            )\n        formatter.dedent()\n\n        # Concatenate the original help message with the subcommand\n        # list.\n        return f\"{out}{''.join(result)}\"\n\n    def _subcommand_for_name(self, name):\n        \"\"\"Return the subcommand in self.subcommands matching the\n        given name. The name may either be the name of a subcommand or\n        an alias. If no subcommand matches, returns None.\n        \"\"\"\n        for subcommand in self.subcommands:\n            if name == subcommand.name or name in subcommand.aliases:\n                return subcommand\n        return None\n\n    def parse_global_options(self, args):\n        \"\"\"Parse options up to the subcommand argument. Returns a tuple\n        of the options object and the remaining arguments.\n        \"\"\"\n        options, subargs = self.parse_args(args)\n\n        # Force the help command\n        if options.help:\n            subargs = [\"help\"]\n        elif options.version:\n            subargs = [\"version\"]\n        return options, subargs\n\n    def parse_subcommand(self, args):\n        \"\"\"Given the `args` left unused by a `parse_global_options`,\n        return the invoked subcommand, the subcommand options, and the\n        subcommand arguments.\n        \"\"\"\n        # Help is default command\n        if not args:\n            args = [\"help\"]\n\n        cmdname = args.pop(0)\n        subcommand = self._subcommand_for_name(cmdname)\n        if not subcommand:\n            raise UserError(f\"unknown command '{cmdname}'\")\n\n        suboptions, subargs = subcommand.parse_args(args)\n        return subcommand, suboptions, subargs\n\n\noptparse.Option.ALWAYS_TYPED_ACTIONS += (\"callback\",)\n\n\n# The main entry point and bootstrapping.\n\n\ndef _setup(\n    options: optparse.Values, lib: library.Library | None\n) -> tuple[list[Subcommand], library.Library]:\n    \"\"\"Prepare and global state and updates it with command line options.\n\n    Returns a list of subcommands, a list of plugins, and a library instance.\n    \"\"\"\n    config = _configure(options)\n\n    plugins.load_plugins()\n\n    # Get the default subcommands.\n    from beets.ui.commands import default_commands\n\n    subcommands = list(default_commands)\n    subcommands.extend(plugins.commands())\n\n    if lib is None:\n        lib = _open_library(config)\n        plugins.send(\"library_opened\", lib=lib)\n\n    return subcommands, lib\n\n\ndef _configure(options):\n    \"\"\"Amend the global configuration object with command line options.\"\"\"\n    # Add any additional config files specified with --config. This\n    # special handling lets specified plugins get loaded before we\n    # finish parsing the command line.\n    if getattr(options, \"config\", None) is not None:\n        overlay_path = options.config\n        del options.config\n        config.set_file(overlay_path)\n    else:\n        overlay_path = None\n    config.set_args(options)\n\n    # Configure the logger.\n    if config[\"verbose\"].get(int):\n        log.set_global_level(logging.DEBUG)\n    else:\n        log.set_global_level(logging.INFO)\n\n    if overlay_path:\n        log.debug(\n            \"overlaying configuration: {}\", util.displayable_path(overlay_path)\n        )\n\n    config_path = config.user_config_path()\n    if os.path.isfile(config_path):\n        log.debug(\"user configuration: {}\", util.displayable_path(config_path))\n    else:\n        log.debug(\n            \"no user configuration found at {}\",\n            util.displayable_path(config_path),\n        )\n\n    log.debug(\"data directory: {}\", util.displayable_path(config.config_dir()))\n    return config\n\n\ndef _ensure_db_directory_exists(path):\n    if path == b\":memory:\":  # in memory db\n        return\n    newpath = os.path.dirname(path)\n    if not os.path.isdir(newpath):\n        if input_yn(\n            f\"The database directory {util.displayable_path(newpath)} does not\"\n            \" exist. Create it (Y/n)?\"\n        ):\n            os.makedirs(newpath)\n\n\ndef _open_library(config: confuse.LazyConfig) -> library.Library:\n    \"\"\"Create a new library instance from the configuration.\"\"\"\n    dbpath = util.bytestring_path(config[\"library\"].as_filename())\n    _ensure_db_directory_exists(dbpath)\n    try:\n        lib = library.Library(\n            dbpath,\n            config[\"directory\"].as_filename(),\n            get_path_formats(),\n            get_replacements(),\n        )\n        lib.get_item(0)  # Test database connection.\n    except (sqlite3.OperationalError, sqlite3.DatabaseError) as db_error:\n        log.debug(\"{}\", traceback.format_exc())\n        raise UserError(\n            f\"database file {util.displayable_path(dbpath)} cannot not be\"\n            f\" opened: {db_error}\"\n        )\n    log.debug(\n        \"library database: {}\\nlibrary directory: {}\",\n        util.displayable_path(lib.path),\n        util.displayable_path(lib.directory),\n    )\n    return lib\n\n\ndef _raw_main(args: list[str], lib=None) -> None:\n    \"\"\"A helper function for `main` without top-level exception\n    handling.\n    \"\"\"\n    parser = SubcommandsOptionParser()\n    parser.add_format_option(flags=(\"--format-item\",), target=library.Item)\n    parser.add_format_option(flags=(\"--format-album\",), target=library.Album)\n    parser.add_option(\n        \"-l\", \"--library\", dest=\"library\", help=\"library database file to use\"\n    )\n    parser.add_option(\n        \"-d\",\n        \"--directory\",\n        dest=\"directory\",\n        help=\"destination music directory\",\n    )\n    parser.add_option(\n        \"-v\",\n        \"--verbose\",\n        dest=\"verbose\",\n        action=\"count\",\n        help=\"log more details (use twice for even more)\",\n    )\n    parser.add_option(\n        \"-c\", \"--config\", dest=\"config\", help=\"path to configuration file\"\n    )\n\n    def parse_csl_callback(\n        option: optparse.Option, _, value: str, parser: SubcommandsOptionParser\n    ):\n        \"\"\"Parse a comma-separated list of values.\"\"\"\n        setattr(\n            parser.values,\n            option.dest,  # type: ignore[arg-type]\n            list(filter(None, value.split(\",\"))),\n        )\n\n    parser.add_option(\n        \"-p\",\n        \"--plugins\",\n        dest=\"plugins\",\n        action=\"callback\",\n        callback=parse_csl_callback,\n        help=\"a comma-separated list of plugins to load\",\n    )\n    parser.add_option(\n        \"-P\",\n        \"--disable-plugins\",\n        dest=\"disabled_plugins\",\n        action=\"callback\",\n        callback=parse_csl_callback,\n        help=\"a comma-separated list of plugins to disable\",\n    )\n    parser.add_option(\n        \"-h\",\n        \"--help\",\n        dest=\"help\",\n        action=\"store_true\",\n        help=\"show this help message and exit\",\n    )\n    parser.add_option(\n        \"--version\",\n        dest=\"version\",\n        action=\"store_true\",\n        help=optparse.SUPPRESS_HELP,\n    )\n\n    options, subargs = parser.parse_global_options(args)\n\n    # Special case for the `config --edit` command: bypass _setup so\n    # that an invalid configuration does not prevent the editor from\n    # starting.\n    if (\n        subargs\n        and subargs[0] == \"config\"\n        and (\"-e\" in subargs or \"--edit\" in subargs)\n    ):\n        from beets.ui.commands.config import config_edit\n\n        return config_edit(options)\n\n    test_lib = bool(lib)\n    subcommands, lib = _setup(options, lib)\n    parser.add_subcommand(*subcommands)\n\n    subcommand, suboptions, subargs = parser.parse_subcommand(subargs)\n    subcommand.func(lib, suboptions, subargs)\n\n    plugins.send(\"cli_exit\", lib=lib)\n    if not test_lib:\n        # Clean up the library unless it came from the test harness.\n        lib._close()\n\n\ndef main(args=None):\n    \"\"\"Run the main command-line interface for beets. Includes top-level\n    exception handlers that print friendly error messages.\n    \"\"\"\n    if \"AppData\\\\Local\\\\Microsoft\\\\WindowsApps\" in sys.exec_prefix:\n        log.error(\n            \"error: beets is unable to use the Microsoft Store version of \"\n            \"Python. Please install Python from https://python.org.\\n\"\n            \"error: More details can be found here \"\n            \"https://beets.readthedocs.io/en/stable/guides/main.html\"\n        )\n        sys.exit(1)\n    try:\n        _raw_main(args)\n    except UserError as exc:\n        message = exc.args[0] if exc.args else None\n        log.error(\"error: {}\", message)\n        sys.exit(1)\n    except util.HumanReadableError as exc:\n        exc.log(log)\n        sys.exit(1)\n    except library.FileOperationError as exc:\n        # These errors have reasonable human-readable descriptions, but\n        # we still want to log their tracebacks for debugging.\n        log.debug(\"{}\", traceback.format_exc())\n        log.error(\"{}\", exc)\n        sys.exit(1)\n    except confuse.ConfigError as exc:\n        log.error(\"configuration error: {}\", exc)\n        sys.exit(1)\n    except db_query.InvalidQueryError as exc:\n        log.error(\"invalid query: {}\", exc)\n        sys.exit(1)\n    except OSError as exc:\n        if exc.errno == errno.EPIPE:\n            # \"Broken pipe\". End silently.\n            sys.stderr.close()\n        else:\n            raise\n    except KeyboardInterrupt:\n        # Silently ignore ^C except in verbose mode.\n        log.debug(\"{}\", traceback.format_exc())\n    except db.DBAccessError as exc:\n        log.error(\n            \"database access error: {}\\n\"\n            \"the library file might have a permissions problem\",\n            exc,\n        )\n        sys.exit(1)\n"
  },
  {
    "path": "beets/ui/commands/__init__.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"This module provides the default commands for beets' command-line\ninterface.\n\"\"\"\n\nfrom beets.util.deprecation import deprecate_imports\n\nfrom .completion import completion_cmd\nfrom .config import config_cmd\nfrom .fields import fields_cmd\nfrom .help import HelpCommand\nfrom .import_ import import_cmd\nfrom .list import list_cmd\nfrom .modify import modify_cmd\nfrom .move import move_cmd\nfrom .remove import remove_cmd\nfrom .stats import stats_cmd\nfrom .update import update_cmd\nfrom .version import version_cmd\nfrom .write import write_cmd\n\n\ndef __getattr__(name: str):\n    \"\"\"Handle deprecated imports.\"\"\"\n    return deprecate_imports(\n        __name__,\n        {\n            \"TerminalImportSession\": \"beets.ui.commands.import_.session\",\n            \"PromptChoice\": \"beets.util\",\n        },\n        name,\n    )\n\n\n# The list of default subcommands. This is populated with Subcommand\n# objects that can be fed to a SubcommandsOptionParser.\ndefault_commands = [\n    fields_cmd,\n    HelpCommand(),\n    import_cmd,\n    list_cmd,\n    update_cmd,\n    remove_cmd,\n    stats_cmd,\n    version_cmd,\n    modify_cmd,\n    move_cmd,\n    write_cmd,\n    config_cmd,\n    completion_cmd,\n]\n\n\n__all__ = [\"default_commands\"]\n"
  },
  {
    "path": "beets/ui/commands/completion.py",
    "content": "\"\"\"The 'completion' command: print shell script for command line completion.\"\"\"\n\nimport os\nimport re\n\nfrom beets import library, logging, plugins, ui\nfrom beets.util import syspath\n\n# Global logger.\nlog = logging.getLogger(\"beets\")\n\n\ndef print_completion(*args):\n    from beets.ui.commands import default_commands\n\n    for line in completion_script(default_commands + plugins.commands()):\n        ui.print_(line, end=\"\")\n    if not any(os.path.isfile(syspath(p)) for p in BASH_COMPLETION_PATHS):\n        log.warning(\n            \"Warning: Unable to find the bash-completion package. \"\n            \"Command line completion might not work.\"\n        )\n\n\ncompletion_cmd = ui.Subcommand(\n    \"completion\",\n    help=\"print shell script that provides command line completion\",\n)\ncompletion_cmd.func = print_completion\ncompletion_cmd.hide = True\n\n\nBASH_COMPLETION_PATHS = [\n    b\"/etc/bash_completion\",\n    b\"/usr/share/bash-completion/bash_completion\",\n    b\"/usr/local/share/bash-completion/bash_completion\",\n    # SmartOS\n    b\"/opt/local/share/bash-completion/bash_completion\",\n    # Homebrew (before bash-completion2)\n    b\"/usr/local/etc/bash_completion\",\n]\n\n\ndef completion_script(commands):\n    \"\"\"Yield the full completion shell script as strings.\n\n    ``commands`` is alist of ``ui.Subcommand`` instances to generate\n    completion data for.\n    \"\"\"\n    base_script = os.path.join(\n        os.path.dirname(__file__), \"./completion_base.sh\"\n    )\n    with open(base_script) as base_script:\n        yield base_script.read()\n\n    options = {}\n    aliases = {}\n    command_names = []\n\n    # Collect subcommands\n    for cmd in commands:\n        name = cmd.name\n        command_names.append(name)\n\n        for alias in cmd.aliases:\n            if re.match(r\"^\\w+$\", alias):\n                aliases[alias] = name\n\n        options[name] = {\"flags\": [], \"opts\": []}\n        for opts in cmd.parser._get_all_options()[1:]:\n            if opts.action in (\"store_true\", \"store_false\"):\n                option_type = \"flags\"\n            else:\n                option_type = \"opts\"\n\n            options[name][option_type].extend(\n                opts._short_opts + opts._long_opts\n            )\n\n    # Add global options\n    options[\"_global\"] = {\n        \"flags\": [\"-v\", \"--verbose\"],\n        \"opts\": \"-l --library -c --config -d --directory -h --help\".split(\" \"),\n    }\n\n    # Add flags common to all commands\n    options[\"_common\"] = {\"flags\": [\"-h\", \"--help\"]}\n\n    # Start generating the script\n    yield \"_beet() {\\n\"\n\n    # Command names\n    yield f\"  local commands={' '.join(command_names)!r}\\n\"\n    yield \"\\n\"\n\n    # Command aliases\n    yield f\"  local aliases={' '.join(aliases.keys())!r}\\n\"\n    for alias, cmd in aliases.items():\n        yield f\"  local alias__{alias.replace('-', '_')}={cmd}\\n\"\n    yield \"\\n\"\n\n    # Fields\n    fields = library.Item._fields.keys() | library.Album._fields.keys()\n    yield f\"  fields={' '.join(fields)!r}\\n\"\n\n    # Command options\n    for cmd, opts in options.items():\n        for option_type, option_list in opts.items():\n            if option_list:\n                option_list = \" \".join(option_list)\n                yield (\n                    \"  local\"\n                    f\" {option_type}__{cmd.replace('-', '_')}='{option_list}'\\n\"\n                )\n\n    yield \"  _beet_dispatch\\n\"\n    yield \"}\\n\"\n"
  },
  {
    "path": "beets/ui/commands/completion_base.sh",
    "content": "# This file is part of beets.\n# Copyright (c) 2014, Thomas Scholtes.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\n\n# Completion for the `beet` command\n# =================================\n#\n# Load this script to complete beets subcommands, options, and\n# queries.\n#\n# If a beets command is found on the command line it completes filenames and\n# the subcommand's options. Otherwise it will complete global options and\n# subcommands. If the previous option on the command line expects an argument,\n# it also completes filenames or directories.  Options are only\n# completed if '-' has already been typed on the command line.\n#\n# Note that completion of plugin commands only works for those plugins\n# that were enabled when running `beet completion`. It does not check\n# plugins dynamically\n#\n# Currently, only Bash 3.2 and newer is supported and the\n# `bash-completion` package (v2.8 or newer) is required.\n#\n# TODO\n# ----\n#\n# * There are some issues with arguments that are quoted on the command line.\n#\n# * Complete arguments for the `--format` option by expanding field variables.\n#\n#     beet ls -f \"$tit[TAB]\n#     beet ls -f \"$title\n#\n# * Support long options with `=`, e.g. `--config=file`. Debian's bash\n#   completion package can handle this.\n#\n# Note that 'bash-completion' v2.8 is a part of Debian 10, which is part of\n# LTS until 2024-06-30.  After this date, the minimum version requirement can\n# be changed, and newer features can be used unconditionally.  See PR#5301.\n#\n\nif [[ ${BASH_COMPLETION_VERSINFO[0]} -ne 2 \\\n   || ${BASH_COMPLETION_VERSINFO[1]} -lt 8 ]]; then\n  echo \"Incompatible version of 'bash-completion'!\"\n  return 1\nfi\n\n# The later code relies on 'bash-completion' version 2.12, but older versions\n# are still supported.  Here, we provide implementations of the newer functions\n# in terms of older ones, if 'bash-completion' is too old to have them.\n\nif [[ ${BASH_COMPLETION_VERSINFO[1]} -lt 12 ]]; then\n  _comp_get_words() {\n    _get_comp_words_by_ref \"$@\"\n  }\n\n  _comp_compgen_filedir() {\n    _filedir \"$@\"\n  }\nfi\n\n# Determines the beets subcommand and dispatches the completion\n# accordingly.\n_beet_dispatch() {\n  local cur prev cmd=\n\n  COMPREPLY=()\n  _comp_get_words -n : cur prev\n\n  # Look for the beets subcommand\n  local arg\n  for (( i=1; i < COMP_CWORD; i++ )); do\n      arg=\"${COMP_WORDS[i]}\"\n      if _list_include_item \"${opts___global}\" $arg; then\n        ((i++))\n      elif [[ \"$arg\" != -* ]]; then\n        cmd=\"$arg\"\n        break\n      fi\n  done\n\n  # Replace command shortcuts\n  if [[ -n $cmd ]] && _list_include_item \"$aliases\" \"$cmd\"; then\n    eval \"cmd=\\$alias__${cmd//-/_}\"\n  fi\n\n  case $cmd in\n    help)\n      COMPREPLY+=( $(compgen -W \"$commands\" -- $cur) )\n      ;;\n    list|remove|move|update|write|stats)\n      _beet_complete_query\n      ;;\n    \"\")\n      _beet_complete_global\n      ;;\n    *)\n      _beet_complete\n      ;;\n  esac\n}\n\n\n# Adds option and file completion to COMPREPLY for the subcommand $cmd\n_beet_complete() {\n  if [[ $cur == -* ]]; then\n    local opts flags completions\n    eval \"opts=\\$opts__${cmd//-/_}\"\n    eval \"flags=\\$flags__${cmd//-/_}\"\n    completions=\"${flags___common} ${opts} ${flags}\"\n    COMPREPLY+=( $(compgen -W \"$completions\"  -- $cur) )\n  else\n    _comp_compgen_filedir\n  fi\n}\n\n\n# Add global options and subcommands to the completion\n_beet_complete_global() {\n  case $prev in\n    -h|--help)\n      # Complete commands\n      COMPREPLY+=( $(compgen -W \"$commands\" -- $cur) )\n      return\n      ;;\n    -l|--library|-c|--config)\n      # Filename completion\n      _comp_compgen_filedir\n      return\n      ;;\n    -d|--directory)\n      # Directory completion\n      _comp_compgen_filedir -d\n      return\n      ;;\n  esac\n\n  if [[ $cur == -* ]]; then\n    local completions=\"$opts___global $flags___global\"\n    COMPREPLY+=( $(compgen -W \"$completions\" -- $cur) )\n  elif [[ -n $cur ]] && _list_include_item \"$aliases\" \"$cur\"; then\n    local cmd\n    eval \"cmd=\\$alias__${cur//-/_}\"\n    COMPREPLY+=( \"$cmd\" )\n  else\n    COMPREPLY+=( $(compgen -W \"$commands\" -- $cur) )\n  fi\n}\n\n_beet_complete_query() {\n  local opts\n  eval \"opts=\\$opts__${cmd//-/_}\"\n\n  if [[ $cur == -* ]] || _list_include_item \"$opts\" \"$prev\"; then\n    _beet_complete\n  elif [[ $cur != \\'* && $cur != \\\"* &&\n          $cur != *:* ]]; then\n    # Do not complete quoted queries or those who already have a field\n    # set.\n    compopt -o nospace\n    COMPREPLY+=( $(compgen -S : -W \"$fields\" -- $cur) )\n    return 0\n  fi\n}\n\n# Returns true if the space separated list $1 includes $2\n_list_include_item() {\n  [[ \" $1 \" == *[[:space:]]$2[[:space:]]* ]]\n}\n\n# This is where beets dynamically adds the _beet function. This\n# function sets the variables $flags, $opts, $commands, and $aliases.\ncomplete -o filenames -F _beet beet\n"
  },
  {
    "path": "beets/ui/commands/config.py",
    "content": "\"\"\"The 'config' command: show and edit user configuration.\"\"\"\n\nimport os\n\nfrom beets import config, ui\nfrom beets.util import displayable_path, editor_command, interactive_open\n\n\ndef config_func(lib, opts, args):\n    # Make sure lazy configuration is loaded\n    config.resolve()\n\n    # Print paths.\n    if opts.paths:\n        filenames = []\n        for source in config.sources:\n            if not opts.defaults and source.default:\n                continue\n            if source.filename:\n                filenames.append(source.filename)\n\n        # In case the user config file does not exist, prepend it to the\n        # list.\n        user_path = config.user_config_path()\n        if user_path not in filenames:\n            filenames.insert(0, user_path)\n\n        for filename in filenames:\n            ui.print_(displayable_path(filename))\n\n    # Open in editor.\n    elif opts.edit:\n        # Note:  This branch *should* be unreachable\n        # since the normal flow should be short-circuited\n        # by the special case in ui._raw_main\n        config_edit(opts)\n\n    # Dump configuration.\n    else:\n        config_out = config.dump(full=opts.defaults, redact=opts.redact)\n        if config_out.strip() != \"{}\":\n            ui.print_(config_out)\n        else:\n            print(\"Empty configuration\")\n\n\ndef config_edit(cli_options):\n    \"\"\"Open a program to edit the user configuration.\n    An empty config file is created if no existing config file exists.\n    \"\"\"\n    path = cli_options.config or config.user_config_path()\n    editor = editor_command()\n\n    if not editor:\n        raise ui.UserError(\n            \"Please set the VISUAL or EDITOR environment variable to edit\"\n            \" configuration.\"\n        )\n    try:\n        if not os.path.isfile(path):\n            open(path, \"w+\").close()\n        interactive_open([path], editor)\n    except FileNotFoundError:\n        raise ui.UserError(f\"Editor {editor!r} not found.\")\n    except OSError as exc:\n        raise ui.UserError(f\"Could not edit configuration: {exc}\")\n\n\nconfig_cmd = ui.Subcommand(\"config\", help=\"show or edit the user configuration\")\nconfig_cmd.parser.add_option(\n    \"-p\",\n    \"--paths\",\n    action=\"store_true\",\n    help=\"show files that configuration was loaded from\",\n)\nconfig_cmd.parser.add_option(\n    \"-e\",\n    \"--edit\",\n    action=\"store_true\",\n    help=\"edit user configuration with $VISUAL (or $EDITOR)\",\n)\nconfig_cmd.parser.add_option(\n    \"-d\",\n    \"--defaults\",\n    action=\"store_true\",\n    help=\"include the default configuration\",\n)\nconfig_cmd.parser.add_option(\n    \"-c\",\n    \"--clear\",\n    action=\"store_false\",\n    dest=\"redact\",\n    default=True,\n    help=\"do not redact sensitive fields\",\n)\nconfig_cmd.func = config_func\n"
  },
  {
    "path": "beets/ui/commands/fields.py",
    "content": "\"\"\"The `fields` command: show available fields for queries and format strings.\"\"\"\n\nimport textwrap\n\nfrom beets import library, ui\n\n\ndef _print_keys(query):\n    \"\"\"Given a SQLite query result, print the `key` field of each\n    returned row, with indentation of 2 spaces.\n    \"\"\"\n    for row in query:\n        ui.print_(f\"  {row['key']}\")\n\n\ndef fields_func(lib, opts, args):\n    def _print_rows(names):\n        names.sort()\n        ui.print_(textwrap.indent(\"\\n\".join(names), \"  \"))\n\n    ui.print_(\"Item fields:\")\n    _print_rows(library.Item.all_keys())\n\n    ui.print_(\"Album fields:\")\n    _print_rows(library.Album.all_keys())\n\n    with lib.transaction() as tx:\n        # The SQL uses the DISTINCT to get unique values from the query\n        unique_fields = \"SELECT DISTINCT key FROM ({})\"\n\n        ui.print_(\"Item flexible attributes:\")\n        _print_keys(tx.query(unique_fields.format(library.Item._flex_table)))\n\n        ui.print_(\"Album flexible attributes:\")\n        _print_keys(tx.query(unique_fields.format(library.Album._flex_table)))\n\n\nfields_cmd = ui.Subcommand(\n    \"fields\", help=\"show fields available for queries and format strings\"\n)\nfields_cmd.func = fields_func\n"
  },
  {
    "path": "beets/ui/commands/help.py",
    "content": "\"\"\"The 'help' command: show help information for commands.\"\"\"\n\nfrom beets import ui\n\n\nclass HelpCommand(ui.Subcommand):\n    def __init__(self):\n        super().__init__(\n            \"help\",\n            aliases=(\"?\",),\n            help=\"give detailed help on a specific sub-command\",\n        )\n\n    def func(self, lib, opts, args):\n        if args:\n            cmdname = args[0]\n            helpcommand = self.root_parser._subcommand_for_name(cmdname)\n            if not helpcommand:\n                raise ui.UserError(f\"unknown command '{cmdname}'\")\n            helpcommand.print_help()\n        else:\n            self.root_parser.print_help()\n"
  },
  {
    "path": "beets/ui/commands/import_/__init__.py",
    "content": "\"\"\"The `import` command: import new music into the library.\"\"\"\n\nimport os\n\nfrom beets import config, logging, plugins, ui\nfrom beets.util import displayable_path, normpath, syspath\n\nfrom .session import TerminalImportSession\n\n# Global logger.\nlog = logging.getLogger(\"beets\")\n\n\ndef paths_from_logfile(path):\n    \"\"\"Parse the logfile and yield skipped paths to pass to the `import`\n    command.\n    \"\"\"\n    with open(path, encoding=\"utf-8\") as fp:\n        for i, line in enumerate(fp, start=1):\n            verb, sep, paths = line.rstrip(\"\\n\").partition(\" \")\n            if not sep:\n                raise ValueError(f\"line {i} is invalid\")\n\n            # Ignore informational lines that don't need to be re-imported.\n            if verb in {\"import\", \"duplicate-keep\", \"duplicate-replace\"}:\n                continue\n\n            if verb not in {\"asis\", \"skip\", \"duplicate-skip\"}:\n                raise ValueError(f\"line {i} contains unknown verb {verb}\")\n\n            yield os.path.commonpath(paths.split(\"; \"))\n\n\ndef parse_logfiles(logfiles):\n    \"\"\"Parse all `logfiles` and yield paths from it.\"\"\"\n    for logfile in logfiles:\n        try:\n            yield from paths_from_logfile(syspath(normpath(logfile)))\n        except ValueError as err:\n            raise ui.UserError(\n                f\"malformed logfile {displayable_path(logfile)}: {err}\"\n            ) from err\n        except OSError as err:\n            raise ui.UserError(\n                f\"unreadable logfile {displayable_path(logfile)}: {err}\"\n            ) from err\n\n\ndef import_files(lib, paths: list[bytes], query):\n    \"\"\"Import the files in the given list of paths or matching the\n    query.\n    \"\"\"\n    # Check parameter consistency.\n    if config[\"import\"][\"quiet\"] and config[\"import\"][\"timid\"]:\n        raise ui.UserError(\"can't be both quiet and timid\")\n\n    # Open the log.\n    if config[\"import\"][\"log\"].get() is not None:\n        logpath = syspath(config[\"import\"][\"log\"].as_filename())\n        try:\n            loghandler = logging.FileHandler(logpath, encoding=\"utf-8\")\n        except OSError:\n            raise ui.UserError(\n                \"Could not open log file for writing:\"\n                f\" {displayable_path(logpath)}\"\n            )\n    else:\n        loghandler = None\n\n    # Never ask for input in quiet mode.\n    if config[\"import\"][\"resume\"].get() == \"ask\" and config[\"import\"][\"quiet\"]:\n        config[\"import\"][\"resume\"] = False\n\n    session = TerminalImportSession(lib, loghandler, paths, query)\n    session.run()\n\n    # Emit event.\n    plugins.send(\"import\", lib=lib, paths=paths)\n\n\ndef import_func(lib, opts, args: list[str]):\n    config[\"import\"].set_args(opts)\n\n    # Special case: --copy flag suppresses import_move (which would\n    # otherwise take precedence).\n    if opts.copy:\n        config[\"import\"][\"move\"] = False\n\n    if opts.library:\n        query = args\n        byte_paths = []\n    else:\n        query = None\n        paths = args\n\n        # The paths from the logfiles go into a separate list to allow handling\n        # errors differently from user-specified paths.\n        paths_from_logfiles = list(parse_logfiles(opts.from_logfiles or []))\n\n        if not paths and not paths_from_logfiles:\n            raise ui.UserError(\"no path specified\")\n\n        byte_paths = [os.fsencode(p) for p in paths]\n        paths_from_logfiles = [os.fsencode(p) for p in paths_from_logfiles]\n\n        # Check the user-specified directories.\n        for path in byte_paths:\n            if not os.path.exists(syspath(normpath(path))):\n                raise ui.UserError(\n                    f\"no such file or directory: {displayable_path(path)}\"\n                )\n\n        # Check the directories from the logfiles, but don't throw an error in\n        # case those paths don't exist. Maybe some of those paths have already\n        # been imported and moved separately, so logging a warning should\n        # suffice.\n        for path in paths_from_logfiles:\n            if not os.path.exists(syspath(normpath(path))):\n                log.warning(\n                    \"No such file or directory: {}\", displayable_path(path)\n                )\n                continue\n\n            byte_paths.append(path)\n\n        # If all paths were read from a logfile, and none of them exist, throw\n        # an error\n        if not byte_paths:\n            raise ui.UserError(\"none of the paths are importable\")\n\n    import_files(lib, byte_paths, query)\n\n\ndef _store_dict(option, opt_str, value, parser):\n    \"\"\"Custom action callback to parse options which have ``key=value``\n    pairs as values. All such pairs passed for this option are\n    aggregated into a dictionary.\n    \"\"\"\n    dest = option.dest\n    option_values = getattr(parser.values, dest, None)\n\n    if option_values is None:\n        # This is the first supplied ``key=value`` pair of option.\n        # Initialize empty dictionary and get a reference to it.\n        setattr(parser.values, dest, {})\n        option_values = getattr(parser.values, dest)\n\n    try:\n        key, value = value.split(\"=\", 1)\n        if not (key and value):\n            raise ValueError\n    except ValueError:\n        raise ui.UserError(\n            f\"supplied argument `{value}' is not of the form `key=value'\"\n        )\n\n    option_values[key] = value\n\n\nimport_cmd = ui.Subcommand(\n    \"import\", help=\"import new music\", aliases=(\"imp\", \"im\")\n)\nimport_cmd.parser.add_option(\n    \"-c\",\n    \"--copy\",\n    action=\"store_true\",\n    default=None,\n    help=\"copy tracks into library directory (default)\",\n)\nimport_cmd.parser.add_option(\n    \"-C\",\n    \"--nocopy\",\n    action=\"store_false\",\n    dest=\"copy\",\n    help=\"don't copy tracks (opposite of -c)\",\n)\nimport_cmd.parser.add_option(\n    \"-m\",\n    \"--move\",\n    action=\"store_true\",\n    dest=\"move\",\n    help=\"move tracks into the library (overrides -c)\",\n)\nimport_cmd.parser.add_option(\n    \"-w\",\n    \"--write\",\n    action=\"store_true\",\n    default=None,\n    help=\"write new metadata to files' tags (default)\",\n)\nimport_cmd.parser.add_option(\n    \"-W\",\n    \"--nowrite\",\n    action=\"store_false\",\n    dest=\"write\",\n    help=\"don't write metadata (opposite of -w)\",\n)\nimport_cmd.parser.add_option(\n    \"-a\",\n    \"--autotag\",\n    action=\"store_true\",\n    dest=\"autotag\",\n    help=\"infer tags for imported files (default)\",\n)\nimport_cmd.parser.add_option(\n    \"-A\",\n    \"--noautotag\",\n    action=\"store_false\",\n    dest=\"autotag\",\n    help=\"don't infer tags for imported files (opposite of -a)\",\n)\nimport_cmd.parser.add_option(\n    \"-p\",\n    \"--resume\",\n    action=\"store_true\",\n    default=None,\n    help=\"resume importing if interrupted\",\n)\nimport_cmd.parser.add_option(\n    \"-P\",\n    \"--noresume\",\n    action=\"store_false\",\n    dest=\"resume\",\n    help=\"do not try to resume importing\",\n)\nimport_cmd.parser.add_option(\n    \"-q\",\n    \"--quiet\",\n    action=\"store_true\",\n    dest=\"quiet\",\n    help=\"never prompt for input: skip albums instead\",\n)\nimport_cmd.parser.add_option(\n    \"--quiet-fallback\",\n    type=\"string\",\n    dest=\"quiet_fallback\",\n    help=\"decision in quiet mode when no strong match: skip or asis\",\n)\nimport_cmd.parser.add_option(\n    \"-l\",\n    \"--log\",\n    dest=\"log\",\n    help=\"file to log untaggable albums for later review\",\n)\nimport_cmd.parser.add_option(\n    \"-s\",\n    \"--singletons\",\n    action=\"store_true\",\n    help=\"import individual tracks instead of full albums\",\n)\nimport_cmd.parser.add_option(\n    \"-t\",\n    \"--timid\",\n    dest=\"timid\",\n    action=\"store_true\",\n    help=\"always confirm all actions\",\n)\nimport_cmd.parser.add_option(\n    \"-L\",\n    \"--library\",\n    dest=\"library\",\n    action=\"store_true\",\n    help=\"retag items matching a query\",\n)\nimport_cmd.parser.add_option(\n    \"-i\",\n    \"--incremental\",\n    dest=\"incremental\",\n    action=\"store_true\",\n    help=\"skip already-imported directories\",\n)\nimport_cmd.parser.add_option(\n    \"-I\",\n    \"--noincremental\",\n    dest=\"incremental\",\n    action=\"store_false\",\n    help=\"do not skip already-imported directories\",\n)\nimport_cmd.parser.add_option(\n    \"-R\",\n    \"--incremental-skip-later\",\n    action=\"store_true\",\n    dest=\"incremental_skip_later\",\n    help=\"do not record skipped files during incremental import\",\n)\nimport_cmd.parser.add_option(\n    \"-r\",\n    \"--noincremental-skip-later\",\n    action=\"store_false\",\n    dest=\"incremental_skip_later\",\n    help=\"record skipped files during incremental import\",\n)\nimport_cmd.parser.add_option(\n    \"--from-scratch\",\n    dest=\"from_scratch\",\n    action=\"store_true\",\n    help=\"erase existing metadata before applying new metadata\",\n)\nimport_cmd.parser.add_option(\n    \"--flat\",\n    dest=\"flat\",\n    action=\"store_true\",\n    help=\"import an entire tree as a single album\",\n)\nimport_cmd.parser.add_option(\n    \"-g\",\n    \"--group-albums\",\n    dest=\"group_albums\",\n    action=\"store_true\",\n    help=\"group tracks in a folder into separate albums\",\n)\nimport_cmd.parser.add_option(\n    \"--pretend\",\n    dest=\"pretend\",\n    action=\"store_true\",\n    help=\"just print the files to import\",\n)\nimport_cmd.parser.add_option(\n    \"-S\",\n    \"--search-id\",\n    dest=\"search_ids\",\n    action=\"append\",\n    metavar=\"ID\",\n    help=\"restrict matching to a specific metadata backend ID\",\n)\nimport_cmd.parser.add_option(\n    \"--from-logfile\",\n    dest=\"from_logfiles\",\n    action=\"append\",\n    metavar=\"PATH\",\n    help=\"read skipped paths from an existing logfile\",\n)\nimport_cmd.parser.add_option(\n    \"--set\",\n    dest=\"set_fields\",\n    action=\"callback\",\n    callback=_store_dict,\n    metavar=\"FIELD=VALUE\",\n    help=\"set the given fields to the supplied values\",\n)\nimport_cmd.func = import_func\n"
  },
  {
    "path": "beets/ui/commands/import_/display.py",
    "content": "from __future__ import annotations\n\nimport os\nfrom dataclasses import dataclass\nfrom functools import cached_property\nfrom typing import TYPE_CHECKING\n\nfrom beets import config, ui\nfrom beets.autotag import hooks\nfrom beets.util import displayable_path\nfrom beets.util.color import colorize, dist_colorize\nfrom beets.util.diff import colordiff\nfrom beets.util.layout import Side, get_layout_lines, indent\nfrom beets.util.units import human_seconds_short\n\nif TYPE_CHECKING:\n    from collections.abc import Sequence\n\n    import confuse\n\n    from beets import autotag\n    from beets.autotag.distance import Distance\n    from beets.library.models import Item\n    from beets.util.color import ColorName\n\nVARIOUS_ARTISTS = \"Various Artists\"\n\n\n@dataclass\nclass ChangeRepresentation:\n    \"\"\"Keeps track of all information needed to generate a (colored) text\n    representation of the changes that will be made if an album or singleton's\n    tags are changed according to `match`, which must be an AlbumMatch or\n    TrackMatch object, accordingly.\n    \"\"\"\n\n    cur_artist: str\n    cur_name: str\n    match: autotag.hooks.Match\n\n    @cached_property\n    def changed_prefix(self) -> str:\n        return colorize(\"changed\", \"\\u2260\")\n\n    @cached_property\n    def _indentation_config(self) -> confuse.Subview:\n        return config[\"ui\"][\"import\"][\"indentation\"]\n\n    @cached_property\n    def indent_header(self) -> str:\n        return indent(self._indentation_config[\"match_header\"].get(int))\n\n    @cached_property\n    def indent_detail(self) -> str:\n        return indent(self._indentation_config[\"match_details\"].get(int))\n\n    @cached_property\n    def indent_tracklist(self) -> str:\n        return indent(self._indentation_config[\"match_tracklist\"].get(int))\n\n    def print_layout(self, indent: str, left: Side, right: Side) -> None:\n        for line in get_layout_lines(indent, left, right, ui.term_width()):\n            ui.print_(line)\n\n    def show_match_header(self) -> None:\n        \"\"\"Print out a 'header' identifying the suggested match (album name,\n        artist name,...) and summarizing the changes that would be made should\n        the user accept the match.\n        \"\"\"\n        # Print newline at beginning of change block.\n        ui.print_(\"\")\n\n        # 'Match' line and similarity.\n        ui.print_(\n            f\"{self.indent_header}Match ({dist_string(self.match.distance)}):\"\n        )\n\n        artist_name_str = f\"{self.match.info.artist} - {self.match.info.name}\"\n        ui.print_(\n            self.indent_header\n            + dist_colorize(artist_name_str, self.match.distance)\n        )\n\n        # Penalties.\n        penalties = penalty_string(self.match.distance)\n        if penalties:\n            ui.print_(f\"{self.indent_header}{penalties}\")\n\n        # Disambiguation.\n        disambig = disambig_string(self.match.info)\n        if disambig:\n            ui.print_(f\"{self.indent_header}{disambig}\")\n\n        # Data URL.\n        if self.match.info.data_url:\n            url = colorize(\"text_faint\", f\"{self.match.info.data_url}\")\n            ui.print_(f\"{self.indent_header}{url}\")\n\n    def show_match_details(self) -> None:\n        \"\"\"Print out the details of the match, including changes in album name\n        and artist name.\n        \"\"\"\n        # Artist.\n        artist_l, artist_r = self.cur_artist or \"\", self.match.info.artist or \"\"\n        if artist_r == VARIOUS_ARTISTS:\n            # Hide artists for VA releases.\n            artist_l, artist_r = \"\", \"\"\n        if artist_l != artist_r:\n            artist_l, artist_r = colordiff(artist_l, artist_r)\n            left = Side(f\"{self.changed_prefix} Artist: \", artist_l, \"\")\n            right = Side(\"\", artist_r, \"\")\n            self.print_layout(self.indent_detail, left, right)\n\n        else:\n            ui.print_(f\"{self.indent_detail}*\", \"Artist:\", artist_r)\n\n        if self.cur_name:\n            type_ = self.match.type\n            name_l, name_r = self.cur_name or \"\", self.match.info.name\n            if self.cur_name != self.match.info.name != VARIOUS_ARTISTS:\n                name_l, name_r = colordiff(name_l, name_r)\n                left = Side(f\"{self.changed_prefix} {type_}: \", name_l, \"\")\n                right = Side(\"\", name_r, \"\")\n                self.print_layout(self.indent_detail, left, right)\n            else:\n                ui.print_(f\"{self.indent_detail}*\", f\"{type_}:\", name_r)\n\n    def make_medium_info_line(self, track_info: hooks.TrackInfo) -> str:\n        \"\"\"Construct a line with the current medium's info.\"\"\"\n        track_media = track_info.get(\"media\", \"Media\")\n        # Build output string.\n        if self.match.info.mediums > 1 and track_info.disctitle:\n            return (\n                f\"* {track_media} {track_info.medium}: {track_info.disctitle}\"\n            )\n        elif self.match.info.mediums > 1:\n            return f\"* {track_media} {track_info.medium}\"\n        elif track_info.disctitle:\n            return f\"* {track_media}: {track_info.disctitle}\"\n        else:\n            return \"\"\n\n    def format_index(self, track_info: hooks.TrackInfo | Item) -> str:\n        \"\"\"Return a string representing the track index of the given\n        TrackInfo or Item object.\n        \"\"\"\n        if isinstance(track_info, hooks.TrackInfo):\n            index = track_info.index\n            medium_index = track_info.medium_index\n            medium = track_info.medium\n            mediums = self.match.info.mediums\n        else:\n            index = medium_index = track_info.track\n            medium = track_info.disc\n            mediums = track_info.disctotal\n        if config[\"per_disc_numbering\"]:\n            if mediums and mediums > 1:\n                return f\"{medium}-{medium_index}\"\n            else:\n                return str(medium_index if medium_index is not None else index)\n        else:\n            return str(index)\n\n    def make_track_numbers(\n        self, item: Item, track_info: hooks.TrackInfo\n    ) -> tuple[str, str, bool]:\n        \"\"\"Format colored track indices.\"\"\"\n        cur_track = self.format_index(item)\n        new_track = self.format_index(track_info)\n        changed = False\n        # Choose color based on change.\n        highlight_color: ColorName\n        if cur_track != new_track:\n            changed = True\n            if item.track in (track_info.index, track_info.medium_index):\n                highlight_color = \"text_highlight_minor\"\n            else:\n                highlight_color = \"text_highlight\"\n        else:\n            highlight_color = \"text_faint\"\n\n        lhs_track = colorize(highlight_color, f\"(#{cur_track})\")\n        rhs_track = colorize(highlight_color, f\"(#{new_track})\")\n        return lhs_track, rhs_track, changed\n\n    @staticmethod\n    def make_track_titles(\n        item: Item, track_info: hooks.TrackInfo\n    ) -> tuple[str, str, bool]:\n        \"\"\"Format colored track titles.\"\"\"\n        new_title = track_info.name\n        if not item.title.strip():\n            # If there's no title, we use the filename. Don't colordiff.\n            cur_title = displayable_path(os.path.basename(item.path))\n            return cur_title, new_title, True\n        else:\n            # If there is a title, highlight differences.\n            cur_title = item.title.strip()\n            cur_col, new_col = colordiff(cur_title, new_title)\n            return cur_col, new_col, cur_title != new_title\n\n    @staticmethod\n    def make_track_lengths(\n        item: Item, track_info: hooks.TrackInfo\n    ) -> tuple[str, str, bool]:\n        \"\"\"Format colored track lengths.\"\"\"\n        changed = False\n        highlight_color: ColorName\n        if (\n            item.length\n            and track_info.length\n            and abs(item.length - track_info.length)\n            >= config[\"ui\"][\"length_diff_thresh\"].as_number()\n        ):\n            highlight_color = \"text_highlight\"\n            changed = True\n        else:\n            highlight_color = \"text_highlight_minor\"\n\n        # Handle nonetype lengths by setting to 0\n        cur_length0 = item.length if item.length else 0\n        new_length0 = track_info.length if track_info.length else 0\n        # format into string\n        cur_length = f\"({human_seconds_short(cur_length0)})\"\n        new_length = f\"({human_seconds_short(new_length0)})\"\n        # colorize\n        lhs_length = colorize(highlight_color, cur_length)\n        rhs_length = colorize(highlight_color, new_length)\n\n        return lhs_length, rhs_length, changed\n\n    def make_line(\n        self, item: Item, track_info: hooks.TrackInfo\n    ) -> tuple[Side, Side]:\n        \"\"\"Extract changes from item -> new TrackInfo object, and colorize\n        appropriately. Returns (lhs, rhs) for column printing.\n        \"\"\"\n        # Track titles.\n        lhs_title, rhs_title, diff_title = self.make_track_titles(\n            item, track_info\n        )\n        # Track number change.\n        lhs_track, rhs_track, diff_track = self.make_track_numbers(\n            item, track_info\n        )\n        # Length change.\n        lhs_length, rhs_length, diff_length = self.make_track_lengths(\n            item, track_info\n        )\n\n        changed = diff_title or diff_track or diff_length\n\n        # Construct lhs and rhs dicts.\n        # Previously, we printed the penalties, however this is no longer\n        # the case, thus the 'info' dictionary is unneeded.\n        # penalties = penalty_string(self.match.distance.tracks[track_info])\n\n        lhs = Side(\n            f\"{self.changed_prefix if changed else '*'} {lhs_track} \",\n            lhs_title,\n            f\" {lhs_length}\",\n        )\n        if not changed:\n            # Only return the left side, as nothing changed.\n            return (lhs, Side(\"\", \"\", \"\"))\n\n        return (lhs, Side(f\"{rhs_track} \", rhs_title, f\" {rhs_length}\"))\n\n    def print_tracklist(self, lines: list[tuple[Side, Side]]) -> None:\n        \"\"\"Calculates column widths for tracks stored as line tuples:\n        (left, right). Then prints each line of tracklist.\n        \"\"\"\n        if len(lines) == 0:\n            # If no lines provided, e.g. details not required, do nothing.\n            return\n\n        # Check how to fit content into terminal window\n        indent_width = len(self.indent_tracklist)\n        terminal_width = ui.term_width()\n        joiner_width = len(\"*  -> \")\n        col_width = (terminal_width - indent_width - joiner_width) // 2\n        max_width_l = max(left.rendered_width for left, _ in lines)\n        max_width_r = max(right.rendered_width for _, right in lines)\n\n        if ((max_width_l <= col_width) and (max_width_r <= col_width)) or (\n            ((max_width_l > col_width) or (max_width_r > col_width))\n            and ((max_width_l + max_width_r) <= col_width * 2)\n        ):\n            # All content fits. Either both maximum widths are below column\n            # widths, or one of the columns is larger than allowed but the\n            # other is smaller than allowed.\n            # In this case we can afford to shrink the columns to fit their\n            # largest string\n            col_width_l = max_width_l\n            col_width_r = max_width_r\n        else:\n            # Not all content fits - stick with original half/half split\n            col_width_l = col_width\n            col_width_r = col_width\n\n        # Print out each line, using the calculated width from above.\n        for left, right in lines:\n            left = left._replace(width=col_width_l)\n            right = right._replace(width=col_width_r)\n            self.print_layout(self.indent_tracklist, left, right)\n\n\nclass AlbumChange(ChangeRepresentation):\n    match: autotag.hooks.AlbumMatch\n\n    def show_match_tracks(self) -> None:\n        \"\"\"Print out the tracks of the match, summarizing changes the match\n        suggests for them.\n        \"\"\"\n        pairs = sorted(\n            self.match.item_info_pairs, key=lambda pair: pair[1].index or 0\n        )\n        # Build up LHS and RHS for track difference display. The `lines` list\n        # contains `(left, right)` tuples.\n        lines: list[tuple[Side, Side]] = []\n        medium = disctitle = None\n        for item, track_info in pairs:\n            # If the track is the first on a new medium, show medium\n            # number and title.\n            if medium != track_info.medium or disctitle != track_info.disctitle:\n                # Create header for new medium\n                header = self.make_medium_info_line(track_info)\n                if header != \"\":\n                    # Print tracks from previous medium\n                    self.print_tracklist(lines)\n                    lines = []\n                    ui.print_(f\"{self.indent_detail}{header}\")\n                # Save new medium details for future comparison.\n                medium, disctitle = track_info.medium, track_info.disctitle\n\n            # Construct the line tuple for the track.\n            left, right = self.make_line(item, track_info)\n            if right.contents != \"\":\n                lines.append((left, right))\n            else:\n                if config[\"import\"][\"detail\"]:\n                    lines.append((left, right))\n        self.print_tracklist(lines)\n\n        # Missing and unmatched tracks.\n        if self.match.extra_tracks:\n            ui.print_(\n                \"Missing tracks\"\n                f\" ({len(self.match.extra_tracks)}/{len(self.match.info.tracks)} -\"\n                f\" {len(self.match.extra_tracks) / len(self.match.info.tracks):.1%}):\"\n            )\n        for track_info in self.match.extra_tracks:\n            line = f\" ! {track_info.title} (#{self.format_index(track_info)})\"\n            if track_info.length:\n                line += f\" ({human_seconds_short(track_info.length)})\"\n            ui.print_(colorize(\"text_warning\", line))\n        if self.match.extra_items:\n            ui.print_(f\"Unmatched tracks ({len(self.match.extra_items)}):\")\n        for item in self.match.extra_items:\n            line = f\" ! {item.title} (#{self.format_index(item)})\"\n            if item.length:\n                line += f\" ({human_seconds_short(item.length)})\"\n            ui.print_(colorize(\"text_warning\", line))\n\n\nclass TrackChange(ChangeRepresentation):\n    \"\"\"Track change representation, comparing item with match.\"\"\"\n\n    match: autotag.hooks.TrackMatch\n\n\ndef show_change(\n    cur_artist: str, cur_album: str, match: hooks.AlbumMatch\n) -> None:\n    \"\"\"Print out a representation of the changes that will be made if an\n    album's tags are changed according to `match`, which must be an AlbumMatch\n    object.\n    \"\"\"\n    change = AlbumChange(cur_artist, cur_album, match)\n\n    # Print the match header.\n    change.show_match_header()\n\n    # Print the match details.\n    change.show_match_details()\n\n    # Print the match tracks.\n    change.show_match_tracks()\n\n\ndef show_item_change(item: Item, match: hooks.TrackMatch) -> None:\n    \"\"\"Print out the change that would occur by tagging `item` with the\n    metadata from `match`, a TrackMatch object.\n    \"\"\"\n    change = TrackChange(item.artist, item.title, match)\n    # Print the match header.\n    change.show_match_header()\n    # Print the match details.\n    change.show_match_details()\n\n\ndef disambig_string(info: hooks.Info) -> str:\n    \"\"\"Generate a string for an AlbumInfo or TrackInfo object that\n    provides context that helps disambiguate similar-looking albums and\n    tracks.\n    \"\"\"\n    if isinstance(info, hooks.AlbumInfo):\n        disambig = get_album_disambig_fields(info)\n    elif isinstance(info, hooks.TrackInfo):\n        disambig = get_singleton_disambig_fields(info)\n    else:\n        return \"\"\n\n    return \", \".join(disambig)\n\n\ndef get_singleton_disambig_fields(info: hooks.TrackInfo) -> Sequence[str]:\n    out = []\n    chosen_fields = config[\"match\"][\"singleton_disambig_fields\"].as_str_seq()\n    calculated_values = {\n        \"index\": f\"Index {info.index}\",\n        \"track_alt\": f\"Track {info.track_alt}\",\n        \"album\": (\n            f\"[{info.album}]\"\n            if (\n                config[\"import\"][\"singleton_album_disambig\"].get()\n                and info.get(\"album\")\n            )\n            else \"\"\n        ),\n    }\n\n    for field in chosen_fields:\n        if field in calculated_values:\n            out.append(str(calculated_values[field]))\n        else:\n            try:\n                out.append(str(info[field]))\n            except (AttributeError, KeyError):\n                print(f\"Disambiguation string key {field} does not exist.\")\n\n    return out\n\n\ndef get_album_disambig_fields(info: hooks.AlbumInfo) -> Sequence[str]:\n    out = []\n    chosen_fields = config[\"match\"][\"album_disambig_fields\"].as_str_seq()\n    calculated_values = {\n        \"media\": (\n            f\"{info.mediums}x{info.media}\"\n            if (info.mediums and info.mediums > 1)\n            else info.media\n        ),\n    }\n\n    for field in chosen_fields:\n        if field in calculated_values:\n            out.append(str(calculated_values[field]))\n        else:\n            try:\n                out.append(str(info[field]))\n            except (AttributeError, KeyError):\n                print(f\"Disambiguation string key {field} does not exist.\")\n\n    return out\n\n\ndef dist_string(dist: Distance) -> str:\n    \"\"\"Formats a distance (a float) as a colorized similarity percentage\n    string.\n    \"\"\"\n    string = f\"{(1 - dist) * 100:.1f}%\"\n    return dist_colorize(string, dist)\n\n\ndef penalty_string(distance: Distance, limit: int | None = None) -> str:\n    \"\"\"Returns a colorized string that indicates all the penalties\n    applied to a distance object.\n    \"\"\"\n    penalties = []\n    for key in distance.keys():\n        key = key.replace(\"album_\", \"\")\n        key = key.replace(\"track_\", \"\")\n        key = key.replace(\"_\", \" \")\n        penalties.append(key)\n    if penalties:\n        if limit and len(penalties) > limit:\n            penalties = [*penalties[:limit], \"...\"]\n        # Prefix penalty string with U+2260: Not Equal To\n        penalty_string = f\"\\u2260 {', '.join(penalties)}\"\n        return colorize(\"changed\", penalty_string)\n\n    return \"\"\n"
  },
  {
    "path": "beets/ui/commands/import_/session.py",
    "content": "from collections import Counter\nfrom itertools import chain\n\nfrom beets import autotag, config, importer, logging, plugins, ui\nfrom beets.autotag import Recommendation\nfrom beets.util import PromptChoice, displayable_path\nfrom beets.util.color import colorize, dist_colorize\nfrom beets.util.units import human_bytes, human_seconds_short\n\nfrom .display import (\n    disambig_string,\n    penalty_string,\n    show_change,\n    show_item_change,\n)\n\n# Global logger.\nlog = logging.getLogger(\"beets\")\n\n\nclass TerminalImportSession(importer.ImportSession):\n    \"\"\"An import session that runs in a terminal.\"\"\"\n\n    def choose_match(self, task):\n        \"\"\"Given an initial autotagging of items, go through an interactive\n        dance with the user to ask for a choice of metadata. Returns an\n        AlbumMatch object, ASIS, or SKIP.\n        \"\"\"\n        # Show what we're tagging.\n        ui.print_()\n\n        path_str0 = displayable_path(task.paths, \"\\n\")\n        path_str = colorize(\"import_path\", path_str0)\n        items_str0 = f\"({len(task.items)} items)\"\n        items_str = colorize(\"import_path_items\", items_str0)\n        ui.print_(\" \".join([path_str, items_str]))\n\n        # Let plugins display info or prompt the user before we go through the\n        # process of selecting candidate.\n        results = plugins.send(\n            \"import_task_before_choice\", session=self, task=task\n        )\n        actions = [action for action in results if action]\n\n        if len(actions) == 1:\n            return actions[0]\n        elif len(actions) > 1:\n            raise plugins.PluginConflictError(\n                \"Only one handler for `import_task_before_choice` may return \"\n                \"an action.\"\n            )\n\n        # Take immediate action if appropriate.\n        action = _summary_judgment(task.rec)\n        if action == importer.Action.APPLY:\n            match = task.candidates[0]\n            show_change(task.cur_artist, task.cur_album, match)\n            return match\n        elif action is not None:\n            return action\n\n        # Loop until we have a choice.\n        while True:\n            # Ask for a choice from the user. The result of\n            # `choose_candidate` may be an `importer.Action`, an\n            # `AlbumMatch` object for a specific selection, or a\n            # `PromptChoice`.\n            choices = self._get_choices(task)\n            choice = choose_candidate(\n                task.candidates,\n                False,\n                task.rec,\n                task.cur_artist,\n                task.cur_album,\n                itemcount=len(task.items),\n                choices=choices,\n            )\n\n            # Basic choices that require no more action here.\n            if choice in (importer.Action.SKIP, importer.Action.ASIS):\n                # Pass selection to main control flow.\n                return choice\n\n            # Plugin-provided choices. We invoke the associated callback\n            # function.\n            elif choice in choices:\n                post_choice = choice.callback(self, task)\n                if isinstance(post_choice, importer.Action):\n                    return post_choice\n                elif isinstance(post_choice, autotag.Proposal):\n                    # Use the new candidates and continue around the loop.\n                    task.candidates = post_choice.candidates\n                    task.rec = post_choice.recommendation\n\n            # Otherwise, we have a specific match selection.\n            else:\n                # We have a candidate! Finish tagging. Here, choice is an\n                # AlbumMatch object.\n                assert isinstance(choice, autotag.AlbumMatch)\n                return choice\n\n    def choose_item(self, task):\n        \"\"\"Ask the user for a choice about tagging a single item. Returns\n        either an action constant or a TrackMatch object.\n        \"\"\"\n        ui.print_()\n        ui.print_(displayable_path(task.item.path))\n        candidates, rec = task.candidates, task.rec\n\n        # Take immediate action if appropriate.\n        action = _summary_judgment(task.rec)\n        if action == importer.Action.APPLY:\n            match = candidates[0]\n            show_item_change(task.item, match)\n            return match\n        elif action is not None:\n            return action\n\n        while True:\n            # Ask for a choice.\n            choices = self._get_choices(task)\n            choice = choose_candidate(\n                candidates, True, rec, item=task.item, choices=choices\n            )\n\n            if choice in (importer.Action.SKIP, importer.Action.ASIS):\n                return choice\n\n            elif choice in choices:\n                post_choice = choice.callback(self, task)\n                if isinstance(post_choice, importer.Action):\n                    return post_choice\n                elif isinstance(post_choice, autotag.Proposal):\n                    candidates = post_choice.candidates\n                    rec = post_choice.recommendation\n\n            else:\n                # Chose a candidate.\n                assert isinstance(choice, autotag.TrackMatch)\n                return choice\n\n    def resolve_duplicate(self, task, found_duplicates):\n        \"\"\"Decide what to do when a new album or item seems similar to one\n        that's already in the library.\n        \"\"\"\n        log.warning(\n            \"This {} is already in the library!\",\n            (\"album\" if task.is_album else \"item\"),\n        )\n\n        if config[\"import\"][\"quiet\"]:\n            # In quiet mode, don't prompt -- just skip.\n            log.info(\"Skipping.\")\n            sel = \"s\"\n        else:\n            # Print some detail about the existing and new items so the\n            # user can make an informed decision.\n            for duplicate in found_duplicates:\n                ui.print_(\n                    \"Old: \"\n                    + summarize_items(\n                        (\n                            list(duplicate.items())\n                            if task.is_album\n                            else [duplicate]\n                        ),\n                        not task.is_album,\n                    )\n                )\n                if config[\"import\"][\"duplicate_verbose_prompt\"]:\n                    if task.is_album:\n                        for dup in duplicate.items():\n                            print(f\"  {dup}\")\n                    else:\n                        print(f\"  {duplicate}\")\n\n            ui.print_(\n                \"New: \"\n                + summarize_items(\n                    task.imported_items(),\n                    not task.is_album,\n                )\n            )\n            if config[\"import\"][\"duplicate_verbose_prompt\"]:\n                for item in task.imported_items():\n                    print(f\"  {item}\")\n\n            sel = ui.input_options(\n                (\"Skip new\", \"Keep all\", \"Remove old\", \"Merge all\")\n            )\n\n        if sel == \"s\":\n            # Skip new.\n            task.set_choice(importer.Action.SKIP)\n        elif sel == \"k\":\n            # Keep both. Do nothing; leave the choice intact.\n            pass\n        elif sel == \"r\":\n            # Remove old.\n            task.should_remove_duplicates = True\n        elif sel == \"m\":\n            task.should_merge_duplicates = True\n        else:\n            assert False\n\n    def should_resume(self, path):\n        return ui.input_yn(\n            f\"Import of the directory:\\n{displayable_path(path)}\\n\"\n            \"was interrupted. Resume (Y/n)?\"\n        )\n\n    def _get_choices(self, task):\n        \"\"\"Get the list of prompt choices that should be presented to the\n        user. This consists of both built-in choices and ones provided by\n        plugins.\n\n        The `before_choose_candidate` event is sent to the plugins, with\n        session and task as its parameters. Plugins are responsible for\n        checking the right conditions and returning a list of `PromptChoice`s,\n        which is flattened and checked for conflicts.\n\n        If two or more choices have the same short letter, a warning is\n        emitted and all but one choices are discarded, giving preference\n        to the default importer choices.\n\n        Returns a list of `PromptChoice`s.\n        \"\"\"\n        # Standard, built-in choices.\n        choices = [\n            PromptChoice(\"s\", \"Skip\", lambda s, t: importer.Action.SKIP),\n            PromptChoice(\"u\", \"Use as-is\", lambda s, t: importer.Action.ASIS),\n        ]\n        if task.is_album:\n            choices += [\n                PromptChoice(\n                    \"t\", \"as Tracks\", lambda s, t: importer.Action.TRACKS\n                ),\n                PromptChoice(\n                    \"g\", \"Group albums\", lambda s, t: importer.Action.ALBUMS\n                ),\n            ]\n        choices += [\n            PromptChoice(\"e\", \"Enter search\", manual_search),\n            PromptChoice(\"i\", \"enter Id\", manual_id),\n            PromptChoice(\"b\", \"aBort\", abort_action),\n        ]\n\n        # Send the before_choose_candidate event and flatten list.\n        extra_choices = list(\n            chain(\n                *plugins.send(\n                    \"before_choose_candidate\", session=self, task=task\n                )\n            )\n        )\n\n        # Add a \"dummy\" choice for the other baked-in option, for\n        # duplicate checking.\n        all_choices = [\n            PromptChoice(\"a\", \"Apply\", None),\n            *choices,\n            *extra_choices,\n        ]\n\n        # Check for conflicts.\n        short_letters = [c.short for c in all_choices]\n        if len(short_letters) != len(set(short_letters)):\n            # Duplicate short letter has been found.\n            duplicates = [\n                i for i, count in Counter(short_letters).items() if count > 1\n            ]\n            for short in duplicates:\n                # Keep the first of the choices, removing the rest.\n                dup_choices = [c for c in all_choices if c.short == short]\n                for c in dup_choices[1:]:\n                    log.warning(\n                        \"Prompt choice '{0.long}' removed due to conflict \"\n                        \"with '{1[0].long}' (short letter: '{0.short}')\",\n                        c,\n                        dup_choices,\n                    )\n                    extra_choices.remove(c)\n\n        return choices + extra_choices\n\n\ndef summarize_items(items, singleton):\n    \"\"\"Produces a brief summary line describing a set of items. Used for\n    manually resolving duplicates during import.\n\n    `items` is a list of `Item` objects. `singleton` indicates whether\n    this is an album or single-item import (if the latter, them `items`\n    should only have one element).\n    \"\"\"\n    summary_parts = []\n    if not singleton:\n        summary_parts.append(f\"{len(items)} items\")\n\n    format_counts = {}\n    for item in items:\n        format_counts[item.format] = format_counts.get(item.format, 0) + 1\n    if len(format_counts) == 1:\n        # A single format.\n        summary_parts.append(items[0].format)\n    else:\n        # Enumerate all the formats by decreasing frequencies:\n        for fmt, count in sorted(\n            format_counts.items(),\n            key=lambda fmt_and_count: (-fmt_and_count[1], fmt_and_count[0]),\n        ):\n            summary_parts.append(f\"{fmt} {count}\")\n\n    if items:\n        average_bitrate = sum([item.bitrate for item in items]) / len(items)\n        total_duration = sum([item.length for item in items])\n        total_filesize = sum([item.filesize for item in items])\n        summary_parts.append(f\"{int(average_bitrate / 1000)}kbps\")\n        if items[0].format == \"FLAC\":\n            sample_bits = (\n                f\"{round(int(items[0].samplerate) / 1000, 1)}kHz\"\n                f\"/{items[0].bitdepth} bit\"\n            )\n            summary_parts.append(sample_bits)\n        summary_parts.append(human_seconds_short(total_duration))\n        summary_parts.append(human_bytes(total_filesize))\n\n    return \", \".join(summary_parts)\n\n\ndef _summary_judgment(rec: Recommendation) -> importer.Action | None:\n    \"\"\"Determines whether a decision should be made without even asking\n    the user. This occurs in quiet mode and when an action is chosen for\n    NONE recommendations. Return None if the user should be queried.\n    Otherwise, returns an action. May also print to the console if a\n    summary judgment is made.\n    \"\"\"\n\n    action: importer.Action | None\n    if config[\"import\"][\"quiet\"]:\n        if rec == Recommendation.strong:\n            return importer.Action.APPLY\n        else:\n            action = config[\"import\"][\"quiet_fallback\"].as_choice(\n                {\n                    \"skip\": importer.Action.SKIP,\n                    \"asis\": importer.Action.ASIS,\n                }\n            )\n    elif config[\"import\"][\"timid\"]:\n        return None\n    elif rec == Recommendation.none:\n        action = config[\"import\"][\"none_rec_action\"].as_choice(\n            {\n                \"skip\": importer.Action.SKIP,\n                \"asis\": importer.Action.ASIS,\n                \"ask\": None,\n            }\n        )\n    else:\n        return None\n\n    if action == importer.Action.SKIP:\n        ui.print_(\"Skipping.\")\n    elif action == importer.Action.ASIS:\n        ui.print_(\"Importing as-is.\")\n    return action\n\n\ndef choose_candidate(\n    candidates,\n    singleton,\n    rec,\n    cur_artist=None,\n    cur_album=None,\n    item=None,\n    itemcount=None,\n    choices=[],\n):\n    \"\"\"Given a sorted list of candidates, ask the user for a selection\n    of which candidate to use. Applies to both full albums and\n    singletons  (tracks). Candidates are either AlbumMatch or TrackMatch\n    objects depending on `singleton`. for albums, `cur_artist`,\n    `cur_album`, and `itemcount` must be provided. For singletons,\n    `item` must be provided.\n\n    `choices` is a list of `PromptChoice`s to be used in each prompt.\n\n    Returns one of the following:\n    * the result of the choice, which may be SKIP or ASIS\n    * a candidate (an AlbumMatch/TrackMatch object)\n    * a chosen `PromptChoice` from `choices`\n    \"\"\"\n    # Sanity check.\n    if singleton:\n        assert item is not None\n    else:\n        assert cur_artist is not None\n        assert cur_album is not None\n\n    # Build helper variables for the prompt choices.\n    choice_opts = tuple(c.long for c in choices)\n    choice_actions = {c.short: c for c in choices}\n\n    # Zero candidates.\n    if not candidates:\n        if singleton:\n            ui.print_(\"No matching recordings found.\")\n        else:\n            ui.print_(f\"No matching release found for {itemcount} tracks.\")\n            ui.print_(\n                \"For help, see: \"\n                \"https://beets.readthedocs.org/en/latest/faq.html#nomatch\"\n            )\n        sel = ui.input_options(choice_opts)\n        if sel in choice_actions:\n            return choice_actions[sel]\n        else:\n            assert False\n\n    # Is the change good enough?\n    bypass_candidates = False\n    if rec != Recommendation.none:\n        match = candidates[0]\n        bypass_candidates = True\n\n    while True:\n        # Display and choose from candidates.\n        require = rec <= Recommendation.low\n\n        if not bypass_candidates:\n            # Display list of candidates.\n            ui.print_(\"\")\n            ui.print_(\n                f\"Finding tags for {'track' if singleton else 'album'} \"\n                f'\"{item.artist if singleton else cur_artist} -'\n                f' {item.title if singleton else cur_album}\".'\n            )\n\n            ui.print_(\"  Candidates:\")\n            for i, match in enumerate(candidates):\n                # Index, metadata, and distance.\n                index0 = f\"{i + 1}.\"\n                index = dist_colorize(index0, match.distance)\n                dist = f\"({(1 - match.distance) * 100:.1f}%)\"\n                distance = dist_colorize(dist, match.distance)\n                metadata = f\"{match.info.artist} - {match.info.name}\"\n                if i == 0:\n                    metadata = dist_colorize(metadata, match.distance)\n                else:\n                    metadata = colorize(\"text_highlight_minor\", metadata)\n                line1 = [index, distance, metadata]\n                ui.print_(f\"  {' '.join(line1)}\")\n\n                # Penalties.\n                penalties = penalty_string(match.distance, 3)\n                if penalties:\n                    ui.print_(f\"{' ' * 13}{penalties}\")\n\n                # Disambiguation\n                disambig = disambig_string(match.info)\n                if disambig:\n                    ui.print_(f\"{' ' * 13}{disambig}\")\n\n            # Ask the user for a choice.\n            sel = ui.input_options(choice_opts, numrange=(1, len(candidates)))\n            if sel == \"m\":\n                pass\n            elif sel in choice_actions:\n                return choice_actions[sel]\n            else:  # Numerical selection.\n                match = candidates[sel - 1]\n                if sel != 1:\n                    # When choosing anything but the first match,\n                    # disable the default action.\n                    require = True\n        bypass_candidates = False\n\n        # Show what we're about to do.\n        if singleton:\n            show_item_change(item, match)\n        else:\n            show_change(cur_artist, cur_album, match)\n\n        # Exact match => tag automatically if we're not in timid mode.\n        if rec == Recommendation.strong and not config[\"import\"][\"timid\"]:\n            return match\n\n        # Ask for confirmation.\n        default = config[\"import\"][\"default_action\"].as_choice(\n            {\n                \"apply\": \"a\",\n                \"skip\": \"s\",\n                \"asis\": \"u\",\n                \"none\": None,\n            }\n        )\n        if default is None:\n            require = True\n        # Bell ring when user interaction is needed.\n        if config[\"import\"][\"bell\"]:\n            ui.print_(\"\\a\", end=\"\")\n        sel = ui.input_options(\n            (\"Apply\", \"More candidates\", *choice_opts),\n            require=require,\n            default=default,\n        )\n        if sel == \"a\":\n            return match\n        elif sel in choice_actions:\n            return choice_actions[sel]\n\n\ndef manual_search(session, task):\n    \"\"\"Get a new `Proposal` using manual search criteria.\n\n    Input either an artist and album (for full albums) or artist and\n    track name (for singletons) for manual search.\n    \"\"\"\n    artist = ui.input_(\"Artist:\").strip()\n    name = ui.input_(\"Album:\" if task.is_album else \"Track:\").strip()\n\n    if task.is_album:\n        _, _, prop = autotag.tag_album(task.items, artist, name)\n        return prop\n    else:\n        return autotag.tag_item(task.item, artist, name)\n\n\ndef manual_id(session, task):\n    \"\"\"Get a new `Proposal` using a manually-entered ID.\n\n    Input an ID, either for an album (\"release\") or a track (\"recording\").\n    \"\"\"\n    prompt = f\"Enter {'release' if task.is_album else 'recording'} ID:\"\n    search_id = ui.input_(prompt).strip()\n\n    if task.is_album:\n        _, _, prop = autotag.tag_album(task.items, search_ids=search_id.split())\n        return prop\n    else:\n        return autotag.tag_item(task.item, search_ids=search_id.split())\n\n\ndef abort_action(session, task):\n    \"\"\"A prompt choice callback that aborts the importer.\"\"\"\n    raise importer.ImportAbortError()\n"
  },
  {
    "path": "beets/ui/commands/list.py",
    "content": "\"\"\"The 'list' command: query and show library contents.\"\"\"\n\nfrom beets import ui\n\n\ndef list_items(lib, query, album, fmt=\"\"):\n    \"\"\"Print out items in lib matching query. If album, then search for\n    albums instead of single items.\n    \"\"\"\n    if album:\n        for album in lib.albums(query):\n            ui.print_(format(album, fmt))\n    else:\n        for item in lib.items(query):\n            ui.print_(format(item, fmt))\n\n\ndef list_func(lib, opts, args):\n    list_items(lib, args, opts.album)\n\n\nlist_cmd = ui.Subcommand(\"list\", help=\"query the library\", aliases=(\"ls\",))\nlist_cmd.parser.usage += \"\\nExample: %prog -f '$album: $title' artist:beatles\"\nlist_cmd.parser.add_all_common_options()\nlist_cmd.func = list_func\n"
  },
  {
    "path": "beets/ui/commands/modify.py",
    "content": "\"\"\"The `modify` command: change metadata fields.\"\"\"\n\nfrom beets import library, ui\nfrom beets.util import functemplate\n\nfrom .utils import do_query\n\n\ndef modify_items(lib, mods, dels, query, write, move, album, confirm, inherit):\n    \"\"\"Modifies matching items according to user-specified assignments and\n    deletions.\n\n    `mods` is a dictionary of field and value pairse indicating\n    assignments. `dels` is a list of fields to be deleted.\n    \"\"\"\n    # Parse key=value specifications into a dictionary.\n    model_cls = library.Album if album else library.Item\n\n    # Get the items to modify.\n    items, albums = do_query(lib, query, album, False)\n    objs = albums if album else items\n\n    # Apply changes *temporarily*, preview them, and collect modified\n    # objects.\n    ui.print_(f\"Modifying {len(objs)} {'album' if album else 'item'}s.\")\n    changed = []\n    templates = {\n        key: functemplate.template(value) for key, value in mods.items()\n    }\n    for obj in objs:\n        obj_mods = {\n            key: model_cls._parse(key, obj.evaluate_template(templates[key]))\n            for key in mods.keys()\n        }\n        if print_and_modify(obj, obj_mods, dels) and obj not in changed:\n            changed.append(obj)\n\n    # Still something to do?\n    if not changed:\n        ui.print_(\"No changes to make.\")\n        return\n\n    # Confirm action.\n    if confirm:\n        if write and move:\n            extra = \", move and write tags\"\n        elif write:\n            extra = \" and write tags\"\n        elif move:\n            extra = \" and move\"\n        else:\n            extra = \"\"\n\n        changed = ui.input_select_objects(\n            f\"Really modify{extra}\",\n            changed,\n            lambda o: print_and_modify(o, mods, dels),\n        )\n\n    # Apply changes to database and files\n    with lib.transaction():\n        for obj in changed:\n            obj.try_sync(write, move, inherit)\n\n\ndef print_and_modify(obj, mods, dels):\n    \"\"\"Print the modifications to an item and return a bool indicating\n    whether any changes were made.\n\n    `mods` is a dictionary of fields and values to update on the object;\n    `dels` is a sequence of fields to delete.\n    \"\"\"\n    obj.update(mods)\n    for field in dels:\n        try:\n            del obj[field]\n        except KeyError:\n            pass\n    return ui.show_model_changes(obj)\n\n\ndef modify_parse_args(args):\n    \"\"\"Split the arguments for the modify subcommand into query parts,\n    assignments (field=value), and deletions (field!).  Returns the result as\n    a three-tuple in that order.\n    \"\"\"\n    mods = {}\n    dels = []\n    query = []\n    for arg in args:\n        if arg.endswith(\"!\") and \"=\" not in arg and \":\" not in arg:\n            dels.append(arg[:-1])  # Strip trailing !.\n        elif \"=\" in arg and \":\" not in arg.split(\"=\", 1)[0]:\n            key, val = arg.split(\"=\", 1)\n            mods[key] = val\n        else:\n            query.append(arg)\n    return query, mods, dels\n\n\ndef modify_func(lib, opts, args):\n    query, mods, dels = modify_parse_args(args)\n    if not mods and not dels:\n        raise ui.UserError(\"no modifications specified\")\n    modify_items(\n        lib,\n        mods,\n        dels,\n        query,\n        ui.should_write(opts.write),\n        ui.should_move(opts.move),\n        opts.album,\n        not opts.yes,\n        opts.inherit,\n    )\n\n\nmodify_cmd = ui.Subcommand(\n    \"modify\", help=\"change metadata fields\", aliases=(\"mod\",)\n)\nmodify_cmd.parser.add_option(\n    \"-m\",\n    \"--move\",\n    action=\"store_true\",\n    dest=\"move\",\n    help=\"move files in the library directory\",\n)\nmodify_cmd.parser.add_option(\n    \"-M\",\n    \"--nomove\",\n    action=\"store_false\",\n    dest=\"move\",\n    help=\"don't move files in library\",\n)\nmodify_cmd.parser.add_option(\n    \"-w\",\n    \"--write\",\n    action=\"store_true\",\n    default=None,\n    help=\"write new metadata to files' tags (default)\",\n)\nmodify_cmd.parser.add_option(\n    \"-W\",\n    \"--nowrite\",\n    action=\"store_false\",\n    dest=\"write\",\n    help=\"don't write metadata (opposite of -w)\",\n)\nmodify_cmd.parser.add_album_option()\nmodify_cmd.parser.add_format_option(target=\"item\")\nmodify_cmd.parser.add_option(\n    \"-y\", \"--yes\", action=\"store_true\", help=\"skip confirmation\"\n)\nmodify_cmd.parser.add_option(\n    \"-I\",\n    \"--noinherit\",\n    action=\"store_false\",\n    dest=\"inherit\",\n    default=True,\n    help=\"when modifying albums, don't also change item data\",\n)\nmodify_cmd.func = modify_func\n"
  },
  {
    "path": "beets/ui/commands/move.py",
    "content": "\"\"\"The 'move' command: Move/copy files to the library or a new base directory.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom typing import TYPE_CHECKING\n\nfrom beets import logging, ui\nfrom beets.util import MoveOperation, displayable_path, normpath, syspath\nfrom beets.util.diff import colordiff\n\nfrom .utils import do_query\n\nif TYPE_CHECKING:\n    from beets.util import PathLike\n\n# Global logger.\nlog = logging.getLogger(\"beets\")\n\n\ndef show_path_changes(path_changes):\n    \"\"\"Given a list of tuples (source, destination) that indicate the\n    path changes, log the changes as INFO-level output to the beets log.\n    The output is guaranteed to be unicode.\n\n    Every pair is shown on a single line if the terminal width permits it,\n    else it is split over two lines. E.g.,\n\n    Source -> Destination\n\n    vs.\n\n    Source\n      -> Destination\n    \"\"\"\n    sources, destinations = zip(*path_changes)\n\n    # Ensure unicode output\n    sources = list(map(displayable_path, sources))\n    destinations = list(map(displayable_path, destinations))\n\n    # Calculate widths for terminal split\n    col_width = (ui.term_width() - len(\" -> \")) // 2\n    max_width = len(max(sources + destinations, key=len))\n\n    if max_width > col_width:\n        # Print every change over two lines\n        for source, dest in zip(sources, destinations):\n            color_source, color_dest = colordiff(source, dest)\n            ui.print_(f\"{color_source} \\n  -> {color_dest}\")\n    else:\n        # Print every change on a single line, and add a header\n        title_pad = max_width - len(\"Source \") + len(\" -> \")\n\n        ui.print_(f\"Source {' ' * title_pad} Destination\")\n        for source, dest in zip(sources, destinations):\n            pad = max_width - len(source)\n            color_source, color_dest = colordiff(source, dest)\n            ui.print_(f\"{color_source} {' ' * pad} -> {color_dest}\")\n\n\ndef move_items(\n    lib,\n    dest_path: PathLike,\n    query,\n    copy,\n    album,\n    pretend,\n    confirm=False,\n    export=False,\n):\n    \"\"\"Moves or copies items to a new base directory, given by dest. If\n    dest is None, then the library's base directory is used, making the\n    command \"consolidate\" files.\n    \"\"\"\n    dest = os.fsencode(dest_path) if dest_path else dest_path\n    items, albums = do_query(lib, query, album, False)\n    objs = albums if album else items\n    num_objs = len(objs)\n\n    # Filter out files that don't need to be moved.\n    def isitemmoved(item):\n        return item.path != item.destination(basedir=dest)\n\n    def isalbummoved(album):\n        return any(isitemmoved(i) for i in album.items())\n\n    objs = [o for o in objs if (isalbummoved if album else isitemmoved)(o)]\n    num_unmoved = num_objs - len(objs)\n    # Report unmoved files that match the query.\n    unmoved_msg = \"\"\n    if num_unmoved > 0:\n        unmoved_msg = f\" ({num_unmoved} already in place)\"\n\n    copy = copy or export  # Exporting always copies.\n    action = \"Copying\" if copy else \"Moving\"\n    act = \"copy\" if copy else \"move\"\n    entity = \"album\" if album else \"item\"\n    log.info(\n        \"{} {} {}{}{}.\",\n        action,\n        len(objs),\n        entity,\n        \"s\" if len(objs) != 1 else \"\",\n        unmoved_msg,\n    )\n    if not objs:\n        return\n\n    if pretend:\n        if album:\n            show_path_changes(\n                [\n                    (item.path, item.destination(basedir=dest))\n                    for obj in objs\n                    for item in obj.items()\n                ]\n            )\n        else:\n            show_path_changes(\n                [(obj.path, obj.destination(basedir=dest)) for obj in objs]\n            )\n    else:\n        if confirm:\n            objs = ui.input_select_objects(\n                f\"Really {act}\",\n                objs,\n                lambda o: show_path_changes(\n                    [(o.path, o.destination(basedir=dest))]\n                ),\n            )\n\n        for obj in objs:\n            log.debug(\"moving: {.filepath}\", obj)\n\n            if export:\n                # Copy without affecting the database.\n                obj.move(\n                    operation=MoveOperation.COPY, basedir=dest, store=False\n                )\n            else:\n                # Ordinary move/copy: store the new path.\n                if copy:\n                    obj.move(operation=MoveOperation.COPY, basedir=dest)\n                else:\n                    obj.move(operation=MoveOperation.MOVE, basedir=dest)\n\n\ndef move_func(lib, opts, args):\n    dest = opts.dest\n    if dest is not None:\n        dest = normpath(dest)\n        if not os.path.isdir(syspath(dest)):\n            raise ui.UserError(f\"no such directory: {displayable_path(dest)}\")\n\n    move_items(\n        lib,\n        dest,\n        args,\n        opts.copy,\n        opts.album,\n        opts.pretend,\n        opts.timid,\n        opts.export,\n    )\n\n\nmove_cmd = ui.Subcommand(\"move\", help=\"move or copy items\", aliases=(\"mv\",))\nmove_cmd.parser.add_option(\n    \"-d\", \"--dest\", metavar=\"DIR\", dest=\"dest\", help=\"destination directory\"\n)\nmove_cmd.parser.add_option(\n    \"-c\",\n    \"--copy\",\n    default=False,\n    action=\"store_true\",\n    help=\"copy instead of moving\",\n)\nmove_cmd.parser.add_option(\n    \"-p\",\n    \"--pretend\",\n    default=False,\n    action=\"store_true\",\n    help=\"show how files would be moved, but don't touch anything\",\n)\nmove_cmd.parser.add_option(\n    \"-t\",\n    \"--timid\",\n    dest=\"timid\",\n    action=\"store_true\",\n    help=\"always confirm all actions\",\n)\nmove_cmd.parser.add_option(\n    \"-e\",\n    \"--export\",\n    default=False,\n    action=\"store_true\",\n    help=\"copy without changing the database path\",\n)\nmove_cmd.parser.add_album_option()\nmove_cmd.func = move_func\n"
  },
  {
    "path": "beets/ui/commands/remove.py",
    "content": "\"\"\"The `remove` command: remove items from the library (and optionally delete files).\"\"\"\n\nfrom beets import ui\n\nfrom .utils import do_query\n\n\ndef remove_items(lib, query, album, delete, force):\n    \"\"\"Remove items matching query from lib. If album, then match and\n    remove whole albums. If delete, also remove files from disk.\n    \"\"\"\n    # Get the matching items.\n    items, albums = do_query(lib, query, album)\n    objs = albums if album else items\n\n    # Confirm file removal if not forcing removal.\n    if not force:\n        # Prepare confirmation with user.\n        album_str = (\n            f\" in {len(albums)} album{'s' if len(albums) > 1 else ''}\"\n            if album\n            else \"\"\n        )\n\n        if delete:\n            fmt = \"$path - $title\"\n            prompt = \"Really DELETE\"\n            prompt_all = (\n                \"Really DELETE\"\n                f\" {len(items)} file{'s' if len(items) > 1 else ''}{album_str}\"\n            )\n        else:\n            fmt = \"\"\n            prompt = \"Really remove from the library?\"\n            prompt_all = (\n                \"Really remove\"\n                f\" {len(items)} item{'s' if len(items) > 1 else ''}{album_str}\"\n                \" from the library?\"\n            )\n\n        # Helpers for printing affected items\n        def fmt_track(t):\n            ui.print_(format(t, fmt))\n\n        def fmt_album(a):\n            ui.print_()\n            for i in a.items():\n                fmt_track(i)\n\n        fmt_obj = fmt_album if album else fmt_track\n\n        # Show all the items.\n        for o in objs:\n            fmt_obj(o)\n\n        # Confirm with user.\n        objs = ui.input_select_objects(\n            prompt, objs, fmt_obj, prompt_all=prompt_all\n        )\n\n    if not objs:\n        return\n\n    # Remove (and possibly delete) items.\n    with lib.transaction():\n        for obj in objs:\n            obj.remove(delete)\n\n\ndef remove_func(lib, opts, args):\n    remove_items(lib, args, opts.album, opts.delete, opts.force)\n\n\nremove_cmd = ui.Subcommand(\n    \"remove\", help=\"remove matching items from the library\", aliases=(\"rm\",)\n)\nremove_cmd.parser.add_option(\n    \"-d\", \"--delete\", action=\"store_true\", help=\"also remove files from disk\"\n)\nremove_cmd.parser.add_option(\n    \"-f\", \"--force\", action=\"store_true\", help=\"do not ask when removing items\"\n)\nremove_cmd.parser.add_album_option()\nremove_cmd.func = remove_func\n"
  },
  {
    "path": "beets/ui/commands/stats.py",
    "content": "\"\"\"The 'stats' command: show library statistics.\"\"\"\n\nimport os\n\nfrom beets import logging, ui\nfrom beets.util import syspath\nfrom beets.util.units import human_bytes, human_seconds\n\n# Global logger.\nlog = logging.getLogger(\"beets\")\n\n\ndef show_stats(lib, query, exact):\n    \"\"\"Shows some statistics about the matched items.\"\"\"\n    items = lib.items(query)\n\n    total_size = 0\n    total_time = 0.0\n    total_items = 0\n    artists = set()\n    albums = set()\n    album_artists = set()\n\n    for item in items:\n        if exact:\n            try:\n                total_size += os.path.getsize(syspath(item.path))\n            except OSError as exc:\n                log.info(\"could not get size of {.path}: {}\", item, exc)\n        else:\n            total_size += int(item.length * item.bitrate / 8)\n        total_time += item.length\n        total_items += 1\n        artists.add(item.artist)\n        album_artists.add(item.albumartist)\n        if item.album_id:\n            albums.add(item.album_id)\n\n    size_str = human_bytes(total_size)\n    if exact:\n        size_str += f\" ({total_size} bytes)\"\n\n    ui.print_(f\"\"\"Tracks: {total_items}\nTotal time: {human_seconds(total_time)}\n{f\" ({total_time:.2f} seconds)\" if exact else \"\"}\n{\"Total size\" if exact else \"Approximate total size\"}: {size_str}\nArtists: {len(artists)}\nAlbums: {len(albums)}\nAlbum artists: {len(album_artists)}\"\"\")\n\n\ndef stats_func(lib, opts, args):\n    show_stats(lib, args, opts.exact)\n\n\nstats_cmd = ui.Subcommand(\n    \"stats\", help=\"show statistics about the library or a query\"\n)\nstats_cmd.parser.add_option(\n    \"-e\", \"--exact\", action=\"store_true\", help=\"exact size and time\"\n)\nstats_cmd.func = stats_func\n"
  },
  {
    "path": "beets/ui/commands/update.py",
    "content": "\"\"\"The `update` command: Update library contents according to on-disk tags.\"\"\"\n\nimport os\n\nfrom beets import library, logging, ui\nfrom beets.util import ancestry, syspath\nfrom beets.util.color import colorize\n\nfrom .utils import do_query\n\n# Global logger.\nlog = logging.getLogger(\"beets\")\n\n\ndef update_items(lib, query, album, move, pretend, fields, exclude_fields=None):\n    \"\"\"For all the items matched by the query, update the library to\n    reflect the item's embedded tags.\n    :param fields: The fields to be stored. If not specified, all fields will\n    be.\n    :param exclude_fields: The fields to not be stored. If not specified, all\n    fields will be.\n    \"\"\"\n    with lib.transaction():\n        items, _ = do_query(lib, query, album)\n        if move and fields is not None and \"path\" not in fields:\n            # Special case: if an item needs to be moved, the path field has to\n            # updated; otherwise the new path will not be reflected in the\n            # database.\n            fields.append(\"path\")\n        if fields is None:\n            # no fields were provided, update all media fields\n            item_fields = fields or library.Item._media_fields\n            if move and \"path\" not in item_fields:\n                # move is enabled, add 'path' to the list of fields to update\n                item_fields.add(\"path\")\n        else:\n            # fields was provided, just update those\n            item_fields = fields\n        # get all the album fields to update\n        album_fields = fields or library.Album._fields.keys()\n        if exclude_fields:\n            # remove any excluded fields from the item and album sets\n            item_fields = [f for f in item_fields if f not in exclude_fields]\n            album_fields = [f for f in album_fields if f not in exclude_fields]\n\n        # Walk through the items and pick up their changes.\n        affected_albums = set()\n        for item in items:\n            # Item deleted?\n            if not item.path or not os.path.exists(syspath(item.path)):\n                ui.print_(format(item))\n                ui.print_(colorize(\"text_error\", \"  deleted\"))\n                if not pretend:\n                    item.remove(True)\n                affected_albums.add(item.album_id)\n                continue\n\n            # Did the item change since last checked?\n            if item.current_mtime() <= item.mtime:\n                log.debug(\n                    \"skipping {0.filepath} because mtime is up to date ({0.mtime})\",\n                    item,\n                )\n                continue\n\n            # Read new data.\n            try:\n                item.read()\n            except library.ReadError as exc:\n                log.error(\"error reading {.filepath}: {}\", item, exc)\n                continue\n\n            # Special-case album artist when it matches track artist. (Hacky\n            # but necessary for preserving album-level metadata for non-\n            # autotagged imports.)\n            if not item.albumartist:\n                old_item = lib.get_item(item.id)\n                if old_item.albumartist == old_item.artist == item.artist:\n                    item.albumartist = old_item.albumartist\n                    item._dirty.discard(\"albumartist\")\n\n            # Check for and display changes.\n            changed = ui.show_model_changes(item, fields=item_fields)\n\n            # Save changes.\n            if not pretend:\n                if changed:\n                    # Move the item if it's in the library.\n                    if move and lib.directory in ancestry(item.path):\n                        item.move(store=False)\n\n                    item.store(fields=item_fields)\n                    affected_albums.add(item.album_id)\n                else:\n                    # The file's mtime was different, but there were no\n                    # changes to the metadata. Store the new mtime,\n                    # which is set in the call to read(), so we don't\n                    # check this again in the future.\n                    item.store(fields=item_fields)\n\n        # Skip album changes while pretending.\n        if pretend:\n            return\n\n        # Modify affected albums to reflect changes in their items.\n        for album_id in affected_albums:\n            if album_id is None:  # Singletons.\n                continue\n            album = lib.get_album(album_id)\n            if not album:  # Empty albums have already been removed.\n                log.debug(\"emptied album {}\", album_id)\n                continue\n            first_item = album.items().get()\n\n            # Update album structure to reflect an item in it.\n            for key in library.Album.item_keys:\n                album[key] = first_item[key]\n            album.store(fields=album_fields)\n\n            # Move album art (and any inconsistent items).\n            if move and lib.directory in ancestry(first_item.path):\n                log.debug(\"moving album {}\", album_id)\n\n                # Manually moving and storing the album.\n                items = list(album.items())\n                for item in items:\n                    item.move(store=False, with_album=False)\n                    item.store(fields=item_fields)\n                album.move(store=False)\n                album.store(fields=album_fields)\n\n\ndef update_func(lib, opts, args):\n    # Verify that the library folder exists to prevent accidental wipes.\n    if not os.path.isdir(syspath(lib.directory)):\n        ui.print_(\"Library path is unavailable or does not exist.\")\n        ui.print_(lib.directory)\n        if not ui.input_yn(\"Are you sure you want to continue (y/n)?\", True):\n            return\n    update_items(\n        lib,\n        args,\n        opts.album,\n        ui.should_move(opts.move),\n        opts.pretend,\n        opts.fields,\n        opts.exclude_fields,\n    )\n\n\nupdate_cmd = ui.Subcommand(\n    \"update\",\n    help=\"update the library\",\n    aliases=(\n        \"upd\",\n        \"up\",\n    ),\n)\nupdate_cmd.parser.add_album_option()\nupdate_cmd.parser.add_format_option()\nupdate_cmd.parser.add_option(\n    \"-m\",\n    \"--move\",\n    action=\"store_true\",\n    dest=\"move\",\n    help=\"move files in the library directory\",\n)\nupdate_cmd.parser.add_option(\n    \"-M\",\n    \"--nomove\",\n    action=\"store_false\",\n    dest=\"move\",\n    help=\"don't move files in library\",\n)\nupdate_cmd.parser.add_option(\n    \"-p\",\n    \"--pretend\",\n    action=\"store_true\",\n    help=\"show all changes but do nothing\",\n)\nupdate_cmd.parser.add_option(\n    \"-F\",\n    \"--field\",\n    default=None,\n    action=\"append\",\n    dest=\"fields\",\n    help=\"list of fields to update\",\n)\nupdate_cmd.parser.add_option(\n    \"-e\",\n    \"--exclude-field\",\n    default=None,\n    action=\"append\",\n    dest=\"exclude_fields\",\n    help=\"list of fields to exclude from updates\",\n)\nupdate_cmd.func = update_func\n"
  },
  {
    "path": "beets/ui/commands/utils.py",
    "content": "\"\"\"Utility functions for beets UI commands.\"\"\"\n\nfrom beets import ui\n\n\ndef do_query(lib, query, album, also_items=True):\n    \"\"\"For commands that operate on matched items, performs a query\n    and returns a list of matching items and a list of matching\n    albums. (The latter is only nonempty when album is True.) Raises\n    a UserError if no items match. also_items controls whether, when\n    fetching albums, the associated items should be fetched also.\n    \"\"\"\n    if album:\n        albums = list(lib.albums(query))\n        items = []\n        if also_items:\n            for al in albums:\n                items += al.items()\n\n    else:\n        albums = []\n        items = list(lib.items(query))\n\n    if album and not albums:\n        raise ui.UserError(\"No matching albums found.\")\n    elif not album and not items:\n        raise ui.UserError(\"No matching items found.\")\n\n    return items, albums\n"
  },
  {
    "path": "beets/ui/commands/version.py",
    "content": "\"\"\"The 'version' command: show version information.\"\"\"\n\nfrom platform import python_version\n\nimport beets\nfrom beets import plugins, ui\n\n\ndef show_version(*args):\n    ui.print_(f\"beets version {beets.__version__}\")\n    ui.print_(f\"Python version {python_version()}\")\n    # Show plugins.\n    names = sorted(p.name for p in plugins.find_plugins())\n    if names:\n        ui.print_(\"plugins:\", \", \".join(names))\n    else:\n        ui.print_(\"no plugins loaded\")\n\n\nversion_cmd = ui.Subcommand(\"version\", help=\"output version information\")\nversion_cmd.func = show_version\n\n__all__ = [\"version_cmd\"]\n"
  },
  {
    "path": "beets/ui/commands/write.py",
    "content": "\"\"\"The `write` command: write tag information to files.\"\"\"\n\nimport os\n\nfrom beets import library, logging, ui\nfrom beets.util import syspath\n\nfrom .utils import do_query\n\n# Global logger.\nlog = logging.getLogger(\"beets\")\n\n\ndef write_items(lib, query, pretend, force):\n    \"\"\"Write tag information from the database to the respective files\n    in the filesystem.\n    \"\"\"\n    items, _ = do_query(lib, query, False, False)\n\n    for item in items:\n        # Item deleted?\n        if not os.path.exists(syspath(item.path)):\n            log.info(\"missing file: {.filepath}\", item)\n            continue\n\n        # Get an Item object reflecting the \"clean\" (on-disk) state.\n        try:\n            clean_item = library.Item.from_path(item.path)\n        except library.ReadError as exc:\n            log.error(\"error reading {.filepath}: {}\", item, exc)\n            continue\n\n        # Check for and display changes.\n        changed = ui.show_model_changes(\n            item, clean_item, library.Item._media_tag_fields, force\n        )\n        if (changed or force) and not pretend:\n            # We use `try_sync` here to keep the mtime up to date in the\n            # database.\n            item.try_sync(True, False)\n\n\ndef write_func(lib, opts, args):\n    write_items(lib, args, opts.pretend, opts.force)\n\n\nwrite_cmd = ui.Subcommand(\"write\", help=\"write tag information to files\")\nwrite_cmd.parser.add_option(\n    \"-p\",\n    \"--pretend\",\n    action=\"store_true\",\n    help=\"show all changes but do nothing\",\n)\nwrite_cmd.parser.add_option(\n    \"-f\",\n    \"--force\",\n    action=\"store_true\",\n    help=\"write tags even if the existing tags match the database\",\n)\nwrite_cmd.func = write_func\n"
  },
  {
    "path": "beets/util/__init__.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Miscellaneous utility functions.\"\"\"\n\nfrom __future__ import annotations\n\nimport errno\nimport fnmatch\nimport os\nimport platform\nimport re\nimport shlex\nimport shutil\nimport subprocess\nimport sys\nimport tempfile\nimport traceback\nfrom collections import Counter\nfrom collections.abc import Sequence\nfrom contextlib import suppress\nfrom enum import Enum\nfrom functools import cache\nfrom importlib import import_module\nfrom multiprocessing.pool import ThreadPool\nfrom pathlib import Path\nfrom re import Pattern\nfrom typing import (\n    TYPE_CHECKING,\n    Any,\n    AnyStr,\n    ClassVar,\n    Generic,\n    NamedTuple,\n    TypeVar,\n    cast,\n)\n\nfrom unidecode import unidecode\n\nimport beets\nfrom beets.util import hidden\n\nif TYPE_CHECKING:\n    from collections.abc import Callable, Iterable, Iterator\n    from logging import Logger\n\n    from beets.library import Item\n\n\nMAX_FILENAME_LENGTH = 200\nWINDOWS_MAGIC_PREFIX = \"\\\\\\\\?\\\\\"\nT = TypeVar(\"T\")\nStrPath = str | Path\nPathLike = StrPath | bytes\nReplacements = Sequence[tuple[Pattern[str], str]]\n\n# Here for now to allow for a easy replace later on\n# once we can move to a PathLike (mainly used in importer)\nPathBytes = bytes\n\n\nclass HumanReadableError(Exception):\n    \"\"\"An Exception that can include a human-readable error message to\n    be logged without a traceback. Can preserve a traceback for\n    debugging purposes as well.\n\n    Has at least two fields: `reason`, the underlying exception or a\n    string describing the problem; and `verb`, the action being\n    performed during the error.\n\n    If `tb` is provided, it is a string containing a traceback for the\n    associated exception. (Note that this is not necessary in Python 3.x\n    and should be removed when we make the transition.)\n    \"\"\"\n\n    error_kind = \"Error\"  # Human-readable description of error type.\n\n    def __init__(self, reason, verb, tb=None):\n        self.reason = reason\n        self.verb = verb\n        self.tb = tb\n        super().__init__(self.get_message())\n\n    def _gerund(self):\n        \"\"\"Generate a (likely) gerund form of the English verb.\"\"\"\n        if \" \" in self.verb:\n            return self.verb\n        gerund = self.verb[:-1] if self.verb.endswith(\"e\") else self.verb\n        gerund += \"ing\"\n        return gerund\n\n    def _reasonstr(self):\n        \"\"\"Get the reason as a string.\"\"\"\n        if isinstance(self.reason, str):\n            return self.reason\n        elif isinstance(self.reason, bytes):\n            return self.reason.decode(\"utf-8\", \"ignore\")\n        elif hasattr(self.reason, \"strerror\"):  # i.e., EnvironmentError\n            return self.reason.strerror\n        else:\n            return f'\"{self.reason}\"'\n\n    def get_message(self):\n        \"\"\"Create the human-readable description of the error, sans\n        introduction.\n        \"\"\"\n        raise NotImplementedError\n\n    def log(self, logger):\n        \"\"\"Log to the provided `logger` a human-readable message as an\n        error and a verbose traceback as a debug message.\n        \"\"\"\n        if self.tb:\n            logger.debug(self.tb)\n        logger.error(\"{0.error_kind}: {0.args[0]}\", self)\n\n\nclass FilesystemError(HumanReadableError):\n    \"\"\"An error that occurred while performing a filesystem manipulation\n    via a function in this module. The `paths` field is a sequence of\n    pathnames involved in the operation.\n    \"\"\"\n\n    def __init__(self, reason, verb, paths, tb=None):\n        self.paths = paths\n        super().__init__(reason, verb, tb)\n\n    def get_message(self):\n        # Use a nicer English phrasing for some specific verbs.\n        if self.verb in (\"move\", \"copy\", \"rename\"):\n            clause = (\n                f\"while {self._gerund()} {displayable_path(self.paths[0])} to\"\n                f\" {displayable_path(self.paths[1])}\"\n            )\n        elif self.verb in (\"delete\", \"write\", \"create\", \"read\"):\n            clause = f\"while {self._gerund()} {displayable_path(self.paths[0])}\"\n        else:\n            clause = (\n                f\"during {self.verb} of paths\"\n                f\" {', '.join(displayable_path(p) for p in self.paths)}\"\n            )\n\n        return f\"{self._reasonstr()} {clause}\"\n\n\nclass MoveOperation(Enum):\n    \"\"\"The file operations that e.g. various move functions can carry out.\"\"\"\n\n    MOVE = 0\n    COPY = 1\n    LINK = 2\n    HARDLINK = 3\n    REFLINK = 4\n    REFLINK_AUTO = 5\n\n\nclass PromptChoice(NamedTuple):\n    short: str\n    long: str\n    callback: Any\n\n\ndef normpath(path: PathLike) -> bytes:\n    \"\"\"Provide the canonical form of the path suitable for storing in\n    the database.\n    \"\"\"\n    str_path = syspath(path, prefix=False)\n    str_path = os.path.normpath(os.path.abspath(os.path.expanduser(str_path)))\n    return bytestring_path(str_path)\n\n\ndef ancestry(path: AnyStr) -> list[AnyStr]:\n    \"\"\"Return a list consisting of path's parent directory, its\n    grandparent, and so on. For instance:\n\n       >>> ancestry(b'/a/b/c')\n       ['/', '/a', '/a/b']\n\n    The argument should *not* be the result of a call to `syspath`.\n    \"\"\"\n    out: list[AnyStr] = []\n    last_path = None\n    while path:\n        path = os.path.dirname(path)\n\n        if path == last_path:\n            break\n        last_path = path\n\n        if path:\n            # don't yield ''\n            out.insert(0, path)\n    return out\n\n\ndef sorted_walk(\n    path: PathLike,\n    ignore: Sequence[PathLike] = (),\n    ignore_hidden: bool = False,\n    logger: Logger | None = None,\n) -> Iterator[tuple[bytes, Sequence[bytes], Sequence[bytes]]]:\n    \"\"\"Like `os.walk`, but yields things in case-insensitive sorted,\n    breadth-first order.  Directory and file names matching any glob\n    pattern in `ignore` are skipped. If `logger` is provided, then\n    warning messages are logged there when a directory cannot be listed.\n    \"\"\"\n    # Make sure the paths aren't Unicode strings.\n    bytes_path = bytestring_path(path)\n    ignore_bytes = [  # rename prevents mypy variable shadowing issue\n        bytestring_path(i) for i in ignore\n    ]\n\n    # Get all the directories and files at this level.\n    try:\n        contents = os.listdir(syspath(bytes_path))\n    except OSError:\n        if logger:\n            logger.warning(\n                \"could not list directory {}\",\n                displayable_path(bytes_path),\n                exc_info=True,\n            )\n        return\n    dirs = []\n    files = []\n    for str_base in contents:\n        base = bytestring_path(str_base)\n\n        # Skip ignored filenames.\n        skip = False\n        for pat in ignore_bytes:\n            if fnmatch.fnmatch(base, pat):\n                if logger:\n                    logger.debug(\n                        \"ignoring '{}' due to ignore rule '{}'\", base, pat\n                    )\n                skip = True\n                break\n        if skip:\n            continue\n\n        # Add to output as either a file or a directory.\n        cur = os.path.join(bytes_path, base)\n        if (ignore_hidden and not hidden.is_hidden(cur)) or not ignore_hidden:\n            if os.path.isdir(syspath(cur)):\n                dirs.append(base)\n            else:\n                files.append(base)\n\n    # Sort lists (case-insensitive) and yield the current level.\n    dirs.sort(key=bytes.lower)\n    files.sort(key=bytes.lower)\n    yield (bytes_path, dirs, files)\n\n    # Recurse into directories.\n    for base in dirs:\n        cur = os.path.join(bytes_path, base)\n        yield from sorted_walk(cur, ignore_bytes, ignore_hidden, logger)\n\n\ndef path_as_posix(path: bytes) -> bytes:\n    \"\"\"Return the string representation of the path with forward (/)\n    slashes.\n    \"\"\"\n    return path.replace(b\"\\\\\", b\"/\")\n\n\ndef mkdirall(path: bytes):\n    \"\"\"Make all the enclosing directories of path (like mkdir -p on the\n    parent).\n    \"\"\"\n    for ancestor in ancestry(path):\n        if not os.path.isdir(syspath(ancestor)):\n            try:\n                os.mkdir(syspath(ancestor))\n            except OSError as exc:\n                raise FilesystemError(\n                    exc, \"create\", (ancestor,), traceback.format_exc()\n                )\n\n\ndef fnmatch_all(names: Sequence[bytes], patterns: Sequence[bytes]) -> bool:\n    \"\"\"Determine whether all strings in `names` match at least one of\n    the `patterns`, which should be shell glob expressions.\n    \"\"\"\n    for name in names:\n        matches = False\n        for pattern in patterns:\n            matches = fnmatch.fnmatch(name, pattern)\n            if matches:\n                break\n        if not matches:\n            return False\n    return True\n\n\ndef prune_dirs(\n    path: PathLike,\n    root: PathLike | None = None,\n    clutter: Sequence[str] = (\".DS_Store\", \"Thumbs.db\"),\n):\n    \"\"\"If path is an empty directory, then remove it. Recursively remove\n    path's ancestry up to root (which is never removed) where there are\n    empty directories. If path is not contained in root, then nothing is\n    removed. Glob patterns in clutter are ignored when determining\n    emptiness. If root is not provided, then only path may be removed\n    (i.e., no recursive removal).\n    \"\"\"\n    path = normpath(path)\n    root = normpath(root) if root else None\n    ancestors = ancestry(path)\n\n    if root is None:\n        # Only remove the top directory.\n        ancestors = []\n    elif root in ancestors:\n        # Only remove directories below the root_bytes.\n        ancestors = ancestors[ancestors.index(root) + 1 :]\n    else:\n        # Remove nothing.\n        return\n\n    bytes_clutter = [bytestring_path(c) for c in clutter]\n\n    # Traverse upward from path.\n    ancestors.append(path)\n    ancestors.reverse()\n    for directory in ancestors:\n        str_directory = syspath(directory)\n        if not os.path.exists(directory):\n            # Directory gone already.\n            continue\n        match_paths = [bytestring_path(d) for d in os.listdir(str_directory)]\n        try:\n            if fnmatch_all(match_paths, bytes_clutter):\n                # Directory contains only clutter (or nothing).\n                shutil.rmtree(str_directory)\n            else:\n                break\n        except OSError:\n            break\n\n\ndef components(path: AnyStr) -> list[AnyStr]:\n    \"\"\"Return a list of the path components in path. For instance:\n\n       >>> components(b'/a/b/c')\n       ['a', 'b', 'c']\n\n    The argument should *not* be the result of a call to `syspath`.\n    \"\"\"\n    comps = []\n    ances = ancestry(path)\n    for anc in ances:\n        comp = os.path.basename(anc)\n        if comp:\n            comps.append(comp)\n        else:  # root\n            comps.append(anc)\n\n    last = os.path.basename(path)\n    if last:\n        comps.append(last)\n\n    return comps\n\n\ndef bytestring_path(path: PathLike) -> bytes:\n    \"\"\"Given a path, which is either a bytes or a unicode, returns a str\n    path (ensuring that we never deal with Unicode pathnames). Path should be\n    bytes but has safeguards for strings to be converted.\n    \"\"\"\n    # Pass through bytestrings.\n    if isinstance(path, bytes):\n        return path\n\n    str_path = str(path)\n\n    # On Windows, remove the magic prefix added by `syspath`. This makes\n    # ``bytestring_path(syspath(X)) == X``, i.e., we can safely\n    # round-trip through `syspath`.\n    if os.path.__name__ == \"ntpath\" and str_path.startswith(\n        WINDOWS_MAGIC_PREFIX\n    ):\n        str_path = str_path[len(WINDOWS_MAGIC_PREFIX) :]\n\n    return os.fsencode(str_path)\n\n\nPATH_SEP: bytes = bytestring_path(os.sep)\n\n\ndef displayable_path(\n    path: PathLike | Iterable[PathLike], separator: str = \"; \"\n) -> str:\n    \"\"\"Attempts to decode a bytestring path to a unicode object for the\n    purpose of displaying it to the user. If the `path` argument is a\n    list or a tuple, the elements are joined with `separator`.\n    \"\"\"\n\n    if isinstance(path, (list, tuple)):\n        return separator.join(displayable_path(p) for p in path)\n    elif isinstance(path, str):\n        return path\n    elif not isinstance(path, bytes):\n        # A non-string object: just get its unicode representation.\n        return str(path)\n\n    return os.fsdecode(path)\n\n\ndef syspath(path: PathLike, prefix: bool = True) -> str:\n    \"\"\"Convert a path for use by the operating system. In particular,\n    paths on Windows must receive a magic prefix and must be converted\n    to Unicode before they are sent to the OS. To disable the magic\n    prefix on Windows, set `prefix` to False---but only do this if you\n    *really* know what you're doing.\n    \"\"\"\n    str_path = os.fsdecode(path)\n    # Don't do anything if we're not on windows\n    if os.path.__name__ != \"ntpath\":\n        return str_path\n\n    # Add the magic prefix if it isn't already there.\n    # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247.aspx\n    if prefix and not str_path.startswith(WINDOWS_MAGIC_PREFIX):\n        if str_path.startswith(\"\\\\\\\\\"):\n            # UNC path. Final path should look like \\\\?\\UNC\\...\n            str_path = f\"UNC{str_path[1:]}\"\n        str_path = f\"{WINDOWS_MAGIC_PREFIX}{str_path}\"\n\n    return str_path\n\n\ndef samefile(p1: bytes, p2: bytes) -> bool:\n    \"\"\"Safer equality for paths.\"\"\"\n    if p1 == p2:\n        return True\n    with suppress(OSError):\n        return os.path.samefile(syspath(p1), syspath(p2))\n\n    return False\n\n\ndef remove(path: PathLike, soft: bool = True):\n    \"\"\"Remove the file. If `soft`, then no error will be raised if the\n    file does not exist.\n    \"\"\"\n    str_path = syspath(path)\n    if not str_path or (soft and not os.path.exists(str_path)):\n        return\n    try:\n        os.remove(str_path)\n    except OSError as exc:\n        raise FilesystemError(\n            exc, \"delete\", (str_path,), traceback.format_exc()\n        )\n\n\ndef copy(path: bytes, dest: bytes, replace: bool = False):\n    \"\"\"Copy a plain file. Permissions are not copied. If `dest` already\n    exists, raises a FilesystemError unless `replace` is True. Has no\n    effect if `path` is the same as `dest`. Paths are translated to\n    system paths before the syscall.\n    \"\"\"\n    if samefile(path, dest):\n        return\n    str_path = syspath(path)\n    str_dest = syspath(dest)\n    if not replace and os.path.exists(str_dest):\n        raise FilesystemError(\"file exists\", \"copy\", (str_path, str_dest))\n    try:\n        shutil.copyfile(str_path, str_dest)\n    except OSError as exc:\n        raise FilesystemError(\n            exc, \"copy\", (str_path, str_dest), traceback.format_exc()\n        )\n\n\ndef move(path: bytes, dest: bytes, replace: bool = False):\n    \"\"\"Rename a file. `dest` may not be a directory. If `dest` already\n    exists, raises an OSError unless `replace` is True. Has no effect if\n    `path` is the same as `dest`. Paths are translated to system paths.\n    \"\"\"\n    if os.path.isdir(syspath(path)):\n        raise FilesystemError(\"source is directory\", \"move\", (path, dest))\n    if os.path.isdir(syspath(dest)):\n        raise FilesystemError(\"destination is directory\", \"move\", (path, dest))\n    if samefile(path, dest):\n        return\n    if os.path.exists(syspath(dest)) and not replace:\n        raise FilesystemError(\"file exists\", \"rename\", (path, dest))\n\n    # First, try renaming the file.\n    try:\n        os.replace(syspath(path), syspath(dest))\n    except OSError:\n        # Copy the file to a temporary destination.\n        basename = os.path.basename(bytestring_path(dest))\n        dirname = os.path.dirname(bytestring_path(dest))\n        tmp = tempfile.NamedTemporaryFile(\n            suffix=\".beets\",\n            prefix=f\".{os.fsdecode(basename)}.\",\n            dir=syspath(dirname),\n            delete=False,\n        )\n        try:\n            with open(syspath(path), \"rb\") as f:\n                # mypy bug:\n                # - https://github.com/python/mypy/issues/15031\n                # - https://github.com/python/mypy/issues/14943\n                # Fix not yet released:\n                # - https://github.com/python/mypy/pull/14975\n                shutil.copyfileobj(f, tmp)  # type: ignore[misc]\n        finally:\n            tmp.close()\n\n        try:\n            # Copy file metadata\n            shutil.copystat(syspath(path), tmp.name)\n        except OSError:\n            # Ignore errors because it doesn't matter too much.  We may be on a\n            # filesystem that doesn't support this.\n            pass\n\n        # Move the copied file into place.\n        tmp_filename = tmp.name\n        try:\n            os.replace(tmp_filename, syspath(dest))\n            tmp_filename = \"\"\n            os.remove(syspath(path))\n        except OSError as exc:\n            raise FilesystemError(\n                exc, \"move\", (path, dest), traceback.format_exc()\n            )\n        finally:\n            if tmp_filename:\n                os.remove(tmp_filename)\n\n\ndef link(path: bytes, dest: bytes, replace: bool = False):\n    \"\"\"Create a symbolic link from path to `dest`. Raises an OSError if\n    `dest` already exists, unless `replace` is True. Does nothing if\n    `path` == `dest`.\n    \"\"\"\n    if samefile(path, dest):\n        return\n\n    if os.path.exists(syspath(dest)) and not replace:\n        raise FilesystemError(\"file exists\", \"rename\", (path, dest))\n    try:\n        os.symlink(syspath(path), syspath(dest))\n    except NotImplementedError:\n        # raised on python >= 3.2 and Windows versions before Vista\n        raise FilesystemError(\n            \"OS does not support symbolic links.link\",\n            (path, dest),\n            traceback.format_exc(),\n        )\n    except OSError as exc:\n        raise FilesystemError(exc, \"link\", (path, dest), traceback.format_exc())\n\n\ndef hardlink(path: bytes, dest: bytes, replace: bool = False):\n    \"\"\"Create a hard link from path to `dest`. Raises an OSError if\n    `dest` already exists, unless `replace` is True. Does nothing if\n    `path` == `dest`.\n    \"\"\"\n    if samefile(path, dest):\n        return\n\n    # Dereference symlinks, expand \"~\", and convert relative paths to absolute\n    origin_path = Path(os.fsdecode(path)).expanduser().resolve()\n    dest_path = Path(os.fsdecode(dest)).expanduser().resolve()\n\n    if dest_path.exists() and not replace:\n        raise FilesystemError(\"file exists\", \"rename\", (path, dest))\n    try:\n        dest_path.hardlink_to(origin_path)\n    except NotImplementedError:\n        raise FilesystemError(\n            \"OS does not support hard links.link\",\n            (path, dest),\n            traceback.format_exc(),\n        )\n    except OSError as exc:\n        if exc.errno == errno.EXDEV:\n            raise FilesystemError(\n                \"Cannot hard link across devices.link\",\n                (path, dest),\n                traceback.format_exc(),\n            )\n        else:\n            raise FilesystemError(\n                exc, \"link\", (path, dest), traceback.format_exc()\n            )\n\n\ndef reflink(\n    path: bytes,\n    dest: bytes,\n    replace: bool = False,\n    fallback: bool = False,\n):\n    \"\"\"Create a reflink from `dest` to `path`.\n\n    Raise an `OSError` if `dest` already exists, unless `replace` is\n    True. If `path` == `dest`, then do nothing.\n\n    If `fallback` is enabled, ignore errors and copy the file instead.\n    Otherwise, errors are re-raised as FilesystemError with an explanation.\n    \"\"\"\n    if samefile(path, dest):\n        return\n\n    if os.path.exists(syspath(dest)) and not replace:\n        raise FilesystemError(\"target exists\", \"rename\", (path, dest))\n\n    if fallback:\n        with suppress(Exception):\n            return import_module(\"reflink\").reflink(path, dest)\n        return copy(path, dest, replace)\n\n    try:\n        import_module(\"reflink\").reflink(path, dest)\n    except (ImportError, OSError):\n        raise\n    except Exception as exc:\n        msg = {\n            \"EXDEV\": \"Cannot reflink across devices\",\n            \"EOPNOTSUPP\": \"Device does not support reflinks\",\n        }.get(str(exc), \"OS does not support reflinks\")\n\n        raise FilesystemError(\n            msg, \"reflink\", (path, dest), traceback.format_exc()\n        ) from exc\n\n\ndef unique_path(path: bytes) -> bytes:\n    \"\"\"Returns a version of ``path`` that does not exist on the\n    filesystem. Specifically, if ``path` itself already exists, then\n    something unique is appended to the path.\n    \"\"\"\n    if not os.path.exists(syspath(path)):\n        return path\n\n    base, ext = os.path.splitext(path)\n    match = re.search(rb\"\\.(\\d)+$\", base)\n    if match:\n        num = int(match.group(1))\n        base = base[: match.start()]\n    else:\n        num = 0\n    while True:\n        num += 1\n        suffix = f\".{num}\".encode() + ext\n        new_path = base + suffix\n        if not os.path.exists(new_path):\n            return new_path\n\n\n# Note: The Windows \"reserved characters\" are, of course, allowed on\n# Unix. They are forbidden here because they cause problems on Samba\n# shares, which are sufficiently common as to cause frequent problems.\n# https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247.aspx\nCHAR_REPLACE = [\n    (re.compile(r\"[\\\\/]\"), \"_\"),  # / and \\ -- forbidden everywhere.\n    (re.compile(r\"^\\.\"), \"_\"),  # Leading dot (hidden files on Unix).\n    (re.compile(r\"[\\x00-\\x1f]\"), \"\"),  # Control characters.\n    (re.compile(r'[<>:\"\\?\\*\\|]'), \"_\"),  # Windows \"reserved characters\".\n    (re.compile(r\"\\.$\"), \"_\"),  # Trailing dots.\n    (re.compile(r\"\\s+$\"), \"\"),  # Trailing whitespace.\n]\n\n\ndef sanitize_path(path: str, replacements: Replacements | None = None) -> str:\n    \"\"\"Takes a path (as a Unicode string) and makes sure that it is\n    legal. Returns a new path. Only works with fragments; won't work\n    reliably on Windows when a path begins with a drive letter. Path\n    separators (including altsep!) should already be cleaned from the\n    path components. If replacements is specified, it is used *instead*\n    of the default set of replacements; it must be a list of (compiled\n    regex, replacement string) pairs.\n    \"\"\"\n    replacements = replacements or CHAR_REPLACE\n\n    comps = components(path)\n    if not comps:\n        return \"\"\n    for i, comp in enumerate(comps):\n        for regex, repl in replacements:\n            comp = regex.sub(repl, comp)\n        comps[i] = comp\n    return os.path.join(*comps)\n\n\ndef truncate_str(s: str, length: int) -> str:\n    \"\"\"Truncate the string to the given byte length.\n\n    If we end up truncating a unicode character in the middle (rendering it invalid),\n    it is removed:\n\n    >>> s = \"🎹🎶\"  # 8 bytes\n    >>> truncate_str(s, 6)\n    '🎹'\n    \"\"\"\n    return os.fsencode(s)[:length].decode(sys.getfilesystemencoding(), \"ignore\")\n\n\ndef truncate_path(str_path: str) -> str:\n    \"\"\"Truncate each path part to a legal length preserving the extension.\"\"\"\n    max_length = get_max_filename_length()\n    path = Path(str_path)\n    parent_parts = [truncate_str(p, max_length) for p in path.parts[:-1]]\n    stem = truncate_str(path.stem, max_length - len(path.suffix))\n    return f\"{Path(*parent_parts, stem)}{path.suffix}\"\n\n\ndef _legalize_stage(\n    path: str, replacements: Replacements | None, extension: str\n) -> tuple[str, bool]:\n    \"\"\"Perform a single round of path legalization steps\n    1. sanitation/replacement\n    2. appending the extension\n    3. truncation.\n\n    Return the path and whether truncation was required.\n    \"\"\"\n    # Perform an initial sanitization including user replacements.\n    path = sanitize_path(path, replacements)\n\n    # Preserve extension.\n    path += extension.lower()\n\n    # Truncate too-long components.\n    pre_truncate_path = path\n    path = truncate_path(path)\n\n    return path, path != pre_truncate_path\n\n\ndef legalize_path(\n    path: str, replacements: Replacements | None, extension: str\n) -> tuple[str, bool]:\n    \"\"\"Given a path-like Unicode string, produce a legal path. Return the path\n    and a flag indicating whether some replacements had to be ignored (see\n    below).\n\n    This function uses `_legalize_stage` function to legalize the path, see its\n    documentation for the details of what this involves. It is called up to\n    three times in case truncation conflicts with replacements (as can happen\n    when truncation creates whitespace at the end of the string, for example).\n\n    The limited number of iterations avoids the possibility of an infinite loop\n    of sanitation and truncation operations, which could be caused by\n    replacement rules that make the string longer.\n\n    The flag returned from this function indicates that the path has to be\n    truncated twice (indicating that replacements made the string longer again\n    after it was truncated); the application should probably log some sort of\n    warning.\n    \"\"\"\n    suffix = as_string(extension)\n\n    first_stage, _ = os.path.splitext(\n        _legalize_stage(path, replacements, suffix)[0]\n    )\n\n    # Re-sanitize following truncation (including user replacements).\n    second_stage, truncated = _legalize_stage(first_stage, replacements, suffix)\n\n    if not truncated:\n        return second_stage, False\n\n    # If the path was truncated, discard user replacements\n    # and run through one last legalization stage.\n    return _legalize_stage(first_stage, None, suffix)[0], True\n\n\ndef str2bool(value: str) -> bool:\n    \"\"\"Returns a boolean reflecting a human-entered string.\"\"\"\n    return value.lower() in (\"yes\", \"1\", \"true\", \"t\", \"y\")\n\n\ndef as_string(value: Any) -> str:\n    \"\"\"Convert a value to a Unicode object for matching with a query.\n    None becomes the empty string. Bytestrings are silently decoded.\n    \"\"\"\n    if value is None:\n        return \"\"\n    elif isinstance(value, memoryview):\n        return bytes(value).decode(\"utf-8\", \"ignore\")\n    elif isinstance(value, bytes):\n        return value.decode(\"utf-8\", \"ignore\")\n    else:\n        return str(value)\n\n\ndef plurality(objs: Iterable[T]) -> tuple[T, int]:\n    \"\"\"Given a sequence of hashble objects, returns the object that\n    is most common in the set and the its number of appearance. The\n    sequence must contain at least one object.\n    \"\"\"\n    c = Counter(objs)\n    if not c:\n        raise ValueError(\"sequence must be non-empty\")\n    return c.most_common(1)[0]\n\n\ndef get_most_common_tags(\n    items: Sequence[Item],\n) -> tuple[dict[str, Any], dict[str, Any]]:\n    \"\"\"Extract the likely current metadata for an album given a list of its\n    items. Return two dictionaries:\n     - The most common value for each field.\n     - Whether each field's value was unanimous (values are booleans).\n    \"\"\"\n    assert items  # Must be nonempty.\n\n    likelies = {}\n    consensus = {}\n    fields = [\n        \"artist\",\n        \"album\",\n        \"albumartist\",\n        \"year\",\n        \"disctotal\",\n        \"mb_albumid\",\n        \"label\",\n        \"barcode\",\n        \"catalognum\",\n        \"country\",\n        \"media\",\n        \"albumdisambig\",\n        \"data_source\",\n    ]\n    for field in fields:\n        values = [item.get(field) for item in items if item]\n        likelies[field], freq = plurality(values)\n        consensus[field] = freq == len(values)\n\n    # If there's an album artist consensus, use this for the artist.\n    if consensus[\"albumartist\"] and likelies[\"albumartist\"]:\n        likelies[\"artist\"] = likelies[\"albumartist\"]\n\n    return likelies, consensus\n\n\n# stdout and stderr as bytes\nclass CommandOutput(NamedTuple):\n    stdout: bytes\n    stderr: bytes\n\n\ndef command_output(\n    cmd: list[str] | list[bytes], shell: bool = False\n) -> CommandOutput:\n    \"\"\"Runs the command and returns its output after it has exited.\n\n    Returns a CommandOutput. The attributes ``stdout`` and ``stderr`` contain\n    byte strings of the respective output streams.\n\n    ``cmd`` is a list of arguments starting with the command names. The\n    arguments are bytes on Unix and strings on Windows.\n    If ``shell`` is true, ``cmd`` is assumed to be a string and passed to a\n    shell to execute.\n\n    If the process exits with a non-zero return code\n    ``subprocess.CalledProcessError`` is raised. May also raise\n    ``OSError``.\n\n    This replaces `subprocess.check_output` which can have problems if lots of\n    output is sent to stderr.\n    \"\"\"\n    devnull = subprocess.DEVNULL\n\n    proc = subprocess.Popen(\n        cmd,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE,\n        stdin=devnull,\n        close_fds=platform.system() != \"Windows\",\n        shell=shell,\n    )\n    stdout, stderr = proc.communicate()\n    if proc.returncode:\n        raise subprocess.CalledProcessError(\n            returncode=proc.returncode,\n            cmd=\" \".join(map(os.fsdecode, cmd)),\n            output=stdout + stderr,\n        )\n    return CommandOutput(stdout, stderr)\n\n\n@cache\ndef get_max_filename_length() -> int:\n    \"\"\"Attempt to determine the maximum filename length for the\n    filesystem containing `path`. If the value is greater than `limit`,\n    then `limit` is used instead (to prevent errors when a filesystem\n    misreports its capacity). If it cannot be determined (e.g., on\n    Windows), return `limit`.\n    \"\"\"\n    if length := beets.config[\"max_filename_length\"].get(int):\n        return length\n\n    limit = MAX_FILENAME_LENGTH\n    if hasattr(os, \"statvfs\"):\n        try:\n            res = os.statvfs(beets.config[\"directory\"].as_str())\n        except OSError:\n            return limit\n        return min(res[9], limit)\n    else:\n        return limit\n\n\ndef open_anything() -> str:\n    \"\"\"Return the system command that dispatches execution to the correct\n    program.\n    \"\"\"\n    sys_name = platform.system()\n    if sys_name == \"Darwin\":\n        base_cmd = \"open\"\n    elif sys_name == \"Windows\":\n        # `start` is a cmd.exe builtin, so invoke it through the shell.\n        base_cmd = 'cmd /c start \"\"'\n    else:  # Assume Unix\n        base_cmd = \"xdg-open\"\n    return base_cmd\n\n\ndef editor_command() -> str:\n    \"\"\"Get a command for opening a text file.\n\n    First try environment variable `VISUAL` followed by `EDITOR`. As last resort\n    fall back to `open_anything()`, the platform-specific tool for opening files\n    in general.\n\n    \"\"\"\n    return (\n        os.environ.get(\"VISUAL\") or os.environ.get(\"EDITOR\") or open_anything()\n    )\n\n\ndef interactive_open(targets: Sequence[str], command: str):\n    \"\"\"Open the files in `targets` by `exec`ing a new `command`, given\n    as a Unicode string. (The new program takes over, and Python\n    execution ends: this does not fork a subprocess.)\n\n    Can raise `OSError`.\n    \"\"\"\n    assert command\n\n    # Split the command string into its arguments.\n    try:\n        args = shlex.split(command)\n    except ValueError:  # Malformed shell tokens.\n        args = [command]\n\n    args.insert(0, args[0])  # for argv[0]\n\n    args += targets\n\n    return os.execlp(*args)\n\n\ndef case_sensitive(path: bytes) -> bool:\n    \"\"\"Check whether the filesystem at the given path is case sensitive.\n\n    To work best, the path should point to a file or a directory. If the path\n    does not exist, assume a case sensitive file system on every platform\n    except Windows.\n\n    Currently only used for absolute paths by beets; may have a trailing\n    path separator.\n    \"\"\"\n    # Look at parent paths until we find a path that actually exists, or\n    # reach the root.\n    while True:\n        head, tail = os.path.split(path)\n        if head == path:\n            # We have reached the root of the file system.\n            # By default, the case sensitivity depends on the platform.\n            return platform.system() != \"Windows\"\n\n        # Trailing path separator, or path does not exist.\n        if not tail or not os.path.exists(path):\n            path = head\n            continue\n\n        upper_tail = tail.upper()\n        lower_tail = tail.lower()\n\n        # In case we can't tell from the given path name, look at the\n        # parent directory.\n        if upper_tail == lower_tail:\n            path = head\n            continue\n\n        upper_sys = syspath(os.path.join(head, upper_tail))\n        lower_sys = syspath(os.path.join(head, lower_tail))\n\n        # If either the upper-cased or lower-cased path does not exist, the\n        # filesystem must be case-sensitive.\n        # (Otherwise, we have more work to do.)\n        if not os.path.exists(upper_sys) or not os.path.exists(lower_sys):\n            return True\n\n        # Original and both upper- and lower-cased versions of the path\n        # exist on the file system. Check whether they refer to different\n        # files by their inodes (or an alternative method on Windows).\n        return not os.path.samefile(lower_sys, upper_sys)\n\n\ndef asciify_path(path: str, sep_replace: str) -> str:\n    \"\"\"Decodes all unicode characters in a path into ASCII equivalents.\n\n    Substitutions are provided by the unidecode module. Path separators in the\n    input are preserved.\n\n    Keyword arguments:\n    path -- The path to be asciified.\n    sep_replace -- the string to be used to replace extraneous path separators.\n    \"\"\"\n    # if this platform has an os.altsep, change it to os.sep.\n    if os.altsep:\n        path = path.replace(os.altsep, os.sep)\n    path_components: list[str] = path.split(os.sep)\n    for index, item in enumerate(path_components):\n        path_components[index] = unidecode(item).replace(os.sep, sep_replace)\n        if os.altsep:\n            path_components[index] = unidecode(item).replace(\n                os.altsep, sep_replace\n            )\n    return os.sep.join(path_components)\n\n\ndef par_map(transform: Callable[[T], Any], items: Sequence[T]) -> None:\n    \"\"\"Apply the function `transform` to all the elements in the\n    iterable `items`, like `map(transform, items)` but with no return\n    value.\n\n    The parallelism uses threads (not processes), so this is only useful\n    for IO-bound `transform`s.\n    \"\"\"\n    pool = ThreadPool()\n    pool.map(transform, items)\n    pool.close()\n    pool.join()\n\n\nclass cached_classproperty(Generic[T]):\n    \"\"\"Descriptor implementing cached class properties.\n\n    Provides class-level dynamic property behavior where the getter function is\n    called once per class and the result is cached for subsequent access. Unlike\n    instance properties, this operates on the class rather than instances.\n    \"\"\"\n\n    cache: ClassVar[dict[tuple[type[object], str], object]] = {}\n\n    name: str = \"\"\n\n    # Ideally, we would like to use `Callable[[type[T]], Any]` here,\n    # however, `mypy` is unable to see this as a **class** property, and thinks\n    # that this callable receives an **instance** of the object, failing the\n    # type check, for example:\n    # >>> class Album:\n    # >>>     @cached_classproperty\n    # >>>     def foo(cls):\n    # >>>         reveal_type(cls)  # mypy: revealed type is \"Album\"\n    # >>>         return cls.bar\n    #\n    #   Argument 1 to \"cached_classproperty\" has incompatible type\n    #   \"Callable[[Album], ...]\"; expected \"Callable[[type[Album]], ...]\"\n    #\n    # Therefore, we just use `Any` here, which is not ideal, but works.\n    def __init__(self, getter: Callable[..., T]) -> None:\n        \"\"\"Initialize the descriptor with the property getter function.\"\"\"\n        self.getter: Callable[..., T] = getter\n\n    def __set_name__(self, owner: object, name: str) -> None:\n        \"\"\"Capture the attribute name this descriptor is assigned to.\"\"\"\n        self.name = name\n\n    def __get__(self, instance: object, owner: type[object]) -> T:\n        \"\"\"Compute and cache if needed, and return the property value.\"\"\"\n        key: tuple[type[object], str] = owner, self.name\n        if key not in self.cache:\n            self.cache[key] = self.getter(owner)\n\n        return cast(T, self.cache[key])\n\n\nclass LazySharedInstance(Generic[T]):\n    \"\"\"A descriptor that provides access to a lazily-created shared instance of\n    the containing class, while calling the class constructor to construct a\n    new object works as usual.\n\n    ```\n    ID: int = 0\n\n    class Foo:\n        def __init__():\n            global ID\n\n            self.id = ID\n            ID += 1\n\n        def func(self):\n            print(self.id)\n\n        shared: LazySharedInstance[Foo] = LazySharedInstance()\n\n    a0 = Foo()\n    a1 = Foo.shared\n    a2 = Foo()\n    a3 = Foo.shared\n\n    a0.func()  # 0\n    a1.func()  # 1\n    a2.func()  # 2\n    a3.func()  # 1\n    ```\n    \"\"\"\n\n    _instance: T | None = None\n\n    def __get__(self, instance: T | None, owner: type[T]) -> T:\n        if instance is not None:\n            raise RuntimeError(\n                \"shared instances must be obtained from the class property, \"\n                \"not an instance\"\n            )\n\n        if self._instance is None:\n            self._instance = owner()\n\n        return self._instance\n\n\ndef get_module_tempdir(module: str) -> Path:\n    \"\"\"Return the temporary directory for the given module.\n\n    The directory is created within the `/tmp/beets/<module>` directory on\n    Linux (or the equivalent temporary directory on other systems).\n\n    Dots in the module name are replaced by underscores.\n    \"\"\"\n    module = module.replace(\"beets.\", \"\").replace(\".\", \"_\")\n    return Path(tempfile.gettempdir()) / \"beets\" / module\n\n\ndef clean_module_tempdir(module: str) -> None:\n    \"\"\"Clean the temporary directory for the given module.\"\"\"\n    tempdir = get_module_tempdir(module)\n    shutil.rmtree(tempdir, ignore_errors=True)\n    with suppress(OSError):\n        # remove parent (/tmp/beets) directory if it is empty\n        tempdir.parent.rmdir()\n\n\ndef get_temp_filename(\n    module: str,\n    prefix: str = \"\",\n    path: PathLike | None = None,\n    suffix: str = \"\",\n) -> bytes:\n    \"\"\"Return temporary filename for the given module and prefix.\n\n    The filename starts with the given `prefix`.\n    If 'suffix' is given, it is used a the file extension.\n    If 'path' is given, we use the same suffix.\n    \"\"\"\n    if not suffix and path:\n        suffix = Path(os.fsdecode(path)).suffix\n\n    tempdir = get_module_tempdir(module)\n    tempdir.mkdir(parents=True, exist_ok=True)\n\n    descriptor, filename = tempfile.mkstemp(\n        dir=tempdir, prefix=prefix, suffix=suffix\n    )\n    os.close(descriptor)\n    return bytestring_path(filename)\n\n\ndef unique_list(elements: Iterable[T]) -> list[T]:\n    \"\"\"Return a list with unique elements in the original order.\"\"\"\n    return list(dict.fromkeys(elements))\n"
  },
  {
    "path": "beets/util/artresizer.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Fabrice Laporte\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Abstraction layer to resize images using PIL, ImageMagick, or a\npublic resizing proxy if neither is available.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport os.path\nimport platform\nimport re\nimport subprocess\nfrom abc import ABC, abstractmethod\nfrom contextlib import suppress\nfrom enum import Enum\nfrom itertools import chain\nfrom typing import TYPE_CHECKING, Any, ClassVar\nfrom urllib.parse import urlencode\n\nfrom beets import logging, util\nfrom beets.util import (\n    LazySharedInstance,\n    displayable_path,\n    get_temp_filename,\n    syspath,\n)\n\nif TYPE_CHECKING:\n    from collections.abc import Mapping\n\nPROXY_URL = \"https://images.weserv.nl/\"\n\nlog = logging.getLogger(\"beets\")\n\n\ndef resize_url(url: str, maxwidth: int, quality: int = 0) -> str:\n    \"\"\"Return a proxied image URL that resizes the original image to\n    maxwidth (preserving aspect ratio).\n    \"\"\"\n    params = {\n        \"url\": url.replace(\"http://\", \"\"),\n        \"w\": maxwidth,\n    }\n\n    if quality > 0:\n        params[\"q\"] = quality\n\n    return f\"{PROXY_URL}?{urlencode(params)}\"\n\n\nclass LocalBackendNotAvailableError(Exception):\n    pass\n\n\n# Singleton pattern that the typechecker understands:\n# https://peps.python.org/pep-0484/#support-for-singleton-types-in-unions\nclass NotAvailable(Enum):\n    token = 0\n\n\n_NOT_AVAILABLE = NotAvailable.token\n\n\nclass LocalBackend(ABC):\n    NAME: ClassVar[str]\n\n    @classmethod\n    @abstractmethod\n    def version(cls) -> Any:\n        \"\"\"Return the backend version if its dependencies are satisfied or\n        raise `LocalBackendNotAvailableError`.\n        \"\"\"\n        pass\n\n    @classmethod\n    def available(cls) -> bool:\n        \"\"\"Return `True` this backend's dependencies are satisfied and it can\n        be used, `False` otherwise.\"\"\"\n        try:\n            cls.version()\n            return True\n        except LocalBackendNotAvailableError:\n            return False\n\n    @abstractmethod\n    def resize(\n        self,\n        maxwidth: int,\n        path_in: bytes,\n        path_out: bytes | None = None,\n        quality: int = 0,\n        max_filesize: int = 0,\n    ) -> bytes:\n        \"\"\"Resize an image to the given width and return the output path.\n\n        On error, logs a warning and returns `path_in`.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_size(self, path_in: bytes) -> tuple[int, int] | None:\n        \"\"\"Return the (width, height) of the image or None if unavailable.\"\"\"\n        pass\n\n    @abstractmethod\n    def deinterlace(\n        self,\n        path_in: bytes,\n        path_out: bytes | None = None,\n    ) -> bytes:\n        \"\"\"Remove interlacing from an image and return the output path.\n\n        On error, logs a warning and returns `path_in`.\n        \"\"\"\n        pass\n\n    @abstractmethod\n    def get_format(self, path_in: bytes) -> str | None:\n        \"\"\"Return the image format (e.g., 'PNG') or None if undetectable.\"\"\"\n        pass\n\n    @abstractmethod\n    def convert_format(\n        self,\n        source: bytes,\n        target: bytes,\n        deinterlaced: bool,\n    ) -> bytes:\n        \"\"\"Convert an image to a new format and return the new file path.\n\n        On error, logs a warning and returns `source`.\n        \"\"\"\n        pass\n\n    @property\n    def can_compare(self) -> bool:\n        \"\"\"Indicate whether image comparison is supported by this backend.\"\"\"\n        return False\n\n    def compare(\n        self,\n        im1: bytes,\n        im2: bytes,\n        compare_threshold: float,\n    ) -> bool | None:\n        \"\"\"Compare two images and return `True` if they are similar enough, or\n        `None` if there is an error.\n\n        This must only be called if `self.can_compare()` returns `True`.\n        \"\"\"\n        # It is an error to call this when ArtResizer.can_compare is not True.\n        raise NotImplementedError()\n\n    @property\n    def can_write_metadata(self) -> bool:\n        \"\"\"Indicate whether writing metadata to images is supported.\"\"\"\n        return False\n\n    def write_metadata(self, file: bytes, metadata: Mapping[str, str]) -> None:\n        \"\"\"Write key-value metadata into the image file.\n\n        This must only be called if `self.can_write_metadata()` returns `True`.\n        \"\"\"\n        # It is an error to call this when ArtResizer.can_write_metadata is not True.\n        raise NotImplementedError()\n\n\nclass IMBackend(LocalBackend):\n    NAME = \"ImageMagick\"\n\n    # These fields are used as a cache for `version()`. `_legacy` indicates\n    # whether the modern `magick` binary is available or whether to fall back\n    # to the old-style `convert`, `identify`, etc. commands.\n    _version: tuple[int, int, int] | NotAvailable | None = None\n    _legacy: bool | None = None\n\n    @classmethod\n    def version(cls) -> tuple[int, int, int]:\n        \"\"\"Obtain and cache ImageMagick version.\n\n        Raises `LocalBackendNotAvailableError` if not available.\n        \"\"\"\n        if cls._version is None:\n            for cmd_name, legacy in ((\"magick\", False), (\"convert\", True)):\n                try:\n                    out = util.command_output([cmd_name, \"--version\"]).stdout\n                except (subprocess.CalledProcessError, OSError) as exc:\n                    log.debug(\"ImageMagick version check failed: {}\", exc)\n                    cls._version = _NOT_AVAILABLE\n                else:\n                    if b\"imagemagick\" in out.lower():\n                        pattern = rb\".+ (\\d+)\\.(\\d+)\\.(\\d+).*\"\n                        match = re.search(pattern, out)\n                        if match:\n                            cls._version = (\n                                int(match.group(1)),\n                                int(match.group(2)),\n                                int(match.group(3)),\n                            )\n                            cls._legacy = legacy\n\n        # cls._version is never None here, but mypy doesn't get that\n        if cls._version is _NOT_AVAILABLE or cls._version is None:\n            raise LocalBackendNotAvailableError()\n        else:\n            return cls._version\n\n    convert_cmd: list[str]\n    identify_cmd: list[str]\n    compare_cmd: list[str]\n\n    def __init__(self) -> None:\n        \"\"\"Initialize a wrapper around ImageMagick for local image operations.\n\n        Stores the ImageMagick version and legacy flag. If ImageMagick is not\n        available, raise an Exception.\n        \"\"\"\n        self.version()\n\n        # Use ImageMagick's magick binary when it's available.\n        # If it's not, fall back to the older, separate convert\n        # and identify commands.\n        if self._legacy:\n            self.convert_cmd = [\"convert\"]\n            self.identify_cmd = [\"identify\"]\n            self.compare_cmd = [\"compare\"]\n        else:\n            self.convert_cmd = [\"magick\"]\n            self.identify_cmd = [\"magick\", \"identify\"]\n            self.compare_cmd = [\"magick\", \"compare\"]\n\n    def resize(\n        self,\n        maxwidth: int,\n        path_in: bytes,\n        path_out: bytes | None = None,\n        quality: int = 0,\n        max_filesize: int = 0,\n    ) -> bytes:\n        \"\"\"Resize using ImageMagick.\n\n        Use the ``magick`` program or ``convert`` on older versions. Return\n        the output path of resized image.\n        \"\"\"\n        if not path_out:\n            path_out = get_temp_filename(__name__, \"resize_IM_\", path_in)\n\n        log.debug(\n            \"artresizer: ImageMagick resizing {} to {}\",\n            displayable_path(path_in),\n            displayable_path(path_out),\n        )\n\n        # \"-resize WIDTHx>\" shrinks images with the width larger\n        # than the given width while maintaining the aspect ratio\n        # with regards to the height.\n        # ImageMagick already seems to default to no interlace, but we include\n        # it here for the sake of explicitness.\n        cmd: list[str] = [\n            *self.convert_cmd,\n            syspath(path_in, prefix=False),\n            \"-resize\",\n            f\"{maxwidth}x>\",\n            \"-interlace\",\n            \"none\",\n        ]\n\n        if quality > 0:\n            cmd += [\"-quality\", f\"{quality}\"]\n\n        # \"-define jpeg:extent=SIZEb\" sets the target filesize for imagemagick\n        # to SIZE in bytes.\n        if max_filesize > 0:\n            cmd += [\"-define\", f\"jpeg:extent={max_filesize}b\"]\n\n        cmd.append(syspath(path_out, prefix=False))\n\n        try:\n            util.command_output(cmd)\n        except subprocess.CalledProcessError:\n            log.warning(\n                \"artresizer: IM convert failed for {}\",\n                displayable_path(path_in),\n            )\n            return path_in\n\n        return path_out\n\n    def get_size(self, path_in: bytes) -> tuple[int, int] | None:\n        cmd: list[str] = [\n            *self.identify_cmd,\n            \"-format\",\n            \"%w %h\",\n            syspath(path_in, prefix=False),\n        ]\n\n        try:\n            out = util.command_output(cmd).stdout\n        except subprocess.CalledProcessError as exc:\n            log.warning(\"ImageMagick size query failed\")\n            log.debug(\n                \"`convert` exited with (status {.returncode}) when \"\n                \"getting size with command {}:\\n{}\",\n                exc,\n                cmd,\n                exc.output.strip(),\n            )\n            return None\n        try:\n            size = tuple(map(int, out.split(b\" \")))\n        except IndexError:\n            log.warning(\"Could not understand IM output: {0!r}\", out)\n            return None\n\n        if len(size) != 2:\n            log.warning(\"Could not understand IM output: {0!r}\", out)\n            return None\n\n        return size\n\n    def deinterlace(\n        self,\n        path_in: bytes,\n        path_out: bytes | None = None,\n    ) -> bytes:\n        if not path_out:\n            path_out = get_temp_filename(__name__, \"deinterlace_IM_\", path_in)\n\n        cmd = [\n            *self.convert_cmd,\n            syspath(path_in, prefix=False),\n            \"-interlace\",\n            \"none\",\n            syspath(path_out, prefix=False),\n        ]\n\n        try:\n            util.command_output(cmd)\n            return path_out\n        except subprocess.CalledProcessError:\n            # FIXME: Should probably issue a warning?\n            return path_in\n\n    def get_format(self, path_in: bytes) -> str | None:\n        cmd = [*self.identify_cmd, \"-format\", \"%[magick]\", syspath(path_in)]\n\n        try:\n            # Image formats should really only be ASCII strings such as \"PNG\",\n            # if anything else is returned, something is off and we return\n            # None for safety.\n            return util.command_output(cmd).stdout.decode(\"ascii\", \"strict\")\n        except (subprocess.CalledProcessError, UnicodeError):\n            # FIXME: Should probably issue a warning?\n            return None\n\n    def convert_format(\n        self,\n        source: bytes,\n        target: bytes,\n        deinterlaced: bool,\n    ) -> bytes:\n        cmd = [\n            *self.convert_cmd,\n            syspath(source),\n            *([\"-interlace\", \"none\"] if deinterlaced else []),\n            syspath(target),\n        ]\n\n        try:\n            subprocess.check_call(\n                cmd, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL\n            )\n            return target\n        except subprocess.CalledProcessError:\n            # FIXME: Should probably issue a warning?\n            return source\n\n    @property\n    def can_compare(self) -> bool:\n        return self.version() > (6, 8, 7)\n\n    def compare(\n        self,\n        im1: bytes,\n        im2: bytes,\n        compare_threshold: float,\n    ) -> bool | None:\n        is_windows = platform.system() == \"Windows\"\n\n        # Converting images to grayscale tends to minimize the weight\n        # of colors in the diff score. So we first convert both images\n        # to grayscale and then pipe them into the `compare` command.\n        # On Windows, ImageMagick doesn't support the magic \\\\?\\ prefix\n        # on paths, so we pass `prefix=False` to `syspath`.\n        convert_cmd = [\n            *self.convert_cmd,\n            syspath(im2, prefix=False),\n            syspath(im1, prefix=False),\n            \"-colorspace\",\n            \"gray\",\n            \"MIFF:-\",\n        ]\n        compare_cmd = [\n            *self.compare_cmd,\n            \"-define\",\n            \"phash:colorspaces=sRGB,HCLp\",\n            \"-metric\",\n            \"PHASH\",\n            \"-\",\n            \"null:\",\n        ]\n        log.debug(\n            \"comparing images with pipeline {} | {}\", convert_cmd, compare_cmd\n        )\n        convert_proc = subprocess.Popen(\n            convert_cmd,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            close_fds=not is_windows,\n        )\n        compare_proc = subprocess.Popen(\n            compare_cmd,\n            stdin=convert_proc.stdout,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            close_fds=not is_windows,\n        )\n\n        # help out mypy\n        assert convert_proc.stdout is not None\n        assert convert_proc.stderr is not None\n\n        # Check the convert output. We're not interested in the\n        # standard output; that gets piped to the next stage.\n        convert_proc.stdout.close()\n        convert_stderr = convert_proc.stderr.read()\n        convert_proc.stderr.close()\n        convert_proc.wait()\n        if convert_proc.returncode:\n            log.debug(\n                \"ImageMagick convert failed with status {.returncode}: {!r}\",\n                convert_proc,\n                convert_stderr,\n            )\n            return None\n\n        # Check the compare output.\n        stdout, stderr = compare_proc.communicate()\n        if compare_proc.returncode:\n            if compare_proc.returncode != 1:\n                log.debug(\n                    \"ImageMagick compare failed: {}, {}\",\n                    displayable_path(im2),\n                    displayable_path(im1),\n                )\n                return None\n            out_str = stderr\n        else:\n            out_str = stdout\n\n        # ImageMagick 7.1.1-44 outputs in a different format.\n        if b\"(\" in out_str and out_str.endswith(b\")\"):\n            # Extract diff from \"... (diff)\".\n            out_str = out_str[out_str.index(b\"(\") + 1 : -1]\n\n        try:\n            phash_diff = float(out_str)\n        except ValueError:\n            log.debug(\"IM output is not a number: {0!r}\", out_str)\n            return None\n\n        log.debug(\"ImageMagick compare score: {}\", phash_diff)\n        return phash_diff <= compare_threshold\n\n    @property\n    def can_write_metadata(self) -> bool:\n        return True\n\n    def write_metadata(self, file: bytes, metadata: Mapping[str, str]) -> None:\n        assignments = chain.from_iterable(\n            (\"-set\", k, v) for k, v in metadata.items()\n        )\n        str_file = os.fsdecode(file)\n        command = [*self.convert_cmd, str_file, *assignments, str_file]\n\n        util.command_output(command)\n\n\nclass PILBackend(LocalBackend):\n    NAME = \"PIL\"\n\n    @classmethod\n    def version(cls) -> None:\n        try:\n            __import__(\"PIL\", fromlist=[\"Image\"])\n        except ImportError:\n            raise LocalBackendNotAvailableError()\n\n    def __init__(self) -> None:\n        \"\"\"Initialize a wrapper around PIL for local image operations.\n\n        If PIL is not available, raise an Exception.\n        \"\"\"\n        self.version()\n\n    def resize(\n        self,\n        maxwidth: int,\n        path_in: bytes,\n        path_out: bytes | None = None,\n        quality: int = 0,\n        max_filesize: int = 0,\n    ) -> bytes:\n        \"\"\"Resize using Python Imaging Library (PIL).  Return the output path\n        of resized image.\n        \"\"\"\n        if not path_out:\n            path_out = get_temp_filename(__name__, \"resize_PIL_\", path_in)\n\n        from PIL import Image\n\n        log.debug(\n            \"artresizer: PIL resizing {} to {}\",\n            displayable_path(path_in),\n            displayable_path(path_out),\n        )\n\n        try:\n            im = Image.open(syspath(path_in))\n            size = maxwidth, maxwidth\n            im.thumbnail(size, Image.Resampling.LANCZOS)\n\n            if quality == 0:\n                # Use PIL's default quality.\n                quality = -1\n\n            # progressive=False only affects JPEGs and is the default,\n            # but we include it here for explicitness.\n            im.save(os.fsdecode(path_out), quality=quality, progressive=False)\n\n            if max_filesize > 0:\n                # If maximum filesize is set, we attempt to lower the quality\n                # of jpeg conversion by a proportional amount, up to 3 attempts\n                # First, set the maximum quality to either provided, or 95\n                if quality > 0:\n                    lower_qual = quality\n                else:\n                    lower_qual = 95\n                for i in range(5):\n                    # 5 attempts is an arbitrary choice\n                    filesize = os.stat(syspath(path_out)).st_size\n                    log.debug(\"PIL Pass {} : Output size: {}B\", i, filesize)\n                    if filesize <= max_filesize:\n                        return path_out\n                    # The relationship between filesize & quality will be\n                    # image dependent.\n                    lower_qual -= 10\n                    # Restrict quality dropping below 10\n                    if lower_qual < 10:\n                        lower_qual = 10\n                    # Use optimize flag to improve filesize decrease\n                    im.save(\n                        os.fsdecode(path_out),\n                        quality=lower_qual,\n                        optimize=True,\n                        progressive=False,\n                    )\n                log.warning(\n                    \"PIL Failed to resize file to below {}B\", max_filesize\n                )\n                return path_out\n\n            else:\n                return path_out\n        except OSError:\n            log.error(\n                \"PIL cannot create thumbnail for '{}'\",\n                displayable_path(path_in),\n            )\n            return path_in\n\n    def get_size(self, path_in: bytes) -> tuple[int, int] | None:\n        from PIL import Image\n\n        try:\n            im = Image.open(syspath(path_in))\n            return im.size\n        except OSError as exc:\n            log.error(\n                \"PIL could not read file {}: {}\", displayable_path(path_in), exc\n            )\n            return None\n\n    def deinterlace(\n        self,\n        path_in: bytes,\n        path_out: bytes | None = None,\n    ) -> bytes:\n        if not path_out:\n            path_out = get_temp_filename(__name__, \"deinterlace_PIL_\", path_in)\n\n        from PIL import Image\n\n        try:\n            im = Image.open(syspath(path_in))\n            im.save(os.fsdecode(path_out), progressive=False)\n            return path_out\n        except OSError:\n            # FIXME: Should probably issue a warning?\n            return path_in\n\n    def get_format(self, path_in: bytes) -> str | None:\n        from PIL import Image, UnidentifiedImageError\n\n        try:\n            with Image.open(syspath(path_in)) as im:\n                return im.format\n        except (\n            ValueError,\n            TypeError,\n            UnidentifiedImageError,\n            FileNotFoundError,\n        ):\n            log.exception(\"failed to detect image format for {}\", path_in)\n            return None\n\n    def convert_format(\n        self,\n        source: bytes,\n        target: bytes,\n        deinterlaced: bool,\n    ) -> bytes:\n        from PIL import Image, UnidentifiedImageError\n\n        try:\n            with Image.open(syspath(source)) as im:\n                im.save(os.fsdecode(target), progressive=not deinterlaced)\n                return target\n        except (\n            ValueError,\n            TypeError,\n            UnidentifiedImageError,\n            FileNotFoundError,\n            OSError,\n        ):\n            log.exception(\"failed to convert image {} -> {}\", source, target)\n            return source\n\n    @property\n    def can_compare(self) -> bool:\n        return False\n\n    def compare(\n        self,\n        im1: bytes,\n        im2: bytes,\n        compare_threshold: float,\n    ) -> bool | None:\n        # It is an error to call this when ArtResizer.can_compare is not True.\n        raise NotImplementedError()\n\n    @property\n    def can_write_metadata(self) -> bool:\n        return True\n\n    def write_metadata(self, file: bytes, metadata: Mapping[str, str]) -> None:\n        from PIL import Image, PngImagePlugin\n\n        # FIXME: Detect and handle other file types (currently, the only user\n        # is the thumbnails plugin, which generates PNG images).\n        im = Image.open(syspath(file))\n        meta = PngImagePlugin.PngInfo()\n        for k, v in metadata.items():\n            meta.add_text(k, v, zip=False)\n        im.save(os.fsdecode(file), \"PNG\", pnginfo=meta)\n\n\nBACKEND_CLASSES: list[type[LocalBackend]] = [\n    IMBackend,\n    PILBackend,\n]\n\n\nclass ArtResizer:\n    \"\"\"A class that dispatches image operations to an available backend.\"\"\"\n\n    local_method: LocalBackend | None\n\n    def __init__(self) -> None:\n        \"\"\"Create a resizer object with an inferred method.\"\"\"\n        # Check if a local backend is available, and store an instance of the\n        # backend class. Otherwise, fallback to the web proxy.\n        for backend_cls in BACKEND_CLASSES:\n            try:\n                self.local_method = backend_cls()\n                log.debug(\"artresizer: method is {.local_method.NAME}\", self)\n                break\n            except LocalBackendNotAvailableError:\n                continue\n        else:\n            # FIXME: Turn WEBPROXY into a backend class as well to remove all\n            # the special casing. Then simply delegate all methods to the\n            # backends. (How does proxy_url fit in here, however?)\n            # Use an ABC (or maybe a typing Protocol?) for backend\n            # methods, such that both individual backends as well as\n            # ArtResizer implement it.\n            # It should probably be configurable which backends classes to\n            # consider, similar to fetchart or lyrics backends (i.e. a list\n            # of backends sorted by priority).\n            log.debug(\"artresizer: method is WEBPROXY\")\n            self.local_method = None\n\n    shared: LazySharedInstance[ArtResizer] = LazySharedInstance()\n\n    @property\n    def method(self) -> str:\n        if self.local_method is not None:\n            return self.local_method.NAME\n        else:\n            return \"WEBPROXY\"\n\n    def resize(\n        self,\n        maxwidth: int,\n        path_in: bytes,\n        path_out: bytes | None = None,\n        quality: int = 0,\n        max_filesize: int = 0,\n    ) -> bytes:\n        \"\"\"Manipulate an image file according to the method, returning a\n        new path. For PIL or IMAGEMAGIC methods, resizes the image to a\n        temporary file and encodes with the specified quality level.\n        For WEBPROXY, returns `path_in` unmodified.\n        \"\"\"\n        if self.local_method is not None:\n            return self.local_method.resize(\n                maxwidth,\n                path_in,\n                path_out,\n                quality=quality,\n                max_filesize=max_filesize,\n            )\n        else:\n            # Handled by `proxy_url` already.\n            return path_in\n\n    def deinterlace(\n        self,\n        path_in: bytes,\n        path_out: bytes | None = None,\n    ) -> bytes:\n        \"\"\"Deinterlace an image.\n\n        Only available locally.\n        \"\"\"\n        if self.local_method is not None:\n            return self.local_method.deinterlace(path_in, path_out)\n        else:\n            # FIXME: Should probably issue a warning?\n            return path_in\n\n    def proxy_url(self, maxwidth: int, url: str, quality: int = 0) -> str:\n        \"\"\"Modifies an image URL according the method, returning a new\n        URL. For WEBPROXY, a URL on the proxy server is returned.\n        Otherwise, the URL is returned unmodified.\n        \"\"\"\n        if self.local:\n            # Going to be handled by `resize()`.\n            return url\n        else:\n            return resize_url(url, maxwidth, quality)\n\n    @property\n    def local(self) -> bool:\n        \"\"\"A boolean indicating whether the resizing method is performed\n        locally (i.e., PIL or ImageMagick).\n        \"\"\"\n        return self.local_method is not None\n\n    def get_size(self, path_in: bytes) -> tuple[int, int] | None:\n        \"\"\"Return the size of an image file as an int couple (width, height)\n        in pixels.\n\n        Only available locally.\n        \"\"\"\n        if self.local_method is not None:\n            return self.local_method.get_size(path_in)\n        else:\n            raise RuntimeError(\n                \"image cannot be obtained without artresizer backend\"\n            )\n\n    def get_format(self, path_in: bytes) -> str | None:\n        \"\"\"Returns the format of the image as a string.\n\n        Only available locally.\n        \"\"\"\n        if self.local_method is not None:\n            return self.local_method.get_format(path_in)\n        else:\n            # FIXME: Should probably issue a warning?\n            return None\n\n    def reformat(\n        self,\n        path_in: bytes,\n        new_format: str,\n        deinterlaced: bool = True,\n    ) -> bytes:\n        \"\"\"Converts image to desired format, updating its extension, but\n        keeping the same filename.\n\n        Only available locally.\n        \"\"\"\n        if self.local_method is None:\n            # FIXME: Should probably issue a warning?\n            return path_in\n\n        new_format = new_format.lower()\n        # A nonexhaustive map of image \"types\" to extensions overrides\n        new_format = {\n            \"jpeg\": \"jpg\",\n        }.get(new_format, new_format)\n\n        fname, _ = os.path.splitext(path_in)\n        path_new = fname + b\".\" + new_format.encode(\"utf8\")\n\n        # allows the exception to propagate, while still making sure a changed\n        # file path was removed\n        result_path = path_in\n        try:\n            result_path = self.local_method.convert_format(\n                path_in, path_new, deinterlaced\n            )\n        finally:\n            if result_path != path_in:\n                with suppress(OSError):\n                    os.unlink(path_in)\n        return result_path\n\n    @property\n    def can_compare(self) -> bool:\n        \"\"\"A boolean indicating whether image comparison is available\"\"\"\n\n        if self.local_method is not None:\n            return self.local_method.can_compare\n        else:\n            return False\n\n    def compare(\n        self,\n        im1: bytes,\n        im2: bytes,\n        compare_threshold: float,\n    ) -> bool | None:\n        \"\"\"Return a boolean indicating whether two images are similar.\n\n        Only available locally.\n        \"\"\"\n        if self.local_method is not None:\n            return self.local_method.compare(im1, im2, compare_threshold)\n        else:\n            # FIXME: Should probably issue a warning?\n            return None\n\n    @property\n    def can_write_metadata(self) -> bool:\n        \"\"\"A boolean indicating whether writing image metadata is supported.\"\"\"\n\n        if self.local_method is not None:\n            return self.local_method.can_write_metadata\n        else:\n            return False\n\n    def write_metadata(self, file: bytes, metadata: Mapping[str, str]) -> None:\n        \"\"\"Write key-value metadata to the image file.\n\n        Only available locally. Currently, expects the image to be a PNG file.\n        \"\"\"\n        if self.local_method is not None:\n            self.local_method.write_metadata(file, metadata)\n        else:\n            # FIXME: Should probably issue a warning?\n            pass\n"
  },
  {
    "path": "beets/util/bluelet.py",
    "content": "\"\"\"Extremely simple pure-Python implementation of coroutine-style\nasynchronous socket I/O. Inspired by, but inferior to, Eventlet.\nBluelet can also be thought of as a less-terrible replacement for\nasyncore.\n\nBluelet: easy concurrency without all the messy parallelism.\n\"\"\"\n\nimport collections\nimport errno\nimport select\nimport socket\nimport sys\nimport time\nimport traceback\nimport types\n\n# Basic events used for thread scheduling.\n\n\nclass Event:\n    \"\"\"Just a base class identifying Bluelet events. An event is an\n    object yielded from a Bluelet thread coroutine to suspend operation\n    and communicate with the scheduler.\n    \"\"\"\n\n    pass\n\n\nclass WaitableEvent(Event):\n    \"\"\"A waitable event is one encapsulating an action that can be\n    waited for using a select() call. That is, it's an event with an\n    associated file descriptor.\n    \"\"\"\n\n    def waitables(self):\n        \"\"\"Return \"waitable\" objects to pass to select(). Should return\n        three iterables for input readiness, output readiness, and\n        exceptional conditions (i.e., the three lists passed to\n        select()).\n        \"\"\"\n        return (), (), ()\n\n    def fire(self):\n        \"\"\"Called when an associated file descriptor becomes ready\n        (i.e., is returned from a select() call).\n        \"\"\"\n        pass\n\n\nclass ValueEvent(Event):\n    \"\"\"An event that does nothing but return a fixed value.\"\"\"\n\n    def __init__(self, value):\n        self.value = value\n\n\nclass ExceptionEvent(Event):\n    \"\"\"Raise an exception at the yield point. Used internally.\"\"\"\n\n    def __init__(self, exc_info):\n        self.exc_info = exc_info\n\n\nclass SpawnEvent(Event):\n    \"\"\"Add a new coroutine thread to the scheduler.\"\"\"\n\n    def __init__(self, coro):\n        self.spawned = coro\n\n\nclass JoinEvent(Event):\n    \"\"\"Suspend the thread until the specified child thread has\n    completed.\n    \"\"\"\n\n    def __init__(self, child):\n        self.child = child\n\n\nclass KillEvent(Event):\n    \"\"\"Unschedule a child thread.\"\"\"\n\n    def __init__(self, child):\n        self.child = child\n\n\nclass DelegationEvent(Event):\n    \"\"\"Suspend execution of the current thread, start a new thread and,\n    once the child thread finished, return control to the parent\n    thread.\n    \"\"\"\n\n    def __init__(self, coro):\n        self.spawned = coro\n\n\nclass ReturnEvent(Event):\n    \"\"\"Return a value the current thread's delegator at the point of\n    delegation. Ends the current (delegate) thread.\n    \"\"\"\n\n    def __init__(self, value):\n        self.value = value\n\n\nclass SleepEvent(WaitableEvent):\n    \"\"\"Suspend the thread for a given duration.\"\"\"\n\n    def __init__(self, duration):\n        self.wakeup_time = time.time() + duration\n\n    def time_left(self):\n        return max(self.wakeup_time - time.time(), 0.0)\n\n\nclass ReadEvent(WaitableEvent):\n    \"\"\"Reads from a file-like object.\"\"\"\n\n    def __init__(self, fd, bufsize):\n        self.fd = fd\n        self.bufsize = bufsize\n\n    def waitables(self):\n        return (self.fd,), (), ()\n\n    def fire(self):\n        return self.fd.read(self.bufsize)\n\n\nclass WriteEvent(WaitableEvent):\n    \"\"\"Writes to a file-like object.\"\"\"\n\n    def __init__(self, fd, data):\n        self.fd = fd\n        self.data = data\n\n    def waitable(self):\n        return (), (self.fd,), ()\n\n    def fire(self):\n        self.fd.write(self.data)\n\n\n# Core logic for executing and scheduling threads.\n\n\ndef _event_select(events):\n    \"\"\"Perform a select() over all the Events provided, returning the\n    ones ready to be fired. Only WaitableEvents (including SleepEvents)\n    matter here; all other events are ignored (and thus postponed).\n    \"\"\"\n    # Gather waitables and wakeup times.\n    waitable_to_event = {}\n    rlist, wlist, xlist = [], [], []\n    earliest_wakeup = None\n    for event in events:\n        if isinstance(event, SleepEvent):\n            if not earliest_wakeup:\n                earliest_wakeup = event.wakeup_time\n            else:\n                earliest_wakeup = min(earliest_wakeup, event.wakeup_time)\n        elif isinstance(event, WaitableEvent):\n            r, w, x = event.waitables()\n            rlist += r\n            wlist += w\n            xlist += x\n            for waitable in r:\n                waitable_to_event[(\"r\", waitable)] = event\n            for waitable in w:\n                waitable_to_event[(\"w\", waitable)] = event\n            for waitable in x:\n                waitable_to_event[(\"x\", waitable)] = event\n\n    # If we have a any sleeping threads, determine how long to sleep.\n    if earliest_wakeup:\n        timeout = max(earliest_wakeup - time.time(), 0.0)\n    else:\n        timeout = None\n\n    # Perform select() if we have any waitables.\n    if rlist or wlist or xlist:\n        rready, wready, xready = select.select(rlist, wlist, xlist, timeout)\n    else:\n        rready, wready, xready = (), (), ()\n        if timeout:\n            time.sleep(timeout)\n\n    # Gather ready events corresponding to the ready waitables.\n    ready_events = set()\n    for ready in rready:\n        ready_events.add(waitable_to_event[(\"r\", ready)])\n    for ready in wready:\n        ready_events.add(waitable_to_event[(\"w\", ready)])\n    for ready in xready:\n        ready_events.add(waitable_to_event[(\"x\", ready)])\n\n    # Gather any finished sleeps.\n    for event in events:\n        if isinstance(event, SleepEvent) and event.time_left() == 0.0:\n            ready_events.add(event)\n\n    return ready_events\n\n\nclass ThreadError(Exception):\n    def __init__(self, coro, exc_info):\n        self.coro = coro\n        self.exc_info = exc_info\n\n    def reraise(self):\n        raise self.exc_info[1].with_traceback(self.exc_info[2])\n\n\nSUSPENDED = Event()  # Special sentinel placeholder for suspended threads.\n\n\nclass Delegated(Event):\n    \"\"\"Placeholder indicating that a thread has delegated execution to a\n    different thread.\n    \"\"\"\n\n    def __init__(self, child):\n        self.child = child\n\n\ndef run(root_coro):\n    \"\"\"Schedules a coroutine, running it to completion. This\n    encapsulates the Bluelet scheduler, which the root coroutine can\n    add to by spawning new coroutines.\n    \"\"\"\n    # The \"threads\" dictionary keeps track of all the currently-\n    # executing and suspended coroutines. It maps coroutines to their\n    # currently \"blocking\" event. The event value may be SUSPENDED if\n    # the coroutine is waiting on some other condition: namely, a\n    # delegated coroutine or a joined coroutine. In this case, the\n    # coroutine should *also* appear as a value in one of the below\n    # dictionaries `delegators` or `joiners`.\n    threads = {root_coro: ValueEvent(None)}\n\n    # Maps child coroutines to delegating parents.\n    delegators = {}\n\n    # Maps child coroutines to joining (exit-waiting) parents.\n    joiners = collections.defaultdict(list)\n\n    def complete_thread(coro, return_value):\n        \"\"\"Remove a coroutine from the scheduling pool, awaking\n        delegators and joiners as necessary and returning the specified\n        value to any delegating parent.\n        \"\"\"\n        del threads[coro]\n\n        # Resume delegator.\n        if coro in delegators:\n            threads[delegators[coro]] = ValueEvent(return_value)\n            del delegators[coro]\n\n        # Resume joiners.\n        if coro in joiners:\n            for parent in joiners[coro]:\n                threads[parent] = ValueEvent(None)\n            del joiners[coro]\n\n    def advance_thread(coro, value, is_exc=False):\n        \"\"\"After an event is fired, run a given coroutine associated with\n        it in the threads dict until it yields again. If the coroutine\n        exits, then the thread is removed from the pool. If the coroutine\n        raises an exception, it is reraised in a ThreadError. If\n        is_exc is True, then the value must be an exc_info tuple and the\n        exception is thrown into the coroutine.\n        \"\"\"\n        try:\n            if is_exc:\n                next_event = coro.throw(*value)\n            else:\n                next_event = coro.send(value)\n        except StopIteration:\n            # Thread is done.\n            complete_thread(coro, None)\n        except BaseException:\n            # Thread raised some other exception.\n            del threads[coro]\n            raise ThreadError(coro, sys.exc_info())\n        else:\n            if isinstance(next_event, types.GeneratorType):\n                # Automatically invoke sub-coroutines. (Shorthand for\n                # explicit bluelet.call().)\n                next_event = DelegationEvent(next_event)\n            threads[coro] = next_event\n\n    def kill_thread(coro):\n        \"\"\"Unschedule this thread and its (recursive) delegates.\"\"\"\n        # Collect all coroutines in the delegation stack.\n        coros = [coro]\n        while isinstance(threads[coro], Delegated):\n            coro = threads[coro].child\n            coros.append(coro)\n\n        # Complete each coroutine from the top to the bottom of the\n        # stack.\n        for coro in reversed(coros):\n            complete_thread(coro, None)\n\n    # Continue advancing threads until root thread exits.\n    exit_te = None\n    while threads:\n        try:\n            # Look for events that can be run immediately. Continue\n            # running immediate events until nothing is ready.\n            while True:\n                have_ready = False\n                for coro, event in list(threads.items()):\n                    if isinstance(event, SpawnEvent):\n                        threads[event.spawned] = ValueEvent(None)  # Spawn.\n                        advance_thread(coro, None)\n                        have_ready = True\n                    elif isinstance(event, ValueEvent):\n                        advance_thread(coro, event.value)\n                        have_ready = True\n                    elif isinstance(event, ExceptionEvent):\n                        advance_thread(coro, event.exc_info, True)\n                        have_ready = True\n                    elif isinstance(event, DelegationEvent):\n                        threads[coro] = Delegated(event.spawned)  # Suspend.\n                        threads[event.spawned] = ValueEvent(None)  # Spawn.\n                        delegators[event.spawned] = coro\n                        have_ready = True\n                    elif isinstance(event, ReturnEvent):\n                        # Thread is done.\n                        complete_thread(coro, event.value)\n                        have_ready = True\n                    elif isinstance(event, JoinEvent):\n                        threads[coro] = SUSPENDED  # Suspend.\n                        joiners[event.child].append(coro)\n                        have_ready = True\n                    elif isinstance(event, KillEvent):\n                        threads[coro] = ValueEvent(None)\n                        kill_thread(event.child)\n                        have_ready = True\n\n                # Only start the select when nothing else is ready.\n                if not have_ready:\n                    break\n\n            # Wait and fire.\n            event2coro = {v: k for k, v in threads.items()}\n            for event in _event_select(threads.values()):\n                # Run the IO operation, but catch socket errors.\n                try:\n                    value = event.fire()\n                except OSError as exc:\n                    if (\n                        isinstance(exc.args, tuple)\n                        and exc.args[0] == errno.EPIPE\n                    ):\n                        # Broken pipe. Remote host disconnected.\n                        pass\n                    elif (\n                        isinstance(exc.args, tuple)\n                        and exc.args[0] == errno.ECONNRESET\n                    ):\n                        # Connection was reset by peer.\n                        pass\n                    else:\n                        traceback.print_exc()\n                    # Abort the coroutine.\n                    threads[event2coro[event]] = ReturnEvent(None)\n                else:\n                    advance_thread(event2coro[event], value)\n\n        except ThreadError as te:\n            # Exception raised from inside a thread.\n            event = ExceptionEvent(te.exc_info)\n            if te.coro in delegators:\n                # The thread is a delegate. Raise exception in its\n                # delegator.\n                threads[delegators[te.coro]] = event\n                del delegators[te.coro]\n            else:\n                # The thread is root-level. Raise in client code.\n                exit_te = te\n                break\n\n        except BaseException:\n            # For instance, KeyboardInterrupt during select(). Raise\n            # into root thread and terminate others.\n            threads = {root_coro: ExceptionEvent(sys.exc_info())}\n\n    # If any threads still remain, kill them.\n    for coro in threads:\n        coro.close()\n\n    # If we're exiting with an exception, raise it in the client.\n    if exit_te:\n        exit_te.reraise()\n\n\n# Sockets and their associated events.\n\n\nclass SocketClosedError(Exception):\n    pass\n\n\nclass Listener:\n    \"\"\"A socket wrapper object for listening sockets.\"\"\"\n\n    def __init__(self, host, port):\n        \"\"\"Create a listening socket on the given hostname and port.\"\"\"\n        self._closed = False\n        self.host = host\n        self.port = port\n        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n        self.sock.bind((host, port))\n        self.sock.listen(5)\n\n    def accept(self):\n        \"\"\"An event that waits for a connection on the listening socket.\n        When a connection is made, the event returns a Connection\n        object.\n        \"\"\"\n        if self._closed:\n            raise SocketClosedError()\n        return AcceptEvent(self)\n\n    def close(self):\n        \"\"\"Immediately close the listening socket. (Not an event.)\"\"\"\n        self._closed = True\n        self.sock.close()\n\n\nclass Connection:\n    \"\"\"A socket wrapper object for connected sockets.\"\"\"\n\n    def __init__(self, sock, addr):\n        self.sock = sock\n        self.addr = addr\n        self._buf = b\"\"\n        self._closed = False\n\n    def close(self):\n        \"\"\"Close the connection.\"\"\"\n        self._closed = True\n        self.sock.close()\n\n    def recv(self, size):\n        \"\"\"Read at most size bytes of data from the socket.\"\"\"\n        if self._closed:\n            raise SocketClosedError()\n\n        if self._buf:\n            # We already have data read previously.\n            out = self._buf[:size]\n            self._buf = self._buf[size:]\n            return ValueEvent(out)\n        else:\n            return ReceiveEvent(self, size)\n\n    def send(self, data):\n        \"\"\"Sends data on the socket, returning the number of bytes\n        successfully sent.\n        \"\"\"\n        if self._closed:\n            raise SocketClosedError()\n        return SendEvent(self, data)\n\n    def sendall(self, data):\n        \"\"\"Send all of data on the socket.\"\"\"\n        if self._closed:\n            raise SocketClosedError()\n        return SendEvent(self, data, True)\n\n    def readline(self, terminator=b\"\\n\", bufsize=1024):\n        \"\"\"Reads a line (delimited by terminator) from the socket.\"\"\"\n        if self._closed:\n            raise SocketClosedError()\n\n        while True:\n            if terminator in self._buf:\n                line, self._buf = self._buf.split(terminator, 1)\n                line += terminator\n                yield ReturnEvent(line)\n                break\n            data = yield ReceiveEvent(self, bufsize)\n            if data:\n                self._buf += data\n            else:\n                line = self._buf\n                self._buf = b\"\"\n                yield ReturnEvent(line)\n                break\n\n\nclass AcceptEvent(WaitableEvent):\n    \"\"\"An event for Listener objects (listening sockets) that suspends\n    execution until the socket gets a connection.\n    \"\"\"\n\n    def __init__(self, listener):\n        self.listener = listener\n\n    def waitables(self):\n        return (self.listener.sock,), (), ()\n\n    def fire(self):\n        sock, addr = self.listener.sock.accept()\n        return Connection(sock, addr)\n\n\nclass ReceiveEvent(WaitableEvent):\n    \"\"\"An event for Connection objects (connected sockets) for\n    asynchronously reading data.\n    \"\"\"\n\n    def __init__(self, conn, bufsize):\n        self.conn = conn\n        self.bufsize = bufsize\n\n    def waitables(self):\n        return (self.conn.sock,), (), ()\n\n    def fire(self):\n        return self.conn.sock.recv(self.bufsize)\n\n\nclass SendEvent(WaitableEvent):\n    \"\"\"An event for Connection objects (connected sockets) for\n    asynchronously writing data.\n    \"\"\"\n\n    def __init__(self, conn, data, sendall=False):\n        self.conn = conn\n        self.data = data\n        self.sendall = sendall\n\n    def waitables(self):\n        return (), (self.conn.sock,), ()\n\n    def fire(self):\n        if self.sendall:\n            return self.conn.sock.sendall(self.data)\n        else:\n            return self.conn.sock.send(self.data)\n\n\n# Public interface for threads; each returns an event object that\n# can immediately be \"yield\"ed.\n\n\ndef null():\n    \"\"\"Event: yield to the scheduler without doing anything special.\"\"\"\n    return ValueEvent(None)\n\n\ndef spawn(coro):\n    \"\"\"Event: add another coroutine to the scheduler. Both the parent\n    and child coroutines run concurrently.\n    \"\"\"\n    if not isinstance(coro, types.GeneratorType):\n        raise ValueError(f\"{coro} is not a coroutine\")\n    return SpawnEvent(coro)\n\n\ndef call(coro):\n    \"\"\"Event: delegate to another coroutine. The current coroutine\n    is resumed once the sub-coroutine finishes. If the sub-coroutine\n    returns a value using end(), then this event returns that value.\n    \"\"\"\n    if not isinstance(coro, types.GeneratorType):\n        raise ValueError(f\"{coro} is not a coroutine\")\n    return DelegationEvent(coro)\n\n\ndef end(value=None):\n    \"\"\"Event: ends the coroutine and returns a value to its\n    delegator.\n    \"\"\"\n    return ReturnEvent(value)\n\n\ndef read(fd, bufsize=None):\n    \"\"\"Event: read from a file descriptor asynchronously.\"\"\"\n    if bufsize is None:\n        # Read all.\n        def reader():\n            buf = []\n            while True:\n                data = yield read(fd, 1024)\n                if not data:\n                    break\n                buf.append(data)\n            yield ReturnEvent(\"\".join(buf))\n\n        return DelegationEvent(reader())\n\n    else:\n        return ReadEvent(fd, bufsize)\n\n\ndef write(fd, data):\n    \"\"\"Event: write to a file descriptor asynchronously.\"\"\"\n    return WriteEvent(fd, data)\n\n\ndef connect(host, port):\n    \"\"\"Event: connect to a network address and return a Connection\n    object for communicating on the socket.\n    \"\"\"\n    addr = (host, port)\n    sock = socket.create_connection(addr)\n    return ValueEvent(Connection(sock, addr))\n\n\ndef sleep(duration):\n    \"\"\"Event: suspend the thread for ``duration`` seconds.\"\"\"\n    return SleepEvent(duration)\n\n\ndef join(coro):\n    \"\"\"Suspend the thread until another, previously `spawn`ed thread\n    completes.\n    \"\"\"\n    return JoinEvent(coro)\n\n\ndef kill(coro):\n    \"\"\"Halt the execution of a different `spawn`ed thread.\"\"\"\n    return KillEvent(coro)\n\n\n# Convenience function for running socket servers.\n\n\ndef server(host, port, func):\n    \"\"\"A coroutine that runs a network server. Host and port specify the\n    listening address. func should be a coroutine that takes a single\n    parameter, a Connection object. The coroutine is invoked for every\n    incoming connection on the listening socket.\n    \"\"\"\n\n    def handler(conn):\n        try:\n            yield func(conn)\n        finally:\n            conn.close()\n\n    listener = Listener(host, port)\n    try:\n        while True:\n            conn = yield listener.accept()\n            yield spawn(handler(conn))\n    except KeyboardInterrupt:\n        pass\n    finally:\n        listener.close()\n"
  },
  {
    "path": "beets/util/color.py",
    "content": "from __future__ import annotations\n\nimport os\nimport re\nfrom functools import cache\nfrom typing import TYPE_CHECKING, Literal\n\nimport confuse\n\nfrom beets import config\n\nif TYPE_CHECKING:\n    from beets.autotag.distance import Distance\n\n# ANSI terminal colorization code heavily inspired by pygments:\n# https://bitbucket.org/birkenfeld/pygments-main/src/default/pygments/console.py\n# (pygments is by Tim Hatch, Armin Ronacher, et al.)\nCOLOR_ESCAPE = \"\\x1b\"\nLEGACY_COLORS = {\n    \"black\": [\"black\"],\n    \"darkred\": [\"red\"],\n    \"darkgreen\": [\"green\"],\n    \"brown\": [\"yellow\"],\n    \"darkyellow\": [\"yellow\"],\n    \"darkblue\": [\"blue\"],\n    \"purple\": [\"magenta\"],\n    \"darkmagenta\": [\"magenta\"],\n    \"teal\": [\"cyan\"],\n    \"darkcyan\": [\"cyan\"],\n    \"lightgray\": [\"white\"],\n    \"darkgray\": [\"bold\", \"black\"],\n    \"red\": [\"bold\", \"red\"],\n    \"green\": [\"bold\", \"green\"],\n    \"yellow\": [\"bold\", \"yellow\"],\n    \"blue\": [\"bold\", \"blue\"],\n    \"fuchsia\": [\"bold\", \"magenta\"],\n    \"magenta\": [\"bold\", \"magenta\"],\n    \"turquoise\": [\"bold\", \"cyan\"],\n    \"cyan\": [\"bold\", \"cyan\"],\n    \"white\": [\"bold\", \"white\"],\n}\n# All ANSI Colors.\nCODE_BY_COLOR = {\n    # Styles.\n    \"normal\": 0,\n    \"bold\": 1,\n    \"faint\": 2,\n    \"italic\": 3,\n    \"underline\": 4,\n    \"blink_slow\": 5,\n    \"blink_rapid\": 6,\n    \"inverse\": 7,\n    \"conceal\": 8,\n    \"crossed_out\": 9,\n    # Text colors.\n    \"black\": 30,\n    \"red\": 31,\n    \"green\": 32,\n    \"yellow\": 33,\n    \"blue\": 34,\n    \"magenta\": 35,\n    \"cyan\": 36,\n    \"white\": 37,\n    \"bright_black\": 90,\n    \"bright_red\": 91,\n    \"bright_green\": 92,\n    \"bright_yellow\": 93,\n    \"bright_blue\": 94,\n    \"bright_magenta\": 95,\n    \"bright_cyan\": 96,\n    \"bright_white\": 97,\n    # Background colors.\n    \"bg_black\": 40,\n    \"bg_red\": 41,\n    \"bg_green\": 42,\n    \"bg_yellow\": 43,\n    \"bg_blue\": 44,\n    \"bg_magenta\": 45,\n    \"bg_cyan\": 46,\n    \"bg_white\": 47,\n    \"bg_bright_black\": 100,\n    \"bg_bright_red\": 101,\n    \"bg_bright_green\": 102,\n    \"bg_bright_yellow\": 103,\n    \"bg_bright_blue\": 104,\n    \"bg_bright_magenta\": 105,\n    \"bg_bright_cyan\": 106,\n    \"bg_bright_white\": 107,\n}\nRESET_COLOR = f\"{COLOR_ESCAPE}[39;49;00m\"\n# Precompile common ANSI-escape regex patterns\nANSI_CODE_REGEX = re.compile(rf\"({COLOR_ESCAPE}\\[[;0-9]*m)\")\nESC_TEXT_REGEX = re.compile(\n    rf\"\"\"(?P<pretext>[^{COLOR_ESCAPE}]*)\n         (?P<esc>(?:{ANSI_CODE_REGEX.pattern})+)\n         (?P<text>[^{COLOR_ESCAPE}]+)(?P<reset>{re.escape(RESET_COLOR)})\n         (?P<posttext>[^{COLOR_ESCAPE}]*)\"\"\",\n    re.VERBOSE,\n)\nColorName = Literal[\n    \"text_success\",\n    \"text_warning\",\n    \"text_error\",\n    \"text_highlight\",\n    \"text_highlight_minor\",\n    \"action_default\",\n    \"action\",\n    # New Colors\n    \"text_faint\",\n    \"import_path\",\n    \"import_path_items\",\n    \"action_description\",\n    \"changed\",\n    \"text_diff_added\",\n    \"text_diff_removed\",\n]\n\n\n@cache\ndef get_color_config() -> dict[ColorName, str]:\n    \"\"\"Parse and validate color configuration, converting names to ANSI codes.\n\n    Processes the UI color configuration, handling both new list format and\n    legacy single-color format. Validates all color names against known codes\n    and raises an error for any invalid entries.\n    \"\"\"\n    template_dict: dict[ColorName, confuse.OneOf[str | list[str]]] = {\n        n: confuse.OneOf(\n            [\n                confuse.Choice(sorted(LEGACY_COLORS)),\n                confuse.Sequence(confuse.Choice(sorted(CODE_BY_COLOR))),\n            ]\n        )\n        for n in ColorName.__args__  # type: ignore[attr-defined]\n    }\n    template = confuse.MappingTemplate(template_dict)\n    colors_by_color_name = {\n        k: (v if isinstance(v, list) else LEGACY_COLORS.get(v, [v]))\n        for k, v in config[\"ui\"][\"colors\"].get(template).items()\n    }\n\n    return {\n        n: \";\".join(str(CODE_BY_COLOR[c]) for c in colors)\n        for n, colors in colors_by_color_name.items()\n    }\n\n\ndef _colorize(color_name: ColorName, text: str) -> str:\n    \"\"\"Apply ANSI color formatting to text based on configuration settings.\"\"\"\n    color_code = get_color_config()[color_name]\n    return f\"{COLOR_ESCAPE}[{color_code}m{text}{RESET_COLOR}\"\n\n\ndef colorize(color_name: ColorName, text: str) -> str:\n    \"\"\"Colorize text when color output is enabled.\"\"\"\n    if config[\"ui\"][\"color\"] and \"NO_COLOR\" not in os.environ:\n        return _colorize(color_name, text)\n\n    return text\n\n\ndef dist_colorize(string: str, dist: Distance) -> str:\n    \"\"\"Formats a string as a colorized similarity string according to\n    a distance.\n    \"\"\"\n    if dist <= config[\"match\"][\"strong_rec_thresh\"].as_number():\n        string = colorize(\"text_success\", string)\n    elif dist <= config[\"match\"][\"medium_rec_thresh\"].as_number():\n        string = colorize(\"text_warning\", string)\n    else:\n        string = colorize(\"text_error\", string)\n    return string\n\n\ndef uncolorize(colored_text: str) -> str:\n    \"\"\"Remove colors from a string.\"\"\"\n    # Define a regular expression to match ANSI codes.\n    # See: http://stackoverflow.com/a/2187024/1382707\n    # Explanation of regular expression:\n    #     \\x1b     - matches ESC character\n    #     \\[       - matches opening square bracket\n    #     [;\\d]*   - matches a sequence consisting of one or more digits or\n    #                semicola\n    #     [A-Za-z] - matches a letter\n    return ANSI_CODE_REGEX.sub(\"\", colored_text)\n\n\ndef color_split(colored_text: str, index: int) -> tuple[str, str]:\n    length = 0\n    pre_split = \"\"\n    post_split = \"\"\n    found_color_code = None\n    found_split = False\n    for part in ANSI_CODE_REGEX.split(colored_text):\n        # Count how many real letters we have passed\n        length += color_len(part)\n        if found_split:\n            post_split += part\n        else:\n            if ANSI_CODE_REGEX.match(part):\n                # This is a color code\n                if part == RESET_COLOR:\n                    found_color_code = None\n                else:\n                    found_color_code = part\n                pre_split += part\n            else:\n                if index < length:\n                    # Found part with our split in.\n                    split_index = index - (length - color_len(part))\n                    found_split = True\n                    if found_color_code:\n                        pre_split += f\"{part[:split_index]}{RESET_COLOR}\"\n                        post_split += f\"{found_color_code}{part[split_index:]}\"\n                    else:\n                        pre_split += part[:split_index]\n                        post_split += part[split_index:]\n                else:\n                    # Not found, add this part to the pre split\n                    pre_split += part\n    return pre_split, post_split\n\n\ndef color_len(colored_text: str) -> int:\n    \"\"\"Measure the length of a string while excluding ANSI codes from the\n    measurement. The standard `len(my_string)` method also counts ANSI codes\n    to the string length, which is counterproductive when layouting a\n    Terminal interface.\n    \"\"\"\n    # Return the length of the uncolored string.\n    return len(uncolorize(colored_text))\n"
  },
  {
    "path": "beets/util/config.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from collections.abc import Collection, Sequence\n\n\ndef sanitize_choices(\n    choices: Sequence[str], choices_all: Collection[str]\n) -> list[str]:\n    \"\"\"Clean up a stringlist configuration attribute: keep only choices\n    elements present in choices_all, remove duplicate elements, expand '*'\n    wildcard while keeping original stringlist order.\n    \"\"\"\n    seen: set[str] = set()\n    others = [x for x in choices_all if x not in choices]\n    res: list[str] = []\n    for s in choices:\n        if s not in seen:\n            if s in list(choices_all):\n                res.append(s)\n            elif s == \"*\":\n                res.extend(others)\n        seen.add(s)\n    return res\n\n\ndef sanitize_pairs(\n    pairs: Sequence[tuple[str, str]], pairs_all: Sequence[tuple[str, str]]\n) -> list[tuple[str, str]]:\n    \"\"\"Clean up a single-element mapping configuration attribute as returned\n    by Confuse's `Pairs` template: keep only two-element tuples present in\n    pairs_all, remove duplicate elements, expand ('str', '*') and ('*', '*')\n    wildcards while keeping the original order. Note that ('*', '*') and\n    ('*', 'whatever') have the same effect.\n\n    For example,\n\n    >>> sanitize_pairs(\n    ...     [('foo', 'baz bar'), ('key', '*'), ('*', '*')],\n    ...     [('foo', 'bar'), ('foo', 'baz'), ('foo', 'foobar'),\n    ...      ('key', 'value')]\n    ...     )\n    [('foo', 'baz'), ('foo', 'bar'), ('key', 'value'), ('foo', 'foobar')]\n    \"\"\"\n    pairs_all = list(pairs_all)\n    seen: set[tuple[str, str]] = set()\n    others = [x for x in pairs_all if x not in pairs]\n    res: list[tuple[str, str]] = []\n    for k, values in pairs:\n        for v in values.split():\n            x = (k, v)\n            if x in pairs_all:\n                if x not in seen:\n                    seen.add(x)\n                    res.append(x)\n            elif k == \"*\":\n                new = [o for o in others if o not in seen]\n                seen.update(new)\n                res.extend(new)\n            elif v == \"*\":\n                new = [o for o in others if o not in seen and o[0] == k]\n                seen.update(new)\n                res.extend(new)\n    return res\n"
  },
  {
    "path": "beets/util/deprecation.py",
    "content": "from __future__ import annotations\n\nimport warnings\nfrom importlib import import_module\nfrom typing import TYPE_CHECKING, Any\n\nfrom packaging.version import Version\n\nimport beets\n\nif TYPE_CHECKING:\n    from logging import Logger\n\n\ndef _format_message(old: str, new: str | None = None) -> str:\n    next_major = f\"{Version(beets.__version__).major + 1}.0.0\"\n    msg = f\"{old} is deprecated and will be removed in version {next_major}.\"\n    if new:\n        msg += f\" Use {new} instead.\"\n\n    return msg\n\n\ndef deprecate_for_user(\n    logger: Logger, old: str, new: str | None = None\n) -> None:\n    logger.warning(_format_message(old, new))\n\n\ndef deprecate_for_maintainers(\n    old: str, new: str | None = None, stacklevel: int = 1\n) -> None:\n    \"\"\"Issue a deprecation warning visible to maintainers during development.\n\n    Emits a DeprecationWarning that alerts developers about deprecated code\n    patterns. Unlike user-facing warnings, these are primarily for internal\n    code maintenance and appear during test runs or with warnings enabled.\n    \"\"\"\n    warnings.warn(\n        _format_message(old, new), DeprecationWarning, stacklevel=stacklevel + 1\n    )\n\n\ndef deprecate_imports(\n    old_module: str, new_module_by_name: dict[str, str], name: str\n) -> Any:\n    \"\"\"Handle deprecated module imports by redirecting to new locations.\n\n    Facilitates gradual migration of module structure by intercepting import\n    attempts for relocated functionality. Issues deprecation warnings while\n    transparently providing access to the moved implementation, allowing\n    existing code to continue working during transition periods.\n    \"\"\"\n    if new_module := new_module_by_name.get(name):\n        deprecate_for_maintainers(\n            f\"'{old_module}.{name}'\", f\"'{new_module}.{name}'\", stacklevel=2\n        )\n\n        return getattr(import_module(new_module), name)\n    raise AttributeError(f\"module '{old_module}' has no attribute '{name}'\")\n"
  },
  {
    "path": "beets/util/diff.py",
    "content": "from __future__ import annotations\n\nfrom difflib import SequenceMatcher\nfrom typing import TYPE_CHECKING\n\nfrom .color import colorize\n\nif TYPE_CHECKING:\n    from collections.abc import Iterable\n\n    from beets.dbcore.db import FormattedMapping\n    from beets.library.models import LibModel\n\n\ndef colordiff(a: str, b: str) -> tuple[str, str]:\n    \"\"\"Intelligently highlight the differences between two strings.\"\"\"\n    before = \"\"\n    after = \"\"\n\n    matcher = SequenceMatcher(lambda _: False, a, b)\n    for op, a_start, a_end, b_start, b_end in matcher.get_opcodes():\n        before_part, after_part = a[a_start:a_end], b[b_start:b_end]\n        if op in {\"delete\", \"replace\"}:\n            before_part = colorize(\"text_diff_removed\", before_part)\n        if op in {\"insert\", \"replace\"}:\n            after_part = colorize(\"text_diff_added\", after_part)\n\n        before += before_part\n        after += after_part\n\n    return before, after\n\n\nFLOAT_EPSILON = 0.01\n\n\ndef _multi_value_diff(field: str, oldset: set[str], newset: set[str]) -> str:\n    added = newset - oldset\n    removed = oldset - newset\n\n    parts = [\n        f\"{field}:\",\n        *(colorize(\"text_diff_removed\", f\"  - {i}\") for i in sorted(removed)),\n        *(colorize(\"text_diff_added\", f\"  + {i}\") for i in sorted(added)),\n    ]\n    return \"\\n\".join(parts)\n\n\ndef _field_diff(\n    field: str, old: FormattedMapping, new: FormattedMapping\n) -> str | None:\n    \"\"\"Given two Model objects and their formatted views, format their values\n    for `field` and highlight changes among them. Return a human-readable\n    string. If the value has not changed, return None instead.\n    \"\"\"\n    # If no change, abort.\n    if (oldval := old.model.get(field)) == (newval := new.model.get(field)) or (\n        isinstance(oldval, float)\n        and isinstance(newval, float)\n        and abs(oldval - newval) < FLOAT_EPSILON\n    ):\n        return None\n\n    if isinstance(oldval, list):\n        if (oldset := set(oldval)) != (newset := set(newval)):\n            return _multi_value_diff(field, oldset, newset)\n        return None\n\n    # Get formatted values for output.\n    oldstr, newstr = old.get(field, \"\"), new.get(field, \"\")\n    if field not in new:\n        return colorize(\"text_diff_removed\", f\"{field}: {oldstr}\")\n\n    if field not in old:\n        return colorize(\"text_diff_added\", f\"{field}: {newstr}\")\n\n    # For strings, highlight changes. For others, colorize the whole thing.\n    if isinstance(oldval, str):\n        oldstr, newstr = colordiff(oldstr, newstr)\n    else:\n        oldstr = colorize(\"text_diff_removed\", oldstr)\n        newstr = colorize(\"text_diff_added\", newstr)\n\n    return f\"{field}: {oldstr} -> {newstr}\"\n\n\ndef get_model_changes(\n    new: LibModel,\n    old: LibModel,\n    fields: Iterable[str] | None,\n) -> list[str]:\n    \"\"\"Compute human-readable diff lines for changed fields between two models.\n\n    Compares ``old`` and ``new`` across fixed and flex fields, excluding\n    internal ones like ``mtime``. If ``fields`` is provided, only the\n    specified subset is considered.\n    \"\"\"\n    # Keep the formatted views around instead of re-creating them in each\n    # iteration step\n    old_fmt = old.formatted()\n    new_fmt = new.formatted()\n\n    # Build up lines showing changed fields.\n    diff_fields = (set(old) | set(new)) - {\"mtime\"}\n    if allowed_fields := set(fields or {}):\n        diff_fields &= allowed_fields\n\n    return [\n        d\n        for f in sorted(diff_fields)\n        if (d := _field_diff(f, old_fmt, new_fmt))\n    ]\n"
  },
  {
    "path": "beets/util/functemplate.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"This module implements a string formatter based on the standard PEP\n292 string.Template class extended with function calls. Variables, as\nwith string.Template, are indicated with $ and functions are delimited\nwith %.\n\nThis module assumes that everything is Unicode: the template and the\nsubstitution values. Bytestrings are not supported. Also, the templates\nalways behave like the ``safe_substitute`` method in the standard\nlibrary: unknown symbols are left intact.\n\nThis is sort of like a tiny, horrible degeneration of a real templating\nengine like Jinja2 or Mustache.\n\"\"\"\n\nimport ast\nimport dis\nimport functools\nimport re\nimport types\n\nSYMBOL_DELIM = \"$\"\nFUNC_DELIM = \"%\"\nGROUP_OPEN = \"{\"\nGROUP_CLOSE = \"}\"\nARG_SEP = \",\"\nESCAPE_CHAR = \"$\"\n\nVARIABLE_PREFIX = \"__var_\"\nFUNCTION_PREFIX = \"__func_\"\n\n\nclass Environment:\n    \"\"\"Contains the values and functions to be substituted into a\n    template.\n    \"\"\"\n\n    def __init__(self, values, functions):\n        self.values = values\n        self.functions = functions\n\n\n# Code generation helpers.\n\n\ndef ex_rvalue(name):\n    \"\"\"A variable store expression.\"\"\"\n    return ast.Name(name, ast.Load())\n\n\ndef ex_literal(val):\n    \"\"\"An int, float, long, bool, string, or None literal with the given\n    value.\n    \"\"\"\n    return ast.Constant(val)\n\n\ndef ex_call(func, args):\n    \"\"\"A function-call expression with only positional parameters. The\n    function may be an expression or the name of a function. Each\n    argument may be an expression or a value to be used as a literal.\n    \"\"\"\n    if isinstance(func, str):\n        func = ex_rvalue(func)\n\n    args = list(args)\n    for i in range(len(args)):\n        if not isinstance(args[i], ast.expr):\n            args[i] = ex_literal(args[i])\n\n    return ast.Call(func, args, [])\n\n\ndef compile_func(arg_names, statements, name=\"_the_func\", debug=False):\n    \"\"\"Compile a list of statements as the body of a function and return\n    the resulting Python function. If `debug`, then print out the\n    bytecode of the compiled function.\n    \"\"\"\n    args_fields = {\n        \"args\": [ast.arg(arg=n, annotation=None) for n in arg_names],\n        \"kwonlyargs\": [],\n        \"kw_defaults\": [],\n        \"defaults\": [ex_literal(None) for _ in arg_names],\n    }\n    args_fields[\"posonlyargs\"] = []\n    args = ast.arguments(**args_fields)\n\n    func_def = ast.FunctionDef(\n        name=name,\n        args=args,\n        body=statements,\n        decorator_list=[],\n    )\n\n    mod = ast.Module([func_def], [])\n\n    ast.fix_missing_locations(mod)\n\n    prog = compile(mod, \"<generated>\", \"exec\")\n\n    # Debug: show bytecode.\n    if debug:\n        dis.dis(prog)\n        for const in prog.co_consts:\n            if isinstance(const, types.CodeType):\n                dis.dis(const)\n\n    the_locals = {}\n    exec(prog, {}, the_locals)\n    return the_locals[name]\n\n\n# AST nodes for the template language.\n\n\nclass Symbol:\n    \"\"\"A variable-substitution symbol in a template.\"\"\"\n\n    def __init__(self, ident, original):\n        self.ident = ident\n        self.original = original\n\n    def __repr__(self):\n        return f\"Symbol({self.ident!r})\"\n\n    def evaluate(self, env):\n        \"\"\"Evaluate the symbol in the environment, returning a Unicode\n        string.\n        \"\"\"\n        if self.ident in env.values:\n            # Substitute for a value.\n            return env.values[self.ident]\n        else:\n            # Keep original text.\n            return self.original\n\n    def translate(self):\n        \"\"\"Compile the variable lookup.\"\"\"\n        ident = self.ident\n        expr = ex_rvalue(f\"{VARIABLE_PREFIX}{ident}\")\n        return [expr], {ident}, set()\n\n\nclass Call:\n    \"\"\"A function call in a template.\"\"\"\n\n    def __init__(self, ident, args, original):\n        self.ident = ident\n        self.args = args\n        self.original = original\n\n    def __repr__(self):\n        return f\"Call({self.ident!r}, {self.args!r}, {self.original!r})\"\n\n    def evaluate(self, env):\n        \"\"\"Evaluate the function call in the environment, returning a\n        Unicode string.\n        \"\"\"\n        if self.ident in env.functions:\n            arg_vals = [expr.evaluate(env) for expr in self.args]\n            try:\n                out = env.functions[self.ident](*arg_vals)\n            except Exception as exc:\n                # Function raised exception! Maybe inlining the name of\n                # the exception will help debug.\n                return f\"<{exc}>\"\n            return str(out)\n        else:\n            return self.original\n\n    def translate(self):\n        \"\"\"Compile the function call.\"\"\"\n        varnames = set()\n        funcnames = {self.ident}\n\n        arg_exprs = []\n        for arg in self.args:\n            subexprs, subvars, subfuncs = arg.translate()\n            varnames.update(subvars)\n            funcnames.update(subfuncs)\n\n            # Create a subexpression that joins the result components of\n            # the arguments.\n            arg_exprs.append(\n                ex_call(\n                    ast.Attribute(ex_literal(\"\"), \"join\", ast.Load()),\n                    [\n                        ex_call(\n                            \"map\",\n                            [\n                                ex_rvalue(str.__name__),\n                                ast.List(subexprs, ast.Load()),\n                            ],\n                        )\n                    ],\n                )\n            )\n\n        subexpr_call = ex_call(f\"{FUNCTION_PREFIX}{self.ident}\", arg_exprs)\n        return [subexpr_call], varnames, funcnames\n\n\nclass Expression:\n    \"\"\"Top-level template construct: contains a list of text blobs,\n    Symbols, and Calls.\n    \"\"\"\n\n    def __init__(self, parts):\n        self.parts = parts\n\n    def __repr__(self):\n        return f\"Expression({self.parts!r})\"\n\n    def evaluate(self, env):\n        \"\"\"Evaluate the entire expression in the environment, returning\n        a Unicode string.\n        \"\"\"\n        out = []\n        for part in self.parts:\n            if isinstance(part, str):\n                out.append(part)\n            else:\n                out.append(part.evaluate(env))\n        return \"\".join(map(str, out))\n\n    def translate(self):\n        \"\"\"Compile the expression to a list of Python AST expressions, a\n        set of variable names used, and a set of function names.\n        \"\"\"\n        expressions = []\n        varnames = set()\n        funcnames = set()\n        for part in self.parts:\n            if isinstance(part, str):\n                expressions.append(ex_literal(part))\n            else:\n                e, v, f = part.translate()\n                expressions.extend(e)\n                varnames.update(v)\n                funcnames.update(f)\n        return expressions, varnames, funcnames\n\n\n# Parser.\n\n\nclass ParseError(Exception):\n    pass\n\n\nclass Parser:\n    \"\"\"Parses a template expression string. Instantiate the class with\n    the template source and call ``parse_expression``. The ``pos`` field\n    will indicate the character after the expression finished and\n    ``parts`` will contain a list of Unicode strings, Symbols, and Calls\n    reflecting the concatenated portions of the expression.\n\n    This is a terrible, ad-hoc parser implementation based on a\n    left-to-right scan with no lexing step to speak of; it's probably\n    both inefficient and incorrect. Maybe this should eventually be\n    replaced with a real, accepted parsing technique (PEG, parser\n    generator, etc.).\n    \"\"\"\n\n    def __init__(self, string, in_argument=False):\n        \"\"\"Create a new parser.\n        :param in_arguments: boolean that indicates the parser is to be\n        used for parsing function arguments, ie. considering commas\n        (`ARG_SEP`) a special character\n        \"\"\"\n        self.string = string\n        self.in_argument = in_argument\n        self.pos = 0\n        self.parts = []\n\n    # Common parsing resources.\n    special_chars = (\n        SYMBOL_DELIM,\n        FUNC_DELIM,\n        GROUP_OPEN,\n        GROUP_CLOSE,\n        ESCAPE_CHAR,\n    )\n    escapable_chars = (SYMBOL_DELIM, FUNC_DELIM, GROUP_CLOSE, ARG_SEP)\n    terminator_chars = (GROUP_CLOSE,)\n\n    def parse_expression(self):\n        \"\"\"Parse a template expression starting at ``pos``. Resulting\n        components (Unicode strings, Symbols, and Calls) are added to\n        the ``parts`` field, a list.  The ``pos`` field is updated to be\n        the next character after the expression.\n        \"\"\"\n        # Append comma (ARG_SEP) to the list of special characters only when\n        # parsing function arguments.\n        extra_special_chars = (ARG_SEP,) if self.in_argument else ()\n        special_chars = (*self.special_chars, *extra_special_chars)\n        special_char_re = re.compile(\n            rf\"[{''.join(map(re.escape, special_chars))}]|\\Z\"\n        )\n\n        text_parts = []\n\n        while self.pos < len(self.string):\n            char = self.string[self.pos]\n\n            if char not in special_chars:\n                # A non-special character. Skip to the next special\n                # character, treating the interstice as literal text.\n                next_pos = (\n                    special_char_re.search(self.string[self.pos :]).start()\n                    + self.pos\n                )\n                text_parts.append(self.string[self.pos : next_pos])\n                self.pos = next_pos\n                continue\n\n            if self.pos == len(self.string) - 1:\n                # The last character can never begin a structure, so we\n                # just interpret it as a literal character (unless it\n                # terminates the expression, as with , and }).\n                if char not in self.terminator_chars + extra_special_chars:\n                    text_parts.append(char)\n                    self.pos += 1\n                break\n\n            next_char = self.string[self.pos + 1]\n            if char == ESCAPE_CHAR and next_char in (\n                self.escapable_chars + extra_special_chars\n            ):\n                # An escaped special character ($$, $}, etc.). Note that\n                # ${ is not an escape sequence: this is ambiguous with\n                # the start of a symbol and it's not necessary (just\n                # using { suffices in all cases).\n                text_parts.append(next_char)\n                self.pos += 2  # Skip the next character.\n                continue\n\n            # Shift all characters collected so far into a single string.\n            if text_parts:\n                self.parts.append(\"\".join(text_parts))\n                text_parts = []\n\n            if char == SYMBOL_DELIM:\n                # Parse a symbol.\n                self.parse_symbol()\n            elif char == FUNC_DELIM:\n                # Parse a function call.\n                self.parse_call()\n            elif char in self.terminator_chars + extra_special_chars:\n                # Template terminated.\n                break\n            elif char == GROUP_OPEN:\n                # Start of a group has no meaning hear; just pass\n                # through the character.\n                text_parts.append(char)\n                self.pos += 1\n            else:\n                assert False\n\n        # If any parsed characters remain, shift them into a string.\n        if text_parts:\n            self.parts.append(\"\".join(text_parts))\n\n    def parse_symbol(self):\n        \"\"\"Parse a variable reference (like ``$foo`` or ``${foo}``)\n        starting at ``pos``. Possibly appends a Symbol object (or,\n        failing that, text) to the ``parts`` field and updates ``pos``.\n        The character at ``pos`` must, as a precondition, be ``$``.\n        \"\"\"\n        assert self.pos < len(self.string)\n        assert self.string[self.pos] == SYMBOL_DELIM\n\n        if self.pos == len(self.string) - 1:\n            # Last character.\n            self.parts.append(SYMBOL_DELIM)\n            self.pos += 1\n            return\n\n        next_char = self.string[self.pos + 1]\n        start_pos = self.pos\n        self.pos += 1\n\n        if next_char == GROUP_OPEN:\n            # A symbol like ${this}.\n            self.pos += 1  # Skip opening.\n            closer = self.string.find(GROUP_CLOSE, self.pos)\n            if closer == -1 or closer == self.pos:\n                # No closing brace found or identifier is empty.\n                self.parts.append(self.string[start_pos : self.pos])\n            else:\n                # Closer found.\n                ident = self.string[self.pos : closer]\n                self.pos = closer + 1\n                self.parts.append(\n                    Symbol(ident, self.string[start_pos : self.pos])\n                )\n\n        else:\n            # A bare-word symbol.\n            ident = self._parse_ident()\n            if ident:\n                # Found a real symbol.\n                self.parts.append(\n                    Symbol(ident, self.string[start_pos : self.pos])\n                )\n            else:\n                # A standalone $.\n                self.parts.append(SYMBOL_DELIM)\n\n    def parse_call(self):\n        \"\"\"Parse a function call (like ``%foo{bar,baz}``) starting at\n        ``pos``.  Possibly appends a Call object to ``parts`` and update\n        ``pos``. The character at ``pos`` must be ``%``.\n        \"\"\"\n        assert self.pos < len(self.string)\n        assert self.string[self.pos] == FUNC_DELIM\n\n        start_pos = self.pos\n        self.pos += 1\n\n        ident = self._parse_ident()\n        if not ident:\n            # No function name.\n            self.parts.append(FUNC_DELIM)\n            return\n\n        if self.pos >= len(self.string):\n            # Identifier terminates string.\n            self.parts.append(self.string[start_pos : self.pos])\n            return\n\n        if self.string[self.pos] != GROUP_OPEN:\n            # Argument list not opened.\n            self.parts.append(self.string[start_pos : self.pos])\n            return\n\n        # Skip past opening brace and try to parse an argument list.\n        self.pos += 1\n        args = self.parse_argument_list()\n        if self.pos >= len(self.string) or self.string[self.pos] != GROUP_CLOSE:\n            # Arguments unclosed.\n            self.parts.append(self.string[start_pos : self.pos])\n            return\n\n        self.pos += 1  # Move past closing brace.\n        self.parts.append(Call(ident, args, self.string[start_pos : self.pos]))\n\n    def parse_argument_list(self):\n        \"\"\"Parse a list of arguments starting at ``pos``, returning a\n        list of Expression objects. Does not modify ``parts``. Should\n        leave ``pos`` pointing to a } character or the end of the\n        string.\n        \"\"\"\n        # Try to parse a subexpression in a subparser.\n        expressions = []\n\n        while self.pos < len(self.string):\n            subparser = Parser(self.string[self.pos :], in_argument=True)\n            subparser.parse_expression()\n\n            # Extract and advance past the parsed expression.\n            expressions.append(Expression(subparser.parts))\n            self.pos += subparser.pos\n\n            if (\n                self.pos >= len(self.string)\n                or self.string[self.pos] == GROUP_CLOSE\n            ):\n                # Argument list terminated by EOF or closing brace.\n                break\n\n            # Only other way to terminate an expression is with ,.\n            # Continue to the next argument.\n            assert self.string[self.pos] == ARG_SEP\n            self.pos += 1\n\n        return expressions\n\n    def _parse_ident(self):\n        \"\"\"Parse an identifier and return it (possibly an empty string).\n        Updates ``pos``.\n        \"\"\"\n        remainder = self.string[self.pos :]\n        ident = re.match(r\"\\w*\", remainder).group(0)\n        self.pos += len(ident)\n        return ident\n\n\ndef _parse(template):\n    \"\"\"Parse a top-level template string Expression. Any extraneous text\n    is considered literal text.\n    \"\"\"\n    parser = Parser(template)\n    parser.parse_expression()\n\n    parts = parser.parts\n    remainder = parser.string[parser.pos :]\n    if remainder:\n        parts.append(remainder)\n    return Expression(parts)\n\n\n@functools.lru_cache(maxsize=128)\ndef template(fmt):\n    return Template(fmt)\n\n\n# External interface.\nclass Template:\n    \"\"\"A string template, including text, Symbols, and Calls.\"\"\"\n\n    def __init__(self, template):\n        self.expr = _parse(template)\n        self.original = template\n        self.compiled = self.translate()\n\n    def __eq__(self, other):\n        return self.original == other.original\n\n    def interpret(self, values={}, functions={}):\n        \"\"\"Like `substitute`, but forces the interpreter (rather than\n        the compiled version) to be used. The interpreter includes\n        exception-handling code for missing variables and buggy template\n        functions but is much slower.\n        \"\"\"\n        return self.expr.evaluate(Environment(values, functions))\n\n    def substitute(self, values={}, functions={}):\n        \"\"\"Evaluate the template given the values and functions.\"\"\"\n        try:\n            res = self.compiled(values, functions)\n        except Exception:  # Handle any exceptions thrown by compiled version.\n            res = self.interpret(values, functions)\n\n        return res\n\n    def translate(self):\n        \"\"\"Compile the template to a Python function.\"\"\"\n        expressions, varnames, funcnames = self.expr.translate()\n\n        argnames = []\n        for varname in varnames:\n            argnames.append(f\"{VARIABLE_PREFIX}{varname}\")\n        for funcname in funcnames:\n            argnames.append(f\"{FUNCTION_PREFIX}{funcname}\")\n\n        func = compile_func(\n            argnames,\n            [ast.Return(ast.List(expressions, ast.Load()))],\n        )\n\n        def wrapper_func(values={}, functions={}):\n            args = {}\n            for varname in varnames:\n                args[f\"{VARIABLE_PREFIX}{varname}\"] = values[varname]\n            for funcname in funcnames:\n                args[f\"{FUNCTION_PREFIX}{funcname}\"] = functions[funcname]\n            parts = func(**args)\n            return \"\".join(parts)\n\n        return wrapper_func\n\n\n# Performance tests.\n\nif __name__ == \"__main__\":\n    import timeit\n\n    _tmpl = Template(\"foo $bar %baz{foozle $bar barzle} $bar\")\n    _vars = {\"bar\": \"qux\"}\n    _funcs = {\"baz\": str.upper}\n    interp_time = timeit.timeit(\n        \"_tmpl.interpret(_vars, _funcs)\",\n        \"from __main__ import _tmpl, _vars, _funcs\",\n        number=10000,\n    )\n    print(interp_time)\n    comp_time = timeit.timeit(\n        \"_tmpl.substitute(_vars, _funcs)\",\n        \"from __main__ import _tmpl, _vars, _funcs\",\n        number=10000,\n    )\n    print(comp_time)\n    print(\"Speedup:\", interp_time / comp_time)\n"
  },
  {
    "path": "beets/util/hidden.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n# Copyright 2024, Arav K.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Simple library to work out if a file is hidden on different platforms.\"\"\"\n\nimport ctypes\nimport os\nimport stat\nimport sys\nfrom pathlib import Path\n\n\ndef is_hidden(path: bytes | Path) -> bool:\n    \"\"\"\n    Determine whether the given path is treated as a 'hidden file' by the OS.\n    \"\"\"\n\n    if isinstance(path, bytes):\n        path = Path(os.fsdecode(path))\n\n    # TODO: Avoid doing a platform check on every invocation of the function.\n    # TODO: Stop supporting 'bytes' inputs once 'pathlib' is fully integrated.\n\n    if sys.platform == \"win32\":\n        # On Windows, we check for an FS-provided attribute.\n\n        # FILE_ATTRIBUTE_HIDDEN = 2 (0x2) from GetFileAttributes documentation.\n        hidden_mask = 2\n\n        # Retrieve the attributes for the file.\n        attrs = ctypes.windll.kernel32.GetFileAttributesW(str(path))\n\n        # Ensure the attribute mask is valid.\n        if attrs < 0:\n            return False\n\n        # Check for the hidden attribute.\n        return attrs & hidden_mask\n\n    # On OS X, we check for an FS-provided attribute.\n    if sys.platform == \"darwin\":\n        if hasattr(os.stat_result, \"st_flags\") and hasattr(stat, \"UF_HIDDEN\"):\n            if path.lstat().st_flags & stat.UF_HIDDEN:\n                return True\n\n    # On all non-Windows platforms, we check for a '.'-prefixed file name.\n    if path.name.startswith(\".\"):\n        return True\n\n    return False\n"
  },
  {
    "path": "beets/util/id_extractors.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Helpers around the extraction of album/track ID's from metadata sources.\"\"\"\n\nfrom __future__ import annotations\n\nimport re\n\nfrom beets import logging\n\nlog = logging.getLogger(\"beets\")\n\n\nPATTERN_BY_SOURCE = {\n    \"spotify\": re.compile(r\"(?:^|open\\.spotify\\.com/[^/]+/)([0-9A-Za-z]{22})\"),\n    \"deezer\": re.compile(r\"(?:^|deezer\\.com/)(?:[a-z]*/)?(?:[^/]+/)?(\\d+)\"),\n    \"beatport\": re.compile(r\"(?:^|beatport\\.com/release/.+/)(\\d+)$\"),\n    \"musicbrainz\": re.compile(r\"(\\w{8}(?:-\\w{4}){3}-\\w{12})\"),\n    # - plain integer, optionally wrapped in brackets and prefixed by an\n    #   'r', as this is how discogs displays the release ID on its webpage.\n    # - legacy url format: discogs.com/<name of release>/release/<id>\n    # - legacy url short format: discogs.com/release/<id>\n    # - current url format: discogs.com/release/<id>-<name of release>\n    # See #291, #4080 and #4085 for the discussions leading up to these\n    # patterns.\n    \"discogs\": re.compile(\n        r\"(?:^|\\[?r|discogs\\.com/(?:[^/]+/)?release/)(\\d+)\\b\"\n    ),\n    # There is no such thing as a Bandcamp album or artist ID, the URL can be\n    # used as the identifier. The Bandcamp metadata source plugin works that way\n    # - https://github.com/snejus/beetcamp. Bandcamp album URLs usually look\n    # like: https://nameofartist.bandcamp.com/album/nameofalbum\n    \"bandcamp\": re.compile(r\"(.+)\"),\n    \"tidal\": re.compile(r\"([^/]+)$\"),\n}\n\n\ndef extract_release_id(source: str, id_: str) -> str | None:\n    \"\"\"Extract the release ID from a given source and ID.\n\n    Normally, the `id_` is a url string which contains the ID of the\n    release. This function extracts the ID from the URL based on the\n    `source` provided.\n    \"\"\"\n    try:\n        source_pattern = PATTERN_BY_SOURCE[source.lower()]\n    except KeyError:\n        log.debug(\n            \"Unknown source '{}' for ID extraction. Returning id/url as-is.\",\n            source,\n        )\n        return id_\n\n    if m := source_pattern.search(str(id_)):\n        return m[1]\n\n    return None\n"
  },
  {
    "path": "beets/util/layout.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING, NamedTuple\n\nimport beets\n\nfrom .color import (\n    ESC_TEXT_REGEX,\n    RESET_COLOR,\n    color_len,\n    color_split,\n    uncolorize,\n)\n\nif TYPE_CHECKING:\n    from collections.abc import Callable, Iterator\n\n\nclass Side(NamedTuple):\n    \"\"\"A labeled segment of a two-column layout row with optional fixed width.\n\n    Holds prefix, content, and suffix strings that together form one side of\n    a formatted row. Width measurements account for ANSI color codes, which\n    do not contribute to visible character count.\n    \"\"\"\n\n    prefix: str\n    contents: str\n    suffix: str\n    width: int = -1\n\n    @property\n    def rendered(self) -> str:\n        \"\"\"Assemble the full display string by joining prefix, contents, and suffix.\"\"\"\n        return f\"{self.prefix}{self.contents}{self.suffix}\"\n\n    @property\n    def prefix_width(self) -> int:\n        \"\"\"Visible character width of the prefix, excluding color codes.\"\"\"\n        return color_len(self.prefix)\n\n    @property\n    def suffix_width(self) -> int:\n        \"\"\"Visible character width of the suffix, excluding color codes.\"\"\"\n        return color_len(self.suffix)\n\n    @property\n    def rendered_width(self) -> int:\n        \"\"\"Visible character width of the fully assembled string.\"\"\"\n        return color_len(self.rendered)\n\n\ndef indent(count: int) -> str:\n    \"\"\"Returns a string with `count` many spaces.\"\"\"\n    return \" \" * count\n\n\ndef split_into_lines(string: str, first_width: int, width: int) -> list[str]:\n    \"\"\"Split string into a list of substrings at whitespace.\n\n    The first substring has a length not longer than `first_width`, and the rest\n    of substrings have a length not longer than `width`.\n\n    `string` may contain ANSI codes at word borders.\n    \"\"\"\n    words = []\n\n    if uncolorize(string) == string:\n        # No colors in string\n        words = string.split()\n    else:\n        # Use a regex to find escapes and the text within them.\n        for m in ESC_TEXT_REGEX.finditer(string):\n            # m contains four groups:\n            # pretext - any text before escape sequence\n            # esc - intitial escape sequence\n            # text - text, no escape sequence, may contain spaces\n            # reset - ASCII colour reset\n            space_before_text = False\n            if m.group(\"pretext\") != \"\":\n                # Some pretext found, let's handle it\n                # Add any words in the pretext\n                words += m.group(\"pretext\").split()\n                if m.group(\"pretext\")[-1] == \" \":\n                    # Pretext ended on a space\n                    space_before_text = True\n                else:\n                    # Pretext ended mid-word, ensure next word\n                    pass\n            else:\n                # pretext empty, treat as if there is a space before\n                space_before_text = True\n            if m.group(\"text\")[0] == \" \":\n                # First character of the text is a space\n                space_before_text = True\n            # Now, handle the words in the main text:\n            raw_words = m.group(\"text\").split()\n            if space_before_text:\n                # Colorize each word with pre/post escapes\n                # Reconstruct colored words\n                words += [\n                    f\"{m['esc']}{raw_word}{RESET_COLOR}\"\n                    for raw_word in raw_words\n                ]\n            elif raw_words:\n                # Pretext stops mid-word\n                if m.group(\"esc\") != RESET_COLOR:\n                    # Add the rest of the current word, with a reset after it\n                    words[-1] += f\"{m['esc']}{raw_words[0]}{RESET_COLOR}\"\n                    # Add the subsequent colored words:\n                    words += [\n                        f\"{m['esc']}{raw_word}{RESET_COLOR}\"\n                        for raw_word in raw_words[1:]\n                    ]\n                else:\n                    # Caught a mid-word escape sequence\n                    words[-1] += raw_words[0]\n                    words += raw_words[1:]\n            if (\n                m.group(\"text\")[-1] != \" \"\n                and m.group(\"posttext\") != \"\"\n                and m.group(\"posttext\")[0] != \" \"\n            ):\n                # reset falls mid-word\n                post_text = m.group(\"posttext\").split()\n                words[-1] += post_text[0]\n                words += post_text[1:]\n            else:\n                # Add any words after escape sequence\n                words += m.group(\"posttext\").split()\n    result: list[str] = []\n    next_substr = \"\"\n    # Iterate over all words.\n    previous_fit = False\n    for i in range(len(words)):\n        if i == 0:\n            pot_substr = words[i]\n        else:\n            # (optimistically) add the next word to check the fit\n            pot_substr = \" \".join([next_substr, words[i]])\n        # Find out if the pot(ential)_substr fits into the next substring.\n        fits_first = len(result) == 0 and color_len(pot_substr) <= first_width\n        fits_middle = len(result) != 0 and color_len(pot_substr) <= width\n        if fits_first or fits_middle:\n            # Fitted(!) let's try and add another word before appending\n            next_substr = pot_substr\n            previous_fit = True\n        elif not fits_first and not fits_middle and previous_fit:\n            # Extra word didn't fit, append what we have\n            result.append(next_substr)\n            next_substr = words[i]\n            previous_fit = color_len(next_substr) <= width\n        else:\n            # Didn't fit anywhere\n            if uncolorize(pot_substr) == pot_substr:\n                # Simple uncolored string, append a cropped word\n                if len(result) == 0:\n                    # Crop word by the first_width for the first line\n                    result.append(pot_substr[:first_width])\n                    # add rest of word to next line\n                    next_substr = pot_substr[first_width:]\n                else:\n                    result.append(pot_substr[:width])\n                    next_substr = pot_substr[width:]\n            else:\n                # Colored strings\n                if len(result) == 0:\n                    this_line, next_line = color_split(pot_substr, first_width)\n                    result.append(this_line)\n                    next_substr = next_line\n                else:\n                    this_line, next_line = color_split(pot_substr, width)\n                    result.append(this_line)\n                    next_substr = next_line\n            previous_fit = color_len(next_substr) <= width\n\n    # We finished constructing the substrings, but the last substring\n    # has not yet been added to the result.\n    result.append(next_substr)\n    return result\n\n\ndef get_column_layout(\n    indent_str: str,\n    left: Side,\n    right: Side,\n    max_width: int,\n    separator: str,\n) -> Iterator[str]:\n    \"\"\"Print left & right data, with separator inbetween\n    'left' and 'right' have a structure of:\n    {'prefix':u'','contents':u'','suffix':u'','width':0}\n    In a column layout the printing will be:\n    {indent_str}{lhs0}{separator}{rhs0}\n            {lhs1 / padding }{rhs1}\n            ...\n    The first line of each column (i.e. {lhs0} or {rhs0}) is:\n    {prefix}{part of contents}{suffix}\n    With subsequent lines (i.e. {lhs1}, {rhs1} onwards) being the\n    rest of contents, wrapped if the width would be otherwise exceeded.\n    \"\"\"\n    if left.width == -1 or right.width == -1:\n        # If widths have not been defined, set to share space.\n        width = (max_width - len(indent_str) - len(separator)) // 2\n        left = left._replace(width=width)\n        right = right._replace(width=width)\n    # On the first line, account for suffix as well as prefix\n    left_width_without_prefix = left.width - left.prefix_width\n    left_split = split_into_lines(\n        left.contents,\n        left_width_without_prefix - left.suffix_width,\n        left_width_without_prefix,\n    )\n\n    right_width_without_prefix = right.width - right.prefix_width\n    right_split = split_into_lines(\n        right.contents,\n        right_width_without_prefix - right.suffix_width,\n        right_width_without_prefix,\n    )\n\n    max_line_count = max(len(left_split), len(right_split))\n\n    out = \"\"\n    for i in range(max_line_count):\n        # indentation\n        out += indent_str\n\n        # Prefix or indent_str for line\n        if i == 0:\n            out += left.prefix\n        else:\n            out += indent(left.prefix_width)\n\n        # Line i of left hand side contents.\n        if i < len(left_split):\n            out += left_split[i]\n            left_part_len = color_len(left_split[i])\n        else:\n            left_part_len = 0\n\n        # Padding until end of column.\n        # Note: differs from original\n        # column calcs in not -1 afterwards for space\n        # in track number as that is included in 'prefix'\n        padding = left.width - left.prefix_width - left_part_len\n\n        # Remove some padding on the first line to display\n        # length\n        if i == 0:\n            padding -= left.suffix_width\n\n        out += indent(padding)\n\n        if i == 0:\n            out += left.suffix\n\n        # Separator between columns.\n        if i == 0:\n            out += separator\n        else:\n            out += indent(len(separator))\n\n        # Right prefix, contents, padding, suffix\n        if i == 0:\n            out += right.prefix\n        else:\n            out += indent(right.prefix_width)\n\n        # Line i of right hand side.\n        if i < len(right_split):\n            out += right_split[i]\n            right_part_len = color_len(right_split[i])\n        else:\n            right_part_len = 0\n\n        # Padding until end of column\n        padding = right.width - right.prefix_width - right_part_len\n        # Remove some padding on the first line to display\n        # length\n        if i == 0:\n            padding -= right.suffix_width\n        out += indent(padding)\n        # Length in first line\n        if i == 0:\n            out += right.suffix\n\n        # Linebreak, except in the last line.\n        if i < max_line_count - 1:\n            out += \"\\n\"\n\n    # Constructed all of the columns, now print\n    yield out\n\n\ndef get_newline_layout(\n    indent_str: str,\n    left: Side,\n    right: Side,\n    max_width: int,\n    separator: str,\n) -> Iterator[str]:\n    \"\"\"Prints using a newline separator between left & right if\n    they go over their allocated widths. The datastructures are\n    shared with the column layout. In contrast to the column layout,\n    the prefix and suffix are printed at the beginning and end of\n    the contents. If no wrapping is required (i.e. everything fits) the\n    first line will look exactly the same as the column layout:\n    {indent}{lhs0}{separator}{rhs0}\n    However if this would go over the width given, the layout now becomes:\n    {indent}{lhs0}\n    {indent}{separator}{rhs0}\n    If {lhs0} would go over the maximum width, the subsequent lines are\n    indented a second time for ease of reading.\n    \"\"\"\n    width_without_prefix = max_width - len(indent_str)\n    width_without_double_prefix = max_width - 2 * len(indent_str)\n    # On lower lines we will double the indent for clarity\n    left_split = split_into_lines(\n        left.rendered,\n        width_without_prefix,\n        width_without_double_prefix,\n    )\n    # Repeat calculations for rhs, including separator on first line\n    right_split = split_into_lines(\n        right.rendered,\n        width_without_prefix - len(separator),\n        width_without_double_prefix,\n    )\n    for i, line in enumerate(left_split):\n        if i == 0:\n            yield f\"{indent_str}{line}\"\n        elif line != \"\":\n            # Ignore empty lines\n            yield f\"{indent_str * 2}{line}\"\n    for i, line in enumerate(right_split):\n        if i == 0:\n            yield f\"{indent_str}{separator}{line}\"\n        elif line != \"\":\n            yield f\"{indent_str * 2}{line}\"\n\n\ndef get_layout_method() -> Callable[[str, Side, Side, int, str], Iterator[str]]:\n    return beets.config[\"ui\"][\"import\"][\"layout\"].as_choice(\n        {\"column\": get_column_layout, \"newline\": get_newline_layout}\n    )\n\n\ndef get_layout_lines(\n    indent_str: str,\n    left: Side,\n    right: Side,\n    max_width: int,\n) -> Iterator[str]:\n    # No right hand information, so we don't need a separator.\n    separator = \"\" if right.rendered == \"\" else \" -> \"\n    first_line_no_wrap = (\n        f\"{indent_str}{left.rendered}{separator}{right.rendered}\"\n    )\n    if color_len(first_line_no_wrap) < max_width:\n        # Everything fits, print out line.\n        yield first_line_no_wrap\n    else:\n        layout_method = get_layout_method()\n        yield from layout_method(indent_str, left, right, max_width, separator)\n"
  },
  {
    "path": "beets/util/lyrics.py",
    "content": "from __future__ import annotations\n\nimport re\nfrom contextlib import suppress\nfrom dataclasses import dataclass, field\nfrom functools import cached_property\nfrom typing import TYPE_CHECKING, Any\nfrom urllib.parse import urlparse\n\nfrom beets.util import unique_list\n\nif TYPE_CHECKING:\n    from beets.library import Item\n\nINSTRUMENTAL_LYRICS = \"[Instrumental]\"\nBACKEND_NAMES = {\"genius\", \"musixmatch\", \"lrclib\", \"tekstowo\"}\n\n\n@dataclass\nclass Lyrics:\n    \"\"\"Represent lyrics text together with structured source metadata.\n\n    This value object keeps the canonical lyrics body, optional provenance, and\n    optional translation metadata synchronized across fetching, translation, and\n    persistence.\n    \"\"\"\n\n    ORIGINAL_PAT = re.compile(r\"[^\\n]+ / \")\n    TRANSLATION_PAT = re.compile(r\" / [^\\n]+\")\n    LINE_PARTS_PAT = re.compile(r\"^(\\[\\d\\d:\\d\\d\\.\\d\\d\\]|) *(.*)$\")\n\n    text: str\n    backend: str | None = None\n    url: str | None = None\n    language: str | None = None\n    translation_language: str | None = None\n    translations: list[str] = field(default_factory=list)\n\n    def __post_init__(self) -> None:\n        \"\"\"Populate missing language metadata from the current text.\"\"\"\n        try:\n            import langdetect\n        except ImportError:\n            return\n\n        # Set seed to 0 for deterministic results\n        langdetect.DetectorFactory.seed = 0\n\n        if not self.text or self.text == INSTRUMENTAL_LYRICS:\n            return\n\n        if not self.language:\n            with suppress(langdetect.LangDetectException):\n                self.language = langdetect.detect(self.original_text).upper()\n\n        if not self.translation_language:\n            all_lines = self.text.splitlines()\n            lines_with_delimiter_count = sum(\n                1 for ln in all_lines if \" / \" in ln\n            )\n            if lines_with_delimiter_count >= len(all_lines) / 2:\n                # we are confident we are dealing with translations\n                with suppress(langdetect.LangDetectException):\n                    self.translation_language = langdetect.detect(\n                        self.ORIGINAL_PAT.sub(\"\", self.text)\n                    ).upper()\n\n    @classmethod\n    def from_legacy_text(cls, text: str) -> Lyrics:\n        \"\"\"Build lyrics from legacy text that may include an inline source.\"\"\"\n        data: dict[str, Any] = {}\n        data[\"text\"], *suffix = text.split(\"\\n\\nSource: \")\n        if suffix:\n            url = suffix[0].strip()\n            url_root = urlparse(url).netloc.removeprefix(\"www.\").split(\".\")[0]\n            data.update(\n                url=url,\n                backend=url_root if url_root in BACKEND_NAMES else \"google\",\n            )\n\n        return cls(**data)\n\n    @classmethod\n    def from_item(cls, item: Item) -> Lyrics:\n        \"\"\"Build lyrics from an item's canonical text and flexible metadata.\"\"\"\n        data = {\"text\": item.lyrics}\n        for key in (\"backend\", \"url\", \"language\", \"translation_language\"):\n            data[key] = item.get(f\"lyrics_{key}\", with_album=False)\n\n        return cls(**data)\n\n    @cached_property\n    def original_text(self) -> str:\n        \"\"\"Return the original text without translations.\"\"\"\n        # Remove translations from the lyrics text.\n        return self.TRANSLATION_PAT.sub(\"\", self.text).strip()\n\n    @cached_property\n    def _split_lines(self) -> list[tuple[str, str]]:\n        \"\"\"Split lyrics into timestamp/text pairs for line-wise processing.\n\n        Timestamps, when present, are kept separate so callers can translate or\n        normalize text without losing synced timing information.\n        \"\"\"\n        return [\n            (m[1], m[2]) if (m := self.LINE_PARTS_PAT.match(line)) else (\"\", \"\")\n            for line in self.text.splitlines()\n        ]\n\n    @cached_property\n    def timestamps(self) -> list[str]:\n        \"\"\"Return per-line timestamp prefixes from the lyrics text.\"\"\"\n        return [ts for ts, _ in self._split_lines]\n\n    @cached_property\n    def text_lines(self) -> list[str]:\n        \"\"\"Return per-line lyric text with timestamps removed.\"\"\"\n        return [ln for _, ln in self._split_lines]\n\n    @property\n    def synced(self) -> bool:\n        \"\"\"Return whether the lyrics contain synced timestamp markers.\"\"\"\n        return any(self.timestamps)\n\n    @property\n    def translated(self) -> bool:\n        \"\"\"Return whether translation metadata is available.\"\"\"\n        return bool(self.translation_language)\n\n    @property\n    def full_text(self) -> str:\n        \"\"\"Return canonical text with translations merged when available.\"\"\"\n        if not self.translations:\n            return self.text\n\n        text_pairs = list(zip(self.text_lines, self.translations))\n\n        # only add the separator for non-empty and differing translations\n        texts = [\" / \".join(unique_list(filter(None, p))) for p in text_pairs]\n        # only add the space between non-empty timestamps and texts\n        return \"\\n\".join(\n            \" \".join(filter(None, p)) for p in zip(self.timestamps, texts)\n        )\n"
  },
  {
    "path": "beets/util/m3u.py",
    "content": "# This file is part of beets.\n# Copyright 2022, J0J0 Todos.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Provides utilities to read, write and manipulate m3u playlist files.\"\"\"\n\nimport traceback\n\nfrom beets.util import FilesystemError, mkdirall, normpath, syspath\n\n\nclass EmptyPlaylistError(Exception):\n    \"\"\"Raised when a playlist file without media files is saved or loaded.\"\"\"\n\n    pass\n\n\nclass M3UFile:\n    \"\"\"Reads and writes m3u or m3u8 playlist files.\"\"\"\n\n    def __init__(self, path):\n        \"\"\"``path`` is the absolute path to the playlist file.\n\n        The playlist file type, m3u or m3u8 is determined by 1) the ending\n        being m3u8 and 2) the file paths contained in the list being utf-8\n        encoded. Since the list is passed from the outside, this is currently\n        out of control of this class.\n        \"\"\"\n        self.path = path\n        self.extm3u = False\n        self.media_list = []\n\n    def load(self):\n        \"\"\"Reads the m3u file from disk and sets the object's attributes.\"\"\"\n        pl_normpath = normpath(self.path)\n        try:\n            with open(syspath(pl_normpath), \"rb\") as pl_file:\n                raw_contents = pl_file.readlines()\n        except OSError as exc:\n            raise FilesystemError(\n                exc, \"read\", (pl_normpath,), traceback.format_exc()\n            )\n\n        self.extm3u = True if raw_contents[0].rstrip() == b\"#EXTM3U\" else False\n        for line in raw_contents[1:]:\n            if line.startswith(b\"#\"):\n                # Support for specific EXTM3U comments could be added here.\n                continue\n            self.media_list.append(normpath(line.rstrip()))\n        if not self.media_list:\n            raise EmptyPlaylistError\n\n    def set_contents(self, media_list, extm3u=True):\n        \"\"\"Sets self.media_list to a list of media file paths.\n\n        Also sets additional flags, changing the final m3u-file's format.\n\n        ``media_list`` is a list of paths to media files that should be added\n        to the playlist (relative or absolute paths, that's the responsibility\n        of the caller). By default the ``extm3u`` flag is set, to ensure a\n        save-operation writes an m3u-extended playlist (comment \"#EXTM3U\" at\n        the top of the file).\n        \"\"\"\n        self.media_list = media_list\n        self.extm3u = extm3u\n\n    def write(self):\n        \"\"\"Writes the m3u file to disk.\n\n        Handles the creation of potential parent directories.\n        \"\"\"\n        header = [b\"#EXTM3U\"] if self.extm3u else []\n        if not self.media_list:\n            raise EmptyPlaylistError\n        contents = header + self.media_list\n        pl_normpath = normpath(self.path)\n        mkdirall(pl_normpath)\n\n        try:\n            with open(syspath(pl_normpath), \"wb\") as pl_file:\n                for line in contents:\n                    pl_file.write(line + b\"\\n\")\n                pl_file.write(b\"\\n\")  # Final linefeed to prevent noeol file.\n        except OSError as exc:\n            raise FilesystemError(\n                exc, \"create\", (pl_normpath,), traceback.format_exc()\n            )\n"
  },
  {
    "path": "beets/util/pipeline.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Simple but robust implementation of generator/coroutine-based\npipelines in Python. The pipelines may be run either sequentially\n(single-threaded) or in parallel (one thread per pipeline stage).\n\nThis implementation supports pipeline bubbles (indications that the\nprocessing for a certain item should abort). To use them, yield the\nBUBBLE constant from any stage coroutine except the last.\n\nIn the parallel case, the implementation transparently handles thread\nshutdown when the processing is complete and when a stage raises an\nexception. KeyboardInterrupts (^C) are also handled.\n\nWhen running a parallel pipeline, it is also possible to use\nmultiple coroutines for the same pipeline stage; this lets you speed\nup a bottleneck stage by dividing its work among multiple threads.\nTo do so, pass an iterable of coroutines to the Pipeline constructor\nin place of any single coroutine.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport queue\nimport sys\nfrom threading import Lock, Thread\nfrom typing import TYPE_CHECKING, TypeVar\n\nfrom typing_extensions import TypeVarTuple, Unpack\n\nif TYPE_CHECKING:\n    from collections.abc import Callable, Generator\n\nBUBBLE = \"__PIPELINE_BUBBLE__\"\nPOISON = \"__PIPELINE_POISON__\"\n\nDEFAULT_QUEUE_SIZE = 16\n\nTq = TypeVar(\"Tq\")\n\n\ndef _invalidate_queue(q, val=None, sync=True):\n    \"\"\"Breaks a Queue such that it never blocks, always has size 1,\n    and has no maximum size. get()ing from the queue returns `val`,\n    which defaults to None. `sync` controls whether a lock is\n    required (because it's not reentrant!).\n    \"\"\"\n\n    def _qsize(len=len):\n        return 1\n\n    def _put(item):\n        pass\n\n    def _get():\n        return val\n\n    if sync:\n        q.mutex.acquire()\n\n    try:\n        # Originally, we set `maxsize` to 0 here, which is supposed to mean\n        # an unlimited queue size. However, there is a race condition since\n        # Python 3.2 when this attribute is changed while another thread is\n        # waiting in put()/get() due to a full/empty queue.\n        # Setting it to 2 is still hacky because Python does not give any\n        # guarantee what happens if Queue methods/attributes are overwritten\n        # when it is already in use. However, because of our dummy _put()\n        # and _get() methods, it provides a workaround to let the queue appear\n        # to be never empty or full.\n        # See issue https://github.com/beetbox/beets/issues/2078\n        q.maxsize = 2\n        q._qsize = _qsize\n        q._put = _put\n        q._get = _get\n        q.not_empty.notify_all()\n        q.not_full.notify_all()\n\n    finally:\n        if sync:\n            q.mutex.release()\n\n\nclass CountedQueue(queue.Queue[Tq]):\n    \"\"\"A queue that keeps track of the number of threads that are\n    still feeding into it. The queue is poisoned when all threads are\n    finished with the queue.\n    \"\"\"\n\n    def __init__(self, maxsize=0):\n        queue.Queue.__init__(self, maxsize)\n        self.nthreads = 0\n        self.poisoned = False\n\n    def acquire(self):\n        \"\"\"Indicate that a thread will start putting into this queue.\n        Should not be called after the queue is already poisoned.\n        \"\"\"\n        with self.mutex:\n            assert not self.poisoned\n            assert self.nthreads >= 0\n            self.nthreads += 1\n\n    def release(self):\n        \"\"\"Indicate that a thread that was putting into this queue has\n        exited. If this is the last thread using the queue, the queue\n        is poisoned.\n        \"\"\"\n        with self.mutex:\n            self.nthreads -= 1\n            assert self.nthreads >= 0\n            if self.nthreads == 0:\n                # All threads are done adding to this queue. Poison it\n                # when it becomes empty.\n                self.poisoned = True\n\n                # Replacement _get invalidates when no items remain.\n                _old_get = self._get\n\n                def _get():\n                    out = _old_get()\n                    if not self.queue:\n                        _invalidate_queue(self, POISON, False)\n                    return out\n\n                if self.queue:\n                    # Items remain.\n                    self._get = _get\n                else:\n                    # No items. Invalidate immediately.\n                    _invalidate_queue(self, POISON, False)\n\n\nclass MultiMessage:\n    \"\"\"A message yielded by a pipeline stage encapsulating multiple\n    values to be sent to the next stage.\n    \"\"\"\n\n    def __init__(self, messages):\n        self.messages = messages\n\n\ndef multiple(messages):\n    \"\"\"Yield multiple([message, ..]) from a pipeline stage to send\n    multiple values to the next pipeline stage.\n    \"\"\"\n    return MultiMessage(messages)\n\n\nA = TypeVarTuple(\"A\")  # Arguments of a function (omitting the task)\nT = TypeVar(\"T\")  # Type of the task\n# Normally these are concatenated i.e. (*args, task)\n\n# Return type of the function (should normally be task but sadly\n# we cant enforce this with the current stage functions without\n# a refactor)\nR = TypeVar(\"R\")\n\n\ndef stage(\n    func: Callable[\n        [Unpack[A], T],\n        R | None,\n    ],\n):\n    \"\"\"Decorate a function to become a simple stage.\n\n    >>> @stage\n    ... def add(n, i):\n    ...     return i + n\n    >>> pipe = Pipeline([\n    ...     iter([1, 2, 3]),\n    ...     add(2),\n    ... ])\n    >>> list(pipe.pull())\n    [3, 4, 5]\n    \"\"\"\n\n    def coro(*args: Unpack[A]) -> Generator[R | T | None, T, None]:\n        task: R | T | None = None\n        while True:\n            task = yield task\n            task = func(*args, task)\n\n    return coro\n\n\ndef mutator_stage(func: Callable[[Unpack[A], T], R]):\n    \"\"\"Decorate a function that manipulates items in a coroutine to\n    become a simple stage.\n\n    >>> @mutator_stage\n    ... def setkey(key, item):\n    ...     item[key] = True\n    >>> pipe = Pipeline([\n    ...     iter([{'x': False}, {'a': False}]),\n    ...     setkey('x'),\n    ... ])\n    >>> list(pipe.pull())\n    [{'x': True}, {'a': False, 'x': True}]\n    \"\"\"\n\n    def coro(*args: Unpack[A]) -> Generator[T | None, T, None]:\n        task = None\n        while True:\n            task = yield task\n            func(*args, task)\n\n    return coro\n\n\ndef _allmsgs(obj):\n    \"\"\"Returns a list of all the messages encapsulated in obj. If obj\n    is a MultiMessage, returns its enclosed messages. If obj is BUBBLE,\n    returns an empty list. Otherwise, returns a list containing obj.\n    \"\"\"\n    if isinstance(obj, MultiMessage):\n        return obj.messages\n    elif obj == BUBBLE:\n        return []\n    else:\n        return [obj]\n\n\nclass PipelineThread(Thread):\n    \"\"\"Abstract base class for pipeline-stage threads.\"\"\"\n\n    def __init__(self, all_threads):\n        super().__init__()\n        self.abort_lock = Lock()\n        self.abort_flag = False\n        self.all_threads = all_threads\n        self.exc_info = None\n\n    def abort(self):\n        \"\"\"Shut down the thread at the next chance possible.\"\"\"\n        with self.abort_lock:\n            self.abort_flag = True\n\n            # Ensure that we are not blocking on a queue read or write.\n            if hasattr(self, \"in_queue\"):\n                _invalidate_queue(self.in_queue, POISON)\n            if hasattr(self, \"out_queue\"):\n                _invalidate_queue(self.out_queue, POISON)\n\n    def abort_all(self, exc_info):\n        \"\"\"Abort all other threads in the system for an exception.\"\"\"\n        self.exc_info = exc_info\n        for thread in self.all_threads:\n            thread.abort()\n\n\nclass FirstPipelineThread(PipelineThread):\n    \"\"\"The thread running the first stage in a parallel pipeline setup.\n    The coroutine should just be a generator.\n    \"\"\"\n\n    def __init__(self, coro, out_queue, all_threads):\n        super().__init__(all_threads)\n        self.coro = coro\n        self.out_queue = out_queue\n        self.out_queue.acquire()\n\n    def run(self):\n        try:\n            while True:\n                with self.abort_lock:\n                    if self.abort_flag:\n                        return\n\n                # Get the value from the generator.\n                try:\n                    msg = next(self.coro)\n                except StopIteration:\n                    break\n\n                # Send messages to the next stage.\n                for msg in _allmsgs(msg):\n                    with self.abort_lock:\n                        if self.abort_flag:\n                            return\n                    self.out_queue.put(msg)\n\n        except BaseException:\n            self.abort_all(sys.exc_info())\n            return\n\n        # Generator finished; shut down the pipeline.\n        self.out_queue.release()\n\n\nclass MiddlePipelineThread(PipelineThread):\n    \"\"\"A thread running any stage in the pipeline except the first or\n    last.\n    \"\"\"\n\n    def __init__(self, coro, in_queue, out_queue, all_threads):\n        super().__init__(all_threads)\n        self.coro = coro\n        self.in_queue = in_queue\n        self.out_queue = out_queue\n        self.out_queue.acquire()\n\n    def run(self):\n        try:\n            # Prime the coroutine.\n            next(self.coro)\n\n            while True:\n                with self.abort_lock:\n                    if self.abort_flag:\n                        return\n\n                # Get the message from the previous stage.\n                msg = self.in_queue.get()\n                if msg is POISON:\n                    break\n\n                with self.abort_lock:\n                    if self.abort_flag:\n                        return\n\n                # Invoke the current stage.\n                out = self.coro.send(msg)\n\n                # Send messages to next stage.\n                for msg in _allmsgs(out):\n                    with self.abort_lock:\n                        if self.abort_flag:\n                            return\n                    self.out_queue.put(msg)\n\n        except BaseException:\n            self.abort_all(sys.exc_info())\n            return\n\n        # Pipeline is shutting down normally.\n        self.out_queue.release()\n\n\nclass LastPipelineThread(PipelineThread):\n    \"\"\"A thread running the last stage in a pipeline. The coroutine\n    should yield nothing.\n    \"\"\"\n\n    def __init__(self, coro, in_queue, all_threads):\n        super().__init__(all_threads)\n        self.coro = coro\n        self.in_queue = in_queue\n\n    def run(self):\n        # Prime the coroutine.\n        next(self.coro)\n\n        try:\n            while True:\n                with self.abort_lock:\n                    if self.abort_flag:\n                        return\n\n                # Get the message from the previous stage.\n                msg = self.in_queue.get()\n                if msg is POISON:\n                    break\n\n                with self.abort_lock:\n                    if self.abort_flag:\n                        return\n\n                # Send to consumer.\n                self.coro.send(msg)\n\n        except BaseException:\n            self.abort_all(sys.exc_info())\n            return\n\n\nclass Pipeline:\n    \"\"\"Represents a staged pattern of work. Each stage in the pipeline\n    is a coroutine that receives messages from the previous stage and\n    yields messages to be sent to the next stage.\n    \"\"\"\n\n    def __init__(self, stages):\n        \"\"\"Makes a new pipeline from a list of coroutines. There must\n        be at least two stages.\n        \"\"\"\n        if len(stages) < 2:\n            raise ValueError(\"pipeline must have at least two stages\")\n        self.stages = []\n        for stage in stages:\n            if isinstance(stage, (list, tuple)):\n                self.stages.append(stage)\n            else:\n                # Default to one thread per stage.\n                self.stages.append((stage,))\n\n    def run_sequential(self):\n        \"\"\"Run the pipeline sequentially in the current thread. The\n        stages are run one after the other. Only the first coroutine\n        in each stage is used.\n        \"\"\"\n        list(self.pull())\n\n    def run_parallel(self, queue_size=DEFAULT_QUEUE_SIZE):\n        \"\"\"Run the pipeline in parallel using one thread per stage. The\n        messages between the stages are stored in queues of the given\n        size.\n        \"\"\"\n        queue_count = len(self.stages) - 1\n        queues = [CountedQueue(queue_size) for i in range(queue_count)]\n        threads = []\n\n        # Set up first stage.\n        for coro in self.stages[0]:\n            threads.append(FirstPipelineThread(coro, queues[0], threads))\n\n        # Middle stages.\n        for i in range(1, queue_count):\n            for coro in self.stages[i]:\n                threads.append(\n                    MiddlePipelineThread(\n                        coro, queues[i - 1], queues[i], threads\n                    )\n                )\n\n        # Last stage.\n        for coro in self.stages[-1]:\n            threads.append(LastPipelineThread(coro, queues[-1], threads))\n\n        # Start threads.\n        for thread in threads:\n            thread.start()\n\n        # Wait for termination. The final thread lasts the longest.\n        try:\n            # Using a timeout allows us to receive KeyboardInterrupt\n            # exceptions during the join().\n            while threads[-1].is_alive():\n                threads[-1].join(1)\n\n        except BaseException:\n            # Stop all the threads immediately.\n            for thread in threads:\n                thread.abort()\n            raise\n\n        finally:\n            # Make completely sure that all the threads have finished\n            # before we return. They should already be either finished,\n            # in normal operation, or aborted, in case of an exception.\n            for thread in threads[:-1]:\n                thread.join()\n\n        for thread in threads:\n            exc_info = thread.exc_info\n            if exc_info:\n                # Make the exception appear as it was raised originally.\n                raise exc_info[1].with_traceback(exc_info[2])\n\n    def pull(self):\n        \"\"\"Yield elements from the end of the pipeline. Runs the stages\n        sequentially until the last yields some messages. Each of the messages\n        is then yielded by ``pulled.next()``. If the pipeline has a consumer,\n        that is the last stage does not yield any messages, then pull will not\n        yield any messages. Only the first coroutine in each stage is used\n        \"\"\"\n        coros = [stage[0] for stage in self.stages]\n\n        # \"Prime\" the coroutines.\n        for coro in coros[1:]:\n            next(coro)\n\n        # Begin the pipeline.\n        for out in coros[0]:\n            msgs = _allmsgs(out)\n            for coro in coros[1:]:\n                next_msgs = []\n                for msg in msgs:\n                    out = coro.send(msg)\n                    next_msgs.extend(_allmsgs(out))\n                msgs = next_msgs\n            for msg in msgs:\n                yield msg\n"
  },
  {
    "path": "beets/util/units.py",
    "content": "import re\n\n\ndef raw_seconds_short(string: str) -> float:\n    \"\"\"Formats a human-readable M:SS string as a float (number of seconds).\n\n    Raises ValueError if the conversion cannot take place due to `string` not\n    being in the right format.\n    \"\"\"\n    match = re.match(r\"^(\\d+):([0-5]\\d)$\", string)\n    if not match:\n        raise ValueError(\"String not in M:SS format\")\n    minutes, seconds = map(int, match.groups())\n    return float(minutes * 60 + seconds)\n\n\ndef human_seconds_short(interval):\n    \"\"\"Formats a number of seconds as a short human-readable M:SS\n    string.\n    \"\"\"\n    interval = int(interval)\n    return f\"{interval // 60}:{interval % 60:02d}\"\n\n\ndef human_bytes(size):\n    \"\"\"Formats size, a number of bytes, in a human-readable way.\"\"\"\n    powers = [\"\", \"K\", \"M\", \"G\", \"T\", \"P\", \"E\", \"Z\", \"Y\", \"H\"]\n    unit = \"B\"\n    for power in powers:\n        if size < 1024:\n            return f\"{size:3.1f} {power}{unit}\"\n        size /= 1024.0\n        unit = \"iB\"\n    return \"big\"\n\n\ndef human_seconds(interval):\n    \"\"\"Formats interval, a number of seconds, as a human-readable time\n    interval using English words.\n    \"\"\"\n    units = [\n        (1, \"second\"),\n        (60, \"minute\"),\n        (60, \"hour\"),\n        (24, \"day\"),\n        (7, \"week\"),\n        (52, \"year\"),\n        (10, \"decade\"),\n    ]\n    for i in range(len(units) - 1):\n        increment, suffix = units[i]\n        next_increment, _ = units[i + 1]\n        interval /= float(increment)\n        if interval < next_increment:\n            break\n    else:\n        # Last unit.\n        increment, suffix = units[-1]\n        interval /= float(increment)\n\n    return f\"{interval:3.1f} {suffix}s\"\n"
  },
  {
    "path": "beetsplug/_typing.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any\n\nfrom typing_extensions import NotRequired, TypedDict\n\nJSONDict = dict[str, Any]\n\n\nclass LRCLibAPI:\n    class Item(TypedDict):\n        \"\"\"Lyrics data item returned by the LRCLib API.\"\"\"\n\n        id: int\n        name: str\n        trackName: str\n        artistName: str\n        albumName: str\n        duration: float | None\n        instrumental: bool\n        plainLyrics: str\n        syncedLyrics: str | None\n\n\nclass GeniusAPI:\n    \"\"\"Genius API data types.\n\n    This documents *only* the fields that are used in the plugin.\n    :attr:`SearchResult` is an exception, since I thought some of the other\n    fields might be useful in the future.\n    \"\"\"\n\n    class DateComponents(TypedDict):\n        year: int\n        month: int\n        day: int\n\n    class Artist(TypedDict):\n        api_path: str\n        header_image_url: str\n        id: int\n        image_url: str\n        is_meme_verified: bool\n        is_verified: bool\n        name: str\n        url: str\n\n    class Stats(TypedDict):\n        unreviewed_annotations: int\n        hot: bool\n\n    class SearchResult(TypedDict):\n        annotation_count: int\n        api_path: str\n        artist_names: str\n        full_title: str\n        header_image_thumbnail_url: str\n        header_image_url: str\n        id: int\n        lyrics_owner_id: int\n        lyrics_state: str\n        path: str\n        primary_artist_names: str\n        pyongs_count: int | None\n        relationships_index_url: str\n        release_date_components: GeniusAPI.DateComponents\n        release_date_for_display: str\n        release_date_with_abbreviated_month_for_display: str\n        song_art_image_thumbnail_url: str\n        song_art_image_url: str\n        stats: GeniusAPI.Stats\n        title: str\n        title_with_featured: str\n        url: str\n        featured_artists: list[GeniusAPI.Artist]\n        primary_artist: GeniusAPI.Artist\n        primary_artists: list[GeniusAPI.Artist]\n\n    class SearchHit(TypedDict):\n        result: GeniusAPI.SearchResult\n\n    class SearchResponse(TypedDict):\n        hits: list[GeniusAPI.SearchHit]\n\n    class Search(TypedDict):\n        response: GeniusAPI.SearchResponse\n\n    class StatusResponse(TypedDict):\n        status: int\n        message: str\n\n    class Meta(TypedDict):\n        meta: GeniusAPI.StatusResponse\n\n    Response = Search | Meta\n\n\nclass GoogleCustomSearchAPI:\n    class Response(TypedDict):\n        \"\"\"Search response from the Google Custom Search API.\n\n        If the search returns no results, the :attr:`items` field is not found.\n        \"\"\"\n\n        items: NotRequired[list[GoogleCustomSearchAPI.Item]]\n\n    class Item(TypedDict):\n        \"\"\"A Google Custom Search API result item.\n\n        :attr:`title` field is shown to the user in the search interface, thus\n        it gets truncated with an ellipsis for longer queries. For most\n        results, the full title is available as ``og:title`` metatag found\n        under the :attr:`pagemap` field. Note neither this metatag nor the\n        ``pagemap`` field is guaranteed to be present in the data.\n        \"\"\"\n\n        title: str\n        link: str\n        pagemap: NotRequired[GoogleCustomSearchAPI.Pagemap]\n\n    class Pagemap(TypedDict):\n        \"\"\"Pagemap data with a single meta tags dict in a list.\"\"\"\n\n        metatags: list[JSONDict]\n\n\nclass TranslatorAPI:\n    class Language(TypedDict):\n        \"\"\"Language data returned by the translator API.\"\"\"\n\n        language: str\n        score: float\n\n    class Translation(TypedDict):\n        \"\"\"Translation data returned by the translator API.\"\"\"\n\n        text: str\n        to: str\n\n    class Response(TypedDict):\n        \"\"\"Response from the translator API.\"\"\"\n\n        detectedLanguage: TranslatorAPI.Language\n        translations: list[TranslatorAPI.Translation]\n"
  },
  {
    "path": "beetsplug/_utils/__init__.py",
    "content": "from . import art, vfs\n\n__all__ = [\"art\", \"vfs\"]\n"
  },
  {
    "path": "beetsplug/_utils/art.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"High-level utilities for manipulating image files associated with\nmusic and items' embedded album art.\n\"\"\"\n\nimport os\nfrom tempfile import NamedTemporaryFile\n\nimport mediafile\n\nfrom beets.util import bytestring_path, displayable_path, syspath\nfrom beets.util.artresizer import ArtResizer\n\n\ndef mediafile_image(image_path, maxwidth=None):\n    \"\"\"Return a `mediafile.Image` object for the path.\"\"\"\n\n    with open(syspath(image_path), \"rb\") as f:\n        data = f.read()\n    return mediafile.Image(data, type=mediafile.ImageType.front)\n\n\ndef get_art(log, item):\n    # Extract the art.\n    try:\n        mf = mediafile.MediaFile(syspath(item.path))\n    except mediafile.UnreadableFileError as exc:\n        log.warning(\"Could not extract art from {.filepath}: {}\", item, exc)\n        return\n\n    return mf.art\n\n\ndef embed_item(\n    log,\n    item,\n    imagepath,\n    maxwidth=None,\n    itempath=None,\n    compare_threshold=0,\n    ifempty=False,\n    as_album=False,\n    id3v23=None,\n    quality=0,\n):\n    \"\"\"Embed an image into the item's media file.\"\"\"\n    # Conditions.\n    if compare_threshold:\n        is_similar = check_art_similarity(\n            log, item, imagepath, compare_threshold\n        )\n        if is_similar is None:\n            log.warning(\"Error while checking art similarity; skipping.\")\n            return\n        elif not is_similar:\n            log.info(\"Image not similar; skipping.\")\n            return\n\n    if ifempty and get_art(log, item):\n        log.info(\"media file already contained art\")\n        return\n\n    # Filters.\n    if maxwidth and not as_album:\n        imagepath = resize_image(log, imagepath, maxwidth, quality)\n\n    # Get the `Image` object from the file.\n    try:\n        log.debug(\"embedding {}\", displayable_path(imagepath))\n        image = mediafile_image(imagepath, maxwidth)\n    except OSError as exc:\n        log.warning(\"could not read image file: {}\", exc)\n        return\n\n    # Make sure the image kind is safe (some formats only support PNG\n    # and JPEG).\n    if image.mime_type not in (\"image/jpeg\", \"image/png\"):\n        log.info(\"not embedding image of unsupported type: {.mime_type}\", image)\n        return\n\n    item.try_write(path=itempath, tags={\"images\": [image]}, id3v23=id3v23)\n\n\ndef embed_album(\n    log,\n    album,\n    maxwidth=None,\n    quiet=False,\n    compare_threshold=0,\n    ifempty=False,\n    quality=0,\n):\n    \"\"\"Embed album art into all of the album's items.\"\"\"\n    imagepath = album.artpath\n    if not imagepath:\n        log.info(\"No album art present for {}\", album)\n        return\n    if not os.path.isfile(syspath(imagepath)):\n        log.info(\n            \"Album art not found at {} for {}\",\n            displayable_path(imagepath),\n            album,\n        )\n        return\n    if maxwidth:\n        imagepath = resize_image(log, imagepath, maxwidth, quality)\n\n    log.info(\"Embedding album art into {}\", album)\n\n    for item in album.items():\n        embed_item(\n            log,\n            item,\n            imagepath,\n            maxwidth,\n            None,\n            compare_threshold,\n            ifempty,\n            as_album=True,\n            quality=quality,\n        )\n\n\ndef resize_image(log, imagepath, maxwidth, quality):\n    \"\"\"Returns path to an image resized to maxwidth and encoded with the\n    specified quality level.\n    \"\"\"\n    log.debug(\n        \"Resizing album art to {} pixels wide and encoding at quality level {}\",\n        maxwidth,\n        quality,\n    )\n    imagepath = ArtResizer.shared.resize(\n        maxwidth, syspath(imagepath), quality=quality\n    )\n    return imagepath\n\n\ndef check_art_similarity(\n    log,\n    item,\n    imagepath,\n    compare_threshold,\n    artresizer=None,\n):\n    \"\"\"A boolean indicating if an image is similar to embedded item art.\n\n    If no embedded art exists, always return `True`. If the comparison fails\n    for some reason, the return value is `None`.\n\n    This must only be called if `ArtResizer.shared.can_compare` is `True`.\n    \"\"\"\n    with NamedTemporaryFile(delete=True) as f:\n        art = extract(log, f.name, item)\n\n        if not art:\n            return True\n\n        if artresizer is None:\n            artresizer = ArtResizer.shared\n\n        return artresizer.compare(art, imagepath, compare_threshold)\n\n\ndef extract(log, outpath, item):\n    art = get_art(log, item)\n    outpath = bytestring_path(outpath)\n    if not art:\n        log.info(\"No album art present in {}, skipping.\", item)\n        return\n\n    # Add an extension to the filename.\n    ext = mediafile.image_extension(art)\n    if not ext:\n        log.warning(\"Unknown image type in {.filepath}.\", item)\n        return\n    outpath += bytestring_path(f\".{ext}\")\n\n    log.info(\n        \"Extracting album art from: {} to: {}\",\n        item,\n        displayable_path(outpath),\n    )\n    with open(syspath(outpath), \"wb\") as f:\n        f.write(art)\n    return outpath\n\n\ndef extract_first(log, outpath, items):\n    for item in items:\n        real_path = extract(log, outpath, item)\n        if real_path:\n            return real_path\n\n\ndef clear_item(item, log):\n    if mediafile.MediaFile(syspath(item.path)).images:\n        log.debug(\"Clearing art for {}\", item)\n        item.try_write(tags={\"images\": None})\n\n\ndef clear(log, lib, query):\n    items = lib.items(query)\n    log.info(\"Clearing album art from {} items\", len(items))\n    for item in items:\n        clear_item(item, log)\n"
  },
  {
    "path": "beetsplug/_utils/musicbrainz.py",
    "content": "\"\"\"Helpers for communicating with the MusicBrainz webservice.\n\nProvides rate-limited HTTP session and convenience methods to fetch and\nnormalize API responses.\n\nThis module centralizes request handling and response shaping so callers can\nwork with consistently structured data without embedding HTTP or rate-limit\nlogic throughout the codebase.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport operator\nimport re\nfrom dataclasses import dataclass, field\nfrom functools import cached_property, singledispatchmethod, wraps\nfrom itertools import groupby, starmap\nfrom typing import TYPE_CHECKING, Any, Literal, ParamSpec, TypedDict, TypeVar\n\nfrom requests_ratelimiter import LimiterMixin\nfrom typing_extensions import NotRequired, Unpack\n\nfrom beets import config, logging\n\nfrom .requests import RequestHandler, TimeoutAndRetrySession\n\nif TYPE_CHECKING:\n    from collections.abc import Callable\n\n    from requests import Response\n\n    from beets.metadata_plugins import IDResponse\n\n    from .._typing import JSONDict\n\nlog = logging.getLogger(\"beets\")\n\n\nLUCENE_SPECIAL_CHAR_PAT = re.compile(r'([-+&|!(){}[\\]^\"~*?:\\\\/])')\n\nRELEASE_INCLUDES = [\n    \"artists\",\n    \"media\",\n    \"recordings\",\n    \"release-groups\",\n    \"labels\",\n    \"artist-credits\",\n    \"aliases\",\n    \"recording-level-rels\",\n    \"work-rels\",\n    \"work-level-rels\",\n    \"artist-rels\",\n    \"isrcs\",\n    \"url-rels\",\n    \"release-rels\",\n    \"genres\",\n    \"tags\",\n]\n\nRECORDING_INCLUDES = [\n    \"artists\",\n    \"aliases\",\n    \"isrcs\",\n    \"work-level-rels\",\n    \"artist-rels\",\n]\n\n\nclass LimiterTimeoutSession(LimiterMixin, TimeoutAndRetrySession):\n    \"\"\"HTTP session that enforces rate limits.\"\"\"\n\n\nEntity = Literal[\n    \"area\",\n    \"artist\",\n    \"collection\",\n    \"event\",\n    \"genre\",\n    \"instrument\",\n    \"label\",\n    \"place\",\n    \"recording\",\n    \"release\",\n    \"release-group\",\n    \"series\",\n    \"work\",\n    \"url\",\n]\n\n\nclass LookupKwargs(TypedDict, total=False):\n    includes: NotRequired[list[str]]\n\n\nclass PagingKwargs(TypedDict, total=False):\n    limit: NotRequired[int]\n    offset: NotRequired[int]\n\n\nclass SearchKwargs(PagingKwargs):\n    query: NotRequired[str]\n\n\nclass BrowseKwargs(LookupKwargs, PagingKwargs, total=False):\n    pass\n\n\nclass BrowseReleaseGroupsKwargs(BrowseKwargs, total=False):\n    artist: NotRequired[str]\n    collection: NotRequired[str]\n    release: NotRequired[str]\n    type: NotRequired[str]\n\n\nclass BrowseRecordingsKwargs(BrowseReleaseGroupsKwargs, total=False):\n    work: NotRequired[str]\n\n\nP = ParamSpec(\"P\")\nR = TypeVar(\"R\")\n\n\ndef require_one_of(*keys: str) -> Callable[[Callable[P, R]], Callable[P, R]]:\n    required = frozenset(keys)\n\n    def deco(func: Callable[P, R]) -> Callable[P, R]:\n        @wraps(func)\n        def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:\n            # kwargs is a real dict at runtime; safe to inspect here\n            if not required & kwargs.keys():\n                required_str = \", \".join(sorted(required))\n                raise ValueError(\n                    f\"At least one of {required_str} filter is required\"\n                )\n            return func(*args, **kwargs)\n\n        return wrapper\n\n    return deco\n\n\n@dataclass\nclass MusicBrainzAPI(RequestHandler):\n    \"\"\"High-level interface to the MusicBrainz WS/2 API.\n\n    Responsibilities:\n\n    - Configure the API host and request rate from application configuration.\n    - Offer helpers to fetch common entity types and to run searches.\n    - Normalize MusicBrainz responses so relation lists are grouped by target\n      type for easier downstream consumption.\n\n    Documentation: https://musicbrainz.org/doc/MusicBrainz_API\n    \"\"\"\n\n    api_host: str = field(init=False)\n    rate_limit: float = field(init=False)\n\n    def __post_init__(self) -> None:\n        mb_config = config[\"musicbrainz\"]\n        mb_config.add(\n            {\n                \"host\": \"musicbrainz.org\",\n                \"https\": False,\n                \"ratelimit\": 1,\n                \"ratelimit_interval\": 1,\n            }\n        )\n\n        hostname = mb_config[\"host\"].as_str()\n        if hostname == \"musicbrainz.org\":\n            self.api_host, self.rate_limit = \"https://musicbrainz.org\", 1.0\n        else:\n            https = mb_config[\"https\"].get(bool)\n            self.api_host = f\"http{'s' if https else ''}://{hostname}\"\n            self.rate_limit = (\n                mb_config[\"ratelimit\"].get(int)\n                / mb_config[\"ratelimit_interval\"].as_number()\n            )\n\n    @cached_property\n    def api_root(self) -> str:\n        return f\"{self.api_host}/ws/2\"\n\n    def create_session(self) -> LimiterTimeoutSession:\n        return LimiterTimeoutSession(per_second=self.rate_limit)\n\n    def request(self, *args, **kwargs) -> Response:\n        \"\"\"Ensure all requests specify JSON response format by default.\"\"\"\n        kwargs.setdefault(\"params\", {})\n        kwargs[\"params\"][\"fmt\"] = \"json\"\n        return super().request(*args, **kwargs)\n\n    def _get_resource(\n        self, resource: str, includes: list[str] | None = None, **kwargs\n    ) -> JSONDict:\n        \"\"\"Retrieve and normalize data from the API resource endpoint.\n\n        If requested, includes are appended to the request. The response is\n        passed through a normalizer that groups relation entries by their\n        target type so that callers receive a consistently structured mapping.\n        \"\"\"\n        if includes:\n            kwargs[\"inc\"] = \"+\".join(includes)\n\n        return self._group_relations(\n            self.get_json(f\"{self.api_root}/{resource}\", params=kwargs)\n        )\n\n    def _lookup(\n        self, entity: Entity, id_: str, **kwargs: Unpack[LookupKwargs]\n    ) -> JSONDict:\n        return self._get_resource(f\"{entity}/{id_}\", **kwargs)\n\n    def _browse(self, entity: Entity, **kwargs) -> list[JSONDict]:\n        return self._get_resource(entity, **kwargs).get(f\"{entity}s\", [])\n\n    @staticmethod\n    def format_search_term(field: str, term: str) -> str:\n        \"\"\"Format a search term for the MusicBrainz API.\n\n        See https://lucene.apache.org/core/4_3_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html\n        \"\"\"\n        if not (term := term.lower().strip()):\n            return \"\"\n\n        term = LUCENE_SPECIAL_CHAR_PAT.sub(r\"\\\\\\1\", term)\n        if field:\n            term = f\"{field}:({term})\"\n\n        return term\n\n    def search(\n        self,\n        entity: Entity,\n        filters: dict[str, str],\n        **kwargs: Unpack[SearchKwargs],\n    ) -> list[IDResponse]:\n        \"\"\"Search for MusicBrainz entities matching the given filters.\n\n        * Query is constructed by combining the provided filters using AND logic\n        * Each filter key-value pair is formatted as 'key:\"value\"' unless\n          - 'key' is empty, in which case only the value is used, '\"value\"'\n          - 'value' is empty, in which case the filter is ignored\n        * Values are lowercased and stripped of whitespace.\n        \"\"\"\n        query = \" \".join(\n            filter(None, starmap(self.format_search_term, filters.items()))\n        )\n        log.debug(\"Searching for MusicBrainz {}s with: {!r}\", entity, query)\n        kwargs[\"query\"] = query\n        return self._get_resource(entity, **kwargs)[f\"{entity}s\"]\n\n    def get_release(self, id_: str, **kwargs: Unpack[LookupKwargs]) -> JSONDict:\n        \"\"\"Retrieve a release by its MusicBrainz ID.\"\"\"\n        kwargs.setdefault(\"includes\", RELEASE_INCLUDES)\n        return self._lookup(\"release\", id_, **kwargs)\n\n    def get_recording(\n        self, id_: str, **kwargs: Unpack[LookupKwargs]\n    ) -> JSONDict:\n        \"\"\"Retrieve a recording by its MusicBrainz ID.\"\"\"\n        kwargs.setdefault(\"includes\", RECORDING_INCLUDES)\n        return self._lookup(\"recording\", id_, **kwargs)\n\n    def get_work(self, id_: str, **kwargs: Unpack[LookupKwargs]) -> JSONDict:\n        \"\"\"Retrieve a work by its MusicBrainz ID.\"\"\"\n        return self._lookup(\"work\", id_, **kwargs)\n\n    @require_one_of(\"artist\", \"collection\", \"release\", \"work\")\n    def browse_recordings(\n        self, **kwargs: Unpack[BrowseRecordingsKwargs]\n    ) -> list[JSONDict]:\n        \"\"\"Browse recordings related to the given entities.\n\n        At least one of artist, collection, release, or work must be provided.\n        \"\"\"\n        return self._browse(\"recording\", **kwargs)\n\n    @require_one_of(\"artist\", \"collection\", \"release\")\n    def browse_release_groups(\n        self, **kwargs: Unpack[BrowseReleaseGroupsKwargs]\n    ) -> list[JSONDict]:\n        \"\"\"Browse release groups related to the given entities.\n\n        At least one of artist, collection, or release must be provided.\n        Optionally filter by type (e.g., \"album|ep\").\n        \"\"\"\n        return self._browse(\"release-group\", **kwargs)\n\n    @singledispatchmethod\n    @classmethod\n    def _group_relations(cls, data: Any) -> Any:\n        \"\"\"Normalize MusicBrainz 'relations' into type-keyed fields recursively.\n\n        This helper rewrites payloads that use a generic 'relations' list into\n        a structure that is easier to consume downstream. When a mapping\n        contains 'relations', those entries are regrouped by their 'target-type'\n        and stored under keys like '<target-type>-relations'. The original\n        'relations' key is removed to avoid ambiguous access patterns.\n\n        The transformation is applied recursively so that nested objects and\n        sequences are normalized consistently, while non-container values are\n        left unchanged.\n        \"\"\"\n        return data\n\n    @_group_relations.register(list)\n    @classmethod\n    def _(cls, data: list[Any]) -> list[Any]:\n        return [cls._group_relations(i) for i in data]\n\n    @_group_relations.register(dict)\n    @classmethod\n    def _(cls, data: JSONDict) -> JSONDict:\n        for k, v in list(data.items()):\n            if k == \"relations\":\n                get_target_type = operator.methodcaller(\"get\", \"target-type\")\n                for target_type, group in groupby(\n                    sorted(v, key=get_target_type), get_target_type\n                ):\n                    relations = [\n                        {k: v for k, v in item.items() if k != \"target-type\"}\n                        for item in group\n                    ]\n                    data[f\"{target_type}-relations\"] = cls._group_relations(\n                        relations\n                    )\n                data.pop(\"relations\")\n            else:\n                data[k] = cls._group_relations(v)\n        return data\n\n\nclass MusicBrainzAPIMixin:\n    \"\"\"Mixin that provides a cached MusicBrainzAPI helper instance.\"\"\"\n\n    @cached_property\n    def mb_api(self) -> MusicBrainzAPI:\n        return MusicBrainzAPI()\n"
  },
  {
    "path": "beetsplug/_utils/requests.py",
    "content": "from __future__ import annotations\n\nimport atexit\nimport threading\nfrom contextlib import contextmanager\nfrom functools import cached_property\nfrom http import HTTPStatus\nfrom typing import TYPE_CHECKING, Any, ClassVar, Generic, Protocol, TypeVar\n\nimport requests\nfrom requests.adapters import HTTPAdapter\nfrom urllib3.util.retry import Retry\n\nfrom beets import __version__\n\nif TYPE_CHECKING:\n    from collections.abc import Iterator\n\n\nclass BeetsHTTPError(requests.exceptions.HTTPError):\n    STATUS: ClassVar[HTTPStatus]\n\n    def __init__(self, *args, **kwargs) -> None:\n        super().__init__(\n            f\"HTTP Error: {self.STATUS.value} {self.STATUS.phrase}\",\n            *args,\n            **kwargs,\n        )\n\n\nclass HTTPNotFoundError(BeetsHTTPError):\n    STATUS = HTTPStatus.NOT_FOUND\n\n\nclass Closeable(Protocol):\n    \"\"\"Protocol for objects that have a close method.\"\"\"\n\n    def close(self) -> None: ...\n\n\nC = TypeVar(\"C\", bound=Closeable)\n\n\nclass SingletonMeta(type, Generic[C]):\n    \"\"\"Metaclass ensuring a single shared instance per class.\n\n    Creates one instance per class type on first instantiation, reusing it\n    for all subsequent calls. Automatically registers cleanup on program exit\n    for proper resource management.\n    \"\"\"\n\n    _instances: ClassVar[dict[type[Any], Any]] = {}\n    _lock: ClassVar[threading.Lock] = threading.Lock()\n\n    def __call__(cls, *args: Any, **kwargs: Any) -> C:\n        if cls not in cls._instances:\n            with cls._lock:\n                if cls not in SingletonMeta._instances:\n                    instance = super().__call__(*args, **kwargs)\n                    SingletonMeta._instances[cls] = instance\n                    atexit.register(instance.close)\n        return SingletonMeta._instances[cls]\n\n\nclass TimeoutAndRetrySession(requests.Session, metaclass=SingletonMeta):\n    \"\"\"HTTP session with sensible defaults.\n\n    * default beets User-Agent header\n    * default request timeout\n    * automatic retries on transient connection or server errors\n    * raises exceptions for HTTP error status codes\n    \"\"\"\n\n    def __init__(self, *args, **kwargs) -> None:\n        super().__init__(*args, **kwargs)\n        self.headers[\"User-Agent\"] = f\"beets/{__version__} https://beets.io/\"\n\n        retry = Retry(\n            total=6,\n            backoff_factor=0.5,\n            # Retry on server errors\n            status_forcelist=[\n                HTTPStatus.INTERNAL_SERVER_ERROR,\n                HTTPStatus.BAD_GATEWAY,\n                HTTPStatus.SERVICE_UNAVAILABLE,\n                HTTPStatus.GATEWAY_TIMEOUT,\n            ],\n        )\n        adapter = HTTPAdapter(max_retries=retry)\n        self.mount(\"https://\", adapter)\n        self.mount(\"http://\", adapter)\n\n    def request(self, *args, **kwargs):\n        \"\"\"Execute HTTP request with automatic timeout and status validation.\n\n        Ensures all requests have a timeout (defaults to 10 seconds) and raises\n        an exception for HTTP error status codes.\n        \"\"\"\n        kwargs.setdefault(\"timeout\", 10)\n        r = super().request(*args, **kwargs)\n        r.raise_for_status()\n\n        return r\n\n\nclass RequestHandler:\n    \"\"\"Manages HTTP requests with custom error handling and session management.\n\n    Provides a reusable interface for making HTTP requests with automatic\n    conversion of standard HTTP errors to beets-specific exceptions. Supports\n    custom session types and error mappings that can be overridden by\n    subclasses.\n\n    Usage:\n        Subclass and override :class:`RequestHandler.create_session`,\n        :class:`RequestHandler.explicit_http_errors` or\n        :class:`RequestHandler.status_to_error()` to customize behavior.\n\n    Use\n\n    - :class:`RequestHandler.get_json()` to get JSON response data\n    - :class:`RequestHandler.get()` to get HTTP response object\n    - :class:`RequestHandler.request()` to invoke arbitrary HTTP methods\n\n    Feel free to define common methods that are used in multiple plugins.\n    \"\"\"\n\n    #: List of custom exceptions to be raised for specific status codes.\n    explicit_http_errors: ClassVar[list[type[BeetsHTTPError]]] = [\n        HTTPNotFoundError\n    ]\n\n    def create_session(self) -> TimeoutAndRetrySession:\n        \"\"\"Create a new HTTP session instance.\n\n        Can be overridden by subclasses to provide custom session types.\n        \"\"\"\n        return TimeoutAndRetrySession()\n\n    @cached_property\n    def session(self) -> TimeoutAndRetrySession:\n        return self.create_session()\n\n    def status_to_error(\n        self, code: int\n    ) -> type[requests.exceptions.HTTPError] | None:\n        \"\"\"Map HTTP status codes to beets-specific exception types.\n\n        Searches the configured explicit HTTP errors for a matching status code.\n        Returns None if no specific error type is registered for the given code.\n        \"\"\"\n        return next(\n            (e for e in self.explicit_http_errors if e.STATUS == code), None\n        )\n\n    @contextmanager\n    def handle_http_error(self) -> Iterator[None]:\n        \"\"\"Convert standard HTTP errors to beets-specific exceptions.\n\n        Wraps operations that may raise HTTPError, automatically translating\n        recognized status codes into their corresponding beets exception types.\n        Unrecognized errors are re-raised unchanged.\n        \"\"\"\n        try:\n            yield\n        except requests.exceptions.HTTPError as e:\n            if beets_error := self.status_to_error(e.response.status_code):\n                raise beets_error(response=e.response) from e\n\n            raise\n\n    def request(self, *args, **kwargs) -> requests.Response:\n        \"\"\"Perform HTTP request using the session with automatic error handling.\n\n        Delegates to the underlying session method while converting recognized\n        HTTP errors to beets-specific exceptions through the error handler.\n        \"\"\"\n        with self.handle_http_error():\n            return self.session.request(*args, **kwargs)\n\n    def get(self, *args, **kwargs) -> requests.Response:\n        \"\"\"Perform HTTP GET request with automatic error handling.\"\"\"\n        return self.request(\"get\", *args, **kwargs)\n\n    def put(self, *args, **kwargs) -> requests.Response:\n        \"\"\"Perform HTTP PUT request with automatic error handling.\"\"\"\n        return self.request(\"put\", *args, **kwargs)\n\n    def delete(self, *args, **kwargs) -> requests.Response:\n        \"\"\"Perform HTTP DELETE request with automatic error handling.\"\"\"\n        return self.request(\"delete\", *args, **kwargs)\n\n    def get_json(self, *args, **kwargs):\n        \"\"\"Fetch and parse JSON data from an HTTP endpoint.\"\"\"\n        return self.get(*args, **kwargs).json()\n"
  },
  {
    "path": "beetsplug/_utils/vfs.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"A simple utility for constructing filesystem-like trees from beets\nlibraries.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING, NamedTuple\n\nfrom beets import util\n\nif TYPE_CHECKING:\n    from beets.library import Library\n\n\nclass Node(NamedTuple):\n    files: dict[str, int]\n    # Maps filenames to Item ids.\n\n    dirs: dict[str, Node]\n    # Maps directory names to child nodes.\n\n\ndef _insert(node: Node, path: list[str], itemid: int):\n    \"\"\"Insert an item into a virtual filesystem node.\"\"\"\n    if len(path) == 1:\n        # Last component. Insert file.\n        node.files[path[0]] = itemid\n    else:\n        # In a directory.\n        dirname = path[0]\n        rest = path[1:]\n        if dirname not in node.dirs:\n            node.dirs[dirname] = Node({}, {})\n        _insert(node.dirs[dirname], rest, itemid)\n\n\ndef libtree(lib: Library) -> Node:\n    \"\"\"Generates a filesystem-like directory tree for the files\n    contained in `lib`. Filesystem nodes are (files, dirs) named\n    tuples in which both components are dictionaries. The first\n    maps filenames to Item ids. The second maps directory names to\n    child node tuples.\n    \"\"\"\n    root = Node({}, {})\n    for item in lib.items():\n        dest = item.destination(relative_to_libdir=True)\n        parts = util.components(util.as_string(dest))\n        _insert(root, parts, item.id)\n    return root\n"
  },
  {
    "path": "beetsplug/absubmit.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Pieter Mulder.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Calculate acoustic information and submit to AcousticBrainz.\"\"\"\n\nimport errno\nimport hashlib\nimport json\nimport os\nimport shutil\nimport subprocess\nimport tempfile\n\nimport requests\n\nfrom beets import plugins, ui, util\n\n# We use this field to check whether AcousticBrainz info is present.\nPROBE_FIELD = \"mood_acoustic\"\n\n\nclass ABSubmitError(Exception):\n    \"\"\"Raised when failing to analyse file with extractor.\"\"\"\n\n\ndef call(args):\n    \"\"\"Execute the command and return its output.\n\n    Raise a AnalysisABSubmitError on failure.\n    \"\"\"\n    try:\n        return util.command_output(args).stdout\n    except subprocess.CalledProcessError as e:\n        raise ABSubmitError(f\"{args[0]} exited with status {e.returncode}\")\n\n\nclass AcousticBrainzSubmitPlugin(plugins.BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n\n        self._log.warning(\"This plugin is deprecated.\")\n\n        self.config.add(\n            {\"extractor\": \"\", \"force\": False, \"pretend\": False, \"base_url\": \"\"}\n        )\n\n        self.extractor = self.config[\"extractor\"].as_str()\n        if self.extractor:\n            self.extractor = util.normpath(self.extractor)\n            # Explicit path to extractor\n            if not os.path.isfile(self.extractor):\n                raise ui.UserError(\n                    f\"Extractor command does not exist: {self.extractor}.\"\n                )\n        else:\n            # Implicit path to extractor, search for it in path\n            self.extractor = \"streaming_extractor_music\"\n            try:\n                call([self.extractor])\n            except OSError:\n                raise ui.UserError(\n                    \"No extractor command found: please install the extractor\"\n                    \" binary from https://essentia.upf.edu/\"\n                )\n            except ABSubmitError:\n                # Extractor found, will exit with an error if not called with\n                # the correct amount of arguments.\n                pass\n\n            # Get the executable location on the system, which we need\n            # to calculate the SHA-1 hash.\n            self.extractor = shutil.which(self.extractor)\n\n        # Calculate extractor hash.\n        self.extractor_sha = hashlib.sha1()\n        with open(self.extractor, \"rb\") as extractor:\n            self.extractor_sha.update(extractor.read())\n        self.extractor_sha = self.extractor_sha.hexdigest()\n\n        self.url = \"\"\n        base_url = self.config[\"base_url\"].as_str()\n        if base_url:\n            if not base_url.startswith(\"http\"):\n                raise ui.UserError(\n                    \"AcousticBrainz server base URL must start \"\n                    \"with an HTTP scheme\"\n                )\n            elif base_url[-1] != \"/\":\n                base_url = f\"{base_url}/\"\n            self.url = f\"{base_url}{{mbid}}/low-level\"\n\n    def commands(self):\n        cmd = ui.Subcommand(\n            \"absubmit\", help=\"calculate and submit AcousticBrainz analysis\"\n        )\n        cmd.parser.add_option(\n            \"-f\",\n            \"--force\",\n            dest=\"force_refetch\",\n            action=\"store_true\",\n            default=False,\n            help=\"re-download data when already present\",\n        )\n        cmd.parser.add_option(\n            \"-p\",\n            \"--pretend\",\n            dest=\"pretend_fetch\",\n            action=\"store_true\",\n            default=False,\n            help=(\n                \"pretend to perform action, but show only files which would be\"\n                \" processed\"\n            ),\n        )\n        cmd.func = self.command\n        return [cmd]\n\n    def command(self, lib, opts, args):\n        if not self.url:\n            raise ui.UserError(\n                \"This plugin is deprecated since AcousticBrainz no longer \"\n                \"accepts new submissions. See the base_url configuration \"\n                \"option.\"\n            )\n        else:\n            # Get items from arguments\n            items = lib.items(args)\n            self.opts = opts\n            util.par_map(self.analyze_submit, items)\n\n    def analyze_submit(self, item):\n        analysis = self._get_analysis(item)\n        if analysis:\n            self._submit_data(item, analysis)\n\n    def _get_analysis(self, item):\n        mbid = item[\"mb_trackid\"]\n\n        # Avoid re-analyzing files that already have AB data.\n        if not self.opts.force_refetch and not self.config[\"force\"]:\n            if item.get(PROBE_FIELD):\n                return None\n\n        # If file has no MBID, skip it.\n        if not mbid:\n            self._log.info(\n                \"Not analysing {}, missing musicbrainz track id.\", item\n            )\n            return None\n\n        if self.opts.pretend_fetch or self.config[\"pretend\"]:\n            self._log.info(\"pretend action - extract item: {}\", item)\n            return None\n\n        # Temporary file to save extractor output to, extractor only works\n        # if an output file is given. Here we use a temporary file to copy\n        # the data into a python object and then remove the file from the\n        # system.\n        tmp_file, filename = tempfile.mkstemp(suffix=\".json\")\n        try:\n            # Close the file, so the extractor can overwrite it.\n            os.close(tmp_file)\n            try:\n                call([self.extractor, util.syspath(item.path), filename])\n            except ABSubmitError as e:\n                self._log.warning(\n                    \"Failed to analyse {item} for AcousticBrainz: {error}\",\n                    item=item,\n                    error=e,\n                )\n                return None\n            with open(filename) as tmp_file:\n                analysis = json.load(tmp_file)\n            # Add the hash to the output.\n            analysis[\"metadata\"][\"version\"][\"essentia_build_sha\"] = (\n                self.extractor_sha\n            )\n            return analysis\n        finally:\n            try:\n                os.remove(filename)\n            except OSError as e:\n                # ENOENT means file does not exist, just ignore this error.\n                if e.errno != errno.ENOENT:\n                    raise\n\n    def _submit_data(self, item, data):\n        mbid = item[\"mb_trackid\"]\n        headers = {\"Content-Type\": \"application/json\"}\n        response = requests.post(\n            self.url.format(mbid=mbid),\n            json=data,\n            headers=headers,\n            timeout=10,\n        )\n        # Test that request was successful and raise an error on failure.\n        if response.status_code != 200:\n            try:\n                message = response.json()[\"message\"]\n            except (ValueError, KeyError) as e:\n                message = f\"unable to get error message: {e}\"\n            self._log.error(\n                \"Failed to submit AcousticBrainz analysis of {item}: \"\n                \"{message}).\",\n                item=item,\n                message=message,\n            )\n        else:\n            self._log.debug(\n                \"Successfully submitted AcousticBrainz analysis for {}.\",\n                item,\n            )\n"
  },
  {
    "path": "beetsplug/acousticbrainz.py",
    "content": "# This file is part of beets.\n# Copyright 2015-2016, Ohm Patel.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Fetch various AcousticBrainz metadata using MBID.\"\"\"\n\nfrom collections import defaultdict\nfrom typing import ClassVar\n\nimport requests\n\nfrom beets import plugins, ui\nfrom beets.dbcore import types\n\nLEVELS = [\"/low-level\", \"/high-level\"]\nABSCHEME = {\n    \"highlevel\": {\n        \"danceability\": {\"all\": {\"danceable\": \"danceable\"}},\n        \"gender\": {\"value\": \"gender\"},\n        \"genre_rosamerica\": {\"value\": \"genre_rosamerica\"},\n        \"mood_acoustic\": {\"all\": {\"acoustic\": \"mood_acoustic\"}},\n        \"mood_aggressive\": {\"all\": {\"aggressive\": \"mood_aggressive\"}},\n        \"mood_electronic\": {\"all\": {\"electronic\": \"mood_electronic\"}},\n        \"mood_happy\": {\"all\": {\"happy\": \"mood_happy\"}},\n        \"mood_party\": {\"all\": {\"party\": \"mood_party\"}},\n        \"mood_relaxed\": {\"all\": {\"relaxed\": \"mood_relaxed\"}},\n        \"mood_sad\": {\"all\": {\"sad\": \"mood_sad\"}},\n        \"moods_mirex\": {\"value\": \"moods_mirex\"},\n        \"ismir04_rhythm\": {\"value\": \"rhythm\"},\n        \"tonal_atonal\": {\"all\": {\"tonal\": \"tonal\"}},\n        \"timbre\": {\"value\": \"timbre\"},\n        \"voice_instrumental\": {\"value\": \"voice_instrumental\"},\n    },\n    \"lowlevel\": {\"average_loudness\": \"average_loudness\"},\n    \"rhythm\": {\"bpm\": \"bpm\"},\n    \"tonal\": {\n        \"chords_changes_rate\": \"chords_changes_rate\",\n        \"chords_key\": \"chords_key\",\n        \"chords_number_rate\": \"chords_number_rate\",\n        \"chords_scale\": \"chords_scale\",\n        \"key_key\": (\"initial_key\", 0),\n        \"key_scale\": (\"initial_key\", 1),\n        \"key_strength\": \"key_strength\",\n    },\n}\n\n\nclass AcousticPlugin(plugins.BeetsPlugin):\n    item_types: ClassVar[dict[str, types.Type]] = {\n        \"average_loudness\": types.Float(6),\n        \"chords_changes_rate\": types.Float(6),\n        \"chords_key\": types.STRING,\n        \"chords_number_rate\": types.Float(6),\n        \"chords_scale\": types.STRING,\n        \"danceable\": types.Float(6),\n        \"gender\": types.STRING,\n        \"genre_rosamerica\": types.STRING,\n        \"initial_key\": types.STRING,\n        \"key_strength\": types.Float(6),\n        \"mood_acoustic\": types.Float(6),\n        \"mood_aggressive\": types.Float(6),\n        \"mood_electronic\": types.Float(6),\n        \"mood_happy\": types.Float(6),\n        \"mood_party\": types.Float(6),\n        \"mood_relaxed\": types.Float(6),\n        \"mood_sad\": types.Float(6),\n        \"moods_mirex\": types.STRING,\n        \"rhythm\": types.Float(6),\n        \"timbre\": types.STRING,\n        \"tonal\": types.Float(6),\n        \"voice_instrumental\": types.STRING,\n    }\n\n    def __init__(self):\n        super().__init__()\n\n        self._log.warning(\"This plugin is deprecated.\")\n\n        self.config.add(\n            {\"auto\": True, \"force\": False, \"tags\": [], \"base_url\": \"\"}\n        )\n\n        self.base_url = self.config[\"base_url\"].as_str()\n        if self.base_url:\n            if not self.base_url.startswith(\"http\"):\n                raise ui.UserError(\n                    \"AcousticBrainz server base URL must start \"\n                    \"with an HTTP scheme\"\n                )\n            elif self.base_url[-1] != \"/\":\n                self.base_url = f\"{self.base_url}/\"\n\n        if self.config[\"auto\"]:\n            self.register_listener(\"import_task_files\", self.import_task_files)\n\n    def commands(self):\n        cmd = ui.Subcommand(\n            \"acousticbrainz\", help=\"fetch metadata from AcousticBrainz\"\n        )\n        cmd.parser.add_option(\n            \"-f\",\n            \"--force\",\n            dest=\"force_refetch\",\n            action=\"store_true\",\n            default=False,\n            help=\"re-download data when already present\",\n        )\n\n        def func(lib, opts, args):\n            items = lib.items(args)\n            self._fetch_info(\n                items,\n                ui.should_write(),\n                opts.force_refetch or self.config[\"force\"],\n            )\n\n        cmd.func = func\n        return [cmd]\n\n    def import_task_files(self, session, task):\n        \"\"\"Function is called upon beet import.\"\"\"\n        self._fetch_info(task.imported_items(), False, True)\n\n    def _get_data(self, mbid):\n        if not self.base_url:\n            raise ui.UserError(\n                \"This plugin is deprecated since AcousticBrainz has shut \"\n                \"down. See the base_url configuration option.\"\n            )\n        data = {}\n        for url in _generate_urls(self.base_url, mbid):\n            self._log.debug(\"fetching URL: {}\", url)\n\n            try:\n                res = requests.get(url, timeout=10)\n            except requests.RequestException as exc:\n                self._log.info(\"request error: {}\", exc)\n                return {}\n\n            if res.status_code == 404:\n                self._log.info(\"recording ID {} not found\", mbid)\n                return {}\n\n            try:\n                data.update(res.json())\n            except ValueError:\n                self._log.debug(\"Invalid Response: {.text}\", res)\n                return {}\n\n        return data\n\n    def _fetch_info(self, items, write, force):\n        \"\"\"Fetch additional information from AcousticBrainz for the `item`s.\"\"\"\n        tags = self.config[\"tags\"].as_str_seq()\n        for item in items:\n            # If we're not forcing re-downloading for all tracks, check\n            # whether the data is already present. We use one\n            # representative field name to check for previously fetched\n            # data.\n            if not force:\n                mood_str = item.get(\"mood_acoustic\", \"\")\n                if mood_str:\n                    self._log.info(\"data already present for: {}\", item)\n                    continue\n\n            # We can only fetch data for tracks with MBIDs.\n            if not item.mb_trackid:\n                continue\n\n            self._log.info(\"getting data for: {}\", item)\n            data = self._get_data(item.mb_trackid)\n            if data:\n                for attr, val in self._map_data_to_scheme(data, ABSCHEME):\n                    if not tags or attr in tags:\n                        self._log.debug(\n                            \"attribute {} of {} set to {}\", attr, item, val\n                        )\n                        setattr(item, attr, val)\n                    else:\n                        self._log.debug(\n                            \"skipping attribute {} of {}\"\n                            \" (value {}) due to config\",\n                            attr,\n                            item,\n                            val,\n                        )\n                item.store()\n                if write:\n                    item.try_write()\n\n    def _map_data_to_scheme(self, data, scheme):\n        \"\"\"Given `data` as a structure of nested dictionaries, and\n        `scheme` as a structure of nested dictionaries , `yield` tuples\n        `(attr, val)` where `attr` and `val` are corresponding leaf\n        nodes in `scheme` and `data`.\n\n        As its name indicates, `scheme` defines how the data is structured,\n        so this function tries to find leaf nodes in `data` that correspond\n        to the leafs nodes of `scheme`, and not the other way around.\n        Leaf nodes of `data` that do not exist in the `scheme` do not matter.\n        If a leaf node of `scheme` is not present in `data`,\n        no value is yielded for that attribute and a simple warning is issued.\n\n        Finally, to account for attributes of which the value is split between\n        several leaf nodes in `data`, leaf nodes of `scheme` can be tuples\n        `(attr, order)` where `attr` is the attribute to which the leaf node\n        belongs, and `order` is the place at which it should appear in the\n        value. The different `value`s belonging to the same `attr` are simply\n        joined with `' '`. This is hardcoded and not very flexible, but it gets\n        the job done.\n\n        For example:\n\n        >>> scheme = {\n            'key1': 'attribute',\n            'key group': {\n                'subkey1': 'subattribute',\n                'subkey2': ('composite attribute', 0)\n            },\n            'key2': ('composite attribute', 1)\n        }\n        >>> data = {\n            'key1': 'value',\n            'key group': {\n                'subkey1': 'subvalue',\n                'subkey2': 'part 1 of composite attr'\n            },\n            'key2': 'part 2'\n        }\n        >>> print(list(_map_data_to_scheme(data, scheme)))\n        [('subattribute', 'subvalue'),\n         ('attribute', 'value'),\n         ('composite attribute', 'part 1 of composite attr part 2')]\n        \"\"\"\n        # First, we traverse `scheme` and `data`, `yield`ing all the non\n        # composites attributes straight away and populating the dictionary\n        # `composites` with the composite attributes.\n\n        # When we are finished traversing `scheme`, `composites` should\n        # map each composite attribute to an ordered list of the values\n        # belonging to the attribute, for example:\n        # `composites = {'initial_key': ['B', 'minor']}`.\n\n        # The recursive traversal.\n        composites = defaultdict(list)\n        yield from self._data_to_scheme_child(data, scheme, composites)\n\n        # When composites has been populated, yield the composite attributes\n        # by joining their parts.\n        for composite_attr, value_parts in composites.items():\n            yield composite_attr, \" \".join(value_parts)\n\n    def _data_to_scheme_child(self, subdata, subscheme, composites):\n        \"\"\"The recursive business logic of :meth:`_map_data_to_scheme`:\n        Traverse two structures of nested dictionaries in parallel and `yield`\n        tuples of corresponding leaf nodes.\n\n        If a leaf node belongs to a composite attribute (is a `tuple`),\n        populate `composites` rather than yielding straight away.\n        All the child functions for a single traversal share the same\n        `composites` instance, which is passed along.\n        \"\"\"\n        for k, v in subscheme.items():\n            if k in subdata:\n                if isinstance(v, dict):\n                    yield from self._data_to_scheme_child(\n                        subdata[k], v, composites\n                    )\n                elif isinstance(v, tuple):\n                    composite_attribute, part_number = v\n                    attribute_parts = composites[composite_attribute]\n                    # Parts are not guaranteed to be inserted in order\n                    while len(attribute_parts) <= part_number:\n                        attribute_parts.append(\"\")\n                    attribute_parts[part_number] = subdata[k]\n                else:\n                    yield v, subdata[k]\n            else:\n                self._log.warning(\n                    \"Acousticbrainz did not provide info about {}\", k\n                )\n                self._log.debug(\n                    \"Data {} could not be mapped to scheme {} \"\n                    \"because key {} was not found\",\n                    subdata,\n                    v,\n                    k,\n                )\n\n\ndef _generate_urls(base_url, mbid):\n    \"\"\"Generates AcousticBrainz end point urls for given `mbid`.\"\"\"\n    for level in LEVELS:\n        yield f\"{base_url}{mbid}{level}\"\n"
  },
  {
    "path": "beetsplug/advancedrewrite.py",
    "content": "# This file is part of beets.\n# Copyright 2023, Max Rumpf.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Plugin to rewrite fields based on a given query.\"\"\"\n\nimport re\nimport shlex\nfrom collections import defaultdict\n\nimport confuse\n\nfrom beets.dbcore import AndQuery, query_from_strings\nfrom beets.dbcore.types import MULTI_VALUE_DSV\nfrom beets.library import Album, Item\nfrom beets.plugins import BeetsPlugin\nfrom beets.ui import UserError\n\n\ndef rewriter(field, simple_rules, advanced_rules):\n    \"\"\"Template field function factory.\n\n    Create a template field function that rewrites the given field\n    with the given rewriting rules.\n    ``simple_rules`` must be a list of (pattern, replacement) pairs.\n    ``advanced_rules`` must be a list of (query, replacement) pairs.\n    \"\"\"\n\n    def fieldfunc(item):\n        value = item._values_fixed[field]\n        for pattern, replacement in simple_rules:\n            if pattern.match(value.lower()):\n                # Rewrite activated.\n                return replacement\n        for query, replacement in advanced_rules:\n            if query.match(item):\n                # Rewrite activated.\n                return replacement\n        # Not activated; return original value.\n        return value\n\n    return fieldfunc\n\n\nclass AdvancedRewritePlugin(BeetsPlugin):\n    \"\"\"Plugin to rewrite fields based on a given query.\"\"\"\n\n    def __init__(self):\n        \"\"\"Parse configuration and register template fields for rewriting.\"\"\"\n        super().__init__()\n        self.register_listener(\"pluginload\", self.loaded)\n\n    def loaded(self):\n        template = confuse.Sequence(\n            confuse.OneOf(\n                [\n                    confuse.MappingValues(str),\n                    {\n                        \"match\": str,\n                        \"replacements\": confuse.MappingValues(\n                            confuse.OneOf([str, confuse.Sequence(str)]),\n                        ),\n                    },\n                ]\n            )\n        )\n\n        # Used to apply the same rewrite to the corresponding album field.\n        corresponding_album_fields = {\n            \"artist\": \"albumartist\",\n            \"artists\": \"albumartists\",\n            \"artist_sort\": \"albumartist_sort\",\n            \"artists_sort\": \"albumartists_sort\",\n        }\n\n        # Gather all the rewrite rules for each field.\n        class RulesContainer:\n            def __init__(self):\n                self.simple = []\n                self.advanced = []\n\n        rules = defaultdict(RulesContainer)\n        for rule in self.config.get(template):\n            if \"match\" not in rule:\n                # Simple syntax\n                if len(rule) != 1:\n                    raise UserError(\n                        \"Simple rewrites must have only one rule, \"\n                        \"but found multiple entries. \"\n                        \"Did you forget to prepend a dash (-)?\"\n                    )\n                key, value = next(iter(rule.items()))\n                try:\n                    fieldname, pattern = key.split(None, 1)\n                except ValueError:\n                    raise UserError(\n                        f\"Invalid simple rewrite specification {key}\"\n                    )\n                if fieldname not in Item._fields:\n                    raise UserError(\n                        f\"invalid field name {fieldname} in rewriter\"\n                    )\n                self._log.debug(\n                    f\"adding simple rewrite '{pattern}' → '{value}' \"\n                    f\"for field {fieldname}\"\n                )\n                pattern = re.compile(pattern.lower())\n                rules[fieldname].simple.append((pattern, value))\n\n                # Apply the same rewrite to the corresponding album field.\n                if fieldname in corresponding_album_fields:\n                    album_fieldname = corresponding_album_fields[fieldname]\n                    rules[album_fieldname].simple.append((pattern, value))\n            else:\n                # Advanced syntax\n                match = rule[\"match\"]\n                replacements = rule[\"replacements\"]\n                if len(replacements) == 0:\n                    raise UserError(\n                        \"Advanced rewrites must have at least one replacement\"\n                    )\n                query = query_from_strings(\n                    AndQuery,\n                    Item,\n                    prefixes={},\n                    query_parts=shlex.split(match),\n                )\n                for fieldname, replacement in replacements.items():\n                    if fieldname not in Item._fields:\n                        raise UserError(\n                            f\"Invalid field name {fieldname} in rewriter\"\n                        )\n                    self._log.debug(\n                        f\"adding advanced rewrite to '{replacement}' \"\n                        f\"for field {fieldname}\"\n                    )\n                    if isinstance(replacement, list):\n                        if Item._fields[fieldname] is not MULTI_VALUE_DSV:\n                            raise UserError(\n                                f\"Field {fieldname} is not a multi-valued field \"\n                                f\"but a list was given: {', '.join(replacement)}\"\n                            )\n                    elif isinstance(replacement, str):\n                        if Item._fields[fieldname] is MULTI_VALUE_DSV:\n                            replacement = [replacement]\n                    else:\n                        raise UserError(\n                            f\"Invalid type of replacement {replacement} \"\n                            f\"for field {fieldname}\"\n                        )\n\n                    rules[fieldname].advanced.append((query, replacement))\n\n                    # Apply the same rewrite to the corresponding album field.\n                    if fieldname in corresponding_album_fields:\n                        album_fieldname = corresponding_album_fields[fieldname]\n                        rules[album_fieldname].advanced.append(\n                            (query, replacement)\n                        )\n\n        # Replace each template field with the new rewriter function.\n        for fieldname, fieldrules in rules.items():\n            getter = rewriter(fieldname, fieldrules.simple, fieldrules.advanced)\n            self.template_fields[fieldname] = getter\n            if fieldname in Album._fields:\n                self.album_template_fields[fieldname] = getter\n"
  },
  {
    "path": "beetsplug/albumtypes.py",
    "content": "# This file is part of beets.\n# Copyright 2021, Edgars Supe.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Adds an album template field for formatted album types.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nfrom beets.plugins import BeetsPlugin\n\nfrom .musicbrainz import VARIOUS_ARTISTS_ID\n\nif TYPE_CHECKING:\n    from beets.library import Album\n\n\nclass AlbumTypesPlugin(BeetsPlugin):\n    \"\"\"Adds an album template field for formatted album types.\"\"\"\n\n    def __init__(self):\n        \"\"\"Init AlbumTypesPlugin.\"\"\"\n        super().__init__()\n        self.album_template_fields[\"atypes\"] = self._atypes\n        self.config.add(\n            {\n                \"types\": [\n                    (\"ep\", \"EP\"),\n                    (\"single\", \"Single\"),\n                    (\"soundtrack\", \"OST\"),\n                    (\"live\", \"Live\"),\n                    (\"compilation\", \"Anthology\"),\n                    (\"remix\", \"Remix\"),\n                ],\n                \"ignore_va\": [\"compilation\"],\n                \"bracket\": \"[]\",\n            }\n        )\n\n    def _atypes(self, item: Album):\n        \"\"\"Returns a formatted string based on album's types.\"\"\"\n        types = self.config[\"types\"].as_pairs()\n        ignore_va = self.config[\"ignore_va\"].as_str_seq()\n        bracket = self.config[\"bracket\"].as_str()\n\n        # Assign a left and right bracket or leave blank if argument is empty.\n        if len(bracket) == 2:\n            bracket_l = bracket[0]\n            bracket_r = bracket[1]\n        else:\n            bracket_l = \"\"\n            bracket_r = \"\"\n\n        res = \"\"\n        albumtypes = item.albumtypes\n        is_va = item.mb_albumartistid == VARIOUS_ARTISTS_ID\n        for type in types:\n            if type[0] in albumtypes and type[1]:\n                if not is_va or (type[0] not in ignore_va and is_va):\n                    res += f\"{bracket_l}{type[1]}{bracket_r}\"\n\n        return res\n"
  },
  {
    "path": "beetsplug/aura.py",
    "content": "# This file is part of beets.\n# Copyright 2020, Callum Brown.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"An AURA server using Flask.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport re\nfrom dataclasses import dataclass\nfrom mimetypes import guess_type\nfrom typing import TYPE_CHECKING, ClassVar\n\nfrom flask import (\n    Blueprint,\n    Flask,\n    current_app,\n    make_response,\n    request,\n    send_file,\n)\nfrom typing_extensions import Self\n\nfrom beets import config\nfrom beets.dbcore.query import (\n    AndQuery,\n    FixedFieldSort,\n    MatchQuery,\n    MultipleSort,\n    NotQuery,\n    RegexpQuery,\n    SlowFieldSort,\n)\nfrom beets.library import Album, Item\nfrom beets.plugins import BeetsPlugin\nfrom beets.ui import Subcommand, _open_library\n\nif TYPE_CHECKING:\n    from collections.abc import Mapping\n\n    from beets.dbcore.query import SQLiteType\n    from beets.library import LibModel, Library\n\n# Constants\n\n# AURA server information\n# TODO: Add version information\nSERVER_INFO = {\n    \"aura-version\": \"0\",\n    \"server\": \"beets-aura\",\n    \"server-version\": \"0.1\",\n    \"auth-required\": False,\n    \"features\": [\"albums\", \"artists\", \"images\"],\n}\n\n# Maps AURA Track attribute to beets Item attribute\nTRACK_ATTR_MAP = {\n    # Required\n    \"title\": \"title\",\n    \"artist\": \"artist\",\n    # Optional\n    \"album\": \"album\",\n    \"track\": \"track\",  # Track number on album\n    \"tracktotal\": \"tracktotal\",\n    \"disc\": \"disc\",\n    \"disctotal\": \"disctotal\",\n    \"year\": \"year\",\n    \"month\": \"month\",\n    \"day\": \"day\",\n    \"bpm\": \"bpm\",\n    \"genre\": \"genres\",\n    \"genres\": \"genres\",\n    \"recording-mbid\": \"mb_trackid\",  # beets trackid is MB recording\n    \"track-mbid\": \"mb_releasetrackid\",\n    \"composer\": \"composer\",\n    \"albumartist\": \"albumartist\",\n    \"comments\": \"comments\",\n    # Optional for Audio Metadata\n    # TODO: Support the mimetype attribute, format != mime type\n    # \"mimetype\": track.format,\n    \"duration\": \"length\",\n    \"framerate\": \"samplerate\",\n    # I don't think beets has a framecount field\n    # \"framecount\": ???,\n    \"channels\": \"channels\",\n    \"bitrate\": \"bitrate\",\n    \"bitdepth\": \"bitdepth\",\n    \"size\": \"filesize\",\n}\n\n# Maps AURA Album attribute to beets Album attribute\nALBUM_ATTR_MAP = {\n    # Required\n    \"title\": \"album\",\n    \"artist\": \"albumartist\",\n    # Optional\n    \"tracktotal\": \"albumtotal\",\n    \"disctotal\": \"disctotal\",\n    \"year\": \"year\",\n    \"month\": \"month\",\n    \"day\": \"day\",\n    \"genre\": \"genres\",\n    \"genres\": \"genres\",\n    \"release-mbid\": \"mb_albumid\",\n    \"release-group-mbid\": \"mb_releasegroupid\",\n}\n\n# Maps AURA Artist attribute to beets Item field\n# Artists are not first-class in beets, so information is extracted from\n# beets Items.\nARTIST_ATTR_MAP = {\n    # Required\n    \"name\": \"artist\",\n    # Optional\n    \"artist-mbid\": \"mb_artistid\",\n}\n\n\n@dataclass\nclass AURADocument:\n    \"\"\"Base class for building AURA documents.\"\"\"\n\n    model_cls: ClassVar[type[LibModel]]\n\n    lib: Library\n    args: Mapping[str, str]\n\n    @classmethod\n    def from_app(cls) -> Self:\n        \"\"\"Initialise the document using the global app and request.\"\"\"\n        return cls(current_app.config[\"lib\"], request.args)\n\n    @staticmethod\n    def error(status, title, detail):\n        \"\"\"Make a response for an error following the JSON:API spec.\n\n        Args:\n            status: An HTTP status code string, e.g. \"404 Not Found\".\n            title: A short, human-readable summary of the problem.\n            detail: A human-readable explanation specific to this\n                occurrence of the problem.\n        \"\"\"\n        document = {\n            \"errors\": [{\"status\": status, \"title\": title, \"detail\": detail}]\n        }\n        return make_response(document, status)\n\n    @classmethod\n    def get_attribute_converter(cls, beets_attr: str) -> type[SQLiteType]:\n        \"\"\"Work out what data type an attribute should be for beets.\n\n        Args:\n            beets_attr: The name of the beets attribute, e.g. \"title\".\n        \"\"\"\n        try:\n            # Look for field in list of Album fields\n            # and get python type of database type.\n            # See beets.library.Album and beets.dbcore.types\n            return cls.model_cls._fields[beets_attr].model_type\n        except KeyError:\n            # Fall back to string (NOTE: probably not good)\n            return str\n\n    def translate_filters(self):\n        \"\"\"Translate filters from request arguments to a beets Query.\"\"\"\n        # The format of each filter key in the request parameter is:\n        # filter[<attribute>]. This regex extracts <attribute>.\n        pattern = re.compile(r\"filter\\[(?P<attribute>[a-zA-Z0-9_-]+)\\]\")\n        queries = []\n        for key, value in self.args.items():\n            match = pattern.match(key)\n            if match:\n                # Extract attribute name from key\n                aura_attr = match.group(\"attribute\")\n                # Get the beets version of the attribute name\n                beets_attr = self.attribute_map.get(aura_attr, aura_attr)\n                converter = self.get_attribute_converter(beets_attr)\n                value = converter(value)\n                # Add exact match query to list\n                # Use a slow query so it works with all fields\n                queries.append(\n                    self.model_cls.field_query(beets_attr, value, MatchQuery)\n                )\n        # NOTE: AURA doesn't officially support multiple queries\n        return AndQuery(queries)\n\n    def translate_sorts(self, sort_arg):\n        \"\"\"Translate an AURA sort parameter into a beets Sort.\n\n        Args:\n            sort_arg: The value of the 'sort' query parameter; a comma\n                separated list of fields to sort by, in order.\n                E.g. \"-year,title\".\n        \"\"\"\n        # Change HTTP query parameter to a list\n        aura_sorts = sort_arg.strip(\",\").split(\",\")\n        sorts = []\n        for aura_attr in aura_sorts:\n            if aura_attr[0] == \"-\":\n                ascending = False\n                # Remove leading \"-\"\n                aura_attr = aura_attr[1:]\n            else:\n                # JSON:API default\n                ascending = True\n            # Get the beets version of the attribute name\n            beets_attr = self.attribute_map.get(aura_attr, aura_attr)\n            # Use slow sort so it works with all fields (inc. computed)\n            sorts.append(SlowFieldSort(beets_attr, ascending=ascending))\n        return MultipleSort(sorts)\n\n    def paginate(self, collection):\n        \"\"\"Get a page of the collection and the URL to the next page.\n\n        Args:\n            collection: The raw data from which resource objects can be\n                built. Could be an sqlite3.Cursor object (tracks and\n                albums) or a list of strings (artists).\n        \"\"\"\n        # Pages start from zero\n        page = self.args.get(\"page\", 0, int)\n        # Use page limit defined in config by default.\n        default_limit = config[\"aura\"][\"page_limit\"].get(int)\n        limit = self.args.get(\"limit\", default_limit, int)\n        # start = offset of first item to return\n        start = page * limit\n        # end = offset of last item + 1\n        end = start + limit\n        if end > len(collection):\n            end = len(collection)\n            next_url = None\n        else:\n            # Not the last page so work out links.next url\n            if not self.args:\n                # No existing arguments, so current page is 0\n                next_url = f\"{request.url}?page=1\"\n            elif not self.args.get(\"page\", None):\n                # No existing page argument, so add one to the end\n                next_url = f\"{request.url}&page=1\"\n            else:\n                # Increment page token by 1\n                next_url = request.url.replace(\n                    f\"page={page}\", f\"page={page + 1}\"\n                )\n        # Get only the items in the page range\n        data = [\n            self.get_resource_object(self.lib, collection[i])\n            for i in range(start, end)\n        ]\n        return data, next_url\n\n    def get_included(self, data, include_str):\n        \"\"\"Build a list of resource objects for inclusion.\n\n        Args:\n            data: An array of dicts in the form of resource objects.\n            include_str: A comma separated list of resource types to\n                include. E.g. \"tracks,images\".\n        \"\"\"\n        # Change HTTP query parameter to a list\n        to_include = include_str.strip(\",\").split(\",\")\n        # Build a list of unique type and id combinations\n        # For each resource object in the primary data, iterate over it's\n        # relationships. If a relationship matches one of the types\n        # requested for inclusion (e.g. \"albums\") then add each type-id pair\n        # under the \"data\" key to unique_identifiers, checking first that\n        # it has not already been added. This ensures that no resources are\n        # included more than once.\n        unique_identifiers = []\n        for res_obj in data:\n            for rel_name, rel_obj in res_obj[\"relationships\"].items():\n                if rel_name in to_include:\n                    # NOTE: Assumes relationship is to-many\n                    for identifier in rel_obj[\"data\"]:\n                        if identifier not in unique_identifiers:\n                            unique_identifiers.append(identifier)\n        # TODO: I think this could be improved\n        included = []\n        for identifier in unique_identifiers:\n            res_type = identifier[\"type\"]\n            if res_type == \"track\":\n                track_id = int(identifier[\"id\"])\n                track = self.lib.get_item(track_id)\n                included.append(\n                    TrackDocument.get_resource_object(self.lib, track)\n                )\n            elif res_type == \"album\":\n                album_id = int(identifier[\"id\"])\n                album = self.lib.get_album(album_id)\n                included.append(\n                    AlbumDocument.get_resource_object(self.lib, album)\n                )\n            elif res_type == \"artist\":\n                artist_id = identifier[\"id\"]\n                included.append(\n                    ArtistDocument.get_resource_object(self.lib, artist_id)\n                )\n            elif res_type == \"image\":\n                image_id = identifier[\"id\"]\n                included.append(\n                    ImageDocument.get_resource_object(self.lib, image_id)\n                )\n            else:\n                raise ValueError(f\"Invalid resource type: {res_type}\")\n        return included\n\n    def all_resources(self):\n        \"\"\"Build document for /tracks, /albums or /artists.\"\"\"\n        query = self.translate_filters()\n        sort_arg = self.args.get(\"sort\", None)\n        if sort_arg:\n            sort = self.translate_sorts(sort_arg)\n            # For each sort field add a query which ensures all results\n            # have a non-empty, non-zero value for that field.\n            query.subqueries.extend(\n                NotQuery(\n                    self.model_cls.field_query(s.field, \"(^$|^0$)\", RegexpQuery)\n                )\n                for s in sort.sorts\n            )\n        else:\n            sort = None\n        # Get information from the library\n        collection = self.get_collection(query=query, sort=sort)\n        # Convert info to AURA form and paginate it\n        data, next_url = self.paginate(collection)\n        document = {\"data\": data}\n        # If there are more pages then provide a way to access them\n        if next_url:\n            document[\"links\"] = {\"next\": next_url}\n        # Include related resources for each element in \"data\"\n        include_str = self.args.get(\"include\", None)\n        if include_str:\n            document[\"included\"] = self.get_included(data, include_str)\n        return document\n\n    def single_resource_document(self, resource_object):\n        \"\"\"Build document for a specific requested resource.\n\n        Args:\n            resource_object: A dictionary in the form of a JSON:API\n                resource object.\n        \"\"\"\n        document = {\"data\": resource_object}\n        include_str = self.args.get(\"include\", None)\n        if include_str:\n            # [document[\"data\"]] is because arg needs to be list\n            document[\"included\"] = self.get_included(\n                [document[\"data\"]], include_str\n            )\n        return document\n\n\nclass TrackDocument(AURADocument):\n    \"\"\"Class for building documents for /tracks endpoints.\"\"\"\n\n    model_cls = Item\n\n    attribute_map = TRACK_ATTR_MAP\n\n    def get_collection(self, query=None, sort=None):\n        \"\"\"Get Item objects from the library.\n\n        Args:\n            query: A beets Query object or a beets query string.\n            sort: A beets Sort object.\n        \"\"\"\n        return self.lib.items(query, sort)\n\n    @classmethod\n    def get_attribute_converter(cls, beets_attr: str) -> type[SQLiteType]:\n        \"\"\"Work out what data type an attribute should be for beets.\n\n        Args:\n            beets_attr: The name of the beets attribute, e.g. \"title\".\n        \"\"\"\n        # filesize is a special field (read from disk not db?)\n        if beets_attr == \"filesize\":\n            return int\n\n        return super().get_attribute_converter(beets_attr)\n\n    @staticmethod\n    def get_resource_object(lib: Library, track):\n        \"\"\"Construct a JSON:API resource object from a beets Item.\n\n        Args:\n            track: A beets Item object.\n        \"\"\"\n        attributes = {}\n        # Use aura => beets attribute map, e.g. size => filesize\n        for aura_attr, beets_attr in TRACK_ATTR_MAP.items():\n            a = getattr(track, beets_attr)\n            # Only set attribute if it's not None, 0, \"\", etc.\n            # NOTE: This could result in required attributes not being set\n            if a:\n                attributes[aura_attr] = a\n\n        # JSON:API one-to-many relationship to parent album\n        relationships = {\n            \"artists\": {\"data\": [{\"type\": \"artist\", \"id\": track.artist}]}\n        }\n        # Only add album relationship if not singleton\n        if not track.singleton:\n            relationships[\"albums\"] = {\n                \"data\": [{\"type\": \"album\", \"id\": str(track.album_id)}]\n            }\n\n        return {\n            \"type\": \"track\",\n            \"id\": str(track.id),\n            \"attributes\": attributes,\n            \"relationships\": relationships,\n        }\n\n    def single_resource(self, track_id):\n        \"\"\"Get track from the library and build a document.\n\n        Args:\n            track_id: The beets id of the track (integer).\n        \"\"\"\n        track = self.lib.get_item(track_id)\n        if not track:\n            return self.error(\n                \"404 Not Found\",\n                \"No track with the requested id.\",\n                f\"There is no track with an id of {track_id} in the library.\",\n            )\n        return self.single_resource_document(\n            self.get_resource_object(self.lib, track)\n        )\n\n\nclass AlbumDocument(AURADocument):\n    \"\"\"Class for building documents for /albums endpoints.\"\"\"\n\n    model_cls = Album\n\n    attribute_map = ALBUM_ATTR_MAP\n\n    def get_collection(self, query=None, sort=None):\n        \"\"\"Get Album objects from the library.\n\n        Args:\n            query: A beets Query object or a beets query string.\n            sort: A beets Sort object.\n        \"\"\"\n        return self.lib.albums(query, sort)\n\n    @staticmethod\n    def get_resource_object(lib: Library, album):\n        \"\"\"Construct a JSON:API resource object from a beets Album.\n\n        Args:\n            album: A beets Album object.\n        \"\"\"\n        attributes = {}\n        # Use aura => beets attribute name map\n        for aura_attr, beets_attr in ALBUM_ATTR_MAP.items():\n            a = getattr(album, beets_attr)\n            # Only set attribute if it's not None, 0, \"\", etc.\n            # NOTE: This could mean required attributes are not set\n            if a:\n                attributes[aura_attr] = a\n\n        # Get beets Item objects for all tracks in the album sorted by\n        # track number. Sorting is not required but it's nice.\n        query = MatchQuery(\"album_id\", album.id)\n        sort = FixedFieldSort(\"track\", ascending=True)\n        tracks = lib.items(query, sort)\n        # JSON:API one-to-many relationship to tracks on the album\n        relationships = {\n            \"tracks\": {\n                \"data\": [{\"type\": \"track\", \"id\": str(t.id)} for t in tracks]\n            }\n        }\n        # Add images relationship if album has associated images\n        if album.artpath:\n            path = os.fsdecode(album.artpath)\n            filename = path.split(\"/\")[-1]\n            image_id = f\"album-{album.id}-{filename}\"\n            relationships[\"images\"] = {\n                \"data\": [{\"type\": \"image\", \"id\": image_id}]\n            }\n        # Add artist relationship if artist name is same on tracks\n        # Tracks are used to define artists so don't albumartist\n        # Check for all tracks in case some have featured artists\n        if album.albumartist in [t.artist for t in tracks]:\n            relationships[\"artists\"] = {\n                \"data\": [{\"type\": \"artist\", \"id\": album.albumartist}]\n            }\n\n        return {\n            \"type\": \"album\",\n            \"id\": str(album.id),\n            \"attributes\": attributes,\n            \"relationships\": relationships,\n        }\n\n    def single_resource(self, album_id):\n        \"\"\"Get album from the library and build a document.\n\n        Args:\n            album_id: The beets id of the album (integer).\n        \"\"\"\n        album = self.lib.get_album(album_id)\n        if not album:\n            return self.error(\n                \"404 Not Found\",\n                \"No album with the requested id.\",\n                f\"There is no album with an id of {album_id} in the library.\",\n            )\n        return self.single_resource_document(\n            self.get_resource_object(self.lib, album)\n        )\n\n\nclass ArtistDocument(AURADocument):\n    \"\"\"Class for building documents for /artists endpoints.\"\"\"\n\n    model_cls = Item\n\n    attribute_map = ARTIST_ATTR_MAP\n\n    def get_collection(self, query=None, sort=None):\n        \"\"\"Get a list of artist names from the library.\n\n        Args:\n            query: A beets Query object or a beets query string.\n            sort: A beets Sort object.\n        \"\"\"\n        # Gets only tracks with matching artist information\n        tracks = self.lib.items(query, sort)\n        collection = []\n        for track in tracks:\n            # Do not add duplicates\n            if track.artist not in collection:\n                collection.append(track.artist)\n        return collection\n\n    @staticmethod\n    def get_resource_object(lib: Library, artist_id):\n        \"\"\"Construct a JSON:API resource object for the given artist.\n\n        Args:\n            artist_id: A string which is the artist's name.\n        \"\"\"\n        # Get tracks where artist field exactly matches artist_id\n        query = MatchQuery(\"artist\", artist_id)\n        tracks = lib.items(query)\n        if not tracks:\n            return None\n\n        # Get artist information from the first track\n        # NOTE: It could be that the first track doesn't have a\n        # MusicBrainz id but later tracks do, which isn't ideal.\n        attributes = {}\n        # Use aura => beets attribute map, e.g. artist => name\n        for aura_attr, beets_attr in ARTIST_ATTR_MAP.items():\n            a = getattr(tracks[0], beets_attr)\n            # Only set attribute if it's not None, 0, \"\", etc.\n            # NOTE: This could mean required attributes are not set\n            if a:\n                attributes[aura_attr] = a\n\n        relationships = {\n            \"tracks\": {\n                \"data\": [{\"type\": \"track\", \"id\": str(t.id)} for t in tracks]\n            }\n        }\n        album_query = MatchQuery(\"albumartist\", artist_id)\n        albums = lib.albums(query=album_query)\n        if len(albums) != 0:\n            relationships[\"albums\"] = {\n                \"data\": [{\"type\": \"album\", \"id\": str(a.id)} for a in albums]\n            }\n\n        return {\n            \"type\": \"artist\",\n            \"id\": artist_id,\n            \"attributes\": attributes,\n            \"relationships\": relationships,\n        }\n\n    def single_resource(self, artist_id):\n        \"\"\"Get info for the requested artist and build a document.\n\n        Args:\n            artist_id: A string which is the artist's name.\n        \"\"\"\n        artist_resource = self.get_resource_object(self.lib, artist_id)\n        if not artist_resource:\n            return self.error(\n                \"404 Not Found\",\n                \"No artist with the requested id.\",\n                f\"There is no artist with an id of {artist_id} in the library.\",\n            )\n        return self.single_resource_document(artist_resource)\n\n\ndef safe_filename(fn):\n    \"\"\"Check whether a string is a simple (non-path) filename.\n\n    For example, `foo.txt` is safe because it is a \"plain\" filename. But\n    `foo/bar.txt` and `../foo.txt` and `.` are all non-safe because they\n    can traverse to other directories other than the current one.\n    \"\"\"\n    # Rule out any directories.\n    if os.path.basename(fn) != fn:\n        return False\n\n    # In single names, rule out Unix directory traversal names.\n    if fn in (\".\", \"..\"):\n        return False\n\n    return True\n\n\nclass ImageDocument(AURADocument):\n    \"\"\"Class for building documents for /images/(id) endpoints.\"\"\"\n\n    model_cls = Album\n\n    @staticmethod\n    def get_image_path(lib: Library, image_id):\n        \"\"\"Works out the full path to the image with the given id.\n\n        Returns None if there is no such image.\n\n        Args:\n            image_id: A string in the form\n                \"<parent_type>-<parent_id>-<img_filename>\".\n        \"\"\"\n        # Split image_id into its constituent parts\n        id_split = image_id.split(\"-\")\n        if len(id_split) < 3:\n            # image_id is not in the required format\n            return None\n        parent_type = id_split[0]\n        parent_id = id_split[1]\n        img_filename = \"-\".join(id_split[2:])\n        if not safe_filename(img_filename):\n            return None\n\n        # Get the path to the directory parent's images are in\n        if parent_type == \"album\":\n            album = lib.get_album(int(parent_id))\n            if not album or not album.artpath:\n                return None\n            # Cut the filename off of artpath\n            # This is in preparation for supporting images in the same\n            # directory that are not tracked by beets.\n            artpath = os.fsdecode(album.artpath)\n            dir_path = \"/\".join(artpath.split(\"/\")[:-1])\n        else:\n            # Images for other resource types are not supported\n            return None\n\n        img_path = os.path.join(dir_path, img_filename)\n        # Check the image actually exists\n        if os.path.isfile(img_path):\n            return img_path\n        else:\n            return None\n\n    @staticmethod\n    def get_resource_object(lib: Library, image_id):\n        \"\"\"Construct a JSON:API resource object for the given image.\n\n        Args:\n            image_id: A string in the form\n                \"<parent_type>-<parent_id>-<img_filename>\".\n        \"\"\"\n        # Could be called as a static method, so can't use\n        # self.get_image_path()\n        image_path = ImageDocument.get_image_path(lib, image_id)\n        if not image_path:\n            return None\n\n        attributes = {\n            \"role\": \"cover\",\n            \"mimetype\": guess_type(image_path)[0],\n            \"size\": os.path.getsize(image_path),\n        }\n        try:\n            from PIL import Image\n        except ImportError:\n            pass\n        else:\n            im = Image.open(image_path)\n            attributes[\"width\"] = im.width\n            attributes[\"height\"] = im.height\n\n        relationships = {}\n        # Split id into [parent_type, parent_id, filename]\n        id_split = image_id.split(\"-\")\n        relationships[f\"{id_split[0]}s\"] = {\n            \"data\": [{\"type\": id_split[0], \"id\": id_split[1]}]\n        }\n\n        return {\n            \"id\": image_id,\n            \"type\": \"image\",\n            # Remove attributes that are None, 0, \"\", etc.\n            \"attributes\": {k: v for k, v in attributes.items() if v},\n            \"relationships\": relationships,\n        }\n\n    def single_resource(self, image_id):\n        \"\"\"Get info for the requested image and build a document.\n\n        Args:\n            image_id: A string in the form\n                \"<parent_type>-<parent_id>-<img_filename>\".\n        \"\"\"\n        image_resource = self.get_resource_object(self.lib, image_id)\n        if not image_resource:\n            return self.error(\n                \"404 Not Found\",\n                \"No image with the requested id.\",\n                f\"There is no image with an id of {image_id} in the library.\",\n            )\n        return self.single_resource_document(image_resource)\n\n\n# Initialise flask blueprint\naura_bp = Blueprint(\"aura_bp\", __name__)\n\n\n@aura_bp.route(\"/server\")\ndef server_info():\n    \"\"\"Respond with info about the server.\"\"\"\n    return {\"data\": {\"type\": \"server\", \"id\": \"0\", \"attributes\": SERVER_INFO}}\n\n\n# Track endpoints\n\n\n@aura_bp.route(\"/tracks\")\ndef all_tracks():\n    \"\"\"Respond with a list of all tracks and related information.\"\"\"\n    return TrackDocument.from_app().all_resources()\n\n\n@aura_bp.route(\"/tracks/<int:track_id>\")\ndef single_track(track_id):\n    \"\"\"Respond with info about the specified track.\n\n    Args:\n        track_id: The id of the track provided in the URL (integer).\n    \"\"\"\n    return TrackDocument.from_app().single_resource(track_id)\n\n\n@aura_bp.route(\"/tracks/<int:track_id>/audio\")\ndef audio_file(track_id):\n    \"\"\"Supply an audio file for the specified track.\n\n    Args:\n        track_id: The id of the track provided in the URL (integer).\n    \"\"\"\n    track = current_app.config[\"lib\"].get_item(track_id)\n    if not track:\n        return AURADocument.error(\n            \"404 Not Found\",\n            \"No track with the requested id.\",\n            f\"There is no track with an id of {track_id} in the library.\",\n        )\n\n    path = os.fsdecode(track.path)\n    if not os.path.isfile(path):\n        return AURADocument.error(\n            \"404 Not Found\",\n            \"No audio file for the requested track.\",\n            f\"There is no audio file for track {track_id} at the expected\"\n            \" location\",\n        )\n\n    file_mimetype = guess_type(path)[0]\n    if not file_mimetype:\n        return AURADocument.error(\n            \"500 Internal Server Error\",\n            \"Requested audio file has an unknown mimetype.\",\n            f\"The audio file for track {track_id} has an unknown mimetype. \"\n            f\"Its file extension is {path.split('.')[-1]}.\",\n        )\n\n    # Check that the Accept header contains the file's mimetype\n    # Takes into account */* and audio/*\n    # Adding support for the bitrate parameter would require some effort so I\n    # left it out. This means the client could be sent an error even if the\n    # audio doesn't need transcoding.\n    if not request.accept_mimetypes.best_match([file_mimetype]):\n        return AURADocument.error(\n            \"406 Not Acceptable\",\n            \"Unsupported MIME type or bitrate parameter in Accept header.\",\n            f\"The audio file for track {track_id} is only available as\"\n            f\" {file_mimetype} and bitrate parameters are not supported.\",\n        )\n\n    return send_file(\n        path,\n        mimetype=file_mimetype,\n        # Handles filename in Content-Disposition header\n        as_attachment=True,\n        # Tries to upgrade the stream to support range requests\n        conditional=True,\n    )\n\n\n# Album endpoints\n\n\n@aura_bp.route(\"/albums\")\ndef all_albums():\n    \"\"\"Respond with a list of all albums and related information.\"\"\"\n    return AlbumDocument.from_app().all_resources()\n\n\n@aura_bp.route(\"/albums/<int:album_id>\")\ndef single_album(album_id):\n    \"\"\"Respond with info about the specified album.\n\n    Args:\n        album_id: The id of the album provided in the URL (integer).\n    \"\"\"\n    return AlbumDocument.from_app().single_resource(album_id)\n\n\n# Artist endpoints\n# Artist ids are their names\n\n\n@aura_bp.route(\"/artists\")\ndef all_artists():\n    \"\"\"Respond with a list of all artists and related information.\"\"\"\n    return ArtistDocument.from_app().all_resources()\n\n\n# Using the path converter allows slashes in artist_id\n@aura_bp.route(\"/artists/<path:artist_id>\")\ndef single_artist(artist_id):\n    \"\"\"Respond with info about the specified artist.\n\n    Args:\n        artist_id: The id of the artist provided in the URL. A string\n            which is the artist's name.\n    \"\"\"\n    return ArtistDocument.from_app().single_resource(artist_id)\n\n\n# Image endpoints\n# Image ids are in the form <parent_type>-<parent_id>-<img_filename>\n# For example: album-13-cover.jpg\n\n\n@aura_bp.route(\"/images/<string:image_id>\")\ndef single_image(image_id):\n    \"\"\"Respond with info about the specified image.\n\n    Args:\n        image_id: The id of the image provided in the URL. A string in\n            the form \"<parent_type>-<parent_id>-<img_filename>\".\n    \"\"\"\n    return ImageDocument.from_app().single_resource(image_id)\n\n\n@aura_bp.route(\"/images/<string:image_id>/file\")\ndef image_file(image_id):\n    \"\"\"Supply an image file for the specified image.\n\n    Args:\n        image_id: The id of the image provided in the URL. A string in\n            the form \"<parent_type>-<parent_id>-<img_filename>\".\n    \"\"\"\n    img_path = ImageDocument.get_image_path(current_app.config[\"lib\"], image_id)\n    if not img_path:\n        return AURADocument.error(\n            \"404 Not Found\",\n            \"No image with the requested id.\",\n            f\"There is no image with an id of {image_id} in the library\",\n        )\n    return send_file(img_path)\n\n\n# WSGI app\n\n\ndef create_app():\n    \"\"\"An application factory for use by a WSGI server.\"\"\"\n    config[\"aura\"].add(\n        {\n            \"host\": \"127.0.0.1\",\n            \"port\": 8337,\n            \"cors\": [],\n            \"cors_supports_credentials\": False,\n            \"page_limit\": 500,\n        }\n    )\n\n    app = Flask(__name__)\n    # Register AURA blueprint view functions under a URL prefix\n    app.register_blueprint(aura_bp, url_prefix=\"/aura\")\n    # AURA specifies mimetype MUST be this\n    app.config[\"JSONIFY_MIMETYPE\"] = \"application/vnd.api+json\"\n    # Disable auto-sorting of JSON keys\n    app.config[\"JSON_SORT_KEYS\"] = False\n    # Provide a way to access the beets library\n    # The normal method of using the Library and config provided in the\n    # command function is not used because create_app() could be called\n    # by an external WSGI server.\n    # NOTE: this uses a 'private' function from beets.ui.__init__\n    app.config[\"lib\"] = _open_library(config)\n\n    # Enable CORS if required\n    cors = config[\"aura\"][\"cors\"].as_str_seq(list)\n    if cors:\n        from flask_cors import CORS\n\n        # \"Accept\" is the only header clients use\n        app.config[\"CORS_ALLOW_HEADERS\"] = \"Accept\"\n        app.config[\"CORS_RESOURCES\"] = {r\"/aura/*\": {\"origins\": cors}}\n        app.config[\"CORS_SUPPORTS_CREDENTIALS\"] = config[\"aura\"][\n            \"cors_supports_credentials\"\n        ].get(bool)\n        CORS(app)\n\n    return app\n\n\n# Beets Plugin Hook\n\n\nclass AURAPlugin(BeetsPlugin):\n    \"\"\"The BeetsPlugin subclass for the AURA server plugin.\"\"\"\n\n    def __init__(self):\n        \"\"\"Add configuration options for the AURA plugin.\"\"\"\n        super().__init__()\n\n    def commands(self):\n        \"\"\"Add subcommand used to run the AURA server.\"\"\"\n\n        def run_aura(lib, opts, args):\n            \"\"\"Run the application using Flask's built in-server.\n\n            Args:\n                lib: A beets Library object (not used).\n                opts: Command line options. An optparse.Values object.\n                args: The list of arguments to process (not used).\n            \"\"\"\n            app = create_app()\n            # Start the built-in server (not intended for production)\n            app.run(\n                host=self.config[\"host\"].get(str),\n                port=self.config[\"port\"].get(int),\n                debug=opts.debug,\n                threaded=True,\n            )\n\n        run_aura_cmd = Subcommand(\"aura\", help=\"run an AURA server\")\n        run_aura_cmd.parser.add_option(\n            \"-d\",\n            \"--debug\",\n            action=\"store_true\",\n            default=False,\n            help=\"use Flask debug mode\",\n        )\n        run_aura_cmd.func = run_aura\n        return [run_aura_cmd]\n"
  },
  {
    "path": "beetsplug/autobpm.py",
    "content": "# This file is part of beets.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Uses Librosa to calculate the `bpm` field.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nimport librosa\nimport numpy as np\n\nfrom beets.plugins import BeetsPlugin\nfrom beets.ui import Subcommand, should_write\n\nif TYPE_CHECKING:\n    from beets.importer import ImportTask\n    from beets.library import Item, Library\n\n\nclass AutoBPMPlugin(BeetsPlugin):\n    def __init__(self) -> None:\n        super().__init__()\n        self.config.add(\n            {\n                \"auto\": True,\n                \"overwrite\": False,\n                \"beat_track_kwargs\": {},\n            }\n        )\n\n        if self.config[\"auto\"]:\n            self.import_stages = [self.imported]\n\n    def commands(self) -> list[Subcommand]:\n        cmd = Subcommand(\n            \"autobpm\", help=\"detect and add bpm from audio using Librosa\"\n        )\n        cmd.func = self.command\n        return [cmd]\n\n    def command(self, lib: Library, _, args: list[str]) -> None:\n        self.calculate_bpm(list(lib.items(args)), write=should_write())\n\n    def imported(self, _, task: ImportTask) -> None:\n        self.calculate_bpm(task.imported_items())\n\n    def calculate_bpm(self, items: list[Item], write: bool = False) -> None:\n        for item in items:\n            path = item.filepath\n            if bpm := item.bpm:\n                self._log.info(\"BPM for {} already exists: {}\", path, bpm)\n                if not self.config[\"overwrite\"]:\n                    continue\n\n            try:\n                y, sr = librosa.load(item.filepath, res_type=\"kaiser_fast\")\n            except Exception as exc:\n                self._log.error(\"Failed to load {}: {}\", path, exc)\n                continue\n\n            kwargs = self.config[\"beat_track_kwargs\"].flatten()\n            try:\n                tempo, _ = librosa.beat.beat_track(y=y, sr=sr, **kwargs)\n            except Exception as exc:\n                self._log.error(\"Failed to measure BPM for {}: {}\", path, exc)\n                continue\n\n            bpm = round(\n                float(tempo[0] if isinstance(tempo, np.ndarray) else tempo)\n            )\n\n            item[\"bpm\"] = bpm\n            self._log.info(\"Computed BPM for {}: {}\", path, bpm)\n\n            if write:\n                item.try_write()\n            item.store()\n"
  },
  {
    "path": "beetsplug/badfiles.py",
    "content": "# This file is part of beets.\n# Copyright 2016, François-Xavier Thomas.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Use command-line tools to check for audio file corruption.\"\"\"\n\nimport errno\nimport os\nimport shlex\nimport sys\nfrom subprocess import STDOUT, CalledProcessError, check_output, list2cmdline\n\nimport confuse\n\nfrom beets import importer, ui\nfrom beets.plugins import BeetsPlugin\nfrom beets.ui import Subcommand\nfrom beets.util import displayable_path, par_map\nfrom beets.util.color import colorize\n\n\nclass CheckerCommandError(Exception):\n    \"\"\"Raised when running a checker failed.\n\n    Attributes:\n        checker: Checker command name.\n        path: Path to the file being validated.\n        errno: Error number from the checker execution error.\n        msg: Message from the checker execution error.\n    \"\"\"\n\n    def __init__(self, cmd, oserror):\n        self.checker = cmd[0]\n        self.path = cmd[-1]\n        self.errno = oserror.errno\n        self.msg = str(oserror)\n\n\nclass BadFiles(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n        self.verbose = False\n\n        self.register_listener(\"import_task_start\", self.on_import_task_start)\n        self.register_listener(\n            \"import_task_before_choice\", self.on_import_task_before_choice\n        )\n\n    def run_command(self, cmd):\n        self._log.debug(\n            \"running command: {}\", displayable_path(list2cmdline(cmd))\n        )\n        try:\n            output = check_output(cmd, stderr=STDOUT)\n            errors = 0\n            status = 0\n        except CalledProcessError as e:\n            output = e.output\n            errors = 1\n            status = e.returncode\n        except OSError as e:\n            raise CheckerCommandError(cmd, e)\n        output = output.decode(sys.getdefaultencoding(), \"replace\")\n        return status, errors, [line for line in output.split(\"\\n\") if line]\n\n    def check_mp3val(self, path):\n        status, errors, output = self.run_command([\"mp3val\", path])\n        if status == 0:\n            output = [line for line in output if line.startswith(\"WARNING:\")]\n            errors = len(output)\n        return status, errors, output\n\n    def check_flac(self, path):\n        return self.run_command([\"flac\", \"-wst\", path])\n\n    def check_custom(self, command):\n        def checker(path):\n            cmd = shlex.split(command)\n            cmd.append(path)\n            return self.run_command(cmd)\n\n        return checker\n\n    def get_checker(self, ext):\n        ext = ext.lower()\n        try:\n            command = self.config[\"commands\"].get(dict).get(ext)\n        except confuse.NotFoundError:\n            command = None\n        if command:\n            return self.check_custom(command)\n        if ext == \"mp3\":\n            return self.check_mp3val\n        if ext == \"flac\":\n            return self.check_flac\n\n    def check_item(self, item):\n        # First, check whether the path exists. If not, the user\n        # should probably run `beet update` to cleanup your library.\n        dpath = displayable_path(item.path)\n        self._log.debug(\"checking path: {}\", dpath)\n        if not os.path.exists(item.path):\n            ui.print_(f\"{colorize('text_error', dpath)}: file does not exist\")\n\n        # Run the checker against the file if one is found\n        ext = os.path.splitext(item.path)[1][1:].decode(\"utf8\", \"ignore\")\n        checker = self.get_checker(ext)\n        if not checker:\n            self._log.error(\"no checker specified in the config for {}\", ext)\n            return []\n        path = item.path\n        if not isinstance(path, str):\n            path = item.path.decode(sys.getfilesystemencoding())\n        try:\n            status, errors, output = checker(path)\n        except CheckerCommandError as e:\n            if e.errno == errno.ENOENT:\n                self._log.error(\n                    \"command not found: {0.checker} when validating file: {0.path}\",\n                    e,\n                )\n            else:\n                self._log.error(\"error invoking {0.checker}: {0.msg}\", e)\n            return []\n\n        error_lines = []\n\n        if status > 0:\n            error_lines.append(\n                f\"{colorize('text_error', dpath)}: checker exited with status {status}\"\n            )\n            for line in output:\n                error_lines.append(f\"  {line}\")\n\n        elif errors > 0:\n            error_lines.append(\n                f\"{colorize('text_warning', dpath)}: checker found\"\n                f\" {errors} errors or warnings\"\n            )\n            for line in output:\n                error_lines.append(f\"  {line}\")\n        elif self.verbose:\n            error_lines.append(f\"{colorize('text_success', dpath)}: ok\")\n\n        return error_lines\n\n    def on_import_task_start(self, task, session):\n        if not self.config[\"check_on_import\"].get(False):\n            return\n\n        checks_failed = []\n\n        for item in task.items:\n            error_lines = self.check_item(item)\n            if error_lines:\n                checks_failed.append(error_lines)\n\n        if checks_failed:\n            task._badfiles_checks_failed = checks_failed\n\n    def on_import_task_before_choice(self, task, session):\n        if hasattr(task, \"_badfiles_checks_failed\"):\n            ui.print_(\n                f\"{colorize('text_warning', 'BAD')} one or more files failed checks:\"\n            )\n            for error in task._badfiles_checks_failed:\n                for error_line in error:\n                    ui.print_(error_line)\n\n            ui.print_()\n            ui.print_(\"What would you like to do?\")\n\n            sel = ui.input_options([\"aBort\", \"skip\", \"continue\"])\n\n            if sel == \"s\":\n                return importer.Action.SKIP\n            elif sel == \"c\":\n                return None\n            elif sel == \"b\":\n                raise importer.ImportAbortError()\n            else:\n                raise Exception(f\"Unexpected selection: {sel}\")\n\n    def command(self, lib, opts, args):\n        # Get items from arguments\n        items = lib.items(args)\n        self.verbose = opts.verbose\n\n        def check_and_print(item):\n            for error_line in self.check_item(item):\n                ui.print_(error_line)\n\n        par_map(check_and_print, items)\n\n    def commands(self):\n        bad_command = Subcommand(\n            \"bad\", help=\"check for corrupt or missing files\"\n        )\n        bad_command.parser.add_option(\n            \"-v\",\n            \"--verbose\",\n            action=\"store_true\",\n            default=False,\n            dest=\"verbose\",\n            help=\"view results for both the bad and uncorrupted files\",\n        )\n        bad_command.func = self.command\n        return [bad_command]\n"
  },
  {
    "path": "beetsplug/bareasc.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Philippe Mongeau.\n# Copyright 2021, Graham R. Cobb.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and ascociated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n#\n# This module is adapted from Fuzzy in accordance to the licence of\n# that module\n\n\"\"\"Provides a bare-ASCII matching query.\"\"\"\n\nfrom unidecode import unidecode\n\nfrom beets import ui\nfrom beets.dbcore.query import StringFieldQuery\nfrom beets.plugins import BeetsPlugin\nfrom beets.ui import print_\n\n\nclass BareascQuery(StringFieldQuery[str]):\n    \"\"\"Compare items using bare ASCII, without accents etc.\"\"\"\n\n    @classmethod\n    def string_match(cls, pattern, val):\n        \"\"\"Convert both pattern and string to plain ASCII before matching.\n\n        If pattern is all lower case, also convert string to lower case so\n        match is also case insensitive\n        \"\"\"\n        # smartcase\n        if pattern.islower():\n            val = val.lower()\n        pattern = unidecode(pattern)\n        val = unidecode(val)\n        return pattern in val\n\n    def col_clause(self):\n        \"\"\"Compare ascii version of the pattern.\"\"\"\n        clause = f\"unidecode({self.field})\"\n        if self.pattern.islower():\n            clause = f\"lower({clause})\"\n\n        return rf\"{clause} LIKE ? ESCAPE '\\'\", [f\"%{unidecode(self.pattern)}%\"]\n\n\nclass BareascPlugin(BeetsPlugin):\n    \"\"\"Plugin to provide bare-ASCII option for beets matching.\"\"\"\n\n    def __init__(self):\n        \"\"\"Default prefix for selecting bare-ASCII matching is #.\"\"\"\n        super().__init__()\n        self.config.add(\n            {\n                \"prefix\": \"#\",\n            }\n        )\n\n    def queries(self):\n        \"\"\"Register bare-ASCII matching.\"\"\"\n        prefix = self.config[\"prefix\"].as_str()\n        return {prefix: BareascQuery}\n\n    def commands(self):\n        \"\"\"Add bareasc command as unidecode version of 'list'.\"\"\"\n        cmd = ui.Subcommand(\n            \"bareasc\", help=\"unidecode version of beet list command\"\n        )\n        cmd.parser.usage += (\n            \"\\nExample: %prog -f '$album: $title' artist:beatles\"\n        )\n        cmd.parser.add_all_common_options()\n        cmd.func = self.unidecode_list\n        return [cmd]\n\n    def unidecode_list(self, lib, opts, args):\n        \"\"\"Emulate normal 'list' command but with unidecode output.\"\"\"\n        album = opts.album\n        # Copied from commands.py - list_items\n        if album:\n            for album in lib.albums(args):\n                bare = unidecode(str(album))\n                print_(bare)\n        else:\n            for item in lib.items(args):\n                bare = unidecode(str(item))\n                print_(bare)\n"
  },
  {
    "path": "beetsplug/beatport.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Adds Beatport release and track search support to the autotagger\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport re\nfrom datetime import datetime, timedelta\nfrom typing import TYPE_CHECKING, Literal, overload\n\nimport confuse\nfrom requests_oauthlib import OAuth1Session\nfrom requests_oauthlib.oauth1_session import (\n    TokenMissing,\n    TokenRequestDenied,\n    VerifierMissing,\n)\n\nimport beets\nimport beets.ui\nfrom beets import config\nfrom beets.autotag.hooks import AlbumInfo, TrackInfo\nfrom beets.metadata_plugins import MetadataSourcePlugin\nfrom beets.util import unique_list\nfrom beets.util.deprecation import deprecate_for_user\n\nif TYPE_CHECKING:\n    from collections.abc import Iterable, Iterator, Sequence\n\n    from beets.importer import ImportSession\n    from beets.library import Item\n\n    from ._typing import JSONDict\n\nAUTH_ERRORS = (TokenRequestDenied, TokenMissing, VerifierMissing)\nUSER_AGENT = f\"beets/{beets.__version__} +https://beets.io/\"\n\n\nclass BeatportAPIError(Exception):\n    pass\n\n\nclass BeatportClient:\n    _api_base = \"https://oauth-api.beatport.com\"\n\n    def __init__(self, c_key, c_secret, auth_key=None, auth_secret=None):\n        \"\"\"Initiate the client with OAuth information.\n\n        For the initial authentication with the backend `auth_key` and\n        `auth_secret` can be `None`. Use `get_authorize_url` and\n        `get_access_token` to obtain them for subsequent uses of the API.\n\n        :param c_key:       OAuth1 client key\n        :param c_secret:    OAuth1 client secret\n        :param auth_key:    OAuth1 resource owner key\n        :param auth_secret: OAuth1 resource owner secret\n        \"\"\"\n        self.api = OAuth1Session(\n            client_key=c_key,\n            client_secret=c_secret,\n            resource_owner_key=auth_key,\n            resource_owner_secret=auth_secret,\n            callback_uri=\"oob\",\n        )\n        self.api.headers = {\"User-Agent\": USER_AGENT}\n\n    def get_authorize_url(self) -> str:\n        \"\"\"Generate the URL for the user to authorize the application.\n\n        Retrieves a request token from the Beatport API and returns the\n        corresponding authorization URL on their end that the user has\n        to visit.\n\n        This is the first step of the initial authorization process with the\n        API. Once the user has visited the URL, call\n        :py:method:`get_access_token` with the displayed data to complete\n        the process.\n\n        :returns:   Authorization URL for the user to visit\n        :rtype:     unicode\n        \"\"\"\n        self.api.fetch_request_token(\n            self._make_url(\"/identity/1/oauth/request-token\")\n        )\n        return self.api.authorization_url(\n            self._make_url(\"/identity/1/oauth/authorize\")\n        )\n\n    def get_access_token(self, auth_data: str) -> tuple[str, str]:\n        \"\"\"Obtain the final access token and secret for the API.\n\n        :param auth_data:   URL-encoded authorization data as displayed at\n                            the authorization url (obtained via\n                            :py:meth:`get_authorize_url`) after signing in\n        :returns:           OAuth resource owner key and secret as unicode\n        \"\"\"\n        self.api.parse_authorization_response(\n            f\"https://beets.io/auth?{auth_data}\"\n        )\n        access_data = self.api.fetch_access_token(\n            self._make_url(\"/identity/1/oauth/access-token\")\n        )\n        return access_data[\"oauth_token\"], access_data[\"oauth_token_secret\"]\n\n    @overload\n    def search(\n        self,\n        query: str,\n        release_type: Literal[\"release\"],\n        details: bool = True,\n    ) -> Iterator[BeatportRelease]: ...\n\n    @overload\n    def search(\n        self,\n        query: str,\n        release_type: Literal[\"track\"],\n        details: bool = True,\n    ) -> Iterator[BeatportTrack]: ...\n\n    def search(\n        self,\n        query: str,\n        release_type: Literal[\"release\", \"track\"],\n        details=True,\n    ) -> Iterator[BeatportRelease | BeatportTrack]:\n        \"\"\"Perform a search of the Beatport catalogue.\n\n        :param query:           Query string\n        :param release_type:    Type of releases to search for.\n        :param details:         Retrieve additional information about the\n                                search results. Currently this will fetch\n                                the tracklist for releases and do nothing for\n                                tracks\n        :returns:               Search results\n        \"\"\"\n        response = self._get(\n            \"catalog/3/search\",\n            query=query,\n            perPage=5,\n            facets=[f\"fieldType:{release_type}\"],\n        )\n        for item in response:\n            if release_type == \"release\":\n                release = BeatportRelease(item)\n                if details:\n                    release.tracks = self.get_release_tracks(item[\"id\"])\n                yield release\n            elif release_type == \"track\":\n                yield BeatportTrack(item)\n\n    def get_release(self, beatport_id: str) -> BeatportRelease | None:\n        \"\"\"Get information about a single release.\n\n        :param beatport_id:     Beatport ID of the release\n        :returns:               The matching release\n        \"\"\"\n        response = self._get(\"/catalog/3/releases\", id=beatport_id)\n        if response:\n            release = BeatportRelease(response[0])\n            release.tracks = self.get_release_tracks(beatport_id)\n            return release\n        return None\n\n    def get_release_tracks(self, beatport_id: str) -> list[BeatportTrack]:\n        \"\"\"Get all tracks for a given release.\n\n        :param beatport_id:     Beatport ID of the release\n        :returns:               Tracks in the matching release\n        \"\"\"\n        response = self._get(\n            \"/catalog/3/tracks\", releaseId=beatport_id, perPage=100\n        )\n        return [BeatportTrack(t) for t in response]\n\n    def get_track(self, beatport_id: str) -> BeatportTrack:\n        \"\"\"Get information about a single track.\n\n        :param beatport_id:     Beatport ID of the track\n        :returns:               The matching track\n        \"\"\"\n        response = self._get(\"/catalog/3/tracks\", id=beatport_id)\n        return BeatportTrack(response[0])\n\n    def _make_url(self, endpoint: str) -> str:\n        \"\"\"Get complete URL for a given API endpoint.\"\"\"\n        if not endpoint.startswith(\"/\"):\n            endpoint = f\"/{endpoint}\"\n        return f\"{self._api_base}{endpoint}\"\n\n    def _get(self, endpoint: str, **kwargs) -> list[JSONDict]:\n        \"\"\"Perform a GET request on a given API endpoint.\n\n        Automatically extracts result data from the response and converts HTTP\n        exceptions into :py:class:`BeatportAPIError` objects.\n        \"\"\"\n        try:\n            response = self.api.get(self._make_url(endpoint), params=kwargs)\n        except Exception as e:\n            raise BeatportAPIError(f\"Error connecting to Beatport API: {e}\")\n        if not response:\n            raise BeatportAPIError(\n                f\"Error {response.status_code} for '{response.request.path_url}\"\n            )\n        return response.json()[\"results\"]\n\n\nclass BeatportObject:\n    beatport_id: str\n    name: str\n\n    release_date: datetime | None = None\n\n    artists: list[tuple[str, str]] | None = None\n    # tuple of artist id and artist name\n\n    def __init__(self, data: JSONDict):\n        self.beatport_id = str(data[\"id\"])  # given as int in the response\n        self.name = str(data[\"name\"])\n        if \"releaseDate\" in data:\n            self.release_date = datetime.strptime(\n                data[\"releaseDate\"], \"%Y-%m-%d\"\n            )\n        if \"artists\" in data:\n            self.artists = [(x[\"id\"], str(x[\"name\"])) for x in data[\"artists\"]]\n\n        self.genres = unique_list(\n            x[\"name\"]\n            for x in (*data.get(\"subGenres\", []), *data.get(\"genres\", []))\n        )\n\n    def artists_str(self) -> str | None:\n        if self.artists is not None:\n            if len(self.artists) < 4:\n                artist_str = \", \".join(x[1] for x in self.artists)\n            else:\n                artist_str = \"Various Artists\"\n        else:\n            artist_str = None\n\n        return artist_str\n\n\nclass BeatportRelease(BeatportObject):\n    catalog_number: str | None\n    label_name: str | None\n    category: str | None\n    url: str | None\n\n    tracks: list[BeatportTrack] | None = None\n\n    def __init__(self, data: JSONDict):\n        super().__init__(data)\n\n        self.catalog_number = data.get(\"catalogNumber\")\n        self.label_name = data.get(\"label\", {}).get(\"name\")\n        self.category = data.get(\"category\")\n\n        if \"slug\" in data:\n            self.url = (\n                f\"https://beatport.com/release/{data['slug']}/{data['id']}\"\n            )\n\n    def __str__(self) -> str:\n        return (\n            \"<BeatportRelease: \"\n            f\"{self.artists_str()} - {self.name} ({self.catalog_number})>\"\n        )\n\n\nclass BeatportTrack(BeatportObject):\n    title: str | None\n    mix_name: str | None\n    length: timedelta\n    url: str | None\n    track_number: int | None\n    bpm: str | None\n    initial_key: str | None\n\n    def __init__(self, data: JSONDict):\n        super().__init__(data)\n        if \"title\" in data:\n            self.title = str(data[\"title\"])\n        if \"mixName\" in data:\n            self.mix_name = str(data[\"mixName\"])\n        self.length = timedelta(milliseconds=data.get(\"lengthMs\", 0) or 0)\n        if not self.length:\n            try:\n                min, sec = data.get(\"length\", \"0:0\").split(\":\")\n                self.length = timedelta(minutes=int(min), seconds=int(sec))\n            except ValueError:\n                pass\n        if \"slug\" in data:\n            self.url = f\"https://beatport.com/track/{data['slug']}/{data['id']}\"\n        self.track_number = data.get(\"trackNumber\")\n        self.bpm = data.get(\"bpm\")\n        self.initial_key = str((data.get(\"key\") or {}).get(\"shortName\"))\n\n\nclass BeatportPlugin(MetadataSourcePlugin):\n    _client: BeatportClient | None = None\n\n    def __init__(self):\n        super().__init__()\n        deprecate_for_user(self._log, \"The 'beatport' plugin\")\n        self.config.add(\n            {\n                \"apikey\": \"57713c3906af6f5def151b33601389176b37b429\",\n                \"apisecret\": \"b3fe08c93c80aefd749fe871a16cd2bb32e2b954\",\n                \"tokenfile\": \"beatport_token.json\",\n            }\n        )\n        self.config[\"apikey\"].redact = True\n        self.config[\"apisecret\"].redact = True\n        self.register_listener(\"import_begin\", self.setup)\n\n    @property\n    def client(self) -> BeatportClient:\n        if self._client is None:\n            raise ValueError(\n                \"Beatport client not initialized. Call setup() first.\"\n            )\n        return self._client\n\n    def setup(self, session: ImportSession):\n        c_key: str = self.config[\"apikey\"].as_str()\n        c_secret: str = self.config[\"apisecret\"].as_str()\n\n        # Get the OAuth token from a file or log in.\n        try:\n            with open(self._tokenfile()) as f:\n                tokendata = json.load(f)\n        except OSError:\n            # No token yet. Generate one.\n            token, secret = self.authenticate(c_key, c_secret)\n        else:\n            token = tokendata[\"token\"]\n            secret = tokendata[\"secret\"]\n\n        self._client = BeatportClient(c_key, c_secret, token, secret)\n\n    def authenticate(self, c_key: str, c_secret: str) -> tuple[str, str]:\n        # Get the link for the OAuth page.\n        auth_client = BeatportClient(c_key, c_secret)\n        try:\n            url = auth_client.get_authorize_url()\n        except AUTH_ERRORS as e:\n            self._log.debug(\"authentication error: {}\", e)\n            raise beets.ui.UserError(\"communication with Beatport failed\")\n\n        beets.ui.print_(\"To authenticate with Beatport, visit:\")\n        beets.ui.print_(url)\n\n        # Ask for the verifier data and validate it.\n        data = beets.ui.input_(\"Enter the string displayed in your browser:\")\n        try:\n            token, secret = auth_client.get_access_token(data)\n        except AUTH_ERRORS as e:\n            self._log.debug(\"authentication error: {}\", e)\n            raise beets.ui.UserError(\"Beatport token request failed\")\n\n        # Save the token for later use.\n        self._log.debug(\"Beatport token {}, secret {}\", token, secret)\n        with open(self._tokenfile(), \"w\") as f:\n            json.dump({\"token\": token, \"secret\": secret}, f)\n\n        return token, secret\n\n    def _tokenfile(self) -> str:\n        \"\"\"Get the path to the JSON file for storing the OAuth token.\"\"\"\n        return self.config[\"tokenfile\"].get(confuse.Filename(in_app_dir=True))\n\n    def candidates(\n        self,\n        items: Sequence[Item],\n        artist: str,\n        album: str,\n        va_likely: bool,\n    ) -> Iterator[AlbumInfo]:\n        if va_likely:\n            query = album\n        else:\n            query = f\"{artist} {album}\"\n        try:\n            yield from self._get_releases(query)\n        except BeatportAPIError as e:\n            self._log.debug(\"API Error: {} (query: {})\", e, query)\n            return\n\n    def item_candidates(\n        self, item: Item, artist: str, title: str\n    ) -> Iterable[TrackInfo]:\n        query = f\"{artist} {title}\"\n        try:\n            return self._get_tracks(query)\n        except BeatportAPIError as e:\n            self._log.debug(\"API Error: {} (query: {})\", e, query)\n            return []\n\n    def album_for_id(self, album_id: str):\n        \"\"\"Fetches a release by its Beatport ID and returns an AlbumInfo object\n        or None if the query is not a valid ID or release is not found.\n        \"\"\"\n        self._log.debug(\"Searching for release {}\", album_id)\n\n        if not (release_id := self._extract_id(album_id)):\n            self._log.debug(\"Not a valid Beatport release ID.\")\n            return None\n\n        release = self.client.get_release(release_id)\n        if release:\n            return self._get_album_info(release)\n        return None\n\n    def track_for_id(self, track_id: str):\n        \"\"\"Fetches a track by its Beatport ID and returns a TrackInfo object\n        or None if the track is not a valid Beatport ID or track is not found.\n        \"\"\"\n        self._log.debug(\"Searching for track {}\", track_id)\n        # TODO: move to extractor\n        match = re.search(r\"(^|beatport\\.com/track/.+/)(\\d+)$\", track_id)\n        if not match:\n            self._log.debug(\"Not a valid Beatport track ID.\")\n            return None\n        bp_track = self.client.get_track(match.group(2))\n        if bp_track is not None:\n            return self._get_track_info(bp_track)\n        return None\n\n    def _get_releases(self, query: str) -> Iterator[AlbumInfo]:\n        \"\"\"Returns a list of AlbumInfo objects for a beatport search query.\"\"\"\n        # Strip non-word characters from query. Things like \"!\" and \"-\" can\n        # cause a query to return no results, even if they match the artist or\n        # album title. Use `re.UNICODE` flag to avoid stripping non-english\n        # word characters.\n        query = re.sub(r\"\\W+\", \" \", query, flags=re.UNICODE)\n        # Strip medium information from query, Things like \"CD1\" and \"disk 1\"\n        # can also negate an otherwise positive result.\n        query = re.sub(r\"\\b(CD|disc)\\s*\\d+\", \"\", query, flags=re.I)\n        for beatport_release in self.client.search(query, \"release\"):\n            if beatport_release is None:\n                continue\n            yield self._get_album_info(beatport_release)\n\n    def _get_album_info(self, release: BeatportRelease) -> AlbumInfo:\n        \"\"\"Returns an AlbumInfo object for a Beatport Release object.\"\"\"\n        va = release.artists is not None and len(release.artists) > 3\n        artist, artist_id = self._get_artist(release.artists)\n        if va:\n            artist = config[\"va_name\"].as_str()\n        tracks: list[TrackInfo] = []\n        if release.tracks is not None:\n            tracks = [self._get_track_info(x) for x in release.tracks]\n\n        release_date = release.release_date\n\n        return AlbumInfo(\n            album=release.name,\n            album_id=release.beatport_id,\n            beatport_album_id=release.beatport_id,\n            artist=artist,\n            artist_id=artist_id,\n            tracks=tracks,\n            albumtype=release.category,\n            va=va,\n            label=release.label_name,\n            catalognum=release.catalog_number,\n            media=\"Digital\",\n            data_source=self.data_source,\n            data_url=release.url,\n            genres=release.genres,\n            year=release_date.year if release_date else None,\n            month=release_date.month if release_date else None,\n            day=release_date.day if release_date else None,\n        )\n\n    def _get_track_info(self, track: BeatportTrack) -> TrackInfo:\n        \"\"\"Returns a TrackInfo object for a Beatport Track object.\"\"\"\n        title = track.name\n        if track.mix_name != \"Original Mix\":\n            title += f\" ({track.mix_name})\"\n        artist, artist_id = self._get_artist(track.artists)\n        length = track.length.total_seconds()\n        return TrackInfo(\n            title=title,\n            track_id=track.beatport_id,\n            artist=artist,\n            artist_id=artist_id,\n            length=length,\n            index=track.track_number,\n            medium_index=track.track_number,\n            data_source=self.data_source,\n            data_url=track.url,\n            bpm=track.bpm,\n            initial_key=track.initial_key,\n            genres=track.genres,\n        )\n\n    def _get_artist(self, artists):\n        \"\"\"Returns an artist string (all artists) and an artist_id (the main\n        artist) for a list of Beatport release or track artists.\n        \"\"\"\n        return self.get_artist(artists=artists, id_key=0, name_key=1)\n\n    def _get_tracks(self, query):\n        \"\"\"Returns a list of TrackInfo objects for a Beatport query.\"\"\"\n        bp_tracks = self.client.search(query, release_type=\"track\")\n        tracks = [self._get_track_info(x) for x in bp_tracks]\n        return tracks\n"
  },
  {
    "path": "beetsplug/bench.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Some simple performance benchmarks for beets.\"\"\"\n\nimport cProfile\nimport timeit\n\nfrom beets import importer, library, plugins, ui\nfrom beets.autotag import match\nfrom beets.plugins import BeetsPlugin\nfrom beets.util.functemplate import Template\nfrom beetsplug._utils import vfs\n\n\ndef aunique_benchmark(lib, prof):\n    def _build_tree():\n        vfs.libtree(lib)\n\n    # Measure path generation performance with %aunique{} included.\n    lib.path_formats = [\n        (\n            library.PF_KEY_DEFAULT,\n            Template(\"$albumartist/$album%aunique{}/$track $title\"),\n        ),\n    ]\n    if prof:\n        cProfile.runctx(\n            \"_build_tree()\",\n            {},\n            {\"_build_tree\": _build_tree},\n            \"paths.withaunique.prof\",\n        )\n    else:\n        interval = timeit.timeit(_build_tree, number=1)\n        print(\"With %aunique:\", interval)\n\n    # And with %aunique replaceed with a \"cheap\" no-op function.\n    lib.path_formats = [\n        (\n            library.PF_KEY_DEFAULT,\n            Template(\"$albumartist/$album%lower{}/$track $title\"),\n        ),\n    ]\n    if prof:\n        cProfile.runctx(\n            \"_build_tree()\",\n            {},\n            {\"_build_tree\": _build_tree},\n            \"paths.withoutaunique.prof\",\n        )\n    else:\n        interval = timeit.timeit(_build_tree, number=1)\n        print(\"Without %aunique:\", interval)\n\n\ndef match_benchmark(lib, prof, query=None, album_id=None):\n    # If no album ID is provided, we'll match against a suitably huge\n    # album.\n    if not album_id:\n        album_id = \"9c5c043e-bc69-4edb-81a4-1aaf9c81e6dc\"\n\n    # Get an album from the library to use as the source for the match.\n    items = lib.albums(query).get().items()\n\n    # Ensure fingerprinting is invoked (if enabled).\n    plugins.send(\n        \"import_task_start\",\n        task=importer.ImportTask(None, None, items),\n        session=importer.ImportSession(lib, None, None, None),\n    )\n\n    # Run the match.\n    def _run_match():\n        match.tag_album(items, search_ids=[album_id])\n\n    if prof:\n        cProfile.runctx(\n            \"_run_match()\", {}, {\"_run_match\": _run_match}, \"match.prof\"\n        )\n    else:\n        interval = timeit.timeit(_run_match, number=1)\n        print(\"match duration:\", interval)\n\n\nclass BenchmarkPlugin(BeetsPlugin):\n    \"\"\"A plugin for performing some simple performance benchmarks.\"\"\"\n\n    def commands(self):\n        aunique_bench_cmd = ui.Subcommand(\n            \"bench_aunique\", help=\"benchmark for %aunique{}\"\n        )\n        aunique_bench_cmd.parser.add_option(\n            \"-p\",\n            \"--profile\",\n            action=\"store_true\",\n            default=False,\n            help=\"performance profiling\",\n        )\n        aunique_bench_cmd.func = lambda lib, opts, args: aunique_benchmark(\n            lib, opts.profile\n        )\n\n        match_bench_cmd = ui.Subcommand(\n            \"bench_match\", help=\"benchmark for track matching\"\n        )\n        match_bench_cmd.parser.add_option(\n            \"-p\",\n            \"--profile\",\n            action=\"store_true\",\n            default=False,\n            help=\"performance profiling\",\n        )\n        match_bench_cmd.parser.add_option(\n            \"-i\", \"--id\", default=None, help=\"album ID to match against\"\n        )\n        match_bench_cmd.func = lambda lib, opts, args: match_benchmark(\n            lib, opts.profile, args, opts.id\n        )\n\n        return [aunique_bench_cmd, match_bench_cmd]\n"
  },
  {
    "path": "beetsplug/bpd/__init__.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"A clone of the Music Player Daemon (MPD) that plays music from a\nBeets library. Attempts to implement a compatible protocol to allow\nuse of the wide range of MPD clients.\n\"\"\"\n\nimport inspect\nimport math\nimport random\nimport re\nimport socket\nimport sys\nimport time\nimport traceback\nfrom string import Template\nfrom typing import TYPE_CHECKING, ClassVar\n\nimport beets\nimport beets.ui\nfrom beets import dbcore\nfrom beets.library import Item\nfrom beets.plugins import BeetsPlugin\nfrom beets.util import as_string, bluelet\nfrom beetsplug._utils import vfs\n\nif TYPE_CHECKING:\n    from beets.dbcore.query import Query\n\n\ntry:\n    from . import gstplayer\nexcept ImportError as e:\n    raise ImportError(\n        \"Gstreamer Python bindings not found.\"\n        ' Install \"gstreamer1.0\" and \"python-gi\" or similar package to use BPD.'\n    ) from e\n\nPROTOCOL_VERSION = \"0.16.0\"\nBUFSIZE = 1024\n\nHELLO = f\"OK MPD {PROTOCOL_VERSION}\"\nCLIST_BEGIN = \"command_list_begin\"\nCLIST_VERBOSE_BEGIN = \"command_list_ok_begin\"\nCLIST_END = \"command_list_end\"\nRESP_OK = \"OK\"\nRESP_CLIST_VERBOSE = \"list_OK\"\nRESP_ERR = \"ACK\"\n\nNEWLINE = \"\\n\"\n\nERROR_NOT_LIST = 1\nERROR_ARG = 2\nERROR_PASSWORD = 3\nERROR_PERMISSION = 4\nERROR_UNKNOWN = 5\nERROR_NO_EXIST = 50\nERROR_PLAYLIST_MAX = 51\nERROR_SYSTEM = 52\nERROR_PLAYLIST_LOAD = 53\nERROR_UPDATE_ALREADY = 54\nERROR_PLAYER_SYNC = 55\nERROR_EXIST = 56\n\nVOLUME_MIN = 0\nVOLUME_MAX = 100\n\nSAFE_COMMANDS = (\n    # Commands that are available when unauthenticated.\n    \"close\",\n    \"commands\",\n    \"notcommands\",\n    \"password\",\n    \"ping\",\n)\n\n# List of subsystems/events used by the `idle` command.\nSUBSYSTEMS = [\n    \"update\",\n    \"player\",\n    \"mixer\",\n    \"options\",\n    \"playlist\",\n    \"database\",\n    # Related to unsupported commands:\n    \"stored_playlist\",\n    \"output\",\n    \"subscription\",\n    \"sticker\",\n    \"message\",\n    \"partition\",\n]\n\n\n# Error-handling, exceptions, parameter parsing.\n\n\nclass BPDError(Exception):\n    \"\"\"An error that should be exposed to the client to the BPD\n    server.\n    \"\"\"\n\n    def __init__(self, code, message, cmd_name=\"\", index=0):\n        self.code = code\n        self.message = message\n        self.cmd_name = cmd_name\n        self.index = index\n\n    template = Template(\"$resp [$code@$index] {$cmd_name} $message\")\n\n    def response(self):\n        \"\"\"Returns a string to be used as the response code for the\n        erring command.\n        \"\"\"\n        return self.template.substitute(\n            {\n                \"resp\": RESP_ERR,\n                \"code\": self.code,\n                \"index\": self.index,\n                \"cmd_name\": self.cmd_name,\n                \"message\": self.message,\n            }\n        )\n\n\ndef make_bpd_error(s_code, s_message):\n    \"\"\"Create a BPDError subclass for a static code and message.\"\"\"\n\n    class NewBPDError(BPDError):\n        code = s_code\n        message = s_message\n        cmd_name = \"\"\n        index = 0\n\n        def __init__(self):\n            pass\n\n    return NewBPDError\n\n\nArgumentTypeError = make_bpd_error(ERROR_ARG, \"invalid type for argument\")\nArgumentIndexError = make_bpd_error(ERROR_ARG, \"argument out of range\")\nArgumentNotFoundError = make_bpd_error(ERROR_NO_EXIST, \"argument not found\")\n\n\ndef cast_arg(t, val):\n    \"\"\"Attempts to call t on val, raising a ArgumentTypeError\n    on ValueError.\n\n    If 't' is the special string 'intbool', attempts to cast first\n    to an int and then to a bool (i.e., 1=True, 0=False).\n    \"\"\"\n    if t == \"intbool\":\n        return cast_arg(bool, cast_arg(int, val))\n    else:\n        try:\n            return t(val)\n        except ValueError:\n            raise ArgumentTypeError()\n\n\nclass BPDCloseError(Exception):\n    \"\"\"Raised by a command invocation to indicate that the connection\n    should be closed.\n    \"\"\"\n\n\nclass BPDIdleError(Exception):\n    \"\"\"Raised by a command to indicate the client wants to enter the idle state\n    and should be notified when a relevant event happens.\n    \"\"\"\n\n    def __init__(self, subsystems):\n        super().__init__()\n        self.subsystems = set(subsystems)\n\n\n# Generic server infrastructure, implementing the basic protocol.\n\n\nclass BaseServer:\n    \"\"\"A MPD-compatible music player server.\n\n    The functions with the `cmd_` prefix are invoked in response to\n    client commands. For instance, if the client says `status`,\n    `cmd_status` will be invoked. The arguments to the client's commands\n    are used as function arguments following the connection issuing the\n    command. The functions may send data on the connection. They may\n    also raise BPDError exceptions to report errors.\n\n    This is a generic superclass and doesn't support many commands.\n    \"\"\"\n\n    def __init__(self, host, port, password, ctrl_port, log, ctrl_host=None):\n        \"\"\"Create a new server bound to address `host` and listening\n        on port `port`. If `password` is given, it is required to do\n        anything significant on the server.\n        A separate control socket is established listening to `ctrl_host` on\n        port `ctrl_port` which is used to forward notifications from the player\n        and can be sent debug commands (e.g. using netcat).\n        \"\"\"\n        self.host, self.port, self.password = host, port, password\n        self.ctrl_host, self.ctrl_port = ctrl_host or host, ctrl_port\n        self.ctrl_sock = None\n        self._log = log\n\n        # Default server values.\n        self.random = False\n        self.repeat = False\n        self.consume = False\n        self.single = False\n        self.volume = VOLUME_MAX\n        self.crossfade = 0\n        self.mixrampdb = 0.0\n        self.mixrampdelay = float(\"nan\")\n        self.replay_gain_mode = \"off\"\n        self.playlist = []\n        self.playlist_version = 0\n        self.current_index = -1\n        self.paused = False\n        self.error = None\n\n        # Current connections\n        self.connections = set()\n\n        # Object for random numbers generation\n        self.random_obj = random.Random()\n\n    def connect(self, conn):\n        \"\"\"A new client has connected.\"\"\"\n        self.connections.add(conn)\n\n    def disconnect(self, conn):\n        \"\"\"Client has disconnected; clean up residual state.\"\"\"\n        self.connections.remove(conn)\n\n    def run(self):\n        \"\"\"Block and start listening for connections from clients. An\n        interrupt (^C) closes the server.\n        \"\"\"\n        self.startup_time = time.time()\n\n        def start():\n            yield bluelet.spawn(\n                bluelet.server(\n                    self.ctrl_host,\n                    self.ctrl_port,\n                    ControlConnection.handler(self),\n                )\n            )\n            yield bluelet.server(\n                self.host, self.port, MPDConnection.handler(self)\n            )\n\n        bluelet.run(start())\n\n    def dispatch_events(self):\n        \"\"\"If any clients have idle events ready, send them.\"\"\"\n        # We need a copy of `self.connections` here since clients might\n        # disconnect once we try and send to them, changing `self.connections`.\n        for conn in list(self.connections):\n            yield bluelet.spawn(conn.send_notifications())\n\n    def _ctrl_send(self, message):\n        \"\"\"Send some data over the control socket.\n        If it's our first time, open the socket. The message should be a\n        string without a terminal newline.\n        \"\"\"\n        if not self.ctrl_sock:\n            self.ctrl_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n            self.ctrl_sock.connect((self.ctrl_host, self.ctrl_port))\n        self.ctrl_sock.sendall((f\"{message}\\n\").encode())\n\n    def _send_event(self, event):\n        \"\"\"Notify subscribed connections of an event.\"\"\"\n        for conn in self.connections:\n            conn.notify(event)\n\n    def _item_info(self, item):\n        \"\"\"An abstract method that should response lines containing a\n        single song's metadata.\n        \"\"\"\n        raise NotImplementedError\n\n    def _item_id(self, item):\n        \"\"\"An abstract method returning the integer id for an item.\"\"\"\n        raise NotImplementedError\n\n    def _id_to_index(self, track_id):\n        \"\"\"Searches the playlist for a song with the given id and\n        returns its index in the playlist.\n        \"\"\"\n        track_id = cast_arg(int, track_id)\n        for index, track in enumerate(self.playlist):\n            if self._item_id(track) == track_id:\n                return index\n        # Loop finished with no track found.\n        raise ArgumentNotFoundError()\n\n    def _random_idx(self):\n        \"\"\"Returns a random index different from the current one.\n        If there are no songs in the playlist it returns -1.\n        If there is only one song in the playlist it returns 0.\n        \"\"\"\n        if len(self.playlist) < 2:\n            return len(self.playlist) - 1\n        new_index = self.random_obj.randint(0, len(self.playlist) - 1)\n        while new_index == self.current_index:\n            new_index = self.random_obj.randint(0, len(self.playlist) - 1)\n        return new_index\n\n    def _succ_idx(self):\n        \"\"\"Returns the index for the next song to play.\n        It also considers random, single and repeat flags.\n        No boundaries are checked.\n        \"\"\"\n        if self.repeat and self.single:\n            return self.current_index\n        if self.random:\n            return self._random_idx()\n        return self.current_index + 1\n\n    def _prev_idx(self):\n        \"\"\"Returns the index for the previous song to play.\n        It also considers random and repeat flags.\n        No boundaries are checked.\n        \"\"\"\n        if self.repeat and self.single:\n            return self.current_index\n        if self.random:\n            return self._random_idx()\n        return self.current_index - 1\n\n    def cmd_ping(self, conn):\n        \"\"\"Succeeds.\"\"\"\n        pass\n\n    def cmd_idle(self, conn, *subsystems):\n        subsystems = subsystems or SUBSYSTEMS\n        for system in subsystems:\n            if system not in SUBSYSTEMS:\n                raise BPDError(ERROR_ARG, f\"Unrecognised idle event: {system}\")\n        raise BPDIdleError(subsystems)  # put the connection into idle mode\n\n    def cmd_kill(self, conn):\n        \"\"\"Exits the server process.\"\"\"\n        sys.exit(0)\n\n    def cmd_close(self, conn):\n        \"\"\"Closes the connection.\"\"\"\n        raise BPDCloseError()\n\n    def cmd_password(self, conn, password):\n        \"\"\"Attempts password authentication.\"\"\"\n        if password == self.password:\n            conn.authenticated = True\n        else:\n            conn.authenticated = False\n            raise BPDError(ERROR_PASSWORD, \"incorrect password\")\n\n    def cmd_commands(self, conn):\n        \"\"\"Lists the commands available to the user.\"\"\"\n        if self.password and not conn.authenticated:\n            # Not authenticated. Show limited list of commands.\n            for cmd in SAFE_COMMANDS:\n                yield f\"command: {cmd}\"\n\n        else:\n            # Authenticated. Show all commands.\n            for func in dir(self):\n                if func.startswith(\"cmd_\"):\n                    yield f\"command: {func[4:]}\"\n\n    def cmd_notcommands(self, conn):\n        \"\"\"Lists all unavailable commands.\"\"\"\n        if self.password and not conn.authenticated:\n            # Not authenticated. Show privileged commands.\n            for func in dir(self):\n                if func.startswith(\"cmd_\"):\n                    cmd = func[4:]\n                    if cmd not in SAFE_COMMANDS:\n                        yield f\"command: {cmd}\"\n\n        else:\n            # Authenticated. No commands are unavailable.\n            pass\n\n    def cmd_status(self, conn):\n        \"\"\"Returns some status information for use with an\n        implementation of cmd_status.\n\n        Gives a list of response-lines for: volume, repeat, random,\n        playlist, playlistlength, and xfade.\n        \"\"\"\n        yield (\n            f\"repeat: {int(self.repeat)}\",\n            f\"random: {int(self.random)}\",\n            f\"consume: {int(self.consume)}\",\n            f\"single: {int(self.single)}\",\n            f\"playlist: {self.playlist_version}\",\n            f\"playlistlength: {len(self.playlist)}\",\n            f\"mixrampdb: {self.mixrampdb}\",\n        )\n\n        if self.volume > 0:\n            yield f\"volume: {self.volume}\"\n\n        if not math.isnan(self.mixrampdelay):\n            yield f\"mixrampdelay: {self.mixrampdelay}\"\n        if self.crossfade > 0:\n            yield f\"xfade: {self.crossfade}\"\n\n        if self.current_index == -1:\n            state = \"stop\"\n        elif self.paused:\n            state = \"pause\"\n        else:\n            state = \"play\"\n        yield f\"state: {state}\"\n\n        if self.current_index != -1:  # i.e., paused or playing\n            current_id = self._item_id(self.playlist[self.current_index])\n            yield f\"song: {self.current_index}\"\n            yield f\"songid: {current_id}\"\n            if len(self.playlist) > self.current_index + 1:\n                # If there's a next song, report its index too.\n                next_id = self._item_id(self.playlist[self.current_index + 1])\n                yield f\"nextsong: {self.current_index + 1}\"\n                yield f\"nextsongid: {next_id}\"\n\n        if self.error:\n            yield f\"error: {self.error}\"\n\n    def cmd_clearerror(self, conn):\n        \"\"\"Removes the persistent error state of the server. This\n        error is set when a problem arises not in response to a\n        command (for instance, when playing a file).\n        \"\"\"\n        self.error = None\n\n    def cmd_random(self, conn, state):\n        \"\"\"Set or unset random (shuffle) mode.\"\"\"\n        self.random = cast_arg(\"intbool\", state)\n        self._send_event(\"options\")\n\n    def cmd_repeat(self, conn, state):\n        \"\"\"Set or unset repeat mode.\"\"\"\n        self.repeat = cast_arg(\"intbool\", state)\n        self._send_event(\"options\")\n\n    def cmd_consume(self, conn, state):\n        \"\"\"Set or unset consume mode.\"\"\"\n        self.consume = cast_arg(\"intbool\", state)\n        self._send_event(\"options\")\n\n    def cmd_single(self, conn, state):\n        \"\"\"Set or unset single mode.\"\"\"\n        # TODO support oneshot in addition to 0 and 1 [MPD 0.20]\n        self.single = cast_arg(\"intbool\", state)\n        self._send_event(\"options\")\n\n    def cmd_setvol(self, conn, vol):\n        \"\"\"Set the player's volume level (0-100).\"\"\"\n        vol = cast_arg(int, vol)\n        if vol < VOLUME_MIN or vol > VOLUME_MAX:\n            raise BPDError(ERROR_ARG, \"volume out of range\")\n        self.volume = vol\n        self._send_event(\"mixer\")\n\n    def cmd_volume(self, conn, vol_delta):\n        \"\"\"Deprecated command to change the volume by a relative amount.\"\"\"\n        vol_delta = cast_arg(int, vol_delta)\n        return self.cmd_setvol(conn, self.volume + vol_delta)\n\n    def cmd_crossfade(self, conn, crossfade):\n        \"\"\"Set the number of seconds of crossfading.\"\"\"\n        crossfade = cast_arg(int, crossfade)\n        if crossfade < 0:\n            raise BPDError(ERROR_ARG, \"crossfade time must be nonnegative\")\n        self._log.warning(\"crossfade is not implemented in bpd\")\n        self.crossfade = crossfade\n        self._send_event(\"options\")\n\n    def cmd_mixrampdb(self, conn, db):\n        \"\"\"Set the mixramp normalised max volume in dB.\"\"\"\n        db = cast_arg(float, db)\n        if db > 0:\n            raise BPDError(ERROR_ARG, \"mixrampdb time must be negative\")\n        self._log.warning(\"mixramp is not implemented in bpd\")\n        self.mixrampdb = db\n        self._send_event(\"options\")\n\n    def cmd_mixrampdelay(self, conn, delay):\n        \"\"\"Set the mixramp delay in seconds.\"\"\"\n        delay = cast_arg(float, delay)\n        if delay < 0:\n            raise BPDError(ERROR_ARG, \"mixrampdelay time must be nonnegative\")\n        self._log.warning(\"mixramp is not implemented in bpd\")\n        self.mixrampdelay = delay\n        self._send_event(\"options\")\n\n    def cmd_replay_gain_mode(self, conn, mode):\n        \"\"\"Set the replay gain mode.\"\"\"\n        if mode not in [\"off\", \"track\", \"album\", \"auto\"]:\n            raise BPDError(ERROR_ARG, \"Unrecognised replay gain mode\")\n        self._log.warning(\"replay gain is not implemented in bpd\")\n        self.replay_gain_mode = mode\n        self._send_event(\"options\")\n\n    def cmd_replay_gain_status(self, conn):\n        \"\"\"Get the replaygain mode.\"\"\"\n        yield f\"replay_gain_mode: {self.replay_gain_mode}\"\n\n    def cmd_clear(self, conn):\n        \"\"\"Clear the playlist.\"\"\"\n        self.playlist = []\n        self.playlist_version += 1\n        self.cmd_stop(conn)\n        self._send_event(\"playlist\")\n\n    def cmd_delete(self, conn, index):\n        \"\"\"Remove the song at index from the playlist.\"\"\"\n        index = cast_arg(int, index)\n        try:\n            del self.playlist[index]\n        except IndexError:\n            raise ArgumentIndexError()\n        self.playlist_version += 1\n\n        if self.current_index == index:  # Deleted playing song.\n            self.cmd_stop(conn)\n        elif index < self.current_index:  # Deleted before playing.\n            # Shift playing index down.\n            self.current_index -= 1\n        self._send_event(\"playlist\")\n\n    def cmd_deleteid(self, conn, track_id):\n        self.cmd_delete(conn, self._id_to_index(track_id))\n\n    def cmd_move(self, conn, idx_from, idx_to):\n        \"\"\"Move a track in the playlist.\"\"\"\n        idx_from = cast_arg(int, idx_from)\n        idx_to = cast_arg(int, idx_to)\n        try:\n            track = self.playlist.pop(idx_from)\n            self.playlist.insert(idx_to, track)\n        except IndexError:\n            raise ArgumentIndexError()\n\n        # Update currently-playing song.\n        if idx_from == self.current_index:\n            self.current_index = idx_to\n        elif idx_from < self.current_index <= idx_to:\n            self.current_index -= 1\n        elif idx_from > self.current_index >= idx_to:\n            self.current_index += 1\n\n        self.playlist_version += 1\n        self._send_event(\"playlist\")\n\n    def cmd_moveid(self, conn, idx_from, idx_to):\n        idx_from = self._id_to_index(idx_from)\n        return self.cmd_move(conn, idx_from, idx_to)\n\n    def cmd_swap(self, conn, i, j):\n        \"\"\"Swaps two tracks in the playlist.\"\"\"\n        i = cast_arg(int, i)\n        j = cast_arg(int, j)\n        try:\n            track_i = self.playlist[i]\n            track_j = self.playlist[j]\n        except IndexError:\n            raise ArgumentIndexError()\n\n        self.playlist[j] = track_i\n        self.playlist[i] = track_j\n\n        # Update currently-playing song.\n        if self.current_index == i:\n            self.current_index = j\n        elif self.current_index == j:\n            self.current_index = i\n\n        self.playlist_version += 1\n        self._send_event(\"playlist\")\n\n    def cmd_swapid(self, conn, i_id, j_id):\n        i = self._id_to_index(i_id)\n        j = self._id_to_index(j_id)\n        return self.cmd_swap(conn, i, j)\n\n    def cmd_urlhandlers(self, conn):\n        \"\"\"Indicates supported URL schemes. None by default.\"\"\"\n        pass\n\n    def cmd_playlistinfo(self, conn, index=None):\n        \"\"\"Gives metadata information about the entire playlist or a\n        single track, given by its index.\n        \"\"\"\n        if index is None:\n            for track in self.playlist:\n                yield self._item_info(track)\n        else:\n            indices = self._parse_range(index, accept_single_number=True)\n            try:\n                tracks = [self.playlist[i] for i in indices]\n            except IndexError:\n                raise ArgumentIndexError()\n            for track in tracks:\n                yield self._item_info(track)\n\n    def cmd_playlistid(self, conn, track_id=None):\n        if track_id is not None:\n            track_id = cast_arg(int, track_id)\n            track_id = self._id_to_index(track_id)\n        return self.cmd_playlistinfo(conn, track_id)\n\n    def cmd_plchanges(self, conn, version):\n        \"\"\"Sends playlist changes since the given version.\n\n        This is a \"fake\" implementation that ignores the version and\n        just returns the entire playlist (rather like version=0). This\n        seems to satisfy many clients.\n        \"\"\"\n        return self.cmd_playlistinfo(conn)\n\n    def cmd_plchangesposid(self, conn, version):\n        \"\"\"Like plchanges, but only sends position and id.\n\n        Also a dummy implementation.\n        \"\"\"\n        for idx, track in enumerate(self.playlist):\n            yield f\"cpos: {idx}\"\n            yield f\"Id: {track.id}\"\n\n    def cmd_currentsong(self, conn):\n        \"\"\"Sends information about the currently-playing song.\"\"\"\n        if self.current_index != -1:  # -1 means stopped.\n            track = self.playlist[self.current_index]\n            yield self._item_info(track)\n\n    def cmd_next(self, conn):\n        \"\"\"Advance to the next song in the playlist.\"\"\"\n        old_index = self.current_index\n        self.current_index = self._succ_idx()\n        if self.consume:\n            # TODO how does consume interact with single+repeat?\n            self.playlist.pop(old_index)\n            if self.current_index > old_index:\n                self.current_index -= 1\n            self.playlist_version += 1\n            self._send_event(\"playlist\")\n        if self.current_index >= len(self.playlist):\n            # Fallen off the end. Move to stopped state or loop.\n            if self.repeat:\n                self.current_index = -1\n                return self.cmd_play(conn)\n            return self.cmd_stop(conn)\n        elif self.single and not self.repeat:\n            return self.cmd_stop(conn)\n        else:\n            return self.cmd_play(conn)\n\n    def cmd_previous(self, conn):\n        \"\"\"Step back to the last song.\"\"\"\n        old_index = self.current_index\n        self.current_index = self._prev_idx()\n        if self.consume:\n            self.playlist.pop(old_index)\n        if self.current_index < 0:\n            if self.repeat:\n                self.current_index = len(self.playlist) - 1\n            else:\n                self.current_index = 0\n        return self.cmd_play(conn)\n\n    def cmd_pause(self, conn, state=None):\n        \"\"\"Set the pause state playback.\"\"\"\n        if state is None:\n            self.paused = not self.paused  # Toggle.\n        else:\n            self.paused = cast_arg(\"intbool\", state)\n        self._send_event(\"player\")\n\n    def cmd_play(self, conn, index=-1):\n        \"\"\"Begin playback, possibly at a specified playlist index.\"\"\"\n        index = cast_arg(int, index)\n\n        if index < -1 or index >= len(self.playlist):\n            raise ArgumentIndexError()\n\n        if index == -1:  # No index specified: start where we are.\n            if not self.playlist:  # Empty playlist: stop immediately.\n                return self.cmd_stop(conn)\n            if self.current_index == -1:  # No current song.\n                self.current_index = 0  # Start at the beginning.\n            # If we have a current song, just stay there.\n\n        else:  # Start with the specified index.\n            self.current_index = index\n\n        self.paused = False\n        self._send_event(\"player\")\n\n    def cmd_playid(self, conn, track_id=0):\n        track_id = cast_arg(int, track_id)\n        if track_id == -1:\n            index = -1\n        else:\n            index = self._id_to_index(track_id)\n        return self.cmd_play(conn, index)\n\n    def cmd_stop(self, conn):\n        \"\"\"Stop playback.\"\"\"\n        self.current_index = -1\n        self.paused = False\n        self._send_event(\"player\")\n\n    def cmd_seek(self, conn, index, pos):\n        \"\"\"Seek to a specified point in a specified song.\"\"\"\n        index = cast_arg(int, index)\n        if index < 0 or index >= len(self.playlist):\n            raise ArgumentIndexError()\n        self.current_index = index\n        self._send_event(\"player\")\n\n    def cmd_seekid(self, conn, track_id, pos):\n        index = self._id_to_index(track_id)\n        return self.cmd_seek(conn, index, pos)\n\n    # Additions to the MPD protocol.\n\n    def cmd_crash(self, conn):\n        \"\"\"Deliberately trigger a TypeError for testing purposes.\n        We want to test that the server properly responds with ERROR_SYSTEM\n        without crashing, and that this is not treated as ERROR_ARG (since it\n        is caused by a programming error, not a protocol error).\n        \"\"\"\n        raise TypeError\n\n\nclass Connection:\n    \"\"\"A connection between a client and the server.\"\"\"\n\n    def __init__(self, server, sock):\n        \"\"\"Create a new connection for the accepted socket `client`.\"\"\"\n        self.server = server\n        self.sock = sock\n        self.address = \":\".join(map(str, sock.sock.getpeername()))\n\n    def debug(self, message, kind=\" \"):\n        \"\"\"Log a debug message about this connection.\"\"\"\n        self.server._log.debug(\"{}[{.address}]: {}\", kind, self, message)\n\n    def run(self):\n        pass\n\n    def send(self, lines):\n        \"\"\"Send lines, which which is either a single string or an\n        iterable consisting of strings, to the client. A newline is\n        added after every string. Returns a Bluelet event that sends\n        the data.\n        \"\"\"\n        if isinstance(lines, str):\n            lines = [lines]\n        out = NEWLINE.join(lines) + NEWLINE\n        for line in out.split(NEWLINE)[:-1]:\n            self.debug(line, kind=\">\")\n        if isinstance(out, str):\n            out = out.encode(\"utf-8\")\n        return self.sock.sendall(out)\n\n    @classmethod\n    def handler(cls, server):\n        def _handle(sock):\n            \"\"\"Creates a new `Connection` and runs it.\"\"\"\n            return cls(server, sock).run()\n\n        return _handle\n\n\nclass MPDConnection(Connection):\n    \"\"\"A connection that receives commands from an MPD-compatible client.\"\"\"\n\n    def __init__(self, server, sock):\n        \"\"\"Create a new connection for the accepted socket `client`.\"\"\"\n        super().__init__(server, sock)\n        self.authenticated = False\n        self.notifications = set()\n        self.idle_subscriptions = set()\n\n    def do_command(self, command):\n        \"\"\"A coroutine that runs the given command and sends an\n        appropriate response.\"\"\"\n        try:\n            yield bluelet.call(command.run(self))\n        except BPDError as e:\n            # Send the error.\n            yield self.send(e.response())\n        else:\n            # Send success code.\n            yield self.send(RESP_OK)\n\n    def disconnect(self):\n        \"\"\"The connection has closed for any reason.\"\"\"\n        self.server.disconnect(self)\n        self.debug(\"disconnected\", kind=\"*\")\n\n    def notify(self, event):\n        \"\"\"Queue up an event for sending to this client.\"\"\"\n        self.notifications.add(event)\n\n    def send_notifications(self, force_close_idle=False):\n        \"\"\"Send the client any queued events now.\"\"\"\n        pending = self.notifications.intersection(self.idle_subscriptions)\n        try:\n            for event in pending:\n                yield self.send(f\"changed: {event}\")\n            if pending or force_close_idle:\n                self.idle_subscriptions = set()\n                self.notifications = self.notifications.difference(pending)\n                yield self.send(RESP_OK)\n        except bluelet.SocketClosedError:\n            self.disconnect()  # Client disappeared.\n\n    def run(self):\n        \"\"\"Send a greeting to the client and begin processing commands\n        as they arrive.\n        \"\"\"\n        self.debug(\"connected\", kind=\"*\")\n        self.server.connect(self)\n        yield self.send(HELLO)\n\n        clist = None  # Initially, no command list is being constructed.\n        while True:\n            line = yield self.sock.readline()\n            if not line:\n                self.disconnect()  # Client disappeared.\n                break\n            line = line.strip()\n            if not line:\n                err = BPDError(ERROR_UNKNOWN, \"No command given\")\n                yield self.send(err.response())\n                self.disconnect()  # Client sent a blank line.\n                break\n            line = line.decode(\"utf8\")  # MPD protocol uses UTF-8.\n            for line in line.split(NEWLINE):\n                self.debug(line, kind=\"<\")\n\n            if self.idle_subscriptions:\n                # The connection is in idle mode.\n                if line == \"noidle\":\n                    yield bluelet.call(self.send_notifications(True))\n                else:\n                    err = BPDError(\n                        ERROR_UNKNOWN, f\"Got command while idle: {line}\"\n                    )\n                    yield self.send(err.response())\n                    break\n                continue\n            if line == \"noidle\":\n                # When not in idle, this command sends no response.\n                continue\n\n            if clist is not None:\n                # Command list already opened.\n                if line == CLIST_END:\n                    yield bluelet.call(self.do_command(clist))\n                    clist = None  # Clear the command list.\n                    yield bluelet.call(self.server.dispatch_events())\n                else:\n                    clist.append(Command(line))\n\n            elif line == CLIST_BEGIN or line == CLIST_VERBOSE_BEGIN:\n                # Begin a command list.\n                clist = CommandList([], line == CLIST_VERBOSE_BEGIN)\n\n            else:\n                # Ordinary command.\n                try:\n                    yield bluelet.call(self.do_command(Command(line)))\n                except BPDCloseError:\n                    # Command indicates that the conn should close.\n                    self.sock.close()\n                    self.disconnect()  # Client explicitly closed.\n                    return\n                except BPDIdleError as e:\n                    self.idle_subscriptions = e.subsystems\n                    self.debug(f\"awaiting: {' '.join(e.subsystems)}\", kind=\"z\")\n                yield bluelet.call(self.server.dispatch_events())\n\n\nclass ControlConnection(Connection):\n    \"\"\"A connection used to control BPD for debugging and internal events.\"\"\"\n\n    def __init__(self, server, sock):\n        \"\"\"Create a new connection for the accepted socket `client`.\"\"\"\n        super().__init__(server, sock)\n\n    def debug(self, message, kind=\" \"):\n        self.server._log.debug(\"CTRL {}[{.address}]: {}\", kind, self, message)\n\n    def run(self):\n        \"\"\"Listen for control commands and delegate to `ctrl_*` methods.\"\"\"\n        self.debug(\"connected\", kind=\"*\")\n        while True:\n            line = yield self.sock.readline()\n            if not line:\n                break  # Client disappeared.\n            line = line.strip()\n            if not line:\n                break  # Client sent a blank line.\n            line = line.decode(\"utf8\")  # Protocol uses UTF-8.\n            for line in line.split(NEWLINE):\n                self.debug(line, kind=\"<\")\n            command = Command(line)\n            try:\n                func = command.delegate(\"ctrl_\", self)\n                yield bluelet.call(func(*command.args))\n            except (AttributeError, TypeError) as e:\n                yield self.send(f\"ERROR: {e.args[0]}\")\n            except Exception:\n                yield self.send(\n                    [\"ERROR: server error\", traceback.format_exc().rstrip()]\n                )\n\n    def ctrl_play_finished(self):\n        \"\"\"Callback from the player signalling a song finished playing.\"\"\"\n        yield bluelet.call(self.server.dispatch_events())\n\n    def ctrl_profile(self):\n        \"\"\"Memory profiling for debugging.\"\"\"\n        from guppy import hpy\n\n        heap = hpy().heap()\n        yield self.send(heap)\n\n    def ctrl_nickname(self, oldlabel, newlabel):\n        \"\"\"Rename a client in the log messages.\"\"\"\n        for c in self.server.connections:\n            if c.address == oldlabel:\n                c.address = newlabel\n                break\n        else:\n            yield self.send(f\"ERROR: no such client: {oldlabel}\")\n\n\nclass Command:\n    \"\"\"A command issued by the client for processing by the server.\"\"\"\n\n    command_re = re.compile(r\"^([^ \\t]+)[ \\t]*\")\n    arg_re = re.compile(r'\"((?:\\\\\"|[^\"])+)\"|([^ \\t\"]+)')\n\n    def __init__(self, s):\n        \"\"\"Creates a new `Command` from the given string, `s`, parsing\n        the string for command name and arguments.\n        \"\"\"\n        command_match = self.command_re.match(s)\n        self.name = command_match.group(1)\n\n        self.args = []\n        arg_matches = self.arg_re.findall(s[command_match.end() :])\n        for match in arg_matches:\n            if match[0]:\n                # Quoted argument.\n                arg = match[0]\n                arg = arg.replace('\\\\\"', '\"').replace(\"\\\\\\\\\", \"\\\\\")\n            else:\n                # Unquoted argument.\n                arg = match[1]\n            self.args.append(arg)\n\n    def delegate(self, prefix, target, extra_args=0):\n        \"\"\"Get the target method that corresponds to this command.\n        The `prefix` is prepended to the command name and then the resulting\n        name is used to search `target` for a method with a compatible number\n        of arguments.\n        \"\"\"\n        # Attempt to get correct command function.\n        func_name = f\"{prefix}{self.name}\"\n        if not hasattr(target, func_name):\n            raise AttributeError(f'unknown command \"{self.name}\"')\n        func = getattr(target, func_name)\n\n        argspec = inspect.getfullargspec(func)\n\n        # Check that `func` is able to handle the number of arguments sent\n        # by the client (so we can raise ERROR_ARG instead of ERROR_SYSTEM).\n        # Maximum accepted arguments: argspec includes \"self\".\n        max_args = len(argspec.args) - 1 - extra_args\n        # Minimum accepted arguments: some arguments might be optional.\n        min_args = max_args\n        if argspec.defaults:\n            min_args -= len(argspec.defaults)\n        wrong_num = (len(self.args) > max_args) or (len(self.args) < min_args)\n        # If the command accepts a variable number of arguments skip the check.\n        if wrong_num and not argspec.varargs:\n            raise TypeError(\n                f'wrong number of arguments for \"{self.name}\"',\n                self.name,\n            )\n\n        return func\n\n    def run(self, conn):\n        \"\"\"A coroutine that executes the command on the given\n        connection.\n        \"\"\"\n        try:\n            # `conn` is an extra argument to all cmd handlers.\n            func = self.delegate(\"cmd_\", conn.server, extra_args=1)\n        except AttributeError as e:\n            raise BPDError(ERROR_UNKNOWN, e.args[0])\n        except TypeError as e:\n            raise BPDError(ERROR_ARG, e.args[0], self.name)\n\n        # Ensure we have permission for this command.\n        if (\n            conn.server.password\n            and not conn.authenticated\n            and self.name not in SAFE_COMMANDS\n        ):\n            raise BPDError(ERROR_PERMISSION, \"insufficient privileges\")\n\n        try:\n            args = [conn, *self.args]\n            results = func(*args)\n            if results:\n                for data in results:\n                    yield conn.send(data)\n\n        except BPDError as e:\n            # An exposed error. Set the command name and then let\n            # the Connection handle it.\n            e.cmd_name = self.name\n            raise e\n\n        except BPDCloseError:\n            # An indication that the connection should close. Send\n            # it on the Connection.\n            raise\n\n        except BPDIdleError:\n            raise\n\n        except Exception:\n            # An \"unintentional\" error. Hide it from the client.\n            conn.server._log.error(\"{}\", traceback.format_exc())\n            raise BPDError(ERROR_SYSTEM, \"server error\", self.name)\n\n\nclass CommandList(list[Command]):\n    \"\"\"A list of commands issued by the client for processing by the\n    server. May be verbose, in which case the response is delimited, or\n    not. Should be a list of `Command` objects.\n    \"\"\"\n\n    def __init__(self, sequence=None, verbose=False):\n        \"\"\"Create a new `CommandList` from the given sequence of\n        `Command`s. If `verbose`, this is a verbose command list.\n        \"\"\"\n        if sequence:\n            for item in sequence:\n                self.append(item)\n        self.verbose = verbose\n\n    def run(self, conn):\n        \"\"\"Coroutine executing all the commands in this list.\"\"\"\n        for i, command in enumerate(self):\n            try:\n                yield bluelet.call(command.run(conn))\n            except BPDError as e:\n                # If the command failed, stop executing.\n                e.index = i  # Give the error the correct index.\n                raise e\n\n            # Otherwise, possibly send the output delimiter if we're in a\n            # verbose (\"OK\") command list.\n            if self.verbose:\n                yield conn.send(RESP_CLIST_VERBOSE)\n\n\n# A subclass of the basic, protocol-handling server that actually plays\n# music.\n\n\nclass Server(BaseServer):\n    \"\"\"An MPD-compatible server using GStreamer to play audio and beets\n    to store its library.\n    \"\"\"\n\n    def __init__(self, library, host, port, password, ctrl_port, log):\n        log.info(\"Starting server...\")\n        super().__init__(host, port, password, ctrl_port, log)\n        self.lib = library\n        self.player = gstplayer.GstPlayer(self.play_finished)\n        self.cmd_update(None)\n        log.info(\"Server ready and listening on {}:{}\", host, port)\n        log.debug(\"Listening for control signals on {}:{}\", host, ctrl_port)\n\n    def run(self):\n        self.player.run()\n        super().run()\n\n    def play_finished(self):\n        \"\"\"A callback invoked every time our player finishes a track.\"\"\"\n        self.cmd_next(None)\n        self._ctrl_send(\"play_finished\")\n\n    # Metadata helper functions.\n\n    def _item_info(self, item):\n        info_lines = [\n            f\"file: {as_string(item.destination(relative_to_libdir=True))}\",\n            f\"Time: {int(item.length)}\",\n            \"duration: {item.length:.3f}\",\n            f\"Id: {item.id}\",\n        ]\n\n        try:\n            pos = self._id_to_index(item.id)\n            info_lines.append(f\"Pos: {pos}\")\n        except ArgumentNotFoundError:\n            # Don't include position if not in playlist.\n            pass\n\n        for tagtype, field in self.tagtype_map.items():\n            field_value = getattr(item, field)\n            if isinstance(field_value, list):\n                field_value = \"; \".join(field_value)\n            info_lines.append(f\"{tagtype}: {field_value}\")\n\n        return info_lines\n\n    def _parse_range(self, items, accept_single_number=False):\n        \"\"\"Convert a range of positions to a list of item info.\n        MPD specifies ranges as START:STOP (endpoint excluded) for some\n        commands. Sometimes a single number can be provided instead.\n        \"\"\"\n        try:\n            start, stop = str(items).split(\":\", 1)\n        except ValueError:\n            if accept_single_number:\n                return [cast_arg(int, items)]\n            raise BPDError(ERROR_ARG, \"bad range syntax\")\n        start = cast_arg(int, start)\n        stop = cast_arg(int, stop)\n        return range(start, stop)\n\n    def _item_id(self, item):\n        return item.id\n\n    # Database updating.\n\n    def cmd_update(self, conn, path=\"/\"):\n        \"\"\"Updates the catalog to reflect the current database state.\"\"\"\n        # Path is ignored. Also, the real MPD does this asynchronously;\n        # this is done inline.\n        self._log.debug(\"Building directory tree...\")\n        self.tree = vfs.libtree(self.lib)\n        self._log.debug(\"Finished building directory tree.\")\n        self.updated_time = time.time()\n        self._send_event(\"update\")\n        self._send_event(\"database\")\n\n    # Path (directory tree) browsing.\n\n    def _resolve_path(self, path):\n        \"\"\"Returns a VFS node or an item ID located at the path given.\n        If the path does not exist, raises a\n        \"\"\"\n        components = path.split(\"/\")\n        node = self.tree\n\n        for component in components:\n            if not component:\n                continue\n\n            if isinstance(node, int):\n                # We're trying to descend into a file node.\n                raise ArgumentNotFoundError()\n\n            if component in node.files:\n                node = node.files[component]\n            elif component in node.dirs:\n                node = node.dirs[component]\n            else:\n                raise ArgumentNotFoundError()\n\n        return node\n\n    def _path_join(self, p1, p2):\n        \"\"\"Smashes together two BPD paths.\"\"\"\n        out = f\"{p1}/{p2}\"\n        return out.replace(\"//\", \"/\").replace(\"//\", \"/\")\n\n    def cmd_lsinfo(self, conn, path=\"/\"):\n        \"\"\"Sends info on all the items in the path.\"\"\"\n        node = self._resolve_path(path)\n        if isinstance(node, int):\n            # Trying to list a track.\n            raise BPDError(ERROR_ARG, \"this is not a directory\")\n        else:\n            for name, itemid in iter(sorted(node.files.items())):\n                item = self.lib.get_item(itemid)\n                yield self._item_info(item)\n            for name, _ in iter(sorted(node.dirs.items())):\n                dirpath = self._path_join(path, name)\n                if dirpath.startswith(\"/\"):\n                    # Strip leading slash (libmpc rejects this).\n                    dirpath = dirpath[1:]\n                yield f\"directory: {dirpath}\"\n\n    def _listall(self, basepath, node, info=False):\n        \"\"\"Helper function for recursive listing. If info, show\n        tracks' complete info; otherwise, just show items' paths.\n        \"\"\"\n        if isinstance(node, int):\n            # List a single file.\n            if info:\n                item = self.lib.get_item(node)\n                yield self._item_info(item)\n            else:\n                yield f\"file: {basepath}\"\n        else:\n            # List a directory. Recurse into both directories and files.\n            for name, itemid in sorted(node.files.items()):\n                newpath = self._path_join(basepath, name)\n                # \"yield from\"\n                yield from self._listall(newpath, itemid, info)\n            for name, subdir in sorted(node.dirs.items()):\n                newpath = self._path_join(basepath, name)\n                yield f\"directory: {newpath}\"\n                yield from self._listall(newpath, subdir, info)\n\n    def cmd_listall(self, conn, path=\"/\"):\n        \"\"\"Send the paths all items in the directory, recursively.\"\"\"\n        return self._listall(path, self._resolve_path(path), False)\n\n    def cmd_listallinfo(self, conn, path=\"/\"):\n        \"\"\"Send info on all the items in the directory, recursively.\"\"\"\n        return self._listall(path, self._resolve_path(path), True)\n\n    # Playlist manipulation.\n\n    def _all_items(self, node):\n        \"\"\"Generator yielding all items under a VFS node.\"\"\"\n        if isinstance(node, int):\n            # Could be more efficient if we built up all the IDs and\n            # then issued a single SELECT.\n            yield self.lib.get_item(node)\n        else:\n            # Recurse into a directory.\n            for name, itemid in sorted(node.files.items()):\n                # \"yield from\"\n                yield from self._all_items(itemid)\n            for name, subdir in sorted(node.dirs.items()):\n                yield from self._all_items(subdir)\n\n    def _add(self, path, send_id=False):\n        \"\"\"Adds a track or directory to the playlist, specified by the\n        path. If `send_id`, write each item's id to the client.\n        \"\"\"\n        for item in self._all_items(self._resolve_path(path)):\n            self.playlist.append(item)\n            if send_id:\n                yield f\"Id: {item.id}\"\n        self.playlist_version += 1\n        self._send_event(\"playlist\")\n\n    def cmd_add(self, conn, path):\n        \"\"\"Adds a track or directory to the playlist, specified by a\n        path.\n        \"\"\"\n        return self._add(path, False)\n\n    def cmd_addid(self, conn, path):\n        \"\"\"Same as `cmd_add` but sends an id back to the client.\"\"\"\n        return self._add(path, True)\n\n    # Server info.\n\n    def cmd_status(self, conn):\n        yield from super().cmd_status(conn)\n        if self.current_index > -1:\n            item = self.playlist[self.current_index]\n\n            yield (\n                f\"bitrate: {item.bitrate / 1000}\",\n                f\"audio: {item.samplerate}:{item.bitdepth}:{item.channels}\",\n            )\n\n            (pos, total) = self.player.time()\n            yield (\n                f\"time: {int(pos)}:{int(total)}\",\n                \"elapsed: \" + f\"{pos:.3f}\",\n                \"duration: \" + f\"{total:.3f}\",\n            )\n\n        # Also missing 'updating_db'.\n\n    def cmd_stats(self, conn):\n        \"\"\"Sends some statistics about the library.\"\"\"\n        with self.lib.transaction() as tx:\n            statement = (\n                \"SELECT COUNT(DISTINCT artist), \"\n                \"COUNT(DISTINCT album), \"\n                \"COUNT(id), \"\n                \"SUM(length) \"\n                \"FROM items\"\n            )\n            artists, albums, songs, totaltime = tx.query(statement)[0]\n\n        yield (\n            f\"artists: {artists}\",\n            f\"albums: {albums}\",\n            f\"songs: {songs}\",\n            f\"uptime: {int(time.time() - self.startup_time)}\",\n            \"playtime: 0\",  # Missing.\n            f\"db_playtime: {int(totaltime)}\",\n            f\"db_update: {int(self.updated_time)}\",\n        )\n\n    def cmd_decoders(self, conn):\n        \"\"\"Send list of supported decoders and formats.\"\"\"\n        decoders = self.player.get_decoders()\n        for name, (mimes, exts) in decoders.items():\n            yield f\"plugin: {name}\"\n            for ext in exts:\n                yield f\"suffix: {ext}\"\n            for mime in mimes:\n                yield f\"mime_type: {mime}\"\n\n    # Searching.\n\n    tagtype_map: ClassVar[dict[str, str]] = {\n        \"Artist\": \"artist\",\n        \"ArtistSort\": \"artist_sort\",\n        \"Album\": \"album\",\n        \"Title\": \"title\",\n        \"Track\": \"track\",\n        \"AlbumArtist\": \"albumartist\",\n        \"AlbumArtistSort\": \"albumartist_sort\",\n        \"Label\": \"label\",\n        \"Genre\": \"genres\",\n        \"Date\": \"year\",\n        \"OriginalDate\": \"original_year\",\n        \"Composer\": \"composer\",\n        \"Disc\": \"disc\",\n        \"Comment\": \"comments\",\n        \"MUSICBRAINZ_TRACKID\": \"mb_trackid\",\n        \"MUSICBRAINZ_ALBUMID\": \"mb_albumid\",\n        \"MUSICBRAINZ_ARTISTID\": \"mb_artistid\",\n        \"MUSICBRAINZ_ALBUMARTISTID\": \"mb_albumartistid\",\n        \"MUSICBRAINZ_RELEASETRACKID\": \"mb_releasetrackid\",\n    }\n\n    def cmd_tagtypes(self, conn):\n        \"\"\"Returns a list of the metadata (tag) fields available for\n        searching.\n        \"\"\"\n        for tag in self.tagtype_map:\n            yield f\"tagtype: {tag}\"\n\n    def _tagtype_lookup(self, tag):\n        \"\"\"Uses `tagtype_map` to look up the beets column name for an\n        MPD tagtype (or throw an appropriate exception). Returns both\n        the canonical name of the MPD tagtype and the beets column\n        name.\n        \"\"\"\n        for test_tag, key in self.tagtype_map.items():\n            # Match case-insensitively.\n            if test_tag.lower() == tag.lower():\n                return test_tag, key\n        raise BPDError(ERROR_UNKNOWN, \"no such tagtype\")\n\n    def _metadata_query(self, query_type, kv, allow_any_query: bool = False):\n        \"\"\"Helper function returns a query object that will find items\n        according to the library query type provided and the key-value\n        pairs specified. The any_query_type is used for queries of\n        type \"any\"; if None, then an error is thrown.\n        \"\"\"\n        if kv:  # At least one key-value pair.\n            queries: list[Query] = []\n            # Iterate pairwise over the arguments.\n            it = iter(kv)\n            for tag, value in zip(it, it):\n                if tag.lower() == \"any\":\n                    if allow_any_query:\n                        queries.append(\n                            Item.any_writable_media_field_query(\n                                query_type, value\n                            )\n                        )\n                    else:\n                        raise BPDError(ERROR_UNKNOWN, \"no such tagtype\")\n                else:\n                    _, key = self._tagtype_lookup(tag)\n                    queries.append(Item.field_query(key, value, query_type))\n            return dbcore.query.AndQuery(queries)\n        else:  # No key-value pairs.\n            return dbcore.query.TrueQuery()\n\n    def cmd_search(self, conn, *kv):\n        \"\"\"Perform a substring match for items.\"\"\"\n        query = self._metadata_query(\n            dbcore.query.SubstringQuery, kv, allow_any_query=True\n        )\n        for item in self.lib.items(query):\n            yield self._item_info(item)\n\n    def cmd_find(self, conn, *kv):\n        \"\"\"Perform an exact match for items.\"\"\"\n        query = self._metadata_query(dbcore.query.MatchQuery, kv)\n        for item in self.lib.items(query):\n            yield self._item_info(item)\n\n    def cmd_list(self, conn, show_tag, *kv):\n        \"\"\"List distinct metadata values for show_tag, possibly\n        filtered by matching match_tag to match_term.\n        \"\"\"\n        show_tag_canon, show_key = self._tagtype_lookup(show_tag)\n        if len(kv) == 1:\n            if show_tag_canon == \"Album\":\n                # If no tag was given, assume artist. This is because MPD\n                # supports a short version of this command for fetching the\n                # albums belonging to a particular artist, and some clients\n                # rely on this behaviour (e.g. MPDroid, M.A.L.P.).\n                kv = (\"Artist\", kv[0])\n            else:\n                raise BPDError(ERROR_ARG, 'should be \"Album\" for 3 arguments')\n        elif len(kv) % 2 != 0:\n            raise BPDError(ERROR_ARG, \"Incorrect number of filter arguments\")\n        query = self._metadata_query(dbcore.query.MatchQuery, kv)\n\n        clause, subvals = query.clause()\n        statement = (\n            f\"SELECT DISTINCT {show_key}\"\n            f\" FROM items WHERE {clause}\"\n            f\" ORDER BY {show_key}\"\n        )\n        self._log.debug(statement)\n        with self.lib.transaction() as tx:\n            rows = tx.query(statement, subvals)\n\n        for row in rows:\n            if not row[0]:\n                # Skip any empty values of the field.\n                continue\n            yield f\"{show_tag_canon}: {row[0]}\"\n\n    def cmd_count(self, conn, tag, value):\n        \"\"\"Returns the number and total time of songs matching the\n        tag/value query.\n        \"\"\"\n        _, key = self._tagtype_lookup(tag)\n        songs = 0\n        playtime = 0.0\n        for item in self.lib.items(\n            Item.field_query(key, value, dbcore.query.MatchQuery)\n        ):\n            songs += 1\n            playtime += item.length\n        yield f\"songs: {songs}\"\n        yield f\"playtime: {int(playtime)}\"\n\n    # Persistent playlist manipulation. In MPD this is an optional feature so\n    # these dummy implementations match MPD's behaviour with the feature off.\n\n    def cmd_listplaylist(self, conn, playlist):\n        raise BPDError(ERROR_NO_EXIST, \"No such playlist\")\n\n    def cmd_listplaylistinfo(self, conn, playlist):\n        raise BPDError(ERROR_NO_EXIST, \"No such playlist\")\n\n    def cmd_listplaylists(self, conn):\n        raise BPDError(ERROR_UNKNOWN, \"Stored playlists are disabled\")\n\n    def cmd_load(self, conn, playlist):\n        raise BPDError(ERROR_NO_EXIST, \"Stored playlists are disabled\")\n\n    def cmd_playlistadd(self, conn, playlist, uri):\n        raise BPDError(ERROR_UNKNOWN, \"Stored playlists are disabled\")\n\n    def cmd_playlistclear(self, conn, playlist):\n        raise BPDError(ERROR_UNKNOWN, \"Stored playlists are disabled\")\n\n    def cmd_playlistdelete(self, conn, playlist, index):\n        raise BPDError(ERROR_UNKNOWN, \"Stored playlists are disabled\")\n\n    def cmd_playlistmove(self, conn, playlist, from_index, to_index):\n        raise BPDError(ERROR_UNKNOWN, \"Stored playlists are disabled\")\n\n    def cmd_rename(self, conn, playlist, new_name):\n        raise BPDError(ERROR_UNKNOWN, \"Stored playlists are disabled\")\n\n    def cmd_rm(self, conn, playlist):\n        raise BPDError(ERROR_UNKNOWN, \"Stored playlists are disabled\")\n\n    def cmd_save(self, conn, playlist):\n        raise BPDError(ERROR_UNKNOWN, \"Stored playlists are disabled\")\n\n    # \"Outputs.\" Just a dummy implementation because we don't control\n    # any outputs.\n\n    def cmd_outputs(self, conn):\n        \"\"\"List the available outputs.\"\"\"\n        yield (\n            \"outputid: 0\",\n            \"outputname: gstreamer\",\n            \"outputenabled: 1\",\n        )\n\n    def cmd_enableoutput(self, conn, output_id):\n        output_id = cast_arg(int, output_id)\n        if output_id != 0:\n            raise ArgumentIndexError()\n\n    def cmd_disableoutput(self, conn, output_id):\n        output_id = cast_arg(int, output_id)\n        if output_id == 0:\n            raise BPDError(ERROR_ARG, \"cannot disable this output\")\n        else:\n            raise ArgumentIndexError()\n\n    # Playback control. The functions below hook into the\n    # half-implementations provided by the base class. Together, they're\n    # enough to implement all normal playback functionality.\n\n    def cmd_play(self, conn, index=-1):\n        new_index = index != -1 and index != self.current_index\n        was_paused = self.paused\n        super().cmd_play(conn, index)\n\n        if self.current_index > -1:  # Not stopped.\n            if was_paused and not new_index:\n                # Just unpause.\n                self.player.play()\n            else:\n                self.player.play_file(self.playlist[self.current_index].path)\n\n    def cmd_pause(self, conn, state=None):\n        super().cmd_pause(conn, state)\n        if self.paused:\n            self.player.pause()\n        elif self.player.playing:\n            self.player.play()\n\n    def cmd_stop(self, conn):\n        super().cmd_stop(conn)\n        self.player.stop()\n\n    def cmd_seek(self, conn, index, pos):\n        \"\"\"Seeks to the specified position in the specified song.\"\"\"\n        index = cast_arg(int, index)\n        pos = cast_arg(float, pos)\n        super().cmd_seek(conn, index, pos)\n        self.player.seek(pos)\n\n    # Volume control.\n\n    def cmd_setvol(self, conn, vol):\n        vol = cast_arg(int, vol)\n        super().cmd_setvol(conn, vol)\n        self.player.volume = float(vol) / 100\n\n\n# Beets plugin hooks.\n\n\nclass BPDPlugin(BeetsPlugin):\n    \"\"\"Provides the \"beet bpd\" command for running a music player\n    server.\n    \"\"\"\n\n    def __init__(self):\n        super().__init__()\n        self.config.add(\n            {\n                \"host\": \"\",\n                \"port\": 6600,\n                \"control_port\": 6601,\n                \"password\": \"\",\n                \"volume\": VOLUME_MAX,\n            }\n        )\n        self.config[\"password\"].redact = True\n\n    def start_bpd(self, lib, host, port, password, volume, ctrl_port):\n        \"\"\"Starts a BPD server.\"\"\"\n        server = Server(lib, host, port, password, ctrl_port, self._log)\n        server.cmd_setvol(None, volume)\n        server.run()\n\n    def commands(self):\n        cmd = beets.ui.Subcommand(\n            \"bpd\", help=\"run an MPD-compatible music player server\"\n        )\n\n        def func(lib, opts, args):\n            host = self.config[\"host\"].as_str()\n            host = args.pop(0) if args else host\n            port = args.pop(0) if args else self.config[\"port\"].get(int)\n            if args:\n                ctrl_port = args.pop(0)\n            else:\n                ctrl_port = self.config[\"control_port\"].get(int)\n            if args:\n                raise beets.ui.UserError(\"too many arguments\")\n            password = self.config[\"password\"].as_str()\n            volume = self.config[\"volume\"].get(int)\n            self.start_bpd(\n                lib, host, int(port), password, volume, int(ctrl_port)\n            )\n\n        cmd.func = func\n        return [cmd]\n"
  },
  {
    "path": "beetsplug/bpd/gstplayer.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"A wrapper for the GStreamer Python bindings that exposes a simple\nmusic player.\n\"\"\"\n\nimport _thread\nimport copy\nimport os\nimport sys\nimport time\nimport urllib\n\nimport gi\n\nfrom beets import ui\n\ntry:\n    gi.require_version(\"Gst\", \"1.0\")\nexcept ValueError as e:\n    # on some scenarios, gi may be importable, but we get a ValueError when\n    # trying to specify the required version. This is problematic in the test\n    # suite where test_bpd.py has a call to\n    # pytest.importorskip(\"beetsplug.bpd\"). Re-raising as an ImportError\n    # makes it so the test collector functions as inteded.\n    raise ImportError from e\n\nfrom gi.repository import GLib, Gst\n\nGst.init(None)\n\n\nclass QueryError(Exception):\n    pass\n\n\nclass GstPlayer:\n    \"\"\"A music player abstracting GStreamer's Playbin element.\n\n    Create a player object, then call run() to start a thread with a\n    runloop. Then call play_file to play music. Use player.playing\n    to check whether music is currently playing.\n\n    A basic play queue is also implemented (just a Python list,\n    player.queue, whose last element is next to play). To use it,\n    just call enqueue() and then play(). When a track finishes and\n    another is available on the queue, it is played automatically.\n    \"\"\"\n\n    def __init__(self, finished_callback=None):\n        \"\"\"Initialize a player.\n\n        If a finished_callback is provided, it is called every time a\n        track started with play_file finishes.\n\n        Once the player has been created, call run() to begin the main\n        runloop in a separate thread.\n        \"\"\"\n\n        # Set up the Gstreamer player. From the pygst tutorial:\n        # https://pygstdocs.berlios.de/pygst-tutorial/playbin.html (gone)\n        # https://brettviren.github.io/pygst-tutorial-org/pygst-tutorial.html\n        ####\n        # Updated to GStreamer 1.0 with:\n        # https://wiki.ubuntu.com/Novacut/GStreamer1.0\n        self.player = Gst.ElementFactory.make(\"playbin\", \"player\")\n\n        if self.player is None:\n            raise ui.UserError(\"Could not create playbin\")\n\n        fakesink = Gst.ElementFactory.make(\"fakesink\", \"fakesink\")\n\n        if fakesink is None:\n            raise ui.UserError(\"Could not create fakesink\")\n\n        self.player.set_property(\"video-sink\", fakesink)\n        bus = self.player.get_bus()\n        bus.add_signal_watch()\n        bus.connect(\"message\", self._handle_message)\n\n        # Set up our own stuff.\n        self.playing = False\n        self.finished_callback = finished_callback\n        self.cached_time = None\n        self._volume = 1.0\n\n    def _get_state(self):\n        \"\"\"Returns the current state flag of the playbin.\"\"\"\n        # gst's get_state function returns a 3-tuple; we just want the\n        # status flag in position 1.\n        return self.player.get_state(Gst.CLOCK_TIME_NONE)[1]\n\n    def _handle_message(self, bus, message):\n        \"\"\"Callback for status updates from GStreamer.\"\"\"\n        if message.type == Gst.MessageType.EOS:\n            # file finished playing\n            self.player.set_state(Gst.State.NULL)\n            self.playing = False\n            self.cached_time = None\n            if self.finished_callback:\n                self.finished_callback()\n\n        elif message.type == Gst.MessageType.ERROR:\n            # error\n            self.player.set_state(Gst.State.NULL)\n            err, _ = message.parse_error()\n            print(f\"Error: {err}\")\n            self.playing = False\n\n    def _set_volume(self, volume):\n        \"\"\"Set the volume level to a value in the range [0, 1.5].\"\"\"\n        # And the volume for the playbin.\n        self._volume = volume\n        self.player.set_property(\"volume\", volume)\n\n    def _get_volume(self):\n        \"\"\"Get the volume as a float in the range [0, 1.5].\"\"\"\n        return self._volume\n\n    volume = property(_get_volume, _set_volume)\n\n    def play_file(self, path):\n        \"\"\"Immediately begin playing the audio file at the given\n        path.\n        \"\"\"\n        self.player.set_state(Gst.State.NULL)\n        if isinstance(path, str):\n            path = path.encode(\"utf-8\")\n        uri = f\"file://{urllib.parse.quote(path)}\"\n        self.player.set_property(\"uri\", uri)\n        self.player.set_state(Gst.State.PLAYING)\n        self.playing = True\n\n    def play(self):\n        \"\"\"If paused, resume playback.\"\"\"\n        if self._get_state() == Gst.State.PAUSED:\n            self.player.set_state(Gst.State.PLAYING)\n            self.playing = True\n\n    def pause(self):\n        \"\"\"Pause playback.\"\"\"\n        self.player.set_state(Gst.State.PAUSED)\n\n    def stop(self):\n        \"\"\"Halt playback.\"\"\"\n        self.player.set_state(Gst.State.NULL)\n        self.playing = False\n        self.cached_time = None\n\n    def run(self):\n        \"\"\"Start a new thread for the player.\n\n        Call this function before trying to play any music with\n        play_file() or play().\n        \"\"\"\n\n        # If we don't use the MainLoop, messages are never sent.\n\n        def start():\n            loop = GLib.MainLoop()\n            loop.run()\n\n        _thread.start_new_thread(start, ())\n\n    def time(self):\n        \"\"\"Returns a tuple containing (position, length) where both\n        values are integers in seconds. If no stream is available,\n        returns (0, 0).\n        \"\"\"\n        fmt = Gst.Format(Gst.Format.TIME)\n        try:\n            posq = self.player.query_position(fmt)\n            if not posq[0]:\n                raise QueryError(\"query_position failed\")\n            pos = posq[1] / (10**9)\n\n            lengthq = self.player.query_duration(fmt)\n            if not lengthq[0]:\n                raise QueryError(\"query_duration failed\")\n            length = lengthq[1] / (10**9)\n\n            self.cached_time = (pos, length)\n            return (pos, length)\n\n        except QueryError:\n            # Stream not ready. For small gaps of time, for instance\n            # after seeking, the time values are unavailable. For this\n            # reason, we cache recent.\n            if self.playing and self.cached_time:\n                return self.cached_time\n            else:\n                return (0, 0)\n\n    def seek(self, position):\n        \"\"\"Seeks to position (in seconds).\"\"\"\n        _, cur_len = self.time()\n        if position > cur_len:\n            self.stop()\n            return\n\n        fmt = Gst.Format(Gst.Format.TIME)\n        ns = position * 10**9  # convert to nanoseconds\n        self.player.seek_simple(fmt, Gst.SeekFlags.FLUSH, ns)\n\n        # save new cached time\n        self.cached_time = (position, cur_len)\n\n    def block(self):\n        \"\"\"Block until playing finishes.\"\"\"\n        while self.playing:\n            time.sleep(1)\n\n    def get_decoders(self):\n        return get_decoders()\n\n\ndef get_decoders():\n    \"\"\"Get supported audio decoders from GStreamer.\n    Returns a dict mapping decoder element names to the associated media types\n    and file extensions.\n    \"\"\"\n    # We only care about audio decoder elements.\n    filt = (\n        Gst.ELEMENT_FACTORY_TYPE_DEPAYLOADER\n        | Gst.ELEMENT_FACTORY_TYPE_DEMUXER\n        | Gst.ELEMENT_FACTORY_TYPE_PARSER\n        | Gst.ELEMENT_FACTORY_TYPE_DECODER\n        | Gst.ELEMENT_FACTORY_TYPE_MEDIA_AUDIO\n    )\n\n    decoders = {}\n    mime_types = set()\n    for f in Gst.ElementFactory.list_get_elements(filt, Gst.Rank.NONE):\n        for pad in f.get_static_pad_templates():\n            if pad.direction == Gst.PadDirection.SINK:\n                caps = pad.static_caps.get()\n                mimes = set()\n                for i in range(caps.get_size()):\n                    struct = caps.get_structure(i)\n                    mime = struct.get_name()\n                    if mime == \"unknown/unknown\":\n                        continue\n                    mimes.add(mime)\n                    mime_types.add(mime)\n                if mimes:\n                    decoders[f.get_name()] = (mimes, set())\n\n    # Check all the TypeFindFactory plugin features form the registry. If they\n    # are associated with an audio media type that we found above, get the list\n    # of corresponding file extensions.\n    mime_extensions = {mime: set() for mime in mime_types}\n    for feat in Gst.Registry.get().get_feature_list(Gst.TypeFindFactory):\n        caps = feat.get_caps()\n        if caps:\n            for i in range(caps.get_size()):\n                struct = caps.get_structure(i)\n                mime = struct.get_name()\n                if mime in mime_types:\n                    mime_extensions[mime].update(feat.get_extensions())\n\n    # Fill in the slot we left for file extensions.\n    for name, (mimes, exts) in decoders.items():\n        for mime in mimes:\n            exts.update(mime_extensions[mime])\n\n    return decoders\n\n\ndef play_simple(paths):\n    \"\"\"Play the files in paths in a straightforward way, without\n    using the player's callback function.\n    \"\"\"\n    p = GstPlayer()\n    p.run()\n    for path in paths:\n        p.play_file(path)\n        p.block()\n\n\ndef play_complicated(paths):\n    \"\"\"Play the files in the path one after the other by using the\n    callback function to advance to the next song.\n    \"\"\"\n    my_paths = copy.copy(paths)\n\n    def next_song():\n        my_paths.pop(0)\n        p.play_file(my_paths[0])\n\n    p = GstPlayer(next_song)\n    p.run()\n    p.play_file(my_paths[0])\n    while my_paths:\n        time.sleep(1)\n\n\nif __name__ == \"__main__\":\n    # A very simple command-line player. Just give it names of audio\n    # files on the command line; these are all played in sequence.\n    paths = [os.path.abspath(os.path.expanduser(p)) for p in sys.argv[1:]]\n    # play_simple(paths)\n    play_complicated(paths)\n"
  },
  {
    "path": "beetsplug/bpm.py",
    "content": "# This file is part of beets.\n# Copyright 2016, aroquen\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Determine BPM by pressing a key to the rhythm.\"\"\"\n\nimport time\n\nfrom beets import ui\nfrom beets.plugins import BeetsPlugin\n\n\ndef bpm(max_strokes):\n    \"\"\"Returns average BPM (possibly of a playing song)\n    listening to Enter keystrokes.\n    \"\"\"\n    t0 = None\n    dt = []\n    for i in range(max_strokes):\n        # Press enter to the rhythm...\n        s = input()\n        if s == \"\":\n            t1 = time.time()\n            # Only start measuring at the second stroke\n            if t0:\n                dt.append(t1 - t0)\n            t0 = t1\n        else:\n            break\n\n    # Return average BPM\n    # bpm = (max_strokes-1) / sum(dt) * 60\n    ave = sum([1.0 / dti * 60 for dti in dt]) / len(dt)\n    return ave\n\n\nclass BPMPlugin(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n        self.config.add(\n            {\n                \"max_strokes\": 3,\n                \"overwrite\": True,\n            }\n        )\n\n    def commands(self):\n        cmd = ui.Subcommand(\n            \"bpm\",\n            help=\"determine bpm of a song by pressing a key to the rhythm\",\n        )\n        cmd.func = self.command\n        return [cmd]\n\n    def command(self, lib, opts, args):\n        write = ui.should_write()\n        self.get_bpm(lib.items(args), write)\n\n    def get_bpm(self, items, write=False):\n        overwrite = self.config[\"overwrite\"].get(bool)\n        if len(items) > 1:\n            raise ValueError(\"Can only get bpm of one song at time\")\n\n        item = items[0]\n        if item[\"bpm\"]:\n            self._log.info(\"Found bpm {}\", item[\"bpm\"])\n            if not overwrite:\n                return\n\n        self._log.info(\n            \"Press Enter {} times to the rhythm or Ctrl-D to exit\",\n            self.config[\"max_strokes\"].get(int),\n        )\n        new_bpm = bpm(self.config[\"max_strokes\"].get(int))\n        item[\"bpm\"] = int(new_bpm)\n        if write:\n            item.try_write()\n        item.store()\n        self._log.info(\"Added new bpm {}\", item[\"bpm\"])\n"
  },
  {
    "path": "beetsplug/bpsync.py",
    "content": "# This file is part of beets.\n# Copyright 2019, Rahul Ahuja.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Update library's tags using Beatport.\"\"\"\n\nfrom beets import autotag, library, ui, util\nfrom beets.plugins import BeetsPlugin, apply_item_changes\nfrom beets.util.deprecation import deprecate_for_user\n\nfrom .beatport import BeatportPlugin\n\n\nclass BPSyncPlugin(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n        deprecate_for_user(self._log, \"The 'bpsync' plugin\")\n        self.beatport_plugin = BeatportPlugin()\n        self.beatport_plugin.setup()\n\n    def commands(self):\n        cmd = ui.Subcommand(\"bpsync\", help=\"update metadata from Beatport\")\n        cmd.parser.add_option(\n            \"-p\",\n            \"--pretend\",\n            action=\"store_true\",\n            help=\"show all changes but do nothing\",\n        )\n        cmd.parser.add_option(\n            \"-m\",\n            \"--move\",\n            action=\"store_true\",\n            dest=\"move\",\n            help=\"move files in the library directory\",\n        )\n        cmd.parser.add_option(\n            \"-M\",\n            \"--nomove\",\n            action=\"store_false\",\n            dest=\"move\",\n            help=\"don't move files in library\",\n        )\n        cmd.parser.add_option(\n            \"-W\",\n            \"--nowrite\",\n            action=\"store_false\",\n            default=None,\n            dest=\"write\",\n            help=\"don't write updated metadata to files\",\n        )\n        cmd.parser.add_format_option()\n        cmd.func = self.func\n        return [cmd]\n\n    def func(self, lib, opts, args):\n        \"\"\"Command handler for the bpsync function.\"\"\"\n        move = ui.should_move(opts.move)\n        pretend = opts.pretend\n        write = ui.should_write(opts.write)\n\n        self.singletons(lib, args, move, pretend, write)\n        self.albums(lib, args, move, pretend, write)\n\n    def singletons(self, lib, query, move, pretend, write):\n        \"\"\"Retrieve and apply info from the autotagger for items matched by\n        query.\n        \"\"\"\n        for item in lib.items([*query, \"singleton:true\"]):\n            if not item.mb_trackid:\n                self._log.info(\n                    \"Skipping singleton with no mb_trackid: {}\", item\n                )\n                continue\n\n            if not self.is_beatport_track(item):\n                self._log.info(\n                    \"Skipping non-{.beatport_plugin.data_source} singleton: {}\",\n                    self,\n                    item,\n                )\n                continue\n\n            # Apply.\n            trackinfo = self.beatport_plugin.track_for_id(item.mb_trackid)\n            with lib.transaction():\n                autotag.apply_item_metadata(item, trackinfo)\n                apply_item_changes(lib, item, move, pretend, write)\n\n    @staticmethod\n    def is_beatport_track(item):\n        return (\n            item.get(\"data_source\") == BeatportPlugin.data_source\n            and item.mb_trackid.isnumeric()\n        )\n\n    def get_album_tracks(self, album):\n        if not album.mb_albumid:\n            self._log.info(\"Skipping album with no mb_albumid: {}\", album)\n            return False\n        if not album.mb_albumid.isnumeric():\n            self._log.info(\n                \"Skipping album with invalid {.beatport_plugin.data_source} ID: {}\",\n                self,\n                album,\n            )\n            return False\n        items = list(album.items())\n        if album.get(\"data_source\") == self.beatport_plugin.data_source:\n            return items\n        if not all(self.is_beatport_track(item) for item in items):\n            self._log.info(\n                \"Skipping non-{.beatport_plugin.data_source} release: {}\",\n                self,\n                album,\n            )\n            return False\n        return items\n\n    def albums(self, lib, query, move, pretend, write):\n        \"\"\"Retrieve and apply info from the autotagger for albums matched by\n        query and their items.\n        \"\"\"\n        # Process matching albums.\n        for album in lib.albums(query):\n            # Do we have a valid Beatport album?\n            items = self.get_album_tracks(album)\n            if not items:\n                continue\n\n            # Get the Beatport album information.\n            albuminfo = self.beatport_plugin.album_for_id(album.mb_albumid)\n            if not albuminfo:\n                self._log.info(\n                    \"Release ID {0.mb_albumid} not found for album {0}\", album\n                )\n                continue\n\n            beatport_trackid_to_trackinfo = {\n                track.track_id: track for track in albuminfo.tracks\n            }\n            library_trackid_to_item = {\n                int(item.mb_trackid): item for item in items\n            }\n            item_info_pairs = [\n                (item, beatport_trackid_to_trackinfo[track_id])\n                for track_id, item in library_trackid_to_item.items()\n            ]\n\n            self._log.info(\"applying changes to {}\", album)\n            with lib.transaction():\n                autotag.apply_metadata(albuminfo, item_info_pairs)\n                changed = False\n                # Find any changed item to apply Beatport changes to album.\n                any_changed_item = items[0]\n                for item in items:\n                    item_changed = ui.show_model_changes(item)\n                    changed |= item_changed\n                    if item_changed:\n                        any_changed_item = item\n                        apply_item_changes(lib, item, move, pretend, write)\n\n                if pretend or not changed:\n                    continue\n\n                # Update album structure to reflect an item in it.\n                for key in library.Album.item_keys:\n                    album[key] = any_changed_item[key]\n                album.store()\n\n                # Move album art (and any inconsistent items).\n                if move and lib.directory in util.ancestry(items[0].path):\n                    self._log.debug(\"moving album {}\", album)\n                    album.move()\n"
  },
  {
    "path": "beetsplug/bucket.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Fabrice Laporte.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Provides the %bucket{} function for path formatting.\"\"\"\n\nimport re\nimport string\nfrom datetime import datetime\nfrom itertools import tee\n\nfrom beets import plugins, ui\n\nASCII_DIGITS = string.digits + string.ascii_lowercase\n\n\nclass BucketError(Exception):\n    pass\n\n\ndef pairwise(iterable):\n    \"s -> (s0,s1), (s1,s2), (s2, s3), ...\"\n    a, b = tee(iterable)\n    next(b, None)\n    return zip(a, b)\n\n\ndef span_from_str(span_str):\n    \"\"\"Build a span dict from the span string representation.\"\"\"\n\n    def normalize_year(d, yearfrom):\n        \"\"\"Convert string to a 4 digits year\"\"\"\n        if yearfrom < 100:\n            raise BucketError(f\"{yearfrom} must be expressed on 4 digits\")\n\n        # if two digits only, pick closest year that ends by these two\n        # digits starting from yearfrom\n        if d < 100:\n            if (d % 100) < (yearfrom % 100):\n                d = (yearfrom - yearfrom % 100) + 100 + d\n            else:\n                d = (yearfrom - yearfrom % 100) + d\n        return d\n\n    years = [int(x) for x in re.findall(r\"\\d+\", span_str)]\n    if not years:\n        raise ui.UserError(\n            f\"invalid range defined for year bucket {span_str!r}: no year found\"\n        )\n    try:\n        years = [normalize_year(x, years[0]) for x in years]\n    except BucketError as exc:\n        raise ui.UserError(\n            f\"invalid range defined for year bucket {span_str!r}: {exc}\"\n        )\n\n    res = {\"from\": years[0], \"str\": span_str}\n    if len(years) > 1:\n        res[\"to\"] = years[-1]\n    return res\n\n\ndef complete_year_spans(spans):\n    \"\"\"Set the `to` value of spans if empty and sort them chronologically.\"\"\"\n    spans.sort(key=lambda x: x[\"from\"])\n    for x, y in pairwise(spans):\n        if \"to\" not in x:\n            x[\"to\"] = y[\"from\"] - 1\n    if spans and \"to\" not in spans[-1]:\n        spans[-1][\"to\"] = datetime.now().year\n\n\ndef extend_year_spans(spans, spanlen, start=1900, end=2014):\n    \"\"\"Add new spans to given spans list so that every year of [start,end]\n    belongs to a span.\n    \"\"\"\n    extended_spans = spans[:]\n    for x, y in pairwise(spans):\n        # if a gap between two spans, fill the gap with as much spans of\n        # spanlen length as necessary\n        for span_from in range(x[\"to\"] + 1, y[\"from\"], spanlen):\n            extended_spans.append({\"from\": span_from})\n    # Create spans prior to declared ones\n    for span_from in range(spans[0][\"from\"] - spanlen, start, -spanlen):\n        extended_spans.append({\"from\": span_from})\n    # Create spans after the declared ones\n    for span_from in range(spans[-1][\"to\"] + 1, end, spanlen):\n        extended_spans.append({\"from\": span_from})\n\n    complete_year_spans(extended_spans)\n    return extended_spans\n\n\ndef build_year_spans(year_spans_str):\n    \"\"\"Build a chronologically ordered list of spans dict from unordered spans\n    stringlist.\n    \"\"\"\n    spans = []\n    for elem in year_spans_str:\n        spans.append(span_from_str(elem))\n    complete_year_spans(spans)\n    return spans\n\n\ndef str2fmt(s):\n    \"\"\"Deduces formatting syntax from a span string.\"\"\"\n    regex = re.compile(\n        r\"(?P<bef>\\D*)(?P<fromyear>\\d+)(?P<sep>\\D*)\"\n        r\"(?P<toyear>\\d*)(?P<after>\\D*)\"\n    )\n    m = re.match(regex, s)\n\n    res = {\n        \"fromnchars\": len(m.group(\"fromyear\")),\n        \"tonchars\": len(m.group(\"toyear\")),\n    }\n    res[\"fmt\"] = (\n        f\"{m['bef']}{{}}{m['sep']}{'{}' if res['tonchars'] else ''}{m['after']}\"\n    )\n    return res\n\n\ndef format_span(fmt, yearfrom, yearto, fromnchars, tonchars):\n    \"\"\"Return a span string representation.\"\"\"\n    args = [str(yearfrom)[-fromnchars:]]\n    if tonchars:\n        args.append(str(yearto)[-tonchars:])\n\n    return fmt.format(*args)\n\n\ndef extract_modes(spans):\n    \"\"\"Extract the most common spans lengths and representation formats\"\"\"\n    rangelen = sorted([x[\"to\"] - x[\"from\"] + 1 for x in spans])\n    deflen = sorted(rangelen, key=rangelen.count)[-1]\n    reprs = [str2fmt(x[\"str\"]) for x in spans]\n    deffmt = sorted(reprs, key=reprs.count)[-1]\n    return deflen, deffmt\n\n\ndef build_alpha_spans(alpha_spans_str, alpha_regexs):\n    \"\"\"Extract alphanumerics from string and return sorted list of chars\n    [from...to]\n    \"\"\"\n    spans = []\n\n    for elem in alpha_spans_str:\n        if elem in alpha_regexs:\n            spans.append(re.compile(alpha_regexs[elem]))\n        else:\n            bucket = sorted([x for x in elem.lower() if x.isalnum()])\n            if bucket:\n                begin_index = ASCII_DIGITS.index(bucket[0])\n                end_index = ASCII_DIGITS.index(bucket[-1])\n            else:\n                raise ui.UserError(\n                    \"invalid range defined for alpha bucket \"\n                    f\"'{elem}': no alphanumeric character found\"\n                )\n            spans.append(\n                re.compile(\n                    rf\"^[{ASCII_DIGITS[begin_index : end_index + 1]}]\",\n                    re.IGNORECASE,\n                )\n            )\n    return spans\n\n\nclass BucketPlugin(plugins.BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n        self.template_funcs[\"bucket\"] = self._tmpl_bucket\n\n        self.config.add(\n            {\n                \"bucket_year\": [],\n                \"bucket_alpha\": [],\n                \"bucket_alpha_regex\": {},\n                \"extrapolate\": False,\n            }\n        )\n        self.setup()\n\n    def setup(self):\n        \"\"\"Setup plugin from config options\"\"\"\n        self.year_spans = build_year_spans(self.config[\"bucket_year\"].get())\n        if self.year_spans and self.config[\"extrapolate\"]:\n            [self.ys_len_mode, self.ys_repr_mode] = extract_modes(\n                self.year_spans\n            )\n            self.year_spans = extend_year_spans(\n                self.year_spans, self.ys_len_mode\n            )\n\n        self.alpha_spans = build_alpha_spans(\n            self.config[\"bucket_alpha\"].get(),\n            self.config[\"bucket_alpha_regex\"].get(),\n        )\n\n    def find_bucket_year(self, year):\n        \"\"\"Return  bucket that matches given year or return the year\n        if no matching bucket.\n        \"\"\"\n        for ys in self.year_spans:\n            if ys[\"from\"] <= int(year) <= ys[\"to\"]:\n                if \"str\" in ys:\n                    return ys[\"str\"]\n                else:\n                    return format_span(\n                        self.ys_repr_mode[\"fmt\"],\n                        ys[\"from\"],\n                        ys[\"to\"],\n                        self.ys_repr_mode[\"fromnchars\"],\n                        self.ys_repr_mode[\"tonchars\"],\n                    )\n        return year\n\n    def find_bucket_alpha(self, s):\n        \"\"\"Return alpha-range bucket that matches given string or return the\n        string initial if no matching bucket.\n        \"\"\"\n        for i, span in enumerate(self.alpha_spans):\n            if span.match(s):\n                return self.config[\"bucket_alpha\"].get()[i]\n        return s[0].upper()\n\n    def _tmpl_bucket(self, text, field=None):\n        if not field and len(text) == 4 and text.isdigit():\n            field = \"year\"\n\n        if field == \"year\":\n            func = self.find_bucket_year\n        else:\n            func = self.find_bucket_alpha\n        return func(text)\n"
  },
  {
    "path": "beetsplug/chroma.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Adds Chromaprint/Acoustid acoustic fingerprinting support to the\nautotagger. Requires the pyacoustid library.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom collections import defaultdict\nfrom functools import cached_property, partial\nfrom typing import TYPE_CHECKING\n\nimport acoustid\nimport confuse\n\nfrom beets import config, ui, util\nfrom beets.autotag.distance import Distance\nfrom beets.metadata_plugins import MetadataSourcePlugin\nfrom beetsplug.musicbrainz import MusicBrainzPlugin\n\nif TYPE_CHECKING:\n    from collections.abc import Iterable\n\n    from beets.autotag.hooks import TrackInfo\n\nAPI_KEY = \"1vOwZtEn\"\nSCORE_THRESH = 0.5\nTRACK_ID_WEIGHT = 10.0\nCOMMON_REL_THRESH = 0.6  # How many tracks must have an album in common?\nMAX_RECORDINGS = 5\nMAX_RELEASES = 5\n\n# Stores the Acoustid match information for each track. This is\n# populated when an import task begins and then used when searching for\n# candidates. It maps audio file paths to (recording_ids, release_ids)\n# pairs. If a given path is not present in the mapping, then no match\n# was found.\n_matches = {}\n\n# Stores the fingerprint and Acoustid ID for each track. This is stored\n# as metadata for each track for later use but is not relevant for\n# autotagging.\n_fingerprints = {}\n_acoustids = {}\n\n\ndef prefix(it, count):\n    \"\"\"Truncate an iterable to at most `count` items.\"\"\"\n    for i, v in enumerate(it):\n        if i >= count:\n            break\n        yield v\n\n\ndef releases_key(release, countries, original_year):\n    \"\"\"Used as a key to sort releases by date then preferred country\"\"\"\n    date = release.get(\"date\")\n    if date and original_year:\n        year = date.get(\"year\", 9999)\n        month = date.get(\"month\", 99)\n        day = date.get(\"day\", 99)\n    else:\n        year = 9999\n        month = 99\n        day = 99\n\n    # Uses index of preferred countries to sort\n    country_key = 99\n    if release.get(\"country\"):\n        for i, country in enumerate(countries):\n            if country.match(release[\"country\"]):\n                country_key = i\n                break\n\n    return (year, month, day, country_key)\n\n\ndef acoustid_match(log, path):\n    \"\"\"Gets metadata for a file from Acoustid and populates the\n    _matches, _fingerprints, and _acoustids dictionaries accordingly.\n    \"\"\"\n    try:\n        duration, fp = acoustid.fingerprint_file(util.syspath(path))\n    except acoustid.FingerprintGenerationError as exc:\n        log.error(\n            \"fingerprinting of {} failed: {}\",\n            util.displayable_path(repr(path)),\n            exc,\n        )\n        return None\n    fp = fp.decode()\n    _fingerprints[path] = fp\n    try:\n        res = acoustid.lookup(\n            API_KEY, fp, duration, meta=\"recordings releases\", timeout=10\n        )\n    except acoustid.AcoustidError as exc:\n        log.debug(\n            \"fingerprint matching {} failed: {}\",\n            util.displayable_path(repr(path)),\n            exc,\n        )\n        return None\n    log.debug(\"chroma: fingerprinted {}\", util.displayable_path(repr(path)))\n\n    # Ensure the response is usable and parse it.\n    if res[\"status\"] != \"ok\" or not res.get(\"results\"):\n        log.debug(\"no match found\")\n        return None\n    result = res[\"results\"][0]  # Best match.\n    if result[\"score\"] < SCORE_THRESH:\n        log.debug(\"no results above threshold\")\n        return None\n    _acoustids[path] = result[\"id\"]\n\n    # Get recording and releases from the result\n    if not result.get(\"recordings\"):\n        log.debug(\"no recordings found\")\n        return None\n    recording_ids = []\n    releases = []\n    for recording in result[\"recordings\"]:\n        recording_ids.append(recording[\"id\"])\n        if \"releases\" in recording:\n            releases.extend(recording[\"releases\"])\n\n    # The releases list is essentially in random order from the Acoustid lookup\n    # so we optionally sort it using the match.preferred configuration options.\n    # 'original_year' to sort the earliest first and\n    # 'countries' to then sort preferred countries first.\n    country_patterns = config[\"match\"][\"preferred\"][\"countries\"].as_str_seq()\n    countries = [re.compile(pat, re.I) for pat in country_patterns]\n    original_year = config[\"match\"][\"preferred\"][\"original_year\"]\n    releases.sort(\n        key=partial(\n            releases_key, countries=countries, original_year=original_year\n        )\n    )\n    release_ids = [rel[\"id\"] for rel in releases]\n\n    log.debug(\n        \"matched recordings {} on releases {}\", recording_ids, release_ids\n    )\n    _matches[path] = recording_ids, release_ids\n\n\n# Plugin structure and autotagging logic.\n\n\ndef _all_releases(items):\n    \"\"\"Given an iterable of Items, determines (according to Acoustid)\n    which releases the items have in common. Generates release IDs.\n    \"\"\"\n    # Count the number of \"hits\" for each release.\n    relcounts = defaultdict(int)\n    for item in items:\n        if item.path not in _matches:\n            continue\n\n        _, release_ids = _matches[item.path]\n        for release_id in release_ids:\n            relcounts[release_id] += 1\n\n    for release_id, count in relcounts.items():\n        if float(count) / len(items) > COMMON_REL_THRESH:\n            yield release_id\n\n\nclass AcoustidPlugin(MetadataSourcePlugin):\n    def __init__(self):\n        super().__init__()\n        self.config.add(\n            {\n                \"auto\": True,\n            }\n        )\n        config[\"acoustid\"][\"apikey\"].redact = True\n\n        if self.config[\"auto\"]:\n            self.register_listener(\"import_task_start\", self.fingerprint_task)\n        self.register_listener(\"import_task_apply\", apply_acoustid_metadata)\n\n    @cached_property\n    def mb(self) -> MusicBrainzPlugin:\n        return MusicBrainzPlugin()\n\n    def fingerprint_task(self, task, session):\n        return fingerprint_task(self._log, task, session)\n\n    def track_distance(self, item, info):\n        dist = Distance()\n        if item.path not in _matches or not info.track_id:\n            # Match failed or no track ID.\n            return dist\n\n        recording_ids, _ = _matches[item.path]\n        dist.add_expr(\"track_id\", info.track_id not in recording_ids)\n        return dist\n\n    def candidates(self, items, artist, album, va_likely):\n        albums = []\n        for relid in prefix(_all_releases(items), MAX_RELEASES):\n            album = self.mb.album_for_id(relid)\n            if album:\n                albums.append(album)\n\n        self._log.debug(\"acoustid album candidates: {}\", len(albums))\n        return albums\n\n    def item_candidates(self, item, artist, title) -> Iterable[TrackInfo]:\n        if item.path not in _matches:\n            return []\n\n        recording_ids, _ = _matches[item.path]\n        tracks = []\n        for recording_id in prefix(recording_ids, MAX_RECORDINGS):\n            track = self.mb.track_for_id(recording_id)\n            if track:\n                tracks.append(track)\n        self._log.debug(\"acoustid item candidates: {}\", len(tracks))\n        return tracks\n\n    def album_for_id(self, *args, **kwargs):\n        # Lookup by fingerprint ID does not make too much sense.\n        return None\n\n    def track_for_id(self, *args, **kwargs):\n        # Lookup by fingerprint ID does not make too much sense.\n        return None\n\n    def commands(self):\n        submit_cmd = ui.Subcommand(\n            \"submit\", help=\"submit Acoustid fingerprints\"\n        )\n\n        def submit_cmd_func(lib, opts, args):\n            try:\n                apikey = config[\"acoustid\"][\"apikey\"].as_str()\n            except confuse.NotFoundError:\n                raise ui.UserError(\"no Acoustid user API key provided\")\n            submit_items(self._log, apikey, lib.items(args))\n\n        submit_cmd.func = submit_cmd_func\n\n        fingerprint_cmd = ui.Subcommand(\n            \"fingerprint\", help=\"generate fingerprints for items without them\"\n        )\n\n        def fingerprint_cmd_func(lib, opts, args):\n            for item in lib.items(args):\n                fingerprint_item(self._log, item, write=ui.should_write())\n\n        fingerprint_cmd.func = fingerprint_cmd_func\n\n        return [submit_cmd, fingerprint_cmd]\n\n\n# Hooks into import process.\n\n\ndef fingerprint_task(log, task, session):\n    \"\"\"Fingerprint each item in the task for later use during the\n    autotagging candidate search.\n    \"\"\"\n    items = task.items if task.is_album else [task.item]\n    for item in items:\n        acoustid_match(log, item.path)\n\n\ndef apply_acoustid_metadata(task, session):\n    \"\"\"Apply Acoustid metadata (fingerprint and ID) to the task's items.\"\"\"\n    for item in task.imported_items():\n        if item.path in _fingerprints:\n            item.acoustid_fingerprint = _fingerprints[item.path]\n        if item.path in _acoustids:\n            item.acoustid_id = _acoustids[item.path]\n\n\n# UI commands.\n\n\ndef submit_items(log, userkey, items, chunksize=64):\n    \"\"\"Submit fingerprints for the items to the Acoustid server.\"\"\"\n    data = []  # The running list of dictionaries to submit.\n\n    def submit_chunk():\n        \"\"\"Submit the current accumulated fingerprint data.\"\"\"\n        log.info(\"submitting {} fingerprints\", len(data))\n        try:\n            acoustid.submit(API_KEY, userkey, data, timeout=10)\n        except acoustid.AcoustidError as exc:\n            log.warning(\"acoustid submission error: {}\", exc)\n        del data[:]\n\n    for item in items:\n        fp = fingerprint_item(log, item, write=ui.should_write())\n\n        # Construct a submission dictionary for this item.\n        item_data = {\n            \"duration\": int(item.length),\n            \"fingerprint\": fp,\n        }\n        if item.mb_trackid:\n            item_data[\"mbid\"] = item.mb_trackid\n            log.debug(\"submitting MBID\")\n        else:\n            item_data.update(\n                {\n                    \"track\": item.title,\n                    \"artist\": item.artist,\n                    \"album\": item.album,\n                    \"albumartist\": item.albumartist,\n                    \"year\": item.year,\n                    \"trackno\": item.track,\n                    \"discno\": item.disc,\n                }\n            )\n            log.debug(\"submitting textual metadata\")\n        data.append(item_data)\n\n        # If we have enough data, submit a chunk.\n        if len(data) >= chunksize:\n            submit_chunk()\n\n    # Submit remaining data in a final chunk.\n    if data:\n        submit_chunk()\n\n\ndef fingerprint_item(log, item, write=False):\n    \"\"\"Get the fingerprint for an Item. If the item already has a\n    fingerprint, it is not regenerated. If fingerprint generation fails,\n    return None. If the items are associated with a library, they are\n    saved to the database. If `write` is set, then the new fingerprints\n    are also written to files' metadata.\n    \"\"\"\n    # Get a fingerprint and length for this track.\n    if not item.length:\n        log.info(\"{.filepath}: no duration available\", item)\n    elif item.acoustid_fingerprint:\n        if write:\n            log.info(\"{.filepath}: fingerprint exists, skipping\", item)\n        else:\n            log.info(\"{.filepath}: using existing fingerprint\", item)\n        return item.acoustid_fingerprint\n    else:\n        log.info(\"{.filepath}: fingerprinting\", item)\n        try:\n            _, fp = acoustid.fingerprint_file(util.syspath(item.path))\n            item.acoustid_fingerprint = fp.decode()\n            if write:\n                log.info(\"{.filepath}: writing fingerprint\", item)\n                item.try_write()\n            if item._db:\n                item.store()\n            return item.acoustid_fingerprint\n        except acoustid.FingerprintGenerationError as exc:\n            log.info(\"fingerprint generation failed: {}\", exc)\n"
  },
  {
    "path": "beetsplug/convert.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Jakob Schnitzer.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Converts tracks or albums to external directory\"\"\"\n\nimport logging\nimport os\nimport shlex\nimport subprocess\nimport tempfile\nimport threading\nfrom string import Template\n\nimport mediafile\nfrom confuse import ConfigTypeError, Optional\n\nfrom beets import config, plugins, ui, util\nfrom beets.library import Item, parse_query_string\nfrom beets.plugins import BeetsPlugin\nfrom beets.util import par_map\nfrom beets.util.artresizer import ArtResizer\nfrom beets.util.m3u import M3UFile\nfrom beetsplug._utils import art\n\n_fs_lock = threading.Lock()\n_temp_files = []  # Keep track of temporary transcoded files for deletion.\n\n# Some convenient alternate names for formats.\nALIASES = {\n    \"windows media\": \"wma\",\n    \"vorbis\": \"ogg\",\n}\n\nLOSSLESS_FORMATS = [\"ape\", \"flac\", \"alac\", \"wave\", \"aiff\"]\n\n\ndef replace_ext(path, ext):\n    \"\"\"Return the path with its extension replaced by `ext`.\n\n    The new extension must not contain a leading dot.\n    \"\"\"\n    ext_dot = b\".\" + ext\n    return os.path.splitext(path)[0] + ext_dot\n\n\ndef get_format(fmt=None):\n    \"\"\"Return the command template and the extension from the config.\"\"\"\n    if not fmt:\n        fmt = config[\"convert\"][\"format\"].as_str().lower()\n    fmt = ALIASES.get(fmt, fmt)\n\n    try:\n        format_info = config[\"convert\"][\"formats\"][fmt].get(dict)\n        command = format_info[\"command\"]\n        extension = format_info.get(\"extension\", fmt)\n    except KeyError:\n        raise ui.UserError(f'convert: format {fmt} needs the \"command\" field')\n    except ConfigTypeError:\n        command = config[\"convert\"][\"formats\"][fmt].get(str)\n        extension = fmt\n\n    # Convenience and backwards-compatibility shortcuts.\n    keys = config[\"convert\"].keys()\n    if \"command\" in keys:\n        command = config[\"convert\"][\"command\"].as_str()\n    elif \"opts\" in keys:\n        # Undocumented option for backwards compatibility with < 1.3.1.\n        command = (\n            f\"ffmpeg -i $source -y {config['convert']['opts'].as_str()} $dest\"\n        )\n    if \"extension\" in keys:\n        extension = config[\"convert\"][\"extension\"].as_str()\n\n    return (command.encode(\"utf-8\"), extension.encode(\"utf-8\"))\n\n\ndef in_no_convert(item: Item) -> bool:\n    no_convert_query = config[\"convert\"][\"no_convert\"].as_str()\n\n    if no_convert_query:\n        query, _ = parse_query_string(no_convert_query, Item)\n        return query.match(item)\n    else:\n        return False\n\n\ndef should_transcode(item, fmt, force: bool = False):\n    \"\"\"Determine whether the item should be transcoded as part of\n    conversion (i.e., its bitrate is high or it has the wrong format).\n\n    If ``force`` is True, safety checks like ``no_convert`` and\n    ``never_convert_lossy_files`` are ignored and the item is always\n    transcoded.\n    \"\"\"\n    if force:\n        return True\n    if in_no_convert(item) or (\n        config[\"convert\"][\"never_convert_lossy_files\"].get(bool)\n        and item.format.lower() not in LOSSLESS_FORMATS\n    ):\n        return False\n    maxbr = config[\"convert\"][\"max_bitrate\"].get(Optional(int))\n    if maxbr is not None and item.bitrate >= 1000 * maxbr:\n        return True\n    return fmt.lower() != item.format.lower()\n\n\nclass ConvertPlugin(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n        self.config.add(\n            {\n                \"dest\": None,\n                \"pretend\": False,\n                \"link\": False,\n                \"hardlink\": False,\n                \"threads\": os.cpu_count(),\n                \"format\": \"mp3\",\n                \"id3v23\": \"inherit\",\n                \"write_metadata\": True,\n                \"formats\": {\n                    \"aac\": {\n                        \"command\": (\n                            \"ffmpeg -i $source -y -vn -acodec aac -aq 1 $dest\"\n                        ),\n                        \"extension\": \"m4a\",\n                    },\n                    \"alac\": {\n                        \"command\": (\n                            \"ffmpeg -i $source -y -vn -acodec alac $dest\"\n                        ),\n                        \"extension\": \"m4a\",\n                    },\n                    \"flac\": \"ffmpeg -i $source -y -vn -acodec flac $dest\",\n                    \"mp3\": \"ffmpeg -i $source -y -vn -aq 2 $dest\",\n                    \"opus\": (\n                        \"ffmpeg -i $source -y -vn -acodec libopus -ab 96k $dest\"\n                    ),\n                    \"ogg\": (\n                        \"ffmpeg -i $source -y -vn -acodec libvorbis -aq 3 $dest\"\n                    ),\n                    \"wma\": \"ffmpeg -i $source -y -vn -acodec wmav2 -vn $dest\",\n                },\n                \"max_bitrate\": None,\n                \"auto\": False,\n                \"auto_keep\": False,\n                \"tmpdir\": None,\n                \"quiet\": False,\n                \"embed\": True,\n                \"paths\": {},\n                \"no_convert\": \"\",\n                \"never_convert_lossy_files\": False,\n                \"copy_album_art\": False,\n                \"album_art_maxwidth\": 0,\n                \"delete_originals\": False,\n                \"playlist\": None,\n            }\n        )\n        self.early_import_stages = [self.auto_convert, self.auto_convert_keep]\n\n        self.register_listener(\"import_task_files\", self._cleanup)\n\n    def commands(self):\n        cmd = ui.Subcommand(\"convert\", help=\"convert to external location\")\n        cmd.parser.add_option(\n            \"-p\",\n            \"--pretend\",\n            action=\"store_true\",\n            help=\"show actions but do nothing\",\n        )\n        cmd.parser.add_option(\n            \"-t\",\n            \"--threads\",\n            action=\"store\",\n            type=\"int\",\n            help=(\n                \"change the number of threads, defaults to maximum available\"\n                \" processors\"\n            ),\n        )\n        cmd.parser.add_option(\n            \"-k\",\n            \"--keep-new\",\n            action=\"store_true\",\n            dest=\"keep_new\",\n            help=\"keep only the converted and move the old files\",\n        )\n        cmd.parser.add_option(\n            \"-d\", \"--dest\", action=\"store\", help=\"set the destination directory\"\n        )\n        cmd.parser.add_option(\n            \"-f\",\n            \"--format\",\n            action=\"store\",\n            dest=\"format\",\n            help=\"set the target format of the tracks\",\n        )\n        cmd.parser.add_option(\n            \"-y\",\n            \"--yes\",\n            action=\"store_true\",\n            dest=\"yes\",\n            help=\"do not ask for confirmation\",\n        )\n        cmd.parser.add_option(\n            \"-l\",\n            \"--link\",\n            action=\"store_true\",\n            dest=\"link\",\n            help=\"symlink files that do not need transcoding.\",\n        )\n        cmd.parser.add_option(\n            \"-H\",\n            \"--hardlink\",\n            action=\"store_true\",\n            dest=\"hardlink\",\n            help=(\n                \"hardlink files that do not need transcoding. Overrides --link.\"\n            ),\n        )\n        cmd.parser.add_option(\n            \"-m\",\n            \"--playlist\",\n            action=\"store\",\n            help=\"\"\"create an m3u8 playlist file containing\n                              the converted files. The playlist file will be\n                              saved below the destination directory, thus\n                              PLAYLIST could be a file name or a relative path.\n                              To ensure a working playlist when transferred to\n                              a different computer, or opened from an external\n                              drive, relative paths pointing to media files\n                              will be used.\"\"\",\n        )\n        cmd.parser.add_option(\n            \"-F\",\n            \"--force\",\n            action=\"store_true\",\n            dest=\"force\",\n            help=(\n                \"force transcoding. Ignores no_convert, \"\n                \"never_convert_lossy_files, and max_bitrate\"\n            ),\n        )\n        cmd.parser.add_album_option()\n        cmd.func = self.convert_func\n        return [cmd]\n\n    def auto_convert(self, config, task):\n        if self.config[\"auto\"]:\n            par_map(\n                lambda item: self.convert_on_import(config.lib, item),\n                task.imported_items(),\n            )\n\n    def auto_convert_keep(self, config, task):\n        if self.config[\"auto_keep\"]:\n            empty_opts = self.commands()[0].parser.get_default_values()\n            (\n                dest,\n                threads,\n                path_formats,\n                fmt,\n                pretend,\n                hardlink,\n                link,\n                _,\n                force,\n            ) = self._get_opts_and_config(empty_opts)\n\n            items = task.imported_items()\n\n            # Filter items based on should_transcode function\n            items = [item for item in items if should_transcode(item, fmt)]\n\n            self._parallel_convert(\n                dest,\n                False,\n                path_formats,\n                fmt,\n                pretend,\n                link,\n                hardlink,\n                threads,\n                items,\n                force,\n            )\n\n    # Utilities converted from functions to methods on logging overhaul\n\n    def encode(self, command, source, dest, pretend=False):\n        \"\"\"Encode `source` to `dest` using command template `command`.\n\n        Raises `subprocess.CalledProcessError` if the command exited with a\n        non-zero status code.\n        \"\"\"\n        # The paths and arguments must be bytes.\n        assert isinstance(command, bytes)\n        assert isinstance(source, bytes)\n        assert isinstance(dest, bytes)\n\n        quiet = self.config[\"quiet\"].get(bool)\n\n        if not quiet and not pretend:\n            self._log.info(\"Encoding {}\", util.displayable_path(source))\n\n        command = os.fsdecode(command)\n        source = os.fsdecode(source)\n        dest = os.fsdecode(dest)\n\n        # Substitute $source and $dest in the argument list.\n        args = shlex.split(command)\n        encode_cmd = []\n        for i, arg in enumerate(args):\n            args[i] = Template(arg).safe_substitute(\n                {\n                    \"source\": source,\n                    \"dest\": dest,\n                }\n            )\n            encode_cmd.append(os.fsdecode(args[i]))\n\n        if pretend:\n            self._log.info(\"{}\", \" \".join(args))\n            return\n\n        try:\n            util.command_output(encode_cmd)\n        except subprocess.CalledProcessError as exc:\n            # Something went wrong (probably Ctrl+C), remove temporary files\n            self._log.info(\n                \"Encoding {} failed. Cleaning up...\",\n                util.displayable_path(source),\n            )\n            self._log.debug(\n                \"Command {0} exited with status {1.returncode}: {1.output}\",\n                args,\n                exc,\n            )\n            util.remove(dest)\n            util.prune_dirs(os.path.dirname(dest))\n            raise\n        except OSError as exc:\n            raise ui.UserError(\n                f\"convert: couldn't invoke {' '.join(args)!r}: {exc}\"\n            )\n\n        if not quiet and not pretend:\n            self._log.info(\n                \"Finished encoding {}\", util.displayable_path(source)\n            )\n\n    def convert_item(\n        self,\n        dest_dir,\n        keep_new,\n        path_formats,\n        fmt,\n        pretend=False,\n        link=False,\n        hardlink=False,\n        force=False,\n    ):\n        \"\"\"A pipeline thread that converts `Item` objects from a\n        library.\n        \"\"\"\n        command, ext = get_format(fmt)\n        item, original, converted = None, None, None\n        while True:\n            item = yield (item, original, converted)\n            dest = item.destination(basedir=dest_dir, path_formats=path_formats)\n\n            # Ensure that desired item is readable before processing it. Needed\n            # to avoid any side-effect of the conversion (linking, keep_new,\n            # refresh) if we already know that it will fail.\n            try:\n                mediafile.MediaFile(util.syspath(item.path))\n            except mediafile.UnreadableFileError as exc:\n                self._log.error(\"Could not open file to convert: {}\", exc)\n                continue\n\n            # When keeping the new file in the library, we first move the\n            # current (pristine) file to the destination. We'll then copy it\n            # back to its old path or transcode it to a new path.\n            if keep_new:\n                original = dest\n                converted = item.path\n                if should_transcode(item, fmt, force):\n                    converted = replace_ext(converted, ext)\n            else:\n                original = item.path\n                if should_transcode(item, fmt, force):\n                    dest = replace_ext(dest, ext)\n                converted = dest\n\n            # Ensure that only one thread tries to create directories at a\n            # time. (The existence check is not atomic with the directory\n            # creation inside this function.)\n            if not pretend:\n                with _fs_lock:\n                    util.mkdirall(dest)\n\n            if os.path.exists(util.syspath(dest)):\n                self._log.info(\n                    \"Skipping {.filepath} (target file exists)\", item\n                )\n                continue\n\n            if keep_new:\n                if pretend:\n                    self._log.info(\n                        \"mv {.filepath} {}\",\n                        item,\n                        util.displayable_path(original),\n                    )\n                else:\n                    self._log.info(\n                        \"Moving to {}\", util.displayable_path(original)\n                    )\n                    util.move(item.path, original)\n\n            if should_transcode(item, fmt, force):\n                linked = False\n                try:\n                    self.encode(command, original, converted, pretend)\n                except subprocess.CalledProcessError:\n                    continue\n            else:\n                linked = link or hardlink\n                if pretend:\n                    msg = \"ln\" if hardlink else (\"ln -s\" if link else \"cp\")\n\n                    self._log.info(\n                        \"{} {} {}\",\n                        msg,\n                        util.displayable_path(original),\n                        util.displayable_path(converted),\n                    )\n                else:\n                    # No transcoding necessary.\n                    msg = (\n                        \"Hardlinking\"\n                        if hardlink\n                        else (\"Linking\" if link else \"Copying\")\n                    )\n\n                    self._log.info(\"{} {.filepath}\", msg, item)\n\n                    if hardlink:\n                        util.hardlink(original, converted)\n                    elif link:\n                        util.link(original, converted)\n                    else:\n                        util.copy(original, converted)\n\n            if pretend:\n                continue\n\n            id3v23 = self.config[\"id3v23\"].as_choice([True, False, \"inherit\"])\n            if id3v23 == \"inherit\":\n                id3v23 = None\n\n            # Write tags from the database to the file if requested\n            if self.config[\"write_metadata\"].get(bool):\n                item.try_write(path=converted, id3v23=id3v23)\n\n            if keep_new:\n                # If we're keeping the transcoded file, read it again (after\n                # writing) to get new bitrate, duration, etc.\n                item.path = converted\n                item.read()\n                item.store()  # Store new path and audio data.\n\n            if self.config[\"embed\"] and not linked:\n                album = item._cached_album\n                if album and album.artpath:\n                    maxwidth = self._get_art_resize(album.artpath)\n                    self._log.debug(\n                        \"embedding album art from {.art_filepath}\", album\n                    )\n                    art.embed_item(\n                        self._log,\n                        item,\n                        album.artpath,\n                        maxwidth,\n                        itempath=converted,\n                        id3v23=id3v23,\n                    )\n\n            if keep_new:\n                plugins.send(\n                    \"after_convert\", item=item, dest=dest, keepnew=True\n                )\n            else:\n                plugins.send(\n                    \"after_convert\", item=item, dest=converted, keepnew=False\n                )\n\n    def copy_album_art(\n        self,\n        album,\n        dest_dir,\n        path_formats,\n        pretend=False,\n        link=False,\n        hardlink=False,\n    ):\n        \"\"\"Copies or converts the associated cover art of the album. Album must\n        have at least one track.\n        \"\"\"\n        if not album or not album.artpath:\n            return\n\n        album_item = album.items().get()\n        # Album shouldn't be empty.\n        if not album_item:\n            return\n\n        # Get the destination of the first item (track) of the album, we use\n        # this function to format the path accordingly to path_formats.\n        dest = album_item.destination(\n            basedir=dest_dir, path_formats=path_formats\n        )\n\n        # Remove item from the path.\n        dest = os.path.join(*util.components(dest)[:-1])\n\n        dest = album.art_destination(album.artpath, item_dir=dest)\n        if album.artpath == dest:\n            return\n\n        if not pretend:\n            util.mkdirall(dest)\n\n        if os.path.exists(util.syspath(dest)):\n            self._log.info(\n                \"Skipping {.art_filepath} (target file exists)\", album\n            )\n            return\n\n        # Decide whether we need to resize the cover-art image.\n        maxwidth = self._get_art_resize(album.artpath)\n\n        # Either copy or resize (while copying) the image.\n        if maxwidth is not None:\n            self._log.info(\n                \"Resizing cover art from {.art_filepath} to {}\",\n                album,\n                util.displayable_path(dest),\n            )\n            if not pretend:\n                ArtResizer.shared.resize(maxwidth, album.artpath, dest)\n        else:\n            if pretend:\n                msg = \"ln\" if hardlink else (\"ln -s\" if link else \"cp\")\n\n                self._log.info(\n                    \"{} {.art_filepath} {}\",\n                    msg,\n                    album,\n                    util.displayable_path(dest),\n                )\n            else:\n                msg = (\n                    \"Hardlinking\"\n                    if hardlink\n                    else (\"Linking\" if link else \"Copying\")\n                )\n\n                self._log.info(\n                    \"{} cover art from {.art_filepath} to {}\",\n                    msg,\n                    album,\n                    util.displayable_path(dest),\n                )\n                if hardlink:\n                    util.hardlink(album.artpath, dest)\n                elif link:\n                    util.link(album.artpath, dest)\n                else:\n                    util.copy(album.artpath, dest)\n\n    def convert_func(self, lib, opts, args):\n        (\n            dest,\n            threads,\n            path_formats,\n            fmt,\n            pretend,\n            hardlink,\n            link,\n            playlist,\n            force,\n        ) = self._get_opts_and_config(opts)\n\n        if opts.album:\n            albums = lib.albums(args)\n            items = [i for a in albums for i in a.items()]\n            if not pretend:\n                for a in albums:\n                    ui.print_(format(a, \"\"))\n        else:\n            items = list(lib.items(args))\n            if not pretend:\n                for i in items:\n                    ui.print_(format(i, \"\"))\n\n        if not items:\n            self._log.error(\"Empty query result.\")\n            return\n        if not (pretend or opts.yes or ui.input_yn(\"Convert? (Y/n)\")):\n            return\n\n        if opts.album and self.config[\"copy_album_art\"]:\n            for album in albums:\n                self.copy_album_art(\n                    album, dest, path_formats, pretend, link, hardlink\n                )\n\n        # If the user supplied a playlist name, create a playlist for files\n        # copied to the destination.\n        pl_normpath = None\n        items_paths = None\n        if playlist:\n            _, ext = get_format(fmt)\n            # Playlist paths are understood as relative to the dest directory.\n            pl_normpath = util.normpath(playlist)\n            pl_dir = os.path.dirname(pl_normpath)\n            items_paths = []\n            for item in items:\n                item_path = item.destination(\n                    basedir=dest, path_formats=path_formats\n                )\n\n                # When keeping new files in the library, destination paths\n                # keep original files and extensions.\n                if not opts.keep_new and should_transcode(item, fmt, force):\n                    item_path = replace_ext(item_path, ext)\n\n                items_paths.append(os.path.relpath(item_path, pl_dir))\n\n        self._parallel_convert(\n            dest,\n            opts.keep_new,\n            path_formats,\n            fmt,\n            pretend,\n            link,\n            hardlink,\n            threads,\n            items,\n            force,\n        )\n\n        if playlist:\n            self._log.info(\"Creating playlist file {}\", pl_normpath)\n            if not pretend:\n                m3ufile = M3UFile(playlist)\n                m3ufile.set_contents(items_paths)\n                m3ufile.write()\n\n    def convert_on_import(self, lib, item):\n        \"\"\"Transcode a file automatically after it is imported into the\n        library.\n        \"\"\"\n        fmt = self.config[\"format\"].as_str().lower()\n        if should_transcode(item, fmt):\n            command, ext = get_format()\n\n            # Create a temporary file for the conversion.\n            tmpdir = self.config[\"tmpdir\"].get()\n            if tmpdir:\n                tmpdir = os.fsdecode(util.bytestring_path(tmpdir))\n            fd, dest = tempfile.mkstemp(f\".{os.fsdecode(ext)}\", dir=tmpdir)\n            os.close(fd)\n            dest = util.bytestring_path(dest)\n            _temp_files.append(dest)  # Delete the transcode later.\n\n            # Convert.\n            try:\n                self.encode(command, item.path, dest)\n            except subprocess.CalledProcessError:\n                return\n\n            # Change the newly-imported database entry to point to the\n            # converted file.\n            source_path = item.path\n            item.path = dest\n            item.write()\n            item.read()  # Load new audio information data.\n            item.store()\n\n            if self.config[\"delete_originals\"]:\n                self._log.log(\n                    logging.DEBUG if self.config[\"quiet\"] else logging.INFO,\n                    \"Removing original file {}\",\n                    source_path,\n                )\n                util.remove(source_path, False)\n\n    def _get_art_resize(self, artpath):\n        \"\"\"For a given piece of album art, determine whether or not it needs\n        to be resized according to the user's settings. If so, returns the\n        new size. If not, returns None.\n        \"\"\"\n        newwidth = None\n        if self.config[\"album_art_maxwidth\"]:\n            maxwidth = self.config[\"album_art_maxwidth\"].get(int)\n            size = ArtResizer.shared.get_size(artpath)\n            self._log.debug(\"image size: {}\", size)\n            if size:\n                if size[0] > maxwidth:\n                    newwidth = maxwidth\n            else:\n                self._log.warning(\n                    \"Could not get size of image (please see \"\n                    \"documentation for dependencies).\"\n                )\n        return newwidth\n\n    def _cleanup(self, task, session):\n        for path in task.old_paths:\n            if path in _temp_files:\n                if os.path.isfile(util.syspath(path)):\n                    util.remove(path)\n                _temp_files.remove(path)\n\n    def _get_opts_and_config(self, opts):\n        \"\"\"Returns parameters needed for convert function.\n        Get parameters from command line if available,\n        default to config if not available.\n        \"\"\"\n        dest = opts.dest or self.config[\"dest\"].get()\n        if not dest:\n            raise ui.UserError(\"no convert destination set\")\n        dest = util.bytestring_path(dest)\n\n        threads = opts.threads or self.config[\"threads\"].get(int)\n\n        path_formats = ui.get_path_formats(self.config[\"paths\"] or None)\n\n        fmt = opts.format or self.config[\"format\"].as_str().lower()\n\n        playlist = opts.playlist or self.config[\"playlist\"].get()\n        if playlist is not None:\n            playlist = os.path.join(dest, util.bytestring_path(playlist))\n\n        if opts.pretend is not None:\n            pretend = opts.pretend\n        else:\n            pretend = self.config[\"pretend\"].get(bool)\n\n        if opts.hardlink is not None:\n            hardlink = opts.hardlink\n            link = False\n        elif opts.link is not None:\n            hardlink = False\n            link = opts.link\n        else:\n            hardlink = self.config[\"hardlink\"].get(bool)\n            link = self.config[\"link\"].get(bool)\n        force = getattr(opts, \"force\", False)\n        return (\n            dest,\n            threads,\n            path_formats,\n            fmt,\n            pretend,\n            hardlink,\n            link,\n            playlist,\n            force,\n        )\n\n    def _parallel_convert(\n        self,\n        dest,\n        keep_new,\n        path_formats,\n        fmt,\n        pretend,\n        link,\n        hardlink,\n        threads,\n        items,\n        force,\n    ):\n        \"\"\"Run the convert_item function for every items on as many thread as\n        defined in threads\n        \"\"\"\n        convert = [\n            self.convert_item(\n                dest,\n                keep_new,\n                path_formats,\n                fmt,\n                pretend,\n                link,\n                hardlink,\n                force,\n            )\n            for _ in range(threads)\n        ]\n        pipe = util.pipeline.Pipeline([iter(items), convert])\n        pipe.run_parallel()\n"
  },
  {
    "path": "beetsplug/deezer.py",
    "content": "# This file is part of beets.\n# Copyright 2019, Rahul Ahuja.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Adds Deezer release and track search support to the autotagger\"\"\"\n\nfrom __future__ import annotations\n\nimport collections\nimport time\nfrom typing import TYPE_CHECKING, ClassVar\n\nimport requests\n\nfrom beets import ui\nfrom beets.autotag import AlbumInfo, TrackInfo\nfrom beets.dbcore import types\nfrom beets.metadata_plugins import IDResponse, SearchApiMetadataSourcePlugin\n\nif TYPE_CHECKING:\n    from collections.abc import Sequence\n\n    from beets.library import Item, Library\n    from beets.metadata_plugins import QueryType, SearchParams\n\n    from ._typing import JSONDict\n\n\nclass DeezerPlugin(SearchApiMetadataSourcePlugin[IDResponse]):\n    item_types: ClassVar[dict[str, types.Type]] = {\n        \"deezer_track_rank\": types.INTEGER,\n        \"deezer_track_id\": types.INTEGER,\n        \"deezer_updated\": types.DATE,\n    }\n    # Base URLs for the Deezer API\n    # Documentation: https://developers.deezer.com/api/\n    search_url = \"https://api.deezer.com/search/\"\n    album_url = \"https://api.deezer.com/album/\"\n    track_url = \"https://api.deezer.com/track/\"\n\n    def __init__(self) -> None:\n        super().__init__()\n\n    def commands(self):\n        \"\"\"Add beet UI commands to interact with Deezer.\"\"\"\n        deezer_update_cmd = ui.Subcommand(\n            \"deezerupdate\", help=f\"Update {self.data_source} rank\"\n        )\n\n        def func(lib: Library, opts, args):\n            items = lib.items(args)\n            self.deezerupdate(list(items), ui.should_write())\n\n        deezer_update_cmd.func = func\n\n        return [deezer_update_cmd]\n\n    def album_for_id(self, album_id: str) -> AlbumInfo | None:\n        \"\"\"Fetch an album by its Deezer ID or URL.\"\"\"\n        if not (deezer_id := self._extract_id(album_id)):\n            return None\n\n        album_url = f\"{self.album_url}{deezer_id}\"\n        if not (album_data := self.fetch_data(album_url)):\n            return None\n\n        contributors = album_data.get(\"contributors\")\n        if contributors is not None:\n            artist, artist_id = self.get_artist(contributors)\n        else:\n            artist, artist_id = None, None\n\n        release_date = album_data[\"release_date\"]\n        date_parts = [int(part) for part in release_date.split(\"-\")]\n        num_date_parts = len(date_parts)\n\n        if num_date_parts == 3:\n            year, month, day = date_parts\n        elif num_date_parts == 2:\n            year, month = date_parts\n            day = None\n        elif num_date_parts == 1:\n            year = date_parts[0]\n            month = None\n            day = None\n        else:\n            raise ui.UserError(\n                f\"Invalid `release_date` returned by {self.data_source} API: \"\n                f\"{release_date!r}\"\n            )\n        tracks_obj = self.fetch_data(f\"{self.album_url}{deezer_id}/tracks\")\n        if tracks_obj is None:\n            return None\n        try:\n            tracks_data = tracks_obj[\"data\"]\n        except KeyError:\n            self._log.debug(\"Error fetching album tracks for {}\", deezer_id)\n            tracks_data = None\n        if not tracks_data:\n            return None\n        while \"next\" in tracks_obj:\n            tracks_obj = requests.get(\n                tracks_obj[\"next\"],\n                timeout=10,\n            ).json()\n            tracks_data.extend(tracks_obj[\"data\"])\n\n        tracks = []\n        medium_totals: dict[int | None, int] = collections.defaultdict(int)\n        for i, track_data in enumerate(tracks_data, start=1):\n            track = self._get_track(track_data)\n            track.index = i\n            medium_totals[track.medium] += 1\n            tracks.append(track)\n        for track in tracks:\n            track.medium_total = medium_totals[track.medium]\n\n        return AlbumInfo(\n            album=album_data[\"title\"],\n            album_id=deezer_id,\n            deezer_album_id=deezer_id,\n            artist=artist,\n            artist_credit=self.get_artist([album_data[\"artist\"]])[0],\n            artist_id=artist_id,\n            tracks=tracks,\n            albumtype=album_data[\"record_type\"],\n            va=(\n                len(album_data[\"contributors\"]) == 1\n                and (artist or \"\").lower() == \"various artists\"\n            ),\n            year=year,\n            month=month,\n            day=day,\n            label=album_data[\"label\"],\n            mediums=max(filter(None, medium_totals.keys())),\n            data_source=self.data_source,\n            data_url=album_data[\"link\"],\n            cover_art_url=album_data.get(\"cover_xl\"),\n        )\n\n    def track_for_id(self, track_id: str) -> None | TrackInfo:\n        \"\"\"Fetch a track by its Deezer ID or URL and return a\n        TrackInfo object or None if the track is not found.\n\n        :param track_id: (Optional) Deezer ID or URL for the track. Either\n            ``track_id`` or ``track_data`` must be provided.\n\n        \"\"\"\n        if not (deezer_id := self._extract_id(track_id)):\n            self._log.debug(\"Invalid Deezer track_id: {}\", track_id)\n            return None\n\n        if not (track_data := self.fetch_data(f\"{self.track_url}{deezer_id}\")):\n            self._log.debug(\"Track not found: {}\", track_id)\n            return None\n\n        track = self._get_track(track_data)\n\n        # Get album's tracks to set `track.index` (position on the entire\n        # release) and `track.medium_total` (total number of tracks on\n        # the track's disc).\n        if not (\n            album_tracks_obj := self.fetch_data(\n                f\"{self.album_url}{track_data['album']['id']}/tracks\"\n            )\n        ):\n            return None\n\n        try:\n            album_tracks_data = album_tracks_obj[\"data\"]\n        except KeyError:\n            self._log.debug(\n                \"Error fetching album tracks for {}\", track_data[\"album\"][\"id\"]\n            )\n            return None\n        medium_total = 0\n        for i, track_data in enumerate(album_tracks_data, start=1):\n            if track_data[\"disk_number\"] == track.medium:\n                medium_total += 1\n                if track_data[\"id\"] == track.track_id:\n                    track.index = i\n        track.medium_total = medium_total\n        return track\n\n    def _get_track(self, track_data: JSONDict) -> TrackInfo:\n        \"\"\"Convert a Deezer track object dict to a TrackInfo object.\n\n        :param track_data: Deezer Track object dict\n        \"\"\"\n        artist, artist_id = self.get_artist(\n            track_data.get(\"contributors\", [track_data[\"artist\"]])\n        )\n        return TrackInfo(\n            title=track_data[\"title\"],\n            track_id=track_data[\"id\"],\n            deezer_track_id=track_data[\"id\"],\n            isrc=track_data.get(\"isrc\"),\n            artist=artist,\n            artist_id=artist_id,\n            length=track_data[\"duration\"],\n            index=track_data.get(\"track_position\"),\n            medium=track_data.get(\"disk_number\"),\n            deezer_track_rank=track_data.get(\"rank\"),\n            medium_index=track_data.get(\"track_position\"),\n            data_source=self.data_source,\n            data_url=track_data[\"link\"],\n            deezer_updated=time.time(),\n        )\n\n    def get_search_query_with_filters(\n        self,\n        query_type: QueryType,\n        items: Sequence[Item],\n        artist: str,\n        name: str,\n        va_likely: bool,\n    ) -> tuple[str, dict[str, str]]:\n        query = f'album:\"{name}\"' if query_type == \"album\" else name\n        if query_type == \"track\" or not va_likely:\n            query += f' artist:\"{artist}\"'\n\n        return query, {}\n\n    def get_search_response(self, params: SearchParams) -> list[IDResponse]:\n        \"\"\"Search Deezer and return the raw result payload entries.\"\"\"\n\n        response = requests.get(\n            f\"{self.search_url}{params.query_type}\",\n            params={\n                **params.filters,\n                \"q\": params.query,\n                \"limit\": str(params.limit),\n            },\n            timeout=10,\n        )\n        response.raise_for_status()\n        return response.json()[\"data\"]\n\n    def deezerupdate(self, items: Sequence[Item], write: bool):\n        \"\"\"Obtain rank information from Deezer.\"\"\"\n        for index, item in enumerate(items, start=1):\n            self._log.info(\n                \"Processing {}/{} tracks - {} \", index, len(items), item\n            )\n            try:\n                deezer_track_id = item.deezer_track_id\n            except AttributeError:\n                self._log.debug(\"No deezer_track_id present for: {}\", item)\n                continue\n            try:\n                rank = self.fetch_data(\n                    f\"{self.track_url}{deezer_track_id}\"\n                ).get(\"rank\")\n                self._log.debug(\n                    \"Deezer track: {} has {} rank\", deezer_track_id, rank\n                )\n            except Exception as e:\n                self._log.debug(\"Invalid Deezer track_id: {}\", e)\n                continue\n            item.deezer_track_rank = int(rank)\n            item.store()\n            item.deezer_updated = time.time()\n            if write:\n                item.try_write()\n\n    def fetch_data(self, url: str):\n        try:\n            response = requests.get(url, timeout=10)\n            response.raise_for_status()\n            data = response.json()\n        except requests.exceptions.RequestException as e:\n            self._log.error(\"Error fetching data from {}\\n Error: {}\", url, e)\n            return None\n        if \"error\" in data:\n            self._log.debug(\"Deezer API error: {}\", data[\"error\"][\"message\"])\n            return None\n        return data\n"
  },
  {
    "path": "beetsplug/discogs/__init__.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Adds Discogs album search support to the autotagger. Requires the\npython3-discogs-client library.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport http.client\nimport json\nimport os\nimport re\nimport socket\nimport time\nimport traceback\nfrom functools import cache, cached_property\nfrom string import ascii_lowercase\nfrom typing import TYPE_CHECKING\n\nimport confuse\nfrom discogs_client import Client, Master, Release\nfrom discogs_client.exceptions import DiscogsAPIError\nfrom requests.exceptions import ConnectionError\n\nimport beets\nimport beets.ui\nfrom beets import config, util\nfrom beets.autotag.distance import string_dist\nfrom beets.autotag.hooks import AlbumInfo, TrackInfo\nfrom beets.metadata_plugins import IDResponse, SearchApiMetadataSourcePlugin\n\nfrom .states import DISAMBIGUATION_RE, ArtistState, TracklistState\n\nif TYPE_CHECKING:\n    from collections.abc import Callable, Iterator, Sequence\n\n    from beets.library import Item\n    from beets.metadata_plugins import QueryType, SearchParams\n\n    from .types import ReleaseFormat, Track\n\nUSER_AGENT = f\"beets/{beets.__version__} +https://beets.io/\"\nAPI_KEY = \"rAzVUQYRaoFjeBjyWuWZ\"\nAPI_SECRET = \"plxtUTqoCzwxZpqdPysCwGuBSmZNdZVy\"\n\n\n# Exceptions that discogs_client should really handle but does not.\nCONNECTION_ERRORS = (\n    ConnectionError,\n    socket.error,\n    http.client.HTTPException,\n    ValueError,  # JSON decoding raises a ValueError.\n    DiscogsAPIError,\n)\n\nTRACK_INDEX_RE = re.compile(\n    r\"\"\"\n    (.*?)   # medium: everything before medium_index.\n    (\\d*?)  # medium_index: a number at the end of\n            # `position`, except if followed by a subtrack index.\n            # subtrack_index: can only be matched if medium\n            # or medium_index have been matched, and can be\n    (\n        (?<=\\w)\\.[\\w]+  # a dot followed by a string (A.1, 2.A)\n      | (?<=\\d)[A-Z]+   # a string that follows a number (1A, B2a)\n    )?\n    \"\"\",\n    re.VERBOSE,\n)\n\nFIELDS_TO_DISCOGS_KEYS = {\n    \"barcode\": \"barcode\",\n    \"catalognum\": \"catno\",\n    \"country\": \"country\",\n    \"label\": \"label\",\n    \"media\": \"format\",\n    \"year\": \"year\",\n}\n\n\nclass DiscogsPlugin(SearchApiMetadataSourcePlugin[IDResponse]):\n    def __init__(self):\n        super().__init__()\n        self.config.add(\n            {\n                \"apikey\": API_KEY,\n                \"apisecret\": API_SECRET,\n                \"tokenfile\": \"discogs_token.json\",\n                \"user_token\": \"\",\n                \"separator\": \", \",\n                \"index_tracks\": False,\n                \"append_style_genre\": False,\n                \"strip_disambiguation\": True,\n                \"featured_string\": \"Feat.\",\n                \"extra_tags\": [],\n                \"anv\": {\n                    \"artist_credit\": True,\n                    \"artist\": False,\n                    \"album_artist\": False,\n                },\n            }\n        )\n        self.config[\"apikey\"].redact = True\n        self.config[\"apisecret\"].redact = True\n        self.config[\"user_token\"].redact = True\n        self.setup()\n\n    @cached_property\n    def extra_discogs_field_by_tag(self) -> dict[str, str]:\n        \"\"\"Map configured extra tags to Discogs API search parameters.\n\n        Process user configuration to determine which additional Discogs\n        fields should be included in search queries.\n        \"\"\"\n        field_by_tag = {\n            tag: FIELDS_TO_DISCOGS_KEYS[tag]\n            for tag in self.config[\"extra_tags\"].as_str_seq()\n            if tag in FIELDS_TO_DISCOGS_KEYS\n        }\n        if field_by_tag:\n            self._log.debug(\n                \"Discogs additional search filters from tags: {}\", field_by_tag\n            )\n\n        return field_by_tag\n\n    def setup(self, session=None) -> None:\n        \"\"\"Create the `discogs_client` field. Authenticate if necessary.\"\"\"\n        c_key = self.config[\"apikey\"].as_str()\n        c_secret = self.config[\"apisecret\"].as_str()\n\n        # Try using a configured user token (bypassing OAuth login).\n        user_token = self.config[\"user_token\"].as_str()\n        if user_token:\n            # The rate limit for authenticated users goes up to 60\n            # requests per minute.\n            self.discogs_client = Client(USER_AGENT, user_token=user_token)\n            return\n\n        # Get the OAuth token from a file or log in.\n        try:\n            with open(self._tokenfile()) as f:\n                tokendata = json.load(f)\n        except OSError:\n            # No token yet. Generate one.\n            token, secret = self.authenticate(c_key, c_secret)\n        else:\n            token = tokendata[\"token\"]\n            secret = tokendata[\"secret\"]\n\n        self.discogs_client = Client(USER_AGENT, c_key, c_secret, token, secret)\n\n    def reset_auth(self) -> None:\n        \"\"\"Delete token file & redo the auth steps.\"\"\"\n        os.remove(self._tokenfile())\n        self.setup()\n\n    def _tokenfile(self) -> str:\n        \"\"\"Get the path to the JSON file for storing the OAuth token.\"\"\"\n        return self.config[\"tokenfile\"].get(confuse.Filename(in_app_dir=True))\n\n    def authenticate(self, c_key: str, c_secret: str) -> tuple[str, str]:\n        # Get the link for the OAuth page.\n        auth_client = Client(USER_AGENT, c_key, c_secret)\n        try:\n            _, _, url = auth_client.get_authorize_url()\n        except CONNECTION_ERRORS as e:\n            self._log.debug(\"connection error: {}\", e)\n            raise beets.ui.UserError(\"communication with Discogs failed\")\n\n        beets.ui.print_(\"To authenticate with Discogs, visit:\")\n        beets.ui.print_(url)\n\n        # Ask for the code and validate it.\n        code = beets.ui.input_(\"Enter the code:\")\n        try:\n            token, secret = auth_client.get_access_token(code)\n        except DiscogsAPIError:\n            raise beets.ui.UserError(\"Discogs authorization failed\")\n        except CONNECTION_ERRORS as e:\n            self._log.debug(\"connection error: {}\", e)\n            raise beets.ui.UserError(\"Discogs token request failed\")\n\n        # Save the token for later use.\n        self._log.debug(\"Discogs token {}, secret {}\", token, secret)\n        with open(self._tokenfile(), \"w\") as f:\n            json.dump({\"token\": token, \"secret\": secret}, f)\n\n        return token, secret\n\n    def get_track_from_album(\n        self, album_info: AlbumInfo, compare: Callable[[TrackInfo], float]\n    ) -> TrackInfo | None:\n        \"\"\"Return the best matching track of the release.\"\"\"\n        scores_and_tracks = [(compare(t), t) for t in album_info.tracks]\n        score, track_info = min(scores_and_tracks, key=lambda x: x[0])\n        if score > 0.3:\n            return None\n\n        track_info[\"artist\"] = album_info.artist\n        track_info[\"artist_id\"] = album_info.artist_id\n        track_info[\"album\"] = album_info.album\n        return track_info\n\n    def item_candidates(\n        self, item: Item, artist: str, title: str\n    ) -> Iterator[TrackInfo]:\n        albums = self.candidates([item], artist, title, False)\n\n        def compare_func(track_info: TrackInfo) -> float:\n            return string_dist(track_info.title, title)\n\n        tracks = (self.get_track_from_album(a, compare_func) for a in albums)\n        return filter(None, tracks)\n\n    def album_for_id(self, album_id: str) -> AlbumInfo | None:\n        \"\"\"Fetches an album by its Discogs ID and returns an AlbumInfo object\n        or None if the album is not found.\n        \"\"\"\n        discogs_id = self._extract_id(album_id)\n\n        if not discogs_id:\n            return None\n\n        result = Release(self.discogs_client, {\"id\": discogs_id})\n        # Try to obtain title to verify that we indeed have a valid Release\n        try:\n            getattr(result, \"title\")\n        except DiscogsAPIError as e:\n            if e.status_code != 404:\n                self._log.debug(\n                    \"API Error: {} (query: {})\",\n                    e,\n                    result.data[\"resource_url\"],\n                )\n                if e.status_code == 401:\n                    self.reset_auth()\n                    return self.album_for_id(album_id)\n            return None\n        except CONNECTION_ERRORS:\n            self._log.debug(\"Connection error in album lookup\", exc_info=True)\n            return None\n        return self.get_album_info(result)\n\n    def track_for_id(self, track_id: str) -> TrackInfo | None:\n        if album := self.album_for_id(track_id):\n            for track in album.tracks:\n                if track.track_id == track_id:\n                    return track\n        return None\n\n    def get_search_query_with_filters(\n        self,\n        query_type: QueryType,\n        items: Sequence[Item],\n        artist: str,\n        name: str,\n        va_likely: bool,\n    ) -> tuple[str, dict[str, str]]:\n        \"\"\"Build a Discogs release query and fixed release-type filter.\n\n        The query is normalized to improve hit rates for punctuation-heavy album\n        names and medium suffixes that can reduce recall.\n        \"\"\"\n\n        query = f\"{artist} {name}\" if va_likely else name\n        # Strip non-word characters from query. Things like \"!\" and \"-\" can\n        # cause a query to return no results, even if they match the artist or\n        # album title. Use `re.UNICODE` flag to avoid stripping non-english\n        # word characters.\n        query = re.sub(r\"(?u)\\W+\", \" \", query)\n        # Strip medium information from query, Things like \"CD1\" and \"disk 1\"\n        # can also negate an otherwise positive result.\n        query = re.sub(r\"(?i)\\b(CD|disc|vinyl)\\s*\\d+\", \"\", query)\n\n        filters: dict[str, str] = {\"type\": \"release\"}\n\n        if not items:\n            return query, filters\n\n        for tag, api_field in self.extra_discogs_field_by_tag.items():\n            most_common, _count = util.plurality(\n                item.get(tag) for item in items\n            )\n            if most_common is None:\n                continue\n\n            value = str(most_common)\n            if tag == \"catalognum\":\n                value = value.replace(\" \", \"\")\n\n            filters[api_field] = value\n\n        return query, filters\n\n    def get_search_response(self, params: SearchParams) -> Sequence[IDResponse]:\n        \"\"\"Search Discogs releases and return raw result mappings with IDs.\"\"\"\n        results = self.discogs_client.search(params.query, **params.filters)\n        results.per_page = params.limit\n        return [r.data for r in results.page(1)]\n\n    @cache\n    def get_master_year(self, master_id: str) -> int | None:\n        \"\"\"Fetches a master release given its Discogs ID and returns its year\n        or None if the master release is not found.\n        \"\"\"\n        self._log.debug(\"Getting master release {}\", master_id)\n        result = Master(self.discogs_client, {\"id\": master_id})\n\n        try:\n            return result.fetch(\"year\")\n        except DiscogsAPIError as e:\n            if e.status_code != 404:\n                self._log.debug(\n                    \"API Error: {} (query: {})\",\n                    e,\n                    result.data[\"resource_url\"],\n                )\n                if e.status_code == 401:\n                    self.reset_auth()\n                    return self.get_master_year(master_id)\n            return None\n        except CONNECTION_ERRORS:\n            self._log.debug(\n                \"Connection error in master release lookup\", exc_info=True\n            )\n            return None\n\n    @staticmethod\n    def get_media_and_albumtype(\n        formats: list[ReleaseFormat] | None,\n    ) -> tuple[str | None, str | None]:\n        media = albumtype = None\n        if formats and (first_format := formats[0]):\n            if descriptions := first_format[\"descriptions\"]:\n                albumtype = \", \".join(descriptions)\n            media = first_format[\"name\"]\n\n        return media, albumtype\n\n    def get_album_info(self, result: Release) -> AlbumInfo | None:\n        \"\"\"Returns an AlbumInfo object for a discogs Release object.\"\"\"\n        # Explicitly reload the `Release` fields, as they might not be yet\n        # present if the result is from a `discogs_client.search()`.\n        if not result.data.get(\"artists\"):\n            try:\n                result.refresh()\n            except CONNECTION_ERRORS:\n                self._log.debug(\n                    \"Connection error in release lookup: {0}\",\n                    result,\n                )\n                return None\n\n        # Sanity check for required fields. The list of required fields is\n        # defined at Guideline 1.3.1.a, but in practice some releases might be\n        # lacking some of these fields. This function expects at least:\n        # `artists` (>0), `title`, `id`, `tracklist` (>0)\n        # https://www.discogs.com/help/doc/submission-guidelines-general-rules\n        if not all(\n            [\n                result.data.get(k)\n                for k in [\"artists\", \"title\", \"id\", \"tracklist\"]\n            ]\n        ):\n            self._log.warning(\"Release does not contain the required fields\")\n            return None\n\n        artist_data = [a.data for a in result.artists]\n        # Information for the album artist\n        albumartist = ArtistState.from_config(\n            self.config, artist_data, for_album_artist=True\n        )\n\n        album = re.sub(r\" +\", \" \", result.title)\n        album_id = result.data[\"id\"]\n        # Use `.data` to access the tracklist directly instead of the\n        # convenient `.tracklist` property, which will strip out useful artist\n        # information and leave us with skeleton `Artist` objects that will\n        # each make an API call just to get the same data back.\n        tracks = self.get_tracks(\n            result.data[\"tracklist\"],\n            ArtistState.from_config(self.config, artist_data),\n        )\n\n        # Extract information for the optional AlbumInfo fields, if possible.\n        va = albumartist.artist == config[\"va_name\"].as_str()\n        year = result.data.get(\"year\")\n        mediums = [t[\"medium\"] for t in tracks]\n        country = result.data.get(\"country\")\n        data_url = result.data.get(\"uri\")\n        styles: list[str] = result.data.get(\"styles\") or []\n        genres: list[str] = result.data.get(\"genres\") or []\n\n        if self.config[\"append_style_genre\"]:\n            genres.extend(styles)\n\n        discogs_albumid = self._extract_id(result.data.get(\"uri\"))\n\n        # Extract information for the optional AlbumInfo fields that are\n        # contained on nested discogs fields.\n        media, albumtype = self.get_media_and_albumtype(\n            result.data.get(\"formats\")\n        )\n\n        label = catalogno = labelid = None\n        if result.data.get(\"labels\"):\n            label = self.strip_disambiguation(\n                result.data[\"labels\"][0].get(\"name\")\n            )\n            catalogno = result.data[\"labels\"][0].get(\"catno\")\n            labelid = result.data[\"labels\"][0].get(\"id\")\n\n        cover_art_url = self.select_cover_art(result)\n\n        # Additional cleanups\n        # (catalog number, media, disambiguation).\n        if catalogno == \"none\":\n            catalogno = None\n        # Explicitly set the `media` for the tracks, since it is expected by\n        # `autotag.apply_metadata`, and set `medium_total`.\n        for track in tracks:\n            track.media = media\n            track.medium_total = mediums.count(track.medium)\n            # Discogs does not have track IDs. Invent our own IDs as proposed\n            # in #2336.\n            track.track_id = f\"{album_id}-{track.track_alt}\"\n            track.data_url = data_url\n            track.data_source = \"Discogs\"\n\n        # Retrieve master release id (returns None if there isn't one).\n        master_id = result.data.get(\"master_id\")\n        # Assume `original_year` is equal to `year` for releases without\n        # a master release, otherwise fetch the master release.\n        original_year = self.get_master_year(master_id) if master_id else year\n\n        return AlbumInfo(\n            album=album,\n            album_id=album_id,\n            **albumartist.info,  # Unpacks values to satisfy the keyword arguments\n            tracks=tracks,\n            albumtype=albumtype,\n            va=va,\n            year=year,\n            label=label,\n            mediums=len(set(mediums)),\n            releasegroup_id=master_id,\n            catalognum=catalogno,\n            country=country,\n            style=(\n                self.config[\"separator\"].as_str().join(sorted(styles)) or None\n            ),\n            genres=sorted(genres),\n            media=media,\n            original_year=original_year,\n            data_source=self.data_source,\n            data_url=data_url,\n            discogs_albumid=discogs_albumid,\n            discogs_labelid=labelid,\n            discogs_artistid=albumartist.artist_id,\n            cover_art_url=cover_art_url,\n        )\n\n    def select_cover_art(self, result: Release) -> str | None:\n        \"\"\"Returns the best candidate image, if any, from a Discogs `Release` object.\"\"\"\n        if result.data.get(\"images\") and len(result.data.get(\"images\")) > 0:\n            # The first image in this list appears to be the one displayed first\n            # on the release page - even if it is not flagged as `type: \"primary\"` - and\n            # so it is the best candidate for the cover art.\n            return result.data.get(\"images\")[0].get(\"uri\")\n\n        return None\n\n    def get_tracks(\n        self,\n        tracklist: list[Track],\n        albumartistinfo: ArtistState,\n    ) -> list[TrackInfo]:\n        \"\"\"Returns a list of TrackInfo objects for a discogs tracklist.\"\"\"\n        try:\n            clean_tracklist: list[Track] = self._coalesce_tracks(tracklist)\n        except Exception as exc:\n            # FIXME: this is an extra precaution for making sure there are no\n            # side effects after #2222. It should be removed after further\n            # testing.\n            self._log.debug(\"{}\", traceback.format_exc())\n            self._log.error(\"uncaught exception in _coalesce_tracks: {}\", exc)\n            clean_tracklist = tracklist\n        t = TracklistState.build(self, clean_tracklist, albumartistinfo)\n        # Fix up medium and medium_index for each track. Discogs position is\n        # unreliable, but tracks are in order.\n        medium = None\n        medium_count, index_count, side_count = 0, 0, 0\n        sides_per_medium = 1\n\n        # If a medium has two sides (ie. vinyl or cassette), each pair of\n        # consecutive sides should belong to the same medium.\n        if all([medium is not None for medium in t.mediums]):\n            m = sorted(\n                {medium.lower() if medium else \"\" for medium in t.mediums}\n            )\n            # If all track.medium are single consecutive letters, assume it is\n            # a 2-sided medium.\n            if \"\".join(m) in ascii_lowercase:\n                sides_per_medium = 2\n\n        for i, track in enumerate(t.tracks):\n            # Handle special case where a different medium does not indicate a\n            # new disc, when there is no medium_index and the ordinal of medium\n            # is not sequential. For example, I, II, III, IV, V. Assume these\n            # are the track index, not the medium.\n            # side_count is the number of mediums or medium sides (in the case\n            # of two-sided mediums) that were seen before.\n            medium_str = t.mediums[i]\n            medium_index = t.medium_indices[i]\n            medium_is_index = (\n                medium_str\n                and not medium_index\n                and (\n                    len(medium_str) != 1\n                    or\n                    # Not within standard incremental medium values (A, B, C, ...).\n                    ord(medium_str) - 64 != side_count + 1\n                )\n            )\n\n            if not medium_is_index and medium != medium_str:\n                side_count += 1\n                if sides_per_medium == 2:\n                    if side_count % sides_per_medium:\n                        # Two-sided medium changed. Reset index_count.\n                        index_count = 0\n                        medium_count += 1\n                else:\n                    # Medium changed. Reset index_count.\n                    medium_count += 1\n                    index_count = 0\n                medium = medium_str\n\n            index_count += 1\n            medium_count = 1 if medium_count == 0 else medium_count\n            track.medium, track.medium_index = medium_count, index_count\n\n        # Get `disctitle` from Discogs index tracks. Assume that an index track\n        # before the first track of each medium is a disc title.\n        for track in t.tracks:\n            if track.medium_index == 1:\n                if track.index in t.index_tracks:\n                    disctitle = t.index_tracks[track.index]\n                else:\n                    disctitle = None\n            track.disctitle = disctitle\n\n        return t.tracks\n\n    def _coalesce_tracks(self, raw_tracklist: list[Track]) -> list[Track]:\n        \"\"\"Pre-process a tracklist, merging subtracks into a single track. The\n        title for the merged track is the one from the previous index track,\n        if present; otherwise it is a combination of the subtracks titles.\n        \"\"\"\n        # Pre-process the tracklist, trying to identify subtracks.\n\n        subtracks: list[Track] = []\n        tracklist: list[Track] = []\n        prev_subindex = \"\"\n        for track in raw_tracklist:\n            # Regular subtrack (track with subindex).\n            if track[\"position\"]:\n                _, _, subindex = self.get_track_index(track[\"position\"])\n                if subindex:\n                    if subindex.rjust(len(raw_tracklist)) > prev_subindex:\n                        # Subtrack still part of the current main track.\n                        subtracks.append(track)\n                    else:\n                        # Subtrack part of a new group (..., 1.3, *2.1*, ...).\n                        self._add_merged_subtracks(tracklist, subtracks)\n                        subtracks = [track]\n                    prev_subindex = subindex.rjust(len(raw_tracklist))\n                    continue\n\n            # Index track with nested sub_tracks.\n            if not track[\"position\"] and \"sub_tracks\" in track:\n                # Append the index track, assuming it contains the track title.\n                tracklist.append(track)\n                self._add_merged_subtracks(tracklist, track[\"sub_tracks\"])\n                continue\n\n            # Regular track or index track without nested sub_tracks.\n            if subtracks:\n                self._add_merged_subtracks(tracklist, subtracks)\n                subtracks = []\n                prev_subindex = \"\"\n            tracklist.append(track)\n\n        # Merge and add the remaining subtracks, if any.\n        if subtracks:\n            self._add_merged_subtracks(tracklist, subtracks)\n\n        return tracklist\n\n    def _add_merged_subtracks(\n        self,\n        tracklist: list[Track],\n        subtracks: list[Track],\n    ) -> None:\n        \"\"\"Modify `tracklist` in place, merging a list of `subtracks` into\n        a single track into `tracklist`.\"\"\"\n        # Calculate position based on first subtrack, without subindex.\n        idx, medium_idx, sub_idx = self.get_track_index(\n            subtracks[0][\"position\"]\n        )\n        position = f\"{idx or ''}{medium_idx or ''}\"\n\n        if tracklist and not tracklist[-1][\"position\"]:\n            # Assume the previous index track contains the track title.\n            if sub_idx:\n                # \"Convert\" the track title to a real track, discarding the\n                # subtracks assuming they are logical divisions of a\n                # physical track (12.2.9 Subtracks).\n                tracklist[-1][\"position\"] = position\n            else:\n                # Promote the subtracks to real tracks, discarding the\n                # index track, assuming the subtracks are physical tracks.\n                index_track = tracklist.pop()\n                # Fix artists when they are specified on the index track.\n                if index_track.get(\"artists\"):\n                    for subtrack in subtracks:\n                        if not subtrack.get(\"artists\"):\n                            subtrack[\"artists\"] = index_track[\"artists\"]\n                # Concatenate index with track title when index_tracks\n                # option is set\n                if self.config[\"index_tracks\"]:\n                    for subtrack in subtracks:\n                        subtrack[\"title\"] = (\n                            f\"{index_track['title']}: {subtrack['title']}\"\n                        )\n                tracklist.extend(subtracks)\n        else:\n            # Merge the subtracks, pick a title, and append the new track.\n            track = subtracks[0].copy()\n            track[\"title\"] = \" / \".join([t[\"title\"] for t in subtracks])\n            tracklist.append(track)\n\n    def strip_disambiguation(self, text: str) -> str:\n        \"\"\"Removes discogs specific disambiguations from a string.\n        Turns 'Label Name (5)' to 'Label Name' or 'Artist (1) & Another Artist (2)'\n        to 'Artist & Another Artist'. Does nothing if strip_disambiguation is False.\"\"\"\n        if not self.config[\"strip_disambiguation\"]:\n            return text\n        return DISAMBIGUATION_RE.sub(\"\", text)\n\n    def get_track_info(\n        self,\n        track: Track,\n        index: int,\n        divisions: list[str],\n        albumartistinfo: ArtistState,\n    ) -> tuple[TrackInfo, str | None, str | None]:\n        \"\"\"Returns a TrackInfo object for a discogs track.\"\"\"\n\n        title = track[\"title\"]\n        if self.config[\"index_tracks\"]:\n            prefix = \", \".join(divisions)\n            if prefix:\n                title = f\"{prefix}: {title}\"\n        track_id = None\n        medium, medium_index, _ = self.get_track_index(track[\"position\"])\n\n        length = self.get_track_length(track[\"duration\"])\n        # If artists are found on the track, we will use those instead\n        artistinfo = ArtistState.from_config(\n            self.config,\n            [\n                *(track.get(\"artists\") or albumartistinfo.raw_artists),\n                *track.get(\"extraartists\", []),\n            ],\n        )\n\n        return (\n            TrackInfo(\n                title=title,\n                track_id=track_id,\n                **artistinfo.info,\n                length=length,\n                index=index,\n            ),\n            medium,\n            medium_index,\n        )\n\n    @staticmethod\n    def get_track_index(\n        position: str,\n    ) -> tuple[str | None, str | None, str | None]:\n        \"\"\"Returns the medium, medium index and subtrack index for a discogs\n        track position.\"\"\"\n        # Match the standard Discogs positions (12.2.9), which can have several\n        # forms (1, 1-1, A1, A1.1, A1a, ...).\n        medium = index = subindex = None\n        if match := TRACK_INDEX_RE.fullmatch(position.upper()):\n            medium, index, subindex = match.groups()\n\n            if subindex and subindex.startswith(\".\"):\n                subindex = subindex[1:]\n\n        return medium or None, index or None, subindex or None\n\n    def get_track_length(self, duration: str) -> int | None:\n        \"\"\"Returns the track length in seconds for a discogs duration.\"\"\"\n        try:\n            length = time.strptime(duration, \"%M:%S\")\n        except ValueError:\n            return None\n        return length.tm_min * 60 + length.tm_sec\n"
  },
  {
    "path": "beetsplug/discogs/states.py",
    "content": "# This file is part of beets.\n# Copyright 2025, Sarunas Nejus, Henry Oberholtzer.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Dataclasses for managing artist credits and tracklists from Discogs.\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom dataclasses import asdict, dataclass, field\nfrom functools import cached_property\nfrom typing import TYPE_CHECKING, NamedTuple\n\nfrom beets import config\n\nfrom .types import ArtistInfo\n\nif TYPE_CHECKING:\n    from confuse import ConfigView\n\n    from beets.autotag.hooks import TrackInfo\n\n    from . import DiscogsPlugin\n    from .types import Artist, Track, TracklistInfo\n\nDISAMBIGUATION_RE = re.compile(r\" \\(\\d+\\)\")\n\n\n@dataclass\nclass ArtistState:\n    \"\"\"Represent Discogs artist credits.\n\n    This object centralizes the plugin's policy for which Discogs artist fields\n    to prefer (name vs. ANV), how to treat 'Various', how to format join\n    phrases, and how to separate featured artists. It exposes both per-artist\n    components and fully joined strings for common tag targets like 'artist' and\n    'artist_credit'.\n    \"\"\"\n\n    class ValidArtist(NamedTuple):\n        \"\"\"A normalized, render-ready artist entry extracted from Discogs data.\n\n        Instances represent the subset of Discogs artist information needed for\n        tagging, including the join token following the artist and whether the\n        entry is considered a featured appearance.\n        \"\"\"\n\n        id: str\n        name: str\n        credit: str\n        join: str\n        is_feat: bool\n\n        def get_artist(self, property_name: str) -> str:\n            \"\"\"Return the requested display field with its trailing join token.\n\n            The join token is normalized so commas become ', ' and other join\n            phrases are surrounded with spaces, producing a single fragment that\n            can be concatenated to form a full artist string.\n            \"\"\"\n            join = {\",\": \", \", \"\": \"\"}.get(self.join, f\" {self.join} \")\n            return f\"{getattr(self, property_name)}{join}\"\n\n    raw_artists: list[Artist]\n    use_anv: bool\n    use_credit_anv: bool\n    featured_string: str\n    should_strip_disambiguation: bool\n\n    @property\n    def info(self) -> ArtistInfo:\n        \"\"\"Expose the state in the shape expected by downstream tag mapping.\"\"\"\n        return {k: getattr(self, k) for k in ArtistInfo.__annotations__}  # type: ignore[return-value]\n\n    def strip_disambiguation(self, text: str) -> str:\n        \"\"\"Strip Discogs disambiguation suffixes from an artist or label string.\n\n        This removes Discogs-specific numeric suffixes like 'Name (5)' and can\n        be applied to multi-artist strings as well (e.g., 'A (1) & B (2)'). When\n        the feature is disabled, the input is returned unchanged.\n        \"\"\"\n        if self.should_strip_disambiguation:\n            return DISAMBIGUATION_RE.sub(\"\", text)\n        return text\n\n    @cached_property\n    def valid_artists(self) -> list[ValidArtist]:\n        \"\"\"Build the ordered, filtered list of artists used for rendering.\n\n        The resulting list normalizes Discogs entries by:\n        - substituting the configured 'Various Artists' name when Discogs uses\n          'Various'\n        - choosing between name and ANV according to plugin settings\n        - excluding non-empty roles unless they indicate a featured appearance\n        - capturing join tokens so the original credit formatting is preserved\n        \"\"\"\n        va_name = config[\"va_name\"].as_str()\n        return [\n            self.ValidArtist(\n                str(a[\"id\"]),\n                self.strip_disambiguation(anv if self.use_anv else name),\n                self.strip_disambiguation(anv if self.use_credit_anv else name),\n                a[\"join\"].strip(),\n                is_feat,\n            )\n            for a in self.raw_artists\n            if (\n                (name := va_name if a[\"name\"] == \"Various\" else a[\"name\"])\n                and (anv := a[\"anv\"] or name)\n                and (\n                    (is_feat := (\"featuring\" in a[\"role\"].lower()))\n                    or not a[\"role\"]\n                )\n            )\n        ]\n\n    @property\n    def artists_ids(self) -> list[str]:\n        \"\"\"Return Discogs artist IDs for all valid artists, preserving order.\"\"\"\n        return [a.id for a in self.valid_artists]\n\n    @property\n    def artist_id(self) -> str:\n        \"\"\"Return the primary Discogs artist ID.\"\"\"\n        return self.artists_ids[0]\n\n    @property\n    def artists(self) -> list[str]:\n        \"\"\"Return the per-artist display names used for the 'artist' field.\"\"\"\n        return [a.name for a in self.valid_artists]\n\n    @property\n    def artists_credit(self) -> list[str]:\n        \"\"\"Return the per-artist display names used for the credit field.\"\"\"\n        return [a.credit for a in self.valid_artists]\n\n    @property\n    def artist(self) -> str:\n        \"\"\"Return the fully rendered artist string using display names.\"\"\"\n        return self.join_artists(\"name\")\n\n    @property\n    def artist_credit(self) -> str:\n        \"\"\"Return the fully rendered artist credit string.\"\"\"\n        return self.join_artists(\"credit\")\n\n    def join_artists(self, property_name: str) -> str:\n        \"\"\"Render a single artist string with join phrases and featured artists.\n\n        Non-featured artists are concatenated using their join tokens. Featured\n        artists are appended after the configured 'featured' marker, preserving\n        Discogs order while keeping featured credits separate from the main\n        artist string.\n        \"\"\"\n        non_featured = [a for a in self.valid_artists if not a.is_feat]\n        featured = [a for a in self.valid_artists if a.is_feat]\n\n        artist = \"\".join(a.get_artist(property_name) for a in non_featured)\n        if featured:\n            if \"feat\" not in artist:\n                artist += f\" {self.featured_string} \"\n\n            artist += \", \".join(a.get_artist(property_name) for a in featured)\n\n        return artist\n\n    @classmethod\n    def from_config(\n        cls,\n        config: ConfigView,\n        artists: list[Artist],\n        for_album_artist: bool = False,\n    ) -> ArtistState:\n        return cls(\n            artists,\n            config[\"anv\"][\"album_artist\" if for_album_artist else \"artist\"].get(\n                bool\n            ),\n            config[\"anv\"][\"artist_credit\"].get(bool),\n            config[\"featured_string\"].as_str(),\n            config[\"strip_disambiguation\"].get(bool),\n        )\n\n\n@dataclass\nclass TracklistState:\n    index: int = 0\n    index_tracks: dict[int, str] = field(default_factory=dict)\n    tracks: list[TrackInfo] = field(default_factory=list)\n    divisions: list[str] = field(default_factory=list)\n    next_divisions: list[str] = field(default_factory=list)\n    mediums: list[str | None] = field(default_factory=list)\n    medium_indices: list[str | None] = field(default_factory=list)\n\n    @property\n    def info(self) -> TracklistInfo:\n        return asdict(self)  # type: ignore[return-value]\n\n    @classmethod\n    def build(\n        cls,\n        plugin: DiscogsPlugin,\n        clean_tracklist: list[Track],\n        albumartistinfo: ArtistState,\n    ) -> TracklistState:\n        state = cls()\n        for track in clean_tracklist:\n            if track[\"position\"]:\n                state.index += 1\n                if state.next_divisions:\n                    state.divisions += state.next_divisions\n                    state.next_divisions.clear()\n                track_info, medium, medium_index = plugin.get_track_info(\n                    track, state.index, state.divisions, albumartistinfo\n                )\n                track_info.track_alt = track[\"position\"]\n                state.tracks.append(track_info)\n                state.mediums.append(medium or None)\n                state.medium_indices.append(medium_index or None)\n            else:\n                state.next_divisions.append(track[\"title\"])\n                try:\n                    state.divisions.pop()\n                except IndexError:\n                    pass\n                state.index_tracks[state.index + 1] = track[\"title\"]\n        return state\n"
  },
  {
    "path": "beetsplug/discogs/types.py",
    "content": "# This file is part of beets.\n# Copyright 2025, Sarunas Nejus, Henry Oberholtzer.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nfrom typing_extensions import NotRequired, TypedDict\n\nif TYPE_CHECKING:\n    from beets.autotag.hooks import TrackInfo\n\n\nclass ReleaseFormat(TypedDict):\n    name: str\n    qty: int\n    descriptions: list[str] | None\n\n\nclass Artist(TypedDict):\n    name: str\n    anv: str\n    join: str\n    role: str\n    tracks: str\n    id: str\n    resource_url: str\n\n\nclass Track(TypedDict):\n    position: str\n    type_: str\n    title: str\n    duration: str\n    artists: list[Artist]\n    extraartists: NotRequired[list[Artist]]\n    sub_tracks: NotRequired[list[Track]]\n\n\nclass ArtistInfo(TypedDict):\n    artist: str\n    artists: list[str]\n    artist_credit: str\n    artists_credit: list[str]\n    artist_id: str\n    artists_ids: list[str]\n\n\nclass TracklistInfo(TypedDict):\n    index: int\n    index_tracks: dict[int, str]\n    tracks: list[TrackInfo]\n    divisions: list[str]\n    next_divisions: list[str]\n    mediums: list[str | None]\n    medium_indices: list[str | None]\n"
  },
  {
    "path": "beetsplug/duplicates.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Pedro Silva.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"List duplicate tracks or albums.\"\"\"\n\nimport os\nimport shlex\n\nfrom beets.library import Album, Item\nfrom beets.plugins import BeetsPlugin\nfrom beets.ui import Subcommand, UserError, print_\nfrom beets.util import (\n    MoveOperation,\n    bytestring_path,\n    command_output,\n    displayable_path,\n    subprocess,\n)\n\nPLUGIN = \"duplicates\"\n\n\nclass DuplicatesPlugin(BeetsPlugin):\n    \"\"\"List duplicate tracks or albums\"\"\"\n\n    def __init__(self):\n        super().__init__()\n\n        self.config.add(\n            {\n                \"album\": False,\n                \"checksum\": \"\",\n                \"copy\": \"\",\n                \"count\": False,\n                \"delete\": False,\n                \"format\": \"\",\n                \"full\": False,\n                \"keys\": [],\n                \"merge\": False,\n                \"move\": \"\",\n                \"path\": False,\n                \"tiebreak\": {},\n                \"strict\": False,\n                \"tag\": \"\",\n                \"remove\": False,\n            }\n        )\n\n        self._command = Subcommand(\"duplicates\", help=__doc__, aliases=[\"dup\"])\n        self._command.parser.add_option(\n            \"-c\",\n            \"--count\",\n            dest=\"count\",\n            action=\"store_true\",\n            help=\"show duplicate counts\",\n        )\n        self._command.parser.add_option(\n            \"-C\",\n            \"--checksum\",\n            dest=\"checksum\",\n            action=\"store\",\n            metavar=\"PROG\",\n            help=\"report duplicates based on arbitrary command\",\n        )\n        self._command.parser.add_option(\n            \"-d\",\n            \"--delete\",\n            dest=\"delete\",\n            action=\"store_true\",\n            help=\"delete items from library and disk\",\n        )\n        self._command.parser.add_option(\n            \"-F\",\n            \"--full\",\n            dest=\"full\",\n            action=\"store_true\",\n            help=\"show all versions of duplicate tracks or albums\",\n        )\n        self._command.parser.add_option(\n            \"-s\",\n            \"--strict\",\n            dest=\"strict\",\n            action=\"store_true\",\n            help=\"report duplicates only if all attributes are set\",\n        )\n        self._command.parser.add_option(\n            \"-k\",\n            \"--key\",\n            dest=\"keys\",\n            action=\"append\",\n            metavar=\"KEY\",\n            help=\"report duplicates based on keys (use multiple times)\",\n        )\n        self._command.parser.add_option(\n            \"-M\",\n            \"--merge\",\n            dest=\"merge\",\n            action=\"store_true\",\n            help=\"merge duplicate items\",\n        )\n        self._command.parser.add_option(\n            \"-m\",\n            \"--move\",\n            dest=\"move\",\n            action=\"store\",\n            metavar=\"DEST\",\n            help=\"move items to dest\",\n        )\n        self._command.parser.add_option(\n            \"-o\",\n            \"--copy\",\n            dest=\"copy\",\n            action=\"store\",\n            metavar=\"DEST\",\n            help=\"copy items to dest\",\n        )\n        self._command.parser.add_option(\n            \"-t\",\n            \"--tag\",\n            dest=\"tag\",\n            action=\"store\",\n            help=\"tag matched items with 'k=v' attribute\",\n        )\n        self._command.parser.add_option(\n            \"-r\",\n            \"--remove\",\n            dest=\"remove\",\n            action=\"store_true\",\n            help=\"remove items from library\",\n        )\n        self._command.parser.add_all_common_options()\n\n    def commands(self):\n        def _dup(lib, opts, args):\n            self.config.set_args(opts)\n            album = self.config[\"album\"].get(bool)\n            checksum = self.config[\"checksum\"].get(str)\n            copy = bytestring_path(self.config[\"copy\"].as_str())\n            count = self.config[\"count\"].get(bool)\n            delete = self.config[\"delete\"].get(bool)\n            remove = self.config[\"remove\"].get(bool)\n            fmt_tmpl = self.config[\"format\"].get(str)\n            full = self.config[\"full\"].get(bool)\n            keys = self.config[\"keys\"].as_str_seq()\n            merge = self.config[\"merge\"].get(bool)\n            move = bytestring_path(self.config[\"move\"].as_str())\n            path = self.config[\"path\"].get(bool)\n            tiebreak = self.config[\"tiebreak\"].get(dict)\n            strict = self.config[\"strict\"].get(bool)\n            tag = self.config[\"tag\"].get(str)\n\n            if album:\n                if not keys:\n                    keys = [\"mb_albumid\"]\n                items = lib.albums(args)\n            else:\n                if not keys:\n                    keys = [\"mb_trackid\", \"mb_albumid\"]\n                items = lib.items(args)\n\n            # If there's nothing to do, return early. The code below assumes\n            # `items` to be non-empty.\n            if not items:\n                return\n\n            if path:\n                fmt_tmpl = \"$path\"\n\n            # Default format string for count mode.\n            if count and not fmt_tmpl:\n                if album:\n                    fmt_tmpl = \"$albumartist - $album\"\n                else:\n                    fmt_tmpl = \"$albumartist - $album - $title\"\n\n            if checksum:\n                for i in items:\n                    k, _ = self._checksum(i, checksum)\n                keys = [k]\n\n            for obj_id, obj_count, objs in self._duplicates(\n                items,\n                keys=keys,\n                full=full,\n                strict=strict,\n                tiebreak=tiebreak,\n                merge=merge,\n            ):\n                if obj_id:  # Skip empty IDs.\n                    for o in objs:\n                        self._process_item(\n                            o,\n                            copy=copy,\n                            move=move,\n                            delete=delete,\n                            remove=remove,\n                            tag=tag,\n                            fmt=f\"{fmt_tmpl}: {obj_count}\",\n                        )\n\n        self._command.func = _dup\n        return [self._command]\n\n    def _process_item(\n        self,\n        item,\n        copy=False,\n        move=False,\n        delete=False,\n        tag=False,\n        fmt=\"\",\n        remove=False,\n    ):\n        \"\"\"Process Item `item`.\"\"\"\n        print_(format(item, fmt))\n        if copy:\n            item.move(basedir=copy, operation=MoveOperation.COPY)\n            item.store()\n        if move:\n            item.move(basedir=move)\n            item.store()\n        if delete:\n            item.remove(delete=True)\n        elif remove:\n            item.remove(delete=False)\n        if tag:\n            try:\n                k, v = tag.split(\"=\")\n            except Exception:\n                raise UserError(f\"{PLUGIN}: can't parse k=v tag: {tag}\")\n            setattr(item, k, v)\n            item.store()\n\n    def _checksum(self, item, prog):\n        \"\"\"Run external `prog` on file path associated with `item`, cache\n        output as flexattr on a key that is the name of the program, and\n        return the key, checksum tuple.\n        \"\"\"\n        args = [\n            p.format(file=os.fsdecode(item.path)) for p in shlex.split(prog)\n        ]\n        key = args[0]\n        checksum = getattr(item, key, False)\n        if not checksum:\n            self._log.debug(\n                \"key {} on item {.filepath} not cached:computing checksum\",\n                key,\n                item,\n            )\n            try:\n                checksum = command_output(args).stdout\n                setattr(item, key, checksum)\n                item.store()\n                self._log.debug(\n                    \"computed checksum for {.title} using {}\", item, key\n                )\n            except subprocess.CalledProcessError as e:\n                self._log.debug(\"failed to checksum {.filepath}: {}\", item, e)\n        else:\n            self._log.debug(\n                \"key {} on item {.filepath} cached:not computing checksum\",\n                key,\n                item,\n            )\n        return key, checksum\n\n    def _group_by(self, objs, keys, strict):\n        \"\"\"Return a dictionary with keys arbitrary concatenations of attributes\n        and values lists of objects (Albums or Items) with those keys.\n\n        If strict, all attributes must be defined for a duplicate match.\n        \"\"\"\n        import collections\n\n        counts = collections.defaultdict(list)\n        for obj in objs:\n            values = [getattr(obj, k, None) for k in keys]\n            values = [v for v in values if v not in (None, \"\")]\n            if strict and len(values) < len(keys):\n                self._log.debug(\n                    \"some keys {} on item {.filepath} are null or empty: skipping\",\n                    keys,\n                    obj,\n                )\n            elif not strict and not len(values):\n                self._log.debug(\n                    \"all keys {} on item {.filepath} are null or empty: skipping\",\n                    keys,\n                    obj,\n                )\n            else:\n                key = tuple(values)\n                counts[key].append(obj)\n\n        return counts\n\n    def _order(self, objs, tiebreak=None):\n        \"\"\"Return the objects (Items or Albums) sorted by descending\n        order of priority.\n\n        If provided, the `tiebreak` dict indicates the field to use to\n        prioritize the objects. Otherwise, Items are placed in order of\n        \"completeness\" (objects with more non-null fields come first)\n        and Albums are ordered by their track count.\n        \"\"\"\n        kind = \"items\" if all(isinstance(o, Item) for o in objs) else \"albums\"\n\n        if tiebreak and kind in tiebreak.keys():\n\n            def key(x):\n                return tuple(getattr(x, k) for k in tiebreak[kind])\n        else:\n            if kind == \"items\":\n\n                def truthy(v):\n                    # Avoid a Unicode warning by avoiding comparison\n                    # between a bytes object and the empty Unicode\n                    # string ''.\n                    return v is not None and (\n                        v != \"\" if isinstance(v, str) else True\n                    )\n\n                fields = Item.all_keys()\n\n                def key(x):\n                    return sum(1 for f in fields if truthy(getattr(x, f)))\n            else:\n\n                def key(x):\n                    return len(x.items())\n\n        return sorted(objs, key=key, reverse=True)\n\n    def _merge_items(self, objs):\n        \"\"\"Merge Item objs by copying missing fields from items in the tail to\n        the head item.\n\n        Return same number of items, with the head item modified.\n        \"\"\"\n        fields = Item.all_keys()\n        for f in fields:\n            for o in objs[1:]:\n                if getattr(objs[0], f, None) in (None, \"\"):\n                    value = getattr(o, f, None)\n                    if value:\n                        self._log.debug(\n                            \"key {} on item {} is null \"\n                            \"or empty: setting from item {.filepath}\",\n                            f,\n                            displayable_path(objs[0].path),\n                            o,\n                        )\n                        setattr(objs[0], f, value)\n                        objs[0].store()\n                        break\n        return objs\n\n    def _merge_albums(self, objs):\n        \"\"\"Merge Album objs by copying missing items from albums in the tail\n        to the head album.\n\n        Return same number of albums, with the head album modified.\"\"\"\n        ids = [i.mb_trackid for i in objs[0].items()]\n        for o in objs[1:]:\n            for i in o.items():\n                if i.mb_trackid not in ids:\n                    missing = Item.from_path(i.path)\n                    missing.album_id = objs[0].id\n                    missing.add(i._db)\n                    self._log.debug(\n                        \"item {} missing from album {}:\"\n                        \" merging from {.filepath} into {}\",\n                        missing,\n                        objs[0],\n                        o,\n                        displayable_path(missing.destination()),\n                    )\n                    missing.move(operation=MoveOperation.COPY)\n        return objs\n\n    def _merge(self, objs):\n        \"\"\"Merge duplicate items. See ``_merge_items`` and ``_merge_albums``\n        for the relevant strategies.\n        \"\"\"\n        kind = Item if all(isinstance(o, Item) for o in objs) else Album\n        if kind is Item:\n            objs = self._merge_items(objs)\n        else:\n            objs = self._merge_albums(objs)\n        return objs\n\n    def _duplicates(self, objs, keys, full, strict, tiebreak, merge):\n        \"\"\"Generate triples of keys, duplicate counts, and constituent objects.\"\"\"\n        offset = 0 if full else 1\n        for k, objs in self._group_by(objs, keys, strict).items():\n            if len(objs) > 1:\n                objs = self._order(objs, tiebreak)\n                if merge:\n                    objs = self._merge(objs)\n                yield (k, len(objs) - offset, objs[offset:])\n"
  },
  {
    "path": "beetsplug/edit.py",
    "content": "# This file is part of beets.\n# Copyright 2016\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Open metadata information in a text editor to let the user edit it.\"\"\"\n\nimport codecs\nimport os\nimport shlex\nimport subprocess\nfrom tempfile import NamedTemporaryFile\n\nimport yaml\n\nfrom beets import plugins, ui, util\nfrom beets.dbcore import types\nfrom beets.importer import Action\nfrom beets.ui.commands.utils import do_query\nfrom beets.util import PromptChoice\n\n# These \"safe\" types can avoid the format/parse cycle that most fields go\n# through: they are safe to edit with native YAML types.\nSAFE_TYPES = (\n    types.BaseFloat,\n    types.BaseInteger,\n    types.Boolean,\n    types.DelimitedString,\n)\n\n\nclass ParseError(Exception):\n    \"\"\"The modified file is unreadable. The user should be offered a chance to\n    fix the error.\n    \"\"\"\n\n\ndef edit(filename, log):\n    \"\"\"Open `filename` in a text editor.\"\"\"\n    cmd = shlex.split(util.editor_command())\n    cmd.append(filename)\n    log.debug(\"invoking editor command: {!r}\", cmd)\n    try:\n        subprocess.call(cmd)\n    except OSError as exc:\n        raise ui.UserError(f\"could not run editor command {cmd[0]!r}: {exc}\")\n\n\ndef dump(arg):\n    \"\"\"Dump a sequence of dictionaries as YAML for editing.\"\"\"\n    return yaml.safe_dump_all(\n        arg,\n        allow_unicode=True,\n        default_flow_style=False,\n    )\n\n\ndef load(s):\n    \"\"\"Read a sequence of YAML documents back to a list of dictionaries\n    with string keys.\n\n    Can raise a `ParseError`.\n    \"\"\"\n    try:\n        out = []\n        for d in yaml.safe_load_all(s):\n            if not isinstance(d, dict):\n                raise ParseError(\n                    f\"each entry must be a dictionary; found {type(d).__name__}\"\n                )\n\n            # Convert all keys to strings. They started out as strings,\n            # but the user may have inadvertently messed this up.\n            out.append({str(k): v for k, v in d.items()})\n\n    except yaml.YAMLError as e:\n        raise ParseError(f\"invalid YAML: {e}\")\n    return out\n\n\ndef _safe_value(obj, key, value):\n    \"\"\"Check whether the `value` is safe to represent in YAML and trust as\n    returned from parsed YAML.\n\n    This ensures that values do not change their type when the user edits their\n    YAML representation.\n    \"\"\"\n    typ = obj._type(key)\n    return isinstance(typ, SAFE_TYPES) and isinstance(value, typ.model_type)\n\n\ndef flatten(obj, fields):\n    \"\"\"Represent `obj`, a `dbcore.Model` object, as a dictionary for\n    serialization. Only include the given `fields` if provided;\n    otherwise, include everything.\n\n    The resulting dictionary's keys are strings and the values are\n    safely YAML-serializable types.\n    \"\"\"\n    # Format each value.\n    d = {}\n    for key in obj.keys():\n        value = obj[key]\n        if _safe_value(obj, key, value):\n            # A safe value that is faithfully representable in YAML.\n            d[key] = value\n        else:\n            # A value that should be edited as a string.\n            d[key] = obj.formatted()[key]\n\n    # Possibly filter field names.\n    if fields:\n        return {k: v for k, v in d.items() if k in fields}\n    else:\n        return d\n\n\ndef apply_(obj, data):\n    \"\"\"Set the fields of a `dbcore.Model` object according to a\n    dictionary.\n\n    This is the opposite of `flatten`. The `data` dictionary should have\n    strings as values.\n    \"\"\"\n    for key, value in data.items():\n        if _safe_value(obj, key, value):\n            # A safe value *stayed* represented as a safe type. Assign it\n            # directly.\n            obj[key] = value\n        else:\n            # Either the field was stringified originally or the user changed\n            # it from a safe type to an unsafe one. Parse it as a string.\n            obj.set_parse(key, str(value))\n\n\nclass EditPlugin(plugins.BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n\n        self.config.add(\n            {\n                # The default fields to edit.\n                \"albumfields\": \"album albumartist\",\n                \"itemfields\": \"track title artist album\",\n                # Silently ignore any changes to these fields.\n                \"ignore_fields\": \"id path\",\n            }\n        )\n\n        self.register_listener(\n            \"before_choose_candidate\", self.before_choose_candidate_listener\n        )\n\n    def commands(self):\n        edit_command = ui.Subcommand(\"edit\", help=\"interactively edit metadata\")\n        edit_command.parser.add_option(\n            \"-f\",\n            \"--field\",\n            metavar=\"FIELD\",\n            action=\"append\",\n            help=\"edit this field also\",\n        )\n        edit_command.parser.add_option(\n            \"--all\",\n            action=\"store_true\",\n            dest=\"all\",\n            help=\"edit all fields\",\n        )\n        edit_command.parser.add_album_option()\n        edit_command.func = self._edit_command\n        return [edit_command]\n\n    def _edit_command(self, lib, opts, args):\n        \"\"\"The CLI command function for the `beet edit` command.\"\"\"\n        # Get the objects to edit.\n        items, albums = do_query(lib, args, opts.album, False)\n        objs = albums if opts.album else items\n        if not objs:\n            ui.print_(\"Nothing to edit.\")\n            return\n\n        # Get the fields to edit.\n        if opts.all:\n            fields = None\n        else:\n            fields = self._get_fields(opts.album, opts.field)\n        self.edit(opts.album, objs, fields)\n\n    def _get_fields(self, album, extra):\n        \"\"\"Get the set of fields to edit.\"\"\"\n        # Start with the configured base fields.\n        if album:\n            fields = self.config[\"albumfields\"].as_str_seq()\n        else:\n            fields = self.config[\"itemfields\"].as_str_seq()\n\n        # Add the requested extra fields.\n        if extra:\n            fields += extra\n\n        # Ensure we always have the `id` field for identification.\n        fields.append(\"id\")\n\n        return set(fields)\n\n    def edit(self, album, objs, fields):\n        \"\"\"The core editor function.\n\n        - `album`: A flag indicating whether we're editing Items or Albums.\n        - `objs`: The `Item`s or `Album`s to edit.\n        - `fields`: The set of field names to edit (or None to edit\n          everything).\n        \"\"\"\n        # Present the YAML to the user and let them change it.\n        success = self.edit_objects(objs, fields)\n\n        # Save the new data.\n        if success:\n            self.save_changes(objs)\n\n    def edit_objects(self, objs, fields):\n        \"\"\"Dump a set of Model objects to a file as text, ask the user\n        to edit it, and apply any changes to the objects.\n\n        Return a boolean indicating whether the edit succeeded.\n        \"\"\"\n        # Get the content to edit as raw data structures.\n        old_data = [flatten(o, fields) for o in objs]\n\n        # Set up a temporary file with the initial data for editing.\n        new = NamedTemporaryFile(\n            mode=\"w\", suffix=\".yaml\", delete=False, encoding=\"utf-8\"\n        )\n        old_str = dump(old_data)\n        new.write(old_str)\n        new.close()\n\n        # Loop until we have parseable data and the user confirms.\n        try:\n            while True:\n                # Ask the user to edit the data.\n                edit(new.name, self._log)\n\n                # Read the data back after editing and check whether anything\n                # changed.\n                with codecs.open(new.name, encoding=\"utf-8\") as f:\n                    new_str = f.read()\n                if new_str == old_str:\n                    ui.print_(\"No changes; aborting.\")\n                    return False\n\n                # Parse the updated data.\n                try:\n                    new_data = load(new_str)\n                except ParseError as e:\n                    ui.print_(f\"Could not read data: {e}\")\n                    if ui.input_yn(\"Edit again to fix? (Y/n)\", True):\n                        continue\n                    else:\n                        return False\n\n                # Show the changes.\n                # If the objects are not on the DB yet, we need a copy of their\n                # original state for show_model_changes.\n                objs_old = [obj.copy() if obj.id < 0 else None for obj in objs]\n                self.apply_data(objs, old_data, new_data)\n                changed = False\n                for obj, obj_old in zip(objs, objs_old):\n                    changed |= ui.show_model_changes(obj, obj_old)\n                if not changed:\n                    ui.print_(\"No changes to apply.\")\n                    return False\n\n                # For cancel/keep-editing, restore objects to their original\n                # in-memory state so temp edits don't leak into the session\n                choice = ui.input_options(\n                    (\"continue Editing\", \"apply\", \"cancel\")\n                )\n                if choice == \"a\":  # Apply.\n                    return True\n                elif choice == \"c\":  # Cancel.\n                    self.apply_data(objs, new_data, old_data)\n                    return False\n                elif choice == \"e\":  # Keep editing.\n                    self.apply_data(objs, new_data, old_data)\n                    continue\n\n        # Remove the temporary file before returning.\n        finally:\n            os.remove(new.name)\n\n    def apply_data(self, objs, old_data, new_data):\n        \"\"\"Take potentially-updated data and apply it to a set of Model\n        objects.\n\n        The objects are not written back to the database, so the changes\n        are temporary.\n        \"\"\"\n        if len(old_data) != len(new_data):\n            self._log.warning(\n                \"number of objects changed from {} to {}\",\n                len(old_data),\n                len(new_data),\n            )\n\n        obj_by_id = {o.id: o for o in objs}\n        ignore_fields = self.config[\"ignore_fields\"].as_str_seq()\n        for old_dict, new_dict in zip(old_data, new_data):\n            # Prohibit any changes to forbidden fields to avoid\n            # clobbering `id` and such by mistake.\n            forbidden = False\n            for key in ignore_fields:\n                if old_dict.get(key) != new_dict.get(key):\n                    self._log.warning(\"ignoring object whose {} changed\", key)\n                    forbidden = True\n                    break\n            if forbidden:\n                continue\n\n            id_ = int(old_dict[\"id\"])\n            apply_(obj_by_id[id_], new_dict)\n\n    def save_changes(self, objs):\n        \"\"\"Save a list of updated Model objects to the database.\"\"\"\n        # Save to the database and possibly write tags.\n        for ob in objs:\n            if ob._dirty:\n                self._log.debug(\"saving changes to {}\", ob)\n                ob.try_sync(ui.should_write(), ui.should_move())\n\n    # Methods for interactive importer execution.\n\n    def before_choose_candidate_listener(self, session, task):\n        \"\"\"Append an \"Edit\" choice and an \"edit Candidates\" choice (if\n        there are candidates) to the interactive importer prompt.\n        \"\"\"\n        choices = [PromptChoice(\"d\", \"eDit\", self.importer_edit)]\n        if task.candidates:\n            choices.append(\n                PromptChoice(\n                    \"c\", \"edit Candidates\", self.importer_edit_candidate\n                )\n            )\n\n        return choices\n\n    def importer_edit(self, session, task):\n        \"\"\"Callback for invoking the functionality during an interactive\n        import session on the *original* item tags.\n        \"\"\"\n        # Assign negative temporary ids to Items that are not in the database\n        # yet. By using negative values, no clash with items in the database\n        # can occur.\n        for i, obj in enumerate(task.items, start=1):\n            # The importer may set the id to None when re-importing albums.\n            if not obj._db or obj.id is None:\n                obj.id = -i\n\n        # Present the YAML to the user and let them change it.\n        fields = self._get_fields(album=False, extra=[])\n        success = self.edit_objects(task.items, fields)\n\n        # Remove temporary ids.\n        for obj in task.items:\n            if obj.id < 0:\n                obj.id = None\n\n        # Save the new data.\n        if success:\n            # Return Action.RETAG, which makes the importer write the tags\n            # to the files if needed without re-applying metadata.\n            return Action.RETAG\n        else:\n            return None\n\n    def importer_edit_candidate(self, session, task):\n        \"\"\"Callback for invoking the functionality during an interactive\n        import session on a *candidate*. The candidate's metadata is\n        applied to the original items.\n        \"\"\"\n        # Prompt the user for a candidate.\n        sel = ui.input_options([], numrange=(1, len(task.candidates)))\n        # Force applying the candidate on the items.\n        task.match = task.candidates[sel - 1]\n        task.apply_metadata()\n\n        return self.importer_edit(session, task)\n"
  },
  {
    "path": "beetsplug/embedart.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Allows beets to embed album art into file metadata.\"\"\"\n\nimport os.path\nimport tempfile\nfrom mimetypes import guess_extension\n\nimport requests\n\nfrom beets import config, ui\nfrom beets.plugins import BeetsPlugin\nfrom beets.ui import print_\nfrom beets.util import bytestring_path, displayable_path, normpath, syspath\nfrom beets.util.artresizer import ArtResizer\nfrom beetsplug._utils import art\n\n\ndef _confirm(objs, album):\n    \"\"\"Show the list of affected objects (items or albums) and confirm\n    that the user wants to modify their artwork.\n\n    `album` is a Boolean indicating whether these are albums (as opposed\n    to items).\n    \"\"\"\n    noun = \"album\" if album else \"file\"\n    prompt = (\n        \"Modify artwork for\"\n        f\" {len(objs)} {noun}{'s' if len(objs) > 1 else ''} (Y/n)?\"\n    )\n\n    # Show all the items or albums.\n    for obj in objs:\n        print_(format(obj))\n\n    # Confirm with user.\n    return ui.input_yn(prompt)\n\n\nclass EmbedCoverArtPlugin(BeetsPlugin):\n    \"\"\"Allows albumart to be embedded into the actual files.\"\"\"\n\n    def __init__(self):\n        super().__init__()\n        self.config.add(\n            {\n                \"maxwidth\": 0,\n                \"auto\": True,\n                \"compare_threshold\": 0,\n                \"ifempty\": False,\n                \"remove_art_file\": False,\n                \"quality\": 0,\n                \"clearart_on_import\": False,\n            }\n        )\n\n        if self.config[\"maxwidth\"].get(int) and not ArtResizer.shared.local:\n            self.config[\"maxwidth\"] = 0\n            self._log.warning(\n                \"ImageMagick or PIL not found; 'maxwidth' option ignored\"\n            )\n        if (\n            self.config[\"compare_threshold\"].get(int)\n            and not ArtResizer.shared.can_compare\n        ):\n            self.config[\"compare_threshold\"] = 0\n            self._log.warning(\n                \"ImageMagick 6.8.7 or higher not installed; \"\n                \"'compare_threshold' option ignored\"\n            )\n\n        self.register_listener(\"art_set\", self.process_album)\n\n        if self.config[\"clearart_on_import\"].get(bool):\n            self.register_listener(\"import_task_files\", self.import_task_files)\n\n    def commands(self):\n        # Embed command.\n        embed_cmd = ui.Subcommand(\n            \"embedart\", help=\"embed image files into file metadata\"\n        )\n        embed_cmd.parser.add_option(\n            \"-f\", \"--file\", metavar=\"PATH\", help=\"the image file to embed\"\n        )\n\n        embed_cmd.parser.add_option(\n            \"-y\", \"--yes\", action=\"store_true\", help=\"skip confirmation\"\n        )\n\n        embed_cmd.parser.add_option(\n            \"-u\",\n            \"--url\",\n            metavar=\"URL\",\n            help=\"the URL of the image file to embed\",\n        )\n\n        maxwidth = self.config[\"maxwidth\"].get(int)\n        quality = self.config[\"quality\"].get(int)\n        compare_threshold = self.config[\"compare_threshold\"].get(int)\n        ifempty = self.config[\"ifempty\"].get(bool)\n\n        def embed_func(lib, opts, args):\n            if opts.file:\n                imagepath = normpath(opts.file)\n                if not os.path.isfile(syspath(imagepath)):\n                    raise ui.UserError(\n                        f\"image file {displayable_path(imagepath)} not found\"\n                    )\n\n                items = lib.items(args)\n\n                # Confirm with user.\n                if not opts.yes and not _confirm(items, not opts.file):\n                    return\n\n                for item in items:\n                    art.embed_item(\n                        self._log,\n                        item,\n                        imagepath,\n                        maxwidth,\n                        None,\n                        compare_threshold,\n                        ifempty,\n                        quality=quality,\n                    )\n            elif opts.url:\n                try:\n                    response = requests.get(opts.url, timeout=5)\n                    response.raise_for_status()\n                except requests.exceptions.RequestException as e:\n                    self._log.error(\"{}\", e)\n                    return\n                extension = guess_extension(response.headers[\"Content-Type\"])\n                if extension is None:\n                    self._log.error(\"Invalid image file\")\n                    return\n                file = f\"image{extension}\"\n                tempimg = os.path.join(tempfile.gettempdir(), file)\n                try:\n                    with open(tempimg, \"wb\") as f:\n                        f.write(response.content)\n                except Exception as e:\n                    self._log.error(\"Unable to save image: {}\", e)\n                    return\n                items = lib.items(args)\n                # Confirm with user.\n                if not opts.yes and not _confirm(items, not opts.url):\n                    os.remove(tempimg)\n                    return\n                for item in items:\n                    art.embed_item(\n                        self._log,\n                        item,\n                        tempimg,\n                        maxwidth,\n                        None,\n                        compare_threshold,\n                        ifempty,\n                        quality=quality,\n                    )\n                os.remove(tempimg)\n            else:\n                albums = lib.albums(args)\n                # Confirm with user.\n                if not opts.yes and not _confirm(albums, not opts.file):\n                    return\n                for album in albums:\n                    art.embed_album(\n                        self._log,\n                        album,\n                        maxwidth,\n                        False,\n                        compare_threshold,\n                        ifempty,\n                        quality=quality,\n                    )\n                    self.remove_artfile(album)\n\n        embed_cmd.func = embed_func\n\n        # Extract command.\n        extract_cmd = ui.Subcommand(\n            \"extractart\",\n            help=\"extract an image from file metadata\",\n        )\n        extract_cmd.parser.add_option(\n            \"-o\",\n            dest=\"outpath\",\n            help=\"image output file\",\n        )\n        extract_cmd.parser.add_option(\n            \"-n\",\n            dest=\"filename\",\n            help=\"image filename to create for all matched albums\",\n        )\n        extract_cmd.parser.add_option(\n            \"-a\",\n            dest=\"associate\",\n            action=\"store_true\",\n            help=\"associate the extracted images with the album\",\n        )\n\n        def extract_func(lib, opts, args):\n            if opts.outpath:\n                art.extract_first(\n                    self._log, normpath(opts.outpath), lib.items(args)\n                )\n            else:\n                filename = bytestring_path(\n                    opts.filename or config[\"art_filename\"].get()\n                )\n                if os.path.dirname(filename) != b\"\":\n                    self._log.error(\n                        \"Only specify a name rather than a path for -n\"\n                    )\n                    return\n                for album in lib.albums(args):\n                    artpath = normpath(os.path.join(album.path, filename))\n                    artpath = art.extract_first(\n                        self._log, artpath, album.items()\n                    )\n                    if artpath and opts.associate:\n                        album.set_art(artpath)\n                        album.store()\n\n        extract_cmd.func = extract_func\n\n        # Clear command.\n        clear_cmd = ui.Subcommand(\n            \"clearart\",\n            help=\"remove images from file metadata\",\n        )\n        clear_cmd.parser.add_option(\n            \"-y\", \"--yes\", action=\"store_true\", help=\"skip confirmation\"\n        )\n\n        def clear_func(lib, opts, args):\n            items = lib.items(args)\n            # Confirm with user.\n            if not opts.yes and not _confirm(items, False):\n                return\n            art.clear(self._log, lib, args)\n\n        clear_cmd.func = clear_func\n\n        return [embed_cmd, extract_cmd, clear_cmd]\n\n    def process_album(self, album):\n        \"\"\"Automatically embed art after art has been set\"\"\"\n        if self.config[\"auto\"] and ui.should_write():\n            max_width = self.config[\"maxwidth\"].get(int)\n            art.embed_album(\n                self._log,\n                album,\n                max_width,\n                True,\n                self.config[\"compare_threshold\"].get(int),\n                self.config[\"ifempty\"].get(bool),\n            )\n            self.remove_artfile(album)\n\n    def remove_artfile(self, album):\n        \"\"\"Possibly delete the album art file for an album (if the\n        appropriate configuration option is enabled).\n        \"\"\"\n        if self.config[\"remove_art_file\"] and album.artpath:\n            if os.path.isfile(syspath(album.artpath)):\n                self._log.debug(\"Removing album art file for {}\", album)\n                os.remove(syspath(album.artpath))\n                album.artpath = None\n                album.store()\n\n    def import_task_files(self, session, task):\n        \"\"\"Automatically clearart of imported files.\"\"\"\n        for item in task.imported_items():\n            self._log.debug(\"clearart-on-import {.filepath}\", item)\n            art.clear_item(item, self._log)\n"
  },
  {
    "path": "beetsplug/embyupdate.py",
    "content": "\"\"\"Updates the Emby Library whenever the beets library is changed.\n\nemby:\n    host: localhost\n    port: 8096\n    username: user\n    apikey: apikey\n    password: password\n\"\"\"\n\nimport hashlib\nfrom urllib.parse import parse_qs, urlencode, urljoin, urlsplit, urlunsplit\n\nimport requests\n\nfrom beets.plugins import BeetsPlugin\n\n\ndef api_url(host, port, endpoint):\n    \"\"\"Returns a joined url.\n\n    Takes host, port and endpoint and generates a valid emby API url.\n\n    :param host: Hostname of the emby server\n    :param port: Portnumber of the emby server\n    :param endpoint: API endpoint\n    :type host: str\n    :type port: int\n    :type endpoint: str\n    :returns: Full API url\n    :rtype: str\n    \"\"\"\n    # check if http or https is defined as host and create hostname\n    hostname_list = [host]\n    if host.startswith(\"http://\") or host.startswith(\"https://\"):\n        hostname = \"\".join(hostname_list)\n    else:\n        hostname_list.insert(0, \"http://\")\n        hostname = \"\".join(hostname_list)\n\n    joined = urljoin(f\"{hostname}:{port}\", endpoint)\n\n    scheme, netloc, path, query_string, fragment = urlsplit(joined)\n    query_params = parse_qs(query_string)\n\n    query_params[\"format\"] = [\"json\"]\n    new_query_string = urlencode(query_params, doseq=True)\n\n    return urlunsplit((scheme, netloc, path, new_query_string, fragment))\n\n\ndef password_data(username, password):\n    \"\"\"Returns a dict with username and its encoded password.\n\n    :param username: Emby username\n    :param password: Emby password\n    :type username: str\n    :type password: str\n    :returns: Dictionary with username and encoded password\n    :rtype: dict\n    \"\"\"\n    return {\n        \"username\": username,\n        \"password\": hashlib.sha1(password.encode(\"utf-8\")).hexdigest(),\n        \"passwordMd5\": hashlib.md5(password.encode(\"utf-8\")).hexdigest(),\n    }\n\n\ndef create_headers(user_id, token=None):\n    \"\"\"Return header dict that is needed to talk to the Emby API.\n\n    :param user_id: Emby user ID\n    :param token: Authentication token for Emby\n    :type user_id: str\n    :type token: str\n    :returns: Headers for requests\n    :rtype: dict\n    \"\"\"\n    headers = {}\n\n    authorization = (\n        f'MediaBrowser UserId=\"{user_id}\", '\n        'Client=\"other\", '\n        'Device=\"beets\", '\n        'DeviceId=\"beets\", '\n        'Version=\"0.0.0\"'\n    )\n\n    headers[\"x-emby-authorization\"] = authorization\n\n    if token:\n        headers[\"x-mediabrowser-token\"] = token\n\n    return headers\n\n\ndef get_token(host, port, headers, auth_data):\n    \"\"\"Return token for a user.\n\n    :param host: Emby host\n    :param port: Emby port\n    :param headers: Headers for requests\n    :param auth_data: Username and encoded password for authentication\n    :type host: str\n    :type port: int\n    :type headers: dict\n    :type auth_data: dict\n    :returns: Access Token\n    :rtype: str\n    \"\"\"\n    url = api_url(host, port, \"/Users/AuthenticateByName\")\n    r = requests.post(\n        url,\n        headers=headers,\n        data=auth_data,\n        timeout=10,\n    )\n\n    return r.json().get(\"AccessToken\")\n\n\ndef get_user(host, port, username):\n    \"\"\"Return user dict from server or None if there is no user.\n\n    :param host: Emby host\n    :param port: Emby port\n    :username: Username\n    :type host: str\n    :type port: int\n    :type username: str\n    :returns: Matched Users\n    :rtype: list\n    \"\"\"\n    url = api_url(host, port, \"/Users/Public\")\n    r = requests.get(url, timeout=10)\n    user = [i for i in r.json() if i[\"Name\"] == username]\n\n    return user\n\n\nclass EmbyUpdate(BeetsPlugin):\n    def __init__(self):\n        super().__init__(\"emby\")\n\n        # Adding defaults.\n        self.config.add(\n            {\n                \"host\": \"http://localhost\",\n                \"port\": 8096,\n                \"username\": None,\n                \"password\": None,\n                \"userid\": None,\n                \"apikey\": None,\n            }\n        )\n        self.config[\"username\"].redact = True\n        self.config[\"password\"].redact = True\n        self.config[\"userid\"].redact = True\n        self.config[\"apikey\"].redact = True\n\n        self.register_listener(\"database_change\", self.listen_for_db_change)\n\n    def listen_for_db_change(self, lib, model):\n        \"\"\"Listens for beets db change and register the update for the end.\"\"\"\n        self.register_listener(\"cli_exit\", self.update)\n\n    def update(self, lib):\n        \"\"\"When the client exists try to send refresh request to Emby.\"\"\"\n        self._log.info(\"Updating Emby library...\")\n\n        host = self.config[\"host\"].get()\n        port = self.config[\"port\"].get()\n        username = self.config[\"username\"].get()\n        password = self.config[\"password\"].get()\n        userid = self.config[\"userid\"].get()\n        token = self.config[\"apikey\"].get()\n\n        # Check if at least a apikey or password is given.\n        if not any([password, token]):\n            self._log.warning(\"Provide at least Emby password or apikey.\")\n            return\n\n        if not userid:\n            # Get user information from the Emby API.\n            user = get_user(host, port, username)\n            if not user:\n                self._log.warning(\"User {} could not be found.\", username)\n                return\n            userid = user[0][\"Id\"]\n\n        if not token:\n            # Create Authentication data and headers.\n            auth_data = password_data(username, password)\n            headers = create_headers(userid)\n\n            # Get authentication token.\n            token = get_token(host, port, headers, auth_data)\n            if not token:\n                self._log.warning(\"Could not get token for user {}\", username)\n                return\n\n        # Recreate headers with a token.\n        headers = create_headers(userid, token=token)\n\n        # Trigger the Update.\n        url = api_url(host, port, \"/Library/Refresh\")\n        r = requests.post(\n            url,\n            headers=headers,\n            timeout=10,\n        )\n        if r.status_code != 204:\n            self._log.warning(\"Update could not be triggered\")\n        else:\n            self._log.info(\"Update triggered.\")\n"
  },
  {
    "path": "beetsplug/export.py",
    "content": "# This file is part of beets.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Exports data from beets\"\"\"\n\nimport codecs\nimport csv\nimport json\nimport sys\nfrom datetime import date, datetime\nfrom xml.etree import ElementTree\n\nimport mediafile\n\nfrom beets import ui, util\nfrom beets.plugins import BeetsPlugin\nfrom beetsplug.info import library_data, tag_data\n\n\nclass ExportEncoder(json.JSONEncoder):\n    \"\"\"Deals with dates because JSON doesn't have a standard\"\"\"\n\n    def default(self, o):\n        if isinstance(o, (datetime, date)):\n            return o.isoformat()\n        return json.JSONEncoder.default(self, o)\n\n\nclass ExportPlugin(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n\n        self.config.add(\n            {\n                \"default_format\": \"json\",\n                \"json\": {\n                    # JSON module formatting options.\n                    \"formatting\": {\n                        \"ensure_ascii\": False,\n                        \"indent\": 4,\n                        \"separators\": (\",\", \": \"),\n                        \"sort_keys\": True,\n                    }\n                },\n                \"jsonlines\": {\n                    # JSON Lines formatting options.\n                    \"formatting\": {\n                        \"ensure_ascii\": False,\n                        \"separators\": (\",\", \": \"),\n                        \"sort_keys\": True,\n                    }\n                },\n                \"csv\": {\n                    # CSV module formatting options.\n                    \"formatting\": {\n                        # The delimiter used to separate columns.\n                        \"delimiter\": \",\",\n                        # The dialect to use when formatting the file output.\n                        \"dialect\": \"excel\",\n                    }\n                },\n                \"xml\": {\n                    # XML module formatting options.\n                    \"formatting\": {}\n                },\n                # TODO: Use something like the edit plugin\n                # 'item_fields': []\n            }\n        )\n\n    def commands(self):\n        cmd = ui.Subcommand(\"export\", help=\"export data from beets\")\n        cmd.func = self.run\n        cmd.parser.add_option(\n            \"-l\",\n            \"--library\",\n            action=\"store_true\",\n            help=\"show library fields instead of tags\",\n        )\n        cmd.parser.add_option(\n            \"-a\",\n            \"--album\",\n            action=\"store_true\",\n            help='show album fields instead of tracks (implies \"--library\")',\n        )\n        cmd.parser.add_option(\n            \"--append\",\n            action=\"store_true\",\n            default=False,\n            help=\"if should append data to the file\",\n        )\n        cmd.parser.add_option(\n            \"-i\",\n            \"--include-keys\",\n            default=[],\n            action=\"append\",\n            dest=\"included_keys\",\n            help=\"comma separated list of keys to show\",\n        )\n        cmd.parser.add_option(\n            \"-o\",\n            \"--output\",\n            help=\"path for the output file. If not given, will print the data\",\n        )\n        cmd.parser.add_option(\n            \"-f\",\n            \"--format\",\n            default=\"json\",\n            help=\"the output format: json (default), jsonlines, csv, or xml\",\n        )\n        return [cmd]\n\n    def run(self, lib, opts, args):\n        file_path = opts.output\n        file_mode = \"a\" if opts.append else \"w\"\n        file_format = opts.format or self.config[\"default_format\"].get(str)\n        file_format_is_line_based = file_format == \"jsonlines\"\n        format_options = self.config[file_format][\"formatting\"].get(dict)\n\n        export_format = ExportFormat.factory(\n            file_type=file_format,\n            **{\"file_path\": file_path, \"file_mode\": file_mode},\n        )\n\n        if opts.library or opts.album:\n            data_collector = library_data\n        else:\n            data_collector = tag_data\n\n        included_keys = []\n        for keys in opts.included_keys:\n            included_keys.extend(keys.split(\",\"))\n\n        items = []\n        for data_emitter in data_collector(\n            lib,\n            args,\n            album=opts.album,\n        ):\n            try:\n                data, _ = data_emitter(included_keys or \"*\")\n            except (mediafile.UnreadableFileError, OSError) as ex:\n                self._log.error(\"cannot read file: {}\", ex)\n                continue\n\n            for key, value in data.items():\n                if isinstance(value, bytes):\n                    data[key] = util.displayable_path(value)\n\n            if file_format_is_line_based:\n                export_format.export(data, **format_options)\n            else:\n                items += [data]\n\n        if not file_format_is_line_based:\n            export_format.export(items, **format_options)\n\n\nclass ExportFormat:\n    \"\"\"The output format type\"\"\"\n\n    def __init__(self, file_path, file_mode=\"w\", encoding=\"utf-8\"):\n        self.path = file_path\n        self.mode = file_mode\n        self.encoding = encoding\n        # creates a file object to write/append or sets to stdout\n        self.out_stream = (\n            codecs.open(self.path, self.mode, self.encoding)\n            if self.path\n            else sys.stdout\n        )\n\n    @classmethod\n    def factory(cls, file_type, **kwargs):\n        if file_type in [\"json\", \"jsonlines\"]:\n            return JsonFormat(**kwargs)\n        elif file_type == \"csv\":\n            return CSVFormat(**kwargs)\n        elif file_type == \"xml\":\n            return XMLFormat(**kwargs)\n        else:\n            raise NotImplementedError()\n\n    def export(self, data, **kwargs):\n        raise NotImplementedError()\n\n\nclass JsonFormat(ExportFormat):\n    \"\"\"Saves in a json file\"\"\"\n\n    def __init__(self, file_path, file_mode=\"w\", encoding=\"utf-8\"):\n        super().__init__(file_path, file_mode, encoding)\n\n    def export(self, data, **kwargs):\n        json.dump(data, self.out_stream, cls=ExportEncoder, **kwargs)\n        self.out_stream.write(\"\\n\")\n\n\nclass CSVFormat(ExportFormat):\n    \"\"\"Saves in a csv file\"\"\"\n\n    def __init__(self, file_path, file_mode=\"w\", encoding=\"utf-8\"):\n        super().__init__(file_path, file_mode, encoding)\n\n    def export(self, data, **kwargs):\n        header = list(data[0].keys()) if data else []\n        writer = csv.DictWriter(self.out_stream, fieldnames=header, **kwargs)\n        writer.writeheader()\n        writer.writerows(data)\n\n\nclass XMLFormat(ExportFormat):\n    \"\"\"Saves in a xml file\"\"\"\n\n    def __init__(self, file_path, file_mode=\"w\", encoding=\"utf-8\"):\n        super().__init__(file_path, file_mode, encoding)\n\n    def export(self, data, **kwargs):\n        # Creates the XML file structure.\n        library = ElementTree.Element(\"library\")\n        tracks = ElementTree.SubElement(library, \"tracks\")\n        if data and isinstance(data[0], dict):\n            for index, item in enumerate(data):\n                track = ElementTree.SubElement(tracks, \"track\")\n                for key, value in item.items():\n                    track_details = ElementTree.SubElement(track, key)\n                    track_details.text = value\n        # Depending on the version of python the encoding needs to change\n        try:\n            data = ElementTree.tostring(library, encoding=\"unicode\", **kwargs)\n        except LookupError:\n            data = ElementTree.tostring(library, encoding=\"utf-8\", **kwargs)\n\n        self.out_stream.write(data)\n"
  },
  {
    "path": "beetsplug/fetchart.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Fetches album art.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport re\nfrom abc import ABC, abstractmethod\nfrom collections import OrderedDict\nfrom contextlib import closing\nfrom enum import Enum\nfrom functools import cached_property\nfrom typing import TYPE_CHECKING, AnyStr, ClassVar, Literal\n\nimport confuse\nimport requests\nfrom mediafile import image_mime_type\n\nfrom beets import config, importer, plugins, ui, util\nfrom beets.util import bytestring_path, get_temp_filename, sorted_walk, syspath\nfrom beets.util.artresizer import ArtResizer\nfrom beets.util.color import colorize\nfrom beets.util.config import sanitize_pairs\n\nif TYPE_CHECKING:\n    from collections.abc import Iterable, Iterator, Sequence\n\n    from beets.importer import ImportSession, ImportTask\n    from beets.library import Album, Library\n    from beets.logging import BeetsLogger as Logger\n\ntry:\n    from bs4 import BeautifulSoup, Tag\n\n    HAS_BEAUTIFUL_SOUP = True\nexcept ImportError:\n    HAS_BEAUTIFUL_SOUP = False\n\n\nCONTENT_TYPES = {\"image/jpeg\": [b\"jpg\", b\"jpeg\"], \"image/png\": [b\"png\"]}\nIMAGE_EXTENSIONS = [ext for exts in CONTENT_TYPES.values() for ext in exts]\n\n\nclass ImageAction(Enum):\n    \"\"\"Indicates whether an image is useable or requires post-processing.\"\"\"\n\n    BAD = 0\n    EXACT = 1\n    DOWNSCALE = 2\n    DOWNSIZE = 3\n    DEINTERLACE = 4\n    REFORMAT = 5\n\n\nclass MetadataMatch(Enum):\n    \"\"\"Indicates whether a `Candidate` matches the search criteria exactly.\"\"\"\n\n    EXACT = 0\n    FALLBACK = 1\n\n\nSourceLocation = Literal[\"local\", \"remote\"]\n\n\nclass Candidate:\n    \"\"\"Holds information about a matching artwork, deals with validation of\n    dimension restrictions and resizing.\n    \"\"\"\n\n    def __init__(\n        self,\n        log: Logger,\n        source_name: str,\n        path: None | bytes = None,\n        url: None | str = None,\n        match: None | MetadataMatch = None,\n        size: None | tuple[int, int] = None,\n    ):\n        self._log = log\n        self.path = path\n        self.url = url\n        self.source_name = source_name\n        self._check: None | ImageAction = None\n        self.match = match\n        self.size = size\n\n    def _validate(\n        self,\n        plugin: FetchArtPlugin,\n        skip_check_for: None | list[ImageAction] = None,\n    ) -> ImageAction:\n        \"\"\"Determine whether the candidate artwork is valid based on\n        its dimensions (width and ratio).\n\n        `skip_check_for` is a check or list of checks to skip. This is used to\n        avoid redundant checks when the candidate has already been\n        validated for a particular operation without changing\n        plugin configuration.\n\n        Return `ImageAction.BAD` if the file is unusable.\n        Return `ImageAction.EXACT` if the file is usable as-is.\n        Return `ImageAction.DOWNSCALE` if the file must be rescaled.\n        Return `ImageAction.DOWNSIZE` if the file must be resized, and possibly\n            also rescaled.\n        Return `ImageAction.DEINTERLACE` if the file must be deinterlaced.\n        Return `ImageAction.REFORMAT` if the file has to be converted.\n        \"\"\"\n        if not self.path:\n            return ImageAction.BAD\n\n        if not (\n            plugin.enforce_ratio\n            or plugin.minwidth\n            or plugin.maxwidth\n            or plugin.max_filesize\n            or plugin.deinterlace\n            or plugin.cover_format\n        ):\n            return ImageAction.EXACT\n\n        # get_size returns None if no local imaging backend is available\n        if not self.size:\n            self.size = ArtResizer.shared.get_size(self.path)\n        self._log.debug(\"image size: {.size}\", self)\n\n        if not self.size:\n            self._log.warning(\n                \"Could not get size of image (please see \"\n                \"documentation for dependencies). \"\n                \"The configuration options `minwidth`, \"\n                \"`enforce_ratio` and `max_filesize` \"\n                \"may be violated.\"\n            )\n            return ImageAction.EXACT\n\n        short_edge = min(self.size)\n        long_edge = max(self.size)\n\n        # Check minimum dimension.\n        if plugin.minwidth and self.size[0] < plugin.minwidth:\n            self._log.debug(\n                \"image too small ({} < {.minwidth})\", self.size[0], plugin\n            )\n            return ImageAction.BAD\n\n        # Check aspect ratio.\n        edge_diff = long_edge - short_edge\n        if plugin.enforce_ratio:\n            if plugin.margin_px:\n                if edge_diff > plugin.margin_px:\n                    self._log.debug(\n                        \"image is not close enough to being \"\n                        \"square, ({} - {} > {.margin_px})\",\n                        long_edge,\n                        short_edge,\n                        plugin,\n                    )\n                    return ImageAction.BAD\n            elif plugin.margin_percent:\n                margin_px = plugin.margin_percent * long_edge\n                if edge_diff > margin_px:\n                    self._log.debug(\n                        \"image is not close enough to being \"\n                        \"square, ({} - {} > {})\",\n                        long_edge,\n                        short_edge,\n                        margin_px,\n                    )\n                    return ImageAction.BAD\n            elif edge_diff:\n                # also reached for margin_px == 0 and margin_percent == 0.0\n                self._log.debug(\n                    \"image is not square ({} != {})\", self.size[0], self.size[1]\n                )\n                return ImageAction.BAD\n\n        # Check maximum dimension.\n        downscale = False\n        if plugin.maxwidth and self.size[0] > plugin.maxwidth:\n            self._log.debug(\n                \"image needs rescaling ({} > {.maxwidth})\", self.size[0], plugin\n            )\n            downscale = True\n\n        # Check filesize.\n        downsize = False\n        if plugin.max_filesize:\n            filesize = os.stat(syspath(self.path)).st_size\n            if filesize > plugin.max_filesize:\n                self._log.debug(\n                    \"image needs resizing ({}B > {.max_filesize}B)\",\n                    filesize,\n                    plugin,\n                )\n                downsize = True\n\n        # Check image format\n        reformat = False\n        if plugin.cover_format:\n            fmt = ArtResizer.shared.get_format(self.path)\n            reformat = fmt != plugin.cover_format\n            if reformat:\n                self._log.debug(\n                    \"image needs reformatting: {} -> {.cover_format}\",\n                    fmt,\n                    plugin,\n                )\n\n        skip_check_for = skip_check_for or []\n\n        if downscale and (ImageAction.DOWNSCALE not in skip_check_for):\n            return ImageAction.DOWNSCALE\n        if reformat and (ImageAction.REFORMAT not in skip_check_for):\n            return ImageAction.REFORMAT\n        if plugin.deinterlace and (\n            ImageAction.DEINTERLACE not in skip_check_for\n        ):\n            return ImageAction.DEINTERLACE\n        if downsize and (ImageAction.DOWNSIZE not in skip_check_for):\n            return ImageAction.DOWNSIZE\n        return ImageAction.EXACT\n\n    def validate(\n        self,\n        plugin: FetchArtPlugin,\n        skip_check_for: None | list[ImageAction] = None,\n    ) -> ImageAction:\n        self._check = self._validate(plugin, skip_check_for)\n        return self._check\n\n    def resize(self, plugin: FetchArtPlugin) -> None:\n        \"\"\"Resize the candidate artwork according to the plugin's\n        configuration until it is valid or no further resizing is\n        possible.\n        \"\"\"\n        # validate the candidate in case it hasn't been done yet\n        current_check = self.validate(plugin)\n        checks_performed = []\n\n        # we don't want to resize the image if it's valid or bad\n        while current_check not in [ImageAction.BAD, ImageAction.EXACT]:\n            self._resize(plugin, current_check)\n            checks_performed.append(current_check)\n            current_check = self.validate(\n                plugin, skip_check_for=checks_performed\n            )\n\n    def _resize(\n        self, plugin: FetchArtPlugin, check: None | ImageAction = None\n    ) -> None:\n        \"\"\"Resize the candidate artwork according to the plugin's\n        configuration and the specified check.\n        \"\"\"\n        # This must only be called when _validate returned something other than\n        # ImageAction.Bad or ImageAction.EXACT; then path and size are known.\n        assert self.path is not None\n        assert self.size is not None\n\n        if check == ImageAction.DOWNSCALE:\n            self.path = ArtResizer.shared.resize(\n                plugin.maxwidth,\n                self.path,\n                quality=plugin.quality,\n                max_filesize=plugin.max_filesize,\n            )\n        elif check == ImageAction.DOWNSIZE:\n            # dimensions are correct, so maxwidth is set to maximum dimension\n            self.path = ArtResizer.shared.resize(\n                max(self.size),\n                self.path,\n                quality=plugin.quality,\n                max_filesize=plugin.max_filesize,\n            )\n        elif check == ImageAction.DEINTERLACE:\n            self.path = ArtResizer.shared.deinterlace(self.path)\n        elif check == ImageAction.REFORMAT:\n            self.path = ArtResizer.shared.reformat(\n                self.path,\n                # TODO: fix this gnarly logic to remove the need for type ignore\n                plugin.cover_format,  # type: ignore[arg-type]\n                deinterlaced=plugin.deinterlace,\n            )\n\n\ndef _logged_get(log: Logger, *args, **kwargs) -> requests.Response:\n    \"\"\"Like `requests.get`, but logs the effective URL to the specified\n    `log` at the `DEBUG` level.\n\n    Use the optional `message` parameter to specify what to log before\n    the URL. By default, the string is \"getting URL\".\n\n    Also sets the User-Agent header to indicate beets.\n    \"\"\"\n    # Use some arguments with the `send` call but most with the\n    # `Request` construction. This is a cheap, magic-filled way to\n    # emulate `requests.get` or, more pertinently,\n    # `requests.Session.request`.\n    req_kwargs = kwargs\n    send_kwargs = {}\n    for arg in (\"stream\", \"verify\", \"proxies\", \"cert\", \"timeout\"):\n        if arg in kwargs:\n            send_kwargs[arg] = req_kwargs.pop(arg)\n    if \"timeout\" not in send_kwargs:\n        send_kwargs[\"timeout\"] = 10\n\n    # Our special logging message parameter.\n    if \"message\" in kwargs:\n        message = kwargs.pop(\"message\")\n    else:\n        message = \"getting URL\"\n\n    req = requests.Request(\"GET\", *args, **req_kwargs)\n\n    with requests.Session() as s:\n        s.headers = {\"User-Agent\": \"beets\"}\n        prepped = s.prepare_request(req)\n        settings = s.merge_environment_settings(\n            prepped.url, {}, None, None, None\n        )\n        send_kwargs.update(settings)\n        log.debug(\"{}: {.url}\", message, prepped)\n        return s.send(prepped, **send_kwargs)\n\n\nclass RequestMixin:\n    \"\"\"Adds a Requests wrapper to the class that uses the logger, which\n    must be named `self._log`.\n    \"\"\"\n\n    _log: Logger\n\n    def request(self, *args, **kwargs) -> requests.Response:\n        \"\"\"Like `requests.get`, but uses the logger `self._log`.\n\n        See also `_logged_get`.\n        \"\"\"\n        return _logged_get(self._log, *args, **kwargs)\n\n\n# ART SOURCES ################################################################\n\n\nclass ArtSource(RequestMixin, ABC):\n    # Specify whether this source fetches local or remote images\n    LOC: ClassVar[SourceLocation]\n    # A list of methods to match metadata, sorted by descending accuracy\n    VALID_MATCHING_CRITERIA: ClassVar[list[str]] = [\"default\"]\n    # A human-readable name for the art source\n    NAME: ClassVar[str]\n    # The key to select the art source in the config. This value will also be\n    # stored in the database.\n    ID: ClassVar[str]\n\n    def __init__(\n        self,\n        log: Logger,\n        config: confuse.ConfigView,\n        match_by: None | list[str] = None,\n    ) -> None:\n        self._log = log\n        self._config = config\n        self.match_by = match_by or self.VALID_MATCHING_CRITERIA\n\n    @cached_property\n    def description(self) -> str:\n        return f\"{self.ID}[{', '.join(self.match_by)}]\"\n\n    @staticmethod\n    def add_default_config(config: confuse.ConfigView) -> None:\n        pass\n\n    @classmethod\n    def available(cls, log: Logger, config: confuse.ConfigView) -> bool:\n        \"\"\"Return whether or not all dependencies are met and the art source is\n        in fact usable.\n        \"\"\"\n        return True\n\n    @abstractmethod\n    def get(\n        self,\n        album: Album,\n        plugin: FetchArtPlugin,\n        paths: None | Sequence[bytes],\n    ) -> Iterator[Candidate]:\n        pass\n\n    def _candidate(self, **kwargs) -> Candidate:\n        return Candidate(source_name=self.ID, log=self._log, **kwargs)\n\n    @abstractmethod\n    def fetch_image(self, candidate: Candidate, plugin: FetchArtPlugin) -> None:\n        \"\"\"Fetch the image to a temporary file if it is not already available\n        as a local file.\n\n        After calling this, `Candidate.path` is set to the image path if\n        successful, or to `None` otherwise.\n        \"\"\"\n        pass\n\n    def cleanup(self, candidate: Candidate) -> None:\n        pass\n\n\nclass LocalArtSource(ArtSource):\n    LOC = \"local\"\n\n    def fetch_image(self, candidate: Candidate, plugin: FetchArtPlugin) -> None:\n        pass\n\n\nclass RemoteArtSource(ArtSource):\n    LOC = \"remote\"\n\n    def fetch_image(self, candidate: Candidate, plugin: FetchArtPlugin) -> None:\n        \"\"\"Downloads an image from a URL and checks whether it seems to\n        actually be an image.\n        \"\"\"\n        # This must only be called for candidates that were returned by\n        # self.get, which are expected to have a url and no path (because they\n        # haven't been downloaded yet).\n        assert candidate.path is None\n        assert candidate.url is not None\n\n        if plugin.maxwidth:\n            candidate.url = ArtResizer.shared.proxy_url(\n                plugin.maxwidth, candidate.url\n            )\n        try:\n            with closing(\n                self.request(\n                    candidate.url, stream=True, message=\"downloading image\"\n                )\n            ) as resp:\n                ct = resp.headers.get(\"Content-Type\", None)\n\n                # Download the image to a temporary file. As some servers\n                # (notably fanart.tv) have proven to return wrong Content-Types\n                # when images were uploaded with a bad file extension, do not\n                # rely on it. Instead validate the type using the file magic\n                # and only then determine the extension.\n                data = resp.iter_content(chunk_size=1024)\n                header = b\"\"\n                for chunk in data:\n                    header += chunk\n                    if len(header) >= 32:\n                        # The imghdr module will only read 32 bytes, and our\n                        # own additions in mediafile even less.\n                        break\n                else:\n                    # server didn't return enough data, i.e. corrupt image\n                    return\n\n                real_ct = image_mime_type(header)\n                if real_ct is None:\n                    # detection by file magic failed, fall back to the\n                    # server-supplied Content-Type\n                    # Is our type detection failsafe enough to drop this?\n                    real_ct = ct\n\n                if real_ct not in CONTENT_TYPES:\n                    self._log.debug(\n                        \"not a supported image: {}\",\n                        real_ct or \"unknown content type\",\n                    )\n                    return\n\n                ext = b\".\" + CONTENT_TYPES[real_ct][0]\n\n                if real_ct != ct:\n                    self._log.warning(\n                        \"Server specified {}, but returned a \"\n                        \"{} image. Correcting the extension \"\n                        \"to {}\",\n                        ct,\n                        real_ct,\n                        ext,\n                    )\n\n                filename = get_temp_filename(__name__, suffix=ext.decode())\n                with open(filename, \"wb\") as fh:\n                    # write the first already loaded part of the image\n                    fh.write(header)\n                    # download the remaining part of the image\n                    for chunk in data:\n                        fh.write(chunk)\n                self._log.debug(\n                    \"downloaded art to: {}\", util.displayable_path(filename)\n                )\n                candidate.path = util.bytestring_path(filename)\n                return\n\n        except (OSError, requests.RequestException, TypeError) as exc:\n            # Handling TypeError works around a urllib3 bug:\n            # https://github.com/shazow/urllib3/issues/556\n            self._log.debug(\"error fetching art: {}\", exc)\n            return\n\n    def cleanup(self, candidate: Candidate) -> None:\n        if candidate.path:\n            try:\n                util.remove(path=candidate.path)\n            except util.FilesystemError as exc:\n                self._log.debug(\"error cleaning up tmp art: {}\", exc)\n\n\nclass CoverArtArchive(RemoteArtSource):\n    NAME = \"Cover Art Archive\"\n    ID = \"coverart\"\n    VALID_MATCHING_CRITERIA: ClassVar[list[str]] = [\"release\", \"releasegroup\"]\n    VALID_THUMBNAIL_SIZES: ClassVar[list[int]] = [250, 500, 1200]\n\n    URL = \"https://coverartarchive.org/release/{mbid}\"\n    GROUP_URL = \"https://coverartarchive.org/release-group/{mbid}\"\n\n    def get(\n        self,\n        album: Album,\n        plugin: FetchArtPlugin,\n        paths: None | Sequence[bytes],\n    ) -> Iterator[Candidate]:\n        \"\"\"Return the Cover Art Archive and Cover Art Archive release\n        group URLs using album MusicBrainz release ID and release group\n        ID.\n        \"\"\"\n\n        def get_image_urls(\n            url: str,\n            preferred_width: None | str = None,\n        ) -> Iterator[str]:\n            try:\n                response = self.request(url)\n            except requests.RequestException:\n                self._log.debug(\"{.NAME}: error receiving response\", self)\n                return\n\n            try:\n                data = response.json()\n            except ValueError:\n                self._log.debug(\n                    \"{.NAME}: error loading response: {.text}\", self, response\n                )\n                return\n\n            for item in data.get(\"images\", []):\n                try:\n                    if \"Front\" not in item[\"types\"]:\n                        continue\n\n                    # If there is a pre-sized thumbnail of the desired size\n                    # we select it. Otherwise, we return the raw image.\n                    image_url: str = item[\"image\"]\n                    if preferred_width is not None:\n                        if isinstance(item.get(\"thumbnails\"), dict):\n                            image_url = item[\"thumbnails\"].get(\n                                preferred_width, image_url\n                            )\n                    yield image_url\n                except KeyError:\n                    pass\n\n        release_url = self.URL.format(mbid=album.mb_albumid)\n        release_group_url = self.GROUP_URL.format(mbid=album.mb_releasegroupid)\n\n        # Cover Art Archive API offers pre-resized thumbnails at several sizes.\n        # If the maxwidth config matches one of the already available sizes\n        # fetch it directly instead of fetching the full sized image and\n        # resizing it.\n        preferred_width = None\n        if plugin.maxwidth in self.VALID_THUMBNAIL_SIZES:\n            preferred_width = str(plugin.maxwidth)\n\n        if \"release\" in self.match_by and album.mb_albumid:\n            for url in get_image_urls(release_url, preferred_width):\n                yield self._candidate(url=url, match=MetadataMatch.EXACT)\n\n        if \"releasegroup\" in self.match_by and album.mb_releasegroupid:\n            for url in get_image_urls(release_group_url, preferred_width):\n                yield self._candidate(url=url, match=MetadataMatch.FALLBACK)\n\n\nclass Amazon(RemoteArtSource):\n    NAME = \"Amazon\"\n    ID = \"amazon\"\n    URL = \"https://images.amazon.com/images/P/{}.{:02d}.LZZZZZZZ.jpg\"\n    INDICES = (1, 2)\n\n    def get(\n        self,\n        album: Album,\n        plugin: FetchArtPlugin,\n        paths: None | Sequence[bytes],\n    ) -> Iterator[Candidate]:\n        \"\"\"Generate URLs using Amazon ID (ASIN) string.\"\"\"\n        if album.asin:\n            for index in self.INDICES:\n                yield self._candidate(\n                    url=self.URL.format(album.asin, index),\n                    match=MetadataMatch.EXACT,\n                )\n\n\nclass AlbumArtOrg(RemoteArtSource):\n    NAME = \"AlbumArt.org scraper\"\n    ID = \"albumart\"\n    URL = \"https://www.albumart.org/index_detail.php\"\n    PAT = r'href\\s*=\\s*\"([^>\"]*)\"[^>]*title\\s*=\\s*\"View larger image\"'\n\n    def get(\n        self,\n        album: Album,\n        plugin: FetchArtPlugin,\n        paths: None | Sequence[bytes],\n    ):\n        \"\"\"Return art URL from AlbumArt.org using album ASIN.\"\"\"\n        if not album.asin:\n            return\n        # Get the page from albumart.org.\n        try:\n            resp = self.request(self.URL, params={\"asin\": album.asin})\n            self._log.debug(\"scraped art URL: {.url}\", resp)\n        except requests.RequestException:\n            self._log.debug(\"error scraping art page\")\n            return\n\n        # Search the page for the image URL.\n        m = re.search(self.PAT, resp.text)\n        if m:\n            image_url = m.group(1)\n            yield self._candidate(url=image_url, match=MetadataMatch.EXACT)\n        else:\n            self._log.debug(\"no image found on page\")\n\n\nclass GoogleImages(RemoteArtSource):\n    NAME = \"Google Images\"\n    ID = \"google\"\n    URL = \"https://www.googleapis.com/customsearch/v1\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.key = (self._config[\"google_key\"].get(),)\n        self.cx = (self._config[\"google_engine\"].get(),)\n\n    @staticmethod\n    def add_default_config(config: confuse.ConfigView):\n        config.add(\n            {\n                \"google_key\": None,\n                \"google_engine\": \"001442825323518660753:hrh5ch1gjzm\",\n            }\n        )\n        config[\"google_key\"].redact = True\n        config[\"google_engine\"].redact = True\n\n    @classmethod\n    def available(cls, log: Logger, config: confuse.ConfigView) -> bool:\n        has_key = bool(config[\"google_key\"].get())\n        if not has_key:\n            log.debug(\"google: Disabling art source due to missing key\")\n        return has_key\n\n    def get(\n        self,\n        album: Album,\n        plugin: FetchArtPlugin,\n        paths: None | Sequence[bytes],\n    ) -> Iterator[Candidate]:\n        \"\"\"Return art URL from google custom search engine\n        given an album title and interpreter.\n        \"\"\"\n        if not (album.albumartist and album.album):\n            return\n        search_string = f\"{album.albumartist},{album.album}\".encode()\n\n        try:\n            response = self.request(\n                self.URL,\n                params={\n                    \"key\": self.key,\n                    \"cx\": self.cx,\n                    \"q\": search_string,\n                    \"searchType\": \"image\",\n                },\n            )\n        except requests.RequestException:\n            self._log.debug(\"google: error receiving response\")\n            return\n\n        # Get results using JSON.\n        try:\n            data = response.json()\n        except ValueError:\n            self._log.debug(\"google: error loading response: {.text}\", response)\n            return\n\n        if \"error\" in data:\n            reason = data[\"error\"][\"errors\"][0][\"reason\"]\n            self._log.debug(\"google fetchart error: {}\", reason)\n            return\n\n        if \"items\" in data.keys():\n            for item in data[\"items\"]:\n                yield self._candidate(\n                    url=item[\"link\"], match=MetadataMatch.EXACT\n                )\n\n\nclass FanartTV(RemoteArtSource):\n    \"\"\"Art from fanart.tv requested using their API\"\"\"\n\n    NAME = \"fanart.tv\"\n    ID = \"fanarttv\"\n    API_URL = \"https://webservice.fanart.tv/v3/\"\n    API_ALBUMS = f\"{API_URL}music/albums/\"\n    PROJECT_KEY = \"61a7d0ab4e67162b7a0c7c35915cd48e\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.client_key = self._config[\"fanarttv_key\"].get()\n\n    @staticmethod\n    def add_default_config(config: confuse.ConfigView):\n        config.add(\n            {\n                \"fanarttv_key\": None,\n            }\n        )\n        config[\"fanarttv_key\"].redact = True\n\n    def get(\n        self,\n        album: Album,\n        plugin: FetchArtPlugin,\n        paths: None | Sequence[bytes],\n    ) -> Iterator[Candidate]:\n        if not album.mb_releasegroupid:\n            return\n\n        try:\n            response = self.request(\n                f\"{self.API_ALBUMS}{album.mb_releasegroupid}\",\n                headers={\n                    \"api-key\": self.PROJECT_KEY,\n                    \"client-key\": self.client_key,\n                },\n            )\n        except requests.RequestException:\n            self._log.debug(\"fanart.tv: error receiving response\")\n            return\n\n        try:\n            data = response.json()\n        except ValueError:\n            self._log.debug(\n                \"fanart.tv: error loading response: {.text}\", response\n            )\n            return\n\n        if \"status\" in data and data[\"status\"] == \"error\":\n            if \"not found\" in data[\"error message\"].lower():\n                self._log.debug(\"fanart.tv: no image found\")\n            elif \"api key\" in data[\"error message\"].lower():\n                self._log.warning(\n                    \"fanart.tv: Invalid API key given, please \"\n                    \"enter a valid one in your config file.\"\n                )\n            else:\n                self._log.debug(\n                    \"fanart.tv: error on request: {}\", data[\"error message\"]\n                )\n            return\n\n        matches = []\n        # can there be more than one releasegroupid per response?\n        for mbid, art in data.get(\"albums\", {}).items():\n            # there might be more art referenced, e.g. cdart, and an albumcover\n            # might not be present, even if the request was successful\n            if album.mb_releasegroupid == mbid and \"albumcover\" in art:\n                matches.extend(art[\"albumcover\"])\n            # can this actually occur?\n            else:\n                self._log.debug(\n                    \"fanart.tv: unexpected mb_releasegroupid in response!\"\n                )\n\n        matches.sort(key=lambda x: int(x[\"likes\"]), reverse=True)\n        for item in matches:\n            # fanart.tv has a strict size requirement for album art to be\n            # uploaded\n            yield self._candidate(\n                url=item[\"url\"], match=MetadataMatch.EXACT, size=(1000, 1000)\n            )\n\n\nclass ITunesStore(RemoteArtSource):\n    NAME = \"iTunes Store\"\n    ID = \"itunes\"\n    API_URL = \"https://itunes.apple.com/search\"\n\n    def get(\n        self,\n        album: Album,\n        plugin: FetchArtPlugin,\n        paths: None | Sequence[bytes],\n    ) -> Iterator[Candidate]:\n        \"\"\"Return art URL from iTunes Store given an album title.\"\"\"\n        if not (album.albumartist and album.album):\n            return\n\n        payload = {\n            \"term\": f\"{album.albumartist} {album.album}\",\n            \"entity\": \"album\",\n            \"media\": \"music\",\n            \"limit\": 200,\n        }\n        try:\n            r = self.request(self.API_URL, params=payload)\n            r.raise_for_status()\n        except requests.RequestException as e:\n            self._log.debug(\"iTunes search failed: {}\", e)\n            return\n\n        try:\n            candidates = r.json()[\"results\"]\n        except ValueError as e:\n            self._log.debug(\"Could not decode json response: {}\", e)\n            return\n        except KeyError as e:\n            self._log.debug(\n                \"{} not found in json. Fields are {} \", e, list(r.json().keys())\n            )\n            return\n\n        if not candidates:\n            self._log.debug(\n                \"iTunes search for {!r} got no results\", payload[\"term\"]\n            )\n            return\n\n        if self._config[\"high_resolution\"]:\n            image_suffix = \"100000x100000-999\"\n        else:\n            image_suffix = \"1200x1200bb\"\n\n        for c in candidates:\n            try:\n                if (\n                    c[\"artistName\"] == album.albumartist\n                    and c[\"collectionName\"] == album.album\n                ):\n                    art_url = c[\"artworkUrl100\"]\n                    art_url = art_url.replace(\"100x100bb\", image_suffix)\n                    yield self._candidate(\n                        url=art_url, match=MetadataMatch.EXACT\n                    )\n            except KeyError as e:\n                self._log.debug(\n                    \"Malformed itunes candidate: {} not found in {}\",\n                    e,\n                    list(c.keys()),\n                )\n\n        try:\n            fallback_art_url = candidates[0][\"artworkUrl100\"]\n            fallback_art_url = fallback_art_url.replace(\n                \"100x100bb\", image_suffix\n            )\n            yield self._candidate(\n                url=fallback_art_url, match=MetadataMatch.FALLBACK\n            )\n        except KeyError as e:\n            self._log.debug(\n                \"Malformed itunes candidate: {} not found in {}\",\n                e,\n                list(c.keys()),\n            )\n\n\nclass Wikipedia(RemoteArtSource):\n    NAME = \"Wikipedia (queried through DBpedia)\"\n    ID = \"wikipedia\"\n    DBPEDIA_URL = \"https://dbpedia.org/sparql\"\n    WIKIPEDIA_URL = \"https://en.wikipedia.org/w/api.php\"\n    SPARQL_QUERY = \"\"\"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>\n                 PREFIX dbpprop: <http://dbpedia.org/property/>\n                 PREFIX owl: <http://dbpedia.org/ontology/>\n                 PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>\n                 PREFIX foaf: <http://xmlns.com/foaf/0.1/>\n\n                 SELECT DISTINCT ?pageId ?coverFilename WHERE {{\n                   ?subject owl:wikiPageID ?pageId .\n                   ?subject dbpprop:name ?name .\n                   ?subject rdfs:label ?label .\n                   {{ ?subject dbpprop:artist ?artist }}\n                     UNION\n                   {{ ?subject owl:artist ?artist }}\n                   {{ ?artist foaf:name \"{artist}\"@en }}\n                     UNION\n                   {{ ?artist dbpprop:name \"{artist}\"@en }}\n                   ?subject rdf:type <http://dbpedia.org/ontology/Album> .\n                   ?subject dbpprop:cover ?coverFilename .\n                   FILTER ( regex(?name, \"{album}\", \"i\") )\n                  }}\n                 Limit 1\"\"\"\n\n    def get(\n        self,\n        album: Album,\n        plugin: FetchArtPlugin,\n        paths: None | Sequence[bytes],\n    ) -> Iterator[Candidate]:\n        if not (album.albumartist and album.album):\n            return\n\n        # Find the name of the cover art filename on DBpedia\n        cover_filename, page_id = None, None\n\n        try:\n            dbpedia_response = self.request(\n                self.DBPEDIA_URL,\n                params={\n                    \"format\": \"application/sparql-results+json\",\n                    \"timeout\": 2500,\n                    \"query\": self.SPARQL_QUERY.format(\n                        artist=album.albumartist.title(), album=album.album\n                    ),\n                },\n                headers={\"content-type\": \"application/json\"},\n            )\n        except requests.RequestException:\n            self._log.debug(\"dbpedia: error receiving response\")\n            return\n\n        try:\n            data = dbpedia_response.json()\n            results = data[\"results\"][\"bindings\"]\n            if results:\n                cover_filename = f\"File:{results[0]['coverFilename']['value']}\"\n                page_id = results[0][\"pageId\"][\"value\"]\n            else:\n                self._log.debug(\"wikipedia: album not found on dbpedia\")\n        except (ValueError, KeyError, IndexError):\n            self._log.debug(\n                \"wikipedia: error scraping dbpedia response: {.text}\",\n                dbpedia_response,\n            )\n\n        # Ensure we have a filename before attempting to query wikipedia\n        if not (cover_filename and page_id):\n            return\n\n        # DBPedia sometimes provides an incomplete cover_filename, indicated\n        # by the filename having a space before the extension, e.g., 'foo .bar'\n        # An additional Wikipedia call can help to find the real filename.\n        # This may be removed once the DBPedia issue is resolved, see:\n        # https://github.com/dbpedia/extraction-framework/issues/396\n        if \" .\" in cover_filename and \".\" not in cover_filename.split(\" .\")[-1]:\n            self._log.debug(\n                \"wikipedia: dbpedia provided incomplete cover_filename\"\n            )\n            lpart, rpart = cover_filename.rsplit(\" .\", 1)\n\n            # Query all the images in the page\n            try:\n                wikipedia_response = self.request(\n                    self.WIKIPEDIA_URL,\n                    params={\n                        \"format\": \"json\",\n                        \"action\": \"query\",\n                        \"continue\": \"\",\n                        \"prop\": \"images\",\n                        \"pageids\": page_id,\n                    },\n                    headers={\"content-type\": \"application/json\"},\n                )\n            except requests.RequestException:\n                self._log.debug(\"wikipedia: error receiving response\")\n                return\n\n            # Try to see if one of the images on the pages matches our\n            # incomplete cover_filename\n            try:\n                data = wikipedia_response.json()\n                results = data[\"query\"][\"pages\"][page_id][\"images\"]\n                for result in results:\n                    if re.match(\n                        rf\"{re.escape(lpart)}.*?\\.{re.escape(rpart)}\",\n                        result[\"title\"],\n                    ):\n                        cover_filename = result[\"title\"]\n                        break\n            except (ValueError, KeyError):\n                self._log.debug(\n                    \"wikipedia: failed to retrieve a cover_filename\"\n                )\n                return\n\n        # Find the absolute url of the cover art on Wikipedia\n        try:\n            wikipedia_response = self.request(\n                self.WIKIPEDIA_URL,\n                params={\n                    \"format\": \"json\",\n                    \"action\": \"query\",\n                    \"continue\": \"\",\n                    \"prop\": \"imageinfo\",\n                    \"iiprop\": \"url\",\n                    \"titles\": cover_filename.encode(\"utf-8\"),\n                },\n                headers={\"content-type\": \"application/json\"},\n            )\n        except requests.RequestException:\n            self._log.debug(\"wikipedia: error receiving response\")\n            return\n\n        try:\n            data = wikipedia_response.json()\n            results = data[\"query\"][\"pages\"]\n            for _, result in results.items():\n                image_url = result[\"imageinfo\"][0][\"url\"]\n                yield self._candidate(url=image_url, match=MetadataMatch.EXACT)\n        except (ValueError, KeyError, IndexError):\n            self._log.debug(\"wikipedia: error scraping imageinfo\")\n            return\n\n\nclass FileSystem(LocalArtSource):\n    NAME = \"Filesystem\"\n    ID = \"filesystem\"\n\n    @staticmethod\n    def filename_priority(\n        filename: AnyStr, cover_names: Sequence[AnyStr]\n    ) -> list[int]:\n        \"\"\"Sort order for image names.\n\n        Return indexes of cover names found in the image filename. This\n        means that images with lower-numbered and more keywords will have\n        higher priority.\n        \"\"\"\n        return [idx for (idx, x) in enumerate(cover_names) if x in filename]\n\n    def get(\n        self,\n        album: Album,\n        plugin: FetchArtPlugin,\n        paths: None | Sequence[bytes],\n    ) -> Iterator[Candidate]:\n        \"\"\"Look for album art files in the specified directories.\"\"\"\n        if not paths:\n            return\n        cover_names = list(map(util.bytestring_path, plugin.cover_names))\n        cover_names_str = b\"|\".join(cover_names)\n        cover_pat = rb\"\".join([rb\"(\\b|_)(\", cover_names_str, rb\")(\\b|_)\"])\n\n        for path in paths:\n            if not os.path.isdir(syspath(path)):\n                continue\n\n            # Find all files that look like images in the directory.\n            images = []\n            ignore = config[\"ignore\"].as_str_seq()\n            ignore_hidden = config[\"ignore_hidden\"].get(bool)\n            for _, _, files in sorted_walk(\n                path, ignore=ignore, ignore_hidden=ignore_hidden\n            ):\n                for fn in files:\n                    fn = bytestring_path(fn)\n                    for ext in IMAGE_EXTENSIONS:\n                        if fn.lower().endswith(b\".\" + ext) and os.path.isfile(\n                            syspath(os.path.join(path, fn))\n                        ):\n                            images.append(fn)\n\n            # Look for \"preferred\" filenames.\n            images = sorted(\n                images, key=lambda x: self.filename_priority(x, cover_names)\n            )\n            remaining = []\n            for fn in images:\n                if re.search(cover_pat, os.path.splitext(fn)[0], re.I):\n                    self._log.debug(\n                        \"using well-named art file {}\",\n                        util.displayable_path(fn),\n                    )\n                    yield self._candidate(\n                        path=os.path.join(path, fn), match=MetadataMatch.EXACT\n                    )\n                else:\n                    remaining.append(fn)\n\n            # Fall back to a configured image.\n            if plugin.fallback:\n                self._log.debug(\n                    \"using fallback art file {}\",\n                    util.displayable_path(plugin.fallback),\n                )\n                yield self._candidate(\n                    path=plugin.fallback, match=MetadataMatch.FALLBACK\n                )\n\n            # Fall back to any image in the folder.\n            if remaining and not plugin.cautious:\n                self._log.debug(\n                    \"using fallback art file {}\",\n                    util.displayable_path(remaining[0]),\n                )\n                yield self._candidate(\n                    path=os.path.join(path, remaining[0]),\n                    match=MetadataMatch.FALLBACK,\n                )\n\n\nclass LastFM(RemoteArtSource):\n    NAME = \"Last.fm\"\n    ID = \"lastfm\"\n\n    # Sizes in priority order.\n    SIZES: ClassVar[dict[str, tuple[int, int]]] = OrderedDict(\n        [\n            (\"mega\", (300, 300)),\n            (\"extralarge\", (300, 300)),\n            (\"large\", (174, 174)),\n            (\"medium\", (64, 64)),\n            (\"small\", (34, 34)),\n        ]\n    )\n\n    API_URL = \"https://ws.audioscrobbler.com/2.0\"\n\n    def __init__(self, *args, **kwargs) -> None:\n        super().__init__(*args, **kwargs)\n        self.key = (self._config[\"lastfm_key\"].get(),)\n\n    @staticmethod\n    def add_default_config(config: confuse.ConfigView) -> None:\n        config.add(\n            {\n                \"lastfm_key\": None,\n            }\n        )\n        config[\"lastfm_key\"].redact = True\n\n    @classmethod\n    def available(cls, log: Logger, config: confuse.ConfigView) -> bool:\n        has_key = bool(config[\"lastfm_key\"].get())\n        if not has_key:\n            log.debug(\"lastfm: Disabling art source due to missing key\")\n        return has_key\n\n    def get(\n        self,\n        album: Album,\n        plugin: FetchArtPlugin,\n        paths: None | Sequence[bytes],\n    ) -> Iterator[Candidate]:\n        if not album.mb_albumid:\n            return\n\n        try:\n            response = self.request(\n                self.API_URL,\n                params={\n                    \"method\": \"album.getinfo\",\n                    \"api_key\": self.key,\n                    \"mbid\": album.mb_albumid,\n                    \"format\": \"json\",\n                },\n            )\n        except requests.RequestException:\n            self._log.debug(\"lastfm: error receiving response\")\n            return\n\n        try:\n            data = response.json()\n\n            if \"error\" in data:\n                if data[\"error\"] == 6:\n                    self._log.debug(\n                        \"lastfm: no results for {.mb_albumid}\", album\n                    )\n                else:\n                    self._log.error(\n                        \"lastfm: failed to get album info: {} ({})\",\n                        data[\"message\"],\n                        data[\"error\"],\n                    )\n            else:\n                images = {\n                    image[\"size\"]: image[\"#text\"]\n                    for image in data[\"album\"][\"image\"]\n                }\n\n                # Provide candidates in order of size.\n                for size in self.SIZES.keys():\n                    if size in images:\n                        yield self._candidate(\n                            url=images[size], size=self.SIZES[size]\n                        )\n        except ValueError:\n            self._log.debug(\"lastfm: error loading response: {.text}\", response)\n            return\n\n\nclass Spotify(RemoteArtSource):\n    NAME = \"Spotify\"\n    ID = \"spotify\"\n\n    SPOTIFY_ALBUM_URL = \"https://open.spotify.com/album/\"\n\n    @classmethod\n    def available(cls, log: Logger, config: confuse.ConfigView) -> bool:\n        if not HAS_BEAUTIFUL_SOUP:\n            log.debug(\n                \"To use Spotify as an album art source, \"\n                \"you must install the beautifulsoup4 module. See \"\n                \"the documentation for further details.\"\n            )\n        return HAS_BEAUTIFUL_SOUP\n\n    def get(\n        self,\n        album: Album,\n        plugin: FetchArtPlugin,\n        paths: None | Sequence[bytes],\n    ) -> Iterator[Candidate]:\n        try:\n            url = f\"{self.SPOTIFY_ALBUM_URL}{album.items().get().spotify_album_id}\"\n        except AttributeError:\n            self._log.debug(\"Fetchart: no Spotify album ID found\")\n            return\n\n        try:\n            response = requests.get(url, timeout=10)\n            response.raise_for_status()\n        except requests.RequestException as e:\n            self._log.debug(\"Error: {!s}\", e)\n            return\n\n        try:\n            html = response.text\n            soup = BeautifulSoup(html, \"html.parser\")\n        except ValueError:\n            self._log.debug(\n                \"Spotify: error loading response: {.text}\", response\n            )\n            return\n\n        tag = soup.find(\"meta\", attrs={\"property\": \"og:image\"})\n        if tag is None or not isinstance(tag, Tag):\n            self._log.debug(\n                \"Spotify: Unexpected response, og:image tag missing\"\n            )\n            return\n\n        image_url = tag[\"content\"]\n        yield self._candidate(url=image_url, match=MetadataMatch.EXACT)\n\n\nclass CoverArtUrl(RemoteArtSource):\n    # This source is intended to be used with a plugin that sets the\n    # cover_art_url field on albums or tracks. Users can also manually update\n    # the cover_art_url field using the \"set\" command. This source will then\n    # use that URL to fetch the image.\n\n    NAME = \"Cover Art URL\"\n    ID = \"cover_art_url\"\n\n    def get(\n        self,\n        album: Album,\n        plugin: FetchArtPlugin,\n        paths: None | Sequence[bytes],\n    ) -> Iterator[Candidate]:\n        image_url = None\n        try:\n            # look for cover_art_url on album or first track\n            if album.get(\"cover_art_url\"):\n                image_url = album.cover_art_url\n            else:\n                image_url = album.items().get().cover_art_url\n            self._log.debug(\"Cover art URL {} found for {}\", image_url, album)\n        except (AttributeError, TypeError):\n            self._log.debug(\"Cover art URL not found for {}\", album)\n            return\n        if image_url:\n            yield self._candidate(url=image_url, match=MetadataMatch.EXACT)\n        else:\n            self._log.debug(\"Cover art URL not found for {}\", album)\n            return\n\n\n# All art sources. The order they will be tried in is specified by the config.\nART_SOURCES: set[type[ArtSource]] = {\n    FileSystem,\n    CoverArtArchive,\n    ITunesStore,\n    AlbumArtOrg,\n    Amazon,\n    Wikipedia,\n    GoogleImages,\n    FanartTV,\n    LastFM,\n    Spotify,\n    CoverArtUrl,\n}\n\n\n# PLUGIN LOGIC ###############################################################\n\n\nclass FetchArtPlugin(plugins.BeetsPlugin, RequestMixin):\n    PAT_PX = r\"(0|[1-9][0-9]*)px\"\n    PAT_PERCENT = r\"(100(\\.00?)?|[1-9]?[0-9](\\.[0-9]{1,2})?)%\"\n\n    def __init__(self) -> None:\n        super().__init__()\n\n        # Holds candidates corresponding to downloaded images between\n        # fetching them and placing them in the filesystem.\n        self.art_candidates: dict[ImportTask, Candidate] = {}\n\n        self.config.add(\n            {\n                \"auto\": True,\n                \"minwidth\": 0,\n                \"maxwidth\": 0,\n                \"quality\": 0,\n                \"max_filesize\": 0,\n                \"enforce_ratio\": False,\n                \"cautious\": False,\n                \"cover_names\": [\"cover\", \"front\", \"art\", \"album\", \"folder\"],\n                \"fallback\": None,\n                \"sources\": [\n                    \"filesystem\",\n                    \"coverart\",\n                    \"itunes\",\n                    \"amazon\",\n                    \"albumart\",\n                    \"cover_art_url\",\n                ],\n                \"store_source\": False,\n                \"high_resolution\": False,\n                \"deinterlace\": False,\n                \"cover_format\": None,\n            }\n        )\n        for source in ART_SOURCES:\n            source.add_default_config(self.config)\n\n        self.minwidth = self.config[\"minwidth\"].get(int)\n        self.maxwidth = self.config[\"maxwidth\"].get(int)\n        self.max_filesize = self.config[\"max_filesize\"].get(int)\n        self.quality = self.config[\"quality\"].get(int)\n\n        # allow both pixel and percentage-based margin specifications\n        self.enforce_ratio = self.config[\"enforce_ratio\"].get(\n            confuse.OneOf[bool | str](\n                [\n                    bool,\n                    confuse.String(pattern=self.PAT_PX),\n                    confuse.String(pattern=self.PAT_PERCENT),\n                ]\n            )\n        )\n        self.margin_px = None\n        self.margin_percent = None\n        self.deinterlace = self.config[\"deinterlace\"].get(bool)\n        if isinstance(self.enforce_ratio, str):\n            if self.enforce_ratio[-1] == \"%\":\n                self.margin_percent = float(self.enforce_ratio[:-1]) / 100\n            elif self.enforce_ratio[-2:] == \"px\":\n                self.margin_px = int(self.enforce_ratio[:-2])\n            else:\n                # shouldn't happen\n                raise confuse.ConfigValueError()\n            self.enforce_ratio = True\n\n        cover_names = self.config[\"cover_names\"].as_str_seq()\n        self.cover_names = list(map(util.bytestring_path, cover_names))\n        self.cautious = self.config[\"cautious\"].get(bool)\n        self.fallback = self.config[\"fallback\"].get(\n            confuse.Optional(confuse.Filename())\n        )\n        self.store_source = self.config[\"store_source\"].get(bool)\n\n        self.cover_format = self.config[\"cover_format\"].get(\n            confuse.Optional(str)\n        )\n\n        if self.config[\"auto\"]:\n            # Enable two import hooks when fetching is enabled.\n            self.import_stages = [self.fetch_art]\n            self.register_listener(\"import_task_files\", self.assign_art)\n\n        available_sources = [\n            (s_cls.ID, c)\n            for s_cls in ART_SOURCES\n            if s_cls.available(self._log, self.config)\n            for c in s_cls.VALID_MATCHING_CRITERIA\n        ]\n        sources = sanitize_pairs(\n            self.config[\"sources\"].as_pairs(default_value=\"*\"),\n            available_sources,\n        )\n\n        if \"remote_priority\" in self.config:\n            self._log.warning(\n                \"The `fetch_art.remote_priority` configuration option has \"\n                \"been deprecated. Instead, place `filesystem` at the end of \"\n                \"your `sources` list.\"\n            )\n            if self.config[\"remote_priority\"].get(bool):\n                fs = []\n                others = []\n                for s, c in sources:\n                    if s == \"filesystem\":\n                        fs.append((s, c))\n                    else:\n                        others.append((s, c))\n                sources = others + fs\n\n        sources_by_name = {s_cls.ID: s_cls for s_cls in ART_SOURCES}\n\n        self.sources = [\n            sources_by_name[s](self._log, self.config, match_by=[c])\n            for s, c in sources\n        ]\n\n    @staticmethod\n    def _is_source_file_removal_enabled() -> bool:\n        return config[\"import\"][\"delete\"].get(bool) or config[\"import\"][\n            \"move\"\n        ].get(bool)\n\n    def _is_candidate_fallback(self, candidate: Candidate) -> bool:\n        try:\n            return (\n                candidate.path is not None\n                and self.fallback is not None\n                and os.path.samefile(candidate.path, self.fallback)\n            )\n        except OSError:\n            return False\n\n    # Asynchronous; after music is added to the library.\n    def fetch_art(self, session: ImportSession, task: ImportTask) -> None:\n        \"\"\"Find art for the album being imported.\"\"\"\n        if task.is_album:  # Only fetch art for full albums.\n            if task.album.artpath and os.path.isfile(\n                syspath(task.album.artpath)\n            ):\n                # Album already has art (probably a re-import); skip it.\n                return\n            if task.choice_flag == importer.Action.ASIS:\n                # For as-is imports, don't search Web sources for art.\n                local = True\n            elif task.choice_flag in (\n                importer.Action.APPLY,\n                importer.Action.RETAG,\n            ):\n                # Search everywhere for art.\n                local = False\n            else:\n                # For any other choices (e.g., TRACKS), do nothing.\n                return\n\n            candidate = self.art_for_album(task.album, task.paths, local)\n\n            if candidate:\n                self.art_candidates[task] = candidate\n\n    def _set_art(\n        self, album: Album, candidate: Candidate, delete: bool = False\n    ) -> None:\n        album.set_art(candidate.path, delete)\n        if self.store_source:\n            # store the source of the chosen artwork in a flexible field\n            self._log.debug(\n                \"Storing art_source for {0.albumartist} - {0.album}\", album\n            )\n            album.art_source = candidate.source_name\n        album.store()\n\n    # Synchronous; after music files are put in place.\n    def assign_art(self, session: ImportSession, task: ImportTask):\n        \"\"\"Place the discovered art in the filesystem.\"\"\"\n        if task in self.art_candidates:\n            candidate = self.art_candidates.pop(task)\n            removal_enabled = self._is_source_file_removal_enabled()\n\n            self._set_art(task.album, candidate, not removal_enabled)\n\n            if removal_enabled and not self._is_candidate_fallback(candidate):\n                task.prune(candidate.path)\n\n    # Manual album art fetching.\n    def commands(self) -> list[ui.Subcommand]:\n        cmd = ui.Subcommand(\"fetchart\", help=\"download album art\")\n        cmd.parser.add_option(\n            \"-f\",\n            \"--force\",\n            dest=\"force\",\n            action=\"store_true\",\n            default=False,\n            help=\"re-download art when already present\",\n        )\n        cmd.parser.add_option(\n            \"-q\",\n            \"--quiet\",\n            dest=\"quiet\",\n            action=\"store_true\",\n            default=False,\n            help=\"quiet mode: do not output albums that already have artwork\",\n        )\n\n        def func(lib: Library, opts, args) -> None:\n            self.batch_fetch_art(lib, lib.albums(args), opts.force, opts.quiet)\n\n        cmd.func = func\n        return [cmd]\n\n    # Utilities converted from functions to methods on logging overhaul\n\n    def art_for_album(\n        self,\n        album: Album,\n        paths: None | Sequence[bytes],\n        local_only: bool = False,\n    ) -> None | Candidate:\n        \"\"\"Given an Album object, returns a path to downloaded art for the\n        album (or None if no art is found). If `maxwidth`, then images are\n        resized to this maximum pixel size. If `quality` then resized images\n        are saved at the specified quality level. If `local_only`, then only\n        local image files from the filesystem are returned; no network\n        requests are made.\n        \"\"\"\n        out = None\n\n        for source in self.sources:\n            if source.LOC == \"local\" or not local_only:\n                self._log.debug(\n                    \"trying source {0.description}\"\n                    \" for album {1.albumartist} - {1.album}\",\n                    source,\n                    album,\n                )\n                # URLs might be invalid at this point, or the image may not\n                # fulfill the requirements\n                for candidate in source.get(album, self, paths):\n                    source.fetch_image(candidate, self)\n                    if candidate.validate(self) != ImageAction.BAD:\n                        out = candidate\n                        assert out.path is not None  # help mypy\n                        self._log.debug(\n                            \"using {.LOC} image {.path}\", source, out\n                        )\n                        break\n                    # Remove temporary files for invalid candidates.\n                    source.cleanup(candidate)\n                if out:\n                    break\n\n        if out:\n            out.resize(self)\n\n        return out\n\n    def batch_fetch_art(\n        self,\n        lib: Library,\n        albums: Iterable[Album],\n        force: bool,\n        quiet: bool,\n    ) -> None:\n        \"\"\"Fetch album art for each of the albums. This implements the manual\n        fetchart CLI command.\n        \"\"\"\n        for album in albums:\n            if (\n                album.artpath\n                and not force\n                and os.path.isfile(syspath(album.artpath))\n            ):\n                if not quiet:\n                    message = colorize(\"text_highlight_minor\", \"has album art\")\n                    ui.print_(f\"{album}: {message}\")\n            else:\n                # In ordinary invocations, look for images on the\n                # filesystem. When forcing, however, always go to the Web\n                # sources.\n                local_paths = None if force else [album.path]\n\n                candidate = self.art_for_album(album, local_paths)\n                if candidate:\n                    self._set_art(album, candidate)\n                    message = colorize(\"text_success\", \"found album art\")\n                else:\n                    message = colorize(\"text_error\", \"no art found\")\n                ui.print_(f\"{album}: {message}\")\n"
  },
  {
    "path": "beetsplug/filefilter.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Malte Ried.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Filter imported files using a regular expression.\"\"\"\n\nimport re\n\nfrom beets import config\nfrom beets.importer import SingletonImportTask\nfrom beets.plugins import BeetsPlugin\nfrom beets.util import bytestring_path\n\n\nclass FileFilterPlugin(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n        self.register_listener(\n            \"import_task_created\", self.import_task_created_event\n        )\n        self.config.add({\"path\": \".*\"})\n\n        self.path_album_regex = self.path_singleton_regex = re.compile(\n            bytestring_path(self.config[\"path\"].get())\n        )\n\n        if \"album_path\" in self.config:\n            self.path_album_regex = re.compile(\n                bytestring_path(self.config[\"album_path\"].get())\n            )\n\n        if \"singleton_path\" in self.config:\n            self.path_singleton_regex = re.compile(\n                bytestring_path(self.config[\"singleton_path\"].get())\n            )\n\n    def import_task_created_event(self, session, task):\n        if task.items and len(task.items) > 0:\n            items_to_import = []\n            for item in task.items:\n                if self.file_filter(item[\"path\"]):\n                    items_to_import.append(item)\n            if len(items_to_import) > 0:\n                task.items = items_to_import\n            else:\n                # Returning an empty list of tasks from the handler\n                # drops the task from the rest of the importer pipeline.\n                return []\n\n        elif isinstance(task, SingletonImportTask):\n            if not self.file_filter(task.item[\"path\"]):\n                return []\n\n        # If not filtered, return the original task unchanged.\n        return [task]\n\n    def file_filter(self, full_path):\n        \"\"\"Checks if the configured regular expressions allow the import\n        of the file given in full_path.\n        \"\"\"\n        import_config = dict(config[\"import\"])\n        full_path = bytestring_path(full_path)\n        if \"singletons\" not in import_config or not import_config[\"singletons\"]:\n            # Album\n            return self.path_album_regex.match(full_path) is not None\n        else:\n            # Singleton\n            return self.path_singleton_regex.match(full_path) is not None\n"
  },
  {
    "path": "beetsplug/fish.py",
    "content": "# This file is part of beets.\n# Copyright 2015, winters jean-marie.\n# Copyright 2020, Justin Mayer <https://justinmayer.com>\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"This plugin generates tab completions for Beets commands for the Fish shell\n<https://fishshell.com/>, including completions for Beets commands, plugin\ncommands, and option flags. Also generated are completions for all the album\nand track fields, suggesting for example `genres:` or `album:` when querying the\nBeets database. Completions for the *values* of those fields are not generated\nby default but can be added via the `-e` / `--extravalues` flag. For example:\n`beet fish -e genres -e albumartist`\n\"\"\"\n\nimport os\nfrom operator import attrgetter\n\nfrom beets import library, plugins, ui\nfrom beets.plugins import BeetsPlugin\nfrom beets.ui import commands\n\nBL_NEED2 = \"\"\"complete -c beet -n '__fish_beet_needs_command' {} {}\\n\"\"\"\nBL_USE3 = \"\"\"complete -c beet -n '__fish_beet_using_command {}' {} {}\\n\"\"\"\nBL_SUBS = \"\"\"complete -c beet -n '__fish_at_level {} \"\"' {}  {}\\n\"\"\"\nBL_EXTRA3 = \"\"\"complete -c beet -n '__fish_beet_use_extra {}' {} {}\\n\"\"\"\n\nHEAD = \"\"\"\nfunction __fish_beet_needs_command\n    set cmd (commandline -opc)\n    if test (count $cmd) -eq 1\n        return 0\n    end\n    return 1\nend\n\nfunction __fish_beet_using_command\n    set cmd (commandline -opc)\n    set needle (count $cmd)\n    if test $needle -gt 1\n        if begin test $argv[1] = $cmd[2];\n            and not contains -- $cmd[$needle] $FIELDS; end\n                return 0\n        end\n    end\n    return 1\nend\n\nfunction __fish_beet_use_extra\n    set cmd (commandline -opc)\n    set needle (count $cmd)\n    if test $argv[2]  = $cmd[$needle]\n        return 0\n    end\n    return 1\nend\n\"\"\"\n\n\nclass FishPlugin(BeetsPlugin):\n    def commands(self):\n        cmd = ui.Subcommand(\"fish\", help=\"generate Fish shell tab completions\")\n        cmd.func = self.run\n        cmd.parser.add_option(\n            \"-f\",\n            \"--noFields\",\n            action=\"store_true\",\n            default=False,\n            help=\"omit album/track field completions\",\n        )\n        cmd.parser.add_option(\n            \"-e\",\n            \"--extravalues\",\n            action=\"append\",\n            type=\"choice\",\n            choices=library.Item.all_keys() + library.Album.all_keys(),\n            help=\"include specified field *values* in completions\",\n        )\n        cmd.parser.add_option(\n            \"-o\",\n            \"--output\",\n            default=\"~/.config/fish/completions/beet.fish\",\n            help=(\n                \"where to save the script. default: ~/.config/fish/completions\"\n            ),\n        )\n        return [cmd]\n\n    def run(self, lib, opts, args):\n        # Gather the commands from Beets core and its plugins.\n        # Collect the album and track fields.\n        # If specified, also collect the values for these fields.\n        # Make a giant string of all the above, formatted in a way that\n        # allows Fish to do tab completion for the `beet` command.\n\n        completion_file_path = os.path.expanduser(opts.output)\n        completion_dir = os.path.dirname(completion_file_path)\n\n        if completion_dir != \"\":\n            os.makedirs(completion_dir, exist_ok=True)\n\n        nobasicfields = opts.noFields  # Do not complete for album/track fields\n        extravalues = opts.extravalues  # e.g., Also complete artists names\n        beetcmds = sorted(\n            (commands.default_commands + plugins.commands()),\n            key=attrgetter(\"name\"),\n        )\n        fields = sorted(set(library.Album.all_keys() + library.Item.all_keys()))\n        # Collect commands, their aliases, and their help text\n        cmd_names_help = []\n        for cmd in beetcmds:\n            names = list(cmd.aliases)\n            names.append(cmd.name)\n            for name in names:\n                cmd_names_help.append((name, cmd.help))\n        # Concatenate the string\n        totstring = f\"{HEAD}\\n\"\n        totstring += get_cmds_list([name[0] for name in cmd_names_help])\n        totstring += \"\" if nobasicfields else get_standard_fields(fields)\n        totstring += get_extravalues(lib, extravalues) if extravalues else \"\"\n        totstring += \"\\n# ====== setup basic beet completion =====\\n\\n\"\n        totstring += get_basic_beet_options()\n        totstring += \"\\n# ====== setup field completion for subcommands =====\\n\"\n        totstring += get_subcommands(cmd_names_help, nobasicfields, extravalues)\n        # Set up completion for all the command options\n        totstring += get_all_commands(beetcmds)\n\n        with open(completion_file_path, \"w\") as fish_file:\n            fish_file.write(totstring)\n\n\ndef _escape(name):\n    # Escape ? in fish\n    if name == \"?\":\n        name = f\"\\\\{name}\"\n    return name\n\n\ndef get_cmds_list(cmds_names):\n    # Make a list of all Beets core & plugin commands\n    return f\"set CMDS {' '.join(cmds_names)}\\n\\n\"\n\n\ndef get_standard_fields(fields):\n    # Make a list of album/track fields and append with ':'\n    fields = (f\"{field}:\" for field in fields)\n    return f\"set FIELDS {' '.join(fields)}\\n\\n\"\n\n\ndef get_extravalues(lib, extravalues):\n    # Make a list of all values from an album/track field.\n    # 'beet ls albumartist: <TAB>' yields completions for ABBA, Beatles, etc.\n    word = \"\"\n    values_set = get_set_of_values_for_field(lib, extravalues)\n    for fld in extravalues:\n        extraname = f\"{fld.upper()}S\"\n        word += f\"set  {extraname} {' '.join(sorted(values_set[fld]))}\\n\\n\"\n    return word\n\n\ndef get_set_of_values_for_field(lib, fields):\n    # Get unique values from a specified album/track field\n    fields_dict = {}\n    for each in fields:\n        fields_dict[each] = set()\n    for item in lib.items():\n        for field in fields:\n            fields_dict[field].add(wrap(item[field]))\n    return fields_dict\n\n\ndef get_basic_beet_options():\n    word = (\n        BL_NEED2.format(\"-l format-item\", \"-f -d 'print with custom format'\")\n        + BL_NEED2.format(\"-l format-album\", \"-f -d 'print with custom format'\")\n        + BL_NEED2.format(\n            \"-s  l  -l library\", \"-F -r -d 'library database file to use'\"\n        )\n        + BL_NEED2.format(\n            \"-s  d  -l directory\", \"-F -r -d 'destination music directory'\"\n        )\n        + BL_NEED2.format(\n            \"-s  v  -l verbose\", \"-f -d 'print debugging information'\"\n        )\n        + BL_NEED2.format(\n            \"-s  c  -l config\", \"-F -r -d 'path to configuration file'\"\n        )\n        + BL_NEED2.format(\n            \"-s  h  -l help\", \"-f -d 'print this help message and exit'\"\n        )\n    )\n    return word\n\n\ndef get_subcommands(cmd_name_and_help, nobasicfields, extravalues):\n    # Formatting for Fish to complete our fields/values\n    word = \"\"\n    for cmdname, cmdhelp in cmd_name_and_help:\n        cmdname = _escape(cmdname)\n\n        word += f\"\\n# ------ fieldsetups for {cmdname} -------\\n\"\n        word += BL_NEED2.format(\n            f\"-a {cmdname}\", f\"-f -d {wrap(clean_whitespace(cmdhelp))}\"\n        )\n\n        if nobasicfields is False:\n            word += BL_USE3.format(\n                cmdname,\n                f\"-a {wrap('$FIELDS')}\",\n                f\"-d {wrap('fieldname')}\",\n            )\n\n        if extravalues:\n            for f in extravalues:\n                setvar = wrap(f\"${f.upper()}S\")\n                word += \" \".join(\n                    BL_EXTRA3.format(\n                        f\"{cmdname} {f}:\",\n                        f\"-f -A -a {setvar}\",\n                        f\"-d {wrap(f)}\",\n                    ).split()\n                )\n                word += \"\\n\"\n    return word\n\n\ndef get_all_commands(beetcmds):\n    # Formatting for Fish to complete command options\n    word = \"\"\n    for cmd in beetcmds:\n        names = list(cmd.aliases)\n        names.append(cmd.name)\n        for name in names:\n            name = _escape(name)\n\n            word += f\"\\n\\n\\n# ====== completions for {name} =====\\n\"\n\n            for option in cmd.parser._get_all_options()[1:]:\n                cmd_l = (\n                    f\" -l {option._long_opts[0].replace('--', '')}\"\n                    if option._long_opts\n                    else \"\"\n                )\n                cmd_s = (\n                    f\" -s {option._short_opts[0].replace('-', '')}\"\n                    if option._short_opts\n                    else \"\"\n                )\n                cmd_need_arg = \" -r \" if option.nargs in [1] else \"\"\n                cmd_helpstr = (\n                    f\" -d {wrap(' '.join(option.help.split()))}\"\n                    if option.help\n                    else \"\"\n                )\n                cmd_arglist = (\n                    f\" -a {wrap(' '.join(option.choices))}\"\n                    if option.choices\n                    else \"\"\n                )\n\n                word += \" \".join(\n                    BL_USE3.format(\n                        name,\n                        f\"{cmd_need_arg}{cmd_s}{cmd_l} {cmd_arglist}\",\n                        cmd_helpstr,\n                    ).split()\n                )\n                word += \"\\n\"\n\n            word = word + BL_USE3.format(\n                name,\n                \"-s h -l help\",\n                f\"-d {wrap('print help')}\",\n            )\n    return word\n\n\ndef clean_whitespace(word):\n    # Remove excess whitespace and tabs in a string\n    return \" \".join(word.split())\n\n\ndef wrap(word):\n    # Need \" or ' around strings but watch out if they're in the string\n    sptoken = '\"'\n    if '\"' in word and (\"'\") in word:\n        word.replace('\"', sptoken)\n        return f'\"{word}\"'\n\n    tok = '\"' if \"'\" in word else \"'\"\n    return f\"{tok}{word}{tok}\"\n"
  },
  {
    "path": "beetsplug/freedesktop.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Matt Lichtenberg.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Creates freedesktop.org-compliant .directory files on an album level.\"\"\"\n\nfrom beets import ui\nfrom beets.plugins import BeetsPlugin\n\n\nclass FreedesktopPlugin(BeetsPlugin):\n    def commands(self):\n        deprecated = ui.Subcommand(\n            \"freedesktop\",\n            help=\"Print a message to redirect to thumbnails --dolphin\",\n        )\n        deprecated.func = self.deprecation_message\n        return [deprecated]\n\n    def deprecation_message(self, lib, opts, args):\n        ui.print_(\n            \"This plugin is deprecated. Its functionality is \"\n            \"superseded by the 'thumbnails' plugin\"\n        )\n        ui.print_(\n            \"'thumbnails --dolphin' replaces freedesktop. See doc & \"\n            \"changelog for more information\"\n        )\n"
  },
  {
    "path": "beetsplug/fromfilename.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Jan-Erik Dahlin\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"If the title is empty, try to extract it from the filename\n(possibly also extract track and artist)\n\"\"\"\n\nimport os\nimport re\n\nfrom beets import plugins\nfrom beets.util import displayable_path\n\n# Filename field extraction patterns.\nPATTERNS = [\n    # Useful patterns.\n    (\n        r\"^(?P<track>\\d+)\\.?\\s*-\\s*(?P<artist>.+?)\\s*-\\s*(?P<title>.+?)\"\n        r\"(\\s*-\\s*(?P<tag>.*))?$\"\n    ),\n    r\"^(?P<artist>.+?)\\s*-\\s*(?P<title>.+?)(\\s*-\\s*(?P<tag>.*))?$\",\n    r\"^(?P<track>\\d+)\\.?[\\s_-]+(?P<title>.+)$\",\n    r\"^(?P<title>.+) by (?P<artist>.+)$\",\n    r\"^(?P<track>\\d+).*$\",\n    r\"^(?P<title>.+)$\",\n]\n\n# Titles considered \"empty\" and in need of replacement.\nBAD_TITLE_PATTERNS = [\n    r\"^$\",\n]\n\n\ndef equal(seq):\n    \"\"\"Determine whether a sequence holds identical elements.\"\"\"\n    return len(set(seq)) <= 1\n\n\ndef equal_fields(matchdict, field):\n    \"\"\"Do all items in `matchdict`, whose values are dictionaries, have\n    the same value for `field`? (If they do, the field is probably not\n    the title.)\n    \"\"\"\n    return equal(m[field] for m in matchdict.values())\n\n\ndef all_matches(names, pattern):\n    \"\"\"If all the filenames in the item/filename mapping match the\n    pattern, return a dictionary mapping the items to dictionaries\n    giving the value for each named subpattern in the match. Otherwise,\n    return None.\n    \"\"\"\n    matches = {}\n    for item, name in names.items():\n        m = re.match(pattern, name, re.IGNORECASE)\n        if m and m.groupdict():\n            # Only yield a match when the regex applies *and* has\n            # capture groups. Otherwise, no information can be extracted\n            # from the filename.\n            matches[item] = m.groupdict()\n        else:\n            return None\n    return matches\n\n\ndef bad_title(title):\n    \"\"\"Determine whether a given title is \"bad\" (empty or otherwise\n    meaningless) and in need of replacement.\n    \"\"\"\n    for pat in BAD_TITLE_PATTERNS:\n        if re.match(pat, title, re.IGNORECASE):\n            return True\n    return False\n\n\ndef apply_matches(d, log):\n    \"\"\"Given a mapping from items to field dicts, apply the fields to\n    the objects.\n    \"\"\"\n    some_map = next(iter(d.values()))\n    keys = some_map.keys()\n\n    # Only proceed if the \"tag\" field is equal across all filenames.\n    if \"tag\" in keys and not equal_fields(d, \"tag\"):\n        return\n\n    # Given both an \"artist\" and \"title\" field, assume that one is\n    # *actually* the artist, which must be uniform, and use the other\n    # for the title. This, of course, won't work for VA albums.\n    # Only check for \"artist\": patterns containing it, also contain \"title\"\n    if \"artist\" in keys:\n        if equal_fields(d, \"artist\"):\n            artist = some_map[\"artist\"]\n            title_field = \"title\"\n        elif equal_fields(d, \"title\"):\n            artist = some_map[\"title\"]\n            title_field = \"artist\"\n        else:\n            # Both vary. Abort.\n            return\n\n        for item in d:\n            if not item.artist:\n                item.artist = artist\n                log.info(\"Artist replaced with: {.artist}\", item)\n    # otherwise, if the pattern contains \"title\", use that for title_field\n    elif \"title\" in keys:\n        title_field = \"title\"\n    else:\n        title_field = None\n\n    # Apply the title and track, if any.\n    for item in d:\n        if title_field and bad_title(item.title):\n            item.title = str(d[item][title_field])\n            log.info(\"Title replaced with: {.title}\", item)\n\n        if \"track\" in d[item] and item.track == 0:\n            item.track = int(d[item][\"track\"])\n            log.info(\"Track replaced with: {.track}\", item)\n\n\n# Plugin structure and hook into import process.\n\n\nclass FromFilenamePlugin(plugins.BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n        self.register_listener(\"import_task_start\", self.filename_task)\n\n    def filename_task(self, task, session):\n        \"\"\"Examine each item in the task to see if we can extract a title\n        from the filename. Try to match all filenames to a number of\n        regexps, starting with the most complex patterns and successively\n        trying less complex patterns. As soon as all filenames match the\n        same regex we can make an educated guess of which part of the\n        regex that contains the title.\n        \"\"\"\n        items = task.items if task.is_album else [task.item]\n\n        # Look for suspicious (empty or meaningless) titles.\n        missing_titles = sum(bad_title(i.title) for i in items)\n\n        if missing_titles:\n            # Get the base filenames (no path or extension).\n            names = {}\n            for item in items:\n                path = displayable_path(item.path)\n                name, _ = os.path.splitext(os.path.basename(path))\n                names[item] = name\n\n            # Look for useful information in the filenames.\n            for pattern in PATTERNS:\n                self._log.debug(f\"Trying pattern: {pattern}\")\n                d = all_matches(names, pattern)\n                if d:\n                    apply_matches(d, self._log)\n"
  },
  {
    "path": "beetsplug/ftintitle.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Verrus, <github.com/Verrus/beets-plugin-featInTitle>\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Moves \"featured\" artists to the title from the artist field.\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom functools import cached_property, lru_cache\nfrom typing import TYPE_CHECKING\n\nfrom beets import config, plugins, ui\n\nif TYPE_CHECKING:\n    from beets.importer import ImportSession, ImportTask\n    from beets.library import Album, Item\n\n\nDEFAULT_BRACKET_KEYWORDS: tuple[str, ...] = (\n    \"abridged\",\n    \"acapella\",\n    \"club\",\n    \"demo\",\n    \"edit\",\n    \"edition\",\n    \"extended\",\n    \"instrumental\",\n    \"live\",\n    \"mix\",\n    \"radio\",\n    \"release\",\n    \"remaster\",\n    \"remastered\",\n    \"remix\",\n    \"rmx\",\n    \"unabridged\",\n    \"unreleased\",\n    \"version\",\n    \"vip\",\n)\n\n\ndef split_on_feat(\n    artist: str,\n    for_artist: bool = True,\n    custom_words: list[str] | None = None,\n) -> tuple[str, str | None]:\n    \"\"\"Given an artist string, split the \"main\" artist from any artist\n    on the right-hand side of a string like \"feat\". Return the main\n    artist, which is always a string, and the featuring artist, which\n    may be a string or None if none is present.\n    \"\"\"\n    # Try explicit featuring tokens first (ft, feat, featuring, etc.)\n    # to avoid splitting on generic separators like \"&\" when both are present\n    regex_explicit = re.compile(\n        plugins.feat_tokens(for_artist=False, custom_words=custom_words),\n        re.IGNORECASE,\n    )\n    parts = tuple(s.strip() for s in regex_explicit.split(artist, 1))\n    if len(parts) == 2:\n        return parts\n\n    # Try comma as separator\n    # (e.g. \"Alice, Bob & Charlie\" where Bob and Charlie are featuring)\n    if for_artist and \",\" in artist:\n        comma_parts = artist.split(\",\", 1)\n        return comma_parts[0].strip(), comma_parts[1].strip()\n\n    # Fall back to all tokens including generic separators if no explicit match\n    if for_artist:\n        regex = re.compile(\n            plugins.feat_tokens(for_artist, custom_words), re.IGNORECASE\n        )\n        parts = tuple(s.strip() for s in regex.split(artist, 1))\n\n    if len(parts) == 1:\n        return parts[0], None\n    else:\n        assert len(parts) == 2  # help mypy out\n        return parts\n\n\ndef contains_feat(title: str, custom_words: list[str] | None = None) -> bool:\n    \"\"\"Determine whether the title contains a \"featured\" marker.\"\"\"\n    return bool(\n        re.search(\n            plugins.feat_tokens(for_artist=False, custom_words=custom_words),\n            title,\n            flags=re.IGNORECASE,\n        )\n    )\n\n\ndef find_feat_part(\n    artist: str,\n    albumartist: str | None,\n    custom_words: list[str] | None = None,\n) -> str | None:\n    \"\"\"Attempt to find featured artists in the item's artist fields and\n    return the results. Returns None if no featured artist found.\n    \"\"\"\n    # Handle a wider variety of extraction cases if the album artist is\n    # contained within the track artist.\n    if albumartist and albumartist in artist:\n        albumartist_split = artist.split(albumartist, 1)\n\n        # If the last element of the split (the right-hand side of the\n        # album artist) is nonempty, then it probably contains the\n        # featured artist.\n        if albumartist_split[1] != \"\":\n            # Extract the featured artist from the right-hand side.\n            _, feat_part = split_on_feat(\n                albumartist_split[1], custom_words=custom_words\n            )\n            return feat_part\n\n        # Otherwise, if there's nothing on the right-hand side,\n        # look for a featuring artist on the left-hand side.\n        else:\n            lhs, _ = split_on_feat(\n                albumartist_split[0], custom_words=custom_words\n            )\n            if lhs:\n                return lhs\n\n    # Fall back to conservative handling of the track artist without relying\n    # on albumartist, which covers compilations using a 'Various Artists'\n    # albumartist and album tracks by a guest artist featuring a third artist.\n    _, feat_part = split_on_feat(artist, False, custom_words)\n    return feat_part\n\n\ndef _album_artist_no_feat(album: Album) -> str:\n    custom_words = config[\"ftintitle\"][\"custom_words\"].as_str_seq()\n    return split_on_feat(album[\"albumartist\"], False, list(custom_words))[0]\n\n\nclass FtInTitlePlugin(plugins.BeetsPlugin):\n    @cached_property\n    def bracket_keywords(self) -> list[str]:\n        return self.config[\"bracket_keywords\"].as_str_seq()\n\n    @staticmethod\n    @lru_cache(maxsize=256)\n    def _bracket_position_pattern(keywords: tuple[str, ...]) -> re.Pattern[str]:\n        \"\"\"\n        Build a compiled regex to find the first bracketed segment that contains\n        any of the provided keywords.\n\n        Cached by keyword tuple to avoid recompiling on every track/title.\n        \"\"\"\n        kw_inner = \"|\".join(map(re.escape, keywords))\n\n        # If we have keywords, require one of them to appear in the bracket text.\n        # If kw == \"\", the lookahead becomes true and we match any bracket content.\n        kw = rf\"\\b(?={kw_inner})\\b\" if kw_inner else \"\"\n        return re.compile(\n            rf\"\"\"\n            (?:   # non-capturing group for the split\n              \\s*?  # optional whitespace before brackets\n              (?=     # any bracket containing a keyword\n                    \\([^)]*{kw}.*?\\)\n                |   \\[[^]]*{kw}.*?\\]\n                |    <[^>]*{kw}.*? >\n                | \\{{[^}}]*{kw}.*?\\}}\n                | $   # or the end of the string\n              )\n            )\n            \"\"\",\n            re.IGNORECASE | re.VERBOSE,\n        )\n\n    def __init__(self) -> None:\n        super().__init__()\n\n        self.config.add(\n            {\n                \"auto\": True,\n                \"drop\": False,\n                \"format\": \"feat. {}\",\n                \"keep_in_artist\": False,\n                \"preserve_album_artist\": True,\n                \"custom_words\": [],\n                \"bracket_keywords\": list(DEFAULT_BRACKET_KEYWORDS),\n            }\n        )\n\n        self._command = ui.Subcommand(\n            \"ftintitle\", help=\"move featured artists to the title field\"\n        )\n\n        self._command.parser.add_option(\n            \"-d\",\n            \"--drop\",\n            dest=\"drop\",\n            action=\"store_true\",\n            default=None,\n            help=\"drop featuring from artists and ignore title update\",\n        )\n\n        if self.config[\"auto\"]:\n            self.import_stages = [self.imported]\n\n        self.album_template_fields[\"album_artist_no_feat\"] = (\n            _album_artist_no_feat\n        )\n\n    def commands(self) -> list[ui.Subcommand]:\n        def func(lib, opts, args):\n            self.config.set_args(opts)\n            drop_feat = self.config[\"drop\"].get(bool)\n            keep_in_artist_field = self.config[\"keep_in_artist\"].get(bool)\n            preserve_album_artist = self.config[\"preserve_album_artist\"].get(\n                bool\n            )\n            custom_words = self.config[\"custom_words\"].get(list)\n            write = ui.should_write()\n\n            for item in lib.items(args):\n                if self.ft_in_title(\n                    item,\n                    drop_feat,\n                    keep_in_artist_field,\n                    preserve_album_artist,\n                    custom_words,\n                ):\n                    item.store()\n                    if write:\n                        item.try_write()\n\n        self._command.func = func\n        return [self._command]\n\n    def imported(self, session: ImportSession, task: ImportTask) -> None:\n        \"\"\"Import hook for moving featuring artist automatically.\"\"\"\n        drop_feat = self.config[\"drop\"].get(bool)\n        keep_in_artist_field = self.config[\"keep_in_artist\"].get(bool)\n        preserve_album_artist = self.config[\"preserve_album_artist\"].get(bool)\n        custom_words = self.config[\"custom_words\"].get(list)\n\n        for item in task.imported_items():\n            if self.ft_in_title(\n                item,\n                drop_feat,\n                keep_in_artist_field,\n                preserve_album_artist,\n                custom_words,\n            ):\n                item.store()\n\n    def update_metadata(\n        self,\n        item: Item,\n        feat_part: str,\n        drop_feat: bool,\n        keep_in_artist_field: bool,\n        custom_words: list[str],\n    ) -> None:\n        \"\"\"Choose how to add new artists to the title and set the new\n        metadata. Also, print out messages about any changes that are made.\n        If `drop_feat` is set, then do not add the artist to the title; just\n        remove it from the artist field.\n        \"\"\"\n        # In case the artist is kept, do not update the artist fields.\n        if keep_in_artist_field:\n            self._log.info(\n                \"artist: {.artist} (Not changing due to keep_in_artist)\", item\n            )\n        else:\n            track_artist, _ = split_on_feat(\n                item.artist, custom_words=custom_words\n            )\n            self._log.info(\"artist: {0.artist} -> {1}\", item, track_artist)\n            item.artist = track_artist\n\n        if item.artist_sort:\n            # Just strip the featured artist from the sort name.\n            item.artist_sort, _ = split_on_feat(\n                item.artist_sort, custom_words=custom_words\n            )\n\n        # Only update the title if it does not already contain a featured\n        # artist and if we do not drop featuring information.\n        if not drop_feat and not contains_feat(item.title, custom_words):\n            feat_format = self.config[\"format\"].as_str()\n            formatted = feat_format.format(feat_part)\n            new_title = self.insert_ft_into_title(\n                item.title, formatted, self.bracket_keywords\n            )\n            self._log.info(\"title: {.title} -> {}\", item, new_title)\n            item.title = new_title\n\n    def ft_in_title(\n        self,\n        item: Item,\n        drop_feat: bool,\n        keep_in_artist_field: bool,\n        preserve_album_artist: bool,\n        custom_words: list[str],\n    ) -> bool:\n        \"\"\"Look for featured artists in the item's artist fields and move\n        them to the title.\n\n        Returns:\n            True if the item has been modified. False otherwise.\n        \"\"\"\n        artist = item.artist.strip()\n        albumartist = item.albumartist.strip()\n\n        # Check whether there is a featured artist on this track and the\n        # artist field does not exactly match the album artist field. In\n        # that case, we attempt to move the featured artist to the title.\n        if preserve_album_artist and albumartist and artist == albumartist:\n            return False\n\n        _, featured = split_on_feat(artist, custom_words=custom_words)\n        if not featured:\n            return False\n\n        self._log.info(\"{.filepath}\", item)\n\n        # Attempt to find the featured artist.\n        feat_part = find_feat_part(artist, albumartist, custom_words)\n\n        if not feat_part:\n            self._log.info(\"no featuring artists found\")\n            return False\n\n        # If we have a featuring artist, move it to the title.\n        self.update_metadata(\n            item, feat_part, drop_feat, keep_in_artist_field, custom_words\n        )\n        return True\n\n    @staticmethod\n    def find_bracket_position(\n        title: str, keywords: list[str] | None = None\n    ) -> int | None:\n        normalized = (\n            DEFAULT_BRACKET_KEYWORDS if keywords is None else tuple(keywords)\n        )\n        pattern = FtInTitlePlugin._bracket_position_pattern(normalized)\n        m: re.Match[str] | None = pattern.search(title)\n        return m.start() if m else None\n\n    @classmethod\n    def insert_ft_into_title(\n        cls, title: str, feat_part: str, keywords: list[str] | None = None\n    ) -> str:\n        \"\"\"Insert featured artist before the first bracket containing\n        remix/edit keywords if present.\n        \"\"\"\n        normalized = (\n            DEFAULT_BRACKET_KEYWORDS if keywords is None else tuple(keywords)\n        )\n        pattern = cls._bracket_position_pattern(normalized)\n        parts = pattern.split(title, maxsplit=1)\n        return f\" {feat_part} \".join(parts).strip()\n"
  },
  {
    "path": "beetsplug/fuzzy.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Philippe Mongeau.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Provides a fuzzy matching query.\"\"\"\n\nimport difflib\n\nfrom beets import config\nfrom beets.dbcore.query import StringFieldQuery\nfrom beets.plugins import BeetsPlugin\n\n\nclass FuzzyQuery(StringFieldQuery[str]):\n    def __init__(self, field_name: str, pattern: str, *_) -> None:\n        # Fuzzy matching is only available via `string_match`.\n        super().__init__(field_name, pattern, fast=False)\n\n    @classmethod\n    def string_match(cls, pattern: str, val: str) -> bool:\n        # smartcase\n        if pattern.islower():\n            val = val.lower()\n        query_matcher = difflib.SequenceMatcher(None, pattern, val)\n        threshold = config[\"fuzzy\"][\"threshold\"].as_number()\n        # Adjust match threshold for the case that the pattern is shorter\n        # than the value being matched. This allows the pattern to match\n        # substrings of the value, not just the entire value.\n        if len(pattern) < len(val):\n            max_possible_ratio = 2 * len(pattern) / (len(pattern) + len(val))\n            threshold *= max_possible_ratio\n\n        # If upper bound of the ratio meets threshold, then calculate\n        # the actual ratio.\n        if query_matcher.quick_ratio() >= threshold:\n            return query_matcher.ratio() >= threshold\n\n        return False\n\n\nclass FuzzyPlugin(BeetsPlugin):\n    def __init__(self) -> None:\n        super().__init__()\n        self.config.add(\n            {\n                \"prefix\": \"~\",\n                \"threshold\": 0.7,\n            }\n        )\n\n    def queries(self):\n        prefix = self.config[\"prefix\"].as_str()\n        return {prefix: FuzzyQuery}\n"
  },
  {
    "path": "beetsplug/hook.py",
    "content": "# This file is part of beets.\n# Copyright 2015, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Allows custom commands to be run when an event is emitted by beets\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport shlex\nimport string\nimport subprocess\nfrom typing import Any\n\nfrom beets.plugins import BeetsPlugin\n\n\nclass BytesToStrFormatter(string.Formatter):\n    \"\"\"A variant of `string.Formatter` that converts `bytes` to `str`.\"\"\"\n\n    def convert_field(self, value: Any, conversion: str | None) -> Any:\n        \"\"\"Converts the provided value given a conversion type.\n\n        This method decodes the converted value using the formatter's coding.\n        \"\"\"\n        converted = super().convert_field(value, conversion)\n\n        if isinstance(converted, bytes):\n            return os.fsdecode(converted)\n\n        return converted\n\n\nclass HookPlugin(BeetsPlugin):\n    \"\"\"Allows custom commands to be run when an event is emitted by beets\"\"\"\n\n    def __init__(self):\n        super().__init__()\n\n        self.config.add({\"hooks\": []})\n\n        hooks = self.config[\"hooks\"].get(list)\n\n        for hook_index in range(len(hooks)):\n            hook = self.config[\"hooks\"][hook_index]\n\n            hook_event = hook[\"event\"].as_str()\n            hook_command = hook[\"command\"].as_str()\n\n            self.create_and_register_hook(hook_event, hook_command)\n\n    def create_and_register_hook(self, event, command):\n        def hook_function(**kwargs):\n            if command is None or len(command) == 0:\n                self._log.error('invalid command \"{}\"', command)\n                return\n\n            # For backwards compatibility, use a string formatter that decodes\n            # bytes (in particular, paths) to strings.\n            formatter = BytesToStrFormatter()\n            command_pieces = [\n                formatter.format(piece, event=event, **kwargs)\n                for piece in shlex.split(command)\n            ]\n\n            self._log.debug(\n                'running command \"{}\" for event {}',\n                \" \".join(command_pieces),\n                event,\n            )\n\n            try:\n                subprocess.check_call(command_pieces)\n            except subprocess.CalledProcessError as exc:\n                self._log.error(\n                    \"hook for {} exited with status {.returncode}\", event, exc\n                )\n            except OSError as exc:\n                self._log.error(\"hook for {} failed: {}\", event, exc)\n\n        self.register_listener(event, hook_function)\n"
  },
  {
    "path": "beetsplug/ihate.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Blemjhoo Tezoulbr <baobab@heresiarch.info>.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\n\"\"\"Warns you about things you hate (or even blocks import).\"\"\"\n\nfrom beets.importer import Action\nfrom beets.library import Album, Item, parse_query_string\nfrom beets.plugins import BeetsPlugin\n\n__author__ = \"baobab@heresiarch.info\"\n__version__ = \"2.0\"\n\n\ndef summary(task):\n    \"\"\"Given an ImportTask, produce a short string identifying the\n    object.\n    \"\"\"\n    if task.is_album:\n        return f\"{task.cur_artist} - {task.cur_album}\"\n    else:\n        return f\"{task.item.artist} - {task.item.title}\"\n\n\nclass IHatePlugin(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n        self.register_listener(\n            \"import_task_choice\", self.import_task_choice_event\n        )\n        self.config.add(\n            {\n                \"warn\": [],\n                \"skip\": [],\n            }\n        )\n\n    @classmethod\n    def do_i_hate_this(cls, task, action_patterns):\n        \"\"\"Process group of patterns (warn or skip) and returns True if\n        task is hated and not whitelisted.\n        \"\"\"\n        if action_patterns:\n            for query_string in action_patterns:\n                query, _ = parse_query_string(\n                    query_string,\n                    Album if task.is_album else Item,\n                )\n                if any(query.match(item) for item in task.imported_items()):\n                    return True\n        return False\n\n    def import_task_choice_event(self, session, task):\n        skip_queries = self.config[\"skip\"].as_str_seq()\n        warn_queries = self.config[\"warn\"].as_str_seq()\n\n        if task.choice_flag == Action.APPLY:\n            if skip_queries or warn_queries:\n                self._log.debug(\"processing your hate\")\n                if self.do_i_hate_this(task, skip_queries):\n                    task.choice_flag = Action.SKIP\n                    self._log.info(\"skipped: {}\", summary(task))\n                    return\n                if self.do_i_hate_this(task, warn_queries):\n                    self._log.info(\"you may hate this: {}\", summary(task))\n            else:\n                self._log.debug(\"nothing to do\")\n        else:\n            self._log.debug(\"user made a decision, nothing to do\")\n"
  },
  {
    "path": "beetsplug/importadded.py",
    "content": "\"\"\"Populate an item's `added` and `mtime` fields by using the file\nmodification time (mtime) of the item's source file before import.\n\nReimported albums and items are skipped.\n\"\"\"\n\nimport os\n\nfrom beets import importer, util\nfrom beets.plugins import BeetsPlugin\n\n\nclass ImportAddedPlugin(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n        self.config.add(\n            {\n                \"preserve_mtimes\": False,\n                \"preserve_write_mtimes\": False,\n            }\n        )\n\n        # item.id for new items that were reimported\n        self.reimported_item_ids = None\n        # album.path for old albums that were replaced by a reimported album\n        self.replaced_album_paths = None\n        # item path in the library to the mtime of the source file\n        self.item_mtime = {}\n\n        register = self.register_listener\n        register(\"import_task_created\", self.check_config)\n        register(\"import_task_created\", self.record_if_inplace)\n        register(\"import_task_files\", self.record_reimported)\n        register(\"before_item_moved\", self.record_import_mtime)\n        register(\"item_copied\", self.record_import_mtime)\n        register(\"item_linked\", self.record_import_mtime)\n        register(\"item_hardlinked\", self.record_import_mtime)\n        register(\"item_reflinked\", self.record_import_mtime)\n        register(\"album_imported\", self.update_album_times)\n        register(\"item_imported\", self.update_item_times)\n        register(\"after_write\", self.update_after_write_time)\n\n    def check_config(self, task, session):\n        self.config[\"preserve_mtimes\"].get(bool)\n\n    def reimported_item(self, item):\n        return item.id in self.reimported_item_ids\n\n    def reimported_album(self, album):\n        return album.path in self.replaced_album_paths\n\n    def record_if_inplace(self, task, session):\n        if not (\n            session.config[\"copy\"]\n            or session.config[\"move\"]\n            or session.config[\"link\"]\n            or session.config[\"hardlink\"]\n            or session.config[\"reflink\"]\n        ):\n            self._log.debug(\n                \"In place import detected, recording mtimes from source paths\"\n            )\n            items = (\n                [task.item]\n                if isinstance(task, importer.SingletonImportTask)\n                else task.items\n            )\n            for item in items:\n                self.record_import_mtime(item, item.path, item.path)\n\n    def record_reimported(self, task, session):\n        self.reimported_item_ids = {\n            item.id\n            for item, replaced_items in task.replaced_items.items()\n            if replaced_items\n        }\n        self.replaced_album_paths = set(task.replaced_albums.keys())\n\n    def write_file_mtime(self, path, mtime):\n        \"\"\"Write the given mtime to the destination path.\"\"\"\n        stat = os.stat(util.syspath(path))\n        os.utime(util.syspath(path), (stat.st_atime, mtime))\n\n    def write_item_mtime(self, item, mtime):\n        \"\"\"Write the given mtime to an item's `mtime` field and to the mtime\n        of the item's file.\n        \"\"\"\n        # The file's mtime on disk must be in sync with the item's mtime\n        self.write_file_mtime(util.syspath(item.path), mtime)\n        item.mtime = mtime\n\n    def record_import_mtime(self, item, source, destination):\n        \"\"\"Record the file mtime of an item's path before its import.\"\"\"\n        mtime = os.stat(util.syspath(source)).st_mtime\n        self.item_mtime[destination] = mtime\n        self._log.debug(\n            \"Recorded mtime {} for item '{}' imported from '{}'\",\n            mtime,\n            util.displayable_path(destination),\n            util.displayable_path(source),\n        )\n\n    def update_album_times(self, lib, album):\n        if self.reimported_album(album):\n            self._log.debug(\n                \"Album '{.filepath}' is reimported, skipping import of \"\n                \"added dates for the album and its items.\",\n                album,\n            )\n            return\n\n        album_mtimes = []\n        for item in album.items():\n            mtime = self.item_mtime.pop(item.path, None)\n            if mtime:\n                album_mtimes.append(mtime)\n                if self.config[\"preserve_mtimes\"].get(bool):\n                    self.write_item_mtime(item, mtime)\n                    item.store()\n        album.added = min(album_mtimes)\n        self._log.debug(\n            \"Import of album '{0.album}', selected album.added={0.added} \"\n            \"from item file mtimes.\",\n            album,\n        )\n        album.store()\n\n    def update_item_times(self, lib, item):\n        if self.reimported_item(item):\n            self._log.debug(\n                \"Item '{.filepath}' is reimported, skipping import of added date.\",\n                item,\n            )\n            return\n        mtime = self.item_mtime.pop(item.path, None)\n        if mtime:\n            item.added = mtime\n            if self.config[\"preserve_mtimes\"].get(bool):\n                self.write_item_mtime(item, mtime)\n            self._log.debug(\n                \"Import of item '{0.filepath}', selected item.added={0.added}\",\n                item,\n            )\n            item.store()\n\n    def update_after_write_time(self, item, path):\n        \"\"\"Update the mtime of the item's file with the item.added value\n        after each write of the item if `preserve_write_mtimes` is enabled.\n        \"\"\"\n        if item.added:\n            if self.config[\"preserve_write_mtimes\"].get(bool):\n                self.write_item_mtime(item, item.added)\n            self._log.debug(\n                \"Write of item '{0.filepath}', selected item.added={0.added}\",\n                item,\n            )\n"
  },
  {
    "path": "beetsplug/importfeeds.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Fabrice Laporte.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\n\"\"\"Write paths of imported files in various formats to ease later import in a\nmusic player. Also allow printing the new file locations to stdout in case\none wants to manually add music to a player by its path.\n\"\"\"\n\nimport datetime\nimport os\nimport re\n\nfrom beets import config\nfrom beets.plugins import BeetsPlugin\nfrom beets.util import bytestring_path, link, mkdirall, normpath, syspath\n\nM3U_DEFAULT_NAME = \"imported.m3u\"\n\n\ndef _build_m3u_session_filename(basename):\n    \"\"\"Builds unique m3u filename by putting current date between given\n    basename and file ending.\"\"\"\n    date = datetime.datetime.now().strftime(\"%Y%m%d_%Hh%M\")\n    basename = re.sub(r\"(\\.m3u|\\.M3U)\", \"\", basename)\n    path = normpath(\n        os.path.join(\n            config[\"importfeeds\"][\"dir\"].as_filename(), f\"{basename}_{date}.m3u\"\n        )\n    )\n    return path\n\n\ndef _build_m3u_filename(basename):\n    \"\"\"Builds unique m3u filename by appending given basename to current\n    date.\"\"\"\n    basename = re.sub(r\"[\\s,/\\\\'\\\"]\", \"_\", basename)\n    date = datetime.datetime.now().strftime(\"%Y%m%d_%Hh%M\")\n    path = normpath(\n        os.path.join(\n            config[\"importfeeds\"][\"dir\"].as_filename(),\n            f\"{date}_{basename}.m3u\",\n        )\n    )\n    return path\n\n\ndef _write_m3u(m3u_path, items_paths):\n    \"\"\"Append relative paths to items into m3u file.\"\"\"\n    mkdirall(m3u_path)\n    with open(syspath(m3u_path), \"ab\") as f:\n        for path in items_paths:\n            f.write(path + b\"\\n\")\n\n\nclass ImportFeedsPlugin(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n\n        self.config.add(\n            {\n                \"formats\": [],\n                \"m3u_name\": \"imported.m3u\",\n                \"dir\": None,\n                \"relative_to\": None,\n                \"absolute_path\": False,\n            }\n        )\n\n        relative_to = self.config[\"relative_to\"].get()\n        if relative_to:\n            self.config[\"relative_to\"] = normpath(relative_to)\n        else:\n            self.config[\"relative_to\"] = self.get_feeds_dir()\n\n        self.register_listener(\"album_imported\", self.album_imported)\n        self.register_listener(\"item_imported\", self.item_imported)\n        self.register_listener(\"import_begin\", self.import_begin)\n\n    def get_feeds_dir(self):\n        feeds_dir = self.config[\"dir\"].get()\n        if feeds_dir:\n            return os.path.expanduser(bytestring_path(feeds_dir))\n        return config[\"directory\"].as_filename()\n\n    def _record_items(self, lib, basename, items):\n        \"\"\"Records relative paths to the given items for each feed format\"\"\"\n        feedsdir = bytestring_path(self.get_feeds_dir())\n        formats = self.config[\"formats\"].as_str_seq()\n        relative_to = self.config[\"relative_to\"].get() or self.get_feeds_dir()\n        relative_to = bytestring_path(relative_to)\n\n        paths = []\n        for item in items:\n            if self.config[\"absolute_path\"]:\n                paths.append(item.path)\n            else:\n                try:\n                    relpath = os.path.relpath(item.path, relative_to)\n                except ValueError:\n                    # On Windows, it is sometimes not possible to construct a\n                    # relative path (if the files are on different disks).\n                    relpath = item.path\n                paths.append(relpath)\n\n        if \"m3u\" in formats:\n            m3u_basename = bytestring_path(self.config[\"m3u_name\"].as_str())\n            m3u_path = os.path.join(feedsdir, m3u_basename)\n            _write_m3u(m3u_path, paths)\n\n        if \"m3u_session\" in formats:\n            m3u_path = os.path.join(feedsdir, self.m3u_session)\n            _write_m3u(m3u_path, paths)\n\n        if \"m3u_multi\" in formats:\n            m3u_path = _build_m3u_filename(basename)\n            _write_m3u(m3u_path, paths)\n\n        if \"link\" in formats:\n            for path in paths:\n                dest = os.path.join(feedsdir, os.path.basename(path))\n                if not os.path.exists(syspath(dest)):\n                    link(path, dest)\n\n        if \"echo\" in formats:\n            self._log.info(\"Location of imported music:\")\n            for path in paths:\n                self._log.info(\"  {}\", path)\n\n    def album_imported(self, lib, album):\n        self._record_items(lib, album.album, album.items())\n\n    def item_imported(self, lib, item):\n        self._record_items(lib, item.title, [item])\n\n    def import_begin(self, session):\n        formats = self.config[\"formats\"].as_str_seq()\n        if \"m3u_session\" in formats:\n            self.m3u_session = _build_m3u_session_filename(\n                self.config[\"m3u_name\"].as_str()\n            )\n"
  },
  {
    "path": "beetsplug/importsource.py",
    "content": "\"\"\"Adds a `source_path` attribute to imported albums indicating from what path\nthe album was imported from. Also suggests removing that source path in case\nyou've removed the album from the library.\n\n\"\"\"\n\nimport os\nfrom pathlib import Path\nfrom shutil import rmtree\n\nfrom beets.dbcore.query import PathQuery\nfrom beets.plugins import BeetsPlugin\nfrom beets.ui import input_options\nfrom beets.util.color import colorize\n\n\nclass ImportSourcePlugin(BeetsPlugin):\n    \"\"\"Main plugin class.\"\"\"\n\n    def __init__(self):\n        \"\"\"Initialize the plugin and read configuration.\"\"\"\n        super().__init__()\n        self.config.add(\n            {\n                \"suggest_removal\": False,\n            }\n        )\n        self.import_stages = [self.import_stage]\n        self.register_listener(\"item_removed\", self.suggest_removal)\n        # In order to stop future removal suggestions for an album we keep\n        # track of `mb_albumid`s in this set.\n        self.stop_suggestions_for_albums = set()\n        # During reimports (import --library) both the import_task_choice and\n        # the item_removed event are triggered. The item_removed event is\n        # triggered first. For the import_task_choice event we prevent removal\n        # suggestions using the existing stop_suggestions_for_album mechanism.\n        self.register_listener(\n            \"import_task_choice\", self.prevent_suggest_removal\n        )\n\n    def prevent_suggest_removal(self, session, task):\n        if task.skip:\n            return\n        for item in task.imported_items():\n            if \"mb_albumid\" in item:\n                self.stop_suggestions_for_albums.add(item.mb_albumid)\n\n    def import_stage(self, _, task):\n        \"\"\"Event handler for albums import finished.\"\"\"\n        for item in task.imported_items():\n            # During reimports (import --library), we prevent overwriting the\n            # source_path attribute with the path from the music library\n            if \"source_path\" in item:\n                self._log.info(\n                    \"Preserving source_path of reimported item {}\", item.id\n                )\n                continue\n            item[\"source_path\"] = item.path\n            item.try_sync(write=False, move=False)\n\n    def suggest_removal(self, item):\n        \"\"\"Prompts the user to delete the original path the item was imported from.\"\"\"\n        if (\n            not self.config[\"suggest_removal\"]\n            or item.mb_albumid in self.stop_suggestions_for_albums\n        ):\n            return\n\n        if \"source_path\" not in item:\n            self._log.warning(\n                \"Item without source_path (probably imported before plugin \"\n                \"usage): {}\",\n                item.filepath,\n            )\n            return\n\n        srcpath = Path(os.fsdecode(item.source_path))\n        if not srcpath.is_file():\n            self._log.warning(\n                \"Original source file no longer exists or is not accessible: {}\",\n                srcpath,\n            )\n            return\n\n        if not (\n            os.access(srcpath, os.W_OK)\n            and os.access(srcpath.parent, os.W_OK | os.X_OK)\n        ):\n            self._log.warning(\n                \"Original source file cannot be deleted (insufficient permissions): {}\",\n                srcpath,\n            )\n            return\n\n        # We ask the user whether they'd like to delete the item's source\n        # directory\n        item_path = colorize(\"text_warning\", item.filepath)\n        source_path = colorize(\"text_warning\", srcpath)\n\n        print(\n            f\"The item:\\n{item_path}\\nis originated from:\\n{source_path}\\n\"\n            \"What would you like to do?\"\n        )\n\n        resp = input_options(\n            [\n                \"Delete the item's source\",\n                \"Recursively delete the source's directory\",\n                \"do Nothing\",\n                \"do nothing and Stop suggesting to delete items from this album\",\n            ],\n            require=True,\n        )\n\n        # Handle user response\n        if resp == \"d\":\n            self._log.info(\n                \"Deleting the item's source file: {}\",\n                srcpath,\n            )\n            srcpath.unlink()\n\n        elif resp == \"r\":\n            self._log.info(\n                \"Searching for other items with a source_path attr containing: {}\",\n                srcpath.parent,\n            )\n\n            source_dir_query = PathQuery(\n                \"source_path\",\n                srcpath.parent,\n                # The \"source_path\" attribute may not be present in all\n                # items of the library, so we avoid errors with this:\n                fast=False,\n            )\n\n            print(\"Doing so will delete the following items' sources as well:\")\n            for searched_item in item._db.items(source_dir_query):\n                print(colorize(\"text_warning\", searched_item.filepath))\n\n            print(\"Would you like to continue?\")\n            continue_resp = input_options(\n                [\"Yes\", \"delete None\", \"delete just the File\"],\n                require=False,  # Yes is the a default\n            )\n\n            if continue_resp == \"y\":\n                self._log.info(\n                    \"Deleting the item's source directory: {}\",\n                    srcpath.parent,\n                )\n                rmtree(srcpath.parent)\n\n            elif continue_resp == \"n\":\n                self._log.info(\"doing nothing - aborting hook function\")\n                return\n\n            elif continue_resp == \"f\":\n                self._log.info(\n                    \"removing just the item's original source: {}\",\n                    srcpath,\n                )\n                srcpath.unlink()\n\n        elif resp == \"s\":\n            self.stop_suggestions_for_albums.add(item.mb_albumid)\n\n        else:\n            self._log.info(\"Doing nothing\")\n"
  },
  {
    "path": "beetsplug/info.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Shows file metadata.\"\"\"\n\nimport os\n\nimport mediafile\n\nfrom beets import ui\nfrom beets.library import Item\nfrom beets.plugins import BeetsPlugin\nfrom beets.util import displayable_path, normpath, syspath\n\n\ndef tag_data(lib, args, album=False):\n    query = []\n    for arg in args:\n        path = normpath(arg)\n        if os.path.isfile(syspath(path)):\n            yield tag_data_emitter(path)\n        else:\n            query.append(arg)\n\n    if query:\n        for item in lib.items(query):\n            yield tag_data_emitter(item.path)\n\n\ndef tag_fields():\n    fields = set(mediafile.MediaFile.readable_fields())\n    fields.add(\"art\")\n    return fields\n\n\ndef tag_data_emitter(path):\n    def emitter(included_keys):\n        if included_keys == \"*\":\n            fields = tag_fields()\n        else:\n            fields = included_keys\n        if \"images\" in fields:\n            # We can't serialize the image data.\n            fields.remove(\"images\")\n        mf = mediafile.MediaFile(syspath(path))\n        tags = {}\n        for field in fields:\n            if field == \"art\":\n                tags[field] = mf.art is not None\n            else:\n                tags[field] = getattr(mf, field, None)\n\n        # create a temporary Item to take advantage of __format__\n        item = Item.from_path(syspath(path))\n\n        return tags, item\n\n    return emitter\n\n\ndef library_data(lib, args, album=False):\n    for item in lib.albums(args) if album else lib.items(args):\n        yield library_data_emitter(item)\n\n\ndef library_data_emitter(item):\n    def emitter(included_keys):\n        data = dict(item.formatted(included_keys=included_keys))\n\n        return data, item\n\n    return emitter\n\n\ndef update_summary(summary, tags):\n    for key, value in tags.items():\n        if key not in summary:\n            summary[key] = value\n        elif summary[key] != value:\n            summary[key] = \"[various]\"\n    return summary\n\n\ndef print_data(data, item=None, fmt=None):\n    \"\"\"Print, with optional formatting, the fields of a single element.\n\n    If no format string `fmt` is passed, the entries on `data` are printed one\n    in each line, with the format 'field: value'. If `fmt` is not `None`, the\n    `item` is printed according to `fmt`, using the `Item.__format__`\n    machinery.\n    \"\"\"\n    if fmt:\n        # use fmt specified by the user\n        ui.print_(format(item, fmt))\n        return\n\n    path = displayable_path(item.path) if item else None\n    formatted = {}\n    for key, value in data.items():\n        if isinstance(value, list):\n            formatted[key] = \"; \".join(value)\n        if value is not None:\n            formatted[key] = value\n\n    if len(formatted) == 0:\n        return\n\n    maxwidth = max(len(key) for key in formatted)\n\n    if path:\n        ui.print_(displayable_path(path))\n\n    for field in sorted(formatted):\n        value = formatted[field]\n        if isinstance(value, list):\n            value = \"; \".join(value)\n        ui.print_(f\"{field:>{maxwidth}}: {value}\")\n\n\ndef print_data_keys(data, item=None):\n    \"\"\"Print only the keys (field names) for an item.\"\"\"\n    path = displayable_path(item.path) if item else None\n    formatted = []\n    for key, value in data.items():\n        formatted.append(key)\n\n    if len(formatted) == 0:\n        return\n\n    if path:\n        ui.print_(displayable_path(path))\n\n    for field in sorted(formatted):\n        ui.print_(f\"    {field}\")\n\n\nclass InfoPlugin(BeetsPlugin):\n    def commands(self):\n        cmd = ui.Subcommand(\"info\", help=\"show file metadata\")\n        cmd.func = self.run\n        cmd.parser.add_option(\n            \"-l\",\n            \"--library\",\n            action=\"store_true\",\n            help=\"show library fields instead of tags\",\n        )\n        cmd.parser.add_option(\n            \"-a\",\n            \"--album\",\n            action=\"store_true\",\n            help='show album fields instead of tracks (implies \"--library\")',\n        )\n        cmd.parser.add_option(\n            \"-s\",\n            \"--summarize\",\n            action=\"store_true\",\n            help=\"summarize the tags of all files\",\n        )\n        cmd.parser.add_option(\n            \"-i\",\n            \"--include-keys\",\n            default=[],\n            action=\"append\",\n            dest=\"included_keys\",\n            help=\"comma separated list of keys to show\",\n        )\n        cmd.parser.add_option(\n            \"-k\",\n            \"--keys-only\",\n            action=\"store_true\",\n            help=\"show only the keys\",\n        )\n        cmd.parser.add_format_option(target=\"item\")\n        return [cmd]\n\n    def run(self, lib, opts, args):\n        \"\"\"Print tag info or library data for each file referenced by args.\n\n        Main entry point for the `beet info ARGS...` command.\n\n        If an argument is a path pointing to an existing file, then the tags\n        of that file are printed. All other arguments are considered\n        queries, and for each item matching all those queries the tags from\n        the file are printed.\n\n        If `opts.summarize` is true, the function merges all tags into one\n        dictionary and only prints that. If two files have different values\n        for the same tag, the value is set to '[various]'\n        \"\"\"\n        if opts.library or opts.album:\n            data_collector = library_data\n        else:\n            data_collector = tag_data\n\n        included_keys = []\n        for keys in opts.included_keys:\n            included_keys.extend(keys.split(\",\"))\n        # Drop path even if user provides it multiple times\n        included_keys = [k for k in included_keys if k != \"path\"]\n\n        first = True\n        summary = {}\n        for data_emitter in data_collector(\n            lib,\n            args,\n            album=opts.album,\n        ):\n            try:\n                data, item = data_emitter(included_keys or \"*\")\n            except (mediafile.UnreadableFileError, OSError) as ex:\n                self._log.error(\"cannot read file: {}\", ex)\n                continue\n\n            if opts.summarize:\n                update_summary(summary, data)\n            else:\n                if not first:\n                    ui.print_()\n                if opts.keys_only:\n                    print_data_keys(data, item)\n                else:\n                    fmt = [opts.format][0] if opts.format else None\n                    print_data(data, item, fmt)\n                first = False\n\n        if opts.summarize:\n            print_data(summary)\n"
  },
  {
    "path": "beetsplug/inline.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Allows inline path template customization code in the config file.\"\"\"\n\nimport itertools\nimport traceback\n\nfrom beets import config\nfrom beets.plugins import BeetsPlugin\n\nFUNC_NAME = \"__INLINE_FUNC__\"\n\n\nclass InlineError(Exception):\n    \"\"\"Raised when a runtime error occurs in an inline expression.\"\"\"\n\n    def __init__(self, code, exc):\n        super().__init__(\n            f\"error in inline path field code:\\n{code}\\n{type(exc).__name__}: {exc}\"\n        )\n\n\ndef _compile_func(body):\n    \"\"\"Given Python code for a function body, return a compiled\n    callable that invokes that code.\n    \"\"\"\n    body = body.replace(\"\\n\", \"\\n    \")\n    body = f\"def {FUNC_NAME}():\\n    {body}\"\n    code = compile(body, \"inline\", \"exec\")\n    env = {}\n    eval(code, env)\n    return env[FUNC_NAME]\n\n\nclass InlinePlugin(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n\n        config.add(\n            {\n                \"pathfields\": {},  # Legacy name.\n                \"item_fields\": {},\n                \"album_fields\": {},\n            }\n        )\n\n        # Item fields.\n        for key, view in itertools.chain(\n            config[\"item_fields\"].items(), config[\"pathfields\"].items()\n        ):\n            self._log.debug(\"adding item field {}\", key)\n            func = self.compile_inline(view.as_str(), False, key)\n            if func is not None:\n                self.template_fields[key] = func\n\n        # Album fields.\n        for key, view in config[\"album_fields\"].items():\n            self._log.debug(\"adding album field {}\", key)\n            func = self.compile_inline(view.as_str(), True, key)\n            if func is not None:\n                self.album_template_fields[key] = func\n\n    def compile_inline(self, python_code, album, field_name):\n        \"\"\"Given a Python expression or function body, compile it as a path\n        field function. The returned function takes a single argument, an\n        Item, and returns a Unicode string. If the expression cannot be\n        compiled, then an error is logged and this function returns None.\n        \"\"\"\n        # First, try compiling as a single function.\n        try:\n            code = compile(f\"({python_code})\", \"inline\", \"eval\")\n        except SyntaxError:\n            # Fall back to a function body.\n            try:\n                func = _compile_func(python_code)\n            except SyntaxError:\n                self._log.error(\n                    \"syntax error in inline field definition:\\n{}\",\n                    traceback.format_exc(),\n                )\n                return\n            else:\n                is_expr = False\n        else:\n            is_expr = True\n\n        def _dict_for(obj):\n            out = {}\n            for key in obj.keys(computed=False):\n                if key == field_name:\n                    continue\n                out[key] = obj._get(key)\n\n            if album:\n                out[\"items\"] = list(obj.items())\n            return out\n\n        if is_expr:\n            # For expressions, just evaluate and return the result.\n            def _expr_func(obj):\n                values = _dict_for(obj)\n                try:\n                    return eval(code, values)\n                except Exception as exc:\n                    raise InlineError(python_code, exc)\n\n            return _expr_func\n        else:\n            # For function bodies, invoke the function with values as global\n            # variables.\n            def _func_func(obj):\n                old_globals = dict(func.__globals__)\n                func.__globals__.update(_dict_for(obj))\n                try:\n                    return func()\n                except Exception as exc:\n                    raise InlineError(python_code, exc)\n                finally:\n                    func.__globals__.clear()\n                    func.__globals__.update(old_globals)\n\n            return _func_func\n"
  },
  {
    "path": "beetsplug/ipfs.py",
    "content": "# This file is part of beets.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Adds support for ipfs. Requires go-ipfs and a running ipfs daemon\"\"\"\n\nimport os\nimport shutil\nimport subprocess\nimport tempfile\n\nfrom beets import config, library, ui, util\nfrom beets.plugins import BeetsPlugin\n\n\nclass IPFSPlugin(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n        self.config.add(\n            {\n                \"auto\": True,\n                \"nocopy\": False,\n            }\n        )\n\n        if self.config[\"auto\"]:\n            self.import_stages = [self.auto_add]\n\n    def commands(self):\n        cmd = ui.Subcommand(\"ipfs\", help=\"interact with ipfs\")\n        cmd.parser.add_option(\n            \"-a\", \"--add\", dest=\"add\", action=\"store_true\", help=\"Add to ipfs\"\n        )\n        cmd.parser.add_option(\n            \"-g\", \"--get\", dest=\"get\", action=\"store_true\", help=\"Get from ipfs\"\n        )\n        cmd.parser.add_option(\n            \"-p\",\n            \"--publish\",\n            dest=\"publish\",\n            action=\"store_true\",\n            help=\"Publish local library to ipfs\",\n        )\n        cmd.parser.add_option(\n            \"-i\",\n            \"--import\",\n            dest=\"_import\",\n            action=\"store_true\",\n            help=\"Import remote library from ipfs\",\n        )\n        cmd.parser.add_option(\n            \"-l\",\n            \"--list\",\n            dest=\"_list\",\n            action=\"store_true\",\n            help=\"Query imported libraries\",\n        )\n        cmd.parser.add_option(\n            \"-m\",\n            \"--play\",\n            dest=\"play\",\n            action=\"store_true\",\n            help=\"Play music from remote libraries\",\n        )\n\n        def func(lib, opts, args):\n            if opts.add:\n                for album in lib.albums(args):\n                    if len(album.items()) == 0:\n                        self._log.info(\n                            \"{} does not contain items, aborting\", album\n                        )\n\n                    self.ipfs_add(album)\n                    album.store()\n\n            if opts.get:\n                self.ipfs_get(lib, args)\n\n            if opts.publish:\n                self.ipfs_publish(lib)\n\n            if opts._import:\n                self.ipfs_import(lib, args)\n\n            if opts._list:\n                self.ipfs_list(lib, args)\n\n            if opts.play:\n                self.ipfs_play(lib, opts, args)\n\n        cmd.func = func\n        return [cmd]\n\n    def auto_add(self, session, task):\n        if task.is_album:\n            if self.ipfs_add(task.album):\n                task.album.store()\n\n    def ipfs_play(self, lib, opts, args):\n        from beetsplug.play import PlayPlugin\n\n        jlib = self.get_remote_lib(lib)\n        player = PlayPlugin()\n        config[\"play\"][\"relative_to\"] = None\n        player.album = True\n        player.play_music(jlib, player, args)\n\n    def ipfs_add(self, album):\n        try:\n            album_dir = album.item_dir()\n        except AttributeError:\n            return False\n        try:\n            if album.ipfs:\n                self._log.debug(\"{} already added\", album_dir)\n                # Already added to ipfs\n                return False\n        except AttributeError:\n            pass\n\n        self._log.info(\"Adding {} to ipfs\", album_dir)\n\n        if self.config[\"nocopy\"]:\n            cmd = \"ipfs add --nocopy -q -r\".split()\n        else:\n            cmd = \"ipfs add -q -r\".split()\n        cmd.append(album_dir)\n        try:\n            output = util.command_output(cmd).stdout.split()\n        except (OSError, subprocess.CalledProcessError) as exc:\n            self._log.error(\"Failed to add {}, error: {}\", album_dir, exc)\n            return False\n        length = len(output)\n\n        for linenr, line in enumerate(output):\n            line = line.strip()\n            if linenr == length - 1:\n                # last printed line is the album hash\n                self._log.info(\"album: {}\", line)\n                album.ipfs = line\n            else:\n                try:\n                    item = album.items()[linenr]\n                    self._log.info(\"item: {}\", line)\n                    item.ipfs = line\n                    item.store()\n                except IndexError:\n                    # if there's non music files in the to-add folder they'll\n                    # get ignored here\n                    pass\n\n        return True\n\n    def ipfs_get(self, lib, query):\n        query = query[0]\n        # Check if query is a hash\n        # TODO: generalize to other hashes; probably use a multihash\n        # implementation\n        if query.startswith(\"Qm\") and len(query) == 46:\n            self.ipfs_get_from_hash(lib, query)\n        else:\n            albums = self.query(lib, query)\n            for album in albums:\n                self.ipfs_get_from_hash(lib, album.ipfs)\n\n    def ipfs_get_from_hash(self, lib, _hash):\n        try:\n            cmd = \"ipfs get\".split()\n            cmd.append(_hash)\n            util.command_output(cmd)\n        except (OSError, subprocess.CalledProcessError) as err:\n            self._log.error(\n                \"Failed to get {} from ipfs.\\n{.output}\", _hash, err\n            )\n            return False\n\n        self._log.info(\"Getting {} from ipfs\", _hash)\n        imp = ui.commands.TerminalImportSession(\n            lib, loghandler=None, query=None, paths=[_hash]\n        )\n        imp.run()\n        # This uses a relative path, hence we cannot use util.syspath(_hash,\n        # prefix=True). However, that should be fine since the hash will not\n        # exceed MAX_PATH.\n        shutil.rmtree(util.syspath(_hash, prefix=False))\n\n    def ipfs_publish(self, lib):\n        with tempfile.NamedTemporaryFile() as tmp:\n            self.ipfs_added_albums(lib, tmp.name)\n            try:\n                if self.config[\"nocopy\"]:\n                    cmd = \"ipfs add --nocopy -q \".split()\n                else:\n                    cmd = \"ipfs add -q \".split()\n                cmd.append(tmp.name)\n                output = util.command_output(cmd).stdout\n            except (OSError, subprocess.CalledProcessError) as err:\n                msg = f\"Failed to publish library. Error: {err}\"\n                self._log.error(msg)\n                return False\n            self._log.info(\"hash of library: {}\", output)\n\n    def ipfs_import(self, lib, args):\n        _hash = args[0]\n        if len(args) > 1:\n            lib_name = args[1]\n        else:\n            lib_name = _hash\n        lib_root = os.path.dirname(lib.path)\n        remote_libs = os.path.join(lib_root, b\"remotes\")\n        if not os.path.exists(remote_libs):\n            try:\n                os.makedirs(remote_libs)\n            except OSError as e:\n                msg = f\"Could not create {remote_libs}. Error: {e}\"\n                self._log.error(msg)\n                return False\n        path = os.path.join(remote_libs, lib_name.encode() + b\".db\")\n        if not os.path.exists(path):\n            cmd = f\"ipfs get {_hash} -o\".split()\n            cmd.append(path)\n            try:\n                util.command_output(cmd)\n            except (OSError, subprocess.CalledProcessError):\n                self._log.error(\"Could not import {}\", _hash)\n                return False\n\n        # add all albums from remotes into a combined library\n        jpath = os.path.join(remote_libs, b\"joined.db\")\n        jlib = library.Library(jpath)\n        nlib = library.Library(path)\n        for album in nlib.albums():\n            if not self.already_added(album, jlib):\n                new_album = []\n                for item in album.items():\n                    item.id = None\n                    new_album.append(item)\n                added_album = jlib.add_album(new_album)\n                added_album.ipfs = album.ipfs\n                added_album.store()\n\n    def already_added(self, check, jlib):\n        for jalbum in jlib.albums():\n            if jalbum.mb_albumid == check.mb_albumid:\n                return True\n        return False\n\n    def ipfs_list(self, lib, args):\n        fmt = config[\"format_album\"].get()\n        try:\n            albums = self.query(lib, args)\n        except OSError:\n            ui.print_(\"No imported libraries yet.\")\n            return\n\n        for album in albums:\n            ui.print_(format(album, fmt), \" : \", album.ipfs.decode())\n\n    def query(self, lib, args):\n        rlib = self.get_remote_lib(lib)\n        albums = rlib.albums(args)\n        return albums\n\n    def get_remote_lib(self, lib):\n        lib_root = os.path.dirname(lib.path)\n        remote_libs = os.path.join(lib_root, b\"remotes\")\n        path = os.path.join(remote_libs, b\"joined.db\")\n        if not os.path.isfile(path):\n            raise OSError\n        return library.Library(path)\n\n    def ipfs_added_albums(self, rlib, tmpname):\n        \"\"\"Returns a new library with only albums/items added to ipfs\"\"\"\n        tmplib = library.Library(tmpname)\n        for album in rlib.albums():\n            try:\n                if album.ipfs:\n                    self.create_new_album(album, tmplib)\n            except AttributeError:\n                pass\n        return tmplib\n\n    def create_new_album(self, album, tmplib):\n        items = []\n        for item in album.items():\n            try:\n                if not item.ipfs:\n                    break\n            except AttributeError:\n                pass\n            item_path = os.fsdecode(os.path.basename(item.path))\n            # Clear current path from item\n            item.path = f\"/ipfs/{album.ipfs}/{item_path}\"\n\n            item.id = None\n            items.append(item)\n        if len(items) < 1:\n            return False\n        self._log.info(\"Adding '{}' to temporary library\", album)\n        new_album = tmplib.add_album(items)\n        new_album.ipfs = album.ipfs\n        new_album.store(inherit=False)\n"
  },
  {
    "path": "beetsplug/keyfinder.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Thomas Scholtes.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Uses the `KeyFinder` program to add the `initial_key` field.\"\"\"\n\nimport os.path\nimport subprocess\n\nfrom beets import ui, util\nfrom beets.plugins import BeetsPlugin\n\n\nclass KeyFinderPlugin(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n        self.config.add(\n            {\n                \"bin\": \"KeyFinder\",\n                \"auto\": True,\n                \"overwrite\": False,\n            }\n        )\n\n        if self.config[\"auto\"].get(bool):\n            self.import_stages = [self.imported]\n\n    def commands(self):\n        cmd = ui.Subcommand(\n            \"keyfinder\", help=\"detect and add initial key from audio\"\n        )\n        cmd.func = self.command\n        return [cmd]\n\n    def command(self, lib, opts, args):\n        self.find_key(lib.items(args), write=ui.should_write())\n\n    def imported(self, session, task):\n        self.find_key(task.imported_items())\n\n    def find_key(self, items, write=False):\n        overwrite = self.config[\"overwrite\"].get(bool)\n        command = [self.config[\"bin\"].as_str()]\n        # The KeyFinder GUI program needs the -f flag before the path.\n        # keyfinder-cli is similar, but just wants the path with no flag.\n        if \"keyfinder-cli\" not in os.path.basename(command[0]).lower():\n            command.append(\"-f\")\n\n        for item in items:\n            if item[\"initial_key\"] and not overwrite:\n                continue\n\n            try:\n                output = util.command_output(\n                    [*command, util.syspath(item.path)]\n                ).stdout\n            except (subprocess.CalledProcessError, OSError) as exc:\n                self._log.error(\"execution failed: {}\", exc)\n                continue\n\n            try:\n                key_raw = output.rsplit(None, 1)[-1]\n            except IndexError:\n                # Sometimes keyfinder-cli returns 0 but with no key, usually\n                # when the file is silent or corrupt, so we log and skip.\n                self._log.error(\"no key returned for path: {.path}\", item)\n                continue\n\n            try:\n                key = key_raw.decode(\"utf-8\")\n            except UnicodeDecodeError:\n                self._log.error(\"output is invalid UTF-8\")\n                continue\n\n            item[\"initial_key\"] = key\n            self._log.info(\n                \"added computed initial key {} for {.filepath}\", key, item\n            )\n\n            if write:\n                item.try_write()\n            item.store()\n"
  },
  {
    "path": "beetsplug/kodiupdate.py",
    "content": "# This file is part of beets.\n# Copyright 2017, Pauli Kettunen.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Updates a Kodi library whenever the beets library is changed.\nThis is based on the Plex Update plugin.\n\nPut something like the following in your config.yaml to configure:\n    kodi:\n        host: localhost\n        port: 8080\n        user: user\n        pwd: secret\n\"\"\"\n\nimport requests\n\nfrom beets.plugins import BeetsPlugin\n\n\ndef update_kodi(host, port, user, password):\n    \"\"\"Sends request to the Kodi api to start a library refresh.\"\"\"\n    url = f\"http://{host}:{port}/jsonrpc\"\n\n    \"\"\"Content-Type: application/json is mandatory\n    according to the kodi jsonrpc documentation\"\"\"\n\n    headers = {\"Content-Type\": \"application/json\"}\n\n    # Create the payload. Id seems to be mandatory.\n    payload = {\"jsonrpc\": \"2.0\", \"method\": \"AudioLibrary.Scan\", \"id\": 1}\n    r = requests.post(\n        url,\n        auth=(user, password),\n        json=payload,\n        headers=headers,\n        timeout=10,\n    )\n\n    return r\n\n\nclass KodiUpdate(BeetsPlugin):\n    def __init__(self):\n        super().__init__(\"kodi\")\n\n        # Adding defaults.\n        self.config.add(\n            [{\"host\": \"localhost\", \"port\": 8080, \"user\": \"kodi\", \"pwd\": \"kodi\"}]\n        )\n\n        self.config[\"user\"].redact = True\n        self.config[\"pwd\"].redact = True\n        self.register_listener(\"database_change\", self.listen_for_db_change)\n\n    def listen_for_db_change(self, lib, model):\n        \"\"\"Listens for beets db change and register the update\"\"\"\n        self.register_listener(\"cli_exit\", self.update)\n\n    def update(self, lib):\n        \"\"\"When the client exists try to send refresh request to Kodi server.\"\"\"\n        self._log.info(\"Requesting a Kodi library update...\")\n\n        kodi = self.config.get()\n\n        # Backwards compatibility in case not configured as an array\n        if not isinstance(kodi, list):\n            kodi = [kodi]\n\n        for instance in kodi:\n            # Try to send update request.\n            try:\n                r = update_kodi(\n                    instance[\"host\"],\n                    instance[\"port\"],\n                    instance[\"user\"],\n                    instance[\"pwd\"],\n                )\n                r.raise_for_status()\n\n                json = r.json()\n                if json.get(\"result\") != \"OK\":\n                    self._log.warning(\n                        \"Kodi update failed: JSON response was {0!r}\", json\n                    )\n                    continue\n\n                self._log.info(\n                    \"Kodi update triggered for {}:{}\",\n                    instance[\"host\"],\n                    instance[\"port\"],\n                )\n            except requests.exceptions.RequestException as e:\n                self._log.warning(\"Kodi update failed: {}\", str(e))\n                continue\n"
  },
  {
    "path": "beetsplug/lastgenre/__init__.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\n\"\"\"Gets genres for imported music based on Last.fm tags.\n\nUses a provided whitelist file to determine which tags are valid genres.\nThe included (default) genre list was originally produced by scraping Wikipedia\nand has been edited to remove some questionable entries.\nThe scraper script used is available here:\nhttps://gist.github.com/1241307\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom functools import singledispatchmethod\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any\n\nimport yaml\n\nfrom beets import config, library, plugins, ui\nfrom beets.library import Album, Item\nfrom beets.util import plurality, unique_list\n\nfrom .client import LastFmClient\n\nif TYPE_CHECKING:\n    import optparse\n    from collections.abc import Iterable\n\n    from beets.importer import ImportSession, ImportTask\n    from beets.library import LibModel\n\n    Whitelist = set[str]\n    \"\"\"Set of valid genre names (lowercase). Empty set means all genres allowed.\"\"\"\n\n    CanonTree = list[list[str]]\n    \"\"\"Genre hierarchy as list of paths from general to specific.\n    Example: [['electronic', 'house'], ['electronic', 'techno']]\"\"\"\n\n\n# Canonicalization tree processing.\n\n\ndef flatten_tree(\n    elem: dict[Any, Any] | list[Any] | str,\n    path: list[str],\n    branches: CanonTree,\n) -> None:\n    \"\"\"Flatten nested lists/dictionaries into lists of strings\n    (branches).\n    \"\"\"\n    if not path:\n        path = []\n\n    if isinstance(elem, dict):\n        for k, v in elem.items():\n            flatten_tree(v, [*path, k], branches)\n    elif isinstance(elem, list):\n        for sub in elem:\n            flatten_tree(sub, path, branches)\n    else:\n        branches.append([*path, str(elem)])\n\n\ndef find_parents(candidate: str, branches: CanonTree) -> list[str]:\n    \"\"\"Find parents genre of a given genre, ordered from the closest to\n    the further parent.\n    \"\"\"\n    for branch in branches:\n        try:\n            idx = branch.index(candidate.lower())\n            return list(reversed(branch[: idx + 1]))\n        except ValueError:\n            continue\n    return [candidate]\n\n\ndef get_depth(tag: str, branches: CanonTree) -> int | None:\n    \"\"\"Find the depth of a tag in the genres tree.\"\"\"\n    for branch in branches:\n        if tag in branch:\n            return branch.index(tag)\n    return None\n\n\ndef sort_by_depth(tags: list[str], branches: CanonTree) -> list[str]:\n    \"\"\"Given a list of tags, sort the tags by their depths in the genre tree.\"\"\"\n    depth_tag_pairs = [(get_depth(t, branches), t) for t in tags]\n    depth_tag_pairs = [e for e in depth_tag_pairs if e[0] is not None]\n    depth_tag_pairs.sort(reverse=True)\n    return [p[1] for p in depth_tag_pairs]\n\n\n# Main plugin logic.\n\nWHITELIST = os.path.join(os.path.dirname(__file__), \"genres.txt\")\nC14N_TREE = os.path.join(os.path.dirname(__file__), \"genres-tree.yaml\")\n\n\nclass LastGenrePlugin(plugins.BeetsPlugin):\n    def __init__(self) -> None:\n        super().__init__()\n\n        self.config.add(\n            {\n                \"whitelist\": True,\n                \"min_weight\": 10,\n                \"count\": 1,\n                \"fallback\": None,\n                \"canonical\": False,\n                \"cleanup_existing\": False,\n                \"source\": \"album\",\n                \"force\": False,\n                \"keep_existing\": False,\n                \"auto\": True,\n                \"prefer_specific\": False,\n                \"title_case\": True,\n                \"pretend\": False,\n            }\n        )\n        self.setup()\n\n    def setup(self) -> None:\n        \"\"\"Setup plugin from config options\"\"\"\n        if self.config[\"auto\"]:\n            self.import_stages = [self.imported]\n\n        self.client = LastFmClient(\n            self._log, self.config[\"min_weight\"].get(int)\n        )\n        self.whitelist: Whitelist = self._load_whitelist()\n        self.c14n_branches: CanonTree\n        self.c14n_branches, self.canonicalize = self._load_c14n_tree()\n\n    def _load_whitelist(self) -> Whitelist:\n        \"\"\"Load the whitelist from a text file.\n\n        Default whitelist is used if config is True, empty string or set to \"nothing\".\n        \"\"\"\n        whitelist = set()\n        wl_filename = self.config[\"whitelist\"].get()\n        if wl_filename in (True, \"\", None):  # Indicates the default whitelist.\n            wl_filename = WHITELIST\n        if wl_filename:\n            self._log.debug(\"Loading whitelist {}\", wl_filename)\n            text = Path(wl_filename).expanduser().read_text(encoding=\"utf-8\")\n            for line in text.splitlines():\n                if (line := line.strip().lower()) and not line.startswith(\"#\"):\n                    whitelist.add(line)\n\n        return whitelist\n\n    def _load_c14n_tree(self) -> tuple[CanonTree, bool]:\n        \"\"\"Load the canonicalization tree from a YAML file.\n\n        Default tree is used if config is True, empty string, set to \"nothing\"\n        or if prefer_specific is enabled.\n        \"\"\"\n        c14n_branches: CanonTree = []\n        c14n_filename = self.config[\"canonical\"].get()\n        canonicalize = c14n_filename is not False\n        # Default tree\n        if c14n_filename in (True, \"\", None) or (\n            # prefer_specific requires a tree, load default tree\n            not canonicalize and self.config[\"prefer_specific\"].get()\n        ):\n            c14n_filename = C14N_TREE\n        # Read the tree\n        if c14n_filename:\n            self._log.debug(\"Loading canonicalization tree {}\", c14n_filename)\n            with Path(c14n_filename).expanduser().open(encoding=\"utf-8\") as f:\n                genres_tree = yaml.safe_load(f)\n            flatten_tree(genres_tree, [], c14n_branches)\n        return c14n_branches, canonicalize\n\n    @property\n    def sources(self) -> tuple[str, ...]:\n        \"\"\"A tuple of allowed genre sources. May contain 'track',\n        'album', or 'artist.'\n        \"\"\"\n        return self.config[\"source\"].as_choice(\n            {\n                \"track\": (\"track\", \"album\", \"artist\"),\n                \"album\": (\"album\", \"artist\"),\n                \"artist\": (\"artist\",),\n            }\n        )\n\n    # Genre list processing.\n\n    def _resolve_genres(self, tags: list[str]) -> list[str]:\n        \"\"\"Canonicalize, sort and filter a list of genres.\n\n        - Returns an empty list if the input tags list is empty.\n        - If canonicalization is enabled, it extends the list by incorporating\n          parent genres from the canonicalization tree. When a whitelist is set,\n          only parent tags that pass the whitelist filter are included;\n          otherwise, it adds the oldest ancestor. Adding parent tags is stopped\n          when the count of tags reaches the configured limit (count).\n        - The tags list is then deduplicated to ensure only unique genres are\n          retained.\n        - If the 'prefer_specific' configuration is enabled, the list is sorted\n          by the specificity (depth in the canonicalization tree) of the genres.\n        - Finally applies whitelist filtering to ensure that only valid\n          genres are kept. (This may result in no genres at all being retained).\n        - Returns the filtered list of genres, limited to the configured count.\n        \"\"\"\n        if not tags:\n            return []\n\n        count = self.config[\"count\"].get(int)\n\n        # Canonicalization (if enabled)\n        if self.canonicalize:\n            # Extend the list to consider tags parents in the c14n tree\n            tags_all = []\n            for tag in tags:\n                # Add parents that are in the whitelist, or add the oldest\n                # ancestor if no whitelist\n                if self.whitelist:\n                    parents = self._filter_valid(\n                        find_parents(tag, self.c14n_branches)\n                    )\n                else:\n                    parents = [find_parents(tag, self.c14n_branches)[-1]]\n\n                tags_all += parents\n                # Stop if we have enough tags already, unless we need to find\n                # the most specific tag (instead of the most popular).\n                if (\n                    not self.config[\"prefer_specific\"]\n                    and len(tags_all) >= count\n                ):\n                    break\n            tags = tags_all\n\n        tags = unique_list(tags)\n\n        # Sort the tags by specificity.\n        if self.config[\"prefer_specific\"]:\n            tags = sort_by_depth(tags, self.c14n_branches)\n\n        # c14n only adds allowed genres but we may have had forbidden genres in\n        # the original tags list\n        valid_tags = self._filter_valid(tags)\n        return valid_tags[:count]\n\n    def _filter_valid(self, genres: Iterable[str]) -> list[str]:\n        \"\"\"Filter genres based on whitelist.\n\n        Returns all genres if no whitelist is configured, otherwise returns\n        only genres that are in the whitelist.\n        \"\"\"\n        # First, drop any falsy or whitespace-only genre strings to avoid\n        # retaining empty tags from multi-valued fields.\n        cleaned = [g for g in genres if g and g.strip()]\n        if not self.whitelist:\n            return cleaned\n\n        return [g for g in cleaned if g.lower() in self.whitelist]\n\n    # Genre resolution pipeline.\n\n    def _format_genres(self, tags: list[str]) -> list[str]:\n        \"\"\"Format to title case if configured.\"\"\"\n        if self.config[\"title_case\"]:\n            return [tag.title() for tag in tags]\n        else:\n            return tags\n\n    def _get_existing_genres(self, obj: LibModel) -> list[str]:\n        \"\"\"Return a list of genres for this Item or Album.\"\"\"\n        if isinstance(obj, library.Item):\n            genres_list = obj.get(\"genres\", with_album=False)\n        else:\n            genres_list = obj.get(\"genres\")\n\n        return genres_list\n\n    def _combine_resolve_and_log(\n        self, old: list[str], new: list[str]\n    ) -> list[str]:\n        \"\"\"Combine old and new genres and process via _resolve_genres.\"\"\"\n        self._log.debug(\"raw last.fm tags: {}\", new)\n        self._log.debug(\"existing genres taken into account: {}\", old)\n        combined = old + new\n        return self._resolve_genres(combined)\n\n    def _get_genre(self, obj: LibModel) -> tuple[list[str], str]:\n        \"\"\"Get the final genre list for an Album or Item object.\n\n        `self.sources` specifies allowed genre sources. Starting with the first\n        source in this tuple, the following stages run through until a genre is\n        found or no options are left:\n            - track (for Items only)\n            - album\n            - artist, albumartist or \"most popular track genre\" (for VA-albums)\n            - original fallback\n            - configured fallback\n            - empty list\n\n        A `(genres, label)` pair is returned, where `label` is a string used for\n        logging. For example, \"keep + artist, whitelist\" indicates that existing\n        genres were combined with new last.fm genres and whitelist filtering was\n        applied, while \"artist, any\" means only new last.fm genres are included\n        and the whitelist feature was disabled.\n        \"\"\"\n\n        def _try_resolve_stage(\n            stage_label: str, keep_genres: list[str], new_genres: list[str]\n        ) -> tuple[list[str], str] | None:\n            \"\"\"Try to resolve genres for a given stage and log the result.\"\"\"\n            resolved_genres = self._combine_resolve_and_log(\n                keep_genres, new_genres\n            )\n            if resolved_genres:\n                suffix = \"whitelist\" if self.whitelist else \"any\"\n                label = f\"{stage_label}, {suffix}\"\n                if keep_genres:\n                    label = f\"keep + {label}\"\n                return self._format_genres(resolved_genres), label\n            return None\n\n        keep_genres = []\n        new_genres = []\n        genres = self._get_existing_genres(obj)\n\n        if genres and not self.config[\"force\"]:\n            # Without force, but cleanup_existing enabled, we attempt\n            # to canonicalize pre-populated tags before returning them.\n            # If none are found, we use the fallback (if set).\n            if self.config[\"cleanup_existing\"]:\n                keep_genres = [g.lower() for g in genres]\n                if result := _try_resolve_stage(\"cleanup\", keep_genres, []):\n                    return result\n\n                # Return fallback string (None if not set).\n                return self.config[\"fallback\"].get(), \"fallback\"\n\n            # If cleanup_existing is not set, the pre-populated tags are\n            # returned as-is.\n            return genres, \"keep any, no-force\"\n\n        if self.config[\"force\"]:\n            # Force doesn't keep any unless keep_existing is set.\n            # Whitelist validation is handled in _resolve_genres.\n            if self.config[\"keep_existing\"]:\n                keep_genres = [g.lower() for g in genres]\n\n        # Run through stages: track, album, artist,\n        # album artist, or most popular track genre.\n        if isinstance(obj, library.Item) and \"track\" in self.sources:\n            if new_genres := self.client.fetch_track_genre(\n                obj.artist, obj.title\n            ):\n                if result := _try_resolve_stage(\n                    \"track\", keep_genres, new_genres\n                ):\n                    return result\n\n        if \"album\" in self.sources:\n            if new_genres := self.client.fetch_album_genre(\n                obj.albumartist, obj.album\n            ):\n                if result := _try_resolve_stage(\n                    \"album\", keep_genres, new_genres\n                ):\n                    return result\n\n        if \"artist\" in self.sources:\n            new_genres = []\n            if isinstance(obj, library.Item):\n                new_genres = self.client.fetch_artist_genre(obj.artist)\n                stage_label = \"artist\"\n            elif obj.albumartist != config[\"va_name\"].as_str():\n                new_genres = self.client.fetch_artist_genre(obj.albumartist)\n                stage_label = \"album artist\"\n                if not new_genres:\n                    self._log.extra_debug(\n                        'No album artist genre found for \"{}\", '\n                        \"trying multi-valued field...\",\n                        obj.albumartist,\n                    )\n                    for albumartist in obj.albumartists:\n                        self._log.extra_debug(\n                            'Fetching artist genre for \"{}\"',\n                            albumartist,\n                        )\n                        new_genres += self.client.fetch_artist_genre(\n                            albumartist\n                        )\n                    if new_genres:\n                        stage_label = \"multi-valued album artist\"\n            else:\n                # For \"Various Artists\", pick the most popular track genre.\n                item_genres = []\n                assert isinstance(obj, Album)  # Type narrowing for mypy\n                for item in obj.items():\n                    item_genre = None\n                    if \"track\" in self.sources:\n                        item_genre = self.client.fetch_track_genre(\n                            item.artist, item.title\n                        )\n                    if not item_genre:\n                        item_genre = self.client.fetch_artist_genre(item.artist)\n                    if item_genre:\n                        item_genres += item_genre\n                if item_genres:\n                    most_popular, rank = plurality(item_genres)\n                    new_genres = [most_popular]\n                    stage_label = \"most popular track\"\n                    self._log.debug(\n                        'Most popular track genre \"{}\" ({}) for VA album.',\n                        most_popular,\n                        rank,\n                    )\n\n            if new_genres:\n                if result := _try_resolve_stage(\n                    stage_label, keep_genres, new_genres\n                ):\n                    return result\n\n        # Nothing found, leave original if configured and valid.\n        if genres and self.config[\"keep_existing\"]:\n            if valid_genres := self._filter_valid(genres):\n                return valid_genres, \"original fallback\"\n            # If the original genre doesn't match a whitelisted genre, check\n            # if we can canonicalize it to find a matching, whitelisted genre!\n            if result := _try_resolve_stage(\n                \"original fallback\", keep_genres, []\n            ):\n                return result\n\n        # Return fallback as a list.\n        if fallback := self.config[\"fallback\"].get():\n            return [fallback], \"fallback\"\n\n        # No fallback configured.\n        return [], \"fallback unconfigured\"\n\n    # Beets plugin hooks and CLI.\n\n    def _fetch_and_log_genre(self, obj: LibModel) -> None:\n        \"\"\"Fetch genre and log it.\"\"\"\n        self._log.info(str(obj))\n        obj.genres, label = self._get_genre(obj)\n        self._log.debug(\"Resolved ({}): {}\", label, obj.genres)\n\n        ui.show_model_changes(obj, fields=[\"genres\"], print_obj=False)\n\n    @singledispatchmethod\n    def _process(self, obj: LibModel, write: bool) -> None:\n        \"\"\"Process an object, dispatching to the appropriate method.\"\"\"\n        raise NotImplementedError\n\n    @_process.register\n    def _process_track(self, obj: Item, write: bool) -> None:\n        \"\"\"Process a single track/item.\"\"\"\n        self._fetch_and_log_genre(obj)\n        if not self.config[\"pretend\"]:\n            obj.try_sync(write=write, move=False)\n\n    @_process.register\n    def _process_album(self, obj: Album, write: bool) -> None:\n        \"\"\"Process an entire album.\"\"\"\n        self._fetch_and_log_genre(obj)\n        if \"track\" in self.sources:\n            for item in obj.items():\n                self._process(item, write)\n\n        if not self.config[\"pretend\"]:\n            obj.try_sync(\n                write=write, move=False, inherit=\"track\" not in self.sources\n            )\n\n    def commands(self) -> list[ui.Subcommand]:\n        lastgenre_cmd = ui.Subcommand(\"lastgenre\", help=\"fetch genres\")\n        lastgenre_cmd.parser.add_option(\n            \"-p\",\n            \"--pretend\",\n            action=\"store_true\",\n            help=\"show actions but do nothing\",\n        )\n        lastgenre_cmd.parser.add_option(\n            \"-f\",\n            \"--force\",\n            dest=\"force\",\n            action=\"store_true\",\n            help=\"modify existing genres\",\n        )\n        lastgenre_cmd.parser.add_option(\n            \"-F\",\n            \"--no-force\",\n            dest=\"force\",\n            action=\"store_false\",\n            help=\"don't modify existing genres\",\n        )\n        lastgenre_cmd.parser.add_option(\n            \"-k\",\n            \"--keep-existing\",\n            dest=\"keep_existing\",\n            action=\"store_true\",\n            help=\"combine with existing genres when modifying\",\n        )\n        lastgenre_cmd.parser.add_option(\n            \"-K\",\n            \"--no-keep-existing\",\n            dest=\"keep_existing\",\n            action=\"store_false\",\n            help=\"don't combine with existing genres when modifying\",\n        )\n        lastgenre_cmd.parser.add_option(\n            \"-s\",\n            \"--source\",\n            dest=\"source\",\n            type=\"string\",\n            help=\"genre source: artist, album, or track\",\n        )\n        lastgenre_cmd.parser.add_option(\n            \"-A\",\n            \"--items\",\n            action=\"store_false\",\n            dest=\"album\",\n            help=\"match items instead of albums\",\n        )\n        lastgenre_cmd.parser.add_option(\n            \"-a\",\n            \"--albums\",\n            action=\"store_true\",\n            dest=\"album\",\n            help=\"match albums instead of items (default)\",\n        )\n        lastgenre_cmd.parser.set_defaults(album=True)\n\n        def lastgenre_func(\n            lib: library.Library, opts: optparse.Values, args: list[str]\n        ) -> None:\n            self.config.set_args(opts)\n\n            method = lib.albums if opts.album else lib.items\n            for obj in method(args):\n                self._process(obj, write=ui.should_write())\n\n        lastgenre_cmd.func = lastgenre_func\n        return [lastgenre_cmd]\n\n    def imported(self, _: ImportSession, task: ImportTask) -> None:\n        self._process(task.album if task.is_album else task.item, write=False)  # type: ignore[attr-defined]\n"
  },
  {
    "path": "beetsplug/lastgenre/client.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n# Copyright 2026, J0J0 Todos.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\n\"\"\"Last.fm API client for genre lookups.\"\"\"\n\nfrom __future__ import annotations\n\nimport traceback\nfrom typing import TYPE_CHECKING, Any\n\nimport pylast\n\nfrom beets import plugins\n\nif TYPE_CHECKING:\n    from collections.abc import Callable\n\n    from beets.logging import BeetsLogger\n\n    GenreCache = dict[str, list[str]]\n    \"\"\"Cache mapping entity keys to their genre lists.\n    Keys are formatted as 'entity.arg1-arg2-...' (e.g., 'album.artist-title').\n    Values are lists of lowercase genre strings.\"\"\"\n\n\nLASTFM = pylast.LastFMNetwork(api_key=plugins.LASTFM_KEY)\n\nPYLAST_EXCEPTIONS = (\n    pylast.WSError,\n    pylast.MalformedResponseError,\n    pylast.NetworkError,\n)\n\n\nclass LastFmClient:\n    \"\"\"Client for fetching genres from Last.fm.\"\"\"\n\n    def __init__(self, log: BeetsLogger, min_weight: int):\n        \"\"\"Initialize the client.\n\n        The min_weight parameter filters tags by their minimum weight.\n        \"\"\"\n        self._log = log\n        self._min_weight = min_weight\n        self._genre_cache: GenreCache = {}\n\n    def fetch_genre(\n        self, lastfm_obj: pylast.Album | pylast.Artist | pylast.Track\n    ) -> list[str]:\n        \"\"\"Return genres for a pylast entity. Returns an empty list if\n        no suitable genres are found.\n        \"\"\"\n        return self._tags_for(lastfm_obj, self._min_weight)\n\n    def _tags_for(\n        self,\n        obj: pylast.Album | pylast.Artist | pylast.Track,\n        min_weight: int | None = None,\n    ) -> list[str]:\n        \"\"\"Core genre identification routine.\n\n        Given a pylast entity (album or track), return a list of\n        tag names for that entity. Return an empty list if the entity is\n        not found or another error occurs.\n\n        If `min_weight` is specified, tags are filtered by weight.\n        \"\"\"\n        # Work around an inconsistency in pylast where\n        # Album.get_top_tags() does not return TopItem instances.\n        # https://github.com/pylast/pylast/issues/86\n        obj_to_query: Any = obj\n        if isinstance(obj, pylast.Album):\n            obj_to_query = super(pylast.Album, obj)\n\n        try:\n            res: Any = obj_to_query.get_top_tags()\n        except PYLAST_EXCEPTIONS as exc:\n            self._log.debug(\"last.fm error: {}\", exc)\n            return []\n        except Exception as exc:\n            # Isolate bugs in pylast.\n            self._log.debug(\"{}\", traceback.format_exc())\n            self._log.error(\"error in pylast library: {}\", exc)\n            return []\n\n        # Filter by weight (optionally).\n        if min_weight:\n            res = [el for el in res if (int(el.weight or 0)) >= min_weight]\n\n        # Get strings from tags.\n        tags: list[str] = [el.item.get_name().lower() for el in res]\n\n        return tags\n\n    def _last_lookup(\n        self, entity: str, method: Callable[..., Any], *args: str\n    ) -> list[str]:\n        \"\"\"Get genres based on the named entity using the callable `method`\n        whose arguments are given in the sequence `args`. The genre lookup\n        is cached based on the entity name and the arguments.\n\n        Before the lookup, each argument has the \"-\" Unicode character replaced\n        with its rough ASCII equivalents in order to return better results from\n        the Last.fm database.\n        \"\"\"\n        # Shortcut if we're missing metadata.\n        if any(not s for s in args):\n            return []\n\n        key = f\"{entity}.{'-'.join(str(a) for a in args)}\"\n        if key not in self._genre_cache:\n            args_replaced = [a.replace(\"\\u2010\", \"-\") for a in args]\n            self._genre_cache[key] = self.fetch_genre(method(*args_replaced))\n\n        genre = self._genre_cache[key]\n        self._log.extra_debug(\"last.fm (unfiltered) {} tags: {}\", entity, genre)\n        return genre\n\n    def fetch_album_genre(self, albumartist: str, albumtitle: str) -> list[str]:\n        \"\"\"Return genres from Last.fm for the album by albumartist.\"\"\"\n        return self._last_lookup(\n            \"album\", LASTFM.get_album, albumartist, albumtitle\n        )\n\n    def fetch_artist_genre(self, artist: str) -> list[str]:\n        \"\"\"Return genres from Last.fm for the artist.\"\"\"\n        return self._last_lookup(\"artist\", LASTFM.get_artist, artist)\n\n    def fetch_track_genre(self, trackartist: str, tracktitle: str) -> list[str]:\n        \"\"\"Return genres from Last.fm for the track by artist.\"\"\"\n        return self._last_lookup(\n            \"track\", LASTFM.get_track, trackartist, tracktitle\n        )\n"
  },
  {
    "path": "beetsplug/lastgenre/genres-tree.yaml",
    "content": "- african:\n    - african heavy metal\n    - african hip hop\n    - afrobeat\n    - apala\n    - benga\n    - bikutsi\n    - bongo flava\n    - cape jazz\n    - chimurenga\n    - coupé-décalé\n    - egyptian\n    - fuji music\n    - genge\n    - highlife\n    - hiplife\n    - isicathamiya\n    - jit\n    - jùjú\n    - kapuka\n    - kizomba\n    - kuduro\n    - kwaito\n    - kwela\n    - makossa\n    - maloya\n    - marrabenta\n    - mbalax\n    - mbaqanga\n    - mbube\n    - morna\n    - museve\n    - palm-wine\n    - raï\n    - sakara\n    - sega\n    - seggae\n    - semba\n    - shangaan electro\n    - soukous\n    - taarab\n    - zouglou\n- asian:\n    - east asian:\n        - anison\n        - c-pop\n        - cantopop\n        - enka\n        - hong kong english pop\n        - j-pop\n        - k-pop\n        - kayōkyoku\n        - korean pop\n        - mandopop\n        - onkyokei\n        - taiwanese pop\n    - fann at-tanbura\n    - fijiri\n    - khaliji\n    - liwa\n    - sawt\n    - south and southeast asian:\n        - baila\n        - bhangra\n        - bhojpuri\n        - dangdut\n        - filmi\n        - indian pop\n        - lavani\n        - luk thung:\n            - luk krung\n        - manila sound\n        - morlam\n        - pinoy pop\n        - pop sunda\n        - ragini\n        - thai pop\n- avant-garde:\n    - experimental music\n    - lo-fi\n    - musique concrète\n- blues:\n    - african blues\n    - blues rock\n    - blues shouter\n    - british blues\n    - canadian blues\n    - chicago blues\n    - classic female blues\n    - contemporary r&b\n    - country blues\n    - delta blues\n    - detroit blues\n    - electric blues\n    - gospel blues\n    - hill country blues\n    - hokum blues\n    - jazz blues\n    - jump blues\n    - kansas city blues\n    - louisiana blues\n    - memphis blues\n    - piano blues\n    - piedmont blues\n    - punk blues\n    - soul blues\n    - st. louis blues\n    - swamp blues\n    - texas blues\n    - west coast blues\n- caribbean and latin american:\n    - bachata\n    - baithak gana\n    - bolero\n    - brazilian:\n        - axé\n        - bossa nova\n        - brazilian rock\n        - brega\n        - choro\n        - forró\n        - frevo\n        - funk carioca\n        - lambada\n        - maracatu\n        - música popular brasileira\n        - música sertaneja\n        - pagode\n        - samba\n        - samba rock\n        - tecnobrega\n        - tropicalia\n        - zouk-lambada\n    - calypso\n    - chutney\n    - chutney soca\n    - compas\n    - folklore argentino\n    - mambo\n    - merengue\n    - méringue\n    - other latin:\n        - chicha\n        - criolla\n        - cumbia\n        - huayno\n        - mariachi\n        - ranchera\n        - tejano\n    - punta\n    - punta rock\n    - rasin\n    - reggaeton\n    - salsa\n    - soca\n    - son\n    - timba\n    - twoubadou\n    - zouk\n- classical:\n    - ballet\n    - baroque:\n        - baroque music\n    - cantata\n    - chamber music:\n        - string quartet\n    - classical music\n    - concerto:\n        - concerto grosso\n    - contemporary classical\n    - modern classical\n    - opera\n    - oratorio\n    - orchestra:\n        - orchestral\n        - symphonic\n        - symphony\n    - organum\n    - mass:\n        - requiem\n    - sacred music:\n        - cantique\n        - gregorian chant\n    - sonata\n- comedy:\n    - comedy music\n    - comedy rock\n    - humor\n    - parody music\n    - stand-up\n    - kabarett\n- country:\n    - alternative country:\n        - cowpunk\n    - americana\n    - australian country music\n    - bakersfield sound\n    - bluegrass:\n        - progressive bluegrass\n        - reactionary bluegrass\n    - blues country\n    - cajun:\n        - cajun fiddle tunes\n    - christian country music\n    - classic country\n    - close harmony\n    - country pop\n    - country rap\n    - country rock\n    - country soul\n    - cowboy/western music\n    - dansband music\n    - franco-country\n    - gulf and western\n    - hellbilly music\n    - hokum\n    - honky tonk\n    - instrumental country\n    - lubbock sound\n    - nashville sound\n    - neotraditional country\n    - outlaw country\n    - progressive country\n    - psychobilly/punkabilly\n    - red dirt\n    - rockabilly\n    - sertanejo\n    - texas country\n    - traditional country music\n    - truck-driving country\n    - western swing\n    - zydeco\n- easy listening:\n    - background music\n    - beautiful music\n    - elevator music\n    - furniture music\n    - lounge music\n    - middle of the road\n    - new-age music\n- electronic:\n    - ambient:\n        - ambient dub\n        - ambient house\n        - ambient techno\n        - dark ambient\n        - drone music\n        - illbient\n        - isolationism\n        - lowercase\n    - asian underground\n    - breakbeat:\n        - 4-beat\n        - acid breaks\n        - baltimore club\n        - big beat\n        - broken beat\n        - florida breaks\n        - nu skool breaks\n    - chiptune:\n        - bitpop\n        - game boy music\n        - nintendocore\n        - video game music\n        - yorkshire bleeps and bass\n    - disco:\n        - cosmic disco\n        - disco polo\n            - euro disco\n        - italo disco\n        - nu-disco\n        - space disco\n    - downtempo:\n        - acid jazz\n        - balearic beat\n        - chill out\n        - dub music\n        - dubtronica\n        - ethnic electronica\n        - moombahton\n        - nu jazz\n        - trip hop\n    - drum and bass:\n        - darkcore\n        - darkstep\n        - drumfunk\n        - drumstep\n        - hardstep\n        - intelligent drum and bass\n        - jump-up\n        - liquid funk\n        - neurofunk\n        - jungle:\n            - darkside jungle\n            - ragga jungle\n            - oldschool jungle\n        - raggacore\n        - sambass\n        - techstep\n        - leftfield\n        - halftime\n    - electro:\n        - crunk\n        - electro backbeat\n        - electro-grime\n        - electropop\n    - electroacoustic:\n        - acousmatic music\n        - computer music\n        - electroacoustic improvisation\n        - field recording\n        - live coding\n        - live electronics\n        - soundscape composition\n        - tape music\n    - electronic rock:\n        - alternative dance:\n            - baggy\n            - madchester\n        - dance-punk\n        - dance-rock\n        - dark wave\n        - electroclash\n        - electronicore\n        - electropunk\n        - ethereal wave\n        - indietronica\n        - new rave\n        - space rock\n        - synthpop\n        - synthpunk\n    - electronica:\n        - berlin school\n        - chillwave\n        - electronic art music\n        - electronic dance music\n        - folktronica\n        - freestyle music\n        - glitch\n        - idm\n        - laptronica\n        - skweee\n        - sound art\n        - synthcore\n    - eurodance:\n        - bubblegum dance\n        - italo dance\n        - turbofolk\n    - hardcore:\n        - bouncy house\n        - bouncy techno\n        - breakbeat hardcore\n        - breakcore\n        - digital hardcore\n        - doomcore\n        - dubstyle\n        - gabber\n        - happy hardcore\n        - hardstyle\n        - jumpstyle\n        - makina\n        - speedcore\n        - terrorcore\n        - uk hardcore\n    - hi-nrg:\n        - eurobeat\n        - hard nrg\n        - new beat\n    - house:\n        - acid house\n        - chicago house\n        - deep house\n        - diva house\n        - dutch house\n        - electro house\n        - freestyle house\n        - french house\n        - funky house\n        - ghetto house\n        - hardbag\n        - hip house\n        - italo house\n        - latin house\n        - minimal house\n        - progressive house\n        - rave music\n        - swing house\n        - tech house\n        - tribal house\n        - uk hard house\n        - us garage\n        - vocal house\n    - industrial:\n        - aggrotech\n        - coldwave\n        - cybergrind\n        - dark electro\n        - death industrial\n        - electro-industrial\n        - electronic body music:\n            - futurepop\n        - industrial metal:\n            - neue deutsche härte\n        - industrial rock\n        - noise:\n            - japanoise\n            - power electronics\n            - power noise\n        - witch house\n    - juke:\n        - footwork\n    - post-disco:\n        - boogie\n        - dance-pop\n    - progressive:\n        - progressive house/trance:\n            - disco house\n            - dream house\n            - space house\n        - progressive breaks\n        - progressive drum & bass\n        - progressive techno\n    - techno:\n        - acid techno\n        - detroit techno\n        - dub techno\n        - free tekno\n        - ghettotech\n        - minimal\n        - nortec\n        - schranz\n        - techno-dnb\n        - technopop\n        - tecno brega\n        - toytown techno\n    - trance:\n        - acid trance\n        - classic trance\n        - dream trance\n        - goa trance:\n            - dark psytrance\n            - full on\n            - psybreaks\n            - psyprog\n            - suomisaundi\n        - hard trance\n        - tech trance\n        - uplifting trance:\n            - orchestral uplifting\n        - vocal trance\n    - uk garage:\n        - 2-step\n        - 4x4\n        - bassline\n        - breakstep\n        - dubstep\n        - funky\n        - grime\n        - speed garage\n        - trap\n- folk:\n    - american folk revival\n    - anti-folk\n    - british folk revival\n    - celtic music\n    - contemporary folk\n    - filk music\n    - freak folk\n    - indie folk\n    - industrial folk\n    - neofolk\n    - progressive folk\n    - psychedelic folk\n    - sung poetry\n    - techno-folk\n- hip hop:\n    - alternative hip hop\n    - avant-garde hip hop\n    - chap hop\n    - christian hip hop\n    - conscious hip hop\n    - crunkcore\n    - cumbia rap\n    - east coast hip hop:\n        - brick city club\n        - hardcore hip hop\n        - mafioso rap\n        - new jersey hip hop\n    - electro music\n    - freestyle rap\n    - g-funk\n    - gangsta rap\n    - glitch hop\n    - golden age hip hop\n    - hip hop soul\n    - hip pop\n    - hyphy\n    - industrial hip hop\n    - instrumental hip hop\n    - jazz rap\n    - low bap\n    - lyrical hip hop\n    - merenrap\n    - midwest hip hop:\n        - chicago hip hop\n        - detroit hip hop\n        - horrorcore\n        - st. louis hip hop\n        - twin cities hip hop\n    - motswako\n    - nerdcore\n    - new jack swing\n    - new school hip hop\n    - old school hip hop\n    - political hip hop\n    - rap opera\n    - rap rock:\n        - rap metal\n        - rapcore\n    - songo-salsa\n    - southern hip hop:\n        - atlanta hip hop:\n            - snap music\n        - bounce music\n        - houston hip hop:\n            - chopped and screwed\n        - miami bass\n    - turntablism\n    - underground hip hop\n    - urban pasifika\n    - west coast hip hop:\n        - chicano rap\n        - jerkin'\n    - austrian hip hop\n    - german hip hop\n- jazz:\n    - asian american jazz\n    - avant-garde jazz\n    - bebop\n    - boogie-woogie\n    - brass band\n    - british dance band\n    - chamber jazz\n    - continental jazz\n    - cool jazz\n    - crossover jazz\n    - cubop\n    - dixieland\n    - ethno jazz\n    - european free jazz\n    - free funk\n    - free improvisation\n    - free jazz\n    - gypsy jazz\n    - hard bop\n    - jazz fusion\n    - jazz rock\n    - jazz-funk\n    - kansas city jazz\n    - latin jazz\n    - livetronica\n    - m-base\n    - mainstream jazz\n    - modal jazz\n    - neo-bop jazz\n    - neo-swing\n    - novelty ragtime\n    - orchestral jazz\n    - post-bop\n    - punk jazz\n    - ragtime\n    - shibuya-kei\n    - ska jazz\n    - smooth jazz\n    - soul jazz\n    - straight-ahead jazz\n    - stride jazz\n    - swing\n    - third stream\n    - trad jazz\n    - vocal jazz\n    - west coast gypsy jazz\n    - west coast jazz\n- kids music:\n    - kinderlieder\n- pop:\n    - adult contemporary\n    - arab pop\n    - baroque pop\n    - bubblegum pop\n    - christian pop\n    - classical crossover\n    - europop:\n        - austropop\n        - balkan pop\n        - french pop\n        - latin pop\n        - laïkó\n        - nederpop\n        - russian pop\n    - iranian pop\n    - jangle pop\n    - latin ballad\n    - levenslied\n    - louisiana swamp pop\n    - mexican pop\n    - motorpop\n    - new romanticism\n    - pop rap\n    - popera\n    - psychedelic pop\n    - schlager\n    - soft rock\n    - sophisti-pop\n    - space age pop\n    - sunshine pop\n    - surf pop\n    - teen pop\n    - traditional pop music\n    - turkish pop\n    - vispop\n    - wonky pop\n- rhythm and blues:\n    - funk:\n        - deep funk\n        - go-go\n        - p-funk\n    - soul:\n        - blue-eyed soul\n        - neo soul\n        - northern soul\n- rock:\n    - alternative rock:\n        - britpop:\n            - post-britpop\n        - dream pop\n        - grunge:\n            - post-grunge\n        - indie pop:\n            - dunedin sound\n            - twee pop\n        - indie rock\n        - noise pop\n        - nu metal\n        - post-punk revival\n        - post-rock:\n            - post-metal\n        - sadcore\n        - shoegaze\n        - slowcore\n    - art rock\n    - beat music\n    - chinese rock\n    - christian rock\n    - classic rock\n    - dark cabaret\n    - desert rock\n    - experimental rock\n    - folk rock\n    - garage rock\n    - glam rock\n    - hard rock\n    - heavy metal:\n        - alternative metal:\n            - funk metal\n        - black metal:\n            - viking metal\n        - christian metal\n        - death metal:\n            - death/doom\n            - goregrind\n            - melodic death metal\n            - technical death metal\n        - doom metal:\n            - epic doom metal\n            - funeral doom\n        - drone metal\n        - epic metal\n        - folk metal:\n            - celtic metal\n            - medieval metal\n            - pagan metal\n        - funk metal\n        - glam metal\n        - gothic metal\n        - industrial metal:\n            - industrial death metal\n        - metalcore:\n            - deathcore\n            - mathcore:\n                - djent\n            - synthcore\n        - neoclassical metal\n        - post-metal\n        - power metal:\n            - progressive power metal\n        - progressive metal\n        - sludge metal\n        - speed metal\n        - stoner rock:\n            - stoner metal\n        - symphonic metal\n        - thrash metal:\n            - crossover thrash\n            - groove metal\n            - progressive thrash metal\n            - teutonic thrash metal\n        - traditional heavy metal\n    - math rock\n    - new wave:\n        - world fusion\n    - paisley underground\n    - pop rock\n    - post-punk:\n        - gothic rock\n        - no wave\n        - noise rock\n    - power pop\n    - progressive rock:\n        - canterbury scene\n        - krautrock\n        - new prog\n        - rock in opposition\n    - psychedelic rock:\n        - acid rock\n        - freakbeat\n        - neo-psychedelia\n        - raga rock\n    - punk rock:\n        - anarcho punk:\n            - crust punk:\n                - d-beat\n        - art punk\n        - christian punk\n        - deathrock\n        - deutschpunk\n        - folk punk:\n            - celtic punk\n            - gypsy punk\n        - garage punk\n        - grindcore:\n            - crustgrind\n            - noisegrind\n        - hardcore punk:\n            - post-hardcore:\n                - emo:\n                    - screamo\n            - powerviolence\n            - street punk\n            - thrashcore\n        - horror punk\n        - oi!\n        - pop punk\n        - psychobilly\n        - riot grrrl\n        - ska punk:\n            - ska-core\n        - skate punk\n    - rock and roll\n    - southern rock\n    - sufi rock\n    - surf rock\n    - visual kei:\n        - nagoya kei\n- reggae:\n    - roots reggae\n    - reggae fusion\n    - reggae en español:\n        - spanish reggae\n        - reggae 110\n        - reggae bultrón\n        - romantic flow\n    - lovers rock\n    - raggamuffin:\n        - ragga\n    - dancehall\n    - ska:\n        - 2 tone\n        - rocksteady\n    - dub\n- soundtrack:\n- singer-songwriter:\n    - cantautorato\n    - cantautor\n    - cantautora\n    - chanson\n    - canción de autor\n    - nueva canción\n- world:\n    - world dub\n    - world fusion\n    - worldbeat\n\n"
  },
  {
    "path": "beetsplug/lastgenre/genres.txt",
    "content": "2 tone\n2-step garage\n4-beat\n4x4 garage\n8-bit\nacapella\nacid\nacid breaks\nacid house\nacid jazz\nacid rock\nacoustic music\nacousticana\nadult contemporary music\nafrican popular music\nafrican rumba\nafrobeat\naleatoric music\nalternative country\nalternative dance\nalternative hip hop\nalternative metal\nalternative rock\nambient\nambient house\nambient music\namericana\nanarcho punk\nanti-folk\napala\nape haters\narab pop\narabesque\narabic pop\nargentine rock\nars antiqua\nars nova\nart punk\nart rock\nashiq\nasian american jazz\naustralian country music\naustralian hip hop\naustralian pub rock\naustropop\navant-garde\navant-garde jazz\navant-garde metal\navant-garde music\naxé\nbac-bal\nbachata\nbaggy\nbaila\nbaile funk\nbaisha xiyue\nbaithak gana\nbaião\nbajourou\nbakersfield sound\nbakou\nbakshy\nbal-musette\nbalakadri\nbalinese gamelan\nbalkan pop\nballad\nballata\nballet\nbamboo band\nbambuco\nbanda\nbangsawan\nbantowbol\nbarbershop music\nbarndance\nbaroque\nbaroque music\nbaroque pop\nbass music\nbatcave\nbatucada\nbatuco\nbatá-rumba\nbeach music\nbeat\nbeatboxing\nbeautiful music\nbebop\nbeiguan\nbel canto\nbend-skin\nbenga\nberlin school of electronic music\nbhajan\nbhangra\nbhangra-wine\nbhangragga\nbhangramuffin\nbig band\nbig band music\nbig beat\nbiguine\nbihu\nbikutsi\nbiomusic\nbitcore\nbitpop\nblack metal\nblackened death metal\nblue-eyed soul\nbluegrass\nblues\nblues ballad\nblues-rock\nboogie\nboogie woogie\nboogie-woogie\nbossa nova\nbrass band\nbrazilian funk\nbrazilian jazz\nbreakbeat\nbreakbeat hardcore\nbreakcore\nbreton music\nbrill building pop\nbritfunk\nbritish blues\nbritish invasion\nbritpop\nbroken beat\nbrown-eyed soul\nbrukdown\nbrutal death metal\nbubblegum dance\nbubblegum pop\nbulerias\nbumba-meu-boi\nbunraku\nburger-highlife\nburgundian school\nbyzantine chant\nca din tulnic\nca pe lunca\nca trù\ncabaret\ncadence\ncadence rampa\ncadence-lypso\ncafé-aman\ncai luong\ncajun music\ncakewalk\ncalenda\ncalentanos\ncalgia\ncalypso\ncalypso jazz\ncalypso-style baila\ncampursari\ncanatronic\ncanción de autor\ncandombe\ncanon\ncanrock\ncantata\ncantautorato\ncantautor\ncantautora\ncante chico\ncante jondo\ncanterbury scene\ncantiga\ncantique\ncantiñas\ncanto livre\ncanto nuevo\ncanto popular\ncantopop\ncanzone napoletana\ncape jazz\ncapoeira music\ncaracoles\ncarceleras\ncardas\ncardiowave\ncarimbó\ncariso\ncarnatic music\ncarol\ncartageneras\ncassette culture\ncasséy-co\ncavacha\ncaveman\ncaña\ncelempungan\ncello rock\nceltic\nceltic fusion\nceltic metal\nceltic punk\nceltic reggae\nceltic rock\ncha-cha-cha\nchakacha\nchalga\nchamamé\nchamber jazz\nchamber music\nchamber pop\nchampeta\nchanguí\nchanson\nchant\ncharanga\ncharanga-vallenata\ncharikawi\nchastushki\nchau van\nchemical breaks\nchicago blues\nchicago house\nchicago soul\nchicano rap\nchicha\nchicken scratch\nchildren's music\nchillout\nchillwave\nchimurenga\nchinese music\nchinese pop\nchinese rock\nchip music\ncho-kantrum\nchongak\nchopera\nchorinho\nchoro\nchouval bwa\nchowtal\nchristian alternative\nchristian black metal\nchristian electronic music\nchristian hardcore\nchristian hip hop\nchristian industrial\nchristian metal\nchristian music\nchristian punk\nchristian r&b\nchristian rock\nchristian ska\nchristmas carol\nchristmas music\nchumba\nchut-kai-pang\nchutney\nchutney soca\nchutney-bhangra\nchutney-hip hop\nchutney-soca\nchylandyk\nchzalni\nchèo\ncigányzene\nclassic\nclassic country\nclassic female blues\nclassic rock\nclassical\nclassical music\nclassical music era\nclicks n cuts\nclose harmony\nclub music\ncocobale\ncoimbra fado\ncoladeira\ncolombianas\ncombined rhythm\ncomedy\ncomedy rap\ncomedy rock\ncomic opera\ncomparsa\ncompas direct\ncompas meringue\nconcert overture\nconcerto\nconcerto grosso\ncongo\nconjunto\ncontemporary christian\ncontemporary christian music\ncontemporary classical\ncontemporary r&b\ncontonbley\ncontradanza\ncool jazz\ncorrido\ncorsican polyphonic song\ncothoza mfana\ncountry\ncountry blues\ncountry gospel\ncountry music\ncountry pop\ncountry r&b\ncountry rock\ncountry-rap\ncountrypolitan\ncouple de sonneurs\ncoupé-décalé\ncowpunk\ncretan music\ncrossover jazz\ncrossover music\ncrossover thrash\ncrossover thrash metal\ncrunk\ncrunk&b\ncrunkcore\ncrust punk\ncsárdás\ncuarteto\ncuban rumba\ncuddlecore\ncueca\ncumbia\ncumbia villera\ncybergrind\ndabka\ndadra\ndaina\ndalauna\ndance\ndance music\ndance-pop\ndance-punk\ndance-rock\ndancehall\ndangdut\ndanger music\ndansband\ndanza\ndanzón\ndark ambient\ndark cabaret\ndark pop\ndarkcore\ndarkstep\ndarkwave\nde ascultat la servici\nde codru\nde dragoste\nde jale\nde pahar\ndeath industrial\ndeath metal\ndeath rock\ndeath/doom\ndeathcore\ndeathgrind\ndeathrock\ndeep funk\ndeep house\ndeep soul\ndegung\ndelta blues\ndementia\ndesert rock\ndesi\ndetroit blues\ndetroit techno\ndub techno\ndhamar\ndhimotiká\ndhrupad\ndhun\ndigital hardcore\ndirge\ndirty dutch\ndirty rap\ndirty rap/pornocore\ndirty south\ndisco\ndisco house\ndisco polo\ndisney\ndisney hardcore\ndisney pop\ndiva house\ndivine rock\ndixieland\ndixieland jazz\ndjambadon\ndjent\ndodompa\ndoina\ndombola\ndondang sayang\ndonegal fiddle tradition\ndongjing\ndoo wop\ndoom metal\ndoomcore\ndowntempo\ndrag\ndream pop\ndrone doom\ndrone metal\ndrone music\ndronology\ndrum and bass\ndub\ndub house\ndubanguthu\ndubstep\ndubtronica\ndunedin sound\ndunun\ndutch jazz\ndécima\nearly music\neast coast blues\neast coast hip hop\neasy listening\nelectric blues\nelectric folk\nelectro\nelectro backbeat\nelectro hop\nelectro house\nelectro punk\nelectro-industrial\nelectro-swing\nelectroclash\nelectrofunk\nelectronic\nelectronic art music\nelectronic body music\nelectronic dance\nelectronic luk thung\nelectronic music\nelectronic rock\nelectronica\nelectropop\nelevator music\nemo\nemo pop\nemo rap\nemocore\nemotronic\nenka\nepic doom metal\nepic metal\neremwu eu\nethereal pop\nethereal wave\neuro\neuro disco\neurobeat\neurodance\neuropop\neurotrance\neurourban\nexotica\nexperimental music\nexperimental noise\nexperimental pop\nexperimental rock\nextreme metal\nezengileer\nfado\nfalak\nfandango\nfarruca\nfife and drum blues\nfilk\nfilm score\nfilmi\nfilmi-ghazal\nfinger-style\nfjatpangarri\nflamenco\nflamenco rumba\nflower power\nfoaie verde\nfofa\nfolk hop\nfolk metal\nfolk music\nfolk pop\nfolk punk\nfolk rock\nfolktronica\nforró\nfranco-country\nfreak-folk\nfreakbeat\nfree improvisation\nfree jazz\nfree music\nfreestyle\nfreestyle house\nfreetekno\nfrench pop\nfrenchcore\nfrevo\nfricote\nfuji\nfuji music\nfulia\nfull on\nfunaná\nfuneral doom\nfunk\nfunk metal\nfunk rock\nfunkcore\nfunky house\nfurniture music\nfusion jazz\ng-funk\ngaana\ngabba\ngabber\ngagaku\ngaikyoku\ngaita\ngalant\ngamad\ngambang kromong\ngamelan\ngamelan angklung\ngamelan bang\ngamelan bebonangan\ngamelan buh\ngamelan degung\ngamelan gede\ngamelan kebyar\ngamelan salendro\ngamelan selunding\ngamelan semar pegulingan\ngamewave\ngammeldans\ngandrung\ngangsta rap\ngar\ngarage rock\ngarrotin\ngavotte\ngelugpa chanting\ngender wayang\ngending\ngerman folk music\ngharbi\ngharnati\nghazal\nghazal-song\nghetto house\nghettotech\ngirl group\nglam metal\nglam punk\nglam rock\nglitch\ngnawa\ngo-go\ngoa\ngoa trance\ngong-chime music\ngoombay\ngoregrind\ngoshu ondo\ngospel music\ngothic metal\ngothic rock\ngranadinas\ngrebo\ngregorian chant\ngrime\ngrindcore\ngroove metal\ngroup sounds\ngrunge\ngrupera\nguaguanbo\nguajira\nguasca\nguitarra baiana\nguitarradas\ngumbe\ngunchei\ngunka\nguoyue\ngwo ka\ngwo ka moderne\ngypsy jazz\ngypsy punk\ngypsybilly\ngyu ke\nhabanera\nhajnali\nhakka\nhalling\nhambo\nhands up\nhapa haole\nhappy hardcore\nhaqibah\nhard\nhard bop\nhard house\nhard rock\nhard trance\nhardcore hip hop\nhardcore metal\nhardcore punk\nhardcore techno\nhardstyle\nharepa\nharmonica blues\nhasaposérviko\nheart attack\nheartland rock\nheavy beat\nheavy metal\nhesher\nhi-nrg\nhighlands\nhighlife\nhighlife fusion\nhillybilly music\nhindustani classical music\nhip hop\nhip hop & rap\nhip hop soul\nhip house\nhiplife\nhiragasy\nhiva usu\nhong kong and cantonese pop\nhong kong english pop\nhonky tonk\nhonkyoku\nhora lunga\nhornpipe\nhorror punk\nhorrorcore\nhorrorcore rap\nhouse\nhouse music\nhua'er\nhuasteco\nhuayno\nhula\nhumor\nhumppa\nhunguhungu\nhyangak\nhymn\nhyphy\nhát chau van\nhát chèo\nhát cãi luong\nhát tuồng\nibiza music\nicaro\nidm\nigbo music\nijexá\nilahije\nillbient\nimpressionist music\nimprovisational\nincidental music\nindian pop\nindie folk\nindie music\nindie pop\nindie rock\nindietronica\nindo jazz\nindo rock\nindonesian pop\nindoyíftika\nindustrial death metal\nindustrial hip hop\nindustrial metal\nindustrial music\nindustrial musical\nindustrial rock\ninstrumental rock\nintelligent dance music\ninternational latin\ninuit music\niranian pop\nirish folk\nirish rebel music\niscathamiya\nisicathamiya\nisikhwela jo\nisland\nisolationist\nitalo dance\nitalo disco\nitalo house\nitsmeños\nizvorna bosanska muzika\nj'ouvert\nj-fusion\nj-pop\nj-rock\njaipongan\njaliscienses\njam band\njam rock\njamana kura\njamrieng samai\njangle pop\njapanese pop\njarana\njariang\njarochos\njawaiian\njazz\njazz blues\njazz fusion\njazz metal\njazz rap\njazz-funk\njazz-rock\njegog\njenkka\njesus music\njibaro\njig\njig punk\njing ping\njingle\njit\njitterbug\njive\njoged\njoged bumbung\njoik\njonnycore\njoropo\njota\njtek\njug band\njujitsu\njuju\njuke joint blues\njump blues\njumpstyle\njungle\njunkanoo\njuré\njùjú\nk-pop\nkaba\nkabuki\nkachāshī\nkadans\nkagok\nkagyupa chanting\nkaiso\nkalamatianó\nkalattuut\nkalinda\nkamba pop\nkan ha diskan\nkansas city blues\nkantrum\nkantádhes\nkargyraa\nkarma\nkaseko\nkatajjaq\nkawachi ondo\nkayōkyoku\nke-kwe\nkebyar\nkecak\nkecapi suling\nkertok\nkhaleeji\nkhap\nkhelimaski djili\nkhene\nkhoomei\nkhorovodi\nkhplam wai\nkhrung sai\nkhyal\nkilapanda\nkinko\nkirtan\nkiwi rock\nkizomba\nklape\nklasik\nklezmer\nkliningan\nkléftiko\nkochare\nkolomyjka\nkomagaku\nkompa\nkonpa\nkorean pop\nkoumpaneia\nkpanlogo\nkrakowiak\nkrautrock\nkriti\nkroncong\nkrump\nkrzesany\nkuduro\nkulintang\nkulning\nkumina\nkun-borrk\nkundere\nkundiman\nkussundé\nkutumba wake\nkveding\nkvæði\nkwaito\nkwassa kwassa\nkwela\nkäng\nkélé\nkĩkũyũ pop\nla la\nlatin american\nlatin jazz\nlatin pop\nlatin rap\nlavway\nlaïko\nlaïkó\nle leagan\nlegényes\nlelio\nletkajenkka\nlevenslied\nlhamo\nlieder\nlight music\nlight rock\nlikanos\nliquid drum&bass\nliquid funk\nliquindi\nllanera\nllanto\nlo-fi\nlo-fi music\nloki djili\nlong-song\nlouisiana blues\nlouisiana swamp pop\nlounge music\nlovers rock\nlowercase\nlubbock sound\nlucknavi thumri\nluhya omutibo\nluk grung\nlullaby\nlundu\nlundum\nm-base\nmadchester\nmadrigal\nmafioso rap\nmaglaal\nmagnificat\nmahori\nmainstream jazz\nmakossa\nmakossa-soukous\nmalagueñas\nmalawian jazz\nmalhun\nmaloya\nmaluf\nmaluka\nmambo\nmanaschi\nmandarin pop\nmanding swing\nmango\nmangue bit\nmangulina\nmanikay\nmanila sound\nmanouche\nmanzuma\nmapouka\nmapouka-serré\nmarabi\nmaracatu\nmarga\nmariachi\nmarimba\nmarinera\nmarrabenta\nmartial industrial\nmartinetes\nmaskanda\nmass\nmatamuerte\nmath rock\nmathcore\nmatt bello\nmaxixe\nmazurka\nmbalax\nmbaqanga\nmbube\nmbumba\nmedh\nmedieval folk rock\nmedieval metal\nmedieval music\nmeditation\nmejorana\nmelhoun\nmelhûn\nmelodic black metal\nmelodic death metal\nmelodic hardcore\nmelodic metalcore\nmelodic music\nmelodic trance\nmemphis blues\nmemphis rap\nmemphis soul\nmento\nmerengue\nmerengue típico moderno\nmerengue-bomba\nmeringue\nmerseybeat\nmetal\nmetalcore\nmetallic hardcore\nmexican pop\nmexican rock\nmexican son\nmeykhana\nmezwed\nmiami bass\nmicrohouse\nmiddle of the road\nmidwest hip hop\nmilonga\nmin'yo\nmineras\nmini compas\nmini-jazz\nminimal techno\nminimalist music\nminimalist trance\nminneapolis sound\nminstrel show\nminuet\nmirolóyia\nmodal jazz\nmodern classical\nmodern classical music\nmodern laika\nmodern rock\nmodinha\nmohabelo\nmontuno\nmonumental dance\nmor lam\nmor lam sing\nmorna\nmotorpop\nmotown\nmozambique\nmpb\nmugam\nmulticultural\nmurga\nmusette\nmuseve\nmushroom jazz\nmusic drama\nmusic hall\nmusiqi-e assil\nmusique concrète\nmutuashi\nmuwashshah\nmuzak\nméringue\nmúsica campesina\nmúsica criolla\nmúsica de la interior\nmúsica llanera\nmúsica nordestina\nmúsica popular brasileira\nmúsica tropical\nnagauta\nnakasi\nnangma\nnanguan\nnarcocorrido\nnardcore\nnarodna muzika\nnasheed\nnashville sound\nnashville sound/countrypolitan\nnational socialist black metal\nnaturalismo\nnederpop\nneo soul\nneo-classical metal\nneo-medieval\nneo-prog\nneo-psychedelia\nneoclassical\nneoclassical metal\nneoclassical music\nneofolk\nneotraditional country\nnerdcore\nneue deutsche härte\nneue deutsche welle\nnew age music\nnew beat\nnew instrumental\nnew jack swing\nnew orleans blues\nnew orleans jazz\nnew pop\nnew prog\nnew rave\nnew romantic\nnew school hip hop\nnew taiwanese song\nnew wave\nnew wave of british heavy metal\nnew wave of new wave\nnew weird america\nnew york blues\nnew york house\nnewgrass\nnganja\nnightcore\nnintendocore\nnisiótika\nno wave\nnoh\nnoise music\nnoise pop\nnoise rock\nnongak\nnorae undong\nnordic folk dance music\nnordic folk music\nnortec\nnorteño\nnorthern soul\nnota\nnu jazz\nnu metal\nnu soul\nnu skool breaks\nnueva canción\nnyatiti\nnéo kýma\nobscuro\noi!\nold school hip hop\nold-time\noldies\nolonkho\noltului\nondo\nopera\noperatic pop\noratorio\norchestra\norchestral\norgan trio\norganic ambient\norganum\norgel\noriental metal\nottava rima\noutlaw country\noutsider music\np-funk\npagan metal\npagan rock\npagode\npaisley underground\npalm wine\npalm-wine\npambiche\npanambih\npanchai baja\npanchavadyam\npansori\nparanda\nparang\nparody\nparranda\npartido alto\npasillo\npatriotic\npeace punk\npelimanni music\npetenera\npeyote song\nphiladelphia soul\npiano blues\npiano rock\npiedmont blues\npimba\npinoy pop\npinoy rock\npinpeat orchestra\npiphat\npiyyutim\nplainchant\nplena\npleng phua cheewit\npleng thai sakorn\npolitical hip hop\npolka\npolo\npolonaise\npols\npolska\npong lang\npop\npop folk\npop music\npop punk\npop rap\npop rock\npop sunda\npornocore\nporro\npost disco\npost-britpop\npost-disco\npost-grunge\npost-hardcore\npost-industrial\npost-metal\npost-minimalism\npost-punk\npost-rock\npost-romanticism\npow-wow\npower electronics\npower metal\npower noise\npower pop\npowerviolence\nppongtchak\npraise song\nprogram symphony\nprogressive bluegrass\nprogressive country\nprogressive death metal\nprogressive electronic\nprogressive electronic music\nprogressive folk\nprogressive folk music\nprogressive house\nprogressive metal\nprogressive power metal\nprogressive rock\nprogressive trance\nprogressive thrash metal\nprotopunk\npsych folk\npsychedelic music\npsychedelic pop\npsychedelic rock\npsychedelic trance\npsychobilly\npunk blues\npunk cabaret\npunk jazz\npunk rock\npunta\npunta rock\nqasidah\nqasidah modern\nqawwali\nquadrille\nquan ho\nqueercore\nquiet storm\nrada\nraga\nraga rock\nragga\nragga jungle\nraggamuffin\nragtime\nrai\nrake-and-scrape\nramkbach\nramvong\nranchera\nrap\nrap metal\nrap rock\nrapcore\nrara\nrare groove\nrasiya\nrave\nraw rock\nraï\nrebetiko\nred dirt\nreel\nreggae\nreggae 110\nreggae bultrón\nreggae en español\nreggae fusion\nreggae highlife\nreggaefusion\nreggaeton\nrekilaulu\nrelax music\nreligious\nrembetiko\nrenaissance music\nrequiem\nrhapsody\nrhyming spiritual\nrhythm & blues\nrhythm and blues\nricercar\nriot grrrl\nrock\nrock and roll\nrock en español\nrock opera\nrockabilly\nrocksteady\nrococo\nromantic flow\nromantic period in music\nrondeaux\nronggeng\nroots reggae\nroots rock\nroots rock reggae\nrumba\nrussian pop\nrímur\nsabar\nsacred harp\nsacred music\nsadcore\nsaibara\nsakara\nsalegy\nsalsa\nsalsa erotica\nsalsa romantica\nsaltarello\nsamba\nsamba-canção\nsamba-reggae\nsamba-rock\nsambai\nsanjo\nsato kagura\nsawt\nsaya\nscat\nschlager\nschottisch\nschranz\nscottish baroque music\nscreamo\nscrumpy and western\nsea shanty\nsean nós\nsecond viennese school\nsega music\nseggae\nseis\nsemba\nsephardic music\nserialism\nset dance\nsevdalinka\nsevillana\nshabab\nshabad\nshalako\nshan'ge\nshango\nshape note\nshibuya-kei\nshidaiqu\nshima uta\nshock rock\nshoegaze\nshoegazer\nshoka\nshomyo\nshow tune\nsica\nsiguiriyas\nsilat\nsinawi\nsituational\nska\nska punk\nskacore\nskald\nskate punk\nskiffle\nslack-key guitar\nslide\nslowcore\nsludge metal\nslängpolska\nsmooth jazz\nsoca\nsoft rock\nson\nson montuno\nson-batá\nsonata\nsongo\nsongo-salsa\nsophisti-pop\nsoukous\nsoul\nsoul blues\nsoul jazz\nsoul music\nsouthern gospel\nsouthern harmony\nsouthern hip hop\nsouthern metal\nsouthern rock\nsouthern soul\nspace age pop\nspace music\nspace rock\nspectralism\nspeed garage\nspeed metal\nspeedcore\nspirituals\nspouge\nsprechgesang\nsquare dance\nsquee\nst. louis blues\nstand-up\nsteelband\nstoner metal\nstoner rock\nstraight edge\nstrathspeys\nstride\nstring\nstring quartet\nsufi music\nsuite\nsunshine pop\nsuomirock\nsuper eurobeat\nsurf ballad\nsurf instrumental\nsurf music\nsurf pop\nsurf rock\nswamp blues\nswamp pop\nswamp rock\nswing\nswing music\nswingbeat\nsygyt\nsymphonic\nsymphonic black metal\nsymphonic metal\nsymphonic poem\nsymphonic rock\nsymphony\nsynthcore\nsynthpop\nsynthpunk\nt'ong guitar\ntaarab\ntai tu\ntaiwanese pop\ntala\ntalempong\ntambu\ntamburitza\ntamil christian keerthanai\ntango\ntanguk\ntappa\ntarana\ntarantella\ntaranto\ntech\ntech house\ntech trance\ntechnical death metal\ntechnical metal\ntechno\ntechnoid\ntechnopop\ntechstep\ntechtonik\nteen pop\ntejano\ntejano music\ntekno\ntembang sunda\nteutonic thrash metal\ntexas blues\nthai pop\nthillana\nthrash metal\nthrashcore\nthumri\ntibetan pop\ntiento\ntimbila\ntin pan alley\ntinga\ntinku\ntoeshey\ntogaku\ntrad jazz\ntraditional bluegrass\ntraditional heavy metal\ntraditional pop music\ntrallalero\ntrance\ntribal house\ntrikitixa\ntrip hop\ntrip rock\ntropicalia\ntropicalismo\ntropipop\ntruck-driving country\ntumba\nturbo-folk\nturkish music\nturkish pop\nturntablism\ntuvan throat-singing\ntwee pop\ntwist\ntwo tone\ntáncház\nuk garage\nuk pub rock\nunblack metal\nunderground music\nuplifting\nuplifting trance\nurban cowboy\nurban folk\nurban jazz\nvallenato\nvaudeville\nvenezuela\nverbunkos\nverismo\nviking metal\nvillanella\nvirelai\nvispop\nvisual kei\nvisual music\nvocal\nvocal house\nvocal jazz\nvocal music\nvolksmusik\nwaila\nwaltz\nwangga\nwarabe uta\nwassoulou\nweld\nwere music\nwest coast hip hop\nwest coast jazz\nwestern\nwestern blues\nwestern swing\nwitch house\nwizard rock\nwomen's music\nwong shadow\nwonky pop\nwood\nwork song\nworld fusion\nworld fusion music\nworld music\nworldbeat\nxhosa music\nxoomii\nyo-pop\nyodeling\nyukar\nyé-yé\nzajal\nzapin\nzarzuela\nzeibekiko\nzeuhl\nziglibithy\nzouglou\nzouk\nzouk chouv\nzouklove\nzulu music\nzydeco\n"
  },
  {
    "path": "beetsplug/lastimport.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Rafael Bodill https://github.com/rafi\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nimport pylast\nfrom pylast import TopItem, _extract, _number\n\nfrom beets import config, dbcore, plugins, ui\nfrom beets.dbcore import types\n\nAPI_URL = \"https://ws.audioscrobbler.com/2.0/\"\n\n\nclass LastImportPlugin(plugins.BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n        config[\"lastfm\"].add(\n            {\n                \"user\": \"\",\n                \"api_key\": plugins.LASTFM_KEY,\n            }\n        )\n        config[\"lastfm\"][\"user\"].redact = True\n        config[\"lastfm\"][\"api_key\"].redact = True\n        self.config.add(\n            {\n                \"per_page\": 500,\n                \"retry_limit\": 3,\n            }\n        )\n        self.item_types = {\n            \"lastfm_play_count\": types.INTEGER,\n        }\n\n    def commands(self):\n        cmd = ui.Subcommand(\"lastimport\", help=\"import last.fm play-count\")\n\n        def func(lib, opts, args):\n            import_lastfm(lib, self._log)\n\n        cmd.func = func\n        return [cmd]\n\n\nclass CustomUser(pylast.User):\n    \"\"\"Custom user class derived from pylast.User, and overriding the\n    _get_things method to return MBID and album. Also introduces new\n    get_top_tracks_by_page method to allow access to more than one page of top\n    tracks.\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n\n    def _get_things(\n        self, method, thing, thing_type, params=None, cacheable=True\n    ):\n        \"\"\"Returns a list of the most played thing_types by this thing, in a\n        tuple with the total number of pages of results. Includes an MBID, if\n        found.\n        \"\"\"\n        doc = self._request(f\"{self.ws_prefix}.{method}\", cacheable, params)\n\n        toptracks_node = doc.getElementsByTagName(\"toptracks\")[0]\n        total_pages = int(toptracks_node.getAttribute(\"totalPages\"))\n\n        seq = []\n        for node in doc.getElementsByTagName(thing):\n            title = _extract(node, \"name\")\n            artist = _extract(node, \"name\", 1)\n            mbid = _extract(node, \"mbid\")\n            playcount = _number(_extract(node, \"playcount\"))\n\n            thing = thing_type(artist, title, self.network)\n            thing.mbid = mbid\n            seq.append(TopItem(thing, playcount))\n\n        return seq, total_pages\n\n    def get_top_tracks_by_page(\n        self, period=pylast.PERIOD_OVERALL, limit=None, page=1, cacheable=True\n    ):\n        \"\"\"Returns the top tracks played by a user, in a tuple with the total\n        number of pages of results.\n        * period: The period of time. Possible values:\n          o PERIOD_OVERALL\n          o PERIOD_7DAYS\n          o PERIOD_1MONTH\n          o PERIOD_3MONTHS\n          o PERIOD_6MONTHS\n          o PERIOD_12MONTHS\n        \"\"\"\n\n        params = self._get_params()\n        params[\"period\"] = period\n        params[\"page\"] = page\n        if limit:\n            params[\"limit\"] = limit\n\n        return self._get_things(\n            \"getTopTracks\", \"track\", pylast.Track, params, cacheable\n        )\n\n\ndef import_lastfm(lib, log):\n    user = config[\"lastfm\"][\"user\"].as_str()\n    per_page = config[\"lastimport\"][\"per_page\"].get(int)\n\n    if not user:\n        raise ui.UserError(\"You must specify a user name for lastimport\")\n\n    log.info(\"Fetching last.fm library for @{}\", user)\n\n    page_total = 1\n    page_current = 0\n    found_total = 0\n    unknown_total = 0\n    retry_limit = config[\"lastimport\"][\"retry_limit\"].get(int)\n    # Iterate through a yet to be known page total count\n    while page_current < page_total:\n        log.info(\n            \"Querying page #{}{}...\",\n            page_current + 1,\n            f\"/{page_total}\" if page_total > 1 else \"\",\n        )\n\n        for retry in range(0, retry_limit):\n            tracks, page_total = fetch_tracks(user, page_current + 1, per_page)\n            if page_total < 1:\n                # It means nothing to us!\n                raise ui.UserError(\"Last.fm reported no data.\")\n\n            if tracks:\n                found, unknown = process_tracks(lib, tracks, log)\n                found_total += found\n                unknown_total += unknown\n                break\n            else:\n                log.error(\"ERROR: unable to read page #{}\", page_current + 1)\n                if retry < retry_limit:\n                    log.info(\n                        \"Retrying page #{}... ({}/{} retry)\",\n                        page_current + 1,\n                        retry + 1,\n                        retry_limit,\n                    )\n                else:\n                    log.error(\n                        \"FAIL: unable to fetch page #{}, \",\n                        \"tried {} times\",\n                        page_current,\n                        retry + 1,\n                    )\n        page_current += 1\n\n    log.info(\"... done!\")\n    log.info(\"finished processing {} song pages\", page_total)\n    log.info(\"{} unknown play-counts\", unknown_total)\n    log.info(\"{} play-counts imported\", found_total)\n\n\ndef fetch_tracks(user, page, limit):\n    \"\"\"JSON format:\n    [\n        {\n            \"mbid\": \"...\",\n            \"artist\": \"...\",\n            \"title\": \"...\",\n            \"playcount\": \"...\"\n        }\n    ]\n    \"\"\"\n    network = pylast.LastFMNetwork(api_key=config[\"lastfm\"][\"api_key\"])\n    user_obj = CustomUser(user, network)\n    results, total_pages = user_obj.get_top_tracks_by_page(\n        limit=limit, page=page\n    )\n    return [\n        {\n            \"mbid\": track.item.mbid if track.item.mbid else \"\",\n            \"artist\": {\"name\": track.item.artist.name},\n            \"name\": track.item.title,\n            \"playcount\": track.weight,\n        }\n        for track in results\n    ], total_pages\n\n\ndef process_tracks(lib, tracks, log):\n    total = len(tracks)\n    total_found = 0\n    total_fails = 0\n    log.info(\"Received {} tracks in this page, processing...\", total)\n\n    for num in range(0, total):\n        song = None\n        trackid = tracks[num][\"mbid\"].strip() if tracks[num][\"mbid\"] else None\n        artist = (\n            tracks[num][\"artist\"].get(\"name\", \"\").strip()\n            if tracks[num][\"artist\"].get(\"name\", \"\")\n            else None\n        )\n        title = tracks[num][\"name\"].strip() if tracks[num][\"name\"] else None\n        album = \"\"\n        if \"album\" in tracks[num]:\n            album = (\n                tracks[num][\"album\"].get(\"name\", \"\").strip()\n                if tracks[num][\"album\"]\n                else None\n            )\n\n        log.debug(\"query: {} - {} ({})\", artist, title, album)\n\n        # First try to query by musicbrainz's trackid\n        if trackid:\n            song = lib.items(\n                dbcore.query.MatchQuery(\"mb_trackid\", trackid)\n            ).get()\n\n        # If not, try just album/title\n        if song is None:\n            log.debug(\n                \"no album match, trying by album/title: {} - {}\", album, title\n            )\n            query = dbcore.AndQuery(\n                [\n                    dbcore.query.SubstringQuery(\"album\", album),\n                    dbcore.query.SubstringQuery(\"title\", title),\n                ]\n            )\n            song = lib.items(query).get()\n\n        # If not, try just artist/title\n        if song is None:\n            log.debug(\"no album match, trying by artist/title\")\n            query = dbcore.AndQuery(\n                [\n                    dbcore.query.SubstringQuery(\"artist\", artist),\n                    dbcore.query.SubstringQuery(\"title\", title),\n                ]\n            )\n            song = lib.items(query).get()\n\n        # Last resort, try just replacing to utf-8 quote\n        if song is None:\n            title = title.replace(\"'\", \"\\u2019\")\n            log.debug(\"no title match, trying utf-8 single quote\")\n            query = dbcore.AndQuery(\n                [\n                    dbcore.query.SubstringQuery(\"artist\", artist),\n                    dbcore.query.SubstringQuery(\"title\", title),\n                ]\n            )\n            song = lib.items(query).get()\n\n        if song is not None:\n            count = int(song.get(\"lastfm_play_count\", 0))\n            new_count = int(tracks[num][\"playcount\"])\n            log.debug(\n                \"match: {0.artist} - {0.title} ({0.album}) updating:\"\n                \" lastfm_play_count {1} => {2}\",\n                song,\n                count,\n                new_count,\n            )\n            song[\"lastfm_play_count\"] = new_count\n            song.store()\n            total_found += 1\n        else:\n            total_fails += 1\n            log.info(\"  - No match: {} - {} ({})\", artist, title, album)\n\n    if total_fails > 0:\n        log.info(\n            \"Acquired {}/{} play-counts ({} unknown)\",\n            total_found,\n            total,\n            total_fails,\n        )\n\n    return total_found, total_fails\n"
  },
  {
    "path": "beetsplug/limit.py",
    "content": "# This file is part of beets.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Adds head/tail functionality to list/ls.\n\n1. Implemented as `lslimit` command with `--head` and `--tail` options. This is\n   the idiomatic way to use this plugin.\n2. Implemented as query prefix `<` for head functionality only. This is the\n   composable way to use the plugin (plays nicely with anything that uses the\n   query language).\n\"\"\"\n\nfrom collections import deque\nfrom itertools import islice\n\nfrom beets.dbcore import FieldQuery\nfrom beets.plugins import BeetsPlugin\nfrom beets.ui import Subcommand, print_\n\n\ndef lslimit(lib, opts, args):\n    \"\"\"Query command with head/tail.\"\"\"\n\n    if (opts.head is not None) and (opts.tail is not None):\n        raise ValueError(\"Only use one of --head and --tail\")\n    if (opts.head or opts.tail or 0) < 0:\n        raise ValueError(\"Limit value must be non-negative\")\n\n    if opts.album:\n        objs = lib.albums(args)\n    else:\n        objs = lib.items(args)\n\n    if opts.head is not None:\n        objs = islice(objs, opts.head)\n    elif opts.tail is not None:\n        objs = deque(objs, opts.tail)\n\n    for obj in objs:\n        print_(format(obj))\n\n\nlslimit_cmd = Subcommand(\"lslimit\", help=\"query with optional head or tail\")\n\nlslimit_cmd.parser.add_option(\n    \"--head\", action=\"store\", type=\"int\", default=None\n)\n\nlslimit_cmd.parser.add_option(\n    \"--tail\", action=\"store\", type=\"int\", default=None\n)\n\nlslimit_cmd.parser.add_all_common_options()\nlslimit_cmd.func = lslimit\n\n\nclass LimitPlugin(BeetsPlugin):\n    \"\"\"Query limit functionality via command and query prefix.\"\"\"\n\n    def commands(self):\n        \"\"\"Expose `lslimit` subcommand.\"\"\"\n        return [lslimit_cmd]\n\n    def queries(self):\n        class HeadQuery(FieldQuery):\n            \"\"\"This inner class pattern allows the query to track state.\"\"\"\n\n            n = 0\n            N = None\n\n            def __init__(self, *args, **kwargs) -> None:\n                \"\"\"Force the query to be slow so that 'value_match' is called.\"\"\"\n                super().__init__(*args, **kwargs)\n                self.fast = False\n\n            @classmethod\n            def value_match(cls, pattern, value):\n                if cls.N is None:\n                    cls.N = int(pattern)\n                    if cls.N < 0:\n                        raise ValueError(\"Limit value must be non-negative\")\n                cls.n += 1\n                return cls.n <= cls.N\n\n        return {\"<\": HeadQuery}\n"
  },
  {
    "path": "beetsplug/listenbrainz.py",
    "content": "\"\"\"Adds Listenbrainz support to Beets.\"\"\"\n\nimport datetime\n\nimport requests\n\nfrom beets import config, ui\nfrom beets.plugins import BeetsPlugin\nfrom beetsplug.lastimport import process_tracks\n\nfrom ._utils.musicbrainz import MusicBrainzAPIMixin\n\n\nclass ListenBrainzPlugin(MusicBrainzAPIMixin, BeetsPlugin):\n    \"\"\"A Beets plugin for interacting with ListenBrainz.\"\"\"\n\n    ROOT = \"http://api.listenbrainz.org/1/\"\n\n    def __init__(self):\n        \"\"\"Initialize the plugin.\"\"\"\n        super().__init__()\n        self.token = self.config[\"token\"].get()\n        self.username = self.config[\"username\"].get()\n        self.AUTH_HEADER = {\"Authorization\": f\"Token {self.token}\"}\n        config[\"listenbrainz\"][\"token\"].redact = True\n\n    def commands(self):\n        \"\"\"Add beet UI commands to interact with ListenBrainz.\"\"\"\n        lbupdate_cmd = ui.Subcommand(\n            \"lbimport\", help=\"Import ListenBrainz history\"\n        )\n\n        def func(lib, opts, args):\n            self._lbupdate(lib, self._log)\n\n        lbupdate_cmd.func = func\n        return [lbupdate_cmd]\n\n    def _lbupdate(self, lib, log):\n        \"\"\"Obtain view count from Listenbrainz.\"\"\"\n        found_total = 0\n        unknown_total = 0\n        ls = self.get_listens()\n        tracks = self.get_tracks_from_listens(ls)\n        log.info(\"Found {} listens\", len(ls))\n        if tracks:\n            found, unknown = process_tracks(lib, tracks, log)\n            found_total += found\n            unknown_total += unknown\n        log.info(\"... done!\")\n        log.info(\"{} unknown play-counts\", unknown_total)\n        log.info(\"{} play-counts imported\", found_total)\n\n    def _make_request(self, url, params=None):\n        \"\"\"Makes a request to the ListenBrainz API.\"\"\"\n        try:\n            response = requests.get(\n                url=url,\n                headers=self.AUTH_HEADER,\n                timeout=10,\n                params=params,\n            )\n            response.raise_for_status()\n            return response.json()\n        except requests.exceptions.RequestException as e:\n            self._log.debug(\"Invalid Search Error: {}\", e)\n            return None\n\n    def get_listens(self, min_ts=None, max_ts=None, count=None):\n        \"\"\"Gets the listen history of a given user.\n\n        Args:\n            username: User to get listen history of.\n            min_ts: History before this timestamp will not be returned.\n                    DO NOT USE WITH max_ts.\n            max_ts: History after this timestamp will not be returned.\n                    DO NOT USE WITH min_ts.\n            count: How many listens to return. If not specified,\n                uses a default from the server.\n\n        Returns:\n            A list of listen info dictionaries if there's an OK status.\n\n        Raises:\n            An HTTPError if there's a failure.\n            A ValueError if the JSON in the response is invalid.\n            An IndexError if the JSON is not structured as expected.\n        \"\"\"\n        url = f\"{self.ROOT}/user/{self.username}/listens\"\n        params = {\n            k: v\n            for k, v in {\n                \"min_ts\": min_ts,\n                \"max_ts\": max_ts,\n                \"count\": count,\n            }.items()\n            if v is not None\n        }\n        response = self._make_request(url, params)\n\n        if response is not None:\n            return response[\"payload\"][\"listens\"]\n        else:\n            return None\n\n    def get_tracks_from_listens(self, listens):\n        \"\"\"Returns a list of tracks from a list of listens.\"\"\"\n        tracks = []\n        for track in listens:\n            if track[\"track_metadata\"].get(\"release_name\") is None:\n                continue\n            mbid_mapping = track[\"track_metadata\"].get(\"mbid_mapping\", {})\n            mbid = None\n            if mbid_mapping.get(\"recording_mbid\") is None:\n                # search for the track using title and release\n                mbid = self.get_mb_recording_id(track)\n            tracks.append(\n                {\n                    \"album\": {\n                        \"name\": track[\"track_metadata\"].get(\"release_name\")\n                    },\n                    \"name\": track[\"track_metadata\"].get(\"track_name\"),\n                    \"artist\": {\n                        \"name\": track[\"track_metadata\"].get(\"artist_name\")\n                    },\n                    \"mbid\": mbid,\n                    \"release_mbid\": mbid_mapping.get(\"release_mbid\"),\n                    \"listened_at\": track.get(\"listened_at\"),\n                }\n            )\n        return tracks\n\n    def get_mb_recording_id(self, track) -> str | None:\n        \"\"\"Returns the MusicBrainz recording ID for a track.\"\"\"\n        results = self.mb_api.search(\n            \"recording\",\n            {\n                \"\": track[\"track_metadata\"].get(\"track_name\"),\n                \"release\": track[\"track_metadata\"].get(\"release_name\"),\n            },\n        )\n        return next((r[\"id\"] for r in results), None)\n\n    def get_playlists_createdfor(self, username):\n        \"\"\"Returns a list of playlists created by a user.\"\"\"\n        url = f\"{self.ROOT}/user/{username}/playlists/createdfor\"\n        return self._make_request(url)\n\n    def get_listenbrainz_playlists(self):\n        resp = self.get_playlists_createdfor(self.username)\n        playlists = resp.get(\"playlists\")\n        listenbrainz_playlists = []\n\n        for playlist in playlists:\n            playlist_info = playlist.get(\"playlist\")\n            if playlist_info.get(\"creator\") == \"listenbrainz\":\n                title = playlist_info.get(\"title\")\n                self._log.debug(\"Playlist title: {}\", title)\n                playlist_type = (\n                    \"Exploration\" if \"Exploration\" in title else \"Jams\"\n                )\n                if \"week of\" in title:\n                    date_str = title.split(\"week of \")[1].split(\" \")[0]\n                    date = datetime.datetime.strptime(\n                        date_str, \"%Y-%m-%d\"\n                    ).date()\n                else:\n                    continue\n                identifier = playlist_info.get(\"identifier\")\n                id = identifier.split(\"/\")[-1]\n                listenbrainz_playlists.append(\n                    {\"type\": playlist_type, \"date\": date, \"identifier\": id}\n                )\n        listenbrainz_playlists = sorted(\n            listenbrainz_playlists, key=lambda x: x[\"type\"]\n        )\n        listenbrainz_playlists = sorted(\n            listenbrainz_playlists, key=lambda x: x[\"date\"], reverse=True\n        )\n        for playlist in listenbrainz_playlists:\n            self._log.debug(\"Playlist: {0[type]} - {0[date]}\", playlist)\n        return listenbrainz_playlists\n\n    def get_playlist(self, identifier):\n        \"\"\"Returns a playlist.\"\"\"\n        url = f\"{self.ROOT}/playlist/{identifier}\"\n        return self._make_request(url)\n\n    def get_tracks_from_playlist(self, playlist):\n        \"\"\"This function returns a list of tracks in the playlist.\"\"\"\n        tracks = []\n        for track in playlist.get(\"playlist\").get(\"track\"):\n            identifier = track.get(\"identifier\")\n            if isinstance(identifier, list):\n                identifier = identifier[0]\n\n            tracks.append(\n                {\n                    \"artist\": track.get(\"creator\", \"Unknown artist\"),\n                    \"identifier\": identifier.split(\"/\")[-1],\n                    \"title\": track.get(\"title\"),\n                }\n            )\n        return self.get_track_info(tracks)\n\n    def get_track_info(self, tracks):\n        track_info = []\n        for track in tracks:\n            identifier = track.get(\"identifier\")\n            recording = self.mb_api.get_recording(\n                identifier, includes=[\"releases\", \"artist-credits\"]\n            )\n            title = recording.get(\"title\")\n            artist_credit = recording.get(\"artist-credit\", [])\n            if artist_credit:\n                artist = artist_credit[0].get(\"artist\", {}).get(\"name\")\n            else:\n                artist = None\n            releases = recording.get(\"releases\", [])\n            if releases:\n                album = releases[0].get(\"title\")\n                date = releases[0].get(\"date\")\n                year = date.split(\"-\")[0] if date else None\n            else:\n                album = None\n                year = None\n            track_info.append(\n                {\n                    \"identifier\": identifier,\n                    \"title\": title,\n                    \"artist\": artist,\n                    \"album\": album,\n                    \"year\": year,\n                }\n            )\n        return track_info\n\n    def get_weekly_playlist(self, playlist_type, most_recent=True):\n        # Fetch all playlists\n        playlists = self.get_listenbrainz_playlists()\n        # Filter playlists by type\n        filtered_playlists = [\n            p for p in playlists if p[\"type\"] == playlist_type\n        ]\n        # Sort playlists by date in descending order\n        sorted_playlists = sorted(\n            filtered_playlists, key=lambda x: x[\"date\"], reverse=True\n        )\n        # Select the most recent or older playlist based on the most_recent flag\n        selected_playlist = (\n            sorted_playlists[0] if most_recent else sorted_playlists[1]\n        )\n        self._log.debug(\n            f\"Selected playlist: {selected_playlist['type']} \"\n            f\"- {selected_playlist['date']}\"\n        )\n        # Fetch and return tracks from the selected playlist\n        playlist = self.get_playlist(selected_playlist.get(\"identifier\"))\n        return self.get_tracks_from_playlist(playlist)\n\n    def get_weekly_exploration(self):\n        return self.get_weekly_playlist(\"Exploration\", most_recent=True)\n\n    def get_weekly_jams(self):\n        return self.get_weekly_playlist(\"Jams\", most_recent=True)\n\n    def get_last_weekly_exploration(self):\n        return self.get_weekly_playlist(\"Exploration\", most_recent=False)\n\n    def get_last_weekly_jams(self):\n        return self.get_weekly_playlist(\"Jams\", most_recent=False)\n"
  },
  {
    "path": "beetsplug/loadext.py",
    "content": "# This file is part of beets.\n# Copyright 2019, Jack Wilsdon <jack.wilsdon@gmail.com>\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Load SQLite extensions.\"\"\"\n\nimport sqlite3\n\nfrom beets.dbcore import Database\nfrom beets.plugins import BeetsPlugin\n\n\nclass LoadExtPlugin(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n\n        if not Database.supports_extensions:\n            self._log.warning(\n                \"loadext is enabled but the current SQLite \"\n                \"installation does not support extensions\"\n            )\n            return\n\n        self.register_listener(\"library_opened\", self.library_opened)\n\n    def library_opened(self, lib):\n        for v in self.config:\n            ext = v.as_filename()\n\n            self._log.debug(\"loading extension {}\", ext)\n\n            try:\n                lib.load_extension(ext)\n            except sqlite3.OperationalError as e:\n                self._log.error(\"failed to load extension {}: {}\", ext, e)\n"
  },
  {
    "path": "beetsplug/lyrics.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Fetches, embeds, and displays lyrics.\"\"\"\n\nfrom __future__ import annotations\n\nimport itertools\nimport math\nimport re\nimport textwrap\nfrom contextlib import contextmanager, suppress\nfrom dataclasses import dataclass\nfrom functools import cached_property, partial, total_ordering\nfrom html import unescape\nfrom itertools import groupby\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, ClassVar, NamedTuple\nfrom urllib.parse import quote, quote_plus, urlencode, urlparse\n\nimport requests\nfrom bs4 import BeautifulSoup\nfrom unidecode import unidecode\n\nfrom beets import plugins, ui\nfrom beets.autotag.distance import string_dist\nfrom beets.dbcore import types\nfrom beets.util.config import sanitize_choices\nfrom beets.util.lyrics import INSTRUMENTAL_LYRICS, Lyrics\n\nfrom ._utils.requests import HTTPNotFoundError, RequestHandler\n\nif TYPE_CHECKING:\n    from collections.abc import Iterable, Iterator\n\n    import confuse\n\n    from beets.importer import ImportTask\n    from beets.library import Item, Library\n    from beets.logging import BeetsLogger as Logger\n\n    from ._typing import (\n        GeniusAPI,\n        GoogleCustomSearchAPI,\n        JSONDict,\n        LRCLibAPI,\n        TranslatorAPI,\n    )\n\n\nclass CaptchaError(requests.exceptions.HTTPError):\n    def __init__(self, *args, **kwargs) -> None:\n        super().__init__(\"Captcha is required\", *args, **kwargs)\n\n\nclass GeniusHTTPError(requests.exceptions.HTTPError):\n    pass\n\n\n# Utilities.\n\n\ndef search_pairs(item):\n    \"\"\"Yield a pairs of artists and titles to search for.\n\n    The first item in the pair is the name of the artist, the second\n    item is a list of song names.\n\n    In addition to the artist and title obtained from the `item` the\n    method tries to strip extra information like paranthesized suffixes\n    and featured artists from the strings and add them as candidates.\n    The artist sort name is added as a fallback candidate to help in\n    cases where artist name includes special characters or is in a\n    non-latin script.\n    The method also tries to split multiple titles separated with `/`.\n    \"\"\"\n\n    def generate_alternatives(string, patterns):\n        \"\"\"Generate string alternatives by extracting first matching group for\n        each given pattern.\n        \"\"\"\n        alternatives = [string]\n        for pattern in patterns:\n            match = re.search(pattern, string, re.IGNORECASE)\n            if match:\n                alternatives.append(match.group(1))\n        return alternatives\n\n    title, artist, artist_sort = (\n        item.title.strip(),\n        item.artist.strip(),\n        item.artist_sort.strip(),\n    )\n    if not title or not artist:\n        return ()\n\n    patterns = [\n        # Remove any featuring artists from the artists name\n        rf\"(.*?) {plugins.feat_tokens()}\"\n    ]\n\n    # Skip various artists\n    artists = []\n    lower_artist = artist.lower()\n    if \"various\" not in lower_artist:\n        artists.extend(generate_alternatives(artist, patterns))\n    # Use the artist_sort as fallback only if it differs from artist to avoid\n    # repeated remote requests with the same search terms\n    artist_sort_lower = artist_sort.lower()\n    if (\n        artist_sort\n        and lower_artist != artist_sort_lower\n        and \"various\" not in artist_sort_lower\n    ):\n        artists.append(artist_sort)\n\n    patterns = [\n        # Remove a parenthesized suffix from a title string. Common\n        # examples include (live), (remix), and (acoustic).\n        r\"(.+?)\\s+[(].*[)]$\",\n        # Remove any featuring artists from the title\n        rf\"(.*?) {plugins.feat_tokens(for_artist=False)}\",\n        # Remove part of title after colon ':' for songs with subtitles\n        r\"(.+?)\\s*:.*\",\n    ]\n    titles = generate_alternatives(title, patterns)\n\n    # Check for a dual song (e.g. Pink Floyd - Speak to Me / Breathe)\n    # and each of them.\n    multi_titles = []\n    for title in titles:\n        multi_titles.append([title])\n        if \" / \" in title:\n            multi_titles.append([x.strip() for x in title.split(\" / \")])\n\n    return itertools.product(artists, multi_titles)\n\n\ndef slug(text: str) -> str:\n    \"\"\"Make a URL-safe, human-readable version of the given text\n\n    This will do the following:\n\n    1. decode unicode characters into ASCII\n    2. shift everything to lowercase\n    3. strip whitespace\n    4. replace other non-word characters with dashes\n    5. strip extra dashes\n    \"\"\"\n    return re.sub(r\"\\W+\", \"-\", unidecode(text).lower().strip()).strip(\"-\")\n\n\nclass LyricsRequestHandler(RequestHandler):\n    _log: Logger\n\n    def status_to_error(self, code: int) -> type[requests.HTTPError] | None:\n        if err := super().status_to_error(code):\n            return err\n\n        if 300 <= code < 400:\n            return CaptchaError\n\n        return None\n\n    def debug(self, message: str, *args) -> None:\n        \"\"\"Log a debug message with the class name.\"\"\"\n        self._log.debug(f\"{self.__class__.__name__}: {message}\", *args)\n\n    def info(self, message: str, *args) -> None:\n        \"\"\"Log an info message with the class name.\"\"\"\n        self._log.info(f\"{self.__class__.__name__}: {message}\", *args)\n\n    def warn(self, message: str, *args) -> None:\n        \"\"\"Log warning with the class name.\"\"\"\n        self._log.warning(f\"{self.__class__.__name__}: {message}\", *args)\n\n    @staticmethod\n    def format_url(url: str, params: JSONDict | None) -> str:\n        if not params:\n            return url\n\n        return f\"{url}?{urlencode(params)}\"\n\n    def get_text(\n        self, url: str, params: JSONDict | None = None, **kwargs\n    ) -> str:\n        \"\"\"Return text / HTML data from the given URL.\n\n        Set the encoding to None to let requests handle it because some sites\n        set it incorrectly.\n        \"\"\"\n        url = self.format_url(url, params)\n        self.debug(\"Fetching HTML from {}\", url)\n        r = self.get(url, **kwargs)\n        r.encoding = None\n        return r.text\n\n    def get_json(self, url: str, params: JSONDict | None = None, **kwargs):\n        \"\"\"Return JSON data from the given URL.\"\"\"\n        url = self.format_url(url, params)\n        self.debug(\"Fetching JSON from {}\", url)\n        return super().get_json(url, **kwargs)\n\n    def post_json(self, url: str, params: JSONDict | None = None, **kwargs):\n        \"\"\"Send POST request and return JSON response.\"\"\"\n        url = self.format_url(url, params)\n        self.debug(\"Posting JSON to {}\", url)\n        return self.request(\"post\", url, **kwargs).json()\n\n    @contextmanager\n    def handle_request(self) -> Iterator[None]:\n        try:\n            yield\n        except requests.JSONDecodeError:\n            self.warn(\"Could not decode response JSON data\")\n        except requests.RequestException as exc:\n            self.warn(\"Request error: {}\", exc)\n\n\nclass BackendClass(type):\n    @property\n    def name(cls) -> str:\n        \"\"\"Return lowercase name of the backend class.\"\"\"\n        return cls.__name__.lower()\n\n\nclass Backend(LyricsRequestHandler, metaclass=BackendClass):\n    config: confuse.Subview\n\n    def __init__(self, config: confuse.Subview, log: Logger) -> None:\n        self._log = log\n        self.config = config\n\n    def fetch(\n        self, artist: str, title: str, album: str, length: int\n    ) -> Lyrics | None:\n        \"\"\"Return lyrics for a song, or ``None`` when no match is found.\"\"\"\n        raise NotImplementedError\n\n\n@dataclass\n@total_ordering\nclass LRCLyrics:\n    \"\"\"Hold LRCLib candidate data and ranking helpers for matching.\"\"\"\n\n    #: Percentage tolerance for max duration difference between lyrics and item.\n    DURATION_DIFF_TOLERANCE = 0.05\n\n    target_duration: float\n    id: int\n    duration: float\n    instrumental: bool\n    plain: str\n    synced: str | None\n\n    def __le__(self, other: LRCLyrics) -> bool:\n        \"\"\"Compare two lyrics items by their score.\"\"\"\n        return self.dist < other.dist\n\n    @classmethod\n    def verify_synced_lyrics(\n        cls, duration: float, lyrics: str | None\n    ) -> str | None:\n        \"\"\"Accept synced lyrics only when the final timestamp fits duration.\"\"\"\n        if lyrics and (\n            m := Lyrics.LINE_PARTS_PAT.match(lyrics.splitlines()[-1])\n        ):\n            ts, _ = m.groups()\n            if ts:\n                mm, ss = map(float, ts.strip(\"[]\").split(\":\"))\n                if 60 * mm + ss <= duration:\n                    return lyrics\n\n        return None\n\n    @classmethod\n    def make(\n        cls, candidate: LRCLibAPI.Item, target_duration: float\n    ) -> LRCLyrics:\n        \"\"\"Build a scored candidate from LRCLib payload data.\"\"\"\n        duration = candidate[\"duration\"] or 0.0\n        return cls(\n            target_duration,\n            candidate[\"id\"],\n            duration,\n            candidate[\"instrumental\"],\n            candidate[\"plainLyrics\"],\n            cls.verify_synced_lyrics(\n                target_duration, candidate[\"syncedLyrics\"]\n            ),\n        )\n\n    @cached_property\n    def duration_dist(self) -> float:\n        \"\"\"Return the absolute difference between lyrics and target duration.\"\"\"\n        return abs(self.duration - self.target_duration)\n\n    @cached_property\n    def is_valid(self) -> bool:\n        \"\"\"Return whether the lyrics item is valid.\n        Lyrics duration must be within the tolerance defined by\n        :attr:`DURATION_DIFF_TOLERANCE`.\n        \"\"\"\n        return (\n            self.duration_dist\n            <= self.target_duration * self.DURATION_DIFF_TOLERANCE\n        )\n\n    @cached_property\n    def dist(self) -> tuple[bool, float]:\n        \"\"\"Distance/score of the given lyrics item.\n\n        Return a tuple with the following values:\n        1. Absolute difference between lyrics and target duration\n        2. Boolean telling whether synced lyrics are available.\n\n        Best lyrics match is the one that has the closest duration to\n        ``target_duration`` and has synced lyrics available.\n        \"\"\"\n        return not self.synced, self.duration_dist\n\n    def get_text(self, want_synced: bool) -> str:\n        \"\"\"Return the preferred text form for this candidate.\"\"\"\n        if self.instrumental:\n            return INSTRUMENTAL_LYRICS\n\n        if want_synced and self.synced:\n            return \"\\n\".join(map(str.strip, self.synced.splitlines()))\n\n        return self.plain\n\n\nclass LRCLib(Backend):\n    \"\"\"Fetch lyrics from the LRCLib API.\"\"\"\n\n    BASE_URL = \"https://lrclib.net/api\"\n    GET_URL = f\"{BASE_URL}/get\"\n    SEARCH_URL = f\"{BASE_URL}/search\"\n\n    def fetch_candidates(\n        self, artist: str, title: str, album: str, length: int\n    ) -> Iterator[list[LRCLibAPI.Item]]:\n        \"\"\"Yield lyrics candidates for the given song data.\n\n        I found that the ``/get`` endpoint sometimes returns inaccurate or\n        unsynced lyrics, while ``search`` yields more suitable candidates.\n        Therefore, we prioritize the latter and rank the results using our own\n        algorithm. If the search does not give suitable lyrics, we fall back to\n        the ``/get`` endpoint.\n\n        Return an iterator over lists of candidates.\n        \"\"\"\n        base_params = {\"artist_name\": artist, \"track_name\": title}\n        get_params = {**base_params, \"duration\": length}\n        if album:\n            get_params[\"album_name\"] = album\n\n        yield self.get_json(self.SEARCH_URL, params=base_params)\n\n        with suppress(HTTPNotFoundError):\n            yield [self.get_json(self.GET_URL, params=get_params)]\n\n    @classmethod\n    def pick_best_match(cls, lyrics: Iterable[LRCLyrics]) -> LRCLyrics | None:\n        \"\"\"Return best matching lyrics item from the given list.\"\"\"\n        return min((li for li in lyrics if li.is_valid), default=None)\n\n    def fetch(\n        self, artist: str, title: str, album: str, length: int\n    ) -> Lyrics | None:\n        \"\"\"Fetch lyrics text for the given song data.\"\"\"\n        evaluate_item = partial(LRCLyrics.make, target_duration=length)\n\n        for group in self.fetch_candidates(artist, title, album, length):\n            candidates = [evaluate_item(item) for item in group]\n            if item := self.pick_best_match(candidates):\n                lyrics = item.get_text(self.config[\"synced\"].get(bool))\n                return Lyrics(\n                    lyrics, self.__class__.name, f\"{self.GET_URL}/{item.id}\"\n                )\n\n        return None\n\n\nclass MusiXmatch(Backend):\n    URL_TEMPLATE = \"https://www.musixmatch.com/lyrics/{}/{}\"\n\n    REPLACEMENTS: ClassVar[dict[str, str]] = {\n        r\"\\s+\": \"-\",\n        \"<\": \"Less_Than\",\n        \">\": \"Greater_Than\",\n        \"#\": \"Number_\",\n        r\"[\\[\\{]\": \"(\",\n        r\"[\\]\\}]\": \")\",\n    }\n\n    @classmethod\n    def encode(cls, text: str) -> str:\n        for old, new in cls.REPLACEMENTS.items():\n            text = re.sub(old, new, text)\n\n        return quote(unidecode(text))\n\n    @classmethod\n    def build_url(cls, *args: str) -> str:\n        return cls.URL_TEMPLATE.format(*map(cls.encode, args))\n\n    def fetch(self, artist: str, title: str, *_) -> Lyrics | None:\n        url = self.build_url(artist, title)\n\n        html = self.get_text(url)\n        if \"We detected that your IP is blocked\" in html:\n            self.warn(\"Failed: Blocked IP address\")\n            return None\n        html_parts = html.split('<p class=\"mxm-lyrics__content')\n        # Sometimes lyrics come in 2 or more parts\n        lyrics_parts = []\n        for html_part in html_parts:\n            lyrics_parts.append(re.sub(r\"^[^>]+>|</p>.*\", \"\", html_part))\n        lyrics = \"\\n\".join(lyrics_parts)\n        lyrics = lyrics.strip(',\"').replace(\"\\\\n\", \"\\n\")\n        # another odd case: sometimes only that string remains, for\n        # missing songs. this seems to happen after being blocked\n        # above, when filling in the CAPTCHA.\n        if \"Instant lyrics for all your music.\" in lyrics:\n            return None\n        # sometimes there are non-existent lyrics with some content\n        if \"Lyrics | Musixmatch\" in lyrics:\n            return None\n        return Lyrics(lyrics, self.__class__.name, url)\n\n\nclass Html:\n    collapse_space = partial(re.compile(r\"(^| ) +\", re.M).sub, r\"\\1\")\n    expand_br = partial(re.compile(r\"\\s*<br[^>]*>\\s*\", re.I).sub, \"\\n\")\n    #: two newlines between paragraphs on the same line (musica, letras.mus.br)\n    merge_blocks = partial(re.compile(r\"(?<!>)</p><p[^>]*>\").sub, \"\\n\\n\")\n    #: a single new line between paragraphs on separate lines\n    #: (paroles.net, sweetslyrics.com, lacoccinelle.net)\n    merge_lines = partial(re.compile(r\"</p>\\s+<p[^>]*>(?!___)\").sub, \"\\n\")\n    #: remove empty divs (lacoccinelle.net)\n    remove_empty_tags = partial(\n        re.compile(r\"(<(div|span)[^>]*>\\s*</\\2>)\").sub, \"\"\n    )\n    #: remove Google Ads tags (musica.com)\n    remove_aside = partial(re.compile(\"<aside .+?</aside>\").sub, \"\")\n    #: remove adslot-Content_1 div from the lyrics text (paroles.net)\n    remove_adslot = partial(\n        re.compile(r\"\\n</div>[^\\n]+-- Content_\\d+ --.*?\\n<div>\", re.S).sub,\n        \"\\n\",\n    )\n    #: remove text formatting (azlyrics.com, lacocinelle.net)\n    remove_formatting = partial(\n        re.compile(r\" *</?(i|em|pre|strong)[^>]*>\").sub, \"\"\n    )\n\n    @classmethod\n    def normalize_space(cls, text: str) -> str:\n        text = unescape(text).replace(\"\\r\", \"\").replace(\"\\xa0\", \" \")\n        return cls.collapse_space(cls.expand_br(text))\n\n    @classmethod\n    def remove_ads(cls, text: str) -> str:\n        return cls.remove_adslot(cls.remove_aside(text))\n\n    @classmethod\n    def merge_paragraphs(cls, text: str) -> str:\n        return cls.merge_blocks(cls.merge_lines(cls.remove_empty_tags(text)))\n\n\nclass SoupMixin:\n    @classmethod\n    def pre_process_html(cls, html: str) -> str:\n        \"\"\"Pre-process the HTML content before scraping.\"\"\"\n        return Html.normalize_space(html)\n\n    @classmethod\n    def get_soup(cls, html: str) -> BeautifulSoup:\n        return BeautifulSoup(cls.pre_process_html(html), \"html.parser\")\n\n\nclass SearchResult(NamedTuple):\n    artist: str\n    title: str\n    url: str\n\n    @property\n    def source(self) -> str:\n        return urlparse(self.url).netloc\n\n\nclass SearchBackend(SoupMixin, Backend):\n    @cached_property\n    def dist_thresh(self) -> float:\n        return self.config[\"dist_thresh\"].get(float)\n\n    def check_match(\n        self, target_artist: str, target_title: str, result: SearchResult\n    ) -> bool:\n        \"\"\"Check if the given search result is a 'good enough' match.\"\"\"\n        max_dist = max(\n            string_dist(target_artist, result.artist),\n            string_dist(target_title, result.title),\n        )\n\n        if (max_dist := round(max_dist, 2)) <= self.dist_thresh:\n            return True\n\n        if math.isclose(max_dist, self.dist_thresh, abs_tol=0.4):\n            # log out the candidate that did not make it but was close.\n            # This may show a matching candidate with some noise in the name\n            self.debug(\n                \"({0.artist}, {0.title}) does not match ({1}, {2}) but dist\"\n                \" was close: {3:.2f}\",\n                result,\n                target_artist,\n                target_title,\n                max_dist,\n            )\n\n        return False\n\n    def search(self, artist: str, title: str) -> Iterable[SearchResult]:\n        \"\"\"Search for the given query and yield search results.\"\"\"\n        raise NotImplementedError\n\n    def get_results(self, artist: str, title: str) -> Iterable[SearchResult]:\n        check_match = partial(self.check_match, artist, title)\n        for candidate in self.search(artist, title):\n            if check_match(candidate):\n                yield candidate\n\n    def fetch(self, artist: str, title: str, *_) -> Lyrics | None:\n        \"\"\"Fetch lyrics for the given artist and title.\"\"\"\n        for result in self.get_results(artist, title):\n            if (html := self.get_text(result.url)) and (\n                lyrics := self.scrape(html)\n            ):\n                return Lyrics(lyrics, self.__class__.name, result.url)\n\n        return None\n\n    @classmethod\n    def scrape(cls, html: str) -> str | None:\n        \"\"\"Scrape the lyrics from the given HTML.\"\"\"\n        raise NotImplementedError\n\n\nclass Genius(SearchBackend):\n    \"\"\"Fetch lyrics from Genius via genius-api.\n\n    Because genius doesn't allow accessing lyrics via the api, we first query\n    the api for a url matching our artist & title, then scrape the HTML text\n    for the JSON data containing the lyrics.\n    \"\"\"\n\n    SEARCH_URL = \"https://api.genius.com/search\"\n    LYRICS_IN_JSON_RE = re.compile(r'(?<=.\\\\\"html\\\\\":\\\\\").*?(?=(?<!\\\\)\\\\\")')\n    remove_backslash = partial(re.compile(r\"\\\\(?=[^\\\\])\").sub, \"\")\n\n    @cached_property\n    def headers(self) -> dict[str, str]:\n        return {\"Authorization\": f\"Bearer {self.config['genius_api_key']}\"}\n\n    def get_json(self, *args, **kwargs) -> GeniusAPI.Search:\n        response: GeniusAPI.Response = super().get_json(*args, **kwargs)\n        if \"response\" in response:\n            return response  # type: ignore[return-value]\n\n        meta = response[\"meta\"]\n        raise GeniusHTTPError(f\"{meta['message']} Status: {meta['status']}\")\n\n    def search(self, artist: str, title: str) -> Iterable[SearchResult]:\n        search_data = self.get_json(\n            self.SEARCH_URL,\n            params={\"q\": f\"{artist} {title}\"},\n            headers=self.headers,\n        )\n        for r in (hit[\"result\"] for hit in search_data[\"response\"][\"hits\"]):\n            yield SearchResult(r[\"artist_names\"], r[\"title\"], r[\"url\"])\n\n    @classmethod\n    def scrape(cls, html: str) -> str | None:\n        if m := cls.LYRICS_IN_JSON_RE.search(html):\n            html_text = cls.remove_backslash(m[0]).replace(r\"\\n\", \"\\n\")\n            return cls.get_soup(html_text).get_text().strip()\n\n        return None\n\n\nclass Tekstowo(SearchBackend):\n    \"\"\"Fetch lyrics from Tekstowo.pl.\"\"\"\n\n    BASE_URL = \"https://www.tekstowo.pl\"\n    SEARCH_URL = f\"{BASE_URL}/szukaj,{{}}.html\"\n\n    def build_url(self, artist, title):\n        artistitle = f\"{artist.title()} {title.title()}\"\n\n        return self.SEARCH_URL.format(quote_plus(unidecode(artistitle)))\n\n    def search(self, artist: str, title: str) -> Iterable[SearchResult]:\n        if html := self.get_text(self.build_url(title, artist)):\n            soup = self.get_soup(html)\n            for tag in soup.select(\"div[class=flex-group] > a[title*=' - ']\"):\n                artist, title = str(tag[\"title\"]).split(\" - \", 1)\n                yield SearchResult(\n                    artist, title, f\"{self.BASE_URL}{tag['href']}\"\n                )\n\n        return None\n\n    @classmethod\n    def scrape(cls, html: str) -> str | None:\n        soup = cls.get_soup(html)\n\n        if lyrics_div := soup.select_one(\"div.song-text > div.inner-text\"):\n            return lyrics_div.get_text()\n\n        return None\n\n\nclass Google(SearchBackend):\n    \"\"\"Fetch lyrics from Google search results.\"\"\"\n\n    SEARCH_URL = \"https://www.googleapis.com/customsearch/v1\"\n\n    #: Exclude some letras.mus.br pages which do not contain lyrics.\n    EXCLUDE_PAGES: ClassVar[list[str]] = [\n        \"significado.html\",\n        \"traduccion.html\",\n        \"traducao.html\",\n        \"significados.html\",\n    ]\n\n    #: Regular expression to match noise in the URL title.\n    URL_TITLE_NOISE_RE = re.compile(\n        r\"\"\"\n\\b\n(\n      paroles(\\ et\\ traduction|\\ de\\ chanson)?\n    | letras?(\\ de)?\n    | liedtexte\n    | dainų\\ žodžiai\n    | original\\ song\\ full\\ text\\.\n    | official\n    | 20[12]\\d\\ version\n    | (absolute\\ |az)?lyrics(\\ complete)?\n    | www\\S+\n    | \\S+\\.(com|net|mus\\.br)\n)\n([^\\w.]|$)\n\"\"\",\n        re.IGNORECASE | re.VERBOSE,\n    )\n    #: Split cleaned up URL title into artist and title parts.\n    URL_TITLE_PARTS_RE = re.compile(r\" +(?:[ :|-]+|par|by) +|, \")\n\n    SOURCE_DIST_FACTOR: ClassVar[dict[str, float]] = {\n        \"www.azlyrics.com\": 0.5,\n        \"www.songlyrics.com\": 0.6,\n    }\n\n    ignored_domains: ClassVar[set[str]] = set()\n\n    @classmethod\n    def pre_process_html(cls, html: str) -> str:\n        \"\"\"Pre-process the HTML content before scraping.\"\"\"\n        html = Html.remove_ads(super().pre_process_html(html))\n        return Html.remove_formatting(Html.merge_paragraphs(html))\n\n    def get_text(self, *args, **kwargs) -> str:\n        \"\"\"Handle an error so that we can continue with the next URL.\"\"\"\n        kwargs.setdefault(\"allow_redirects\", False)\n        with self.handle_request():\n            try:\n                return super().get_text(*args, **kwargs)\n            except CaptchaError:\n                self.ignored_domains.add(urlparse(args[0]).netloc)\n                raise\n\n    @staticmethod\n    def get_part_dist(artist: str, title: str, part: str) -> float:\n        \"\"\"Return the distance between the given part and the artist and title.\n\n        A number between -1 and 1 is returned, where -1 means the part is\n        closer to the artist and 1 means it is closer to the title.\n        \"\"\"\n        return string_dist(artist, part) - string_dist(title, part)\n\n    @classmethod\n    def make_search_result(\n        cls, artist: str, title: str, item: GoogleCustomSearchAPI.Item\n    ) -> SearchResult:\n        \"\"\"Parse artist and title from the URL title and return a search result.\"\"\"\n        url_title = (\n            # get full title from metatags if available\n            item.get(\"pagemap\", {}).get(\"metatags\", [{}])[0].get(\"og:title\")\n            # default to the dispolay title\n            or item[\"title\"]\n        )\n        clean_title = cls.URL_TITLE_NOISE_RE.sub(\"\", url_title).strip(\" .-|\")\n        # split it into parts which may be part of the artist or the title\n        # `dict.fromkeys` removes duplicates keeping the order\n        parts = list(dict.fromkeys(cls.URL_TITLE_PARTS_RE.split(clean_title)))\n\n        if len(parts) == 1:\n            part = parts[0]\n            if m := re.search(rf\"(?i)\\W*({re.escape(title)})\\W*\", part):\n                # artist and title may not have a separator\n                result_title = m[1]\n                result_artist = part.replace(m[0], \"\")\n            else:\n                # assume that this is the title\n                result_artist, result_title = \"\", parts[0]\n        else:\n            # sort parts by their similarity to the artist\n            result_artist = min(parts, key=lambda p: string_dist(artist, p))\n            result_title = min(parts, key=lambda p: string_dist(title, p))\n\n        return SearchResult(result_artist, result_title, item[\"link\"])\n\n    def search(self, artist: str, title: str) -> Iterable[SearchResult]:\n        params = {\n            \"key\": self.config[\"google_API_key\"].as_str(),\n            \"cx\": self.config[\"google_engine_ID\"].as_str(),\n            \"q\": f\"{artist} {title}\",\n            \"siteSearch\": \"www.musixmatch.com\",\n            \"siteSearchFilter\": \"e\",\n            \"excludeTerms\": \", \".join(self.EXCLUDE_PAGES),\n        }\n\n        data: GoogleCustomSearchAPI.Response = self.get_json(\n            self.SEARCH_URL, params=params\n        )\n        for item in data.get(\"items\", []):\n            yield self.make_search_result(artist, title, item)\n\n    def get_results(self, *args) -> Iterable[SearchResult]:\n        \"\"\"Try results from preferred sources first.\"\"\"\n        for result in sorted(\n            super().get_results(*args),\n            key=lambda r: self.SOURCE_DIST_FACTOR.get(r.source, 1),\n        ):\n            if result.source not in self.ignored_domains:\n                yield result\n\n    @classmethod\n    def scrape(cls, html: str) -> str | None:\n        # Get the longest text element (if any).\n        if strings := sorted(cls.get_soup(html).stripped_strings, key=len):\n            return strings[-1]\n\n        return None\n\n\n@dataclass\nclass Translator(LyricsRequestHandler):\n    \"\"\"Translate lyrics text while preserving existing structured metadata.\"\"\"\n\n    TRANSLATE_URL = \"https://api.cognitive.microsofttranslator.com/translate\"\n    SEPARATOR = \" | \"\n\n    _log: Logger\n    api_key: str\n    to_language: str\n    from_languages: list[str]\n\n    @classmethod\n    def from_config(\n        cls,\n        log: Logger,\n        api_key: str,\n        to_language: str,\n        from_languages: list[str] | None = None,\n    ) -> Translator:\n        \"\"\"Construct a translator with normalized language configuration.\"\"\"\n        return cls(\n            log,\n            api_key,\n            to_language.upper(),\n            [x.upper() for x in from_languages or []],\n        )\n\n    def get_translations(self, texts: Iterable[str]) -> list[str]:\n        \"\"\"Return translations for the given texts.\n\n        To reduce the translation 'cost', we translate unique texts, and then\n        map the translations back to the original texts.\n        \"\"\"\n        unique_texts = list(dict.fromkeys(texts))\n        text = self.SEPARATOR.join(unique_texts)\n        data: list[TranslatorAPI.Response] = self.post_json(\n            self.TRANSLATE_URL,\n            headers={\"Ocp-Apim-Subscription-Key\": self.api_key},\n            json=[{\"text\": text}],\n            params={\"api-version\": \"3.0\", \"to\": self.to_language},\n        )\n\n        translated_text = data[0][\"translations\"][0][\"text\"]\n        translations = translated_text.split(self.SEPARATOR)\n        trans_by_text = dict(zip(unique_texts, translations))\n        return [trans_by_text.get(t, \"\") for t in texts]\n\n    def translate(self, lyrics: Lyrics, old_lyrics: Lyrics) -> Lyrics:\n        \"\"\"Translate the given lyrics to the target language.\n\n        Check old lyrics for existing translations and return them if their\n        original text matches the new lyrics. This is to avoid translating\n        the same lyrics multiple times.\n\n        If the lyrics are already in the target language or not in any of\n        of the source languages (if configured), they are returned as is.\n        \"\"\"\n        if (\n            lyrics.original_text\n        ) == old_lyrics.original_text and old_lyrics.translated:\n            self.info(\"🔵 Translations already exist\")\n            return old_lyrics\n\n        if (lyrics_language := lyrics.language) == self.to_language:\n            self.info(\n                \"🔵 Lyrics are already in the target language {.to_language}\",\n                self,\n            )\n        elif (\n            from_lang_config := self.from_languages\n        ) and lyrics_language not in from_lang_config:\n            self.info(\n                \"🔵 Configuration {} does not permit translating from {}\",\n                from_lang_config,\n                lyrics_language,\n            )\n        else:\n            with self.handle_request():\n                lyrics.translations = self.get_translations(lyrics.text_lines)\n                lyrics.translation_language = self.to_language\n                self.info(\"🟢 Translated lyrics to {.to_language}\", self)\n\n        return lyrics\n\n\n@dataclass\nclass RestFiles:\n    # The content for the base index.rst generated in ReST mode.\n    REST_INDEX_TEMPLATE = textwrap.dedent(\"\"\"\n        Lyrics\n        ======\n\n        * :ref:`Song index <genindex>`\n        * :ref:`search`\n\n        Artist index:\n\n        .. toctree::\n           :maxdepth: 1\n           :glob:\n\n           artists/*\n        \"\"\").strip()\n\n    # The content for the base conf.py generated.\n    REST_CONF_TEMPLATE = textwrap.dedent(\"\"\"\n        master_doc = \"index\"\n        project = \"Lyrics\"\n        copyright = \"none\"\n        author = \"Various Authors\"\n        latex_documents = [\n            (master_doc, \"Lyrics.tex\", project, author, \"manual\"),\n        ]\n        epub_exclude_files = [\"search.html\"]\n        epub_tocdepth = 1\n        epub_tocdup = False\n        \"\"\").strip()\n\n    directory: Path\n\n    @cached_property\n    def artists_dir(self) -> Path:\n        dir = self.directory / \"artists\"\n        dir.mkdir(parents=True, exist_ok=True)\n        return dir\n\n    def write_indexes(self) -> None:\n        \"\"\"Write conf.py and index.rst files necessary for Sphinx\n\n        We write minimal configurations that are necessary for Sphinx\n        to operate. We do not overwrite existing files so that\n        customizations are respected.\"\"\"\n        index_file = self.directory / \"index.rst\"\n        if not index_file.exists():\n            index_file.write_text(self.REST_INDEX_TEMPLATE)\n        conf_file = self.directory / \"conf.py\"\n        if not conf_file.exists():\n            conf_file.write_text(self.REST_CONF_TEMPLATE)\n\n    def write_artist(self, artist: str, items: Iterable[Item]) -> None:\n        parts = [\n            f\"{artist}\\n{'=' * len(artist)}\",\n            \".. contents::\\n   :local:\",\n        ]\n        for album, items in groupby(items, key=lambda i: i.album):\n            parts.append(f\"{album}\\n{'-' * len(album)}\")\n            parts.extend(\n                part\n                for i in items\n                if (title := f\":index:`{i.title.strip()}`\")\n                for part in (\n                    f\"{title}\\n{'~' * len(title)}\",\n                    textwrap.indent(i.lyrics, \"| \"),\n                )\n            )\n        file = self.artists_dir / f\"{slug(artist)}.rst\"\n        file.write_text(\"\\n\\n\".join(parts).strip())\n\n    def write(self, items: list[Item]) -> None:\n        self.directory.mkdir(exist_ok=True, parents=True)\n        self.write_indexes()\n\n        items.sort(key=lambda i: i.albumartist)\n        for artist, artist_items in groupby(items, key=lambda i: i.albumartist):\n            self.write_artist(artist.strip(), artist_items)\n\n        d = self.directory\n        text = f\"\"\"\n        ReST files generated. to build, use one of:\n          sphinx-build -b html  {d} {d / \"html\"}\n          sphinx-build -b epub  {d} {d / \"epub\"}\n          sphinx-build -b latex {d} {d / \"latex\"} && make -C {d / \"latex\"} all-pdf\n        \"\"\"\n        ui.print_(textwrap.dedent(text))\n\n\nBACKEND_BY_NAME = {\n    b.name: b for b in [LRCLib, Google, Genius, Tekstowo, MusiXmatch]\n}\n\n\nclass LyricsPlugin(LyricsRequestHandler, plugins.BeetsPlugin):\n    item_types: ClassVar[dict[str, types.Type]] = {\n        \"lyrics_url\": types.STRING,\n        \"lyrics_backend\": types.STRING,\n        \"lyrics_language\": types.STRING,\n        \"lyrics_translation_language\": types.STRING,\n    }\n\n    @cached_property\n    def backends(self) -> list[Backend]:\n        user_sources = self.config[\"sources\"].as_str_seq()\n\n        chosen = sanitize_choices(user_sources, BACKEND_BY_NAME)\n        if \"google\" in chosen and not self.config[\"google_API_key\"].get():\n            self.warn(\"Disabling Google source: no API key configured.\")\n            chosen.remove(\"google\")\n\n        return [BACKEND_BY_NAME[c](self.config, self._log) for c in chosen]\n\n    @cached_property\n    def translator(self) -> Translator | None:\n        config = self.config[\"translate\"]\n        if config[\"api_key\"].get() and config[\"to_language\"].get():\n            return Translator.from_config(self._log, **config.flatten())\n        return None\n\n    def __init__(self):\n        super().__init__()\n        self.config.add(\n            {\n                \"auto\": True,\n                \"translate\": {\n                    \"api_key\": None,\n                    \"from_languages\": [],\n                    \"to_language\": None,\n                },\n                \"dist_thresh\": 0.11,\n                \"google_API_key\": None,\n                \"google_engine_ID\": \"009217259823014548361:lndtuqkycfu\",\n                \"genius_api_key\": (\n                    \"Ryq93pUGm8bM6eUWwD_M3NOFFDAtp2yEE7W\"\n                    \"76V-uFL5jks5dNvcGCdarqFjDhP9c\"\n                ),\n                \"fallback\": None,\n                \"force\": False,\n                \"local\": False,\n                \"print\": False,\n                \"synced\": False,\n                # Musixmatch and Tekstowo are disabled by default as they\n                # currently block requests with the beets user agent.\n                \"sources\": [\n                    n\n                    for n in BACKEND_BY_NAME\n                    if n not in {\"musixmatch\", \"tekstowo\"}\n                ],\n            }\n        )\n        self.config[\"translate\"][\"api_key\"].redact = True\n        self.config[\"google_API_key\"].redact = True\n        self.config[\"google_engine_ID\"].redact = True\n        self.config[\"genius_api_key\"].redact = True\n\n        if self.config[\"auto\"]:\n            self.import_stages = [self.imported]\n\n    def commands(self):\n        cmd = ui.Subcommand(\"lyrics\", help=\"fetch song lyrics\")\n        cmd.parser.add_option(\n            \"-p\",\n            \"--print\",\n            action=\"store_true\",\n            default=self.config[\"print\"].get(),\n            help=\"print lyrics to console\",\n        )\n        cmd.parser.add_option(\n            \"-r\",\n            \"--write-rest\",\n            dest=\"rest_directory\",\n            action=\"store\",\n            default=None,\n            metavar=\"dir\",\n            help=\"write lyrics to given directory as ReST files\",\n        )\n        cmd.parser.add_option(\n            \"-f\",\n            \"--force\",\n            action=\"store_true\",\n            default=self.config[\"force\"].get(),\n            help=\"always re-download lyrics\",\n        )\n        cmd.parser.add_option(\n            \"-l\",\n            \"--local\",\n            action=\"store_true\",\n            default=self.config[\"local\"].get(),\n            help=\"do not fetch missing lyrics\",\n        )\n\n        def func(lib: Library, opts, args) -> None:\n            # The \"write to files\" option corresponds to the\n            # import_write config value.\n            self.config.set(vars(opts))\n            items = list(lib.items(args))\n            for item in items:\n                self.add_item_lyrics(item, ui.should_write())\n                if item.lyrics and opts.print:\n                    ui.print_(item.lyrics)\n\n            if opts.rest_directory and (\n                items := [i for i in items if i.lyrics]\n            ):\n                RestFiles(Path(opts.rest_directory)).write(items)\n\n        cmd.func = func\n        return [cmd]\n\n    def imported(self, _, task: ImportTask) -> None:\n        \"\"\"Import hook for fetching lyrics automatically.\"\"\"\n        for item in task.imported_items():\n            self.add_item_lyrics(item, False)\n\n    def find_lyrics(self, item: Item) -> Lyrics | None:\n        \"\"\"Return the first lyrics match from the configured source search.\"\"\"\n        album, length = item.album, round(item.length)\n        matches = (\n            self.get_lyrics(a, t, album, length)\n            for a, titles in search_pairs(item)\n            for t in titles\n        )\n\n        return next(filter(None, matches), None)\n\n    def add_item_lyrics(self, item: Item, write: bool) -> None:\n        \"\"\"Fetch and store lyrics for a single item. If ``write``, then the\n        lyrics will also be written to the file itself.\n        \"\"\"\n        if self.config[\"local\"]:\n            return\n\n        if not self.config[\"force\"] and item.lyrics:\n            self.info(\"🔵 Lyrics already present: {}\", item)\n            return\n\n        existing_lyrics = Lyrics.from_item(item)\n        if new_lyrics := self.find_lyrics(item):\n            self.info(\"🟢 Found lyrics: {}\", item)\n            if translator := self.translator:\n                new_lyrics = translator.translate(new_lyrics, existing_lyrics)\n\n            synced_mode = self.config[\"synced\"].get(bool)\n            if synced_mode and existing_lyrics.synced and not new_lyrics.synced:\n                self.info(\n                    \"🔴 Not updating synced lyrics with non-synced ones: {}\",\n                    item,\n                )\n                return\n\n            for key in (\"backend\", \"url\", \"language\", \"translation_language\"):\n                item_key = f\"lyrics_{key}\"\n                if value := getattr(new_lyrics, key):\n                    item[item_key] = value\n                elif item_key in item:\n                    del item[item_key]\n\n            lyrics_text = new_lyrics.full_text\n        else:\n            self.info(\"🔴 Lyrics not found: {}\", item)\n            lyrics_text = self.config[\"fallback\"].get()\n\n        if lyrics_text not in {None, item.lyrics}:\n            item.lyrics = lyrics_text\n            item.store()\n            if write:\n                item.try_write()\n\n    def get_lyrics(self, artist: str, title: str, *args) -> Lyrics | None:\n        \"\"\"Get first found lyrics, trying each source in turn.\"\"\"\n        self.info(\"Fetching lyrics for {} - {}\", artist, title)\n        for backend in self.backends:\n            with backend.handle_request():\n                if lyrics_info := backend.fetch(artist, title, *args):\n                    return lyrics_info\n\n        return None\n"
  },
  {
    "path": "beetsplug/mbcollection.py",
    "content": "# This file is part of beets.\n# Copyright (c) 2011, Jeffrey Aylesworth <mail@jeffrey.red>\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nfrom __future__ import annotations\n\nimport re\nfrom dataclasses import dataclass, field\nfrom functools import cached_property\nfrom typing import TYPE_CHECKING, ClassVar\n\nfrom requests.auth import HTTPDigestAuth\n\nfrom beets import __version__, config, ui\nfrom beets.plugins import BeetsPlugin\nfrom beets.ui import Subcommand\n\nfrom ._utils.musicbrainz import MusicBrainzAPI\n\nif TYPE_CHECKING:\n    from collections.abc import Iterable, Iterator\n\n    from requests import Response\n\n    from beets.importer import ImportSession, ImportTask\n    from beets.library import Album, Library\n\n    from ._typing import JSONDict\n\nUUID_PAT = re.compile(r\"^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$\")\n\n\n@dataclass\nclass MusicBrainzUserAPI(MusicBrainzAPI):\n    \"\"\"MusicBrainz API client with user authentication.\n\n    In order to retrieve private user collections and modify them, we need to\n    authenticate the requests with the user's MusicBrainz credentials.\n\n    See documentation for authentication details:\n        https://musicbrainz.org/doc/MusicBrainz_API#Authentication\n\n    Note that the documentation misleadingly states HTTP 'basic' authentication,\n    and I had to reverse-engineer musicbrainzngs to discover that it actually\n    uses HTTP 'digest' authentication.\n    \"\"\"\n\n    auth: HTTPDigestAuth = field(init=False)\n\n    def __post_init__(self) -> None:\n        super().__post_init__()\n        config[\"musicbrainz\"][\"pass\"].redact = True\n        self.auth = HTTPDigestAuth(\n            config[\"musicbrainz\"][\"user\"].as_str(),\n            config[\"musicbrainz\"][\"pass\"].as_str(),\n        )\n\n    def request(self, *args, **kwargs) -> Response:\n        \"\"\"Authenticate and include required client param in all requests.\"\"\"\n        kwargs.setdefault(\"params\", {})\n        kwargs[\"params\"][\"client\"] = f\"beets-{__version__}\"\n        kwargs[\"auth\"] = self.auth\n        return super().request(*args, **kwargs)\n\n    def browse_collections(self) -> list[JSONDict]:\n        \"\"\"Get all collections for the authenticated user.\"\"\"\n        return self._browse(\"collection\")\n\n\n@dataclass\nclass MBCollection:\n    \"\"\"Representation of a user's MusicBrainz collection.\n\n    Provides convenient, chunked operations for retrieving releases and updating\n    the collection via the MusicBrainz web API. Fetch and submission limits are\n    controlled by class-level constants to avoid oversized requests.\n    \"\"\"\n\n    SUBMISSION_CHUNK_SIZE: ClassVar[int] = 200\n    FETCH_CHUNK_SIZE: ClassVar[int] = 100\n\n    data: JSONDict\n    mb_api: MusicBrainzUserAPI\n\n    @property\n    def id(self) -> str:\n        \"\"\"Unique identifier assigned to the collection by MusicBrainz.\"\"\"\n        return self.data[\"id\"]\n\n    @property\n    def release_count(self) -> int:\n        \"\"\"Total number of releases recorded in the collection.\"\"\"\n        return self.data[\"release-count\"]\n\n    @property\n    def releases_url(self) -> str:\n        \"\"\"Complete API endpoint URL for listing releases in this collection.\"\"\"\n        return f\"{self.mb_api.api_root}/collection/{self.id}/releases\"\n\n    @property\n    def releases(self) -> list[JSONDict]:\n        \"\"\"Retrieve all releases in the collection, fetched in successive pages.\n\n        The fetch is performed in chunks and returns a flattened sequence of\n        release records.\n        \"\"\"\n        offsets = list(range(0, self.release_count, self.FETCH_CHUNK_SIZE))\n        return [r for offset in offsets for r in self.get_releases(offset)]\n\n    def get_releases(self, offset: int) -> list[JSONDict]:\n        \"\"\"Fetch a single page of releases beginning at a given position.\"\"\"\n        return self.mb_api.get_json(\n            self.releases_url,\n            params={\"limit\": self.FETCH_CHUNK_SIZE, \"offset\": offset},\n        )[\"releases\"]\n\n    @classmethod\n    def get_id_chunks(cls, id_list: list[str]) -> Iterator[list[str]]:\n        \"\"\"Yield successive sublists of identifiers sized for safe submission.\n\n        Splits a long sequence of identifiers into batches that respect the\n        service's submission limits to avoid oversized requests.\n        \"\"\"\n        for i in range(0, len(id_list), cls.SUBMISSION_CHUNK_SIZE):\n            yield id_list[i : i + cls.SUBMISSION_CHUNK_SIZE]\n\n    def add_releases(self, releases: list[str]) -> None:\n        \"\"\"Add releases to the collection in batches.\"\"\"\n        for chunk in self.get_id_chunks(releases):\n            # Need to escape semicolons: https://github.com/psf/requests/issues/6990\n            self.mb_api.put(f\"{self.releases_url}/{'%3B'.join(chunk)}\")\n\n    def remove_releases(self, releases: list[str]) -> None:\n        \"\"\"Remove releases from the collection in chunks.\"\"\"\n        for chunk in self.get_id_chunks(releases):\n            # Need to escape semicolons: https://github.com/psf/requests/issues/6990\n            self.mb_api.delete(f\"{self.releases_url}/{'%3B'.join(chunk)}\")\n\n\ndef submit_albums(collection: MBCollection, release_ids):\n    \"\"\"Add all of the release IDs to the indicated collection. Multiple\n    requests are made if there are many release IDs to submit.\n    \"\"\"\n    collection.add_releases(release_ids)\n\n\nclass MusicBrainzCollectionPlugin(BeetsPlugin):\n    def __init__(self) -> None:\n        super().__init__()\n        self.config.add(\n            {\n                \"auto\": False,\n                \"collection\": \"\",\n                \"remove\": False,\n            }\n        )\n        if self.config[\"auto\"]:\n            self.import_stages = [self.imported]\n\n    @cached_property\n    def mb_api(self) -> MusicBrainzUserAPI:\n        return MusicBrainzUserAPI()\n\n    @cached_property\n    def collection(self) -> MBCollection:\n        if not (collections := self.mb_api.browse_collections()):\n            raise ui.UserError(\"no collections exist for user\")\n\n        # Get all release collection IDs, avoiding event collections\n        if not (\n            collection_by_id := {\n                c[\"id\"]: c for c in collections if c[\"entity-type\"] == \"release\"\n            }\n        ):\n            raise ui.UserError(\"No release collection found.\")\n\n        # Check that the collection exists so we can present a nice error\n        if collection_id := self.config[\"collection\"].as_str():\n            if not (collection := collection_by_id.get(collection_id)):\n                raise ui.UserError(f\"invalid collection ID: {collection_id}\")\n        else:\n            # No specified collection. Just return the first collection ID\n            collection = next(iter(collection_by_id.values()))\n\n        return MBCollection(collection, self.mb_api)\n\n    def commands(self):\n        mbupdate = Subcommand(\"mbupdate\", help=\"Update MusicBrainz collection\")\n        mbupdate.parser.add_option(\n            \"-r\",\n            \"--remove\",\n            action=\"store_true\",\n            default=None,\n            dest=\"remove\",\n            help=\"Remove albums not in beets library\",\n        )\n        mbupdate.func = self.update_collection\n        return [mbupdate]\n\n    def update_collection(self, lib: Library, opts, args) -> None:\n        self.config.set_args(opts)\n        remove_missing = self.config[\"remove\"].get(bool)\n        self.update_album_list(lib, lib.albums(), remove_missing)\n\n    def imported(self, session: ImportSession, task: ImportTask) -> None:\n        \"\"\"Add each imported album to the collection.\"\"\"\n        if task.is_album:\n            self.update_album_list(\n                session.lib, [task.album], remove_missing=False\n            )\n\n    def update_album_list(\n        self, lib: Library, albums: Iterable[Album], remove_missing: bool\n    ) -> None:\n        \"\"\"Update the MusicBrainz collection from a list of Beets albums\"\"\"\n        collection = self.collection\n\n        # Get a list of all the album IDs.\n        album_ids = [id_ for a in albums if UUID_PAT.match(id_ := a.mb_albumid)]\n\n        # Submit to MusicBrainz.\n        self._log.info(\"Updating MusicBrainz collection {}...\", collection.id)\n        collection.add_releases(album_ids)\n        if remove_missing:\n            lib_ids = {x.mb_albumid for x in lib.albums()}\n            albums_in_collection = {r[\"id\"] for r in collection.releases}\n            collection.remove_releases(list(albums_in_collection - lib_ids))\n\n        self._log.info(\"...MusicBrainz collection updated.\")\n"
  },
  {
    "path": "beetsplug/mbpseudo.py",
    "content": "# This file is part of beets.\n# Copyright 2025, Alexis Sarda-Espinosa.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Adds pseudo-releases from MusicBrainz as candidates during import.\"\"\"\n\nfrom __future__ import annotations\n\nimport itertools\nfrom copy import deepcopy\nfrom typing import TYPE_CHECKING, Any\n\nimport mediafile\nfrom typing_extensions import override\n\nfrom beets import config\nfrom beets.autotag.distance import distance\nfrom beets.autotag.hooks import AlbumInfo\nfrom beets.autotag.match import assign_items\nfrom beets.plugins import find_plugins\nfrom beets.util.id_extractors import extract_release_id\nfrom beetsplug.musicbrainz import (\n    MusicBrainzPlugin,\n    _merge_pseudo_and_actual_album,\n    _preferred_alias,\n)\n\nif TYPE_CHECKING:\n    from collections.abc import Iterable, Sequence\n\n    from beets.autotag import AlbumMatch\n    from beets.autotag.distance import Distance\n    from beets.library import Item\n    from beetsplug._typing import JSONDict\n\n_STATUS_PSEUDO = \"Pseudo-Release\"\n\n\nclass MusicBrainzPseudoReleasePlugin(MusicBrainzPlugin):\n    def __init__(self) -> None:\n        super().__init__()\n\n        self.config.add(\n            {\n                \"scripts\": [],\n                \"custom_tags_only\": False,\n                \"album_custom_tags\": {\n                    \"album_transl\": \"album\",\n                    \"album_artist_transl\": \"artist\",\n                },\n                \"track_custom_tags\": {\n                    \"title_transl\": \"title\",\n                    \"artist_transl\": \"artist\",\n                },\n            }\n        )\n\n        self._scripts = self.config[\"scripts\"].as_str_seq()\n        self._log.debug(\"Desired scripts: {0}\", self._scripts)\n\n        album_custom_tags = self.config[\"album_custom_tags\"].get().keys()\n        track_custom_tags = self.config[\"track_custom_tags\"].get().keys()\n        self._log.debug(\n            \"Custom tags for albums and tracks: {0} + {1}\",\n            album_custom_tags,\n            track_custom_tags,\n        )\n        for custom_tag in album_custom_tags | track_custom_tags:\n            if not isinstance(custom_tag, str):\n                continue\n\n            media_field = mediafile.MediaField(\n                mediafile.MP3DescStorageStyle(custom_tag),\n                mediafile.MP4StorageStyle(\n                    f\"----:com.apple.iTunes:{custom_tag}\"\n                ),\n                mediafile.StorageStyle(custom_tag),\n                mediafile.ASFStorageStyle(custom_tag),\n            )\n            try:\n                self.add_media_field(custom_tag, media_field)\n            except ValueError:\n                # ignore errors due to duplicates\n                pass\n\n        self.register_listener(\"pluginload\", self._on_plugins_loaded)\n        self.register_listener(\"album_matched\", self._adjust_final_album_match)\n\n    # noinspection PyMethodMayBeStatic\n    def _on_plugins_loaded(self):\n        for plugin in find_plugins():\n            if isinstance(plugin, MusicBrainzPlugin) and not isinstance(\n                plugin, MusicBrainzPseudoReleasePlugin\n            ):\n                raise RuntimeError(\n                    \"The musicbrainz plugin should not be enabled together with\"\n                    \" the mbpseudo plugin\"\n                )\n\n    @override\n    def candidates(\n        self,\n        items: Sequence[Item],\n        artist: str,\n        album: str,\n        va_likely: bool,\n    ) -> Iterable[AlbumInfo]:\n        if len(self._scripts) == 0:\n            yield from super().candidates(items, artist, album, va_likely)\n        else:\n            for album_info in super().candidates(\n                items, artist, album, va_likely\n            ):\n                if isinstance(album_info, PseudoAlbumInfo):\n                    self._log.debug(\n                        \"Using {0} release for distance calculations for album {1}\",\n                        album_info.determine_best_ref(items),\n                        album_info.album_id,\n                    )\n                    yield album_info  # first yield pseudo to give it priority\n                    yield album_info.get_official_release()\n                else:\n                    yield album_info\n\n    @override\n    def album_info(self, release: JSONDict) -> AlbumInfo:\n        official_release = super().album_info(release)\n\n        if release.get(\"status\") == _STATUS_PSEUDO:\n            return official_release\n\n        if (ids := self._intercept_mb_release(release)) and (\n            album_id := self._extract_id(ids[0])\n        ):\n            raw_pseudo_release = self.mb_api.get_release(album_id)\n            pseudo_release = super().album_info(raw_pseudo_release)\n\n            if self.config[\"custom_tags_only\"].get(bool):\n                self._replace_artist_with_alias(\n                    raw_pseudo_release, pseudo_release\n                )\n                self._add_custom_tags(official_release, pseudo_release)\n                return official_release\n            else:\n                return PseudoAlbumInfo(\n                    pseudo_release=_merge_pseudo_and_actual_album(\n                        pseudo_release, official_release\n                    ),\n                    official_release=official_release,\n                )\n        else:\n            return official_release\n\n    def _intercept_mb_release(self, data: JSONDict) -> list[str]:\n        album_id = data[\"id\"] if \"id\" in data else None\n        if self._has_desired_script(data) or not isinstance(album_id, str):\n            return []\n\n        return [\n            pr_id\n            for rel in data.get(\"release-relations\", [])\n            if (pr_id := self._wanted_pseudo_release_id(album_id, rel))\n            is not None\n        ]\n\n    def _has_desired_script(self, release: JSONDict) -> bool:\n        if len(self._scripts) == 0:\n            return False\n        elif script := release.get(\"text-representation\", {}).get(\"script\"):\n            return script in self._scripts\n        else:\n            return False\n\n    def _wanted_pseudo_release_id(\n        self,\n        album_id: str,\n        relation: JSONDict,\n    ) -> str | None:\n        if (\n            len(self._scripts) == 0\n            or relation.get(\"type\", \"\") != \"transl-tracklisting\"\n            or relation.get(\"direction\", \"\") != \"forward\"\n            or \"release\" not in relation\n        ):\n            return None\n\n        release = relation[\"release\"]\n        if \"id\" in release and self._has_desired_script(release):\n            self._log.debug(\n                \"Adding pseudo-release {0} for main release {1}\",\n                release[\"id\"],\n                album_id,\n            )\n            return release[\"id\"]\n        else:\n            return None\n\n    def _replace_artist_with_alias(\n        self,\n        raw_pseudo_release: JSONDict,\n        pseudo_release: AlbumInfo,\n    ):\n        \"\"\"Use the pseudo-release's language to search for artist\n        alias if the user hasn't configured import languages.\"\"\"\n\n        if len(config[\"import\"][\"languages\"].as_str_seq()) > 0:\n            return\n\n        lang = raw_pseudo_release.get(\"text-representation\", {}).get(\"language\")\n        artist_credits = raw_pseudo_release.get(\"release-group\", {}).get(\n            \"artist-credit\", []\n        )\n        aliases = [\n            artist_credit.get(\"artist\", {}).get(\"aliases\", [])\n            for artist_credit in artist_credits\n        ]\n\n        if lang and len(lang) >= 2 and len(aliases) > 0:\n            locale = lang[0:2]\n            aliases_flattened = list(itertools.chain.from_iterable(aliases))\n            self._log.debug(\n                \"Using locale '{0}' to search aliases {1}\",\n                locale,\n                aliases_flattened,\n            )\n            if alias_dict := _preferred_alias(aliases_flattened, [locale]):\n                if alias := alias_dict.get(\"name\"):\n                    self._log.debug(\"Got alias '{0}'\", alias)\n                    pseudo_release.artist = alias\n                    for track in pseudo_release.tracks:\n                        track.artist = alias\n\n    def _add_custom_tags(\n        self,\n        official_release: AlbumInfo,\n        pseudo_release: AlbumInfo,\n    ):\n        for tag_key, pseudo_key in (\n            self.config[\"album_custom_tags\"].get().items()\n        ):\n            official_release[tag_key] = pseudo_release[pseudo_key]\n\n        track_custom_tags = self.config[\"track_custom_tags\"].get().items()\n        for track, pseudo_track in zip(\n            official_release.tracks, pseudo_release.tracks\n        ):\n            for tag_key, pseudo_key in track_custom_tags:\n                track[tag_key] = pseudo_track[pseudo_key]\n\n    def _adjust_final_album_match(self, match: AlbumMatch):\n        album_info = match.info\n        if isinstance(album_info, PseudoAlbumInfo):\n            self._log.debug(\n                \"Switching {0} to pseudo-release source for final proposal\",\n                album_info.album_id,\n            )\n            album_info.use_pseudo_as_ref()\n            new_pairs, *_ = assign_items(match.items, album_info.tracks)\n            album_info.mapping = dict(new_pairs)\n\n        if album_info.data_source == self.data_source:\n            album_info.data_source = \"MusicBrainz\"\n\n    @override\n    def _extract_id(self, url: str) -> str | None:\n        return extract_release_id(\"MusicBrainz\", url)\n\n\nclass PseudoAlbumInfo(AlbumInfo):\n    \"\"\"This is a not-so-ugly hack.\n\n    We want the pseudo-release to result in a distance that is lower or equal to that of\n    the official release, otherwise it won't qualify as a good candidate. However, if\n    the input is in a script that's different from the pseudo-release (and we want to\n    translate/transliterate it in the library), it will receive unwanted penalties.\n\n    This class is essentially a view of the ``AlbumInfo`` of both official and\n    pseudo-releases, where it's possible to change the details that are exposed to other\n    parts of the auto-tagger, enabling a \"fair\" distance calculation based on the\n    current input's script but still preferring the translation/transliteration in the\n    final proposal.\n    \"\"\"\n\n    def __init__(\n        self,\n        pseudo_release: AlbumInfo,\n        official_release: AlbumInfo,\n        **kwargs,\n    ):\n        super().__init__(pseudo_release.tracks, **kwargs)\n        self.__dict__[\"_pseudo_source\"] = True\n        self.__dict__[\"_official_release\"] = official_release\n        for k, v in pseudo_release.items():\n            if k not in kwargs:\n                self[k] = v\n\n    def get_official_release(self) -> AlbumInfo:\n        return self.__dict__[\"_official_release\"]\n\n    def determine_best_ref(self, items: Sequence[Item]) -> str:\n        self.use_pseudo_as_ref()\n        pseudo_dist = self._compute_distance(items)\n\n        self.use_official_as_ref()\n        official_dist = self._compute_distance(items)\n\n        if official_dist < pseudo_dist:\n            self.use_official_as_ref()\n            return \"official\"\n        else:\n            self.use_pseudo_as_ref()\n            return \"pseudo\"\n\n    def _compute_distance(self, items: Sequence[Item]) -> Distance:\n        mapping, _, _ = assign_items(items, self.tracks)\n        return distance(items, self, mapping)\n\n    def use_pseudo_as_ref(self):\n        self.__dict__[\"_pseudo_source\"] = True\n\n    def use_official_as_ref(self):\n        self.__dict__[\"_pseudo_source\"] = False\n\n    def __getattr__(self, attr: str) -> Any:\n        # ensure we don't duplicate an official release's id, always return pseudo's\n        if self.__dict__[\"_pseudo_source\"] or attr == \"album_id\":\n            return super().__getattr__(attr)\n        else:\n            return self.__dict__[\"_official_release\"].__getattr__(attr)\n\n    def __deepcopy__(self, memo):\n        cls = self.__class__\n        result = cls.__new__(cls)\n\n        memo[id(self)] = result\n        result.__dict__.update(self.__dict__)\n        for k, v in self.items():\n            result[k] = deepcopy(v, memo)\n\n        return result\n"
  },
  {
    "path": "beetsplug/mbsubmit.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson and Diego Moreda.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Aid in submitting information to MusicBrainz.\n\nThis plugin allows the user to print track information in a format that is\nparseable by the MusicBrainz track parser [1]. Programmatic submitting is not\nimplemented by MusicBrainz yet.\n\n[1] https://wiki.musicbrainz.org/History:How_To_Parse_Track_Listings\n\"\"\"\n\nimport subprocess\n\nfrom beets import ui\nfrom beets.autotag import Recommendation\nfrom beets.plugins import BeetsPlugin\nfrom beets.util import PromptChoice, displayable_path\nfrom beetsplug.info import print_data\n\n\nclass MBSubmitPlugin(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n\n        self.config.add(\n            {\n                \"format\": \"$track. $title - $artist ($length)\",\n                \"threshold\": \"medium\",\n                \"picard_path\": \"picard\",\n            }\n        )\n\n        # Validate and store threshold.\n        self.threshold = self.config[\"threshold\"].as_choice(\n            {\n                \"none\": Recommendation.none,\n                \"low\": Recommendation.low,\n                \"medium\": Recommendation.medium,\n                \"strong\": Recommendation.strong,\n            }\n        )\n\n        self.register_listener(\n            \"before_choose_candidate\", self.before_choose_candidate_event\n        )\n\n    def before_choose_candidate_event(self, session, task):\n        if task.rec <= self.threshold:\n            return [\n                PromptChoice(\"p\", \"Print tracks\", self.print_tracks),\n                PromptChoice(\"o\", \"Open files with Picard\", self.picard),\n            ]\n\n    def picard(self, session, task):\n        paths = []\n        for p in task.paths:\n            paths.append(displayable_path(p))\n        try:\n            picard_path = self.config[\"picard_path\"].as_str()\n            subprocess.Popen([picard_path, *paths])\n            self._log.info(\"launched picard from\\n{}\", picard_path)\n        except OSError as exc:\n            self._log.error(\"Could not open picard, got error:\\n{}\", exc)\n\n    def print_tracks(self, session, task):\n        for i in sorted(task.items, key=lambda i: i.track):\n            print_data(None, i, self.config[\"format\"].as_str())\n\n    def commands(self):\n        \"\"\"Add beet UI commands for mbsubmit.\"\"\"\n        mbsubmit_cmd = ui.Subcommand(\n            \"mbsubmit\", help=\"Submit Tracks to MusicBrainz\"\n        )\n\n        def func(lib, opts, args):\n            items = lib.items(args)\n            self._mbsubmit(items)\n\n        mbsubmit_cmd.func = func\n\n        return [mbsubmit_cmd]\n\n    def _mbsubmit(self, items):\n        \"\"\"Print track information to be submitted to MusicBrainz.\"\"\"\n        for i in sorted(items, key=lambda i: i.track):\n            print_data(None, i, self.config[\"format\"].as_str())\n"
  },
  {
    "path": "beetsplug/mbsync.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Jakob Schnitzer.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Synchronise library metadata with metadata source backends.\"\"\"\n\nfrom collections import defaultdict\n\nfrom beets import autotag, library, metadata_plugins, ui, util\nfrom beets.plugins import BeetsPlugin, apply_item_changes\n\n\nclass MBSyncPlugin(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n\n    def commands(self):\n        cmd = ui.Subcommand(\"mbsync\", help=\"update metadata from musicbrainz\")\n        cmd.parser.add_option(\n            \"-p\",\n            \"--pretend\",\n            action=\"store_true\",\n            help=\"show all changes but do nothing\",\n        )\n        cmd.parser.add_option(\n            \"-m\",\n            \"--move\",\n            action=\"store_true\",\n            dest=\"move\",\n            help=\"move files in the library directory\",\n        )\n        cmd.parser.add_option(\n            \"-M\",\n            \"--nomove\",\n            action=\"store_false\",\n            dest=\"move\",\n            help=\"don't move files in library\",\n        )\n        cmd.parser.add_option(\n            \"-W\",\n            \"--nowrite\",\n            action=\"store_false\",\n            default=None,\n            dest=\"write\",\n            help=\"don't write updated metadata to files\",\n        )\n        cmd.parser.add_format_option()\n        cmd.func = self.func\n        return [cmd]\n\n    def func(self, lib, opts, args):\n        \"\"\"Command handler for the mbsync function.\"\"\"\n        move = ui.should_move(opts.move)\n        pretend = opts.pretend\n        write = ui.should_write(opts.write)\n\n        self.singletons(lib, args, move, pretend, write)\n        self.albums(lib, args, move, pretend, write)\n\n    def singletons(self, lib, query, move, pretend, write):\n        \"\"\"Retrieve and apply info from the autotagger for items matched by\n        query.\n        \"\"\"\n        for item in lib.items([*query, \"singleton:true\"]):\n            if not (track_id := item.mb_trackid):\n                self._log.info(\n                    \"Skipping singleton with no mb_trackid: {}\", item\n                )\n                continue\n\n            if not (\n                track_info := metadata_plugins.track_for_id(\n                    track_id, item.get(\"data_source\", \"MusicBrainz\")\n                )\n            ):\n                self._log.info(\n                    \"Recording ID not found: {} for track {}\", track_id, item\n                )\n                continue\n\n            # Apply.\n            with lib.transaction():\n                autotag.apply_item_metadata(item, track_info)\n                apply_item_changes(lib, item, move, pretend, write)\n\n    def albums(self, lib, query, move, pretend, write):\n        \"\"\"Retrieve and apply info from the autotagger for albums matched by\n        query and their items.\n        \"\"\"\n        # Process matching albums.\n        for album in lib.albums(query):\n            if not (album_id := album.mb_albumid):\n                self._log.info(\"Skipping album with no mb_albumid: {}\", album)\n                continue\n\n            data_source = album.get(\"data_source\") or album.items()[0].get(\n                \"data_source\", \"MusicBrainz\"\n            )\n            if not (\n                album_info := metadata_plugins.album_for_id(\n                    album_id, data_source\n                )\n            ):\n                self._log.info(\n                    \"Release ID {} not found for album {}\", album_id, album\n                )\n                continue\n\n            # Map release track and recording MBIDs to their information.\n            # Recordings can appear multiple times on a release, so each MBID\n            # maps to a list of TrackInfo objects.\n            releasetrack_index = {}\n            track_index = defaultdict(list)\n            for track_info in album_info.tracks:\n                releasetrack_index[track_info.release_track_id] = track_info\n                track_index[track_info.track_id].append(track_info)\n\n            # Construct a track mapping according to MBIDs (release track MBIDs\n            # first, if available, and recording MBIDs otherwise). This should\n            # work for albums that have missing or extra tracks.\n            item_info_pairs = []\n            items = list(album.items())\n            for item in items:\n                if (\n                    item.mb_releasetrackid\n                    and item.mb_releasetrackid in releasetrack_index\n                ):\n                    item_info_pairs.append(\n                        (item, releasetrack_index[item.mb_releasetrackid])\n                    )\n                else:\n                    candidates = track_index[item.mb_trackid]\n                    if len(candidates) == 1:\n                        item_info_pairs.append((item, candidates[0]))\n                    else:\n                        # If there are multiple copies of a recording, they are\n                        # disambiguated using their disc and track number.\n                        for c in candidates:\n                            if (\n                                c.medium_index == item.track\n                                and c.medium == item.disc\n                            ):\n                                item_info_pairs.append((item, c))\n                                break\n\n            # Apply.\n            self._log.debug(\"applying changes to {}\", album)\n            with lib.transaction():\n                autotag.apply_metadata(album_info, item_info_pairs)\n                changed = False\n                # Find any changed item to apply changes to album.\n                any_changed_item = items[0]\n                for item in items:\n                    item_changed = ui.show_model_changes(item)\n                    changed |= item_changed\n                    if item_changed:\n                        any_changed_item = item\n                        apply_item_changes(lib, item, move, pretend, write)\n\n                if not changed:\n                    # No change to any item.\n                    continue\n\n                if not pretend:\n                    # Update album structure to reflect an item in it.\n                    for key in library.Album.item_keys:\n                        album[key] = any_changed_item[key]\n                    album.store()\n\n                    # Move album art (and any inconsistent items).\n                    if move and lib.directory in util.ancestry(items[0].path):\n                        self._log.debug(\"moving album {}\", album)\n                        album.move()\n"
  },
  {
    "path": "beetsplug/metasync/__init__.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Heinz Wiesinger.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Synchronize information from music player libraries\"\"\"\n\nfrom __future__ import annotations\n\nfrom abc import ABCMeta, abstractmethod\nfrom importlib import import_module\nfrom typing import TYPE_CHECKING, ClassVar\n\nfrom confuse import ConfigValueError\n\nfrom beets import ui\nfrom beets.plugins import BeetsPlugin\n\nif TYPE_CHECKING:\n    from beets.dbcore import types\n\nMETASYNC_MODULE = \"beetsplug.metasync\"\n\n# Dictionary to map the MODULE and the CLASS NAME of meta sources\nSOURCES = {\n    \"amarok\": \"Amarok\",\n    \"itunes\": \"Itunes\",\n}\n\n\nclass MetaSource(metaclass=ABCMeta):\n    item_types: ClassVar[dict[str, types.Type]]\n\n    def __init__(self, config, log):\n        self.config = config\n        self._log = log\n\n    @abstractmethod\n    def sync_from_source(self, item):\n        pass\n\n\ndef load_meta_sources():\n    \"\"\"Returns a dictionary of all the MetaSources\n    E.g., {'itunes': Itunes} with isinstance(Itunes, MetaSource) true\n    \"\"\"\n    meta_sources = {}\n\n    for module_path, class_name in SOURCES.items():\n        module = import_module(f\"{METASYNC_MODULE}.{module_path}\")\n        meta_sources[class_name.lower()] = getattr(module, class_name)\n\n    return meta_sources\n\n\nMETA_SOURCES = load_meta_sources()\n\n\ndef load_item_types():\n    \"\"\"Returns a dictionary containing the item_types of all the MetaSources\"\"\"\n    item_types = {}\n    for meta_source in META_SOURCES.values():\n        item_types.update(meta_source.item_types)\n    return item_types\n\n\nclass MetaSyncPlugin(BeetsPlugin):\n    item_types = load_item_types()\n\n    def __init__(self):\n        super().__init__()\n\n    def commands(self):\n        cmd = ui.Subcommand(\n            \"metasync\", help=\"update metadata from music player libraries\"\n        )\n        cmd.parser.add_option(\n            \"-p\",\n            \"--pretend\",\n            action=\"store_true\",\n            help=\"show all changes but do nothing\",\n        )\n        cmd.parser.add_option(\n            \"-s\",\n            \"--source\",\n            default=[],\n            action=\"append\",\n            dest=\"sources\",\n            help=\"comma-separated list of sources to sync\",\n        )\n        cmd.parser.add_format_option()\n        cmd.func = self.func\n        return [cmd]\n\n    def func(self, lib, opts, args):\n        \"\"\"Command handler for the metasync function.\"\"\"\n        pretend = opts.pretend\n\n        sources = []\n        for source in opts.sources:\n            sources.extend(source.split(\",\"))\n\n        sources = sources or self.config[\"source\"].as_str_seq()\n\n        meta_source_instances = {}\n        items = lib.items(args)\n\n        # Avoid needlessly instantiating meta sources (can be expensive)\n        if not items:\n            self._log.info(\"No items found matching query\")\n            return\n\n        # Instantiate the meta sources\n        for player in sources:\n            try:\n                cls = META_SOURCES[player]\n            except KeyError:\n                self._log.error(\"Unknown metadata source '{}'\", player)\n\n            try:\n                meta_source_instances[player] = cls(self.config, self._log)\n            except (ImportError, ConfigValueError) as e:\n                self._log.error(\n                    \"Failed to instantiate metadata source {!r}: {}\", player, e\n                )\n\n        # Avoid needlessly iterating over items\n        if not meta_source_instances:\n            self._log.error(\"No valid metadata sources found\")\n            return\n\n        # Sync the items with all of the meta sources\n        for item in items:\n            for meta_source in meta_source_instances.values():\n                meta_source.sync_from_source(item)\n\n            changed = ui.show_model_changes(item)\n\n            if changed and not pretend:\n                item.store()\n"
  },
  {
    "path": "beetsplug/metasync/amarok.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Heinz Wiesinger.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Synchronize information from amarok's library via dbus\"\"\"\n\nfrom datetime import datetime\nfrom os.path import basename\nfrom time import mktime\nfrom typing import ClassVar\nfrom xml.sax.saxutils import quoteattr\n\nfrom beets.dbcore import types\nfrom beets.util import displayable_path\nfrom beetsplug.metasync import MetaSource\n\n\ndef import_dbus():\n    try:\n        return __import__(\"dbus\")\n    except ImportError:\n        return None\n\n\ndbus = import_dbus()\n\n\nclass Amarok(MetaSource):\n    item_types: ClassVar[dict[str, types.Type]] = {\n        \"amarok_rating\": types.INTEGER,\n        \"amarok_score\": types.FLOAT,\n        \"amarok_uid\": types.STRING,\n        \"amarok_playcount\": types.INTEGER,\n        \"amarok_firstplayed\": types.DATE,\n        \"amarok_lastplayed\": types.DATE,\n    }\n\n    query_xml = \"\"\"\n        <query version=\"1.0\">\n            <filters>\n                <and><include field=\"filename\" value={} /></and>\n            </filters>\n        </query>\"\"\"\n\n    def __init__(self, config, log):\n        super().__init__(config, log)\n\n        if not dbus:\n            raise ImportError(\"failed to import dbus\")\n\n        self.collection = dbus.SessionBus().get_object(\n            \"org.kde.amarok\", \"/Collection\"\n        )\n\n    def sync_from_source(self, item):\n        path = displayable_path(item.path)\n\n        # amarok unfortunately doesn't allow searching for the full path, only\n        # for the patch relative to the mount point. But the full path is part\n        # of the result set. So query for the filename and then try to match\n        # the correct item from the results we get back\n        results = self.collection.Query(\n            self.query_xml.format(quoteattr(basename(path)))\n        )\n        for result in results:\n            if result[\"xesam:url\"] != path:\n                continue\n\n            item.amarok_rating = result[\"xesam:userRating\"]\n            item.amarok_score = result[\"xesam:autoRating\"]\n            item.amarok_playcount = result[\"xesam:useCount\"]\n            item.amarok_uid = result[\"xesam:id\"].replace(\n                \"amarok-sqltrackuid://\", \"\"\n            )\n\n            if result[\"xesam:firstUsed\"][0][0] != 0:\n                # These dates are stored as timestamps in amarok's db, but\n                # exposed over dbus as fixed integers in the current timezone.\n                first_played = datetime(\n                    result[\"xesam:firstUsed\"][0][0],\n                    result[\"xesam:firstUsed\"][0][1],\n                    result[\"xesam:firstUsed\"][0][2],\n                    result[\"xesam:firstUsed\"][1][0],\n                    result[\"xesam:firstUsed\"][1][1],\n                    result[\"xesam:firstUsed\"][1][2],\n                )\n\n                if result[\"xesam:lastUsed\"][0][0] != 0:\n                    last_played = datetime(\n                        result[\"xesam:lastUsed\"][0][0],\n                        result[\"xesam:lastUsed\"][0][1],\n                        result[\"xesam:lastUsed\"][0][2],\n                        result[\"xesam:lastUsed\"][1][0],\n                        result[\"xesam:lastUsed\"][1][1],\n                        result[\"xesam:lastUsed\"][1][2],\n                    )\n                else:\n                    last_played = first_played\n\n                item.amarok_firstplayed = mktime(first_played.timetuple())\n                item.amarok_lastplayed = mktime(last_played.timetuple())\n"
  },
  {
    "path": "beetsplug/metasync/itunes.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Tom Jaspers.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Synchronize information from iTunes's library\"\"\"\n\nimport os\nimport plistlib\nimport shutil\nimport tempfile\nfrom contextlib import contextmanager\nfrom time import mktime\nfrom typing import ClassVar\nfrom urllib.parse import unquote, urlparse\n\nfrom confuse import ConfigValueError\n\nfrom beets import util\nfrom beets.dbcore import types\nfrom beets.util import bytestring_path, syspath\nfrom beetsplug.metasync import MetaSource\n\n\n@contextmanager\ndef create_temporary_copy(path):\n    temp_dir = bytestring_path(tempfile.mkdtemp())\n    temp_path = os.path.join(temp_dir, b\"temp_itunes_lib\")\n    shutil.copyfile(syspath(path), syspath(temp_path))\n    try:\n        yield temp_path\n    finally:\n        shutil.rmtree(syspath(temp_dir))\n\n\ndef _norm_itunes_path(path):\n    # Itunes prepends the location with 'file://' on posix systems,\n    # and with 'file://localhost/' on Windows systems.\n    # The actual path to the file is always saved as posix form\n    # E.g., 'file://Users/Music/bar' or 'file://localhost/G:/Music/bar'\n\n    # The entire path will also be capitalized (e.g., '/Music/Alt-J')\n    # Note that this means the path will always have a leading separator,\n    # which is unwanted in the case of Windows systems.\n    # E.g., '\\\\G:\\\\Music\\\\bar' needs to be stripped to 'G:\\\\Music\\\\bar'\n\n    return util.bytestring_path(\n        os.path.normpath(unquote(urlparse(path).path)).lstrip(\"\\\\\")\n    ).lower()\n\n\nclass Itunes(MetaSource):\n    item_types: ClassVar[dict[str, types.Type]] = {\n        \"itunes_rating\": types.INTEGER,  # 0..100 scale\n        \"itunes_playcount\": types.INTEGER,\n        \"itunes_skipcount\": types.INTEGER,\n        \"itunes_lastplayed\": types.DATE,\n        \"itunes_lastskipped\": types.DATE,\n        \"itunes_dateadded\": types.DATE,\n    }\n\n    def __init__(self, config, log):\n        super().__init__(config, log)\n\n        config.add({\"itunes\": {\"library\": \"~/Music/iTunes/iTunes Library.xml\"}})\n\n        # Load the iTunes library, which has to be the .xml one (not the .itl)\n        library_path = config[\"itunes\"][\"library\"].as_filename()\n\n        try:\n            self._log.debug(\"loading iTunes library from {}\", library_path)\n            with create_temporary_copy(library_path) as library_copy:\n                with open(library_copy, \"rb\") as library_copy_f:\n                    raw_library = plistlib.load(library_copy_f)\n        except OSError as e:\n            raise ConfigValueError(f\"invalid iTunes library: {e.strerror}\")\n        except Exception:\n            # It's likely the user configured their '.itl' library (<> xml)\n            if os.path.splitext(library_path)[1].lower() != \".xml\":\n                hint = (\n                    \": please ensure that the configured path\"\n                    \" points to the .XML library\"\n                )\n            else:\n                hint = \"\"\n            raise ConfigValueError(f\"invalid iTunes library{hint}\")\n\n        # Make the iTunes library queryable using the path\n        self.collection = {\n            _norm_itunes_path(track[\"Location\"]): track\n            for track in raw_library[\"Tracks\"].values()\n            if \"Location\" in track\n        }\n\n    def sync_from_source(self, item):\n        result = self.collection.get(util.bytestring_path(item.path).lower())\n\n        if not result:\n            self._log.warning(\"no iTunes match found for {}\", item)\n            return\n\n        item.itunes_rating = result.get(\"Rating\")\n        item.itunes_playcount = result.get(\"Play Count\")\n        item.itunes_skipcount = result.get(\"Skip Count\")\n\n        if result.get(\"Play Date UTC\"):\n            item.itunes_lastplayed = mktime(\n                result.get(\"Play Date UTC\").timetuple()\n            )\n\n        if result.get(\"Skip Date\"):\n            item.itunes_lastskipped = mktime(\n                result.get(\"Skip Date\").timetuple()\n            )\n\n        if result.get(\"Date Added\"):\n            item.itunes_dateadded = mktime(result.get(\"Date Added\").timetuple())\n"
  },
  {
    "path": "beetsplug/missing.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Pedro Silva.\n# Copyright 2017, Quentin Young.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"List missing tracks.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections import defaultdict\nfrom typing import TYPE_CHECKING, ClassVar\n\nimport requests\n\nfrom beets import config, metadata_plugins\nfrom beets.dbcore import types\nfrom beets.library import Item\nfrom beets.plugins import BeetsPlugin\nfrom beets.ui import Subcommand, print_\n\nfrom ._utils.musicbrainz import MusicBrainzAPIMixin\n\nif TYPE_CHECKING:\n    from collections.abc import Iterator\n\n    from beets.library import Album, Library\n\n# Valid MusicBrainz release types for filtering release groups\nVALID_RELEASE_TYPES = [\n    \"nat\",\n    \"album\",\n    \"single\",\n    \"ep\",\n    \"broadcast\",\n    \"other\",\n    \"compilation\",\n    \"soundtrack\",\n    \"spokenword\",\n    \"interview\",\n    \"audiobook\",\n    \"live\",\n    \"remix\",\n    \"dj-mix\",\n    \"mixtape/street\",\n]\n\nMB_ARTIST_QUERY = r\"mb_albumartistid::^\\w{8}-\\w{4}-\\w{4}-\\w{4}-\\w{12}$\"\n\n\ndef _missing_count(album):\n    \"\"\"Return number of missing items in `album`.\"\"\"\n    return (album.albumtotal or 0) - len(album.items())\n\n\ndef _item(track_info, album_info, album_id):\n    \"\"\"Build and return `item` from `track_info` and `album info`\n    objects. `item` is missing what fields cannot be obtained from\n    MusicBrainz alone (encoder, rg_track_gain, rg_track_peak,\n    rg_album_gain, rg_album_peak, original_year, original_month,\n    original_day, length, bitrate, format, samplerate, bitdepth,\n    channels, mtime.)\n    \"\"\"\n    t = track_info\n    a = album_info\n\n    return Item(\n        **{\n            \"album_id\": album_id,\n            \"album\": a.album,\n            \"albumartist\": a.artist,\n            \"albumartist_credit\": a.artist_credit,\n            \"albumartist_sort\": a.artist_sort,\n            \"albumdisambig\": a.albumdisambig,\n            \"albumstatus\": a.albumstatus,\n            \"albumtype\": a.albumtype,\n            \"artist\": t.artist,\n            \"artist_credit\": t.artist_credit,\n            \"artist_sort\": t.artist_sort,\n            \"asin\": a.asin,\n            \"catalognum\": a.catalognum,\n            \"comp\": a.va,\n            \"country\": a.country,\n            \"day\": a.day,\n            \"disc\": t.medium,\n            \"disctitle\": t.disctitle,\n            \"disctotal\": a.mediums,\n            \"label\": a.label,\n            \"language\": a.language,\n            \"length\": t.length,\n            \"mb_albumid\": a.album_id,\n            \"mb_artistid\": t.artist_id,\n            \"mb_releasegroupid\": a.releasegroup_id,\n            \"mb_trackid\": t.track_id,\n            \"media\": t.media,\n            \"month\": a.month,\n            \"script\": a.script,\n            \"title\": t.title,\n            \"track\": t.index,\n            \"tracktotal\": len(a.tracks),\n            \"year\": a.year,\n        }\n    )\n\n\nclass MissingPlugin(MusicBrainzAPIMixin, BeetsPlugin):\n    \"\"\"List missing tracks\"\"\"\n\n    album_types: ClassVar[dict[str, types.Type]] = {\n        \"missing\": types.INTEGER,\n    }\n\n    def __init__(self):\n        super().__init__()\n\n        self.config.add(\n            {\n                \"count\": False,\n                \"total\": False,\n                \"album\": False,\n                \"release_types\": [\"album\"],\n            }\n        )\n\n        self.album_template_fields[\"missing\"] = _missing_count\n\n        self._command = Subcommand(\"missing\", help=__doc__, aliases=[\"miss\"])\n        self._command.parser.add_option(\n            \"-c\",\n            \"--count\",\n            dest=\"count\",\n            action=\"store_true\",\n            help=\"count missing tracks per album\",\n        )\n        self._command.parser.add_option(\n            \"-t\",\n            \"--total\",\n            dest=\"total\",\n            action=\"store_true\",\n            help=\"count total of missing tracks\",\n        )\n        self._command.parser.add_option(\n            \"-a\",\n            \"--album\",\n            dest=\"album\",\n            action=\"store_true\",\n            help=(\n                \"show missing album releases for artist instead of tracks; \"\n                \"Defaults to only releases of type 'album'\"\n            ),\n        )\n        self._command.parser.add_option(\n            \"--release-types\",\n            action=\"append\",\n            dest=\"release_types\",\n            help=(\n                \"comma-separated list of release types for missing albums \"\n                f\"(valid: {', '.join(VALID_RELEASE_TYPES)})\"\n            ),\n        )\n        self._command.parser.add_format_option()\n\n    def commands(self):\n        def _miss(lib, opts, args):\n            self.config.set_args(opts)\n            albms = self.config[\"album\"].get()\n\n            helper = self._missing_albums if albms else self._missing_tracks\n            helper(lib, args)\n\n        self._command.func = _miss\n        return [self._command]\n\n    def _missing_tracks(self, lib, query):\n        \"\"\"Print a listing of tracks missing from each album in the library\n        matching query.\n        \"\"\"\n        albums = lib.albums(query)\n\n        count = self.config[\"count\"].get()\n        total = self.config[\"total\"].get()\n        fmt = config[\"format_album\" if count else \"format_item\"].get()\n\n        if total:\n            print(sum([_missing_count(a) for a in albums]))\n            return\n\n        # Default format string for count mode.\n        if count:\n            fmt += \": $missing\"\n\n        for album in albums:\n            if count:\n                if _missing_count(album):\n                    print_(format(album, fmt))\n\n            else:\n                for item in self._missing(album):\n                    print_(format(item, fmt))\n\n    def _missing_albums(self, lib: Library, query: list[str]) -> None:\n        \"\"\"Print a listing of albums missing from each artist in the library\n        matching query.\n        \"\"\"\n        query.append(MB_ARTIST_QUERY)\n\n        # build dict mapping artist to set of their release group ids in library\n        album_ids_by_artist = defaultdict(set)\n        for album in lib.albums(query):\n            # TODO(@snejus): Some releases have different `albumartist` for the\n            # same `mb_albumartistid`. Since we're grouping by the combination\n            # of these two fields, we end up processing the same\n            # `mb_albumartistid` multiple times: calling MusicBrainz API and\n            # reporting the same set of missing albums. Instead, we should\n            # group by `mb_albumartistid` field only.\n            artist = (album[\"albumartist\"], album[\"mb_albumartistid\"])\n            album_ids_by_artist[artist].add(album[\"mb_releasegroupid\"])\n\n        total_missing = 0\n        release_types = []\n        for rt in self.config[\"release_types\"].as_str_seq():\n            release_types.extend(rt.split(\",\"))\n        calculating_total = self.config[\"total\"].get()\n        for (artist, artist_id), album_ids in album_ids_by_artist.items():\n            try:\n                resp = self.mb_api.browse_release_groups(\n                    artist=artist_id,\n                    type=\"|\".join(release_types),\n                )\n            except requests.exceptions.RequestException:\n                self._log.info(\n                    \"Couldn't fetch info for artist '{}' ({})\",\n                    artist,\n                    artist_id,\n                    exc_info=True,\n                )\n                continue\n\n            missing_titles = [\n                f\"{artist} - {rg['title']}\"\n                for rg in resp\n                if rg[\"id\"] not in album_ids\n            ]\n\n            if calculating_total:\n                total_missing += len(missing_titles)\n            else:\n                for title in missing_titles:\n                    print(title)\n\n        if calculating_total:\n            print(total_missing)\n\n    def _missing(self, album: Album) -> Iterator[Item]:\n        \"\"\"Query MusicBrainz to determine items missing from `album`.\"\"\"\n        if len(album.items()) == album.albumtotal:\n            return\n\n        # fetch missing items\n        # TODO: Implement caching that without breaking other stuff\n        data_source = album.get(\"data_source\") or album.items()[0].get(\n            \"data_source\", \"MusicBrainz\"\n        )\n        if album_info := metadata_plugins.album_for_id(\n            album.mb_albumid, data_source\n        ):\n            item_mbids = {x.mb_trackid for x in album.items()}\n            for track_info in album_info.tracks:\n                if track_info.track_id not in item_mbids:\n                    self._log.debug(\n                        \"track {.track_id} in album {.album_id}\",\n                        track_info,\n                        album_info,\n                    )\n                    yield _item(track_info, album_info, album.id)\n"
  },
  {
    "path": "beetsplug/mpdstats.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Peter Schnebel and Johann Klähn.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nimport os\nimport time\nfrom typing import ClassVar\n\nimport mpd\n\nfrom beets import config, plugins, ui\nfrom beets.dbcore import types\nfrom beets.dbcore.query import PathQuery\nfrom beets.util import displayable_path\n\n# If we lose the connection, how many times do we want to retry and how\n# much time should we wait between retries?\nRETRIES = 10\nRETRY_INTERVAL = 5\nDUPLICATE_PLAY_THRESHOLD = 10.0\n\n\nmpd_config = config[\"mpd\"]\n\n\ndef is_url(path):\n    \"\"\"Try to determine if the path is an URL.\"\"\"\n    if isinstance(path, bytes):  # if it's bytes, then it's a path\n        return False\n    return path.split(\"://\", 1)[0] in [\"http\", \"https\"]\n\n\nclass MPDClientWrapper:\n    def __init__(self, log):\n        self._log = log\n\n        self.music_directory = mpd_config[\"music_directory\"].as_str()\n        self.strip_path = mpd_config[\"strip_path\"].as_str()\n\n        # Ensure strip_path end with '/'\n        if not self.strip_path.endswith(\"/\"):\n            self.strip_path += \"/\"\n\n        self._log.debug(\"music_directory: {.music_directory}\", self)\n        self._log.debug(\"strip_path: {.strip_path}\", self)\n\n        self.client = mpd.MPDClient()\n\n    def connect(self):\n        \"\"\"Connect to the MPD.\"\"\"\n        host = mpd_config[\"host\"].as_str()\n        port = mpd_config[\"port\"].get(int)\n\n        if host[0] in [\"/\", \"~\"]:\n            host = os.path.expanduser(host)\n\n        self._log.info(\"connecting to {}:{}\", host, port)\n        try:\n            self.client.connect(host, port)\n        except OSError as e:\n            raise ui.UserError(f\"could not connect to MPD: {e}\")\n\n        password = mpd_config[\"password\"].as_str()\n        if password:\n            try:\n                self.client.password(password)\n            except mpd.CommandError as e:\n                raise ui.UserError(f\"could not authenticate to MPD: {e}\")\n\n    def disconnect(self):\n        \"\"\"Disconnect from the MPD.\"\"\"\n        self.client.close()\n        self.client.disconnect()\n\n    def get(self, command, retries=RETRIES):\n        \"\"\"Wrapper for requests to the MPD server. Tries to re-connect if the\n        connection was lost (f.ex. during MPD's library refresh).\n        \"\"\"\n        try:\n            return getattr(self.client, command)()\n        except (OSError, mpd.ConnectionError) as err:\n            self._log.error(\"{}\", err)\n\n        if retries <= 0:\n            # if we exited without breaking, we couldn't reconnect in time :(\n            raise ui.UserError(\"communication with MPD server failed\")\n\n        time.sleep(RETRY_INTERVAL)\n\n        try:\n            self.disconnect()\n        except mpd.ConnectionError:\n            pass\n\n        self.connect()\n        return self.get(command, retries=retries - 1)\n\n    def currentsong(self):\n        \"\"\"Return the path to the currently playing song, along with its\n        songid.  Prefixes paths with the music_directory, to get the absolute\n        path.\n        In some cases, we need to remove the local path from MPD server,\n        we replace 'strip_path' with ''.\n        `strip_path` defaults to ''.\n        \"\"\"\n        result = None\n        entry = self.get(\"currentsong\")\n        if \"file\" in entry:\n            if not is_url(entry[\"file\"]):\n                file = entry[\"file\"]\n                if file.startswith(self.strip_path):\n                    file = file[len(self.strip_path) :]\n                result = os.path.join(self.music_directory, file)\n            else:\n                result = entry[\"file\"]\n        self._log.debug(\"returning: {}\", result)\n        return result, entry.get(\"id\")\n\n    def status(self):\n        \"\"\"Return the current status of the MPD.\"\"\"\n        return self.get(\"status\")\n\n    def events(self):\n        \"\"\"Return list of events. This may block a long time while waiting for\n        an answer from MPD.\n        \"\"\"\n        return self.get(\"idle\")\n\n\nclass MPDStats:\n    def __init__(self, lib, log):\n        self.lib = lib\n        self._log = log\n\n        self.do_rating = mpd_config[\"rating\"].get(bool)\n        self.rating_mix = mpd_config[\"rating_mix\"].get(float)\n        self.played_ratio_threshold = mpd_config[\"played_ratio_threshold\"].get(\n            float\n        )\n\n        self.now_playing = None\n        self.mpd = MPDClientWrapper(log)\n\n    def rating(self, play_count, skip_count, rating, skipped):\n        \"\"\"Calculate a new rating for a song based on play count, skip count,\n        old rating and the fact if it was skipped or not.\n        \"\"\"\n        if skipped:\n            rolling = rating - rating / 2.0\n        else:\n            rolling = rating + (1.0 - rating) / 2.0\n        stable = (play_count + 1.0) / (play_count + skip_count + 2.0)\n        return self.rating_mix * stable + (1.0 - self.rating_mix) * rolling\n\n    def get_item(self, path):\n        \"\"\"Return the beets item related to path.\"\"\"\n        query = PathQuery(\"path\", path)\n        item = self.lib.items(query).get()\n        if item:\n            return item\n        else:\n            self._log.info(\"item not found: {}\", displayable_path(path))\n\n    def update_item(self, item, attribute, value=None, increment=None):\n        \"\"\"Update the beets item. Set attribute to value or increment the value\n        of attribute. If the increment argument is used the value is cast to\n        the corresponding type.\n        \"\"\"\n        if item is None:\n            return\n\n        if increment is not None:\n            item.load()\n            value = type(increment)(item.get(attribute, 0)) + increment\n\n        if value is not None:\n            item[attribute] = value\n            item.store()\n\n            self._log.debug(\n                \"updated: {} = {} [{.filepath}]\",\n                attribute,\n                item[attribute],\n                item,\n            )\n\n    def update_rating(self, item, skipped):\n        \"\"\"Update the rating for a beets item. The `item` can either be a\n        beets `Item` or None. If the item is None, nothing changes.\n        \"\"\"\n        if item is None:\n            return\n\n        item.load()\n        rating = self.rating(\n            int(item.get(\"play_count\", 0)),\n            int(item.get(\"skip_count\", 0)),\n            float(item.get(\"rating\", 0.5)),\n            skipped,\n        )\n\n        self.update_item(item, \"rating\", rating)\n\n    def handle_song_change(self, song):\n        \"\"\"Determine if a song was skipped or not and update its attributes.\n        To this end the difference between the song's supposed end time\n        and the current time is calculated. If it's greater than a threshold,\n        the song is considered skipped.\n\n        Returns whether the change was manual (skipped previous song or not)\n        \"\"\"\n        elapsed = song[\"elapsed_at_start\"] + (time.time() - song[\"started\"])\n        skipped = elapsed / song[\"duration\"] < self.played_ratio_threshold\n        if skipped:\n            self.handle_skipped(song)\n        else:\n            self.handle_played(song)\n\n        if self.do_rating:\n            self.update_rating(song[\"beets_item\"], skipped)\n\n        return skipped\n\n    def handle_played(self, song):\n        \"\"\"Updates the play count of a song.\"\"\"\n        self.update_item(song[\"beets_item\"], \"play_count\", increment=1)\n        self._log.info(\"played {}\", displayable_path(song[\"path\"]))\n\n    def handle_skipped(self, song):\n        \"\"\"Updates the skip count of a song.\"\"\"\n        self.update_item(song[\"beets_item\"], \"skip_count\", increment=1)\n        self._log.info(\"skipped {}\", displayable_path(song[\"path\"]))\n\n    def on_stop(self, status):\n        self._log.info(\"stop\")\n\n        # if the current song stays the same it means that we stopped on the\n        # current track and should not record a skip.\n        if self.now_playing and self.now_playing[\"id\"] != status.get(\"songid\"):\n            self.handle_song_change(self.now_playing)\n\n        self.now_playing = None\n\n    def on_pause(self, status):\n        self._log.info(\"pause\")\n        self.now_playing = None\n\n    def on_play(self, status):\n        path, songid = self.mpd.currentsong()\n        if not path:\n            return\n\n        played, duration = map(int, status[\"time\"].split(\":\", 1))\n        if self.now_playing:\n            if self.now_playing[\"path\"] != path:\n                self.handle_song_change(self.now_playing)\n            else:\n                # In case we got mpd play event with same song playing\n                # multiple times,\n                # assume low diff means redundant second play event\n                # after natural song start.\n                diff = abs(time.time() - self.now_playing[\"started\"])\n\n                if diff <= DUPLICATE_PLAY_THRESHOLD:\n                    return\n\n                if self.now_playing[\"path\"] == path and played == 0:\n                    self.handle_song_change(self.now_playing)\n\n        if is_url(path):\n            self._log.info(\"playing stream {}\", displayable_path(path))\n            self.now_playing = None\n            return\n\n        self._log.info(\"playing {}\", displayable_path(path))\n\n        self.now_playing = {\n            \"started\": time.time(),\n            \"elapsed_at_start\": played,\n            \"duration\": duration,\n            \"path\": path,\n            \"id\": songid,\n            \"beets_item\": self.get_item(path),\n        }\n\n        self.update_item(\n            self.now_playing[\"beets_item\"],\n            \"last_played\",\n            value=int(time.time()),\n        )\n\n    def run(self):\n        self.mpd.connect()\n        events = [\"player\"]\n\n        while True:\n            if \"player\" in events:\n                status = self.mpd.status()\n\n                handler = getattr(self, f\"on_{status['state']}\", None)\n\n                if handler:\n                    handler(status)\n                else:\n                    self._log.debug('unhandled status \"{}\"', status)\n\n            events = self.mpd.events()\n\n\nclass MPDStatsPlugin(plugins.BeetsPlugin):\n    item_types: ClassVar[dict[str, types.Type]] = {\n        \"play_count\": types.INTEGER,\n        \"skip_count\": types.INTEGER,\n        \"last_played\": types.DATE,\n        \"rating\": types.FLOAT,\n    }\n\n    def __init__(self):\n        super().__init__()\n        mpd_config.add(\n            {\n                \"music_directory\": config[\"directory\"].as_filename(),\n                \"strip_path\": \"\",\n                \"rating\": True,\n                \"rating_mix\": 0.75,\n                \"host\": os.environ.get(\"MPD_HOST\", \"localhost\"),\n                \"port\": int(os.environ.get(\"MPD_PORT\", 6600)),\n                \"password\": \"\",\n                \"played_ratio_threshold\": 0.85,\n            }\n        )\n        mpd_config[\"password\"].redact = True\n\n    def commands(self):\n        cmd = ui.Subcommand(\n            \"mpdstats\", help=\"run a MPD client to gather play statistics\"\n        )\n        cmd.parser.add_option(\n            \"--host\",\n            dest=\"host\",\n            type=\"string\",\n            help=\"set the hostname of the server to connect to\",\n        )\n        cmd.parser.add_option(\n            \"--port\",\n            dest=\"port\",\n            type=\"int\",\n            help=\"set the port of the MPD server to connect to\",\n        )\n        cmd.parser.add_option(\n            \"--password\",\n            dest=\"password\",\n            type=\"string\",\n            help=\"set the password of the MPD server to connect to\",\n        )\n\n        def func(lib, opts, args):\n            mpd_config.set_args(opts)\n\n            # Overrides for MPD settings.\n            if opts.host:\n                mpd_config[\"host\"] = opts.host.decode(\"utf-8\")\n            if opts.port:\n                mpd_config[\"host\"] = int(opts.port)\n            if opts.password:\n                mpd_config[\"password\"] = opts.password.decode(\"utf-8\")\n\n            try:\n                MPDStats(lib, self._log).run()\n            except KeyboardInterrupt:\n                pass\n\n        cmd.func = func\n        return [cmd]\n"
  },
  {
    "path": "beetsplug/mpdupdate.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Updates an MPD index whenever the library is changed.\n\nPut something like the following in your config.yaml to configure:\n    mpd:\n        host: localhost\n        port: 6600\n        password: seekrit\n\"\"\"\n\nimport os\nimport socket\n\nfrom beets import config\nfrom beets.plugins import BeetsPlugin\n\n\n# No need to introduce a dependency on an MPD library for such a\n# simple use case. Here's a simple socket abstraction to make things\n# easier.\nclass BufferedSocket:\n    \"\"\"Socket abstraction that allows reading by line.\"\"\"\n\n    def __init__(self, host, port, sep=b\"\\n\"):\n        if host[0] in [\"/\", \"~\"]:\n            self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n            self.sock.connect(os.path.expanduser(host))\n        else:\n            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n            self.sock.connect((host, port))\n        self.buf = b\"\"\n        self.sep = sep\n\n    def readline(self):\n        while self.sep not in self.buf:\n            data = self.sock.recv(1024)\n            if not data:\n                break\n            self.buf += data\n        if self.sep in self.buf:\n            res, self.buf = self.buf.split(self.sep, 1)\n            return res + self.sep\n        else:\n            return b\"\"\n\n    def send(self, data):\n        self.sock.send(data)\n\n    def close(self):\n        self.sock.close()\n\n\nclass MPDUpdatePlugin(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n        config[\"mpd\"].add(\n            {\n                \"host\": os.environ.get(\"MPD_HOST\", \"localhost\"),\n                \"port\": int(os.environ.get(\"MPD_PORT\", 6600)),\n                \"password\": \"\",\n            }\n        )\n        config[\"mpd\"][\"password\"].redact = True\n\n        # For backwards compatibility, use any values from the\n        # plugin-specific \"mpdupdate\" section.\n        for key in config[\"mpd\"].keys():\n            if self.config[key].exists():\n                config[\"mpd\"][key] = self.config[key].get()\n\n        self.register_listener(\"database_change\", self.db_change)\n\n    def db_change(self, lib, model):\n        self.register_listener(\"cli_exit\", self.update)\n\n    def update(self, lib):\n        self.update_mpd(\n            config[\"mpd\"][\"host\"].as_str(),\n            config[\"mpd\"][\"port\"].get(int),\n            config[\"mpd\"][\"password\"].as_str(),\n        )\n\n    def update_mpd(self, host=\"localhost\", port=6600, password=None):\n        \"\"\"Sends the \"update\" command to the MPD server indicated,\n        possibly authenticating with a password first.\n        \"\"\"\n        self._log.info(\"Updating MPD database...\")\n\n        try:\n            s = BufferedSocket(host, port)\n        except OSError:\n            self._log.warning(\"MPD connection failed\", exc_info=True)\n            return\n\n        resp = s.readline()\n        if b\"OK MPD\" not in resp:\n            self._log.warning(\"MPD connection failed: {0!r}\", resp)\n            return\n\n        if password:\n            s.send(f'password \"{password}\"\\n'.encode())\n            resp = s.readline()\n            if b\"OK\" not in resp:\n                self._log.warning(\"Authentication failed: {0!r}\", resp)\n                s.send(b\"close\\n\")\n                s.close()\n                return\n\n        s.send(b\"update\\n\")\n        resp = s.readline()\n        if b\"updating_db\" not in resp:\n            self._log.warning(\"Update failed: {0!r}\", resp)\n\n        s.send(b\"close\\n\")\n        s.close()\n        self._log.info(\"Database updated.\")\n"
  },
  {
    "path": "beetsplug/musicbrainz.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Searches for albums in the MusicBrainz database.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections import Counter\nfrom contextlib import suppress\nfrom functools import cached_property\nfrom itertools import product\nfrom typing import TYPE_CHECKING, Any, Literal\nfrom urllib.parse import urljoin\n\nfrom confuse.exceptions import NotFoundError\n\nimport beets\nimport beets.autotag.hooks\nfrom beets import config, plugins, util\nfrom beets.metadata_plugins import IDResponse, SearchApiMetadataSourcePlugin\nfrom beets.util.deprecation import deprecate_for_user\nfrom beets.util.id_extractors import extract_release_id\n\nfrom ._utils.musicbrainz import MusicBrainzAPIMixin\nfrom ._utils.requests import HTTPNotFoundError\n\nif TYPE_CHECKING:\n    from collections.abc import Sequence\n\n    from beets.library import Item\n    from beets.metadata_plugins import QueryType, SearchParams\n\n    from ._typing import JSONDict\n\nVARIOUS_ARTISTS_ID = \"89ad4ac3-39f7-470e-963a-56509c546377\"\n\nBASE_URL = \"https://musicbrainz.org/\"\n\nSKIPPED_TRACKS = [\"[data track]\"]\n\nFIELDS_TO_MB_KEYS = {\n    \"barcode\": \"barcode\",\n    \"catalognum\": \"catno\",\n    \"country\": \"country\",\n    \"label\": \"label\",\n    \"media\": \"format\",\n    \"year\": \"date\",\n    \"tracks\": \"tracks\",\n    \"alias\": \"alias\",\n}\n\nBROWSE_INCLUDES = [\n    \"artist-credits\",\n    \"work-rels\",\n    \"artist-rels\",\n    \"recording-rels\",\n    \"release-rels\",\n]\nBROWSE_CHUNKSIZE = 100\nBROWSE_MAXTRACKS = 500\n\n\ndef _preferred_alias(\n    aliases: list[JSONDict], languages: list[str] | None = None\n) -> JSONDict | None:\n    \"\"\"Given a list of alias structures for an artist credit, select\n    and return the user's preferred alias or None if no matching\n    \"\"\"\n    if not aliases:\n        return None\n\n    # Only consider aliases that have locales set.\n    valid_aliases = [a for a in aliases if \"locale\" in a]\n\n    # Get any ignored alias types and lower case them to prevent case issues\n    ignored_alias_types = config[\"import\"][\"ignored_alias_types\"].as_str_seq()\n    ignored_alias_types = [a.lower() for a in ignored_alias_types]\n\n    # Search configured locales in order.\n    if languages is None:\n        languages = config[\"import\"][\"languages\"].as_str_seq()\n\n    for locale in languages:\n        # Find matching primary aliases for this locale that are not\n        # being ignored\n        for alias in valid_aliases:\n            if (\n                alias[\"locale\"] == locale\n                and alias.get(\"primary\")\n                and (alias.get(\"type\") or \"\").lower() not in ignored_alias_types\n            ):\n                return alias\n\n    return None\n\n\ndef _key_with_preferred_alias(obj: JSONDict, key: str) -> str:\n    alias = _preferred_alias(obj.get(\"aliases\", ()))\n    return alias[\"name\"] if alias else obj[key]\n\n\ndef _multi_artist_credit(\n    credit: list[JSONDict], include_join_phrase: bool\n) -> tuple[list[str], list[str], list[str]]:\n    \"\"\"Given a list representing an ``artist-credit`` block, accumulate\n    data into a triple of joined artist name lists: canonical, sort, and\n    credit.\n    \"\"\"\n    artist_parts = []\n    artist_sort_parts = []\n    artist_credit_parts = []\n    for el in credit:\n        alias = _preferred_alias(el[\"artist\"].get(\"aliases\", ()))\n\n        # An artist.\n        cur_artist_name = alias[\"name\"] if alias else el[\"artist\"][\"name\"]\n        artist_parts.append(cur_artist_name)\n\n        # Artist sort name.\n        if alias:\n            artist_sort_parts.append(alias[\"sort-name\"])\n        elif \"sort-name\" in el[\"artist\"]:\n            artist_sort_parts.append(el[\"artist\"][\"sort-name\"])\n        else:\n            artist_sort_parts.append(cur_artist_name)\n\n        # Artist credit.\n        if \"name\" in el:\n            artist_credit_parts.append(el[\"name\"])\n        else:\n            artist_credit_parts.append(cur_artist_name)\n\n        if include_join_phrase and (joinphrase := el.get(\"joinphrase\")):\n            artist_parts.append(joinphrase)\n            artist_sort_parts.append(joinphrase)\n            artist_credit_parts.append(joinphrase)\n\n    return (\n        artist_parts,\n        artist_sort_parts,\n        artist_credit_parts,\n    )\n\n\ndef track_url(trackid: str) -> str:\n    return urljoin(BASE_URL, f\"recording/{trackid}\")\n\n\ndef _flatten_artist_credit(credit: list[JSONDict]) -> tuple[str, str, str]:\n    \"\"\"Given a list representing an ``artist-credit`` block, flatten the\n    data into a triple of joined artist name strings: canonical, sort, and\n    credit.\n    \"\"\"\n    artist_parts, artist_sort_parts, artist_credit_parts = _multi_artist_credit(\n        credit, include_join_phrase=True\n    )\n    return (\n        \"\".join(artist_parts),\n        \"\".join(artist_sort_parts),\n        \"\".join(artist_credit_parts),\n    )\n\n\ndef _artist_ids(credit: list[JSONDict]) -> list[str]:\n    \"\"\"\n    Given a list representing an ``artist-credit``,\n    return a list of artist IDs\n    \"\"\"\n    artist_ids: list[str] = []\n    for el in credit:\n        if isinstance(el, dict):\n            artist_ids.append(el[\"artist\"][\"id\"])\n\n    return artist_ids\n\n\ndef _get_related_artist_names(relations, relation_type):\n    \"\"\"Given a list representing the artist relationships extract the names of\n    the remixers and concatenate them.\n    \"\"\"\n    related_artists = []\n\n    for relation in relations:\n        if relation[\"type\"] == relation_type:\n            related_artists.append(relation[\"artist\"][\"name\"])\n\n    return \", \".join(related_artists)\n\n\ndef album_url(albumid: str) -> str:\n    return urljoin(BASE_URL, f\"release/{albumid}\")\n\n\ndef _preferred_release_event(\n    release: dict[str, Any],\n) -> tuple[str | None, str | None]:\n    \"\"\"Given a release, select and return the user's preferred release\n    event as a tuple of (country, release_date). Fall back to the\n    default release event if a preferred event is not found.\n    \"\"\"\n    preferred_countries: Sequence[str] = config[\"match\"][\"preferred\"][\n        \"countries\"\n    ].as_str_seq()\n\n    for country in preferred_countries:\n        for event in release.get(\"release-events\", {}):\n            try:\n                if area := event.get(\"area\"):\n                    if country in area[\"iso-3166-1-codes\"]:\n                        return country, event[\"date\"]\n            except KeyError:\n                pass\n\n    return release.get(\"country\"), release.get(\"date\")\n\n\ndef _set_date_str(\n    info: beets.autotag.hooks.AlbumInfo,\n    date_str: str,\n    original: bool = False,\n):\n    \"\"\"Given a (possibly partial) YYYY-MM-DD string and an AlbumInfo\n    object, set the object's release date fields appropriately. If\n    `original`, then set the original_year, etc., fields.\n    \"\"\"\n    if date_str:\n        date_parts = date_str.split(\"-\")\n        for key in (\"year\", \"month\", \"day\"):\n            if date_parts:\n                date_part = date_parts.pop(0)\n                try:\n                    date_num = int(date_part)\n                except ValueError:\n                    continue\n\n                if original:\n                    key = f\"original_{key}\"\n                setattr(info, key, date_num)\n\n\ndef _merge_pseudo_and_actual_album(\n    pseudo: beets.autotag.hooks.AlbumInfo, actual: beets.autotag.hooks.AlbumInfo\n) -> beets.autotag.hooks.AlbumInfo:\n    \"\"\"\n    Merges a pseudo release with its actual release.\n\n    This implementation is naive, it doesn't overwrite fields,\n    like status or ids.\n\n    According to the ticket PICARD-145, the main release id should be used.\n    But the ticket has been in limbo since over a decade now.\n    It also suggests the introduction of the tag `musicbrainz_pseudoreleaseid`,\n    but as of this field can't be found in any official Picard docs,\n    hence why we did not implement that for now.\n    \"\"\"\n    merged = pseudo.copy()\n    from_actual = {\n        k: actual[k]\n        for k in [\n            \"media\",\n            \"mediums\",\n            \"country\",\n            \"catalognum\",\n            \"year\",\n            \"month\",\n            \"day\",\n            \"original_year\",\n            \"original_month\",\n            \"original_day\",\n            \"label\",\n            \"barcode\",\n            \"asin\",\n            \"style\",\n            \"genre\",\n        ]\n    }\n    merged.update(from_actual)\n    return merged\n\n\nclass MusicBrainzPlugin(\n    MusicBrainzAPIMixin, SearchApiMetadataSourcePlugin[IDResponse]\n):\n    @cached_property\n    def genres_field(self) -> str:\n        return f\"{self.config['genres_tag'].as_choice(['genre', 'tag'])}s\"\n\n    def __init__(self):\n        \"\"\"Set up the python-musicbrainz-ngs module according to settings\n        from the beets configuration. This should be called at startup.\n        \"\"\"\n        super().__init__()\n        self.config.add(\n            {\n                \"genres\": False,\n                \"genres_tag\": \"genre\",\n                \"external_ids\": {\n                    \"discogs\": False,\n                    \"bandcamp\": False,\n                    \"spotify\": False,\n                    \"deezer\": False,\n                    \"tidal\": False,\n                },\n                \"extra_tags\": [],\n            },\n        )\n        # TODO: Remove in 3.0.0\n        with suppress(NotFoundError):\n            self.config[\"search_limit\"] = self.config[\"match\"][\n                \"searchlimit\"\n            ].get()\n            deprecate_for_user(\n                self._log,\n                \"'musicbrainz.searchlimit' configuration option\",\n                \"'musicbrainz.search_limit'\",\n            )\n\n    def track_info(\n        self,\n        recording: JSONDict,\n        index: int | None = None,\n        medium: int | None = None,\n        medium_index: int | None = None,\n        medium_total: int | None = None,\n    ) -> beets.autotag.hooks.TrackInfo:\n        \"\"\"Translates a MusicBrainz recording result dictionary into a beets\n        ``TrackInfo`` object. Three parameters are optional and are used\n        only for tracks that appear on releases (non-singletons): ``index``,\n        the overall track number; ``medium``, the disc number;\n        ``medium_index``, the track's index on its medium; ``medium_total``,\n        the number of tracks on the medium. Each number is a 1-based index.\n        \"\"\"\n        title = _key_with_preferred_alias(recording, key=\"title\")\n\n        info = beets.autotag.hooks.TrackInfo(\n            title=title,\n            track_id=recording[\"id\"],\n            index=index,\n            medium=medium,\n            medium_index=medium_index,\n            medium_total=medium_total,\n            data_source=self.data_source,\n            data_url=track_url(recording[\"id\"]),\n        )\n\n        if recording.get(\"artist-credit\"):\n            # Get the artist names.\n            (\n                info.artist,\n                info.artist_sort,\n                info.artist_credit,\n            ) = _flatten_artist_credit(recording[\"artist-credit\"])\n\n            (\n                info.artists,\n                info.artists_sort,\n                info.artists_credit,\n            ) = _multi_artist_credit(\n                recording[\"artist-credit\"], include_join_phrase=False\n            )\n\n            info.artists_ids = _artist_ids(recording[\"artist-credit\"])\n            info.artist_id = info.artists_ids[0]\n\n        if recording.get(\"artist-relations\"):\n            info.remixer = _get_related_artist_names(\n                recording[\"artist-relations\"], relation_type=\"remixer\"\n            )\n\n        if recording.get(\"length\"):\n            info.length = int(recording[\"length\"]) / 1000.0\n\n        info.trackdisambig = recording.get(\"disambiguation\")\n\n        if recording.get(\"isrcs\"):\n            info.isrc = \";\".join(recording[\"isrcs\"])\n\n        lyricist = []\n        composer = []\n        composer_sort = []\n        for work_relation in recording.get(\"work-relations\", ()):\n            if work_relation[\"type\"] != \"performance\":\n                continue\n            info.work = work_relation[\"work\"][\"title\"]\n            info.mb_workid = work_relation[\"work\"][\"id\"]\n            if \"disambiguation\" in work_relation[\"work\"]:\n                info.work_disambig = work_relation[\"work\"][\"disambiguation\"]\n\n            for artist_relation in work_relation[\"work\"].get(\n                \"artist-relations\", ()\n            ):\n                if \"type\" in artist_relation:\n                    type = artist_relation[\"type\"]\n                    if type == \"lyricist\":\n                        lyricist.append(artist_relation[\"artist\"][\"name\"])\n                    elif type == \"composer\":\n                        composer.append(artist_relation[\"artist\"][\"name\"])\n                        composer_sort.append(\n                            artist_relation[\"artist\"][\"sort-name\"]\n                        )\n        if lyricist:\n            info.lyricist = \", \".join(lyricist)\n        if composer:\n            info.composer = \", \".join(composer)\n            info.composer_sort = \", \".join(composer_sort)\n\n        arranger = []\n        for artist_relation in recording.get(\"artist-relations\", ()):\n            if \"type\" in artist_relation:\n                type = artist_relation[\"type\"]\n                if type == \"arranger\":\n                    arranger.append(artist_relation[\"artist\"][\"name\"])\n        if arranger:\n            info.arranger = \", \".join(arranger)\n\n        # Supplementary fields provided by plugins\n        extra_trackdatas = plugins.send(\"mb_track_extract\", data=recording)\n        for extra_trackdata in extra_trackdatas:\n            info.update(extra_trackdata)\n\n        return info\n\n    def album_info(self, release: JSONDict) -> beets.autotag.hooks.AlbumInfo:\n        \"\"\"Takes a MusicBrainz release result dictionary and returns a beets\n        AlbumInfo object containing the interesting data about that release.\n        \"\"\"\n        # Get artist name using join phrases.\n        artist_name, artist_sort_name, artist_credit_name = (\n            _flatten_artist_credit(release[\"artist-credit\"])\n        )\n\n        (\n            artists_names,\n            artists_sort_names,\n            artists_credit_names,\n        ) = _multi_artist_credit(\n            release[\"artist-credit\"], include_join_phrase=False\n        )\n\n        ntracks = sum(len(m.get(\"tracks\", [])) for m in release[\"media\"])\n\n        # The MusicBrainz API omits 'relations'\n        # when the release has more than 500 tracks. So we use browse_recordings\n        # on chunks of tracks to recover the same information in this case.\n        if ntracks > BROWSE_MAXTRACKS:\n            self._log.debug(\"Album {} has too many tracks\", release[\"id\"])\n            recording_list = []\n            for i in range(0, ntracks, BROWSE_CHUNKSIZE):\n                self._log.debug(\"Retrieving tracks starting at {}\", i)\n                recording_list.extend(\n                    self.mb_api.browse_recordings(\n                        release=release[\"id\"],\n                        limit=BROWSE_CHUNKSIZE,\n                        includes=BROWSE_INCLUDES,\n                        offset=i,\n                    )\n                )\n            recording_by_id = {r[\"id\"]: r for r in recording_list}\n            for medium in release[\"media\"]:\n                for track in medium[\"tracks\"]:\n                    track[\"recording\"] = recording_by_id[\n                        track[\"recording\"][\"id\"]\n                    ]\n\n        # Basic info.\n        track_infos = []\n        index = 0\n        for medium in release[\"media\"]:\n            disctitle = medium.get(\"title\")\n            format = medium.get(\"format\")\n\n            if format in config[\"match\"][\"ignored_media\"].as_str_seq():\n                continue\n\n            all_tracks = medium.get(\"tracks\", [])\n            if (\n                \"data-tracks\" in medium\n                and not config[\"match\"][\"ignore_data_tracks\"]\n            ):\n                all_tracks += medium[\"data-tracks\"]\n            track_count = len(all_tracks)\n\n            if \"pregap\" in medium:\n                all_tracks.insert(0, medium[\"pregap\"])\n\n            for track in all_tracks:\n                if (\n                    \"title\" in track[\"recording\"]\n                    and track[\"recording\"][\"title\"] in SKIPPED_TRACKS\n                ):\n                    continue\n\n                if (\n                    \"video\" in track[\"recording\"]\n                    and track[\"recording\"][\"video\"]\n                    and config[\"match\"][\"ignore_video_tracks\"]\n                ):\n                    continue\n\n                # Basic information from the recording.\n                index += 1\n                ti = self.track_info(\n                    track[\"recording\"],\n                    index,\n                    int(medium[\"position\"]),\n                    int(track[\"position\"]),\n                    track_count,\n                )\n                ti.release_track_id = track[\"id\"]\n                ti.disctitle = disctitle\n                ti.media = format\n                ti.track_alt = track[\"number\"]\n\n                # Prefer track data, where present, over recording data except\n                # if a preferred recording alias is available.\n                if track.get(\"title\") and not _preferred_alias(\n                    track[\"recording\"].get(\"aliases\", ())\n                ):\n                    ti.title = track[\"title\"]\n                if track.get(\"artist-credit\"):\n                    # Get the artist names.\n                    (\n                        ti.artist,\n                        ti.artist_sort,\n                        ti.artist_credit,\n                    ) = _flatten_artist_credit(track[\"artist-credit\"])\n\n                    (\n                        ti.artists,\n                        ti.artists_sort,\n                        ti.artists_credit,\n                    ) = _multi_artist_credit(\n                        track[\"artist-credit\"], include_join_phrase=False\n                    )\n\n                    ti.artists_ids = _artist_ids(track[\"artist-credit\"])\n                    ti.artist_id = ti.artists_ids[0]\n                if track.get(\"length\"):\n                    ti.length = int(track[\"length\"]) / (1000.0)\n\n                track_infos.append(ti)\n\n        album_artist_ids = _artist_ids(release[\"artist-credit\"])\n        release_title = _key_with_preferred_alias(release, key=\"title\")\n        info = beets.autotag.hooks.AlbumInfo(\n            album=release_title,\n            album_id=release[\"id\"],\n            artist=artist_name,\n            artist_id=album_artist_ids[0],\n            artists=artists_names,\n            artists_ids=album_artist_ids,\n            tracks=track_infos,\n            mediums=len(release[\"media\"]),\n            artist_sort=artist_sort_name,\n            artists_sort=artists_sort_names,\n            artist_credit=artist_credit_name,\n            artists_credit=artists_credit_names,\n            data_source=self.data_source,\n            data_url=album_url(release[\"id\"]),\n            barcode=release.get(\"barcode\"),\n        )\n        info.va = info.artist_id == VARIOUS_ARTISTS_ID\n        if info.va:\n            va_name = config[\"va_name\"].as_str()\n            info.artist = va_name\n            info.artist_sort = va_name\n            info.artists = [va_name]\n            info.artists_sort = [va_name]\n            info.artist_credit = va_name\n            info.artists_credit = [va_name]\n        info.asin = release.get(\"asin\")\n        info.releasegroup_id = release[\"release-group\"][\"id\"]\n        info.albumstatus = release.get(\"status\")\n\n        if release[\"release-group\"].get(\"title\"):\n            info.release_group_title = _key_with_preferred_alias(\n                release[\"release-group\"], key=\"title\"\n            )\n\n        # Get the disambiguation strings at the release and release group level.\n        if release[\"release-group\"].get(\"disambiguation\"):\n            info.releasegroupdisambig = release[\"release-group\"].get(\n                \"disambiguation\"\n            )\n        if release.get(\"disambiguation\"):\n            info.albumdisambig = release.get(\"disambiguation\")\n\n        if reltype := release[\"release-group\"].get(\"primary-type\"):\n            info.albumtype = reltype.lower()\n\n        # Set the new-style \"primary\" and \"secondary\" release types.\n        albumtypes = []\n        if \"primary-type\" in release[\"release-group\"]:\n            rel_primarytype = release[\"release-group\"][\"primary-type\"]\n            if rel_primarytype:\n                albumtypes.append(rel_primarytype.lower())\n        if \"secondary-types\" in release[\"release-group\"]:\n            if release[\"release-group\"][\"secondary-types\"]:\n                for sec_type in release[\"release-group\"][\"secondary-types\"]:\n                    albumtypes.append(sec_type.lower())\n        info.albumtypes = albumtypes\n\n        # Release events.\n        info.country, release_date = _preferred_release_event(release)\n        release_group_date = release[\"release-group\"].get(\"first-release-date\")\n        if not release_date:\n            # Fall back if release-specific date is not available.\n            release_date = release_group_date\n\n        if release_date:\n            _set_date_str(info, release_date, False)\n        _set_date_str(info, release_group_date, True)\n\n        # Label name.\n        if release.get(\"label-info\"):\n            label_info = release[\"label-info\"][0]\n            if label_info.get(\"label\"):\n                label = label_info[\"label\"][\"name\"]\n                if label != \"[no label]\":\n                    info.label = label\n            info.catalognum = label_info.get(\"catalog-number\")\n\n        # Text representation data.\n        if release.get(\"text-representation\"):\n            rep = release[\"text-representation\"]\n            info.script = rep.get(\"script\")\n            info.language = rep.get(\"language\")\n\n        # Media (format).\n        if release[\"media\"]:\n            # If all media are the same, use that medium name\n            if len({m.get(\"format\") for m in release[\"media\"]}) == 1:\n                info.media = release[\"media\"][0].get(\"format\")\n            # Otherwise, let's just call it \"Media\"\n            else:\n                info.media = \"Media\"\n\n        if self.config[\"genres\"]:\n            sources = [\n                release[\"release-group\"].get(self.genres_field, []),\n                release.get(self.genres_field, []),\n            ]\n            genres: Counter[str] = Counter()\n            for source in sources:\n                for genreitem in source:\n                    genres[genreitem[\"name\"]] += int(genreitem[\"count\"])\n            if genres:\n                info.genres = [\n                    genre\n                    for genre, _count in sorted(\n                        genres.items(), key=lambda g: -g[1]\n                    )\n                ]\n\n        # We might find links to external sources (Discogs, Bandcamp, ...)\n        external_ids = self.config[\"external_ids\"].get()\n        wanted_sources = {\n            site for site, wanted in external_ids.items() if wanted\n        }\n        if wanted_sources and (url_rels := release.get(\"url-relations\")):\n            urls = {}\n\n            for source, url in product(wanted_sources, url_rels):\n                if f\"{source}.com\" in (target := url[\"url\"][\"resource\"]):\n                    urls[source] = target\n                    self._log.debug(\n                        \"Found link to {} release via MusicBrainz\",\n                        source.capitalize(),\n                    )\n\n            for source, url in urls.items():\n                setattr(\n                    info, f\"{source}_album_id\", extract_release_id(source, url)\n                )\n\n        extra_albumdatas = plugins.send(\"mb_album_extract\", data=release)\n        for extra_albumdata in extra_albumdatas:\n            info.update(extra_albumdata)\n\n        return info\n\n    @cached_property\n    def extra_mb_field_by_tag(self) -> dict[str, str]:\n        \"\"\"Map configured extra tags to their MusicBrainz API field names.\n\n        Process user configuration to determine which additional MusicBrainz\n        fields should be included in search queries.\n        \"\"\"\n        mb_field_by_tag = {\n            t: FIELDS_TO_MB_KEYS[t]\n            for t in self.config[\"extra_tags\"].as_str_seq()\n            if t in FIELDS_TO_MB_KEYS\n        }\n        if mb_field_by_tag:\n            self._log.debug(\"Additional search terms: {}\", mb_field_by_tag)\n\n        return mb_field_by_tag\n\n    def get_album_criteria(\n        self, items: Sequence[Item], artist: str, album: str, va_likely: bool\n    ) -> dict[str, str]:\n        criteria = {\"release\": album} | (\n            {\"arid\": VARIOUS_ARTISTS_ID} if va_likely else {\"artist\": artist}\n        )\n\n        for tag, mb_field in self.extra_mb_field_by_tag.items():\n            if tag == \"tracks\":\n                value = str(len(items))\n            elif tag == \"alias\":\n                value = album\n            else:\n                most_common, _ = util.plurality(i.get(tag) for i in items)\n                value = str(most_common)\n                if tag == \"catalognum\":\n                    value = value.replace(\" \", \"\")\n\n            criteria[mb_field] = value\n\n        return criteria\n\n    def get_search_query_with_filters(\n        self,\n        query_type: QueryType,\n        items: Sequence[Item],\n        artist: str,\n        name: str,\n        va_likely: bool,\n    ) -> tuple[str, dict[str, str]]:\n        \"\"\"Build MusicBrainz criteria filters for album and recording search.\"\"\"\n\n        if query_type == \"album\":\n            criteria = self.get_album_criteria(items, artist, name, va_likely)\n        else:\n            criteria = {\"artist\": artist, \"recording\": name, \"alias\": name}\n\n        return \"\", {\n            k: _v for k, v in criteria.items() if (_v := v.lower().strip())\n        }\n\n    def get_search_response(self, params: SearchParams) -> Sequence[IDResponse]:\n        \"\"\"Search MusicBrainz and return release or recording result mappings.\"\"\"\n\n        mb_entity: Literal[\"release\", \"recording\"] = (\n            \"release\" if params.query_type == \"album\" else \"recording\"\n        )\n        return self.mb_api.search(\n            mb_entity, dict(params.filters), limit=params.limit\n        )\n\n    def album_for_id(\n        self, album_id: str\n    ) -> beets.autotag.hooks.AlbumInfo | None:\n        \"\"\"Fetches an album by its MusicBrainz ID and returns an AlbumInfo\n        object or None if the album is not found. May raise a\n        MusicBrainzAPIError.\n        \"\"\"\n        self._log.debug(\"Requesting MusicBrainz release {}\", album_id)\n        if not (albumid := self._extract_id(album_id)):\n            self._log.debug(\"Invalid MBID ({}).\", album_id)\n            return None\n\n        # A 404 error here is fine. e.g. re-importing a release that has\n        # been deleted on MusicBrainz.\n        try:\n            res = self.mb_api.get_release(albumid)\n        except HTTPNotFoundError:\n            self._log.debug(\"Release {} not found on MusicBrainz.\", albumid)\n            return None\n\n        # resolve linked release relations\n        actual_res = None\n\n        if res.get(\"status\") == \"Pseudo-Release\" and (\n            relations := res.get(\"release-relations\")\n        ):\n            for rel in relations:\n                if (\n                    rel[\"type\"] == \"transl-tracklisting\"\n                    and rel[\"direction\"] == \"backward\"\n                ):\n                    actual_res = self.mb_api.get_release(rel[\"release\"][\"id\"])\n\n        # release is potentially a pseudo release\n        release = self.album_info(res)\n\n        # should be None unless we're dealing with a pseudo release\n        if actual_res is not None:\n            actual_release = self.album_info(actual_res)\n            return _merge_pseudo_and_actual_album(release, actual_release)\n        else:\n            return release\n\n    def track_for_id(\n        self, track_id: str\n    ) -> beets.autotag.hooks.TrackInfo | None:\n        \"\"\"Fetches a track by its MusicBrainz ID. Returns a TrackInfo object\n        or None if no track is found. May raise a MusicBrainzAPIError.\n        \"\"\"\n        if not (trackid := self._extract_id(track_id)):\n            self._log.debug(\"Invalid MBID ({}).\", track_id)\n            return None\n\n        with suppress(HTTPNotFoundError):\n            return self.track_info(self.mb_api.get_recording(trackid))\n\n        return None\n"
  },
  {
    "path": "beetsplug/parentwork.py",
    "content": "# This file is part of beets.\n# Copyright 2017, Dorian Soergel.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Gets parent work, its disambiguation and id, composer, composer sort name\nand work composition date\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Any\n\nimport requests\n\nfrom beets import ui\nfrom beets.plugins import BeetsPlugin\n\nfrom ._utils.musicbrainz import MusicBrainzAPIMixin\n\n\nclass ParentWorkPlugin(MusicBrainzAPIMixin, BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n\n        self.config.add(\n            {\n                \"auto\": False,\n                \"force\": False,\n            }\n        )\n\n        if self.config[\"auto\"]:\n            self.import_stages = [self.imported]\n\n    def commands(self):\n        def func(lib, opts, args):\n            self.config.set_args(opts)\n            force_parent = self.config[\"force\"].get(bool)\n            write = ui.should_write()\n\n            for item in lib.items(args):\n                changed = self.find_work(item, force_parent, verbose=True)\n                if changed:\n                    item.store()\n                    if write:\n                        item.try_write()\n\n        command = ui.Subcommand(\n            \"parentwork\", help=\"fetch parent works, composers and dates\"\n        )\n\n        command.parser.add_option(\n            \"-f\",\n            \"--force\",\n            dest=\"force\",\n            action=\"store_true\",\n            default=None,\n            help=\"re-fetch when parent work is already present\",\n        )\n\n        command.func = func\n        return [command]\n\n    def imported(self, session, task):\n        \"\"\"Import hook for fetching parent works automatically.\"\"\"\n        force_parent = self.config[\"force\"].get(bool)\n\n        for item in task.imported_items():\n            self.find_work(item, force_parent, verbose=False)\n            item.store()\n\n    def get_info(self, item, work_info):\n        \"\"\"Given the parent work info dict, fetch parent_composer,\n        parent_composer_sort, parentwork, parentwork_disambig, mb_workid and\n        composer_ids.\n        \"\"\"\n\n        parent_composer = []\n        parent_composer_sort = []\n        parentwork_info = {}\n\n        composer_exists = False\n        for artist in work_info.get(\"artist-relations\", []):\n            if artist[\"type\"] == \"composer\":\n                composer_exists = True\n                parent_composer.append(artist[\"artist\"][\"name\"])\n                parent_composer_sort.append(artist[\"artist\"][\"sort-name\"])\n                if \"end\" in artist.keys():\n                    parentwork_info[\"parentwork_date\"] = artist[\"end\"]\n\n            parentwork_info[\"parent_composer\"] = \", \".join(parent_composer)\n            parentwork_info[\"parent_composer_sort\"] = \", \".join(\n                parent_composer_sort\n            )\n\n        if not composer_exists:\n            self._log.debug(\n                \"no composer for {}; add one at \"\n                \"https://musicbrainz.org/work/{}\",\n                item,\n                work_info[\"id\"],\n            )\n\n        parentwork_info[\"parentwork\"] = work_info[\"title\"]\n        parentwork_info[\"mb_parentworkid\"] = work_info[\"id\"]\n\n        if \"disambiguation\" in work_info:\n            parentwork_info[\"parentwork_disambig\"] = work_info[\"disambiguation\"]\n\n        else:\n            parentwork_info[\"parentwork_disambig\"] = None\n\n        return parentwork_info\n\n    def find_work(self, item, force, verbose):\n        \"\"\"Finds the parent work of a recording and populates the tags\n        accordingly.\n\n        The parent work is found recursively, by finding the direct parent\n        repeatedly until there are no more links in the chain. We return the\n        final, topmost work in the chain.\n\n        Namely, the tags parentwork, parentwork_disambig, mb_parentworkid,\n        parent_composer, parent_composer_sort and work_date are populated.\n        \"\"\"\n\n        if not item.mb_workid:\n            self._log.info(\n                \"No work for {0}, add one at https://musicbrainz.org/recording/{0.mb_trackid}\",\n                item,\n            )\n            return\n\n        hasparent = hasattr(item, \"parentwork\")\n        work_changed = True\n        if hasattr(item, \"parentwork_workid_current\"):\n            work_changed = item.parentwork_workid_current != item.mb_workid\n        if force or not hasparent or work_changed:\n            try:\n                work_info, work_date = self.find_parentwork_info(item.mb_workid)\n            except requests.exceptions.RequestException:\n                self._log.debug(\"error fetching work\", item, exc_info=True)\n                return\n            parent_info = self.get_info(item, work_info)\n            parent_info[\"parentwork_workid_current\"] = item.mb_workid\n            if \"parent_composer\" in parent_info:\n                self._log.debug(\n                    \"Work fetched: {} - {}\",\n                    parent_info[\"parentwork\"],\n                    parent_info[\"parent_composer\"],\n                )\n            else:\n                self._log.debug(\n                    \"Work fetched: {} - no parent composer\",\n                    parent_info[\"parentwork\"],\n                )\n\n        elif hasparent:\n            self._log.debug(\"{}: Work present, skipping\", item)\n            return\n\n        # apply all non-null values to the item\n        for key, value in parent_info.items():\n            if value:\n                item[key] = value\n\n        if work_date:\n            item[\"work_date\"] = work_date\n        if verbose:\n            return ui.show_model_changes(\n                item,\n                fields=[\n                    \"parentwork\",\n                    \"parentwork_disambig\",\n                    \"mb_parentworkid\",\n                    \"parent_composer\",\n                    \"parent_composer_sort\",\n                    \"work_date\",\n                    \"parentwork_workid_current\",\n                    \"parentwork_date\",\n                ],\n            )\n\n    def find_parentwork_info(\n        self, mb_workid: str\n    ) -> tuple[dict[str, Any], str | None]:\n        \"\"\"Get the MusicBrainz information dict about a parent work, including\n        the artist relations, and the composition date for a work's parent work.\n        \"\"\"\n        work_date = None\n\n        parent_id: str | None = mb_workid\n\n        while parent_id:\n            current_id = parent_id\n            work_info = self.mb_api.get_work(\n                current_id, includes=[\"work-rels\", \"artist-rels\"]\n            )\n            work_date = work_date or next(\n                (\n                    end\n                    for a in work_info.get(\"artist-relations\", [])\n                    if a[\"type\"] == \"composer\" and (end := a.get(\"end\"))\n                ),\n                None,\n            )\n            parent_id = next(\n                (\n                    w[\"work\"][\"id\"]\n                    for w in work_info.get(\"work-relations\", [])\n                    if w[\"type\"] == \"parts\" and w[\"direction\"] == \"backward\"\n                ),\n                None,\n            )\n\n        return work_info, work_date\n"
  },
  {
    "path": "beetsplug/permissions.py",
    "content": "\"\"\"Fixes file permissions after the file gets written on import. Put something\nlike the following in your config.yaml to configure:\n\n    permissions:\n            file: 644\n            dir: 755\n\"\"\"\n\nimport os\nimport stat\n\nfrom beets import config\nfrom beets.plugins import BeetsPlugin\nfrom beets.util import ancestry, displayable_path, syspath\n\n\ndef convert_perm(perm):\n    \"\"\"Convert a string to an integer, interpreting the text as octal.\n    Or, if `perm` is an integer, reinterpret it as an octal number that\n    has been \"misinterpreted\" as decimal.\n    \"\"\"\n    if isinstance(perm, int):\n        perm = str(perm)\n    return int(perm, 8)\n\n\ndef check_permissions(path, permission):\n    \"\"\"Check whether the file's permissions equal the given vector.\n    Return a boolean.\n    \"\"\"\n    return oct(stat.S_IMODE(os.stat(syspath(path)).st_mode)) == oct(permission)\n\n\ndef assert_permissions(path, permission, log):\n    \"\"\"Check whether the file's permissions are as expected, otherwise,\n    log a warning message. Return a boolean indicating the match, like\n    `check_permissions`.\n    \"\"\"\n    if not check_permissions(path, permission):\n        log.warning(\"could not set permissions on {}\", displayable_path(path))\n        log.debug(\n            \"set permissions to {}, but permissions are now {}\",\n            permission,\n            os.stat(syspath(path)).st_mode & 0o777,\n        )\n\n\ndef dirs_in_library(library, item):\n    \"\"\"Creates a list of ancestor directories in the beets library path.\"\"\"\n    return [\n        ancestor for ancestor in ancestry(item) if ancestor.startswith(library)\n    ][1:]\n\n\nclass Permissions(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n\n        # Adding defaults.\n        self.config.add(\n            {\n                \"file\": \"644\",\n                \"dir\": \"755\",\n            }\n        )\n\n        self.register_listener(\"item_imported\", self.fix)\n        self.register_listener(\"album_imported\", self.fix)\n        self.register_listener(\"art_set\", self.fix_art)\n\n    def fix(self, lib, item=None, album=None):\n        \"\"\"Fix the permissions for an imported Item or Album.\"\"\"\n        files = []\n        dirs = set()\n        if item:\n            files.append(item.path)\n            dirs.update(dirs_in_library(lib.directory, item.path))\n        elif album:\n            for album_item in album.items():\n                files.append(album_item.path)\n                dirs.update(dirs_in_library(lib.directory, album_item.path))\n        self.set_permissions(files=files, dirs=dirs)\n\n    def fix_art(self, album):\n        \"\"\"Fix the permission for Album art file.\"\"\"\n        if album.artpath:\n            self.set_permissions(files=[album.artpath])\n\n    def set_permissions(self, files=[], dirs=[]):\n        # Get the configured permissions. The user can specify this either a\n        # string (in YAML quotes) or, for convenience, as an integer so the\n        # quotes can be omitted. In the latter case, we need to reinterpret the\n        # integer as octal, not decimal.\n        file_perm = config[\"permissions\"][\"file\"].get()\n        dir_perm = config[\"permissions\"][\"dir\"].get()\n        file_perm = convert_perm(file_perm)\n        dir_perm = convert_perm(dir_perm)\n\n        for path in files:\n            # Changing permissions on the destination file.\n            self._log.debug(\n                \"setting file permissions on {}\",\n                displayable_path(path),\n            )\n            if not check_permissions(path, file_perm):\n                os.chmod(syspath(path), file_perm)\n\n            # Checks if the destination path has the permissions configured.\n            assert_permissions(path, file_perm, self._log)\n\n        # Change permissions for the directories.\n        for path in dirs:\n            # Changing permissions on the destination directory.\n            self._log.debug(\n                \"setting directory permissions on {}\",\n                displayable_path(path),\n            )\n            if not check_permissions(path, dir_perm):\n                os.chmod(syspath(path), dir_perm)\n\n            # Checks if the destination path has the permissions configured.\n            assert_permissions(path, dir_perm, self._log)\n"
  },
  {
    "path": "beetsplug/play.py",
    "content": "# This file is part of beets.\n# Copyright 2016, David Hamp-Gonsalves\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Send the results of a query to the configured music player as a playlist.\"\"\"\n\nimport random\nimport shlex\nimport subprocess\nfrom os.path import relpath\n\nfrom beets import config, ui, util\nfrom beets.plugins import BeetsPlugin\nfrom beets.ui import Subcommand\nfrom beets.util import PromptChoice, get_temp_filename\nfrom beets.util.color import colorize\n\n# Indicate where arguments should be inserted into the command string.\n# If this is missing, they're placed at the end.\nARGS_MARKER = \"$args\"\n\n# Indicate where the playlist file (with absolute path) should be inserted into\n# the command string. If this is missing, its placed at the end, but before\n# arguments.\nPLS_MARKER = \"$playlist\"\n\n\ndef play(\n    command_str,\n    selection,\n    paths,\n    open_args,\n    log,\n    item_type=\"track\",\n    keep_open=False,\n):\n    \"\"\"Play items in paths with command_str and optional arguments. If\n    keep_open, return to beets, otherwise exit once command runs.\n    \"\"\"\n    # Print number of tracks or albums to be played, log command to be run.\n    item_type += \"s\" if len(selection) > 1 else \"\"\n    ui.print_(f\"Playing {len(selection)} {item_type}.\")\n    log.debug(\"executing command: {} {!r}\", command_str, open_args)\n\n    try:\n        if keep_open:\n            command = shlex.split(command_str)\n            command = command + open_args\n            subprocess.call(command)\n        else:\n            util.interactive_open(open_args, command_str)\n    except OSError as exc:\n        raise ui.UserError(f\"Could not play the query: {exc}\")\n\n\nclass PlayPlugin(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n\n        config[\"play\"].add(\n            {\n                \"command\": None,\n                \"use_folders\": False,\n                \"relative_to\": None,\n                \"raw\": False,\n                \"warning_threshold\": 100,\n                \"bom\": False,\n            }\n        )\n\n        self.register_listener(\n            \"before_choose_candidate\", self.before_choose_candidate_listener\n        )\n\n    def commands(self):\n        play_command = Subcommand(\n            \"play\", help=\"send music to a player as a playlist\"\n        )\n        play_command.parser.add_album_option()\n        play_command.parser.add_option(\n            \"-A\",\n            \"--args\",\n            action=\"store\",\n            help=\"add additional arguments to the command\",\n        )\n        play_command.parser.add_option(\n            \"-R\",\n            \"--randomize\",\n            action=\"store_true\",\n            help=\"randomize the order of playlist entries\",\n        )\n        play_command.parser.add_option(\n            \"-y\",\n            \"--yes\",\n            action=\"store_true\",\n            help=\"skip the warning threshold\",\n        )\n        play_command.func = self._play_command\n        return [play_command]\n\n    def _play_command(self, lib, opts, args):\n        \"\"\"The CLI command function for `beet play`. Create a list of paths\n        from query, determine if tracks or albums are to be played.\n        \"\"\"\n        use_folders = config[\"play\"][\"use_folders\"].get(bool)\n        relative_to = config[\"play\"][\"relative_to\"].get()\n        if relative_to:\n            relative_to = util.normpath(relative_to)\n        # Perform search by album and add folders rather than tracks to\n        # playlist.\n        if opts.album:\n            selection = lib.albums(args)\n            paths = []\n\n            sort = lib.get_default_album_sort()\n            for album in selection:\n                if use_folders:\n                    paths.append(album.item_dir())\n                else:\n                    paths.extend(item.path for item in sort.sort(album.items()))\n            item_type = \"album\"\n\n        # Perform item query and add tracks to playlist.\n        else:\n            selection = lib.items(args)\n            paths = [item.path for item in selection]\n            item_type = \"track\"\n\n        if relative_to:\n            paths = [relpath(path, relative_to) for path in paths]\n\n        if not selection:\n            ui.print_(colorize(\"text_warning\", f\"No {item_type} to play.\"))\n            return\n\n        if opts.randomize:\n            random.shuffle(paths)\n\n        open_args = self._playlist_or_paths(paths)\n        open_args_str = [\n            p.decode(\"utf-8\") for p in self._playlist_or_paths(paths)\n        ]\n        command_str = self._command_str(opts.args)\n\n        if PLS_MARKER in command_str:\n            if not config[\"play\"][\"raw\"]:\n                command_str = command_str.replace(\n                    PLS_MARKER, \"\".join(open_args_str)\n                )\n                self._log.debug(\n                    \"command altered by PLS_MARKER to: {}\", command_str\n                )\n                open_args = []\n            else:\n                command_str = command_str.replace(PLS_MARKER, \" \")\n\n        # Check if the selection exceeds configured threshold. If True,\n        # cancel, otherwise proceed with play command.\n        if opts.yes or not self._exceeds_threshold(\n            selection, command_str, open_args, item_type\n        ):\n            play(command_str, selection, paths, open_args, self._log, item_type)\n\n    def _command_str(self, args=None):\n        \"\"\"Create a command string from the config command and optional args.\"\"\"\n        command_str = config[\"play\"][\"command\"].get()\n        if not command_str:\n            return util.open_anything()\n        # Add optional arguments to the player command.\n        if args:\n            if ARGS_MARKER in command_str:\n                return command_str.replace(ARGS_MARKER, args)\n            else:\n                return f\"{command_str} {args}\"\n        else:\n            # Don't include the marker in the command.\n            return command_str.replace(f\" {ARGS_MARKER}\", \"\")\n\n    def _playlist_or_paths(self, paths):\n        \"\"\"Return either the raw paths of items or a playlist of the items.\"\"\"\n        if config[\"play\"][\"raw\"]:\n            return paths\n        else:\n            return [self._create_tmp_playlist(paths)]\n            return [shlex.quote(self._create_tmp_playlist(paths))]\n\n    def _exceeds_threshold(\n        self, selection, command_str, open_args, item_type=\"track\"\n    ):\n        \"\"\"Prompt user whether to abort if playlist exceeds threshold. If\n        True, cancel playback. If False, execute play command.\n        \"\"\"\n        warning_threshold = config[\"play\"][\"warning_threshold\"].get(int)\n\n        # Warn user before playing any huge playlists.\n        if warning_threshold and len(selection) > warning_threshold:\n            if len(selection) > 1:\n                item_type += \"s\"\n\n            ui.print_(\n                colorize(\n                    \"text_warning\",\n                    f\"You are about to queue {len(selection)} {item_type}.\",\n                )\n            )\n\n            if ui.input_options((\"Continue\", \"Abort\")) == \"a\":\n                return True\n\n        return False\n\n    def _create_tmp_playlist(self, paths_list):\n        \"\"\"Create a temporary .m3u file. Return the filename.\"\"\"\n        utf8_bom = config[\"play\"][\"bom\"].get(bool)\n        filename = get_temp_filename(__name__, suffix=\".m3u\")\n        with open(filename, \"wb\") as m3u:\n            if utf8_bom:\n                m3u.write(b\"\\xef\\xbb\\xbf\")\n\n            for item in paths_list:\n                m3u.write(item + b\"\\n\")\n\n        return filename\n\n    def before_choose_candidate_listener(self, session, task):\n        \"\"\"Append a \"Play\" choice to the interactive importer prompt.\"\"\"\n        return [PromptChoice(\"y\", \"plaY\", self.importer_play)]\n\n    def importer_play(self, session, task):\n        \"\"\"Get items from current import task and send to play function.\"\"\"\n        selection = task.items\n        paths = [item.path for item in selection]\n\n        open_args = self._playlist_or_paths(paths)\n        command_str = self._command_str()\n\n        if not self._exceeds_threshold(selection, command_str, open_args):\n            play(\n                command_str,\n                selection,\n                paths,\n                open_args,\n                self._log,\n                keep_open=True,\n            )\n"
  },
  {
    "path": "beetsplug/playlist.py",
    "content": "# This file is part of beets.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\nfrom __future__ import annotations\n\nimport os\nimport tempfile\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, ClassVar\n\nimport beets\nfrom beets.dbcore.query import BLOB_TYPE, InQuery\nfrom beets.util import path_as_posix\n\nif TYPE_CHECKING:\n    from collections.abc import Sequence\n\n    from beets.dbcore.query import FieldQueryType\n\n\ndef is_m3u_file(path: str) -> bool:\n    return Path(path).suffix.lower() in {\".m3u\", \".m3u8\"}\n\n\nclass PlaylistQuery(InQuery[bytes]):\n    \"\"\"Matches files listed by a playlist file.\"\"\"\n\n    @property\n    def subvals(self) -> Sequence[BLOB_TYPE]:\n        return [BLOB_TYPE(p) for p in self.pattern]\n\n    def __init__(self, _, pattern: str, __):\n        config = beets.config[\"playlist\"]\n\n        # Get the full path to the playlist\n        playlist_paths = (\n            pattern,\n            os.path.abspath(\n                os.path.join(\n                    config[\"playlist_dir\"].as_filename(),\n                    f\"{pattern}.m3u\",\n                )\n            ),\n        )\n\n        paths = []\n        for playlist_path in playlist_paths:\n            if not is_m3u_file(playlist_path):\n                # This is not am M3U playlist, skip this candidate\n                continue\n\n            try:\n                f = open(beets.util.syspath(playlist_path), mode=\"rb\")\n            except OSError:\n                continue\n\n            if config[\"relative_to\"].get() == \"library\":\n                relative_to = beets.config[\"directory\"].as_filename()\n            elif config[\"relative_to\"].get() == \"playlist\":\n                relative_to = os.path.dirname(playlist_path)\n            else:\n                relative_to = config[\"relative_to\"].as_filename()\n            relative_to_bytes = beets.util.bytestring_path(relative_to)\n\n            for line in f:\n                if line[0] == \"#\":\n                    # ignore comments, and extm3u extension\n                    continue\n\n                paths.append(\n                    beets.util.normpath(\n                        os.path.join(relative_to_bytes, line.rstrip())\n                    )\n                )\n            f.close()\n            break\n        super().__init__(\"path\", paths)\n\n\nclass PlaylistPlugin(beets.plugins.BeetsPlugin):\n    item_queries: ClassVar[dict[str, FieldQueryType]] = {\n        \"playlist\": PlaylistQuery\n    }\n\n    def __init__(self):\n        super().__init__()\n        self.config.add(\n            {\n                \"auto\": False,\n                \"playlist_dir\": \".\",\n                \"relative_to\": \"library\",\n                \"forward_slash\": False,\n            }\n        )\n\n        self.playlist_dir = self.config[\"playlist_dir\"].as_filename()\n        self.changes = {}\n\n        if self.config[\"relative_to\"].get() == \"library\":\n            self.relative_to = beets.util.bytestring_path(\n                beets.config[\"directory\"].as_filename()\n            )\n        elif self.config[\"relative_to\"].get() != \"playlist\":\n            self.relative_to = beets.util.bytestring_path(\n                self.config[\"relative_to\"].as_filename()\n            )\n        else:\n            self.relative_to = None\n\n        if self.config[\"auto\"]:\n            self.register_listener(\"item_moved\", self.item_moved)\n            self.register_listener(\"item_removed\", self.item_removed)\n            self.register_listener(\"cli_exit\", self.cli_exit)\n\n    def item_moved(self, item, source, destination):\n        self.changes[source] = destination\n\n    def item_removed(self, item):\n        if not os.path.exists(beets.util.syspath(item.path)):\n            self.changes[item.path] = None\n\n    def cli_exit(self, lib):\n        for playlist in self.find_playlists():\n            self._log.info(\"Updating playlist: {}\", playlist)\n            base_dir = beets.util.bytestring_path(\n                self.relative_to\n                if self.relative_to\n                else os.path.dirname(playlist)\n            )\n\n            try:\n                self.update_playlist(playlist, base_dir)\n            except beets.util.FilesystemError:\n                self._log.error(\"Failed to update playlist: {}\", playlist)\n\n    def find_playlists(self):\n        \"\"\"Find M3U playlists in the playlist directory.\"\"\"\n        playlist_dir = beets.util.syspath(self.playlist_dir)\n        try:\n            dir_contents = os.listdir(playlist_dir)\n        except OSError:\n            self._log.warning(\n                \"Unable to open playlist directory {.playlist_dir}\", self\n            )\n            return\n\n        for filename in dir_contents:\n            if is_m3u_file(filename):\n                yield os.path.join(self.playlist_dir, filename)\n\n    def update_playlist(self, filename, base_dir):\n        \"\"\"Find M3U playlists in the specified directory.\"\"\"\n        changes = 0\n        deletions = 0\n\n        with tempfile.NamedTemporaryFile(mode=\"w+b\", delete=False) as tempfp:\n            new_playlist = tempfp.name\n            with open(filename, mode=\"rb\") as fp:\n                for line in fp:\n                    original_path = line.rstrip(b\"\\r\\n\")\n\n                    # Ensure that path from playlist is absolute\n                    is_relative = not os.path.isabs(line)\n                    if is_relative:\n                        lookup = os.path.join(base_dir, original_path)\n                    else:\n                        lookup = original_path\n\n                    try:\n                        new_path = self.changes[beets.util.normpath(lookup)]\n                    except KeyError:\n                        if self.config[\"forward_slash\"]:\n                            line = path_as_posix(line)\n                        tempfp.write(line)\n                    else:\n                        if new_path is None:\n                            # Item has been deleted\n                            deletions += 1\n                            continue\n\n                        changes += 1\n                        if is_relative:\n                            new_path = os.path.relpath(new_path, base_dir)\n                        line = line.replace(original_path, new_path)\n                        if self.config[\"forward_slash\"]:\n                            line = path_as_posix(line)\n                        tempfp.write(line)\n\n        if changes or deletions:\n            self._log.info(\n                \"Updated playlist {} ({} changes, {} deletions)\",\n                filename,\n                changes,\n                deletions,\n            )\n            beets.util.copy(new_playlist, filename, replace=True)\n        beets.util.remove(new_playlist)\n"
  },
  {
    "path": "beetsplug/plexupdate.py",
    "content": "\"\"\"Updates an Plex library whenever the beets library is changed.\n\nPlex Home users enter the Plex Token to enable updating.\nPut something like the following in your config.yaml to configure:\n    plex:\n        host: localhost\n        port: 32400\n        token: token\n\"\"\"\n\nfrom urllib.parse import urlencode, urljoin\nfrom xml.etree import ElementTree\n\nimport requests\n\nfrom beets import config\nfrom beets.plugins import BeetsPlugin\n\n\ndef get_music_section(\n    host, port, token, library_name, secure, ignore_cert_errors\n):\n    \"\"\"Getting the section key for the music library in Plex.\"\"\"\n    api_endpoint = append_token(\"library/sections\", token)\n    url = urljoin(f\"{get_protocol(secure)}://{host}:{port}\", api_endpoint)\n\n    # Sends request.\n    r = requests.get(\n        url,\n        verify=not ignore_cert_errors,\n        timeout=10,\n    )\n\n    # Parse xml tree and extract music section key.\n    tree = ElementTree.fromstring(r.content)\n    for child in tree.findall(\"Directory\"):\n        if child.get(\"title\") == library_name:\n            return child.get(\"key\")\n\n\ndef update_plex(host, port, token, library_name, secure, ignore_cert_errors):\n    \"\"\"Ignore certificate errors if configured to.\"\"\"\n    if ignore_cert_errors:\n        import urllib3\n\n        urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\n    \"\"\"Sends request to the Plex api to start a library refresh.\n    \"\"\"\n    # Getting section key and build url.\n    section_key = get_music_section(\n        host, port, token, library_name, secure, ignore_cert_errors\n    )\n    api_endpoint = f\"library/sections/{section_key}/refresh\"\n    api_endpoint = append_token(api_endpoint, token)\n    url = urljoin(f\"{get_protocol(secure)}://{host}:{port}\", api_endpoint)\n\n    # Sends request and returns requests object.\n    r = requests.get(\n        url,\n        verify=not ignore_cert_errors,\n        timeout=10,\n    )\n    return r\n\n\ndef append_token(url, token):\n    \"\"\"Appends the Plex Home token to the api call if required.\"\"\"\n    if token:\n        url += f\"?{urlencode({'X-Plex-Token': token})}\"\n    return url\n\n\ndef get_protocol(secure):\n    if secure:\n        return \"https\"\n    else:\n        return \"http\"\n\n\nclass PlexUpdate(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n\n        # Adding defaults.\n        config[\"plex\"].add(\n            {\n                \"host\": \"localhost\",\n                \"port\": 32400,\n                \"token\": \"\",\n                \"library_name\": \"Music\",\n                \"secure\": False,\n                \"ignore_cert_errors\": False,\n            }\n        )\n\n        config[\"plex\"][\"token\"].redact = True\n        self.register_listener(\"database_change\", self.listen_for_db_change)\n\n    def listen_for_db_change(self, lib, model):\n        \"\"\"Listens for beets db change and register the update for the end\"\"\"\n        self.register_listener(\"cli_exit\", self.update)\n\n    def update(self, lib):\n        \"\"\"When the client exists try to send refresh request to Plex server.\"\"\"\n        self._log.info(\"Updating Plex library...\")\n\n        # Try to send update request.\n        try:\n            update_plex(\n                config[\"plex\"][\"host\"].get(),\n                config[\"plex\"][\"port\"].get(),\n                config[\"plex\"][\"token\"].get(),\n                config[\"plex\"][\"library_name\"].get(),\n                config[\"plex\"][\"secure\"].get(bool),\n                config[\"plex\"][\"ignore_cert_errors\"].get(bool),\n            )\n            self._log.info(\"... started.\")\n\n        except requests.exceptions.RequestException:\n            self._log.warning(\"Update failed.\")\n"
  },
  {
    "path": "beetsplug/random.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Philippe Mongeau.\n# Copyright 2025, Sebastian Mohr.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\nfrom __future__ import annotations\n\nimport random\nfrom itertools import groupby, islice\nfrom operator import methodcaller\nfrom typing import TYPE_CHECKING\n\nfrom beets.plugins import BeetsPlugin\nfrom beets.ui import Subcommand, print_\n\nif TYPE_CHECKING:\n    import optparse\n    from collections.abc import Iterable\n\n    from beets.library import LibModel, Library\n\n\ndef random_func(lib: Library, opts: optparse.Values, args: list[str]):\n    \"\"\"Select some random items or albums and print the results.\"\"\"\n    # Fetch all the objects matching the query into a list.\n    objs = lib.albums(args) if opts.album else lib.items(args)\n\n    # Print a random subset.\n    for obj in random_objs(\n        objs=objs,\n        equal_chance_field=opts.field,\n        number=opts.number,\n        time_minutes=opts.time,\n        equal_chance=opts.equal_chance,\n    ):\n        print_(format(obj))\n\n\nrandom_cmd = Subcommand(\"random\", help=\"choose a random track or album\")\nrandom_cmd.parser.add_option(\n    \"-n\",\n    \"--number\",\n    action=\"store\",\n    type=\"int\",\n    help=\"number of objects to choose\",\n    default=1,\n)\nrandom_cmd.parser.add_option(\n    \"-e\",\n    \"--equal-chance\",\n    action=\"store_true\",\n    help=\"each field has the same chance\",\n)\nrandom_cmd.parser.add_option(\n    \"-t\",\n    \"--time\",\n    action=\"store\",\n    type=\"float\",\n    help=\"total length in minutes of objects to choose\",\n)\nrandom_cmd.parser.add_option(\n    \"--field\",\n    action=\"store\",\n    type=\"string\",\n    default=\"albumartist\",\n    help=\"field to use for equal chance sampling (default: albumartist)\",\n)\nrandom_cmd.parser.add_all_common_options()\nrandom_cmd.func = random_func\n\n\nclass Random(BeetsPlugin):\n    def commands(self):\n        return [random_cmd]\n\n\ndef _equal_chance_permutation(\n    objs: Iterable[LibModel], field: str\n) -> Iterable[LibModel]:\n    \"\"\"Generate (lazily) a permutation of the objects where every group\n    with equal values for `field` have an equal chance of appearing in\n    any given position.\n    \"\"\"\n    # Group the objects by field so we can sample from them.\n    get_attr = methodcaller(\"get\", field)\n\n    groups = {}\n    for k, values in groupby(sorted(objs, key=get_attr), key=get_attr):\n        if k is not None:\n            vals = list(values)\n            # shuffle in category\n            random.shuffle(vals)\n            groups[str(k)] = vals\n\n    while groups:\n        group = random.choice(list(groups.keys()))\n        yield groups[group].pop()\n        if not groups[group]:\n            del groups[group]\n\n\ndef _take_time(\n    iter: Iterable[LibModel],\n    secs: float,\n) -> Iterable[LibModel]:\n    \"\"\"Return a list containing the first values in `iter`, which should\n    be Item or Album objects, that add up to the given amount of time in\n    seconds.\n    \"\"\"\n    total_time = 0.0\n    for obj in iter:\n        length = obj.length\n        if total_time + length <= secs:\n            yield obj\n            total_time += length\n\n\ndef random_objs(\n    objs: Iterable[LibModel],\n    equal_chance_field: str,\n    number: int = 1,\n    time_minutes: float | None = None,\n    equal_chance: bool = False,\n) -> Iterable[LibModel]:\n    \"\"\"Get a random subset of items, optionally constrained by time or count.\n\n    Args:\n    - objs: The sequence of objects to choose from.\n    - number: The number of objects to select.\n    - time_minutes: If specified, the total length of selected objects\n      should not exceed this many minutes.\n    - equal_chance: If True, each field has the same chance of being\n      selected, regardless of how many tracks they have.\n    - random_gen: An optional random generator to use for shuffling.\n    \"\"\"\n    # Permute the objects either in a straightforward way or an\n    # field-balanced way.\n    if equal_chance:\n        perm = _equal_chance_permutation(objs, equal_chance_field)\n    else:\n        perm = list(objs)\n        random.shuffle(perm)\n\n    # Select objects by time our count.\n    if time_minutes:\n        return _take_time(perm, time_minutes * 60)\n    else:\n        return islice(perm, number)\n"
  },
  {
    "path": "beetsplug/replace.py",
    "content": "from __future__ import annotations\n\nimport shutil\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\n\nimport mediafile\n\nfrom beets import ui, util\nfrom beets.plugins import BeetsPlugin\n\nif TYPE_CHECKING:\n    from beets.library import Item, Library\n\n\nclass ReplacePlugin(BeetsPlugin):\n    def commands(self):\n        cmd = ui.Subcommand(\n            \"replace\", help=\"replace audio file while keeping tags\"\n        )\n        cmd.func = self.run\n        return [cmd]\n\n    def run(self, lib: Library, args: list[str]) -> None:\n        if len(args) < 2:\n            raise ui.UserError(\"Usage: beet replace <query> <new_file_path>\")\n\n        new_file_path: Path = Path(args[-1])\n        item_query: list[str] = args[:-1]\n\n        self.file_check(new_file_path)\n\n        item_list = list(lib.items(item_query))\n\n        if not item_list:\n            raise ui.UserError(\"No matching songs found.\")\n\n        song = self.select_song(item_list)\n\n        if not song:\n            ui.print_(\"Operation cancelled.\")\n            return\n\n        if not self.confirm_replacement(new_file_path, song):\n            ui.print_(\"Aborting replacement.\")\n            return\n\n        self.replace_file(new_file_path, song)\n\n    def file_check(self, filepath: Path) -> None:\n        \"\"\"Check if the file exists and is supported\"\"\"\n        if not filepath.is_file():\n            raise ui.UserError(\n                f\"'{util.displayable_path(filepath)}' is not a valid file.\"\n            )\n\n        try:\n            mediafile.MediaFile(util.syspath(filepath))\n        except mediafile.FileTypeError as fte:\n            raise ui.UserError(fte)\n\n    def select_song(self, items: list[Item]):\n        \"\"\"Present a menu of matching songs and get user selection.\"\"\"\n        ui.print_(\"\\nMatching songs:\")\n        for i, item in enumerate(items, 1):\n            ui.print_(f\"{i}. {util.displayable_path(item)}\")\n\n        while True:\n            try:\n                index = int(\n                    input(\n                        f\"Which song would you like to replace? \"\n                        f\"[1-{len(items)}] (0 to cancel): \"\n                    )\n                )\n                if index == 0:\n                    return None\n                if 1 <= index <= len(items):\n                    return items[index - 1]\n                ui.print_(\n                    f\"Invalid choice. Please enter a number \"\n                    f\"between 1 and {len(items)}.\"\n                )\n            except ValueError:\n                ui.print_(\"Invalid input. Please type in a number.\")\n\n    def confirm_replacement(self, new_file_path: Path, song: Item):\n        \"\"\"Get user confirmation for the replacement.\"\"\"\n        original_file_path: Path = Path(song.path.decode())\n\n        if not original_file_path.exists():\n            raise ui.UserError(\"The original song file was not found.\")\n\n        ui.print_(\n            f\"\\nReplacing: {util.displayable_path(new_file_path)} \"\n            f\"-> {util.displayable_path(original_file_path)}\"\n        )\n        decision: str = (\n            input(\"Are you sure you want to replace this track? (y/N): \")\n            .strip()\n            .casefold()\n        )\n        return decision in {\"yes\", \"y\"}\n\n    def replace_file(self, new_file_path: Path, song: Item) -> None:\n        \"\"\"Replace the existing file with the new one.\"\"\"\n        original_file_path = Path(song.path.decode())\n        dest = original_file_path.with_suffix(new_file_path.suffix)\n\n        try:\n            shutil.move(util.syspath(new_file_path), util.syspath(dest))\n        except Exception as e:\n            raise ui.UserError(f\"Error replacing file: {e}\")\n\n        if (\n            new_file_path.suffix != original_file_path.suffix\n            and original_file_path.exists()\n        ):\n            try:\n                original_file_path.unlink()\n            except Exception as e:\n                raise ui.UserError(f\"Could not delete original file: {e}\")\n\n        song.path = str(dest).encode()\n        song.store()\n\n        ui.print_(\"Replacement successful.\")\n"
  },
  {
    "path": "beetsplug/replaygain.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Fabrice Laporte, Yevgeny Bezman, and Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nfrom __future__ import annotations\n\nimport collections\nimport enum\nimport math\nimport os\nimport queue\nimport shutil\nimport signal\nimport subprocess\nimport sys\nimport warnings\nfrom abc import ABC, abstractmethod\nfrom dataclasses import dataclass\nfrom multiprocessing.pool import ThreadPool\nfrom pathlib import Path\nfrom threading import Event, Thread\nfrom typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeVar\n\nfrom beets import ui\nfrom beets.plugins import BeetsPlugin\nfrom beets.util import command_output, syspath\n\nif TYPE_CHECKING:\n    import optparse\n    from collections.abc import Callable, Sequence\n    from logging import Logger\n\n    from confuse import ConfigView\n\n    from beets.importer import ImportSession, ImportTask\n    from beets.library import Album, Item, Library\n\n# Utilities.\n\n\nclass ReplayGainError(Exception):\n    \"\"\"Raised when a local (to a track or an album) error occurs in one\n    of the backends.\n    \"\"\"\n\n\nclass FatalReplayGainError(Exception):\n    \"\"\"Raised when a fatal error occurs in one of the backends.\"\"\"\n\n\nclass FatalGstreamerPluginReplayGainError(FatalReplayGainError):\n    \"\"\"Raised when a fatal error occurs in the GStreamerBackend when\n    loading the required plugins.\"\"\"\n\n\ndef call(args: list[str], log: Logger, **kwargs: Any):\n    \"\"\"Execute the command and return its output or raise a\n    ReplayGainError on failure.\n    \"\"\"\n    try:\n        return command_output(args, **kwargs)\n    except subprocess.CalledProcessError as e:\n        log.debug(e.output.decode(\"utf8\", \"ignore\"))\n        raise ReplayGainError(f\"{args[0]} exited with status {e.returncode}\")\n\n\ndef db_to_lufs(db: float) -> float:\n    \"\"\"Convert db to LUFS.\n\n    According to https://wiki.hydrogenaud.io/index.php?title=\n      ReplayGain_2.0_specification#Reference_level\n    \"\"\"\n    return db - 107\n\n\ndef lufs_to_db(db: float) -> float:\n    \"\"\"Convert LUFS to db.\n\n    According to https://wiki.hydrogenaud.io/index.php?title=\n      ReplayGain_2.0_specification#Reference_level\n    \"\"\"\n    return db + 107\n\n\n# Backend base and plumbing classes.\n\n\n@dataclass\nclass Gain:\n    # gain: in LU to reference level\n    gain: float\n    # peak: part of full scale (FS is 1.0)\n    peak: float\n\n\nclass PeakMethod(enum.Enum):\n    true = 1\n    sample = 2\n\n\nclass RgTask:\n    \"\"\"State and methods for a single replaygain calculation (rg version).\n\n    Bundles the state (parameters and results) of a single replaygain\n    calculation (either for one item, one disk, or one full album).\n\n    This class provides methods to store the resulting gains and peaks as plain\n    old rg tags.\n    \"\"\"\n\n    def __init__(\n        self,\n        items: Sequence[Item],\n        album: Album | None,\n        target_level: float,\n        peak_method: PeakMethod | None,\n        backend_name: str,\n        log: Logger,\n    ):\n        self.items = items\n        self.album = album\n        self.target_level = target_level\n        self.peak_method = peak_method\n        self.backend_name = backend_name\n        self._log = log\n        self.album_gain: Gain | None = None\n        self.track_gains: list[Gain] | None = None\n\n    def _store_track_gain(self, item: Item, track_gain: Gain):\n        \"\"\"Store track gain for a single item in the database.\"\"\"\n        item.rg_track_gain = track_gain.gain\n        item.rg_track_peak = track_gain.peak\n        item.store()\n        self._log.debug(\n            \"applied track gain {0.rg_track_gain} LU, peak {0.rg_track_peak} of FS\",\n            item,\n        )\n\n    def _store_album_gain(self, item: Item, album_gain: Gain):\n        \"\"\"Store album gain for a single item in the database.\n\n        The caller needs to ensure that `self.album_gain is not None`.\n        \"\"\"\n        item.rg_album_gain = album_gain.gain\n        item.rg_album_peak = album_gain.peak\n        item.store()\n        self._log.debug(\n            \"applied album gain {0.rg_album_gain} LU, peak {0.rg_album_peak} of FS\",\n            item,\n        )\n\n    def _store_track(self, write: bool):\n        \"\"\"Store track gain for the first track of the task in the database.\"\"\"\n        item = self.items[0]\n        if self.track_gains is None or len(self.track_gains) != 1:\n            # In some cases, backends fail to produce a valid\n            # `track_gains` without throwing FatalReplayGainError\n            #  => raise non-fatal exception & continue\n            raise ReplayGainError(\n                f\"ReplayGain backend `{self.backend_name}` failed for track\"\n                f\" {item}\"\n            )\n\n        self._store_track_gain(item, self.track_gains[0])\n        if write:\n            item.try_write()\n        self._log.debug(\"done analyzing {}\", item)\n\n    def _store_album(self, write: bool):\n        \"\"\"Store track/album gains for all tracks of the task in the database.\"\"\"\n        if (\n            self.album_gain is None\n            or self.track_gains is None\n            or len(self.track_gains) != len(self.items)\n        ):\n            # In some cases, backends fail to produce a valid\n            # `album_gain` without throwing FatalReplayGainError\n            #  => raise non-fatal exception & continue\n            raise ReplayGainError(\n                f\"ReplayGain backend `{self.backend_name}` failed \"\n                f\"for some tracks in album {self.album}\"\n            )\n        for item, track_gain in zip(self.items, self.track_gains):\n            self._store_track_gain(item, track_gain)\n            self._store_album_gain(item, self.album_gain)\n            if write:\n                item.try_write()\n            self._log.debug(\"done analyzing {}\", item)\n\n    def store(self, write: bool):\n        \"\"\"Store computed gains for the items of this task in the database.\"\"\"\n        if self.album is not None:\n            self._store_album(write)\n        else:\n            self._store_track(write)\n\n\nclass R128Task(RgTask):\n    \"\"\"State and methods for a single replaygain calculation (r128 version).\n\n    Bundles the state (parameters and results) of a single replaygain\n    calculation (either for one item, one disk, or one full album).\n\n    This class provides methods to store the resulting gains and peaks as R128\n    tags.\n    \"\"\"\n\n    def __init__(\n        self,\n        items: Sequence[Item],\n        album: Album | None,\n        target_level: float,\n        backend_name: str,\n        log: Logger,\n    ):\n        # R128_* tags do not store the track/album peak\n        super().__init__(items, album, target_level, None, backend_name, log)\n\n    def _store_track_gain(self, item: Item, track_gain: Gain):\n        item.r128_track_gain = track_gain.gain\n        item.store()\n        self._log.debug(\"applied r128 track gain {.r128_track_gain} LU\", item)\n\n    def _store_album_gain(self, item: Item, album_gain: Gain):\n        \"\"\"\n\n        The caller needs to ensure that `self.album_gain is not None`.\n        \"\"\"\n        item.r128_album_gain = album_gain.gain\n        item.store()\n        self._log.debug(\"applied r128 album gain {.r128_album_gain} LU\", item)\n\n\nAnyRgTask = TypeVar(\"AnyRgTask\", bound=RgTask)\n\n\nclass Backend(ABC):\n    \"\"\"An abstract class representing engine for calculating RG values.\"\"\"\n\n    NAME = \"\"\n    do_parallel = False\n\n    def __init__(self, config: ConfigView, log: Logger):\n        \"\"\"Initialize the backend with the configuration view for the\n        plugin.\n        \"\"\"\n        self._log = log\n\n    @abstractmethod\n    def compute_track_gain(self, task: AnyRgTask) -> AnyRgTask:\n        \"\"\"Computes the track gain for the tracks belonging to `task`, and sets\n        the `track_gains` attribute on the task. Returns `task`.\n        \"\"\"\n        raise NotImplementedError()\n\n    @abstractmethod\n    def compute_album_gain(self, task: AnyRgTask) -> AnyRgTask:\n        \"\"\"Computes the album gain for the album belonging to `task`, and sets\n        the `album_gain` attribute on the task. Returns `task`.\n        \"\"\"\n        raise NotImplementedError()\n\n\n# ffmpeg backend\nclass FfmpegBackend(Backend):\n    \"\"\"A replaygain backend using ffmpeg's ebur128 filter.\"\"\"\n\n    NAME = \"ffmpeg\"\n    do_parallel = True\n\n    def __init__(self, config: ConfigView, log: Logger):\n        super().__init__(config, log)\n        self._ffmpeg_path = \"ffmpeg\"\n\n        # check that ffmpeg is installed\n        try:\n            ffmpeg_version_out = call([self._ffmpeg_path, \"-version\"], log)\n        except OSError:\n            raise FatalReplayGainError(\n                f\"could not find ffmpeg at {self._ffmpeg_path}\"\n            )\n        incompatible_ffmpeg = True\n        for line in ffmpeg_version_out.stdout.splitlines():\n            if line.startswith(b\"configuration:\"):\n                if b\"--enable-libebur128\" in line:\n                    incompatible_ffmpeg = False\n            if line.startswith(b\"libavfilter\"):\n                version = line.split(b\" \", 1)[1].split(b\"/\", 1)[0].split(b\".\")\n                version = tuple(map(int, version))\n                if version >= (6, 67, 100):\n                    incompatible_ffmpeg = False\n        if incompatible_ffmpeg:\n            raise FatalReplayGainError(\n                \"Installed FFmpeg version does not support ReplayGain.\"\n                \"calculation. Either libavfilter version 6.67.100 or above or\"\n                \"the --enable-libebur128 configuration option is required.\"\n            )\n\n    def compute_track_gain(self, task: AnyRgTask) -> AnyRgTask:\n        \"\"\"Computes the track gain for the tracks belonging to `task`, and sets\n        the `track_gains` attribute on the task. Returns `task`.\n        \"\"\"\n        task.track_gains = [\n            self._analyse_item(\n                item,\n                task.target_level,\n                task.peak_method,\n                count_blocks=False,\n            )[0]  # take only the gain, discarding number of gating blocks\n            for item in task.items\n        ]\n\n        return task\n\n    def compute_album_gain(self, task: AnyRgTask) -> AnyRgTask:\n        \"\"\"Computes the album gain for the album belonging to `task`, and sets\n        the `album_gain` attribute on the task. Returns `task`.\n        \"\"\"\n        target_level_lufs = db_to_lufs(task.target_level)\n\n        # analyse tracks\n        # Gives a list of tuples (track_gain, track_n_blocks)\n        track_results: list[tuple[Gain, int]] = [\n            self._analyse_item(\n                item,\n                task.target_level,\n                task.peak_method,\n                count_blocks=True,\n            )\n            for item in task.items\n        ]\n\n        track_gains: list[Gain] = [tg for tg, _nb in track_results]\n\n        # Album peak is maximum track peak\n        album_peak = max(tg.peak for tg in track_gains)\n\n        # Total number of BS.1770 gating blocks\n        n_blocks = sum(nb for _tg, nb in track_results)\n\n        def sum_of_track_powers(track_gain: Gain, track_n_blocks: int):\n            # convert `LU to target_level` -> LUFS\n            loudness = target_level_lufs - track_gain.gain\n\n            # This reverses ITU-R BS.1770-4 p. 6 equation (5) to convert\n            # from loudness to power. The result is the average gating\n            # block power.\n            power = 10 ** ((loudness + 0.691) / 10)\n\n            # Multiply that average power by the number of gating blocks to get\n            # the sum of all block powers in this track.\n            return track_n_blocks * power\n\n        # calculate album gain\n        if n_blocks > 0:\n            # Sum over all tracks to get the sum of BS.1770 gating block powers\n            # for the entire album.\n            sum_powers = sum(\n                sum_of_track_powers(tg, nb) for tg, nb in track_results\n            )\n\n            # compare ITU-R BS.1770-4 p. 6 equation (5)\n            # Album gain is the replaygain of the concatenation of all tracks.\n            album_gain = -0.691 + 10 * math.log10(sum_powers / n_blocks)\n        else:\n            album_gain = -70\n\n        # convert LUFS -> `LU to target_level`\n        album_gain = target_level_lufs - album_gain\n\n        self._log.debug(\n            \"{.album}: gain {} LU, peak {}\", task, album_gain, album_peak\n        )\n\n        task.album_gain = Gain(album_gain, album_peak)\n        task.track_gains = track_gains\n\n        return task\n\n    def _construct_cmd(\n        self, item: Item, peak_method: PeakMethod | None\n    ) -> list[str]:\n        \"\"\"Construct the shell command to analyse items.\"\"\"\n        return [\n            self._ffmpeg_path,\n            \"-nostats\",\n            \"-hide_banner\",\n            \"-i\",\n            str(item.filepath),\n            \"-map\",\n            \"a:0\",\n            \"-filter\",\n            f\"ebur128=peak={'none' if peak_method is None else peak_method.name}\",\n            \"-f\",\n            \"null\",\n            \"-\",\n        ]\n\n    def _analyse_item(\n        self,\n        item: Item,\n        target_level: float,\n        peak_method: PeakMethod | None,\n        count_blocks: bool = True,\n    ) -> tuple[Gain, int]:\n        \"\"\"Analyse item. Return a pair of a Gain object and the number\n        of gating blocks above the threshold.\n\n        If `count_blocks` is False, the number of gating blocks returned\n        will be 0.\n        \"\"\"\n        target_level_lufs = db_to_lufs(target_level)\n\n        # call ffmpeg\n        self._log.debug(\"analyzing {}\", item)\n        cmd = self._construct_cmd(item, peak_method)\n        self._log.debug(\"executing {}\", \" \".join(cmd))\n        output = call(cmd, self._log).stderr.splitlines()\n\n        # parse output\n\n        if peak_method is None:\n            peak = 0.0\n        else:\n            line_peak = self._find_line(\n                output,\n                # `peak_method` is non-`None` in this arm of the conditional\n                f\"  {peak_method.name.capitalize()} peak:\".encode(),\n                start_line=len(output) - 1,\n                step_size=-1,\n            )\n            peak = self._parse_float(\n                output[\n                    self._find_line(\n                        output,\n                        b\"    Peak:\",\n                        line_peak,\n                    )\n                ]\n            )\n            # convert TPFS -> part of FS\n            peak = 10 ** (peak / 20)\n\n        line_integrated_loudness = self._find_line(\n            output,\n            b\"  Integrated loudness:\",\n            start_line=len(output) - 1,\n            step_size=-1,\n        )\n        gain = self._parse_float(\n            output[\n                self._find_line(\n                    output,\n                    b\"    I:\",\n                    line_integrated_loudness,\n                )\n            ]\n        )\n        # convert LUFS -> LU from target level\n        gain = target_level_lufs - gain\n\n        # count BS.1770 gating blocks\n        n_blocks = 0\n        if count_blocks:\n            gating_threshold = self._parse_float(\n                output[\n                    self._find_line(\n                        output,\n                        b\"    Threshold:\",\n                        start_line=line_integrated_loudness,\n                    )\n                ]\n            )\n            for line in output:\n                if not line.startswith(b\"[Parsed_ebur128\"):\n                    continue\n                if line.endswith(b\"Summary:\"):\n                    continue\n                line = line.split(b\"M:\", 1)\n                if len(line) < 2:\n                    continue\n                if self._parse_float(b\"M: \" + line[1]) >= gating_threshold:\n                    n_blocks += 1\n            self._log.debug(\n                \"{}: {} blocks over {} LUFS\", item, n_blocks, gating_threshold\n            )\n\n        self._log.debug(\"{}: gain {} LU, peak {}\", item, gain, peak)\n\n        return Gain(gain, peak), n_blocks\n\n    def _find_line(\n        self,\n        output: Sequence[bytes],\n        search: bytes,\n        start_line: int = 0,\n        step_size: int = 1,\n    ) -> int:\n        \"\"\"Return index of line beginning with `search`.\n\n        Begins searching at index `start_line` in `output`.\n        \"\"\"\n        end_index = len(output) if step_size > 0 else -1\n        for i in range(start_line, end_index, step_size):\n            if output[i].startswith(search):\n                return i\n        raise ReplayGainError(\n            f\"ffmpeg output: missing {search!r} after line {start_line}\"\n        )\n\n    def _parse_float(self, line: bytes) -> float:\n        \"\"\"Extract a float from a key value pair in `line`.\n\n        This format is expected: /[^:]:[[:space:]]*value.*/, where `value` is\n        the float.\n        \"\"\"\n        # extract value\n        parts = line.split(b\":\", 1)\n        if len(parts) < 2:\n            raise ReplayGainError(\n                f\"ffmpeg output: expected key value pair, found {line!r}\"\n            )\n        value = parts[1].lstrip()\n        # strip unit\n        value = value.split(b\" \", 1)[0]\n        # cast value to float\n        try:\n            return float(value)\n        except ValueError:\n            raise ReplayGainError(\n                f\"ffmpeg output: expected float value, found {value!r}\"\n            )\n\n\n# mpgain/aacgain CLI tool backend.\nTool = Literal[\"mp3rgain\", \"aacgain\", \"mp3gain\"]\n\n\nclass CommandBackend(Backend):\n    NAME = \"command\"\n    SUPPORTED_FORMATS_BY_TOOL: ClassVar[dict[Tool, set[str]]] = {\n        \"mp3rgain\": {\"AAC\", \"MP3\"},\n        \"aacgain\": {\"AAC\", \"MP3\"},\n        \"mp3gain\": {\"MP3\"},\n    }\n    do_parallel = True\n\n    cmd_name: Tool\n\n    def __init__(self, config: ConfigView, log: Logger):\n        super().__init__(config, log)\n        config.add(\n            {\n                \"command\": \"\",\n                \"noclip\": True,\n            }\n        )\n\n        cmd_path: Path = Path(config[\"command\"].as_str())\n        supported_tools = set(self.SUPPORTED_FORMATS_BY_TOOL)\n\n        if (cmd_name := cmd_path.name) not in supported_tools:\n            raise FatalReplayGainError(\n                f\"replaygain.command must be one of {supported_tools!r},\"\n                f\" not {cmd_name!r}\"\n            )\n\n        if command_exec := shutil.which(str(cmd_path)):\n            self.command = command_exec\n            self.cmd_name = cmd_name  # type: ignore[assignment]\n        else:\n            raise FatalReplayGainError(\n                f\"replaygain command not found: {cmd_path}\"\n            )\n\n        self.noclip = config[\"noclip\"].get(bool)\n\n    def compute_track_gain(self, task: AnyRgTask) -> AnyRgTask:\n        \"\"\"Computes the track gain for the tracks belonging to `task`, and sets\n        the `track_gains` attribute on the task. Returns `task`.\n        \"\"\"\n        supported_items = list(filter(self.format_supported, task.items))\n        output = self.compute_gain(supported_items, task.target_level, False)\n        task.track_gains = output\n        return task\n\n    def compute_album_gain(self, task: AnyRgTask) -> AnyRgTask:\n        \"\"\"Computes the album gain for the album belonging to `task`, and sets\n        the `album_gain` attribute on the task. Returns `task`.\n        \"\"\"\n        # TODO: What should be done when not all tracks in the album are\n        # supported?\n\n        supported_items = list(filter(self.format_supported, task.items))\n        if len(supported_items) != len(task.items):\n            self._log.debug(\"tracks are of unsupported format\")\n            task.album_gain = None\n            task.track_gains = None\n            return task\n\n        output = self.compute_gain(supported_items, task.target_level, True)\n        task.album_gain = output[-1]\n        task.track_gains = output[:-1]\n        return task\n\n    def format_supported(self, item: Item) -> bool:\n        \"\"\"Checks whether the given item is supported by the selected tool.\"\"\"\n        return item.format in self.SUPPORTED_FORMATS_BY_TOOL[self.cmd_name]\n\n    def compute_gain(\n        self,\n        items: Sequence[Item],\n        target_level: float,\n        is_album: bool,\n    ) -> list[Gain]:\n        \"\"\"Computes the track or album gain of a list of items, returns\n        a list of TrackGain objects.\n\n        When computing album gain, the last TrackGain object returned is\n        the album gain\n        \"\"\"\n        if not items:\n            self._log.debug(\"no supported tracks to analyze\")\n            return []\n\n        \"\"\"Compute ReplayGain values and return a list of results\n        dictionaries as given by `parse_tool_output`.\n        \"\"\"\n        # Construct shell command. The \"-o\" option makes the output\n        # easily parseable (tab-delimited). \"-s s\" forces gain\n        # recalculation even if tags are already present and disables\n        # tag-writing; this turns the mp3gain/aacgain tool into a gain\n        # calculator rather than a tag manipulator because we take care\n        # of changing tags ourselves.\n        cmd = [\n            self.command,\n            \"-o\",\n            \"-s\",\n            \"s\",\n            # Avoid clipping or disable clipping warning\n            \"-k\" if self.noclip else \"-c\",\n            \"-d\",\n            str(int(target_level - 89)),\n            *[str(i.filepath) for i in items],\n        ]\n\n        self._log.debug(\"analyzing {} files\", len(items))\n        self._log.debug(\"executing {}\", \" \".join(cmd))\n        output = call(cmd, self._log).stdout\n        self._log.debug(\"analysis finished\")\n        return self.parse_tool_output(\n            output, len(items) + (1 if is_album else 0)\n        )\n\n    def parse_tool_output(self, text: bytes, num_lines: int) -> list[Gain]:\n        \"\"\"Given the tab-delimited output from an invocation of mp3gain\n        or aacgain, parse the text and return a list of dictionaries\n        containing information about each analyzed file.\n        \"\"\"\n        out = []\n        for line in text.split(b\"\\n\")[1 : num_lines + 1]:\n            parts = line.split(b\"\\t\")\n            if len(parts) != 6 or parts[0] == b\"File\":\n                self._log.debug(\"bad tool output: {}\", text)\n                raise ReplayGainError(\"mp3gain failed\")\n\n            # _file = parts[0]\n            # _mp3gain = int(parts[1])\n            gain = float(parts[2])\n            peak = float(parts[3]) / (1 << 15)\n            # _maxgain = int(parts[4])\n            # _mingain = int(parts[5])\n\n            out.append(Gain(gain, peak))\n        return out\n\n\n# GStreamer-based backend.\n\n\nclass GStreamerBackend(Backend):\n    NAME = \"gstreamer\"\n\n    def __init__(self, config: ConfigView, log: Logger):\n        super().__init__(config, log)\n        self._import_gst()\n\n        # Initialized a GStreamer pipeline of the form filesrc ->\n        # decodebin -> audioconvert -> audioresample -> rganalysis ->\n        # fakesink The connection between decodebin and audioconvert is\n        # handled dynamically after decodebin figures out the type of\n        # the input file.\n        self._src = self.Gst.ElementFactory.make(\"filesrc\", \"src\")\n        self._decbin = self.Gst.ElementFactory.make(\"decodebin\", \"decbin\")\n        self._conv = self.Gst.ElementFactory.make(\"audioconvert\", \"conv\")\n        self._res = self.Gst.ElementFactory.make(\"audioresample\", \"res\")\n        self._rg = self.Gst.ElementFactory.make(\"rganalysis\", \"rg\")\n\n        if (\n            self._src is None\n            or self._decbin is None\n            or self._conv is None\n            or self._res is None\n            or self._rg is None\n        ):\n            raise FatalGstreamerPluginReplayGainError(\n                \"Failed to load required GStreamer plugins\"\n            )\n\n        # We check which files need gain ourselves, so all files given\n        # to rganalsys should have their gain computed, even if it\n        # already exists.\n        self._rg.set_property(\"forced\", True)\n        self._sink = self.Gst.ElementFactory.make(\"fakesink\", \"sink\")\n\n        self._pipe = self.Gst.Pipeline()\n        self._pipe.add(self._src)\n        self._pipe.add(self._decbin)\n        self._pipe.add(self._conv)\n        self._pipe.add(self._res)\n        self._pipe.add(self._rg)\n        self._pipe.add(self._sink)\n\n        self._src.link(self._decbin)\n        self._conv.link(self._res)\n        self._res.link(self._rg)\n        self._rg.link(self._sink)\n\n        self._bus = self._pipe.get_bus()\n        self._bus.add_signal_watch()\n        self._bus.connect(\"message::eos\", self._on_eos)\n        self._bus.connect(\"message::error\", self._on_error)\n        self._bus.connect(\"message::tag\", self._on_tag)\n        # Needed for handling the dynamic connection between decodebin\n        # and audioconvert\n        self._decbin.connect(\"pad-added\", self._on_pad_added)\n        self._decbin.connect(\"pad-removed\", self._on_pad_removed)\n\n        self._main_loop = self.GLib.MainLoop()\n\n        self._files: list[bytes] = []\n\n    def _import_gst(self):\n        \"\"\"Import the necessary GObject-related modules and assign `Gst`\n        and `GObject` fields on this object.\n        \"\"\"\n\n        try:\n            import gi\n        except ImportError:\n            raise FatalReplayGainError(\n                \"Failed to load GStreamer: python-gi not found\"\n            )\n\n        try:\n            gi.require_version(\"Gst\", \"1.0\")\n        except ValueError as e:\n            raise FatalReplayGainError(f\"Failed to load GStreamer 1.0: {e}\")\n\n        from gi.repository import GLib, GObject, Gst\n\n        # Calling GObject.threads_init() is not needed for\n        # PyGObject 3.10.2+\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"ignore\")\n            GObject.threads_init()\n        Gst.init([sys.argv[0]])\n\n        self.GObject = GObject\n        self.GLib = GLib\n        self.Gst = Gst\n\n    def compute(self, items: Sequence[Item], target_level: float, album: bool):\n        if len(items) == 0:\n            return\n\n        self._error = None\n        self._files = [i.path for i in items]\n\n        # FIXME: Turn this into DefaultDict[bytes, Gain]\n        self._file_tags: collections.defaultdict[bytes, dict[str, float]] = (\n            collections.defaultdict(dict)\n        )\n\n        self._rg.set_property(\"reference-level\", target_level)\n\n        if album:\n            self._rg.set_property(\"num-tracks\", len(self._files))\n\n        if self._set_first_file():\n            self._main_loop.run()\n            if self._error is not None:\n                raise self._error\n\n    def compute_track_gain(self, task: AnyRgTask) -> AnyRgTask:\n        \"\"\"Computes the track gain for the tracks belonging to `task`, and sets\n        the `track_gains` attribute on the task. Returns `task`.\n        \"\"\"\n        self.compute(task.items, task.target_level, False)\n        if len(self._file_tags) != len(task.items):\n            raise ReplayGainError(\"Some tracks did not receive tags\")\n\n        ret = []\n        for item in task.items:\n            ret.append(\n                Gain(\n                    self._file_tags[item.path][\"TRACK_GAIN\"],\n                    self._file_tags[item.path][\"TRACK_PEAK\"],\n                )\n            )\n\n        task.track_gains = ret\n        return task\n\n    def compute_album_gain(self, task: AnyRgTask) -> AnyRgTask:\n        \"\"\"Computes the album gain for the album belonging to `task`, and sets\n        the `album_gain` attribute on the task. Returns `task`.\n        \"\"\"\n        items = list(task.items)\n        self.compute(items, task.target_level, True)\n        if len(self._file_tags) != len(items):\n            raise ReplayGainError(\"Some items in album did not receive tags\")\n\n        # Collect track gains.\n        track_gains = []\n        for item in items:\n            try:\n                gain = self._file_tags[item.path][\"TRACK_GAIN\"]\n                peak = self._file_tags[item.path][\"TRACK_PEAK\"]\n            except KeyError:\n                raise ReplayGainError(\"results missing for track\")\n            track_gains.append(Gain(gain, peak))\n\n        # Get album gain information from the last track.\n        last_tags = self._file_tags[items[-1].path]\n        try:\n            gain = last_tags[\"ALBUM_GAIN\"]\n            peak = last_tags[\"ALBUM_PEAK\"]\n        except KeyError:\n            raise ReplayGainError(\"results missing for album\")\n\n        task.album_gain = Gain(gain, peak)\n        task.track_gains = track_gains\n        return task\n\n    def close(self):\n        self._bus.remove_signal_watch()\n\n    def _on_eos(self, bus, message):\n        # A file finished playing in all elements of the pipeline. The\n        # RG tags have already been propagated.  If we don't have a next\n        # file, we stop processing.\n        if not self._set_next_file():\n            self._pipe.set_state(self.Gst.State.NULL)\n            self._main_loop.quit()\n\n    def _on_error(self, bus, message):\n        self._pipe.set_state(self.Gst.State.NULL)\n        self._main_loop.quit()\n        err, debug = message.parse_error()\n        f = self._src.get_property(\"location\")\n        # A GStreamer error, either an unsupported format or a bug.\n        self._error = ReplayGainError(\n            f\"Error {err!r} - {debug!r} on file {f!r}\"\n        )\n\n    def _on_tag(self, bus, message):\n        tags = message.parse_tag()\n\n        def handle_tag(taglist, tag, userdata):\n            # The rganalysis element provides both the existing tags for\n            # files and the new computes tags.  In order to ensure we\n            # store the computed tags, we overwrite the RG values of\n            # received a second time.\n            if tag == self.Gst.TAG_TRACK_GAIN:\n                self._file_tags[self._file][\"TRACK_GAIN\"] = taglist.get_double(\n                    tag\n                )[1]\n            elif tag == self.Gst.TAG_TRACK_PEAK:\n                self._file_tags[self._file][\"TRACK_PEAK\"] = taglist.get_double(\n                    tag\n                )[1]\n            elif tag == self.Gst.TAG_ALBUM_GAIN:\n                self._file_tags[self._file][\"ALBUM_GAIN\"] = taglist.get_double(\n                    tag\n                )[1]\n            elif tag == self.Gst.TAG_ALBUM_PEAK:\n                self._file_tags[self._file][\"ALBUM_PEAK\"] = taglist.get_double(\n                    tag\n                )[1]\n            elif tag == self.Gst.TAG_REFERENCE_LEVEL:\n                self._file_tags[self._file][\"REFERENCE_LEVEL\"] = (\n                    taglist.get_double(tag)[1]\n                )\n\n        tags.foreach(handle_tag, None)\n\n    def _set_first_file(self) -> bool:\n        if len(self._files) == 0:\n            return False\n\n        self._file = self._files.pop(0)\n        self._pipe.set_state(self.Gst.State.NULL)\n        self._src.set_property(\"location\", os.fsdecode(syspath(self._file)))\n        self._pipe.set_state(self.Gst.State.PLAYING)\n        return True\n\n    def _set_file(self) -> bool:\n        \"\"\"Initialize the filesrc element with the next file to be analyzed.\"\"\"\n        # No more files, we're done\n        if len(self._files) == 0:\n            return False\n\n        self._file = self._files.pop(0)\n\n        # Ensure the filesrc element received the paused state of the\n        # pipeline in a blocking manner\n        self._src.sync_state_with_parent()\n        self._src.get_state(self.Gst.CLOCK_TIME_NONE)\n\n        # Ensure the decodebin element receives the paused state of the\n        # pipeline in a blocking manner\n        self._decbin.sync_state_with_parent()\n        self._decbin.get_state(self.Gst.CLOCK_TIME_NONE)\n\n        # Disconnect the decodebin element from the pipeline, set its\n        # state to READY to to clear it.\n        self._decbin.unlink(self._conv)\n        self._decbin.set_state(self.Gst.State.READY)\n\n        # Set a new file on the filesrc element, can only be done in the\n        # READY state\n        self._src.set_state(self.Gst.State.READY)\n        self._src.set_property(\"location\", os.fsdecode(syspath(self._file)))\n\n        self._decbin.link(self._conv)\n        self._pipe.set_state(self.Gst.State.READY)\n\n        return True\n\n    def _set_next_file(self) -> bool:\n        \"\"\"Set the next file to be analyzed while keeping the pipeline\n        in the PAUSED state so that the rganalysis element can correctly\n        handle album gain.\n        \"\"\"\n        # A blocking pause\n        self._pipe.set_state(self.Gst.State.PAUSED)\n        self._pipe.get_state(self.Gst.CLOCK_TIME_NONE)\n\n        # Try setting the next file\n        ret = self._set_file()\n        if ret:\n            # Seek to the beginning in order to clear the EOS state of the\n            # various elements of the pipeline\n            self._pipe.seek_simple(\n                self.Gst.Format.TIME, self.Gst.SeekFlags.FLUSH, 0\n            )\n            self._pipe.set_state(self.Gst.State.PLAYING)\n\n        return ret\n\n    def _on_pad_added(self, decbin, pad):\n        sink_pad = self._conv.get_compatible_pad(pad, None)\n        assert sink_pad is not None\n        pad.link(sink_pad)\n\n    def _on_pad_removed(self, decbin, pad):\n        # Called when the decodebin element is disconnected from the\n        # rest of the pipeline while switching input files\n        peer = pad.get_peer()\n        assert peer is None\n\n\nclass AudioToolsBackend(Backend):\n    \"\"\"ReplayGain backend that uses `Python Audio Tools\n    <http://audiotools.sourceforge.net/>`_ and its capabilities to read more\n    file formats and compute ReplayGain values using it replaygain module.\n    \"\"\"\n\n    NAME = \"audiotools\"\n\n    def __init__(self, config: ConfigView, log: Logger):\n        super().__init__(config, log)\n        self._import_audiotools()\n\n    def _import_audiotools(self):\n        \"\"\"Check whether it's possible to import the necessary modules.\n        There is no check on the file formats at runtime.\n\n        :raises :exc:`ReplayGainError`: if the modules cannot be imported\n        \"\"\"\n        try:\n            import audiotools\n            import audiotools.replaygain\n        except ImportError:\n            raise FatalReplayGainError(\n                \"Failed to load audiotools: audiotools not found\"\n            )\n        self._mod_audiotools = audiotools\n        self._mod_replaygain = audiotools.replaygain\n\n    def open_audio_file(self, item: Item):\n        \"\"\"Open the file to read the PCM stream from the using\n        ``item.path``.\n\n        :return: the audiofile instance\n        :rtype: :class:`audiotools.AudioFile`\n        :raises :exc:`ReplayGainError`: if the file is not found or the\n        file format is not supported\n        \"\"\"\n        try:\n            audiofile = self._mod_audiotools.open(\n                os.fsdecode(syspath(item.path))\n            )\n        except OSError:\n            raise ReplayGainError(f\"File {item.filepath} was not found\")\n        except self._mod_audiotools.UnsupportedFile:\n            raise ReplayGainError(f\"Unsupported file type {item.format}\")\n\n        return audiofile\n\n    def init_replaygain(self, audiofile, item: Item):\n        \"\"\"Return an initialized :class:`audiotools.replaygain.ReplayGain`\n        instance, which requires the sample rate of the song(s) on which\n        the ReplayGain values will be computed. The item is passed in case\n        the sample rate is invalid to log the stored item sample rate.\n\n        :return: initialized replagain object\n        :rtype: :class:`audiotools.replaygain.ReplayGain`\n        :raises: :exc:`ReplayGainError` if the sample rate is invalid\n        \"\"\"\n        try:\n            rg = self._mod_replaygain.ReplayGain(audiofile.sample_rate())\n        except ValueError:\n            raise ReplayGainError(f\"Unsupported sample rate {item.samplerate}\")\n            return\n        return rg\n\n    def compute_track_gain(self, task: AnyRgTask) -> AnyRgTask:\n        \"\"\"Computes the track gain for the tracks belonging to `task`, and sets\n        the `track_gains` attribute on the task. Returns `task`.\n        \"\"\"\n        gains = [\n            self._compute_track_gain(i, task.target_level) for i in task.items\n        ]\n        task.track_gains = gains\n        return task\n\n    def _with_target_level(self, gain: float, target_level: float):\n        \"\"\"Return `gain` relative to `target_level`.\n\n        Assumes `gain` is relative to 89 db.\n        \"\"\"\n        return gain + (target_level - 89)\n\n    def _title_gain(self, rg, audiofile, target_level: float):\n        \"\"\"Get the gain result pair from PyAudioTools using the `ReplayGain`\n        instance `rg` for the given `audiofile`.\n\n        Wraps `rg.title_gain(audiofile.to_pcm())` and throws a\n        `ReplayGainError` when the library fails.\n        \"\"\"\n        try:\n            # The method needs an audiotools.PCMReader instance that can\n            # be obtained from an audiofile instance.\n            gain, peak = rg.title_gain(audiofile.to_pcm())\n        except ValueError as exc:\n            # `audiotools.replaygain` can raise a `ValueError` if the sample\n            # rate is incorrect.\n            self._log.debug(\"error in rg.title_gain() call: {}\", exc)\n            raise ReplayGainError(\"audiotools audio data error\")\n        return self._with_target_level(gain, target_level), peak\n\n    def _compute_track_gain(self, item: Item, target_level: float):\n        \"\"\"Compute ReplayGain value for the requested item.\n\n        :rtype: :class:`Gain`\n        \"\"\"\n        audiofile = self.open_audio_file(item)\n        rg = self.init_replaygain(audiofile, item)\n\n        # Each call to title_gain on a ReplayGain object returns peak and gain\n        # of the track.\n        rg_track_gain, rg_track_peak = self._title_gain(\n            rg, audiofile, target_level\n        )\n\n        self._log.debug(\n            \"ReplayGain for track {0.artist} - {0.title}: {1:.2f}, {2:.2f}\",\n            item,\n            rg_track_gain,\n            rg_track_peak,\n        )\n        return Gain(gain=rg_track_gain, peak=rg_track_peak)\n\n    def compute_album_gain(self, task: AnyRgTask) -> AnyRgTask:\n        \"\"\"Computes the album gain for the album belonging to `task`, and sets\n        the `album_gain` attribute on the task. Returns `task`.\n        \"\"\"\n        # The first item is taken and opened to get the sample rate to\n        # initialize the replaygain object. The object is used for all the\n        # tracks in the album to get the album values.\n        item = next(iter(task.items))\n        audiofile = self.open_audio_file(item)\n        rg = self.init_replaygain(audiofile, item)\n\n        track_gains = []\n        for item in task.items:\n            audiofile = self.open_audio_file(item)\n            rg_track_gain, rg_track_peak = self._title_gain(\n                rg, audiofile, task.target_level\n            )\n            track_gains.append(Gain(gain=rg_track_gain, peak=rg_track_peak))\n            self._log.debug(\n                \"ReplayGain for track {}: {.2f}, {.2f}\",\n                item,\n                rg_track_gain,\n                rg_track_peak,\n            )\n\n        # After getting the values for all tracks, it's possible to get the\n        # album values.\n        rg_album_gain, rg_album_peak = rg.album_gain()\n        rg_album_gain = self._with_target_level(\n            rg_album_gain, task.target_level\n        )\n        self._log.debug(\n            \"ReplayGain for album {.items[0].album}: {.2f}, {.2f}\",\n            task,\n            rg_album_gain,\n            rg_album_peak,\n        )\n\n        task.album_gain = Gain(gain=rg_album_gain, peak=rg_album_peak)\n        task.track_gains = track_gains\n        return task\n\n\nclass ExceptionWatcher(Thread):\n    \"\"\"Monitors a queue for exceptions asynchronously.\n    Once an exception occurs, raise it and execute a callback.\n    \"\"\"\n\n    def __init__(\n        self, queue: queue.Queue[Exception], callback: Callable[[], None]\n    ):\n        self._queue = queue\n        self._callback = callback\n        self._stopevent = Event()\n        Thread.__init__(self)\n\n    def run(self):\n        while not self._stopevent.is_set():\n            try:\n                exc = self._queue.get_nowait()\n                self._callback()\n                raise exc\n            except queue.Empty:\n                # No exceptions yet, loop back to check\n                #  whether `_stopevent` is set\n                pass\n\n    def join(self, timeout: float | None = None):\n        self._stopevent.set()\n        Thread.join(self, timeout)\n\n\n# Main plugin logic.\n\nBACKEND_CLASSES: list[type[Backend]] = [\n    CommandBackend,\n    GStreamerBackend,\n    AudioToolsBackend,\n    FfmpegBackend,\n]\nBACKENDS: dict[str, type[Backend]] = {b.NAME: b for b in BACKEND_CLASSES}\n\n\nclass ReplayGainPlugin(BeetsPlugin):\n    \"\"\"Provides ReplayGain analysis.\"\"\"\n\n    pool: ThreadPool | None = None\n\n    def __init__(self) -> None:\n        super().__init__()\n\n        # default backend is 'command' for backward-compatibility.\n        self.config.add(\n            {\n                \"overwrite\": False,\n                \"auto\": True,\n                \"backend\": \"command\",\n                \"threads\": os.cpu_count(),\n                \"parallel_on_import\": False,\n                \"per_disc\": False,\n                \"peak\": \"true\",\n                \"targetlevel\": 89,\n                \"r128\": [\"Opus\"],\n                \"r128_targetlevel\": lufs_to_db(-23),\n            }\n        )\n\n        # FIXME: Consider renaming the configuration option and deprecating the\n        # old name 'overwrite'.\n        self.force_on_import: bool = self.config[\"overwrite\"].get(bool)\n\n        # Remember which backend is used for CLI feedback\n        self.backend_name = self.config[\"backend\"].as_str()\n\n        if self.backend_name not in BACKENDS:\n            raise ui.UserError(\n                f\"Selected ReplayGain backend {self.backend_name} is not\"\n                f\" supported. Please select one of: {', '.join(BACKENDS)}\"\n            )\n\n        # FIXME: Consider renaming the configuration option to 'peak_method'\n        # and deprecating the old name 'peak'.\n        peak_method = self.config[\"peak\"].as_str()\n        if peak_method not in PeakMethod.__members__:\n            raise ui.UserError(\n                f\"Selected ReplayGain peak method {peak_method} is not\"\n                \" supported. Please select one of:\"\n                f\" {', '.join(PeakMethod.__members__)}\"\n            )\n        # This only applies to plain old rg tags, r128 doesn't store peak\n        # values.\n        self.peak_method = PeakMethod[peak_method]\n\n        # On-import analysis.\n        if self.config[\"auto\"]:\n            self.register_listener(\"import_begin\", self.import_begin)\n            self.register_listener(\"import\", self.import_end)\n            self.import_stages = [self.imported]\n\n        # Formats to use R128.\n        self.r128_whitelist = self.config[\"r128\"].as_str_seq()\n\n        try:\n            self.backend_instance = BACKENDS[self.backend_name](\n                self.config, self._log\n            )\n        except (ReplayGainError, FatalReplayGainError) as e:\n            raise ui.UserError(f\"replaygain initialization failed: {e}\")\n\n    def should_use_r128(self, item: Item) -> bool:\n        \"\"\"Checks the plugin setting to decide whether the calculation\n        should be done using the EBU R128 standard and use R128_ tags instead.\n        \"\"\"\n        return item.format in self.r128_whitelist\n\n    @staticmethod\n    def has_r128_track_data(item: Item) -> bool:\n        return item.r128_track_gain is not None\n\n    @staticmethod\n    def has_rg_track_data(item: Item) -> bool:\n        return item.rg_track_gain is not None and item.rg_track_peak is not None\n\n    def track_requires_gain(self, item: Item) -> bool:\n        if self.should_use_r128(item):\n            if not self.has_r128_track_data(item):\n                return True\n        else:\n            if not self.has_rg_track_data(item):\n                return True\n\n        return False\n\n    @staticmethod\n    def has_r128_album_data(item: Item) -> bool:\n        return (\n            item.r128_track_gain is not None\n            and item.r128_album_gain is not None\n        )\n\n    @staticmethod\n    def has_rg_album_data(item: Item) -> bool:\n        return item.rg_album_gain is not None and item.rg_album_peak is not None\n\n    def album_requires_gain(self, album: Album) -> bool:\n        # Skip calculating gain only when *all* files don't need\n        # recalculation. This way, if any file among an album's tracks\n        # needs recalculation, we still get an accurate album gain\n        # value.\n        for item in album.items():\n            if self.should_use_r128(item):\n                if not self.has_r128_album_data(item):\n                    return True\n            else:\n                if not self.has_rg_album_data(item):\n                    return True\n\n        return False\n\n    def create_task(\n        self,\n        items: Sequence[Item],\n        use_r128: bool,\n        album: Album | None = None,\n    ) -> RgTask:\n        if use_r128:\n            return R128Task(\n                items,\n                album,\n                self.config[\"r128_targetlevel\"].as_number(),\n                self.backend_instance.NAME,\n                self._log,\n            )\n        else:\n            return RgTask(\n                items,\n                album,\n                self.config[\"targetlevel\"].as_number(),\n                self.peak_method,\n                self.backend_instance.NAME,\n                self._log,\n            )\n\n    def handle_album(self, album: Album, write: bool, force: bool = False):\n        \"\"\"Compute album and track replay gain store it in all of the\n        album's items.\n\n        If ``write`` is truthy then ``item.write()`` is called for each\n        item. If replay gain information is already present in all\n        items, nothing is done.\n        \"\"\"\n        if not force and not self.album_requires_gain(album):\n            self._log.info(\"Skipping album {}\", album)\n            return\n\n        items_iter = iter(album.items())\n        use_r128 = self.should_use_r128(next(items_iter))\n        if any(use_r128 != self.should_use_r128(i) for i in items_iter):\n            self._log.error(\n                \"Cannot calculate gain for album {} (incompatible formats)\",\n                album,\n            )\n            return\n\n        self._log.info(\"analyzing {}\", album)\n\n        discs: dict[int, list[Item]] = {}\n        if self.config[\"per_disc\"].get(bool):\n            for item in album.items():\n                if discs.get(item.disc) is None:\n                    discs[item.disc] = []\n                discs[item.disc].append(item)\n        else:\n            discs[1] = album.items()\n\n        def store_cb(task: RgTask):\n            task.store(write)\n\n        for discnumber, items in discs.items():\n            task = self.create_task(items, use_r128, album=album)\n            try:\n                self._apply(\n                    self.backend_instance.compute_album_gain,\n                    args=[task],\n                    kwds={},\n                    callback=store_cb,\n                )\n            except ReplayGainError as e:\n                self._log.info(\"ReplayGain error: {}\", e)\n            except FatalReplayGainError as e:\n                raise ui.UserError(f\"Fatal replay gain error: {e}\")\n\n    def handle_track(self, item: Item, write: bool, force: bool = False):\n        \"\"\"Compute track replay gain and store it in the item.\n\n        If ``write`` is truthy then ``item.write()`` is called to write\n        the data to disk.  If replay gain information is already present\n        in the item, nothing is done.\n        \"\"\"\n        if not force and not self.track_requires_gain(item):\n            self._log.info(\"Skipping track {}\", item)\n            return\n\n        use_r128 = self.should_use_r128(item)\n\n        def store_cb(task: RgTask):\n            task.store(write)\n\n        task = self.create_task([item], use_r128)\n        try:\n            self._apply(\n                self.backend_instance.compute_track_gain,\n                args=[task],\n                kwds={},\n                callback=store_cb,\n            )\n        except ReplayGainError as e:\n            self._log.info(\"ReplayGain error: {}\", e)\n        except FatalReplayGainError as e:\n            raise ui.UserError(f\"Fatal replay gain error: {e}\")\n\n    def open_pool(self, threads: int):\n        \"\"\"Open a `ThreadPool` instance in `self.pool`\"\"\"\n        if self.pool is None and self.backend_instance.do_parallel:\n            self.pool = ThreadPool(threads)\n            self.exc_queue: queue.Queue[Exception] = queue.Queue()\n\n            signal.signal(signal.SIGINT, self._interrupt)\n\n            self.exc_watcher = ExceptionWatcher(\n                self.exc_queue,  # threads push exceptions here\n                self.terminate_pool,  # abort once an exception occurs\n            )\n            self.exc_watcher.start()\n\n    def _apply(\n        self,\n        func: Callable[..., AnyRgTask],\n        args: list[Any],\n        kwds: dict[str, Any],\n        callback: Callable[[AnyRgTask], Any],\n    ):\n        if self.pool is not None:\n\n            def handle_exc(exc):\n                \"\"\"Handle exceptions in the async work.\"\"\"\n                if isinstance(exc, ReplayGainError):\n                    self._log.info(exc.args[0])  # Log non-fatal exceptions.\n                else:\n                    self.exc_queue.put(exc)\n\n            self.pool.apply_async(\n                func, args, kwds, callback, error_callback=handle_exc\n            )\n        else:\n            callback(func(*args, **kwds))\n\n    def terminate_pool(self):\n        \"\"\"Forcibly terminate the `ThreadPool` instance in `self.pool`\n\n        Sends SIGTERM to all processes.\n        \"\"\"\n        if self.pool is not None:\n            self.pool.terminate()\n            self.pool.join()\n            # Terminating the processes leaves the ExceptionWatcher's queues\n            # in an unknown state, so don't wait for it.\n            # self.exc_watcher.join()\n            self.pool = None\n\n    def _interrupt(self, signal, frame):\n        try:\n            self._log.info(\"interrupted\")\n            self.terminate_pool()\n            sys.exit(0)\n        except SystemExit:\n            # Silence raised SystemExit ~ exit(0)\n            pass\n\n    def close_pool(self):\n        \"\"\"Regularly close the `ThreadPool` instance in `self.pool`.\"\"\"\n        if self.pool is not None:\n            self.pool.close()\n            self.pool.join()\n            self.exc_watcher.join()\n            self.pool = None\n\n    def import_begin(self, session: ImportSession):\n        \"\"\"Handle `import_begin` event -> open pool\"\"\"\n        threads: int = self.config[\"threads\"].get(int)\n\n        if (\n            self.config[\"parallel_on_import\"]\n            and self.config[\"auto\"]\n            and threads\n        ):\n            self.open_pool(threads)\n\n    def import_end(self, paths):\n        \"\"\"Handle `import` event -> close pool\"\"\"\n        self.close_pool()\n\n    def imported(self, session: ImportSession, task: ImportTask):\n        \"\"\"Add replay gain info to items or albums of ``task``.\"\"\"\n        if self.config[\"auto\"]:\n            if task.is_album:\n                self.handle_album(task.album, False, self.force_on_import)\n            else:\n                # Should be a SingletonImportTask\n                assert hasattr(task, \"item\")\n                self.handle_track(task.item, False, self.force_on_import)\n\n    def command_func(\n        self,\n        lib: Library,\n        opts: optparse.Values,\n        args: list[str],\n    ):\n        try:\n            write = ui.should_write(opts.write)\n            force = opts.force\n\n            # Bypass self.open_pool() if called with  `--threads 0`\n            if opts.threads != 0:\n                threads: int = opts.threads or self.config[\"threads\"].get(int)\n                self.open_pool(threads)\n\n            if opts.album:\n                albums = lib.albums(args)\n                self._log.info(\n                    f\"Analyzing {len(albums)} albums ~\"\n                    f\" {self.backend_name} backend...\"\n                )\n                for album in albums:\n                    self.handle_album(album, write, force)\n            else:\n                items = lib.items(args)\n                self._log.info(\n                    f\"Analyzing {len(items)} tracks ~\"\n                    f\" {self.backend_name} backend...\"\n                )\n                for item in items:\n                    self.handle_track(item, write, force)\n\n            self.close_pool()\n        except (SystemExit, KeyboardInterrupt):\n            # Silence interrupt exceptions\n            pass\n\n    def commands(self) -> list[ui.Subcommand]:\n        \"\"\"Return the \"replaygain\" ui subcommand.\"\"\"\n        cmd = ui.Subcommand(\"replaygain\", help=\"analyze for ReplayGain\")\n        cmd.parser.add_album_option()\n        cmd.parser.add_option(\n            \"-t\",\n            \"--threads\",\n            dest=\"threads\",\n            type=int,\n            help=(\n                \"change the number of threads, defaults to maximum available\"\n                \" processors\"\n            ),\n        )\n        cmd.parser.add_option(\n            \"-f\",\n            \"--force\",\n            dest=\"force\",\n            action=\"store_true\",\n            default=False,\n            help=(\n                \"analyze all files, including those that already have\"\n                \" ReplayGain metadata\"\n            ),\n        )\n        cmd.parser.add_option(\n            \"-w\",\n            \"--write\",\n            default=None,\n            action=\"store_true\",\n            help=\"write new metadata to files' tags\",\n        )\n        cmd.parser.add_option(\n            \"-W\",\n            \"--nowrite\",\n            dest=\"write\",\n            action=\"store_false\",\n            help=\"don't write metadata (opposite of -w)\",\n        )\n        cmd.func = self.command_func\n        return [cmd]\n"
  },
  {
    "path": "beetsplug/rewrite.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Uses user-specified rewriting rules to canonicalize names for path\nformats.\n\"\"\"\n\nimport re\nfrom collections import defaultdict\n\nfrom beets import library, ui\nfrom beets.plugins import BeetsPlugin\n\n\ndef rewriter(field, rules):\n    \"\"\"Create a template field function that rewrites the given field\n    with the given rewriting rules. ``rules`` must be a list of\n    (pattern, replacement) pairs.\n    \"\"\"\n\n    def fieldfunc(item):\n        value = item._values_fixed[field]\n        for pattern, replacement in rules:\n            if pattern.match(value.lower()):\n                # Rewrite activated.\n                return replacement\n        # Not activated; return original value.\n        return value\n\n    return fieldfunc\n\n\nclass RewritePlugin(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n\n        self.config.add({})\n\n        # Gather all the rewrite rules for each field.\n        rules = defaultdict(list)\n        for key, view in self.config.items():\n            value = view.as_str()\n            try:\n                fieldname, pattern = key.split(None, 1)\n            except ValueError:\n                raise ui.UserError(\"invalid rewrite specification\")\n            if fieldname not in library.Item._fields:\n                raise ui.UserError(\n                    f\"invalid field name ({fieldname}) in rewriter\"\n                )\n            self._log.debug(\"adding template field {}\", key)\n            pattern = re.compile(pattern.lower())\n            rules[fieldname].append((pattern, value))\n            if fieldname == \"artist\":\n                # Special case for the artist field: apply the same\n                # rewrite for \"albumartist\" as well.\n                rules[\"albumartist\"].append((pattern, value))\n\n        # Replace each template field with the new rewriter function.\n        for fieldname, fieldrules in rules.items():\n            getter = rewriter(fieldname, fieldrules)\n            self.template_fields[fieldname] = getter\n            if fieldname in library.Album._fields:\n                self.album_template_fields[fieldname] = getter\n"
  },
  {
    "path": "beetsplug/scrub.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Cleans extraneous metadata from files' tags via a command or\nautomatically whenever tags are written.\n\"\"\"\n\nimport mediafile\nimport mutagen\n\nfrom beets import config, ui, util\nfrom beets.plugins import BeetsPlugin\n\n_MUTAGEN_FORMATS = {\n    \"asf\": \"ASF\",\n    \"apev2\": \"APEv2File\",\n    \"flac\": \"FLAC\",\n    \"id3\": \"ID3FileType\",\n    \"mp3\": \"MP3\",\n    \"mp4\": \"MP4\",\n    \"oggflac\": \"OggFLAC\",\n    \"oggspeex\": \"OggSpeex\",\n    \"oggtheora\": \"OggTheora\",\n    \"oggvorbis\": \"OggVorbis\",\n    \"oggopus\": \"OggOpus\",\n    \"trueaudio\": \"TrueAudio\",\n    \"wavpack\": \"WavPack\",\n    \"monkeysaudio\": \"MonkeysAudio\",\n    \"optimfrog\": \"OptimFROG\",\n}\n\n\nclass ScrubPlugin(BeetsPlugin):\n    \"\"\"Removes extraneous metadata from files' tags.\"\"\"\n\n    def __init__(self):\n        super().__init__()\n        self.config.add(\n            {\n                \"auto\": True,\n            }\n        )\n\n        if self.config[\"auto\"]:\n            self.register_listener(\"import_task_files\", self.import_task_files)\n\n    def commands(self):\n        def scrub_func(lib, opts, args):\n            # Walk through matching files and remove tags.\n            for item in lib.items(args):\n                self._log.info(\"scrubbing: {.filepath}\", item)\n                self._scrub_item(item, opts.write)\n\n        scrub_cmd = ui.Subcommand(\"scrub\", help=\"clean audio tags\")\n        scrub_cmd.parser.add_option(\n            \"-W\",\n            \"--nowrite\",\n            dest=\"write\",\n            action=\"store_false\",\n            default=True,\n            help=\"leave tags empty\",\n        )\n        scrub_cmd.func = scrub_func\n\n        return [scrub_cmd]\n\n    @staticmethod\n    def _mutagen_classes():\n        \"\"\"Get a list of file type classes from the Mutagen module.\"\"\"\n        classes = []\n        for modname, clsname in _MUTAGEN_FORMATS.items():\n            mod = __import__(f\"mutagen.{modname}\", fromlist=[clsname])\n            classes.append(getattr(mod, clsname))\n        return classes\n\n    def _scrub(self, path):\n        \"\"\"Remove all tags from a file.\"\"\"\n        for cls in self._mutagen_classes():\n            # Try opening the file with this type, but just skip in the\n            # event of any error.\n            try:\n                f = cls(util.syspath(path))\n            except Exception:\n                continue\n            if f.tags is None:\n                continue\n\n            # Remove the tag for this type.\n            try:\n                f.delete()\n            except NotImplementedError:\n                # Some Mutagen metadata subclasses (namely, ASFTag) do not\n                # support .delete(), presumably because it is impossible to\n                # remove them. In this case, we just remove all the tags.\n                for tag in f.keys():\n                    del f[tag]\n                f.save()\n            except (OSError, mutagen.MutagenError) as exc:\n                self._log.error(\n                    \"could not scrub {}: {}\", util.displayable_path(path), exc\n                )\n\n    def _scrub_item(self, item, restore):\n        \"\"\"Remove tags from an Item's associated file and, if `restore`\n        is enabled, write the database's tags back to the file.\n        \"\"\"\n        # Get album art if we need to restore it.\n        if restore:\n            try:\n                mf = mediafile.MediaFile(\n                    util.syspath(item.path), config[\"id3v23\"].get(bool)\n                )\n            except mediafile.UnreadableFileError as exc:\n                self._log.error(\"could not open file to scrub: {}\", exc)\n                return\n            images = mf.images\n\n        # Remove all tags.\n        self._scrub(item.path)\n\n        # Restore tags, if enabled.\n        if restore:\n            self._log.debug(\"writing new tags after scrub\")\n            item.try_write()\n            if images:\n                self._log.debug(\"restoring art\")\n                try:\n                    mf = mediafile.MediaFile(\n                        util.syspath(item.path), config[\"id3v23\"].get(bool)\n                    )\n                    mf.images = images\n                    mf.save()\n                except mediafile.UnreadableFileError as exc:\n                    self._log.error(\"could not write tags: {}\", exc)\n\n    def import_task_files(self, session, task):\n        \"\"\"Automatically scrub imported files.\"\"\"\n        for item in task.imported_items():\n            self._log.debug(\"auto-scrubbing {.filepath}\", item)\n            self._scrub_item(item, ui.should_write())\n"
  },
  {
    "path": "beetsplug/smartplaylist.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Dang Mai <contact@dangmai.net>.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Generates smart playlists based on beets queries.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nfrom typing import TYPE_CHECKING, Any, TypeAlias\nfrom urllib.parse import quote\nfrom urllib.request import pathname2url\n\nfrom beets import ui\nfrom beets.dbcore.query import ParsingError, Query, Sort\nfrom beets.library import Album, Item, parse_query_string\nfrom beets.plugins import BeetsPlugin\nfrom beets.plugins import send as send_event\nfrom beets.util import (\n    bytestring_path,\n    displayable_path,\n    mkdirall,\n    normpath,\n    path_as_posix,\n    sanitize_path,\n    syspath,\n)\n\nif TYPE_CHECKING:\n    from beets.library import Library\n\nQueryAndSort = tuple[Query, Sort]\nPlaylistQuery = Query | tuple[QueryAndSort, ...] | None\nPlaylistMatch: TypeAlias = tuple[\n    str,\n    tuple[PlaylistQuery, Sort | None],\n    tuple[PlaylistQuery, Sort | None],\n]\n\n\nclass SmartPlaylistPlugin(BeetsPlugin):\n    def __init__(self) -> None:\n        super().__init__()\n        self.config.add(\n            {\n                \"dest_regen\": False,\n                \"relative_to\": None,\n                \"playlist_dir\": \".\",\n                \"auto\": True,\n                \"playlists\": [],\n                \"uri_format\": None,\n                \"fields\": [],\n                \"forward_slash\": False,\n                \"prefix\": \"\",\n                \"urlencode\": False,\n                \"pretend_paths\": False,\n                \"output\": \"m3u\",\n            }\n        )\n\n        self.config[\"prefix\"].redact = True  # May contain username/password.\n        self._matched_playlists: set[PlaylistMatch] = set()\n        self._unmatched_playlists: set[PlaylistMatch] = set()\n\n        if self.config[\"auto\"]:\n            self.register_listener(\"database_change\", self.db_change)\n\n    def commands(self) -> list[ui.Subcommand]:\n        spl_update = ui.Subcommand(\n            \"splupdate\",\n            help=\"update the smart playlists. Playlist names may be \"\n            \"passed as arguments.\",\n        )\n        spl_update.parser.add_option(\n            \"-p\",\n            \"--pretend\",\n            action=\"store_true\",\n            help=\"display query results but don't write playlist files.\",\n        )\n        spl_update.parser.add_option(\n            \"--pretend-paths\",\n            action=\"store_true\",\n            dest=\"pretend_paths\",\n            help=\"in pretend mode, log the playlist item URIs/paths.\",\n        )\n        spl_update.parser.add_option(\n            \"-d\",\n            \"--playlist-dir\",\n            dest=\"playlist_dir\",\n            metavar=\"PATH\",\n            type=\"string\",\n            help=\"directory to write the generated playlist files to.\",\n        )\n        spl_update.parser.add_option(\n            \"--dest-regen\",\n            action=\"store_true\",\n            dest=\"dest_regen\",\n            help=\"regenerate the destination path as 'move' or 'convert' \"\n            \"commands would do.\",\n        )\n        spl_update.parser.add_option(\n            \"--relative-to\",\n            dest=\"relative_to\",\n            metavar=\"PATH\",\n            type=\"string\",\n            help=\"generate playlist item paths relative to this path.\",\n        )\n        spl_update.parser.add_option(\n            \"--prefix\",\n            type=\"string\",\n            help=\"prepend string to every path in the playlist file.\",\n        )\n        spl_update.parser.add_option(\n            \"--forward-slash\",\n            action=\"store_true\",\n            dest=\"forward_slash\",\n            help=\"force forward slash in paths within playlists.\",\n        )\n        spl_update.parser.add_option(\n            \"--urlencode\",\n            action=\"store_true\",\n            help=\"URL-encode all paths.\",\n        )\n        spl_update.parser.add_option(\n            \"--uri-format\",\n            dest=\"uri_format\",\n            type=\"string\",\n            help=\"playlist item URI template, e.g. http://beets:8337/item/$id/file.\",\n        )\n        spl_update.parser.add_option(\n            \"--output\",\n            type=\"string\",\n            help=\"specify the playlist format: m3u|extm3u.\",\n        )\n        spl_update.func = self.update_cmd\n        return [spl_update]\n\n    def update_cmd(self, lib: Library, opts: Any, args: list[str]) -> None:\n        self.build_queries()\n        if args:\n            args_set = set(args)\n            for a in list(args_set):\n                if not a.endswith(\".m3u\"):\n                    args_set.add(f\"{a}.m3u\")\n\n            playlists = {\n                (name, q, a_q)\n                for name, q, a_q in self._unmatched_playlists\n                if name in args_set\n            }\n            if not playlists:\n                unmatched = [name for name, _, _ in self._unmatched_playlists]\n                raise ui.UserError(\n                    f\"No playlist matching any of {unmatched} found\"\n                )\n\n            self._matched_playlists = playlists\n            self._unmatched_playlists -= playlists\n        else:\n            self._matched_playlists = self._unmatched_playlists\n\n        self.__apply_opts_to_config(opts)\n        self.update_playlists(lib, opts.pretend)\n\n    def __apply_opts_to_config(self, opts: Any) -> None:\n        for k, v in opts.__dict__.items():\n            if v is not None and k in self.config:\n                self.config[k] = v\n\n    def _parse_one_query(\n        self, playlist: dict[str, Any], key: str, model_cls: type\n    ) -> tuple[PlaylistQuery, Sort | None]:\n        qs = playlist.get(key)\n        if qs is None:\n            return None, None\n        if isinstance(qs, str):\n            return parse_query_string(qs, model_cls)\n        if len(qs) == 1:\n            return parse_query_string(qs[0], model_cls)\n\n        queries_and_sorts: tuple[QueryAndSort, ...] = tuple(\n            parse_query_string(q, model_cls) for q in qs\n        )\n        return queries_and_sorts, None\n\n    def build_queries(self) -> None:\n        \"\"\"\n        Instantiate queries for the playlists.\n\n        Each playlist has 2 queries: one for items, one for albums, each with a\n        sort. We must also remember its name. _unmatched_playlists is a set of\n        tuples (name, (q, q_sort), (album_q, album_q_sort)).\n\n        sort may be any sort, or NullSort, or None. None and NullSort are\n        equivalent and both eval to False.\n        More precisely\n        - it will be NullSort when a playlist query ('query' or 'album_query')\n          is a single item or a list with 1 element\n        - it will be None when there are multiple items in a query\n        \"\"\"\n        self._unmatched_playlists = set()\n        self._matched_playlists = set()\n\n        for playlist in self.config[\"playlists\"].get(list):\n            if \"name\" not in playlist:\n                self._log.warning(\"playlist configuration is missing name\")\n                continue\n\n            try:\n                q_match = self._parse_one_query(playlist, \"query\", Item)\n                a_match = self._parse_one_query(playlist, \"album_query\", Album)\n            except ParsingError as exc:\n                self._log.warning(\n                    \"invalid query in playlist {}: {}\", playlist[\"name\"], exc\n                )\n                continue\n\n            self._unmatched_playlists.add((playlist[\"name\"], q_match, a_match))\n\n    def _matches_query(self, model: Item | Album, query: PlaylistQuery) -> bool:\n        if not query:\n            return False\n        if isinstance(query, (list, tuple)):\n            return any(q.match(model) for q, _ in query)\n        return query.match(model)\n\n    def matches(\n        self,\n        model: Item | Album,\n        query: PlaylistQuery,\n        album_query: PlaylistQuery,\n    ) -> bool:\n        if isinstance(model, Album):\n            return self._matches_query(model, album_query)\n        if isinstance(model, Item):\n            return self._matches_query(model, query)\n        return False\n\n    def db_change(self, lib: Library, model: Item | Album) -> None:\n        if self._unmatched_playlists is None:\n            self.build_queries()\n\n        for playlist in self._unmatched_playlists:\n            n, (q, _), (a_q, _) = playlist\n            if self.matches(model, q, a_q):\n                self._log.debug(\"{} will be updated because of {}\", n, model)\n                self._matched_playlists.add(playlist)\n                self.register_listener(\"cli_exit\", self.update_playlists)\n\n        self._unmatched_playlists -= self._matched_playlists\n\n    def update_playlists(self, lib: Library, pretend: bool = False) -> None:\n        if pretend:\n            self._log.info(\n                \"Showing query results for {} smart playlists...\",\n                len(self._matched_playlists),\n            )\n        else:\n            self._log.info(\n                \"Updating {} smart playlists...\", len(self._matched_playlists)\n            )\n\n        playlist_dir = bytestring_path(\n            self.config[\"playlist_dir\"].as_filename()\n        )\n        tpl = self.config[\"uri_format\"].get()\n        prefix = bytestring_path(self.config[\"prefix\"].as_str())\n        dest_regen = self.config[\"dest_regen\"].get()\n        relative_to = self.config[\"relative_to\"].get()\n        if relative_to:\n            relative_to = normpath(relative_to)\n\n        # Maps playlist filenames to lists of track filenames.\n        m3us: dict[str, list[PlaylistItem]] = {}\n\n        for playlist in self._matched_playlists:\n            name, (query, q_sort), (album_query, a_q_sort) = playlist\n            if pretend:\n                self._log.info(\"Results for playlist {}:\", name)\n            else:\n                self._log.info(\"Creating playlist {}\", name)\n            items = []\n\n            # Handle tuple/list of queries (preserves order)\n            # Track seen items to avoid duplicates when an item matches\n            # multiple queries\n            seen_ids = set()\n\n            if isinstance(query, (list, tuple)):\n                for q, sort in query:\n                    for item in lib.items(q, sort):\n                        if item.id not in seen_ids:\n                            items.append(item)\n                            seen_ids.add(item.id)\n            elif query:\n                items.extend(lib.items(query, q_sort))\n\n            if isinstance(album_query, (list, tuple)):\n                for q, sort in album_query:\n                    for album in lib.albums(q, sort):\n                        for item in album.items():\n                            if item.id not in seen_ids:\n                                items.append(item)\n                                seen_ids.add(item.id)\n            elif album_query:\n                for album in lib.albums(album_query, a_q_sort):\n                    items.extend(album.items())\n\n            # As we allow tags in the m3u names, we'll need to iterate through\n            # the items and generate the correct m3u file names.\n            for item in items:\n                m3u_name = item.evaluate_template(name, True)\n                m3u_name = sanitize_path(m3u_name, lib.replacements)\n                if m3u_name not in m3us:\n                    m3us[m3u_name] = []\n                item_uri = item.path\n                if tpl:\n                    item_uri = tpl.replace(\"$id\", str(item.id)).encode(\"utf-8\")\n                else:\n                    if dest_regen is True:\n                        item_uri = item.destination()\n                    if relative_to:\n                        item_uri = os.path.relpath(item_uri, relative_to)\n                    if self.config[\"forward_slash\"].get():\n                        item_uri = path_as_posix(item_uri)\n                    if self.config[\"urlencode\"]:\n                        item_uri = bytestring_path(\n                            pathname2url(os.fsdecode(item_uri))\n                        )\n                    item_uri = prefix + item_uri\n\n                if item_uri not in m3us[m3u_name]:\n                    m3us[m3u_name].append(PlaylistItem(item, item_uri))\n                    if pretend and self.config[\"pretend_paths\"]:\n                        print(displayable_path(item_uri))\n                    elif pretend:\n                        print(item)\n\n        if not pretend:\n            # Write all of the accumulated track lists to files.\n            for m3u in m3us:\n                m3u_path = normpath(\n                    os.path.join(playlist_dir, bytestring_path(m3u))\n                )\n                mkdirall(m3u_path)\n                pl_format = self.config[\"output\"].get()\n                if pl_format != \"m3u\" and pl_format != \"extm3u\":\n                    msg = \"Unsupported output format '{}' provided! \"\n                    msg += \"Supported: m3u, extm3u\"\n                    raise Exception(msg.format(pl_format))\n                extm3u = pl_format == \"extm3u\"\n                with open(syspath(m3u_path), \"wb\") as f:\n                    keys = []\n                    if extm3u:\n                        keys = self.config[\"fields\"].get(list)\n                        f.write(b\"#EXTM3U\\n\")\n                    for entry in m3us[m3u]:\n                        item = entry.item\n                        comment = \"\"\n                        if extm3u:\n                            attr = [(k, entry.item[k]) for k in keys]\n                            al = [\n                                f' {k}=\"{quote(\"; \".join(v) if isinstance(v, list) else str(v), safe=\"/:\")}\"'  # noqa: E501\n                                for k, v in attr\n                            ]\n                            attrs = \"\".join(al)\n                            comment = (\n                                f\"#EXTINF:{int(item.length)}{attrs},\"\n                                f\"{item.artist} - {item.title}\\n\"\n                            )\n                        f.write(comment.encode(\"utf-8\") + entry.uri + b\"\\n\")\n            # Send an event when playlists were updated.\n            send_event(\"smartplaylist_update\")  # type: ignore\n\n        if pretend:\n            self._log.info(\n                \"Displayed results for {} playlists\",\n                len(self._matched_playlists),\n            )\n        else:\n            self._log.info(\"{} playlists updated\", len(self._matched_playlists))\n\n\nclass PlaylistItem:\n    def __init__(self, item: Item, uri: bytes) -> None:\n        self.item = item\n        self.uri = uri\n"
  },
  {
    "path": "beetsplug/sonosupdate.py",
    "content": "# This file is part of beets.\n# Copyright 2018, Tobias Sauerwein.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Updates a Sonos library whenever the beets library is changed.\nThis is based on the Kodi Update plugin.\n\"\"\"\n\nimport soco\n\nfrom beets.plugins import BeetsPlugin\n\n\nclass SonosUpdate(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n        self.register_listener(\"database_change\", self.listen_for_db_change)\n\n    def listen_for_db_change(self, lib, model):\n        \"\"\"Listens for beets db change and register the update\"\"\"\n        self.register_listener(\"cli_exit\", self.update)\n\n    def update(self, lib):\n        \"\"\"When the client exists try to send refresh request to a Sonos\n        controller.\n        \"\"\"\n        self._log.info(\"Requesting a Sonos library update...\")\n\n        device = soco.discovery.any_soco()\n\n        if device:\n            device.music_library.start_library_update()\n        else:\n            self._log.warning(\"Could not find a Sonos device.\")\n            return\n\n        self._log.info(\"Sonos update triggered\")\n"
  },
  {
    "path": "beetsplug/spotify.py",
    "content": "# This file is part of beets.\n# Copyright 2019, Rahul Ahuja.\n# Copyright 2022, Alok Saboo.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Adds Spotify release and track search support to the autotagger.\n\nAlso includes Spotify playlist construction.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport collections\nimport json\nimport re\nimport threading\nimport time\nimport webbrowser\nfrom http import HTTPStatus\nfrom typing import TYPE_CHECKING, Any, ClassVar, Literal\n\nimport confuse\nimport requests\n\nfrom beets import ui\nfrom beets.autotag.hooks import AlbumInfo, TrackInfo\nfrom beets.dbcore import types\nfrom beets.library import Library\nfrom beets.metadata_plugins import IDResponse, SearchApiMetadataSourcePlugin\n\nif TYPE_CHECKING:\n    from collections.abc import Sequence\n\n    from beets.library import Item, Library\n    from beets.metadata_plugins import QueryType, SearchParams\n    from beetsplug._typing import JSONDict\n\nDEFAULT_WAITING_TIME = 5\n\n\nclass SearchResponseAlbums(IDResponse):\n    \"\"\"A response returned by the Spotify API.\n\n    We only use items and disregard the pagination information. i.e.\n    res[\"albums\"][\"items\"][0].\n\n    There are more fields in the response, but we only type the ones we\n    currently use.\n\n    see https://developer.spotify.com/documentation/web-api/reference/search\n\n    \"\"\"\n\n    album_type: str\n    available_markets: Sequence[str]\n    name: str\n\n\nclass SearchResponseTracks(IDResponse):\n    \"\"\"A track response returned by the Spotify API.\"\"\"\n\n    album: SearchResponseAlbums\n    available_markets: Sequence[str]\n    popularity: int\n    name: str\n\n\nclass APIError(Exception):\n    pass\n\n\nclass AudioFeaturesUnavailableError(Exception):\n    \"\"\"Raised when audio features API returns 403 (deprecated).\"\"\"\n\n    pass\n\n\nclass SpotifyPlugin(\n    SearchApiMetadataSourcePlugin[SearchResponseAlbums | SearchResponseTracks]\n):\n    item_types: ClassVar[dict[str, types.Type]] = {\n        \"spotify_track_popularity\": types.INTEGER,\n        \"spotify_acousticness\": types.FLOAT,\n        \"spotify_danceability\": types.FLOAT,\n        \"spotify_energy\": types.FLOAT,\n        \"spotify_instrumentalness\": types.FLOAT,\n        \"spotify_key\": types.FLOAT,\n        \"spotify_liveness\": types.FLOAT,\n        \"spotify_loudness\": types.FLOAT,\n        \"spotify_mode\": types.INTEGER,\n        \"spotify_speechiness\": types.FLOAT,\n        \"spotify_tempo\": types.FLOAT,\n        \"spotify_time_signature\": types.INTEGER,\n        \"spotify_valence\": types.FLOAT,\n        \"spotify_updated\": types.DATE,\n    }\n\n    # Base URLs for the Spotify API\n    # Documentation: https://developer.spotify.com/web-api\n    oauth_token_url = \"https://accounts.spotify.com/api/token\"\n    open_track_url = \"https://open.spotify.com/track/\"\n    search_url = \"https://api.spotify.com/v1/search\"\n    album_url = \"https://api.spotify.com/v1/albums/\"\n    track_url = \"https://api.spotify.com/v1/tracks/\"\n    audio_features_url = \"https://api.spotify.com/v1/audio-features/\"\n\n    spotify_audio_features: ClassVar[dict[str, str]] = {\n        \"acousticness\": \"spotify_acousticness\",\n        \"danceability\": \"spotify_danceability\",\n        \"energy\": \"spotify_energy\",\n        \"instrumentalness\": \"spotify_instrumentalness\",\n        \"key\": \"spotify_key\",\n        \"liveness\": \"spotify_liveness\",\n        \"loudness\": \"spotify_loudness\",\n        \"mode\": \"spotify_mode\",\n        \"speechiness\": \"spotify_speechiness\",\n        \"tempo\": \"spotify_tempo\",\n        \"time_signature\": \"spotify_time_signature\",\n        \"valence\": \"spotify_valence\",\n    }\n\n    def __init__(self):\n        super().__init__()\n        self.config.add(\n            {\n                \"mode\": \"list\",\n                \"tiebreak\": \"popularity\",\n                \"show_failures\": False,\n                \"region_filter\": None,\n                \"regex\": [],\n                \"client_id\": \"4e414367a1d14c75a5c5129a627fcab8\",\n                \"client_secret\": \"4a9b5b7848e54e118a7523b1c7c3e1e5\",\n                \"tokenfile\": \"spotify_token.json\",\n            }\n        )\n        self.config[\"client_id\"].redact = True\n        self.config[\"client_secret\"].redact = True\n\n        self.audio_features_available = (\n            True  # Track if audio features API is available\n        )\n        self._audio_features_lock = (\n            threading.Lock()\n        )  # Protects audio_features_available\n        self.setup()\n\n    def setup(self):\n        \"\"\"Retrieve previously saved OAuth token or generate a new one.\"\"\"\n\n        try:\n            with open(self._tokenfile()) as f:\n                token_data = json.load(f)\n        except OSError:\n            self._authenticate()\n        else:\n            self.access_token = token_data[\"access_token\"]\n\n    def _tokenfile(self) -> str:\n        \"\"\"Get the path to the JSON file for storing the OAuth token.\"\"\"\n        return self.config[\"tokenfile\"].get(confuse.Filename(in_app_dir=True))\n\n    def _authenticate(self) -> None:\n        \"\"\"Request an access token via the Client Credentials Flow: https://developer.spotify.com/documentation/general/guides/authorization-guide/#client-credentials-flow\"\"\"\n        c_id: str = self.config[\"client_id\"].as_str()\n        c_secret: str = self.config[\"client_secret\"].as_str()\n\n        headers = {\n            \"Authorization\": (\n                \"Basic\"\n                f\" {base64.b64encode(f'{c_id}:{c_secret}'.encode()).decode()}\"\n            )\n        }\n        response = requests.post(\n            self.oauth_token_url,\n            data={\"grant_type\": \"client_credentials\"},\n            headers=headers,\n            timeout=10,\n        )\n        try:\n            response.raise_for_status()\n        except requests.exceptions.HTTPError as e:\n            raise ui.UserError(\n                f\"Spotify authorization failed: {e}\\n{response.text}\"\n            )\n        self.access_token = response.json()[\"access_token\"]\n\n        # Save the token for later use.\n        self._log.debug(\"{0.data_source} access token: {0.access_token}\", self)\n        with open(self._tokenfile(), \"w\") as f:\n            json.dump({\"access_token\": self.access_token}, f)\n\n    def _handle_response(\n        self,\n        method: Literal[\"get\", \"post\", \"put\", \"delete\"],\n        url: str,\n        params: Any = None,\n        retry_count: int = 0,\n        max_retries: int = 3,\n    ) -> JSONDict:\n        \"\"\"Send a request, reauthenticating if necessary.\n\n        :param method: HTTP method to use for the request.\n        :param url: URL for the new :class:`Request` object.\n        :param dict params: (optional) list of tuples or bytes to send\n            in the query string for the :class:`Request`.\n\n        \"\"\"\n\n        if retry_count > max_retries:\n            raise APIError(\"Maximum retries reached.\")\n\n        try:\n            response = requests.request(\n                method,\n                url,\n                headers={\"Authorization\": f\"Bearer {self.access_token}\"},\n                params=params,\n                timeout=10,\n            )\n            response.raise_for_status()\n            return response.json()\n        except requests.exceptions.ReadTimeout:\n            self._log.error(\"ReadTimeout.\")\n            raise APIError(\"Request timed out.\")\n        except requests.exceptions.ConnectionError as e:\n            self._log.error(\"Network error: {}\", e)\n            raise APIError(\"Network error.\")\n        except requests.exceptions.RequestException as e:\n            if e.response is None:\n                self._log.error(\"Request failed: {}\", e)\n                raise APIError(\"Request failed.\")\n            if e.response.status_code == 401:\n                self._log.debug(\n                    \"{.data_source} access token has expired. Reauthenticating.\",\n                    self,\n                )\n                self._authenticate()\n                return self._handle_response(\n                    method,\n                    url,\n                    params=params,\n                    retry_count=retry_count + 1,\n                )\n            elif e.response.status_code == 404:\n                raise APIError(\n                    f\"API Error: {e.response.status_code}\\n\"\n                    f\"URL: {url}\\nparams: {params}\"\n                )\n            elif e.response.status_code == 403:\n                # Check if this is the audio features endpoint\n                if url.startswith(self.audio_features_url):\n                    raise AudioFeaturesUnavailableError(\n                        \"Audio features API returned 403 \"\n                        \"(deprecated or unavailable)\"\n                    )\n                raise APIError(\n                    f\"API Error: {e.response.status_code}\\n\"\n                    f\"URL: {url}\\nparams: {params}\"\n                )\n            elif e.response.status_code == 429:\n                seconds = e.response.headers.get(\n                    \"Retry-After\", DEFAULT_WAITING_TIME\n                )\n                self._log.debug(\n                    \"Too many API requests. Retrying after {} seconds.\", seconds\n                )\n                time.sleep(int(seconds) + 1)\n                return self._handle_response(\n                    method,\n                    url,\n                    params=params,\n                    retry_count=retry_count + 1,\n                )\n            elif e.response.status_code == 503:\n                self._log.error(\"Service Unavailable.\")\n                raise APIError(\"Service Unavailable.\")\n            elif e.response.status_code == 502:\n                self._log.error(\"Bad Gateway.\")\n                raise APIError(\"Bad Gateway.\")\n            elif e.response is not None:\n                raise APIError(\n                    f\"{self.data_source} API error:\\n\"\n                    f\"{e.response.text}\\n\"\n                    f\"URL:\\n{url}\\nparams:\\n{params}\"\n                )\n            else:\n                self._log.error(\"Request failed. Error: {}\", e)\n                raise APIError(\"Request failed.\")\n\n    def _multi_artist_credit(\n        self, artists: list[dict[str | int, str]]\n    ) -> tuple[list[str], list[str]]:\n        \"\"\"Given a list of artist dictionaries, accumulate data into a pair\n        of lists: the first being the artist names, and the second being the\n        artist IDs.\n        \"\"\"\n        artist_names = []\n        artist_ids = []\n        for artist in artists:\n            artist_names.append(artist[\"name\"])\n            artist_ids.append(artist[\"id\"])\n        return artist_names, artist_ids\n\n    def album_for_id(self, album_id: str) -> AlbumInfo | None:\n        \"\"\"Fetch an album by its Spotify ID or URL and return an\n        AlbumInfo object or None if the album is not found.\n\n        :param str album_id: Spotify ID or URL for the album\n\n        :returns: AlbumInfo object for album\n        :rtype: beets.autotag.hooks.AlbumInfo or None\n\n        \"\"\"\n        if not (spotify_id := self._extract_id(album_id)):\n            return None\n\n        album_data = self._handle_response(\n            \"get\", f\"{self.album_url}{spotify_id}\"\n        )\n        if album_data[\"name\"] == \"\":\n            self._log.debug(\"Album removed from Spotify: {}\", album_id)\n            return None\n        artists_names, artists_ids = self._multi_artist_credit(\n            album_data[\"artists\"]\n        )\n        artist = \", \".join(artists_names)\n\n        date_parts = [\n            int(part) for part in album_data[\"release_date\"].split(\"-\")\n        ]\n\n        release_date_precision = album_data[\"release_date_precision\"]\n        if release_date_precision == \"day\":\n            year, month, day = date_parts\n        elif release_date_precision == \"month\":\n            year, month = date_parts\n            day = None\n        elif release_date_precision == \"year\":\n            year = date_parts[0]\n            month = None\n            day = None\n        else:\n            raise ui.UserError(\n                \"Invalid `release_date_precision` returned \"\n                f\"by {self.data_source} API: '{release_date_precision}'\"\n            )\n\n        tracks_data = album_data[\"tracks\"]\n        tracks_items = tracks_data[\"items\"]\n        while tracks_data[\"next\"]:\n            tracks_data = self._handle_response(\"get\", tracks_data[\"next\"])\n            tracks_items.extend(tracks_data[\"items\"])\n\n        tracks = []\n        medium_totals: dict[int | None, int] = collections.defaultdict(int)\n        for i, track_data in enumerate(tracks_items, start=1):\n            track = self._get_track(track_data)\n            track.index = i\n            medium_totals[track.medium] += 1\n            tracks.append(track)\n        for track in tracks:\n            track.medium_total = medium_totals[track.medium]\n\n        return AlbumInfo(\n            album=album_data[\"name\"],\n            album_id=spotify_id,\n            spotify_album_id=spotify_id,\n            artist=artist,\n            artist_id=artists_ids[0] if len(artists_ids) > 0 else None,\n            spotify_artist_id=artists_ids[0] if len(artists_ids) > 0 else None,\n            artists=artists_names,\n            artists_ids=artists_ids,\n            tracks=tracks,\n            albumtype=album_data[\"album_type\"],\n            va=len(album_data[\"artists\"]) == 1\n            and artist.lower() == \"various artists\",\n            year=year,\n            month=month,\n            day=day,\n            label=album_data[\"label\"],\n            mediums=max(filter(None, medium_totals.keys())),\n            data_source=self.data_source,\n            data_url=album_data[\"external_urls\"][\"spotify\"],\n        )\n\n    def _get_track(self, track_data: JSONDict) -> TrackInfo:\n        \"\"\"Convert a Spotify track object dict to a TrackInfo object.\n\n        :param track_data: Simplified track object\n            (https://developer.spotify.com/documentation/web-api/reference/object-model/#track-object-simplified)\n\n        :returns: TrackInfo object for track\n\n        \"\"\"\n        artists_names, artists_ids = self._multi_artist_credit(\n            track_data[\"artists\"]\n        )\n        artist = \", \".join(artists_names)\n\n        # Get album information for spotify tracks\n        try:\n            album = track_data[\"album\"][\"name\"]\n        except (KeyError, TypeError):\n            album = None\n        return TrackInfo(\n            title=track_data[\"name\"],\n            track_id=track_data[\"id\"],\n            spotify_track_id=track_data[\"id\"],\n            artist=artist,\n            album=album,\n            artist_id=artists_ids[0] if len(artists_ids) > 0 else None,\n            spotify_artist_id=artists_ids[0] if len(artists_ids) > 0 else None,\n            artists=artists_names,\n            artists_ids=artists_ids,\n            length=track_data[\"duration_ms\"] / 1000,\n            index=track_data[\"track_number\"],\n            medium=track_data[\"disc_number\"],\n            medium_index=track_data[\"track_number\"],\n            data_source=self.data_source,\n            data_url=track_data[\"external_urls\"][\"spotify\"],\n        )\n\n    def track_for_id(self, track_id: str) -> None | TrackInfo:\n        \"\"\"Fetch a track by its Spotify ID or URL.\n\n        Returns a TrackInfo object or None if the track is not found.\n\n        \"\"\"\n\n        if not (spotify_id := self._extract_id(track_id)):\n            self._log.debug(\"Invalid Spotify ID: {}\", track_id)\n            return None\n\n        if not (\n            track_data := self._handle_response(\n                \"get\", f\"{self.track_url}{spotify_id}\"\n            )\n        ):\n            self._log.debug(\"Track not found: {}\", track_id)\n            return None\n\n        track = self._get_track(track_data)\n\n        # Get album's tracks to set `track.index` (position on the entire\n        # release) and `track.medium_total` (total number of tracks on\n        # the track's disc).\n        album_data = self._handle_response(\n            \"get\", f\"{self.album_url}{track_data['album']['id']}\"\n        )\n        medium_total = 0\n        for i, track_data in enumerate(album_data[\"tracks\"][\"items\"], start=1):\n            if track_data[\"disc_number\"] == track.medium:\n                medium_total += 1\n                if track_data[\"id\"] == track.track_id:\n                    track.index = i\n        track.medium_total = medium_total\n        return track\n\n    def get_search_query_with_filters(\n        self,\n        query_type: QueryType,\n        items: Sequence[Item],\n        artist: str,\n        name: str,\n        va_likely: bool,\n    ) -> tuple[str, dict[str, str]]:\n        query = f'album:\"{name}\"' if query_type == \"album\" else name\n        if query_type == \"track\" or not va_likely:\n            query += f' artist:\"{artist}\"'\n\n        return query, {}\n\n    def get_search_response(\n        self, params: SearchParams\n    ) -> Sequence[SearchResponseAlbums | SearchResponseTracks]:\n        \"\"\"Search Spotify and return raw album or track result items.\n\n        Unauthorized responses trigger one token refresh attempt before the\n        method gives up and falls back to an empty result set.\n        \"\"\"\n        for _ in range(2):\n            response = requests.get(\n                self.search_url,\n                headers={\"Authorization\": f\"Bearer {self.access_token}\"},\n                params={\n                    **params.filters,\n                    \"q\": params.query,\n                    \"type\": params.query_type,\n                    \"limit\": str(params.limit),\n                },\n                timeout=10,\n            )\n            try:\n                response.raise_for_status()\n            except requests.exceptions.HTTPError:\n                if response.status_code == HTTPStatus.UNAUTHORIZED:\n                    self._authenticate()\n                    continue\n                raise\n\n            return (\n                response.json()\n                .get(f\"{params.query_type}s\", {})\n                .get(\"items\", [])\n            )\n\n        return ()\n\n    def commands(self) -> list[ui.Subcommand]:\n        # autotagger import command\n        def queries(lib, opts, args):\n            success = self._parse_opts(opts)\n            if success:\n                results = self._match_library_tracks(lib, args)\n                self._output_match_results(results)\n\n        spotify_cmd = ui.Subcommand(\n            \"spotify\", help=f\"build a {self.data_source} playlist\"\n        )\n        spotify_cmd.parser.add_option(\n            \"-m\",\n            \"--mode\",\n            action=\"store\",\n            help=(\n                f'\"open\" to open {self.data_source} with playlist, '\n                '\"list\" to print (default)'\n            ),\n        )\n        spotify_cmd.parser.add_option(\n            \"-f\",\n            \"--show-failures\",\n            action=\"store_true\",\n            dest=\"show_failures\",\n            help=f\"list tracks that did not match a {self.data_source} ID\",\n        )\n        spotify_cmd.func = queries\n\n        # spotifysync command\n        sync_cmd = ui.Subcommand(\n            \"spotifysync\", help=\"fetch track attributes from Spotify\"\n        )\n        sync_cmd.parser.add_option(\n            \"-f\",\n            \"--force\",\n            dest=\"force_refetch\",\n            action=\"store_true\",\n            default=False,\n            help=\"re-download data when already present\",\n        )\n\n        def func(lib, opts, args):\n            items = lib.items(args)\n            self._fetch_info(items, ui.should_write(), opts.force_refetch)\n\n        sync_cmd.func = func\n        return [spotify_cmd, sync_cmd]\n\n    def _parse_opts(self, opts):\n        if opts.mode:\n            self.config[\"mode\"].set(opts.mode)\n\n        if opts.show_failures:\n            self.config[\"show_failures\"].set(True)\n\n        if self.config[\"mode\"].get() not in [\"list\", \"open\"]:\n            self._log.warning(\n                \"{} is not a valid mode\", self.config[\"mode\"].get()\n            )\n            return False\n\n        self.opts = opts\n        return True\n\n    def _match_library_tracks(self, library: Library, keywords: str):\n        \"\"\"Get simplified track object dicts for library tracks.\n\n        Matches tracks based on the specified ``keywords``.\n\n        :param library: beets library object to query.\n        :param keywords: Query to match library items against.\n\n        :returns: List of simplified track object dicts for library\n            items matching the specified query.\n\n        \"\"\"\n        results = []\n        failures = []\n\n        items = library.items(keywords)\n\n        if not items:\n            self._log.debug(\n                \"Your beets query returned no items, skipping {.data_source}.\",\n                self,\n            )\n            return\n\n        self._log.info(\"Processing {} tracks...\", len(items))\n\n        for item in items:\n            # Apply regex transformations if provided\n            for regex in self.config[\"regex\"].get():\n                if (\n                    not regex[\"field\"]\n                    or not regex[\"search\"]\n                    or not regex[\"replace\"]\n                ):\n                    continue\n\n                value = item[regex[\"field\"]]\n                item[regex[\"field\"]] = re.sub(\n                    regex[\"search\"], regex[\"replace\"], value\n                )\n\n            artist = item[\"artist\"] or item[\"albumartist\"]\n            album = item[\"album\"]\n            query_string = item[\"title\"]\n\n            # Query the Web API for each track, look for the items' JSON data\n            query = query_string\n            if artist:\n                query += f\" artist:'{artist}'\"\n            if album:\n                query += f\" album:'{album}'\"\n\n            response_data_tracks = self._search_api(\"track\", query, {})\n            if not response_data_tracks:\n                failures.append(query)\n                continue\n\n            # Apply market filter if requested\n            region_filter: str = self.config[\"region_filter\"].get()\n            if region_filter:\n                response_data_tracks = [\n                    track_data\n                    for track_data in response_data_tracks\n                    if region_filter in track_data[\"available_markets\"]\n                ]\n\n            if (\n                len(response_data_tracks) == 1\n                or self.config[\"tiebreak\"].get() == \"first\"\n            ):\n                self._log.debug(\n                    \"{.data_source} track(s) found, count: {}\",\n                    self,\n                    len(response_data_tracks),\n                )\n                chosen_result = response_data_tracks[0]\n            else:\n                # Use the popularity filter\n                self._log.debug(\n                    \"Most popular track chosen, count: {}\",\n                    len(response_data_tracks),\n                )\n                chosen_result = max(\n                    response_data_tracks,\n                    key=lambda x: x[\n                        # We are sure this is a track response!\n                        \"popularity\"  # type: ignore[typeddict-item]\n                    ],\n                )\n            results.append(chosen_result)\n\n        failure_count = len(failures)\n        if failure_count > 0:\n            if self.config[\"show_failures\"].get():\n                self._log.info(\n                    \"{} track(s) did not match a {.data_source} ID:\",\n                    failure_count,\n                    self,\n                )\n                for track in failures:\n                    self._log.info(\"track: {}\", track)\n                self._log.info(\"\")\n            else:\n                self._log.warning(\n                    \"{} track(s) did not match a {.data_source} ID:\\n\"\n                    \"use --show-failures to display\",\n                    failure_count,\n                    self,\n                )\n\n        return results\n\n    def _output_match_results(self, results):\n        \"\"\"Open a playlist or print Spotify URLs.\n\n        Uses the provided track object dicts.\n\n        :param list[dict] results: List of simplified track object dicts\n            (https://developer.spotify.com/documentation/web-api/\n            reference/object-model/#track-object-simplified)\n\n        \"\"\"\n        if results:\n            spotify_ids = [track_data[\"id\"] for track_data in results]\n            if self.config[\"mode\"].get() == \"open\":\n                self._log.info(\n                    \"Attempting to open {.data_source} with playlist\", self\n                )\n                spotify_url = (\n                    f\"spotify:trackset:Playlist:{','.join(spotify_ids)}\"\n                )\n                webbrowser.open(spotify_url)\n            else:\n                for spotify_id in spotify_ids:\n                    print(f\"{self.open_track_url}{spotify_id}\")\n        else:\n            self._log.warning(\n                \"No {.data_source} tracks found from beets query\", self\n            )\n\n    def _fetch_info(self, items, write, force):\n        \"\"\"Obtain track information from Spotify.\"\"\"\n\n        self._log.debug(\"Total {} tracks\", len(items))\n\n        for index, item in enumerate(items, start=1):\n            self._log.info(\n                \"Processing {}/{} tracks - {} \", index, len(items), item\n            )\n            # If we're not forcing re-downloading for all tracks, check\n            # whether the popularity data is already present\n            if not force:\n                if \"spotify_track_popularity\" in item:\n                    self._log.debug(\"Popularity already present for: {}\", item)\n                    continue\n            try:\n                spotify_track_id = item.spotify_track_id\n            except AttributeError:\n                self._log.debug(\"No track_id present for: {}\", item)\n                continue\n\n            popularity, isrc, ean, upc = self.track_info(spotify_track_id)\n            item[\"spotify_track_popularity\"] = popularity\n            item[\"isrc\"] = isrc\n            item[\"ean\"] = ean\n            item[\"upc\"] = upc\n\n            if self.audio_features_available:\n                audio_features = self.track_audio_features(spotify_track_id)\n                if audio_features is None:\n                    self._log.info(\"No audio features found for: {}\", item)\n                else:\n                    for feature, value in audio_features.items():\n                        if feature in self.spotify_audio_features:\n                            item[self.spotify_audio_features[feature]] = value\n            else:\n                self._log.debug(\"Audio features API unavailable, skipping\")\n\n            item[\"spotify_updated\"] = time.time()\n            item.store()\n            if write:\n                item.try_write()\n\n    def track_info(self, track_id: str):\n        \"\"\"Fetch a track's popularity and external IDs using its Spotify ID.\"\"\"\n        track_data = self._handle_response(\"get\", f\"{self.track_url}{track_id}\")\n        external_ids = track_data.get(\"external_ids\", {})\n        popularity = track_data.get(\"popularity\")\n        self._log.debug(\n            \"track_popularity: {} and track_isrc: {}\",\n            popularity,\n            external_ids.get(\"isrc\"),\n        )\n        return (\n            popularity,\n            external_ids.get(\"isrc\"),\n            external_ids.get(\"ean\"),\n            external_ids.get(\"upc\"),\n        )\n\n    def track_audio_features(self, track_id: str):\n        \"\"\"Fetch track audio features by its Spotify ID.\n\n        Thread-safe: avoids redundant API calls and logs the 403 warning only\n        once.\n\n        \"\"\"\n        # Fast path: if we've already detected unavailability, skip the call.\n        with self._audio_features_lock:\n            if not self.audio_features_available:\n                return None\n\n        try:\n            return self._handle_response(\n                \"get\", f\"{self.audio_features_url}{track_id}\"\n            )\n        except AudioFeaturesUnavailableError:\n            # Disable globally in a thread-safe manner and warn once.\n            should_log = False\n            with self._audio_features_lock:\n                if self.audio_features_available:\n                    self.audio_features_available = False\n                    should_log = True\n            if should_log:\n                self._log.warning(\n                    \"Audio features API is unavailable (403 error). \"\n                    \"Skipping audio features for remaining tracks.\"\n                )\n            return None\n        except APIError as e:\n            self._log.debug(\"Spotify API error: {}\", e)\n            return None\n"
  },
  {
    "path": "beetsplug/subsonicplaylist.py",
    "content": "# This file is part of beets.\n# Copyright 2019, Joris Jensen\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nimport random\nimport string\nfrom hashlib import md5\nfrom urllib.parse import urlencode\nfrom xml.etree import ElementTree\n\nimport requests\n\nfrom beets.dbcore import AndQuery\nfrom beets.dbcore.query import MatchQuery\nfrom beets.plugins import BeetsPlugin\nfrom beets.ui import Subcommand\n\n__author__ = \"https://github.com/MrNuggelz\"\n\n\ndef filter_to_be_removed(items, keys):\n    if len(items) > len(keys):\n        dont_remove = []\n        for artist, album, title in keys:\n            for item in items:\n                if (\n                    artist == item[\"artist\"]\n                    and album == item[\"album\"]\n                    and title == item[\"title\"]\n                ):\n                    dont_remove.append(item)\n        return [item for item in items if item not in dont_remove]\n    else:\n\n        def to_be_removed(item):\n            for artist, album, title in keys:\n                if (\n                    artist == item[\"artist\"]\n                    and album == item[\"album\"]\n                    and title == item[\"title\"]\n                ):\n                    return False\n            return True\n\n        return [item for item in items if to_be_removed(item)]\n\n\nclass SubsonicPlaylistPlugin(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n        self.config.add(\n            {\n                \"delete\": False,\n                \"playlist_ids\": [],\n                \"playlist_names\": [],\n                \"username\": \"\",\n                \"password\": \"\",\n            }\n        )\n        self.config[\"password\"].redact = True\n\n    def update_tags(self, playlist_dict, lib):\n        with lib.transaction():\n            for query, playlist_tag in playlist_dict.items():\n                query = AndQuery(\n                    [\n                        MatchQuery(\"artist\", query[0]),\n                        MatchQuery(\"album\", query[1]),\n                        MatchQuery(\"title\", query[2]),\n                    ]\n                )\n                items = lib.items(query)\n                if not items:\n                    self._log.warn(\n                        \"{} | track not found ({})\", playlist_tag, query\n                    )\n                    continue\n                for item in items:\n                    item.subsonic_playlist = playlist_tag\n                    item.try_sync(write=True, move=False)\n\n    def get_playlist(self, playlist_id):\n        xml = self.send(\"getPlaylist\", {\"id\": playlist_id}).text\n        playlist = ElementTree.fromstring(xml)[0]\n        if playlist.attrib.get(\"code\", \"200\") != \"200\":\n            alt_error = \"error getting playlist, but no error message found\"\n            self._log.warn(playlist.attrib.get(\"message\", alt_error))\n            return\n\n        name = playlist.attrib.get(\"name\", \"undefined\")\n        tracks = [\n            (t.attrib[\"artist\"], t.attrib[\"album\"], t.attrib[\"title\"])\n            for t in playlist\n        ]\n        return name, tracks\n\n    def commands(self):\n        def build_playlist(lib, opts, args):\n            self.config.set_args(opts)\n            ids = self.config[\"playlist_ids\"].as_str_seq()\n            if self.config[\"playlist_names\"].as_str_seq():\n                playlists = ElementTree.fromstring(\n                    self.send(\"getPlaylists\").text\n                )[0]\n                if playlists.attrib.get(\"code\", \"200\") != \"200\":\n                    alt_error = (\n                        \"error getting playlists, but no error message found\"\n                    )\n                    self._log.warn(playlists.attrib.get(\"message\", alt_error))\n                    return\n                for name in self.config[\"playlist_names\"].as_str_seq():\n                    for playlist in playlists:\n                        if name == playlist.attrib[\"name\"]:\n                            ids.append(playlist.attrib[\"id\"])\n\n            playlist_dict = self.get_playlists(ids)\n\n            # delete old tags\n            if self.config[\"delete\"]:\n                existing = list(lib.items('subsonic_playlist:\";\"'))\n                to_be_removed = filter_to_be_removed(\n                    existing, playlist_dict.keys()\n                )\n                for item in to_be_removed:\n                    item[\"subsonic_playlist\"] = \"\"\n                    with lib.transaction():\n                        item.try_sync(write=True, move=False)\n\n            self.update_tags(playlist_dict, lib)\n\n        subsonicplaylist_cmds = Subcommand(\n            \"subsonicplaylist\", help=\"import a subsonic playlist\"\n        )\n        subsonicplaylist_cmds.parser.add_option(\n            \"-d\",\n            \"--delete\",\n            action=\"store_true\",\n            help=\"delete tag from items not in any playlist anymore\",\n        )\n        subsonicplaylist_cmds.func = build_playlist\n        return [subsonicplaylist_cmds]\n\n    def generate_token(self):\n        salt = \"\".join(random.choices(string.ascii_lowercase + string.digits))\n        return (\n            md5((self.config[\"password\"].get() + salt).encode()).hexdigest(),\n            salt,\n        )\n\n    def send(self, endpoint, params=None):\n        if params is None:\n            params = {}\n        a, b = self.generate_token()\n        params[\"u\"] = self.config[\"username\"]\n        params[\"t\"] = a\n        params[\"s\"] = b\n        params[\"v\"] = \"1.12.0\"\n        params[\"c\"] = \"beets\"\n        resp = requests.get(\n            f\"{self.config['base_url'].get()}/rest/{endpoint}?{urlencode(params)}\",\n            timeout=10,\n        )\n        return resp\n\n    def get_playlists(self, ids):\n        output = {}\n        for playlist_id in ids:\n            name, tracks = self.get_playlist(playlist_id)\n            for track in tracks:\n                if track not in output:\n                    output[track] = \";\"\n                output[track] += f\"{name};\"\n        return output\n"
  },
  {
    "path": "beetsplug/subsonicupdate.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Updates Subsonic library on Beets import\nYour Beets configuration file should contain\na \"subsonic\" section like the following:\n    subsonic:\n        url: https://mydomain.com:443/subsonic\n        user: username\n        pass: password\n        auth: token\nFor older Subsonic versions, token authentication\nis not supported, use password instead:\n    subsonic:\n        url: https://mydomain.com:443/subsonic\n        user: username\n        pass: password\n        auth: pass\n\"\"\"\n\nimport hashlib\nimport random\nimport string\nfrom binascii import hexlify\n\nimport requests\n\nfrom beets.plugins import BeetsPlugin\n\n__author__ = \"https://github.com/maffo999\"\n\n\nclass SubsonicUpdate(BeetsPlugin):\n    def __init__(self):\n        super().__init__(\"subsonic\")\n        # Set default configuration values\n        self.config.add(\n            {\n                \"user\": \"admin\",\n                \"pass\": \"admin\",\n                \"url\": \"http://localhost:4040\",\n                \"auth\": \"token\",\n            }\n        )\n        self.config[\"user\"].redact = True\n        self.config[\"pass\"].redact = True\n        self.register_listener(\"database_change\", self.db_change)\n        self.register_listener(\"smartplaylist_update\", self.spl_update)\n\n    def db_change(self, lib, model):\n        self.register_listener(\"cli_exit\", self.start_scan)\n\n    def spl_update(self):\n        self.register_listener(\"cli_exit\", self.start_scan)\n\n    def __create_token(self):\n        \"\"\"Create salt and token from given password.\n\n        :return: The generated salt and hashed token\n        \"\"\"\n        password = self.config[\"pass\"].as_str()\n\n        # Pick the random sequence and salt the password\n        r = string.ascii_letters + string.digits\n        salt = \"\".join([random.choice(r) for _ in range(6)])\n        salted_password = f\"{password}{salt}\"\n        token = hashlib.md5(salted_password.encode(\"utf-8\")).hexdigest()\n\n        # Put together the payload of the request to the server and the URL\n        return salt, token\n\n    def __format_url(self, endpoint):\n        \"\"\"Get the Subsonic URL to trigger the given endpoint.\n        Uses either the url config option or the deprecated host, port,\n        and context_path config options together.\n\n        :return: Endpoint for updating Subsonic\n        \"\"\"\n\n        url = self.config[\"url\"].as_str()\n        if url and url.endswith(\"/\"):\n            url = url[:-1]\n\n        # @deprecated(\"Use url config option instead\")\n        if not url:\n            host = self.config[\"host\"].as_str()\n            port = self.config[\"port\"].get(int)\n            context_path = self.config[\"contextpath\"].as_str()\n            if context_path == \"/\":\n                context_path = \"\"\n            url = f\"http://{host}:{port}{context_path}\"\n\n        return f\"{url}/rest/{endpoint}\"\n\n    def start_scan(self):\n        user = self.config[\"user\"].as_str()\n        auth = self.config[\"auth\"].as_str()\n        url = self.__format_url(\"startScan\")\n        self._log.debug(\"URL is {}\", url)\n        self._log.debug(\"auth type is {.config[auth]}\", self)\n\n        if auth == \"token\":\n            salt, token = self.__create_token()\n            payload = {\n                \"u\": user,\n                \"t\": token,\n                \"s\": salt,\n                \"v\": \"1.13.0\",  # Subsonic 5.3 and newer\n                \"c\": \"beets\",\n                \"f\": \"json\",\n            }\n        elif auth == \"password\":\n            password = self.config[\"pass\"].as_str()\n            encpass = hexlify(password.encode()).decode()\n            payload = {\n                \"u\": user,\n                \"p\": f\"enc:{encpass}\",\n                \"v\": \"1.12.0\",\n                \"c\": \"beets\",\n                \"f\": \"json\",\n            }\n        else:\n            return\n        try:\n            response = requests.get(\n                url,\n                params=payload,\n                timeout=10,\n            )\n            json = response.json()\n\n            if (\n                response.status_code == 200\n                and json[\"subsonic-response\"][\"status\"] == \"ok\"\n            ):\n                count = json[\"subsonic-response\"][\"scanStatus\"][\"count\"]\n                self._log.info(\"Updating Subsonic; scanning {} tracks\", count)\n            elif (\n                response.status_code == 200\n                and json[\"subsonic-response\"][\"status\"] == \"failed\"\n            ):\n                self._log.error(\n                    \"Error: {[subsonic-response][error][message]}\", json\n                )\n            else:\n                self._log.error(\"Error: {}\", json)\n        except Exception as error:\n            self._log.error(\"Error: {}\", error)\n"
  },
  {
    "path": "beetsplug/substitute.py",
    "content": "# This file is part of beets.\n# Copyright 2023, Daniele Ferone.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"The substitute plugin module.\n\nUses user-specified substitution rules to canonicalize names for path formats.\n\"\"\"\n\nimport re\n\nfrom beets.plugins import BeetsPlugin\n\n\nclass Substitute(BeetsPlugin):\n    \"\"\"The substitute plugin class.\n\n    Create a template field function that substitute the given field with the\n    given substitution rules. ``rules`` must be a list of (pattern,\n    replacement) pairs.\n    \"\"\"\n\n    def tmpl_substitute(self, text):\n        \"\"\"Do the actual replacing.\"\"\"\n        if text:\n            for pattern, replacement in self.substitute_rules:\n                text = pattern.sub(replacement, text)\n            return text\n        else:\n            return \"\"\n\n    def __init__(self):\n        \"\"\"Initialize the substitute plugin.\n\n        Get the configuration, register template function and create list of\n        substitute rules.\n        \"\"\"\n        super().__init__()\n        self.template_funcs[\"substitute\"] = self.tmpl_substitute\n        self.substitute_rules = [\n            (re.compile(key, flags=re.IGNORECASE), value)\n            for key, value in self.config.flatten().items()\n        ]\n"
  },
  {
    "path": "beetsplug/the.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Blemjhoo Tezoulbr <baobab@heresiarch.info>.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Moves patterns in path formats (suitable for moving articles).\"\"\"\n\nimport re\nfrom typing import ClassVar\n\nfrom beets.plugins import BeetsPlugin\n\n__author__ = \"baobab@heresiarch.info\"\n__version__ = \"1.1\"\n\nPATTERN_THE = \"^the\\\\s\"\nPATTERN_A = \"^[a][n]?\\\\s\"\nFORMAT = \"{}, {}\"\n\n\nclass ThePlugin(BeetsPlugin):\n    patterns: ClassVar[list[str]] = []\n\n    def __init__(self):\n        super().__init__()\n\n        self.template_funcs[\"the\"] = self.the_template_func\n\n        self.config.add(\n            {\n                \"the\": True,\n                \"a\": True,\n                \"format\": \"{}, {}\",\n                \"strip\": False,\n                \"patterns\": [],\n            }\n        )\n\n        self.patterns = self.config[\"patterns\"].as_str_seq()\n        for p in self.patterns:\n            if p:\n                try:\n                    re.compile(p)\n                except re.error:\n                    self._log.error(\"invalid pattern: {}\", p)\n                else:\n                    if not (p.startswith(\"^\") or p.endswith(\"$\")):\n                        self._log.warning(\n                            'warning: \"{}\" will not match string start/end',\n                            p,\n                        )\n        if self.config[\"a\"]:\n            self.patterns = [PATTERN_A, *self.patterns]\n        if self.config[\"the\"]:\n            self.patterns = [PATTERN_THE, *self.patterns]\n        if not self.patterns:\n            self._log.warning(\"no patterns defined!\")\n\n    def unthe(self, text, pattern):\n        \"\"\"Moves pattern in the path format string or strips it\n\n        text -- text to handle\n        pattern -- regexp pattern (case ignore is already on)\n        strip -- if True, pattern will be removed\n        \"\"\"\n        if text:\n            r = re.compile(pattern, flags=re.IGNORECASE)\n            try:\n                t = r.findall(text)[0]\n            except IndexError:\n                return text\n            else:\n                r = re.sub(r, \"\", text).strip()\n                if self.config[\"strip\"]:\n                    return r\n                else:\n                    fmt = self.config[\"format\"].as_str()\n                    return fmt.format(r, t.strip()).strip()\n        else:\n            return \"\"\n\n    def the_template_func(self, text):\n        if not self.patterns:\n            return text\n        if text:\n            for p in self.patterns:\n                r = self.unthe(text, p)\n                if r != text:\n                    self._log.debug('\"{}\" -> \"{}\"', text, r)\n                    break\n            return r\n        else:\n            return \"\"\n"
  },
  {
    "path": "beetsplug/thumbnails.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Bruno Cauet\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Create freedesktop.org-compliant thumbnails for album folders\n\nThis plugin is POSIX-only.\nSpec: standards.freedesktop.org/thumbnail-spec/latest/index.html\n\"\"\"\n\nimport ctypes\nimport ctypes.util\nimport os\nimport shutil\nfrom hashlib import md5\nfrom pathlib import PurePosixPath\n\nfrom xdg import BaseDirectory\n\nfrom beets.plugins import BeetsPlugin\nfrom beets.ui import Subcommand\nfrom beets.util import bytestring_path, displayable_path, syspath\nfrom beets.util.artresizer import ArtResizer\n\nBASE_DIR = os.path.join(BaseDirectory.xdg_cache_home, \"thumbnails\")\nNORMAL_DIR = bytestring_path(os.path.join(BASE_DIR, \"normal\"))\nLARGE_DIR = bytestring_path(os.path.join(BASE_DIR, \"large\"))\n\n\nclass ThumbnailsPlugin(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n        self.config.add(\n            {\n                \"auto\": True,\n                \"force\": False,\n                \"dolphin\": False,\n            }\n        )\n\n        if self.config[\"auto\"] and self._check_local_ok():\n            self.register_listener(\"art_set\", self.process_album)\n\n    def commands(self):\n        thumbnails_command = Subcommand(\n            \"thumbnails\", help=\"Create album thumbnails\"\n        )\n        thumbnails_command.parser.add_option(\n            \"-f\",\n            \"--force\",\n            dest=\"force\",\n            action=\"store_true\",\n            default=False,\n            help=\"force regeneration of thumbnails deemed fine (existing & \"\n            \"recent enough)\",\n        )\n        thumbnails_command.parser.add_option(\n            \"--dolphin\",\n            dest=\"dolphin\",\n            action=\"store_true\",\n            default=False,\n            help=\"create Dolphin-compatible thumbnail information (for KDE)\",\n        )\n        thumbnails_command.func = self.process_query\n\n        return [thumbnails_command]\n\n    def process_query(self, lib, opts, args):\n        self.config.set_args(opts)\n        if self._check_local_ok():\n            for album in lib.albums(args):\n                self.process_album(album)\n\n    def _check_local_ok(self):\n        \"\"\"Check that everything is ready:\n        - local capability to resize images\n        - thumbnail dirs exist (create them if needed)\n        - detect whether we'll use PIL or IM\n        - detect whether we'll use GIO or Python to get URIs\n        \"\"\"\n        if not ArtResizer.shared.local:\n            self._log.warning(\n                \"No local image resizing capabilities, \"\n                \"cannot generate thumbnails\"\n            )\n            return False\n\n        for dir in (NORMAL_DIR, LARGE_DIR):\n            if not os.path.exists(syspath(dir)):\n                os.makedirs(syspath(dir))\n\n        if not ArtResizer.shared.can_write_metadata:\n            raise RuntimeError(\n                f\"Thumbnails: ArtResizer backend {ArtResizer.shared.method}\"\n                f\" unexpectedly cannot write image metadata.\"\n            )\n        self._log.debug(\"using {.shared.method} to write metadata\", ArtResizer)\n\n        uri_getter = GioURI()\n        if not uri_getter.available:\n            uri_getter = PathlibURI()\n        self._log.debug(\"using {.name} to compute URIs\", uri_getter)\n        self.get_uri = uri_getter.uri\n\n        return True\n\n    def process_album(self, album):\n        \"\"\"Produce thumbnails for the album folder.\"\"\"\n        self._log.debug(\"generating thumbnail for {}\", album)\n        if not album.artpath:\n            self._log.info(\"album {} has no art\", album)\n            return\n\n        if self.config[\"dolphin\"]:\n            self.make_dolphin_cover_thumbnail(album)\n\n        size = ArtResizer.shared.get_size(album.artpath)\n        if not size:\n            self._log.warning(\n                \"problem getting the picture size for {.artpath}\", album\n            )\n            return\n\n        wrote = True\n        if max(size) >= 256:\n            wrote &= self.make_cover_thumbnail(album, 256, LARGE_DIR)\n        wrote &= self.make_cover_thumbnail(album, 128, NORMAL_DIR)\n\n        if wrote:\n            self._log.info(\"wrote thumbnail for {}\", album)\n        else:\n            self._log.info(\"nothing to do for {}\", album)\n\n    def make_cover_thumbnail(self, album, size, target_dir):\n        \"\"\"Make a thumbnail of given size for `album` and put it in\n        `target_dir`.\n        \"\"\"\n        target = os.path.join(target_dir, self.thumbnail_file_name(album.path))\n\n        if (\n            os.path.exists(syspath(target))\n            and os.stat(syspath(target)).st_mtime\n            > os.stat(syspath(album.artpath)).st_mtime\n        ):\n            if self.config[\"force\"]:\n                self._log.debug(\n                    \"found a suitable {0}x{0} thumbnail for {1}, \"\n                    \"forcing regeneration\",\n                    size,\n                    album,\n                )\n            else:\n                self._log.debug(\n                    \"{0}x{0} thumbnail for {1} exists and is recent enough\",\n                    size,\n                    album,\n                )\n                return False\n        resized = ArtResizer.shared.resize(size, album.artpath, target)\n        self.add_tags(album, resized)\n        shutil.move(syspath(resized), syspath(target))\n        return True\n\n    def thumbnail_file_name(self, path):\n        \"\"\"Compute the thumbnail file name\n        See https://standards.freedesktop.org/thumbnail-spec/latest/x227.html\n        \"\"\"\n        uri = self.get_uri(path)\n        hash = md5(uri.encode(\"utf-8\")).hexdigest()\n        return bytestring_path(f\"{hash}.png\")\n\n    def add_tags(self, album, image_path):\n        \"\"\"Write required metadata to the thumbnail\n        See https://standards.freedesktop.org/thumbnail-spec/latest/x142.html\n        \"\"\"\n        mtime = os.stat(syspath(album.artpath)).st_mtime\n        metadata = {\n            \"Thumb::URI\": self.get_uri(album.artpath),\n            \"Thumb::MTime\": str(mtime),\n        }\n        try:\n            ArtResizer.shared.write_metadata(image_path, metadata)\n        except Exception:\n            self._log.exception(\n                \"could not write metadata to {}\", displayable_path(image_path)\n            )\n\n    def make_dolphin_cover_thumbnail(self, album):\n        outfilename = os.path.join(album.path, b\".directory\")\n        if os.path.exists(syspath(outfilename)):\n            return\n        artfile = os.path.split(album.artpath)[1]\n        with open(syspath(outfilename), \"w\") as f:\n            f.write(\"[Desktop Entry]\\n\")\n            f.write(f\"Icon=./{artfile.decode('utf-8')}\")\n            f.close()\n        self._log.debug(\"Wrote file {}\", displayable_path(outfilename))\n\n\nclass URIGetter:\n    available = False\n    name = \"Abstract base\"\n\n    def uri(self, path):\n        raise NotImplementedError()\n\n\nclass PathlibURI(URIGetter):\n    available = True\n    name = \"Python Pathlib\"\n\n    def uri(self, path):\n        return PurePosixPath(os.fsdecode(path)).as_uri()\n\n\ndef copy_c_string(c_string):\n    \"\"\"Copy a `ctypes.POINTER(ctypes.c_char)` value into a new Python\n    string and return it. The old memory is then safe to free.\n    \"\"\"\n    # This is a pretty dumb way to get a string copy, but it seems to\n    # work. A more surefire way would be to allocate a ctypes buffer and copy\n    # the data with `memcpy` or somesuch.\n    return ctypes.cast(c_string, ctypes.c_char_p).value\n\n\nclass GioURI(URIGetter):\n    \"\"\"Use gio URI function g_file_get_uri. Paths must be utf-8 encoded.\"\"\"\n\n    name = \"GIO\"\n\n    def __init__(self):\n        self.libgio = self.get_library()\n        self.available = bool(self.libgio)\n        if self.available:\n            self.libgio.g_type_init()  # for glib < 2.36\n\n            self.libgio.g_file_new_for_path.argtypes = [ctypes.c_char_p]\n            self.libgio.g_file_new_for_path.restype = ctypes.c_void_p\n\n            self.libgio.g_file_get_uri.argtypes = [ctypes.c_void_p]\n            self.libgio.g_file_get_uri.restype = ctypes.POINTER(ctypes.c_char)\n\n            self.libgio.g_object_unref.argtypes = [ctypes.c_void_p]\n\n    def get_library(self):\n        lib_name = ctypes.util.find_library(\"gio-2\")\n        try:\n            if not lib_name:\n                return False\n            return ctypes.cdll.LoadLibrary(lib_name)\n        except OSError:\n            return False\n\n    def uri(self, path):\n        g_file_ptr = self.libgio.g_file_new_for_path(path)\n        if not g_file_ptr:\n            raise RuntimeError(\n                f\"No gfile pointer received for {displayable_path(path)}\"\n            )\n\n        try:\n            uri_ptr = self.libgio.g_file_get_uri(g_file_ptr)\n        finally:\n            self.libgio.g_object_unref(g_file_ptr)\n        if not uri_ptr:\n            self.libgio.g_free(uri_ptr)\n            raise RuntimeError(\n                f\"No URI received from the gfile pointer for {displayable_path(path)}\"\n            )\n\n        try:\n            uri = copy_c_string(uri_ptr)\n        finally:\n            self.libgio.g_free(uri_ptr)\n\n        try:\n            return os.fsdecode(uri)\n        except UnicodeDecodeError:\n            raise RuntimeError(f\"Could not decode filename from GIO: {uri!r}\")\n"
  },
  {
    "path": "beetsplug/titlecase.py",
    "content": "# This file is part of beets.\n# Copyright 2025, Henry Oberholtzer\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Apply NYT manual of style title case rules, to text.\nTitle case logic is derived from the python-titlecase library.\nProvides a template function and a tag modification function.\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom functools import cached_property\nfrom typing import TYPE_CHECKING, TypedDict\n\nfrom titlecase import titlecase\n\nfrom beets import ui\nfrom beets.autotag.hooks import AlbumInfo\nfrom beets.plugins import BeetsPlugin\n\nif TYPE_CHECKING:\n    from beets.autotag.hooks import Info\n    from beets.importer import ImportSession, ImportTask\n    from beets.library import Item\n\n__author__ = \"henryoberholtzer@gmail.com\"\n__version__ = \"1.0\"\n\n\nclass PreservedText(TypedDict):\n    words: dict[str, str]\n    phrases: dict[str, re.Pattern[str]]\n\n\nclass TitlecasePlugin(BeetsPlugin):\n    def __init__(self) -> None:\n        super().__init__()\n\n        self.config.add(\n            {\n                \"auto\": True,\n                \"preserve\": [],\n                \"fields\": [],\n                \"replace\": [],\n                \"separators\": [],\n                \"force_lowercase\": False,\n                \"small_first_last\": True,\n                \"the_artist\": True,\n                \"all_caps\": False,\n                \"all_lowercase\": False,\n                \"after_choice\": False,\n            }\n        )\n\n        \"\"\"\n        auto - Automatically apply titlecase to new import metadata.\n        preserve - Provide a list of strings with specific case requirements.\n        fields - Fields to apply titlecase to.\n        replace - List of pairs, first is the target, second is the replacement\n        separators - Other characters to treat like periods.\n        force_lowercase - Lowercase the string before titlecase.\n        small_first_last - If small characters should be cased at the start of strings.\n        the_artist - If the plugin infers the field to be an artist field\n        (e.g. the field contains \"artist\")\n        It will capitalize a lowercase The, helpful for the artist names\n        that start with 'The', like 'The Who' or 'The Talking Heads' when\n        they are not at the start of a string. Superseded by preserved phrases.\n        all_caps - If the alphabet in the string is all uppercase, do not modify.\n        all_lowercase - If the alphabet in the string is all lowercase, do not modify.\n        \"\"\"\n        # Register template function\n        self.template_funcs[\"titlecase\"] = self.titlecase\n\n        # Register UI subcommands\n        self._command = ui.Subcommand(\n            \"titlecase\",\n            help=\"Apply titlecasing to metadata specified in config.\",\n        )\n\n        if self.config[\"auto\"].get(bool):\n            if self.config[\"after_choice\"].get(bool):\n                self.import_stages = [self.imported]\n            else:\n                self.register_listener(\n                    \"trackinfo_received\", self.received_info_handler\n                )\n                self.register_listener(\n                    \"albuminfo_received\", self.received_info_handler\n                )\n\n    @cached_property\n    def force_lowercase(self) -> bool:\n        return self.config[\"force_lowercase\"].get(bool)\n\n    @cached_property\n    def replace(self) -> list[tuple[str, str]]:\n        return self.config[\"replace\"].as_pairs(default_value=\"\")\n\n    @cached_property\n    def the_artist(self) -> bool:\n        return self.config[\"the_artist\"].get(bool)\n\n    @cached_property\n    def fields_to_process(self) -> set[str]:\n        fields = set(self.config[\"fields\"].as_str_seq())\n        self._log.debug(f\"fields: {', '.join(fields)}\")\n        return fields\n\n    @cached_property\n    def preserve(self) -> PreservedText:\n        strings = self.config[\"preserve\"].as_str_seq()\n        preserved: PreservedText = {\"words\": {}, \"phrases\": {}}\n        for s in strings:\n            if \" \" in s:\n                preserved[\"phrases\"][s] = re.compile(\n                    rf\"\\b{re.escape(s)}\\b\", re.IGNORECASE\n                )\n            else:\n                preserved[\"words\"][s.upper()] = s\n        return preserved\n\n    @cached_property\n    def separators(self) -> re.Pattern[str] | None:\n        if separators := \"\".join(\n            dict.fromkeys(self.config[\"separators\"].as_str_seq())\n        ):\n            return re.compile(rf\"(.*?[{re.escape(separators)}]+)(\\s*)(?=.)\")\n        return None\n\n    @cached_property\n    def small_first_last(self) -> bool:\n        return self.config[\"small_first_last\"].get(bool)\n\n    @cached_property\n    def all_caps(self) -> bool:\n        return self.config[\"all_caps\"].get(bool)\n\n    @cached_property\n    def all_lowercase(self) -> bool:\n        return self.config[\"all_lowercase\"].get(bool)\n\n    @cached_property\n    def the_artist_regexp(self) -> re.Pattern[str]:\n        return re.compile(r\"\\bthe\\b\")\n\n    def titlecase_callback(self, word, **kwargs) -> str | None:\n        \"\"\"Callback function for words to preserve case of.\"\"\"\n        if preserved_word := self.preserve[\"words\"].get(word.upper(), \"\"):\n            return preserved_word\n        return None\n\n    def received_info_handler(self, info: Info):\n        \"\"\"Calls titlecase fields for AlbumInfo or TrackInfo\n        Processes the tracks field for AlbumInfo\n        \"\"\"\n        self.titlecase_fields(info)\n        if isinstance(info, AlbumInfo):\n            for track in info.tracks:\n                self.titlecase_fields(track)\n\n    def commands(self) -> list[ui.Subcommand]:\n        def func(lib, opts, args):\n            write = ui.should_write()\n            for item in lib.items(args):\n                self._log.info(f\"titlecasing {item.title}:\")\n                self.titlecase_fields(item)\n                item.store()\n                if write:\n                    item.try_write()\n\n        self._command.func = func\n        return [self._command]\n\n    def titlecase_fields(self, item: Item | Info) -> None:\n        \"\"\"Applies titlecase to fields, except\n        those excluded by the default exclusions and the\n        set exclude lists.\n        \"\"\"\n        for field in self.fields_to_process:\n            init_field = getattr(item, field, \"\")\n            if init_field:\n                if isinstance(init_field, list) and isinstance(\n                    init_field[0], str\n                ):\n                    cased_list: list[str] = [\n                        self.titlecase(i, field) for i in init_field\n                    ]\n                    if cased_list != init_field:\n                        setattr(item, field, cased_list)\n                        self._log.debug(\n                            f\"{field}: {', '.join(init_field)} ->\",\n                            f\"{', '.join(cased_list)}\",\n                        )\n                elif isinstance(init_field, str):\n                    cased: str = self.titlecase(init_field, field)\n                    if cased != init_field:\n                        setattr(item, field, cased)\n                        self._log.debug(f\"{field}: {init_field} -> {cased}\")\n                else:\n                    self._log.debug(f\"{field}: no string present\")\n            else:\n                self._log.debug(f\"{field}: does not exist on {type(item)}\")\n\n    def titlecase(self, text: str, field: str = \"\") -> str:\n        \"\"\"Titlecase the given text.\"\"\"\n        # Check we should split this into two substrings.\n        if self.separators:\n            if len(splits := self.separators.findall(text)):\n                split_cased = \"\".join(\n                    [self.titlecase(s[0], field) + s[1] for s in splits]\n                )\n                # Add on the remaining portion\n                return split_cased + self.titlecase(\n                    text[len(split_cased) :], field\n                )\n        # Check if A-Z is all uppercase or all lowercase\n        if self.all_lowercase and text.islower():\n            return text\n        elif self.all_caps and text.isupper():\n            return text\n        # Any necessary replacements go first, mainly punctuation.\n        titlecased = text.lower() if self.force_lowercase else text\n        for pair in self.replace:\n            target, replacement = pair\n            titlecased = titlecased.replace(target, replacement)\n        # General titlecase operation\n        titlecased = titlecase(\n            titlecased,\n            small_first_last=self.small_first_last,\n            callback=self.titlecase_callback,\n        )\n        # Apply \"The Artist\" feature\n        if self.the_artist and \"artist\" in field:\n            titlecased = self.the_artist_regexp.sub(\"The\", titlecased)\n        # More complicated phrase replacements.\n        for phrase, regexp in self.preserve[\"phrases\"].items():\n            titlecased = regexp.sub(phrase, titlecased)\n        return titlecased\n\n    def imported(self, session: ImportSession, task: ImportTask) -> None:\n        \"\"\"Import hook for titlecasing on import.\"\"\"\n        for item in task.imported_items():\n            try:\n                self._log.debug(f\"titlecasing {item.title}:\")\n                self.titlecase_fields(item)\n                item.store()\n            except Exception as e:\n                self._log.debug(f\"titlecasing exception {e}\")\n"
  },
  {
    "path": "beetsplug/types.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Thomas Scholtes.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nfrom confuse import ConfigValueError\n\nfrom beets.dbcore import types\nfrom beets.plugins import BeetsPlugin\n\n\nclass TypesPlugin(BeetsPlugin):\n    @property\n    def item_types(self):\n        return self._types()\n\n    @property\n    def album_types(self):\n        return self._types()\n\n    def _types(self):\n        if not self.config.exists():\n            return {}\n\n        mytypes = {}\n        for key, value in self.config.items():\n            if value.get() == \"int\":\n                mytypes[key] = types.INTEGER\n            elif value.get() == \"float\":\n                mytypes[key] = types.FLOAT\n            elif value.get() == \"bool\":\n                mytypes[key] = types.BOOLEAN\n            elif value.get() == \"date\":\n                mytypes[key] = types.DATE\n            else:\n                raise ConfigValueError(\n                    f\"unknown type '{value}' for the '{key}' field\"\n                )\n        return mytypes\n"
  },
  {
    "path": "beetsplug/unimported.py",
    "content": "# This file is part of beets.\n# Copyright 2019, Joris Jensen\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"\nList all files in the library folder which are not listed in the\n beets library database, including art files\n\"\"\"\n\nimport os\n\nfrom beets import util\nfrom beets.plugins import BeetsPlugin\nfrom beets.ui import Subcommand, print_\n\n__author__ = \"https://github.com/MrNuggelz\"\n\n\nclass Unimported(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n        self.config.add({\"ignore_extensions\": [], \"ignore_subdirectories\": []})\n\n    def commands(self):\n        def print_unimported(lib, opts, args):\n            ignore_exts = [\n                f\".{x}\".encode()\n                for x in self.config[\"ignore_extensions\"].as_str_seq()\n            ]\n            ignore_dirs = [\n                os.path.join(lib.directory, x.encode())\n                for x in self.config[\"ignore_subdirectories\"].as_str_seq()\n            ]\n            in_folder = set()\n            for root, _, files in os.walk(lib.directory):\n                # do not traverse if root is a child of an ignored directory\n                if any(root.startswith(ignored) for ignored in ignore_dirs):\n                    continue\n                for file in files:\n                    # ignore files with ignored extensions\n                    if any(file.endswith(ext) for ext in ignore_exts):\n                        continue\n                    in_folder.add(os.path.join(root, file))\n\n            in_library = {x.path for x in lib.items()}\n            art_files = {x.artpath for x in lib.albums()}\n            for f in in_folder - in_library - art_files:\n                print_(util.displayable_path(f))\n\n        unimported = Subcommand(\n            \"unimported\",\n            help=\"list all files in the library folder which are not listed\"\n            \" in the beets library database\",\n        )\n        unimported.func = print_unimported\n        return [unimported]\n"
  },
  {
    "path": "beetsplug/web/__init__.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"A Web interface to beets.\"\"\"\n\nimport base64\nimport json\nimport os\nimport typing as t\n\nimport flask\nfrom flask import jsonify\nfrom unidecode import unidecode\nfrom werkzeug.routing import BaseConverter, PathConverter\n\nimport beets.library\nfrom beets import ui, util\nfrom beets.dbcore.query import PathQuery\nfrom beets.plugins import BeetsPlugin\n\n# Type checking hacks\n\nif t.TYPE_CHECKING:\n\n    class LibraryCtx(flask.ctx._AppCtxGlobals):\n        lib: beets.library.Library\n\n    g = LibraryCtx()\nelse:\n    from flask import g\n\n# Utilities.\n\n\ndef _rep(obj, expand=False):\n    \"\"\"Get a flat -- i.e., JSON-ish -- representation of a beets Item or\n    Album object. For Albums, `expand` dictates whether tracks are\n    included.\n    \"\"\"\n    out = dict(obj)\n\n    if isinstance(obj, beets.library.Item):\n        if app.config.get(\"INCLUDE_PATHS\", False):\n            out[\"path\"] = util.displayable_path(out[\"path\"])\n        else:\n            del out[\"path\"]\n\n        # Filter all bytes attributes and convert them to strings.\n        for key, value in out.items():\n            if isinstance(out[key], bytes):\n                out[key] = base64.b64encode(value).decode(\"ascii\")\n\n        # Get the size (in bytes) of the backing file. This is useful\n        # for the Tomahawk resolver API.\n        try:\n            out[\"size\"] = os.path.getsize(util.syspath(obj.path))\n        except OSError:\n            out[\"size\"] = 0\n\n        return out\n\n    elif isinstance(obj, beets.library.Album):\n        if app.config.get(\"INCLUDE_PATHS\", False):\n            out[\"artpath\"] = util.displayable_path(out[\"artpath\"])\n        else:\n            del out[\"artpath\"]\n        if expand:\n            out[\"items\"] = [_rep(item) for item in obj.items()]\n        return out\n\n\ndef json_generator(items, root, expand=False):\n    \"\"\"Generator that dumps list of beets Items or Albums as JSON\n\n    :param root:  root key for JSON\n    :param items: list of :class:`Item` or :class:`Album` to dump\n    :param expand: If true every :class:`Album` contains its items in the json\n                   representation\n    :returns:     generator that yields strings\n    \"\"\"\n    yield f'{{\"{root}\":['\n    first = True\n    for item in items:\n        if first:\n            first = False\n        else:\n            yield \",\"\n        yield json.dumps(_rep(item, expand=expand))\n    yield \"]}\"\n\n\ndef is_expand():\n    \"\"\"Returns whether the current request is for an expanded response.\"\"\"\n\n    return flask.request.args.get(\"expand\") is not None\n\n\ndef is_delete():\n    \"\"\"Returns whether the current delete request should remove the selected\n    files.\n    \"\"\"\n\n    return flask.request.args.get(\"delete\") is not None\n\n\ndef get_method():\n    \"\"\"Returns the HTTP method of the current request.\"\"\"\n    return flask.request.method\n\n\ndef resource(name, patchable=False):\n    \"\"\"Decorates a function to handle RESTful HTTP requests for a resource.\"\"\"\n\n    def make_responder(retriever):\n        def responder(ids):\n            entities = [retriever(id) for id in ids]\n            entities = [entity for entity in entities if entity]\n\n            if get_method() == \"DELETE\":\n                if app.config.get(\"READONLY\", True):\n                    return flask.abort(405)\n\n                for entity in entities:\n                    entity.remove(delete=is_delete())\n\n                return flask.make_response(jsonify({\"deleted\": True}), 200)\n\n            elif get_method() == \"PATCH\" and patchable:\n                if app.config.get(\"READONLY\", True):\n                    return flask.abort(405)\n\n                for entity in entities:\n                    entity.update(flask.request.get_json())\n                    entity.try_sync(True, False)  # write, don't move\n\n                if len(entities) == 1:\n                    return flask.jsonify(_rep(entities[0], expand=is_expand()))\n                elif entities:\n                    return app.response_class(\n                        json_generator(entities, root=name),\n                        mimetype=\"application/json\",\n                    )\n\n            elif get_method() == \"GET\":\n                if len(entities) == 1:\n                    return flask.jsonify(_rep(entities[0], expand=is_expand()))\n                elif entities:\n                    return app.response_class(\n                        json_generator(entities, root=name),\n                        mimetype=\"application/json\",\n                    )\n                else:\n                    return flask.abort(404)\n\n            else:\n                return flask.abort(405)\n\n        responder.__name__ = f\"get_{name}\"\n\n        return responder\n\n    return make_responder\n\n\ndef resource_query(name, patchable=False):\n    \"\"\"Decorates a function to handle RESTful HTTP queries for resources.\"\"\"\n\n    def make_responder(query_func):\n        def responder(queries):\n            entities = query_func(queries)\n\n            if get_method() == \"DELETE\":\n                if app.config.get(\"READONLY\", True):\n                    return flask.abort(405)\n\n                for entity in entities:\n                    entity.remove(delete=is_delete())\n\n                return flask.make_response(jsonify({\"deleted\": True}), 200)\n\n            elif get_method() == \"PATCH\" and patchable:\n                if app.config.get(\"READONLY\", True):\n                    return flask.abort(405)\n\n                for entity in entities:\n                    entity.update(flask.request.get_json())\n                    entity.try_sync(True, False)  # write, don't move\n\n                return app.response_class(\n                    json_generator(entities, root=name),\n                    mimetype=\"application/json\",\n                )\n\n            elif get_method() == \"GET\":\n                return app.response_class(\n                    json_generator(\n                        entities, root=\"results\", expand=is_expand()\n                    ),\n                    mimetype=\"application/json\",\n                )\n\n            else:\n                return flask.abort(405)\n\n        responder.__name__ = f\"query_{name}\"\n\n        return responder\n\n    return make_responder\n\n\ndef resource_list(name):\n    \"\"\"Decorates a function to handle RESTful HTTP request for a list of\n    resources.\n    \"\"\"\n\n    def make_responder(list_all):\n        def responder():\n            return app.response_class(\n                json_generator(list_all(), root=name, expand=is_expand()),\n                mimetype=\"application/json\",\n            )\n\n        responder.__name__ = f\"all_{name}\"\n        return responder\n\n    return make_responder\n\n\ndef _get_unique_table_field_values(model, field, sort_field):\n    \"\"\"retrieve all unique values belonging to a key from a model\"\"\"\n    if field not in model.all_keys() or sort_field not in model.all_keys():\n        raise KeyError\n    with g.lib.transaction() as tx:\n        rows = tx.query(\n            f\"SELECT DISTINCT {field} FROM {model._table} ORDER BY {sort_field}\"\n        )\n    return [row[0] for row in rows]\n\n\nclass IdListConverter(BaseConverter):\n    \"\"\"Converts comma separated lists of ids in urls to integer lists.\"\"\"\n\n    def to_python(self, value):\n        ids = []\n        for id in value.split(\",\"):\n            try:\n                ids.append(int(id))\n            except ValueError:\n                pass\n        return ids\n\n    def to_url(self, value):\n        return \",\".join(str(v) for v in value)\n\n\nclass QueryConverter(PathConverter):\n    \"\"\"Converts slash separated lists of queries in the url to string list.\"\"\"\n\n    def to_python(self, value):\n        queries = value.split(\"/\")\n        \"\"\"Do not do path substitution on regex value tests\"\"\"\n        return [\n            query if \"::\" in query else query.replace(\"\\\\\", os.sep)\n            for query in queries\n        ]\n\n    def to_url(self, value):\n        return \"/\".join([v.replace(os.sep, \"\\\\\") for v in value])\n\n\nclass EverythingConverter(PathConverter):\n    part_isolating = False\n    regex = \".*?\"\n\n\n# Flask setup.\n\napp = flask.Flask(__name__)\napp.url_map.converters[\"idlist\"] = IdListConverter\napp.url_map.converters[\"query\"] = QueryConverter\napp.url_map.converters[\"everything\"] = EverythingConverter\n\n\n@app.before_request\ndef before_request():\n    g.lib = app.config[\"lib\"]\n\n\n# Items.\n\n\n@app.route(\"/item/<idlist:ids>\", methods=[\"GET\", \"DELETE\", \"PATCH\"])\n@resource(\"items\", patchable=True)\ndef get_item(id):\n    return g.lib.get_item(id)\n\n\n@app.route(\"/item/\")\n@app.route(\"/item/query/\")\n@resource_list(\"items\")\ndef all_items():\n    return g.lib.items()\n\n\n@app.route(\"/item/<int:item_id>/file\")\ndef item_file(item_id):\n    item = g.lib.get_item(item_id)\n\n    item_path = util.syspath(item.path)\n    base_filename = os.path.basename(item_path)\n\n    try:\n        # Imitate http.server behaviour\n        base_filename.encode(\"latin-1\", \"strict\")\n    except UnicodeError:\n        safe_filename = unidecode(base_filename)\n    else:\n        safe_filename = base_filename\n\n    response = flask.send_file(\n        item_path, as_attachment=True, download_name=safe_filename\n    )\n    return response\n\n\n@app.route(\"/item/query/<query:queries>\", methods=[\"GET\", \"DELETE\", \"PATCH\"])\n@resource_query(\"items\", patchable=True)\ndef item_query(queries):\n    return g.lib.items(queries)\n\n\n@app.route(\"/item/path/<everything:path>\")\ndef item_at_path(path):\n    query = PathQuery(\"path\", path.encode(\"utf-8\"))\n    item = g.lib.items(query).get()\n    if item:\n        return flask.jsonify(_rep(item))\n    else:\n        return flask.abort(404)\n\n\n@app.route(\"/item/values/<string:key>\")\ndef item_unique_field_values(key):\n    sort_key = flask.request.args.get(\"sort_key\", key)\n    try:\n        values = _get_unique_table_field_values(\n            beets.library.Item, key, sort_key\n        )\n    except KeyError:\n        return flask.abort(404)\n    return flask.jsonify(values=values)\n\n\n# Albums.\n\n\n@app.route(\"/album/<idlist:ids>\", methods=[\"GET\", \"DELETE\"])\n@resource(\"albums\")\ndef get_album(id):\n    return g.lib.get_album(id)\n\n\n@app.route(\"/album/\")\n@app.route(\"/album/query/\")\n@resource_list(\"albums\")\ndef all_albums():\n    return g.lib.albums()\n\n\n@app.route(\"/album/query/<query:queries>\", methods=[\"GET\", \"DELETE\"])\n@resource_query(\"albums\")\ndef album_query(queries):\n    return g.lib.albums(queries)\n\n\n@app.route(\"/album/<int:album_id>/art\")\ndef album_art(album_id):\n    album = g.lib.get_album(album_id)\n    if album and album.artpath:\n        return flask.send_file(album.artpath.decode())\n    else:\n        return flask.abort(404)\n\n\n@app.route(\"/album/values/<string:key>\")\ndef album_unique_field_values(key):\n    sort_key = flask.request.args.get(\"sort_key\", key)\n    try:\n        values = _get_unique_table_field_values(\n            beets.library.Album, key, sort_key\n        )\n    except KeyError:\n        return flask.abort(404)\n    return flask.jsonify(values=values)\n\n\n# Artists.\n\n\n@app.route(\"/artist/\")\ndef all_artists():\n    with g.lib.transaction() as tx:\n        rows = tx.query(\"SELECT DISTINCT albumartist FROM albums\")\n    all_artists = [row[0] for row in rows]\n    return flask.jsonify(artist_names=all_artists)\n\n\n# Library information.\n\n\n@app.route(\"/stats\")\ndef stats():\n    with g.lib.transaction() as tx:\n        item_rows = tx.query(\"SELECT COUNT(*) FROM items\")\n        album_rows = tx.query(\"SELECT COUNT(*) FROM albums\")\n    return flask.jsonify(\n        {\n            \"items\": item_rows[0][0],\n            \"albums\": album_rows[0][0],\n        }\n    )\n\n\n# UI.\n\n\n@app.route(\"/\")\ndef home():\n    return flask.render_template(\"index.html\")\n\n\n# Plugin hook.\n\n\nclass WebPlugin(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n        self.config.add(\n            {\n                \"host\": \"127.0.0.1\",\n                \"port\": 8337,\n                \"cors\": \"\",\n                \"cors_supports_credentials\": False,\n                \"reverse_proxy\": False,\n                \"include_paths\": False,\n                \"readonly\": True,\n            }\n        )\n\n    def commands(self):\n        cmd = ui.Subcommand(\"web\", help=\"start a Web interface\")\n        cmd.parser.add_option(\n            \"-d\",\n            \"--debug\",\n            action=\"store_true\",\n            default=False,\n            help=\"debug mode\",\n        )\n\n        def func(lib, opts, args):\n            args = args\n            if args:\n                self.config[\"host\"] = args.pop(0)\n            if args:\n                self.config[\"port\"] = int(args.pop(0))\n\n            app.config[\"lib\"] = lib\n            # Normalizes json output\n            app.config[\"JSONIFY_PRETTYPRINT_REGULAR\"] = False\n\n            app.config[\"INCLUDE_PATHS\"] = self.config[\"include_paths\"]\n            app.config[\"READONLY\"] = self.config[\"readonly\"]\n\n            # Enable CORS if required.\n            if self.config[\"cors\"]:\n                self._log.info(\n                    \"Enabling CORS with origin: {}\", self.config[\"cors\"]\n                )\n                from flask_cors import CORS\n\n                app.config[\"CORS_ALLOW_HEADERS\"] = \"Content-Type\"\n                app.config[\"CORS_RESOURCES\"] = {\n                    r\"/*\": {\"origins\": self.config[\"cors\"].get(str)}\n                }\n                CORS(\n                    app,\n                    supports_credentials=self.config[\n                        \"cors_supports_credentials\"\n                    ].get(bool),\n                )\n\n            # Allow serving behind a reverse proxy\n            if self.config[\"reverse_proxy\"]:\n                app.wsgi_app = ReverseProxied(app.wsgi_app)\n\n            # Start the web application.\n            app.run(\n                host=self.config[\"host\"].as_str(),\n                port=self.config[\"port\"].get(int),\n                debug=opts.debug,\n                threaded=True,\n            )\n\n        cmd.func = func\n        return [cmd]\n\n\nclass ReverseProxied:\n    \"\"\"Wrap the application in this middleware and configure the\n    front-end server to add these headers, to let you quietly bind\n    this to a URL other than / and to an HTTP scheme that is\n    different than what is used locally.\n\n    In nginx:\n    location /myprefix {\n        proxy_pass http://192.168.0.1:5001;\n        proxy_set_header Host $host;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Scheme $scheme;\n        proxy_set_header X-Script-Name /myprefix;\n        }\n\n    From: http://flask.pocoo.org/snippets/35/\n\n    :param app: the WSGI application\n    \"\"\"\n\n    def __init__(self, app):\n        self.app = app\n\n    def __call__(self, environ, start_response):\n        script_name = environ.get(\"HTTP_X_SCRIPT_NAME\", \"\")\n        if script_name:\n            environ[\"SCRIPT_NAME\"] = script_name\n            path_info = environ[\"PATH_INFO\"]\n            if path_info.startswith(script_name):\n                environ[\"PATH_INFO\"] = path_info[len(script_name) :]\n\n        scheme = environ.get(\"HTTP_X_SCHEME\", \"\")\n        if scheme:\n            environ[\"wsgi.url_scheme\"] = scheme\n        return self.app(environ, start_response)\n"
  },
  {
    "path": "beetsplug/web/static/backbone.js",
    "content": "//     Backbone.js 0.5.3\n//     (c) 2010 Jeremy Ashkenas, DocumentCloud Inc.\n//     Backbone may be freely distributed under the MIT license.\n//     For all details and documentation:\n//     http://documentcloud.github.com/backbone\n\n(function(){\n\n  // Initial Setup\n  // -------------\n\n  // Save a reference to the global object.\n  var root = this;\n\n  // Save the previous value of the `Backbone` variable.\n  var previousBackbone = root.Backbone;\n\n  // The top-level namespace. All public Backbone classes and modules will\n  // be attached to this. Exported for both CommonJS and the browser.\n  var Backbone;\n  if (typeof exports !== 'undefined') {\n    Backbone = exports;\n  } else {\n    Backbone = root.Backbone = {};\n  }\n\n  // Current version of the library. Keep in sync with `package.json`.\n  Backbone.VERSION = '0.5.3';\n\n  // Require Underscore, if we're on the server, and it's not already present.\n  var _ = root._;\n  if (!_ && (typeof require !== 'undefined')) _ = require('underscore')._;\n\n  // For Backbone's purposes, jQuery or Zepto owns the `$` variable.\n  var $ = root.jQuery || root.Zepto;\n\n  // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable\n  // to its previous owner. Returns a reference to this Backbone object.\n  Backbone.noConflict = function() {\n    root.Backbone = previousBackbone;\n    return this;\n  };\n\n  // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option will\n  // fake `\"PUT\"` and `\"DELETE\"` requests via the `_method` parameter and set a\n  // `X-Http-Method-Override` header.\n  Backbone.emulateHTTP = false;\n\n  // Turn on `emulateJSON` to support legacy servers that can't deal with direct\n  // `application/json` requests ... will encode the body as\n  // `application/x-www-form-urlencoded` instead and will send the model in a\n  // form param named `model`.\n  Backbone.emulateJSON = false;\n\n  // Backbone.Events\n  // -----------------\n\n  // A module that can be mixed in to *any object* in order to provide it with\n  // custom events. You may `bind` or `unbind` a callback function to an event;\n  // `trigger`-ing an event fires all callbacks in succession.\n  //\n  //     var object = {};\n  //     _.extend(object, Backbone.Events);\n  //     object.bind('expand', function(){ alert('expanded'); });\n  //     object.trigger('expand');\n  //\n  Backbone.Events = {\n\n    // Bind an event, specified by a string name, `ev`, to a `callback` function.\n    // Passing `\"all\"` will bind the callback to all events fired.\n    bind : function(ev, callback, context) {\n      var calls = this._callbacks || (this._callbacks = {});\n      var list  = calls[ev] || (calls[ev] = []);\n      list.push([callback, context]);\n      return this;\n    },\n\n    // Remove one or many callbacks. If `callback` is null, removes all\n    // callbacks for the event. If `ev` is null, removes all bound callbacks\n    // for all events.\n    unbind : function(ev, callback) {\n      var calls;\n      if (!ev) {\n        this._callbacks = {};\n      } else if (calls = this._callbacks) {\n        if (!callback) {\n          calls[ev] = [];\n        } else {\n          var list = calls[ev];\n          if (!list) return this;\n          for (var i = 0, l = list.length; i < l; i++) {\n            if (list[i] && callback === list[i][0]) {\n              list[i] = null;\n              break;\n            }\n          }\n        }\n      }\n      return this;\n    },\n\n    // Trigger an event, firing all bound callbacks. Callbacks are passed the\n    // same arguments as `trigger` is, apart from the event name.\n    // Listening for `\"all\"` passes the true event name as the first argument.\n    trigger : function(eventName) {\n      var list, calls, ev, callback, args;\n      var both = 2;\n      if (!(calls = this._callbacks)) return this;\n      while (both--) {\n        ev = both ? eventName : 'all';\n        if (list = calls[ev]) {\n          for (var i = 0, l = list.length; i < l; i++) {\n            if (!(callback = list[i])) {\n              list.splice(i, 1); i--; l--;\n            } else {\n              args = both ? Array.prototype.slice.call(arguments, 1) : arguments;\n              callback[0].apply(callback[1] || this, args);\n            }\n          }\n        }\n      }\n      return this;\n    }\n\n  };\n\n  // Backbone.Model\n  // --------------\n\n  // Create a new model, with defined attributes. A client id (`cid`)\n  // is automatically generated and assigned for you.\n  Backbone.Model = function(attributes, options) {\n    var defaults;\n    attributes || (attributes = {});\n    if (defaults = this.defaults) {\n      if (_.isFunction(defaults)) defaults = defaults.call(this);\n      attributes = _.extend({}, defaults, attributes);\n    }\n    this.attributes = {};\n    this._escapedAttributes = {};\n    this.cid = _.uniqueId('c');\n    this.set(attributes, {silent : true});\n    this._changed = false;\n    this._previousAttributes = _.clone(this.attributes);\n    if (options && options.collection) this.collection = options.collection;\n    this.initialize(attributes, options);\n  };\n\n  // Attach all inheritable methods to the Model prototype.\n  _.extend(Backbone.Model.prototype, Backbone.Events, {\n\n    // A snapshot of the model's previous attributes, taken immediately\n    // after the last `\"change\"` event was fired.\n    _previousAttributes : null,\n\n    // Has the item been changed since the last `\"change\"` event?\n    _changed : false,\n\n    // The default name for the JSON `id` attribute is `\"id\"`. MongoDB and\n    // CouchDB users may want to set this to `\"_id\"`.\n    idAttribute : 'id',\n\n    // Initialize is an empty function by default. Override it with your own\n    // initialization logic.\n    initialize : function(){},\n\n    // Return a copy of the model's `attributes` object.\n    toJSON : function() {\n      return _.clone(this.attributes);\n    },\n\n    // Get the value of an attribute.\n    get : function(attr) {\n      return this.attributes[attr];\n    },\n\n    // Get the HTML-escaped value of an attribute.\n    escape : function(attr) {\n      var html;\n      if (html = this._escapedAttributes[attr]) return html;\n      var val = this.attributes[attr];\n      return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : '' + val);\n    },\n\n    // Returns `true` if the attribute contains a value that is not null\n    // or undefined.\n    has : function(attr) {\n      return this.attributes[attr] != null;\n    },\n\n    // Set a hash of model attributes on the object, firing `\"change\"` unless you\n    // choose to silence it.\n    set : function(attrs, options) {\n\n      // Extract attributes and options.\n      options || (options = {});\n      if (!attrs) return this;\n      if (attrs.attributes) attrs = attrs.attributes;\n      var now = this.attributes, escaped = this._escapedAttributes;\n\n      // Run validation.\n      if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false;\n\n      // Check for changes of `id`.\n      if (this.idAttribute in attrs) this.id = attrs[this.idAttribute];\n\n      // We're about to start triggering change events.\n      var alreadyChanging = this._changing;\n      this._changing = true;\n\n      // Update attributes.\n      for (var attr in attrs) {\n        var val = attrs[attr];\n        if (!_.isEqual(now[attr], val)) {\n          now[attr] = val;\n          delete escaped[attr];\n          this._changed = true;\n          if (!options.silent) this.trigger('change:' + attr, this, val, options);\n        }\n      }\n\n      // Fire the `\"change\"` event, if the model has been changed.\n      if (!alreadyChanging && !options.silent && this._changed) this.change(options);\n      this._changing = false;\n      return this;\n    },\n\n    // Remove an attribute from the model, firing `\"change\"` unless you choose\n    // to silence it. `unset` is a noop if the attribute doesn't exist.\n    unset : function(attr, options) {\n      if (!(attr in this.attributes)) return this;\n      options || (options = {});\n      var value = this.attributes[attr];\n\n      // Run validation.\n      var validObj = {};\n      validObj[attr] = void 0;\n      if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false;\n\n      // Remove the attribute.\n      delete this.attributes[attr];\n      delete this._escapedAttributes[attr];\n      if (attr == this.idAttribute) delete this.id;\n      this._changed = true;\n      if (!options.silent) {\n        this.trigger('change:' + attr, this, void 0, options);\n        this.change(options);\n      }\n      return this;\n    },\n\n    // Clear all attributes on the model, firing `\"change\"` unless you choose\n    // to silence it.\n    clear : function(options) {\n      options || (options = {});\n      var attr;\n      var old = this.attributes;\n\n      // Run validation.\n      var validObj = {};\n      for (attr in old) validObj[attr] = void 0;\n      if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false;\n\n      this.attributes = {};\n      this._escapedAttributes = {};\n      this._changed = true;\n      if (!options.silent) {\n        for (attr in old) {\n          this.trigger('change:' + attr, this, void 0, options);\n        }\n        this.change(options);\n      }\n      return this;\n    },\n\n    // Fetch the model from the server. If the server's representation of the\n    // model differs from its current attributes, they will be overridden,\n    // triggering a `\"change\"` event.\n    fetch : function(options) {\n      options || (options = {});\n      var model = this;\n      var success = options.success;\n      options.success = function(resp, status, xhr) {\n        if (!model.set(model.parse(resp, xhr), options)) return false;\n        if (success) success(model, resp);\n      };\n      options.error = wrapError(options.error, model, options);\n      return (this.sync || Backbone.sync).call(this, 'read', this, options);\n    },\n\n    // Set a hash of model attributes, and sync the model to the server.\n    // If the server returns an attributes hash that differs, the model's\n    // state will be `set` again.\n    save : function(attrs, options) {\n      options || (options = {});\n      if (attrs && !this.set(attrs, options)) return false;\n      var model = this;\n      var success = options.success;\n      options.success = function(resp, status, xhr) {\n        if (!model.set(model.parse(resp, xhr), options)) return false;\n        if (success) success(model, resp, xhr);\n      };\n      options.error = wrapError(options.error, model, options);\n      var method = this.isNew() ? 'create' : 'update';\n      return (this.sync || Backbone.sync).call(this, method, this, options);\n    },\n\n    // Destroy this model on the server if it was already persisted. Upon success, the model is removed\n    // from its collection, if it has one.\n    destroy : function(options) {\n      options || (options = {});\n      if (this.isNew()) return this.trigger('destroy', this, this.collection, options);\n      var model = this;\n      var success = options.success;\n      options.success = function(resp) {\n        model.trigger('destroy', model, model.collection, options);\n        if (success) success(model, resp);\n      };\n      options.error = wrapError(options.error, model, options);\n      return (this.sync || Backbone.sync).call(this, 'delete', this, options);\n    },\n\n    // Default URL for the model's representation on the server -- if you're\n    // using Backbone's restful methods, override this to change the endpoint\n    // that will be called.\n    url : function() {\n      var base = getUrl(this.collection) || this.urlRoot || urlError();\n      if (this.isNew()) return base;\n      return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);\n    },\n\n    // **parse** converts a response into the hash of attributes to be `set` on\n    // the model. The default implementation is just to pass the response along.\n    parse : function(resp, xhr) {\n      return resp;\n    },\n\n    // Create a new model with identical attributes to this one.\n    clone : function() {\n      return new this.constructor(this);\n    },\n\n    // A model is new if it has never been saved to the server, and lacks an id.\n    isNew : function() {\n      return this.id == null;\n    },\n\n    // Call this method to manually fire a `change` event for this model.\n    // Calling this will cause all objects observing the model to update.\n    change : function(options) {\n      this.trigger('change', this, options);\n      this._previousAttributes = _.clone(this.attributes);\n      this._changed = false;\n    },\n\n    // Determine if the model has changed since the last `\"change\"` event.\n    // If you specify an attribute name, determine if that attribute has changed.\n    hasChanged : function(attr) {\n      if (attr) return this._previousAttributes[attr] != this.attributes[attr];\n      return this._changed;\n    },\n\n    // Return an object containing all the attributes that have changed, or false\n    // if there are no changed attributes. Useful for determining what parts of a\n    // view need to be updated and/or what attributes need to be persisted to\n    // the server.\n    changedAttributes : function(now) {\n      now || (now = this.attributes);\n      var old = this._previousAttributes;\n      var changed = false;\n      for (var attr in now) {\n        if (!_.isEqual(old[attr], now[attr])) {\n          changed = changed || {};\n          changed[attr] = now[attr];\n        }\n      }\n      return changed;\n    },\n\n    // Get the previous value of an attribute, recorded at the time the last\n    // `\"change\"` event was fired.\n    previous : function(attr) {\n      if (!attr || !this._previousAttributes) return null;\n      return this._previousAttributes[attr];\n    },\n\n    // Get all of the attributes of the model at the time of the previous\n    // `\"change\"` event.\n    previousAttributes : function() {\n      return _.clone(this._previousAttributes);\n    },\n\n    // Run validation against a set of incoming attributes, returning `true`\n    // if all is well. If a specific `error` callback has been passed,\n    // call that instead of firing the general `\"error\"` event.\n    _performValidation : function(attrs, options) {\n      var error = this.validate(attrs);\n      if (error) {\n        if (options.error) {\n          options.error(this, error, options);\n        } else {\n          this.trigger('error', this, error, options);\n        }\n        return false;\n      }\n      return true;\n    }\n\n  });\n\n  // Backbone.Collection\n  // -------------------\n\n  // Provides a standard collection class for our sets of models, ordered\n  // or unordered. If a `comparator` is specified, the Collection will maintain\n  // its models in sort order, as they're added and removed.\n  Backbone.Collection = function(models, options) {\n    options || (options = {});\n    if (options.comparator) this.comparator = options.comparator;\n    _.bindAll(this, '_onModelEvent', '_removeReference');\n    this._reset();\n    if (models) this.reset(models, {silent: true});\n    this.initialize.apply(this, arguments);\n  };\n\n  // Define the Collection's inheritable methods.\n  _.extend(Backbone.Collection.prototype, Backbone.Events, {\n\n    // The default model for a collection is just a **Backbone.Model**.\n    // This should be overridden in most cases.\n    model : Backbone.Model,\n\n    // Initialize is an empty function by default. Override it with your own\n    // initialization logic.\n    initialize : function(){},\n\n    // The JSON representation of a Collection is an array of the\n    // models' attributes.\n    toJSON : function() {\n      return this.map(function(model){ return model.toJSON(); });\n    },\n\n    // Add a model, or list of models to the set. Pass **silent** to avoid\n    // firing the `added` event for every new model.\n    add : function(models, options) {\n      if (_.isArray(models)) {\n        for (var i = 0, l = models.length; i < l; i++) {\n          this._add(models[i], options);\n        }\n      } else {\n        this._add(models, options);\n      }\n      return this;\n    },\n\n    // Remove a model, or a list of models from the set. Pass silent to avoid\n    // firing the `removed` event for every model removed.\n    remove : function(models, options) {\n      if (_.isArray(models)) {\n        for (var i = 0, l = models.length; i < l; i++) {\n          this._remove(models[i], options);\n        }\n      } else {\n        this._remove(models, options);\n      }\n      return this;\n    },\n\n    // Get a model from the set by id.\n    get : function(id) {\n      if (id == null) return null;\n      return this._byId[id.id != null ? id.id : id];\n    },\n\n    // Get a model from the set by client id.\n    getByCid : function(cid) {\n      return cid && this._byCid[cid.cid || cid];\n    },\n\n    // Get the model at the given index.\n    at: function(index) {\n      return this.models[index];\n    },\n\n    // Force the collection to re-sort itself. You don't need to call this under normal\n    // circumstances, as the set will maintain sort order as each item is added.\n    sort : function(options) {\n      options || (options = {});\n      if (!this.comparator) throw new Error('Cannot sort a set without a comparator');\n      this.models = this.sortBy(this.comparator);\n      if (!options.silent) this.trigger('reset', this, options);\n      return this;\n    },\n\n    // Pluck an attribute from each model in the collection.\n    pluck : function(attr) {\n      return _.map(this.models, function(model){ return model.get(attr); });\n    },\n\n    // When you have more items than you want to add or remove individually,\n    // you can reset the entire set with a new list of models, without firing\n    // any `added` or `removed` events. Fires `reset` when finished.\n    reset : function(models, options) {\n      models  || (models = []);\n      options || (options = {});\n      this.each(this._removeReference);\n      this._reset();\n      this.add(models, {silent: true});\n      if (!options.silent) this.trigger('reset', this, options);\n      return this;\n    },\n\n    // Fetch the default set of models for this collection, resetting the\n    // collection when they arrive. If `add: true` is passed, appends the\n    // models to the collection instead of resetting.\n    fetch : function(options) {\n      options || (options = {});\n      var collection = this;\n      var success = options.success;\n      options.success = function(resp, status, xhr) {\n        collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);\n        if (success) success(collection, resp);\n      };\n      options.error = wrapError(options.error, collection, options);\n      return (this.sync || Backbone.sync).call(this, 'read', this, options);\n    },\n\n    // Create a new instance of a model in this collection. After the model\n    // has been created on the server, it will be added to the collection.\n    // Returns the model, or 'false' if validation on a new model fails.\n    create : function(model, options) {\n      var coll = this;\n      options || (options = {});\n      model = this._prepareModel(model, options);\n      if (!model) return false;\n      var success = options.success;\n      options.success = function(nextModel, resp, xhr) {\n        coll.add(nextModel, options);\n        if (success) success(nextModel, resp, xhr);\n      };\n      model.save(null, options);\n      return model;\n    },\n\n    // **parse** converts a response into a list of models to be added to the\n    // collection. The default implementation is just to pass it through.\n    parse : function(resp, xhr) {\n      return resp;\n    },\n\n    // Proxy to _'s chain. Can't be proxied the same way the rest of the\n    // underscore methods are proxied because it relies on the underscore\n    // constructor.\n    chain: function () {\n      return _(this.models).chain();\n    },\n\n    // Reset all internal state. Called when the collection is reset.\n    _reset : function(options) {\n      this.length = 0;\n      this.models = [];\n      this._byId  = {};\n      this._byCid = {};\n    },\n\n    // Prepare a model to be added to this collection\n    _prepareModel: function(model, options) {\n      if (!(model instanceof Backbone.Model)) {\n        var attrs = model;\n        model = new this.model(attrs, {collection: this});\n        if (model.validate && !model._performValidation(attrs, options)) model = false;\n      } else if (!model.collection) {\n        model.collection = this;\n      }\n      return model;\n    },\n\n    // Internal implementation of adding a single model to the set, updating\n    // hash indexes for `id` and `cid` lookups.\n    // Returns the model, or 'false' if validation on a new model fails.\n    _add : function(model, options) {\n      options || (options = {});\n      model = this._prepareModel(model, options);\n      if (!model) return false;\n      var already = this.getByCid(model);\n      if (already) throw new Error([\"Can't add the same model to a set twice\", already.id]);\n      this._byId[model.id] = model;\n      this._byCid[model.cid] = model;\n      var index = options.at != null ? options.at :\n                  this.comparator ? this.sortedIndex(model, this.comparator) :\n                  this.length;\n      this.models.splice(index, 0, model);\n      model.bind('all', this._onModelEvent);\n      this.length++;\n      if (!options.silent) model.trigger('add', model, this, options);\n      return model;\n    },\n\n    // Internal implementation of removing a single model from the set, updating\n    // hash indexes for `id` and `cid` lookups.\n    _remove : function(model, options) {\n      options || (options = {});\n      model = this.getByCid(model) || this.get(model);\n      if (!model) return null;\n      delete this._byId[model.id];\n      delete this._byCid[model.cid];\n      this.models.splice(this.indexOf(model), 1);\n      this.length--;\n      if (!options.silent) model.trigger('remove', model, this, options);\n      this._removeReference(model);\n      return model;\n    },\n\n    // Internal method to remove a model's ties to a collection.\n    _removeReference : function(model) {\n      if (this == model.collection) {\n        delete model.collection;\n      }\n      model.unbind('all', this._onModelEvent);\n    },\n\n    // Internal method called every time a model in the set fires an event.\n    // Sets need to update their indexes when models change ids. All other\n    // events simply proxy through. \"add\" and \"remove\" events that originate\n    // in other collections are ignored.\n    _onModelEvent : function(ev, model, collection, options) {\n      if ((ev == 'add' || ev == 'remove') && collection != this) return;\n      if (ev == 'destroy') {\n        this._remove(model, options);\n      }\n      if (model && ev === 'change:' + model.idAttribute) {\n        delete this._byId[model.previous(model.idAttribute)];\n        this._byId[model.id] = model;\n      }\n      this.trigger.apply(this, arguments);\n    }\n\n  });\n\n  // Underscore methods that we want to implement on the Collection.\n  var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect',\n    'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include',\n    'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size',\n    'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty', 'groupBy'];\n\n  // Mix in each Underscore method as a proxy to `Collection#models`.\n  _.each(methods, function(method) {\n    Backbone.Collection.prototype[method] = function() {\n      return _[method].apply(_, [this.models].concat(_.toArray(arguments)));\n    };\n  });\n\n  // Backbone.Router\n  // -------------------\n\n  // Routers map faux-URLs to actions, and fire events when routes are\n  // matched. Creating a new one sets its `routes` hash, if not set statically.\n  Backbone.Router = function(options) {\n    options || (options = {});\n    if (options.routes) this.routes = options.routes;\n    this._bindRoutes();\n    this.initialize.apply(this, arguments);\n  };\n\n  // Cached regular expressions for matching named param parts and splatted\n  // parts of route strings.\n  var namedParam    = /:([\\w\\d]+)/g;\n  var splatParam    = /\\*([\\w\\d]+)/g;\n  var escapeRegExp  = /[-[\\]{}()+?.,\\\\^$|#\\s]/g;\n\n  // Set up all inheritable **Backbone.Router** properties and methods.\n  _.extend(Backbone.Router.prototype, Backbone.Events, {\n\n    // Initialize is an empty function by default. Override it with your own\n    // initialization logic.\n    initialize : function(){},\n\n    // Manually bind a single named route to a callback. For example:\n    //\n    //     this.route('search/:query/p:num', 'search', function(query, num) {\n    //       ...\n    //     });\n    //\n    route : function(route, name, callback) {\n      Backbone.history || (Backbone.history = new Backbone.History);\n      if (!_.isRegExp(route)) route = this._routeToRegExp(route);\n      Backbone.history.route(route, _.bind(function(fragment) {\n        var args = this._extractParameters(route, fragment);\n        callback.apply(this, args);\n        this.trigger.apply(this, ['route:' + name].concat(args));\n      }, this));\n    },\n\n    // Simple proxy to `Backbone.history` to save a fragment into the history.\n    navigate : function(fragment, triggerRoute) {\n      Backbone.history.navigate(fragment, triggerRoute);\n    },\n\n    // Bind all defined routes to `Backbone.history`. We have to reverse the\n    // order of the routes here to support behavior where the most general\n    // routes can be defined at the bottom of the route map.\n    _bindRoutes : function() {\n      if (!this.routes) return;\n      var routes = [];\n      for (var route in this.routes) {\n        routes.unshift([route, this.routes[route]]);\n      }\n      for (var i = 0, l = routes.length; i < l; i++) {\n        this.route(routes[i][0], routes[i][1], this[routes[i][1]]);\n      }\n    },\n\n    // Convert a route string into a regular expression, suitable for matching\n    // against the current location hash.\n    _routeToRegExp : function(route) {\n      route = route.replace(escapeRegExp, \"\\\\$&\")\n                   .replace(namedParam, \"([^\\/]*)\")\n                   .replace(splatParam, \"(.*?)\");\n      return new RegExp('^' + route + '$');\n    },\n\n    // Given a route, and a URL fragment that it matches, return the array of\n    // extracted parameters.\n    _extractParameters : function(route, fragment) {\n      return route.exec(fragment).slice(1);\n    }\n\n  });\n\n  // Backbone.History\n  // ----------------\n\n  // Handles cross-browser history management, based on URL fragments. If the\n  // browser does not support `onhashchange`, falls back to polling.\n  Backbone.History = function() {\n    this.handlers = [];\n    _.bindAll(this, 'checkUrl');\n  };\n\n  // Cached regex for cleaning hashes.\n  var hashStrip = /^#*/;\n\n  // Cached regex for detecting MSIE.\n  var isExplorer = /msie [\\w.]+/;\n\n  // Has the history handling already been started?\n  var historyStarted = false;\n\n  // Set up all inheritable **Backbone.History** properties and methods.\n  _.extend(Backbone.History.prototype, {\n\n    // The default interval to poll for hash changes, if necessary, is\n    // twenty times a second.\n    interval: 50,\n\n    // Get the cross-browser normalized URL fragment, either from the URL,\n    // the hash, or the override.\n    getFragment : function(fragment, forcePushState) {\n      if (fragment == null) {\n        if (this._hasPushState || forcePushState) {\n          fragment = window.location.pathname;\n          var search = window.location.search;\n          if (search) fragment += search;\n          if (fragment.indexOf(this.options.root) == 0) fragment = fragment.substr(this.options.root.length);\n        } else {\n          fragment = window.location.hash;\n        }\n      }\n      return decodeURIComponent(fragment.replace(hashStrip, ''));\n    },\n\n    // Start the hash change handling, returning `true` if the current URL matches\n    // an existing route, and `false` otherwise.\n    start : function(options) {\n\n      // Figure out the initial configuration. Do we need an iframe?\n      // Is pushState desired ... is it available?\n      if (historyStarted) throw new Error(\"Backbone.history has already been started\");\n      this.options          = _.extend({}, {root: '/'}, this.options, options);\n      this._wantsPushState  = !!this.options.pushState;\n      this._hasPushState    = !!(this.options.pushState && window.history && window.history.pushState);\n      var fragment          = this.getFragment();\n      var docMode           = document.documentMode;\n      var oldIE             = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));\n      if (oldIE) {\n        this.iframe = $('<iframe src=\"javascript:0\" tabindex=\"-1\" />').hide().appendTo('body')[0].contentWindow;\n        this.navigate(fragment);\n      }\n\n      // Depending on whether we're using pushState or hashes, and whether\n      // 'onhashchange' is supported, determine how we check the URL state.\n      if (this._hasPushState) {\n        $(window).bind('popstate', this.checkUrl);\n      } else if ('onhashchange' in window && !oldIE) {\n        $(window).bind('hashchange', this.checkUrl);\n      } else {\n        setInterval(this.checkUrl, this.interval);\n      }\n\n      // Determine if we need to change the base url, for a pushState link\n      // opened by a non-pushState browser.\n      this.fragment = fragment;\n      historyStarted = true;\n      var loc = window.location;\n      var atRoot  = loc.pathname == this.options.root;\n      if (this._wantsPushState && !this._hasPushState && !atRoot) {\n        this.fragment = this.getFragment(null, true);\n        window.location.replace(this.options.root + '#' + this.fragment);\n        // Return immediately as browser will do redirect to new url\n        return true;\n      } else if (this._wantsPushState && this._hasPushState && atRoot && loc.hash) {\n        this.fragment = loc.hash.replace(hashStrip, '');\n        window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);\n      }\n\n      if (!this.options.silent) {\n        return this.loadUrl();\n      }\n    },\n\n    // Add a route to be tested when the fragment changes. Routes added later may\n    // override previous routes.\n    route : function(route, callback) {\n      this.handlers.unshift({route : route, callback : callback});\n    },\n\n    // Checks the current URL to see if it has changed, and if it has,\n    // calls `loadUrl`, normalizing across the hidden iframe.\n    checkUrl : function(e) {\n      var current = this.getFragment();\n      if (current == this.fragment && this.iframe) current = this.getFragment(this.iframe.location.hash);\n      if (current == this.fragment || current == decodeURIComponent(this.fragment)) return false;\n      if (this.iframe) this.navigate(current);\n      this.loadUrl() || this.loadUrl(window.location.hash);\n    },\n\n    // Attempt to load the current URL fragment. If a route succeeds with a\n    // match, returns `true`. If no defined routes matches the fragment,\n    // returns `false`.\n    loadUrl : function(fragmentOverride) {\n      var fragment = this.fragment = this.getFragment(fragmentOverride);\n      var matched = _.any(this.handlers, function(handler) {\n        if (handler.route.test(fragment)) {\n          handler.callback(fragment);\n          return true;\n        }\n      });\n      return matched;\n    },\n\n    // Save a fragment into the hash history. You are responsible for properly\n    // URL-encoding the fragment in advance. This does not trigger\n    // a `hashchange` event.\n    navigate : function(fragment, triggerRoute) {\n      var frag = (fragment || '').replace(hashStrip, '');\n      if (this.fragment == frag || this.fragment == decodeURIComponent(frag)) return;\n      if (this._hasPushState) {\n        var loc = window.location;\n        if (frag.indexOf(this.options.root) != 0) frag = this.options.root + frag;\n        this.fragment = frag;\n        window.history.pushState({}, document.title, loc.protocol + '//' + loc.host + frag);\n      } else {\n        window.location.hash = this.fragment = frag;\n        if (this.iframe && (frag != this.getFragment(this.iframe.location.hash))) {\n          this.iframe.document.open().close();\n          this.iframe.location.hash = frag;\n        }\n      }\n      if (triggerRoute) this.loadUrl(fragment);\n    }\n\n  });\n\n  // Backbone.View\n  // -------------\n\n  // Creating a Backbone.View creates its initial element outside of the DOM,\n  // if an existing element is not provided...\n  Backbone.View = function(options) {\n    this.cid = _.uniqueId('view');\n    this._configure(options || {});\n    this._ensureElement();\n    this.delegateEvents();\n    this.initialize.apply(this, arguments);\n  };\n\n  // Element lookup, scoped to DOM elements within the current view.\n  // This should be preferred to global lookups, if you're dealing with\n  // a specific view.\n  var selectorDelegate = function(selector) {\n    return $(selector, this.el);\n  };\n\n  // Cached regex to split keys for `delegate`.\n  var eventSplitter = /^(\\S+)\\s*(.*)$/;\n\n  // List of view options to be merged as properties.\n  var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];\n\n  // Set up all inheritable **Backbone.View** properties and methods.\n  _.extend(Backbone.View.prototype, Backbone.Events, {\n\n    // The default `tagName` of a View's element is `\"div\"`.\n    tagName : 'div',\n\n    // Attach the `selectorDelegate` function as the `$` property.\n    $       : selectorDelegate,\n\n    // Initialize is an empty function by default. Override it with your own\n    // initialization logic.\n    initialize : function(){},\n\n    // **render** is the core function that your view should override, in order\n    // to populate its element (`this.el`), with the appropriate HTML. The\n    // convention is for **render** to always return `this`.\n    render : function() {\n      return this;\n    },\n\n    // Remove this view from the DOM. Note that the view isn't present in the\n    // DOM by default, so calling this method may be a no-op.\n    remove : function() {\n      $(this.el).remove();\n      return this;\n    },\n\n    // For small amounts of DOM Elements, where a full-blown template isn't\n    // needed, use **make** to manufacture elements, one at a time.\n    //\n    //     var el = this.make('li', {'class': 'row'}, this.model.escape('title'));\n    //\n    make : function(tagName, attributes, content) {\n      var el = document.createElement(tagName);\n      if (attributes) $(el).attr(attributes);\n      if (content) $(el).html(content);\n      return el;\n    },\n\n    // Set callbacks, where `this.callbacks` is a hash of\n    //\n    // *{\"event selector\": \"callback\"}*\n    //\n    //     {\n    //       'mousedown .title':  'edit',\n    //       'click .button':     'save'\n    //     }\n    //\n    // pairs. Callbacks will be bound to the view, with `this` set properly.\n    // Uses event delegation for efficiency.\n    // Omitting the selector binds the event to `this.el`.\n    // This only works for delegate-able events: not `focus`, `blur`, and\n    // not `change`, `submit`, and `reset` in Internet Explorer.\n    delegateEvents : function(events) {\n      if (!(events || (events = this.events))) return;\n      if (_.isFunction(events)) events = events.call(this);\n      $(this.el).unbind('.delegateEvents' + this.cid);\n      for (var key in events) {\n        var method = this[events[key]];\n        if (!method) throw new Error('Event \"' + events[key] + '\" does not exist');\n        var match = key.match(eventSplitter);\n        var eventName = match[1], selector = match[2];\n        method = _.bind(method, this);\n        eventName += '.delegateEvents' + this.cid;\n        if (selector === '') {\n          $(this.el).bind(eventName, method);\n        } else {\n          $(this.el).delegate(selector, eventName, method);\n        }\n      }\n    },\n\n    // Performs the initial configuration of a View with a set of options.\n    // Keys with special meaning *(model, collection, id, className)*, are\n    // attached directly to the view.\n    _configure : function(options) {\n      if (this.options) options = _.extend({}, this.options, options);\n      for (var i = 0, l = viewOptions.length; i < l; i++) {\n        var attr = viewOptions[i];\n        if (options[attr]) this[attr] = options[attr];\n      }\n      this.options = options;\n    },\n\n    // Ensure that the View has a DOM element to render into.\n    // If `this.el` is a string, pass it through `$()`, take the first\n    // matching element, and re-assign it to `el`. Otherwise, create\n    // an element from the `id`, `className` and `tagName` properties.\n    _ensureElement : function() {\n      if (!this.el) {\n        var attrs = this.attributes || {};\n        if (this.id) attrs.id = this.id;\n        if (this.className) attrs['class'] = this.className;\n        this.el = this.make(this.tagName, attrs);\n      } else if (_.isString(this.el)) {\n        this.el = $(this.el).get(0);\n      }\n    }\n\n  });\n\n  // The self-propagating extend function that Backbone classes use.\n  var extend = function (protoProps, classProps) {\n    var child = inherits(this, protoProps, classProps);\n    child.extend = this.extend;\n    return child;\n  };\n\n  // Set up inheritance for the model, collection, and view.\n  Backbone.Model.extend = Backbone.Collection.extend =\n    Backbone.Router.extend = Backbone.View.extend = extend;\n\n  // Map from CRUD to HTTP for our default `Backbone.sync` implementation.\n  var methodMap = {\n    'create': 'POST',\n    'update': 'PUT',\n    'delete': 'DELETE',\n    'read'  : 'GET'\n  };\n\n  // Backbone.sync\n  // -------------\n\n  // Override this function to change the manner in which Backbone persists\n  // models to the server. You will be passed the type of request, and the\n  // model in question. By default, uses makes a RESTful Ajax request\n  // to the model's `url()`. Some possible customizations could be:\n  //\n  // * Use `setTimeout` to batch rapid-fire updates into a single request.\n  // * Send up the models as XML instead of JSON.\n  // * Persist models via WebSockets instead of Ajax.\n  //\n  // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests\n  // as `POST`, with a `_method` parameter containing the true HTTP method,\n  // as well as all requests with the body as `application/x-www-form-urlencoded` instead of\n  // `application/json` with the model in a param named `model`.\n  // Useful when interfacing with server-side languages like **PHP** that make\n  // it difficult to read the body of `PUT` requests.\n  Backbone.sync = function(method, model, options) {\n    var type = methodMap[method];\n\n    // Default JSON-request options.\n    var params = _.extend({\n      type:         type,\n      dataType:     'json'\n    }, options);\n\n    // Ensure that we have a URL.\n    if (!params.url) {\n      params.url = getUrl(model) || urlError();\n    }\n\n    // Ensure that we have the appropriate request data.\n    if (!params.data && model && (method == 'create' || method == 'update')) {\n      params.contentType = 'application/json';\n      params.data = JSON.stringify(model.toJSON());\n    }\n\n    // For older servers, emulate JSON by encoding the request into an HTML-form.\n    if (Backbone.emulateJSON) {\n      params.contentType = 'application/x-www-form-urlencoded';\n      params.data        = params.data ? {model : params.data} : {};\n    }\n\n    // For older servers, emulate HTTP by mimicking the HTTP method with `_method`\n    // And an `X-HTTP-Method-Override` header.\n    if (Backbone.emulateHTTP) {\n      if (type === 'PUT' || type === 'DELETE') {\n        if (Backbone.emulateJSON) params.data._method = type;\n        params.type = 'POST';\n        params.beforeSend = function(xhr) {\n          xhr.setRequestHeader('X-HTTP-Method-Override', type);\n        };\n      }\n    }\n\n    // Don't process data on a non-GET request.\n    if (params.type !== 'GET' && !Backbone.emulateJSON) {\n      params.processData = false;\n    }\n\n    // Make the request.\n    return $.ajax(params);\n  };\n\n  // Helpers\n  // -------\n\n  // Shared empty constructor function to aid in prototype-chain creation.\n  var ctor = function(){};\n\n  // Helper function to correctly set up the prototype chain, for subclasses.\n  // Similar to `goog.inherits`, but uses a hash of prototype properties and\n  // class properties to be extended.\n  var inherits = function(parent, protoProps, staticProps) {\n    var child;\n\n    // The constructor function for the new subclass is either defined by you\n    // (the \"constructor\" property in your `extend` definition), or defaulted\n    // by us to simply call `super()`.\n    if (protoProps && protoProps.hasOwnProperty('constructor')) {\n      child = protoProps.constructor;\n    } else {\n      child = function(){ return parent.apply(this, arguments); };\n    }\n\n    // Inherit class (static) properties from parent.\n    _.extend(child, parent);\n\n    // Set the prototype chain to inherit from `parent`, without calling\n    // `parent`'s constructor function.\n    ctor.prototype = parent.prototype;\n    child.prototype = new ctor();\n\n    // Add prototype properties (instance properties) to the subclass,\n    // if supplied.\n    if (protoProps) _.extend(child.prototype, protoProps);\n\n    // Add static properties to the constructor function, if supplied.\n    if (staticProps) _.extend(child, staticProps);\n\n    // Correctly set child's `prototype.constructor`.\n    child.prototype.constructor = child;\n\n    // Set a convenience property in case the parent's prototype is needed later.\n    child.__super__ = parent.prototype;\n\n    return child;\n  };\n\n  // Helper function to get a URL from a Model or Collection as a property\n  // or as a function.\n  var getUrl = function(object) {\n    if (!(object && object.url)) return null;\n    return _.isFunction(object.url) ? object.url() : object.url;\n  };\n\n  // Throw an error when a URL is needed, and none is supplied.\n  var urlError = function() {\n    throw new Error('A \"url\" property or function must be specified');\n  };\n\n  // Wrap an optional error callback with a fallback error event.\n  var wrapError = function(onError, model, options) {\n    return function(resp) {\n      if (onError) {\n        onError(model, resp, options);\n      } else {\n        model.trigger('error', model, resp, options);\n      }\n    };\n  };\n\n  // Helper function to escape a string for HTML rendering.\n  var escapeHTML = function(string) {\n    return string.replace(/&(?!\\w+;|#\\d+;|#x[\\da-f]+;)/gi, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;').replace(/'/g, '&#x27;').replace(/\\//g,'&#x2F;');\n  };\n\n}).call(this);\n"
  },
  {
    "path": "beetsplug/web/static/beets.css",
    "content": "body {\n    font-family: Helvetica, Arial, sans-serif;\n}\n\n#header {\n    position: fixed;\n    left: 0;\n    right: 0;\n    top: 0;\n    height: 36px;\n\n    color: white;\n\n    cursor: default;\n\n    /* shadowy border */\n    box-shadow: 0 0 20px #999;\n    -webkit-box-shadow: 0 0 20px #999;\n    -moz-box-shadow: 0 0 20px #999;\n\n    /* background gradient */\n    background: #0e0e0e;\n    background: -moz-linear-gradient(top, #6b6b6b 0%, #0e0e0e 100%);\n    background: -webkit-linear-gradient(top, #6b6b6b 0%,#0e0e0e 100%);\n}\n#header h1 {\n    font-size: 1.1em;\n    font-weight: bold;\n    color: white;\n    margin: 0.35em;\n    float: left;\n}\n\n#entities {\n    width: 17em;\n\n    position: fixed;\n    top: 36px;\n    left: 0;\n    bottom: 0;\n    margin: 0;\n\n    z-index: 1;\n    background: #dde4eb;\n\n    /* shadowy border */\n    box-shadow: 0 0 20px #666;\n    -webkit-box-shadow: 0 0 20px #666;\n    -moz-box-shadow: 0 0 20px #666;\n}\n#queryForm {\n    display: block;\n    text-align: center;\n    margin: 0.25em 0;\n}\n#query {\n    width: 95%;\n    font-size: 1em;\n}\n#entities ul {\n    width: 17em;\n\n    position: fixed;\n    top: 36px;\n    left: 0;\n    bottom: 0;\n    margin: 2.2em 0 0 0;\n    padding: 0;\n\n    overflow-y: auto;\n    overflow-x: hidden;\n}\n#entities ul li {\n    list-style: none;\n    padding: 4px 8px;\n    margin: 0;\n    cursor: default;\n}\n#entities ul li.selected {\n    background: #7abcff;\n    background: -moz-linear-gradient(top, #7abcff 0%, #60abf8 44%, #4096ee 100%);\n    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#7abcff), color-stop(44%,#60abf8), color-stop(100%,#4096ee));\n    color: white;\n}\n#entities ul li .playing {\n    margin-left: 5px;\n    font-size: 0.9em;\n}\n\n\n#main-detail, #extra-detail {\n    position: fixed;\n    left: 17em;\n    margin: 1.0em 0 0 1.5em;\n}\n#main-detail {\n    top: 36px;\n    height: 98px;\n}\n#main-detail .artist, #main-detail .album, #main-detail .title {\n    display: block;\n}\n#main-detail .title {\n    font-size: 1.3em;\n    font-weight: bold;\n}\n#main-detail .albumtitle {\n    font-style: italic;\n}\n\n#extra-detail {\n    overflow-x: hidden;\n    overflow-y: auto;\n    top: 134px;\n    bottom: 0;\n    right: 0;\n}\n/*Fix for correctly displaying line breaks in lyrics*/\n#extra-detail .lyrics {\n    white-space: pre-wrap;\n}\n#extra-detail dl dt, #extra-detail dl dd {\n    list-style: none;\n    margin: 0;\n    padding: 0;\n}\n#extra-detail dl dt {\n    width: 10em;\n    float: left;\n    text-align: right;\n    font-weight: bold;\n    clear: both;\n}\n#extra-detail dl dd {\n    margin-left: 10.5em;\n}\n\n\n#player {\n    float: left;\n    width: 150px;\n    height: 36px;\n}\n#player .play, #player .pause, #player .disabled {\n    -webkit-appearance: none;\n    font-size: 1em;\n    font-family: Helvetica, Arial, sans-serif;\n    background: none;\n    border: none;\n    color: white;\n    padding: 5px;\n    margin: 0;\n    text-align: center;\n\n    width: 36px;\n    height: 36px;\n}\n#player .disabled {\n    color: #666;\n}\n"
  },
  {
    "path": "beetsplug/web/static/beets.js",
    "content": "// Format times as minutes and seconds.\nvar timeFormat = function(secs) {\n    if (secs == undefined || isNaN(secs)) {\n        return '0:00';\n    }\n    secs = Math.round(secs);\n    var mins = '' + Math.floor(secs / 60);\n    secs = '' + (secs % 60);\n    if (secs.length < 2) {\n        secs = '0' + secs;\n    }\n    return mins + ':' + secs;\n}\n\n// jQuery extension encapsulating event hookups for audio element controls.\n$.fn.player = function(debug) {\n    // Selected element should contain an HTML5 Audio element.\n    var audio = $('audio', this).get(0);\n\n    // Control elements that may be present, identified by class.\n    var playBtn = $('.play', this);\n    var pauseBtn = $('.pause', this);\n    var disabledInd = $('.disabled', this);\n    var timesEl = $('.times', this);\n    var curTimeEl = $('.currentTime', this);\n    var totalTimeEl  = $('.totalTime', this);\n    var sliderPlayedEl = $('.slider .played', this);\n    var sliderLoadedEl = $('.slider .loaded', this);\n\n    // Button events.\n    playBtn.click(function() {\n        audio.play();\n    });\n    pauseBtn.click(function(ev) {\n        audio.pause();\n    });\n\n    // Utilities.\n    var timePercent = function(cur, total) {\n        if (cur == undefined || isNaN(cur) ||\n                total == undefined || isNaN(total) || total == 0) {\n            return 0;\n        }\n        var ratio = cur / total;\n        if (ratio > 1.0) {\n            ratio = 1.0;\n        }\n        return (Math.round(ratio * 10000) / 100) + '%';\n    }\n\n    // Event helpers.\n    var dbg = function(msg) {\n        if (debug)\n            console.log(msg);\n    }\n    var showState = function() {\n        if (audio.duration == undefined || isNaN(audio.duration)) {\n            playBtn.hide();\n            pauseBtn.hide();\n            disabledInd.show();\n            timesEl.hide();\n        } else if (audio.paused) {\n            playBtn.show();\n            pauseBtn.hide();\n            disabledInd.hide();\n            timesEl.show();\n        } else {\n            playBtn.hide();\n            pauseBtn.show();\n            disabledInd.hide();\n            timesEl.show();\n        }\n    }\n    var showTimes = function() {\n        curTimeEl.text(timeFormat(audio.currentTime));\n        totalTimeEl.text(timeFormat(audio.duration));\n\n        sliderPlayedEl.css('width',\n                timePercent(audio.currentTime, audio.duration));\n\n        // last time buffered\n        var bufferEnd = 0;\n        for (var i = 0; i < audio.buffered.length; ++i) {\n            if (audio.buffered.end(i) > bufferEnd)\n                bufferEnd = audio.buffered.end(i);\n        }\n        sliderLoadedEl.css('width',\n                timePercent(bufferEnd, audio.duration));\n    }\n\n    // Initialize controls.\n    showState();\n    showTimes();\n\n    // Bind events.\n    $('audio', this).bind({\n        playing: function() {\n            dbg('playing');\n            showState();\n        },\n        pause: function() {\n            dbg('pause');\n            showState();\n        },\n        ended: function() {\n            dbg('ended');\n            showState();\n        },\n        progress: function() {\n            dbg('progress ' + audio.buffered);\n        },\n        timeupdate: function() {\n            dbg('timeupdate ' + audio.currentTime);\n            showTimes();\n        },\n        durationchange: function() {\n            dbg('durationchange ' + audio.duration);\n            showState();\n            showTimes();\n        },\n        loadeddata: function() {\n            dbg('loadeddata');\n        },\n        loadedmetadata: function() {\n            dbg('loadedmetadata');\n        }\n    });\n}\n\n// Simple selection disable for jQuery.\n// Cut-and-paste from:\n// https://stackoverflow.com/questions/2700000\n$.fn.disableSelection = function() {\n    $(this).attr('unselectable', 'on')\n           .css('-moz-user-select', 'none')\n           .each(function() {\n               this.onselectstart = function() { return false; };\n            });\n};\n\n$(function() {\n\n// Routes.\nvar BeetsRouter = Backbone.Router.extend({\n    routes: {\n        \"item/query/:query\": \"itemQuery\",\n    },\n    itemQuery: function(query) {\n        var queryURL = query.split(/\\s+/).map(encodeURIComponent).join('/');\n        $.getJSON('item/query/' + queryURL, function(data) {\n            var models = _.map(\n                data['results'],\n                function(d) { return new Item(d); }\n            );\n            var results = new Items(models);\n            app.showItems(results);\n        });\n    }\n});\nvar router = new BeetsRouter();\n\n// Model.\nvar Item = Backbone.Model.extend({\n    urlRoot: 'item'\n});\nvar Items = Backbone.Collection.extend({\n    model: Item\n});\n\n// Item views.\nvar ItemEntryView = Backbone.View.extend({\n    tagName: \"li\",\n    template: _.template($('#item-entry-template').html()),\n    events: {\n        'click': 'select',\n        'dblclick': 'play'\n    },\n    initialize: function() {\n        this.playing = false;\n    },\n    render: function() {\n        $(this.el).html(this.template(this.model.toJSON()));\n        this.setPlaying(this.playing);\n        return this;\n    },\n    select: function() {\n        app.selectItem(this);\n    },\n    play: function() {\n        app.playItem(this.model);\n    },\n    setPlaying: function(val) {\n        this.playing = val;\n        if (val)\n            this.$('.playing').show();\n        else\n            this.$('.playing').hide();\n    }\n});\n//Holds Title, Artist, Album etc.\nvar ItemMainDetailView = Backbone.View.extend({\n    tagName: \"div\",\n    template: _.template($('#item-main-detail-template').html()),\n    events: {\n        'click .play': 'play',\n    },\n    render: function() {\n        $(this.el).html(this.template(this.model.toJSON()));\n        return this;\n    },\n    play: function() {\n        app.playItem(this.model);\n    }\n});\n// Holds Track no., Format, MusicBrainz link, Lyrics, Comments etc.\nvar ItemExtraDetailView = Backbone.View.extend({\n    tagName: \"div\",\n    template: _.template($('#item-extra-detail-template').html()),\n    render: function() {\n        $(this.el).html(this.template(this.model.toJSON()));\n        return this;\n    }\n});\n// Main app view.\nvar AppView = Backbone.View.extend({\n    el: $('body'),\n    events: {\n        'submit #queryForm': 'querySubmit',\n    },\n    querySubmit: function(ev) {\n        ev.preventDefault();\n        router.navigate('item/query/' + encodeURIComponent($('#query').val()), true);\n    },\n    initialize: function() {\n        this.playingItem = null;\n        this.shownItems = null;\n\n        // Not sure why these events won't bind automatically.\n        this.$('audio').bind({\n            'play': _.bind(this.audioPlay, this),\n            'pause': _.bind(this.audioPause, this),\n            'ended': _.bind(this.audioEnded, this)\n        });\n\tif (\"mediaSession\" in navigator) {\n\t    navigator.mediaSession.setActionHandler(\"nexttrack\", () => {\n\t        this.playNext();\n\t    });\n\t}\n    },\n    showItems: function(items) {\n        this.shownItems = items;\n        $('#results').empty();\n        items.each(function(item) {\n            var view = new ItemEntryView({model: item});\n            item.entryView = view;\n            $('#results').append(view.render().el);\n        });\n    },\n    selectItem: function(view) {\n        // Mark row as selected.\n        $('#results li').removeClass(\"selected\");\n        $(view.el).addClass(\"selected\");\n\n        // Show main and extra detail.\n        var mainDetailView = new ItemMainDetailView({model: view.model});\n        $('#main-detail').empty().append(mainDetailView.render().el);\n\n        var extraDetailView = new ItemExtraDetailView({model: view.model});\n        $('#extra-detail').empty().append(extraDetailView.render().el);\n    },\n    playItem: function(item) {\n        var url = 'item/' + item.get('id') + '/file';\n        $('#player audio').attr('src', url);\n        $('#player audio').get(0).play().then(() => {\n            this.updateMediaSession(item);\n        });\n\n        if (this.playingItem != null) {\n            this.playingItem.entryView.setPlaying(false);\n        }\n        item.entryView.setPlaying(true);\n        this.playingItem = item;\n    },\n\n    updateMediaSession: function (item) {\n      if (\"mediaSession\" in navigator) {\n        const album_id = item.get(\"album_id\");\n        const album_art_url = \"album/\" + album_id + \"/art\";\n        navigator.mediaSession.metadata = new MediaMetadata({\n          title: item.get(\"title\"),\n          artist: item.get(\"artist\"),\n          album: item.get(\"album\"),\n          artwork: [\n            { src: album_art_url, sizes: \"96x96\" },\n            { src: album_art_url, sizes: \"128x128\" },\n            { src: album_art_url, sizes: \"192x192\" },\n            { src: album_art_url, sizes: \"256x256\" },\n            { src: album_art_url, sizes: \"384x384\" },\n            { src: album_art_url, sizes: \"512x512\" },\n          ],\n        });\n      }\n    },\n\n    audioPause: function() {\n        this.playingItem.entryView.setPlaying(false);\n    },\n    audioPlay: function() {\n        if (this.playingItem != null)\n            this.playingItem.entryView.setPlaying(true);\n    },\n    audioEnded: function() {\n        this.playingItem.entryView.setPlaying(false);\n        this.playNext();\n    },\n    playNext: function(){\n        // Try to play the next track.\n        var idx = this.shownItems.indexOf(this.playingItem);\n        if (idx == -1) {\n            // Not in current list.\n            return;\n        }\n        var nextIdx = idx + 1;\n        if (nextIdx >= this.shownItems.size()) {\n            // End of  list.\n            return;\n        }\n        this.playItem(this.shownItems.at(nextIdx));\n    }\n});\nvar app = new AppView();\n\n// App setup.\nBackbone.history.start({pushState: false});\n\n// Disable selection on UI elements.\n$('#entities ul').disableSelection();\n$('#header').disableSelection();\n\n// Audio player setup.\n$('#player').player();\n\n});\n"
  },
  {
    "path": "beetsplug/web/static/jquery.js",
    "content": "/*!\n * jQuery JavaScript Library v1.7.1\n * http://jquery.com/\n *\n * Copyright 2016, John Resig\n * Dual licensed under the MIT or GPL Version 2 licenses.\n * http://jquery.org/license\n *\n * Includes Sizzle.js\n * http://sizzlejs.com/\n * Copyright 2016, The Dojo Foundation\n * Released under the MIT, BSD, and GPL Licenses.\n *\n * Date: Mon Nov 21 21:11:03 2011 -0500\n */\n(function( window, undefined ) {\n\n// Use the correct document accordingly with window argument (sandbox)\nvar document = window.document,\n\tnavigator = window.navigator,\n\tlocation = window.location;\nvar jQuery = (function() {\n\n// Define a local copy of jQuery\nvar jQuery = function( selector, context ) {\n\t\t// The jQuery object is actually just the init constructor 'enhanced'\n\t\treturn new jQuery.fn.init( selector, context, rootjQuery );\n\t},\n\n\t// Map over jQuery in case of overwrite\n\t_jQuery = window.jQuery,\n\n\t// Map over the $ in case of overwrite\n\t_$ = window.$,\n\n\t// A central reference to the root jQuery(document)\n\trootjQuery,\n\n\t// A simple way to check for HTML strings or ID strings\n\t// Prioritize #id over <tag> to avoid XSS via location.hash (#9521)\n\tquickExpr = /^(?:[^#<]*(<[\\w\\W]+>)[^>]*$|#([\\w\\-]*)$)/,\n\n\t// Check if a string has a non-whitespace character in it\n\trnotwhite = /\\S/,\n\n\t// Used for trimming whitespace\n\ttrimLeft = /^\\s+/,\n\ttrimRight = /\\s+$/,\n\n\t// Match a standalone tag\n\trsingleTag = /^<(\\w+)\\s*\\/?>(?:<\\/\\1>)?$/,\n\n\t// JSON RegExp\n\trvalidchars = /^[\\],:{}\\s]*$/,\n\trvalidescape = /\\\\(?:[\"\\\\\\/bfnrt]|u[0-9a-fA-F]{4})/g,\n\trvalidtokens = /\"[^\"\\\\\\n\\r]*\"|true|false|null|-?\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d+)?/g,\n\trvalidbraces = /(?:^|:|,)(?:\\s*\\[)+/g,\n\n\t// Useragent RegExp\n\trwebkit = /(webkit)[ \\/]([\\w.]+)/,\n\tropera = /(opera)(?:.*version)?[ \\/]([\\w.]+)/,\n\trmsie = /(msie) ([\\w.]+)/,\n\trmozilla = /(mozilla)(?:.*? rv:([\\w.]+))?/,\n\n\t// Matches dashed string for camelizing\n\trdashAlpha = /-([a-z]|[0-9])/ig,\n\trmsPrefix = /^-ms-/,\n\n\t// Used by jQuery.camelCase as callback to replace()\n\tfcamelCase = function( all, letter ) {\n\t\treturn ( letter + \"\" ).toUpperCase();\n\t},\n\n\t// Keep a UserAgent string for use with jQuery.browser\n\tuserAgent = navigator.userAgent,\n\n\t// For matching the engine and version of the browser\n\tbrowserMatch,\n\n\t// The deferred used on DOM ready\n\treadyList,\n\n\t// The ready event handler\n\tDOMContentLoaded,\n\n\t// Save a reference to some core methods\n\ttoString = Object.prototype.toString,\n\thasOwn = Object.prototype.hasOwnProperty,\n\tpush = Array.prototype.push,\n\tslice = Array.prototype.slice,\n\ttrim = String.prototype.trim,\n\tindexOf = Array.prototype.indexOf,\n\n\t// [[Class]] -> type pairs\n\tclass2type = {};\n\njQuery.fn = jQuery.prototype = {\n\tconstructor: jQuery,\n\tinit: function( selector, context, rootjQuery ) {\n\t\tvar match, elem, ret, doc;\n\n\t\t// Handle $(\"\"), $(null), or $(undefined)\n\t\tif ( !selector ) {\n\t\t\treturn this;\n\t\t}\n\n\t\t// Handle $(DOMElement)\n\t\tif ( selector.nodeType ) {\n\t\t\tthis.context = this[0] = selector;\n\t\t\tthis.length = 1;\n\t\t\treturn this;\n\t\t}\n\n\t\t// The body element only exists once, optimize finding it\n\t\tif ( selector === \"body\" && !context && document.body ) {\n\t\t\tthis.context = document;\n\t\t\tthis[0] = document.body;\n\t\t\tthis.selector = selector;\n\t\t\tthis.length = 1;\n\t\t\treturn this;\n\t\t}\n\n\t\t// Handle HTML strings\n\t\tif ( typeof selector === \"string\" ) {\n\t\t\t// Are we dealing with HTML string or an ID?\n\t\t\tif ( selector.charAt(0) === \"<\" && selector.charAt( selector.length - 1 ) === \">\" && selector.length >= 3 ) {\n\t\t\t\t// Assume that strings that start and end with <> are HTML and skip the regex check\n\t\t\t\tmatch = [ null, selector, null ];\n\n\t\t\t} else {\n\t\t\t\tmatch = quickExpr.exec( selector );\n\t\t\t}\n\n\t\t\t// Verify a match, and that no context was specified for #id\n\t\t\tif ( match && (match[1] || !context) ) {\n\n\t\t\t\t// HANDLE: $(html) -> $(array)\n\t\t\t\tif ( match[1] ) {\n\t\t\t\t\tcontext = context instanceof jQuery ? context[0] : context;\n\t\t\t\t\tdoc = ( context ? context.ownerDocument || context : document );\n\n\t\t\t\t\t// If a single string is passed in and it's a single tag\n\t\t\t\t\t// just do a createElement and skip the rest\n\t\t\t\t\tret = rsingleTag.exec( selector );\n\n\t\t\t\t\tif ( ret ) {\n\t\t\t\t\t\tif ( jQuery.isPlainObject( context ) ) {\n\t\t\t\t\t\t\tselector = [ document.createElement( ret[1] ) ];\n\t\t\t\t\t\t\tjQuery.fn.attr.call( selector, context, true );\n\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tselector = [ doc.createElement( ret[1] ) ];\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} else {\n\t\t\t\t\t\tret = jQuery.buildFragment( [ match[1] ], [ doc ] );\n\t\t\t\t\t\tselector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes;\n\t\t\t\t\t}\n\n\t\t\t\t\treturn jQuery.merge( this, selector );\n\n\t\t\t\t// HANDLE: $(\"#id\")\n\t\t\t\t} else {\n\t\t\t\t\telem = document.getElementById( match[2] );\n\n\t\t\t\t\t// Check parentNode to catch when Blackberry 4.6 returns\n\t\t\t\t\t// nodes that are no longer in the document #6963\n\t\t\t\t\tif ( elem && elem.parentNode ) {\n\t\t\t\t\t\t// Handle the case where IE and Opera return items\n\t\t\t\t\t\t// by name instead of ID\n\t\t\t\t\t\tif ( elem.id !== match[2] ) {\n\t\t\t\t\t\t\treturn rootjQuery.find( selector );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Otherwise, we inject the element directly into the jQuery object\n\t\t\t\t\t\tthis.length = 1;\n\t\t\t\t\t\tthis[0] = elem;\n\t\t\t\t\t}\n\n\t\t\t\t\tthis.context = document;\n\t\t\t\t\tthis.selector = selector;\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\n\t\t\t// HANDLE: $(expr, $(...))\n\t\t\t} else if ( !context || context.jquery ) {\n\t\t\t\treturn ( context || rootjQuery ).find( selector );\n\n\t\t\t// HANDLE: $(expr, context)\n\t\t\t// (which is just equivalent to: $(context).find(expr)\n\t\t\t} else {\n\t\t\t\treturn this.constructor( context ).find( selector );\n\t\t\t}\n\n\t\t// HANDLE: $(function)\n\t\t// Shortcut for document ready\n\t\t} else if ( jQuery.isFunction( selector ) ) {\n\t\t\treturn rootjQuery.ready( selector );\n\t\t}\n\n\t\tif ( selector.selector !== undefined ) {\n\t\t\tthis.selector = selector.selector;\n\t\t\tthis.context = selector.context;\n\t\t}\n\n\t\treturn jQuery.makeArray( selector, this );\n\t},\n\n\t// Start with an empty selector\n\tselector: \"\",\n\n\t// The current version of jQuery being used\n\tjquery: \"1.7.1\",\n\n\t// The default length of a jQuery object is 0\n\tlength: 0,\n\n\t// The number of elements contained in the matched element set\n\tsize: function() {\n\t\treturn this.length;\n\t},\n\n\ttoArray: function() {\n\t\treturn slice.call( this, 0 );\n\t},\n\n\t// Get the Nth element in the matched element set OR\n\t// Get the whole matched element set as a clean array\n\tget: function( num ) {\n\t\treturn num == null ?\n\n\t\t\t// Return a 'clean' array\n\t\t\tthis.toArray() :\n\n\t\t\t// Return just the object\n\t\t\t( num < 0 ? this[ this.length + num ] : this[ num ] );\n\t},\n\n\t// Take an array of elements and push it onto the stack\n\t// (returning the new matched element set)\n\tpushStack: function( elems, name, selector ) {\n\t\t// Build a new jQuery matched element set\n\t\tvar ret = this.constructor();\n\n\t\tif ( jQuery.isArray( elems ) ) {\n\t\t\tpush.apply( ret, elems );\n\n\t\t} else {\n\t\t\tjQuery.merge( ret, elems );\n\t\t}\n\n\t\t// Add the old object onto the stack (as a reference)\n\t\tret.prevObject = this;\n\n\t\tret.context = this.context;\n\n\t\tif ( name === \"find\" ) {\n\t\t\tret.selector = this.selector + ( this.selector ? \" \" : \"\" ) + selector;\n\t\t} else if ( name ) {\n\t\t\tret.selector = this.selector + \".\" + name + \"(\" + selector + \")\";\n\t\t}\n\n\t\t// Return the newly-formed element set\n\t\treturn ret;\n\t},\n\n\t// Execute a callback for every element in the matched set.\n\t// (You can seed the arguments with an array of args, but this is\n\t// only used internally.)\n\teach: function( callback, args ) {\n\t\treturn jQuery.each( this, callback, args );\n\t},\n\n\tready: function( fn ) {\n\t\t// Attach the listeners\n\t\tjQuery.bindReady();\n\n\t\t// Add the callback\n\t\treadyList.add( fn );\n\n\t\treturn this;\n\t},\n\n\teq: function( i ) {\n\t\ti = +i;\n\t\treturn i === -1 ?\n\t\t\tthis.slice( i ) :\n\t\t\tthis.slice( i, i + 1 );\n\t},\n\n\tfirst: function() {\n\t\treturn this.eq( 0 );\n\t},\n\n\tlast: function() {\n\t\treturn this.eq( -1 );\n\t},\n\n\tslice: function() {\n\t\treturn this.pushStack( slice.apply( this, arguments ),\n\t\t\t\"slice\", slice.call(arguments).join(\",\") );\n\t},\n\n\tmap: function( callback ) {\n\t\treturn this.pushStack( jQuery.map(this, function( elem, i ) {\n\t\t\treturn callback.call( elem, i, elem );\n\t\t}));\n\t},\n\n\tend: function() {\n\t\treturn this.prevObject || this.constructor(null);\n\t},\n\n\t// For internal use only.\n\t// Behaves like an Array's method, not like a jQuery method.\n\tpush: push,\n\tsort: [].sort,\n\tsplice: [].splice\n};\n\n// Give the init function the jQuery prototype for later instantiation\njQuery.fn.init.prototype = jQuery.fn;\n\njQuery.extend = jQuery.fn.extend = function() {\n\tvar options, name, src, copy, copyIsArray, clone,\n\t\ttarget = arguments[0] || {},\n\t\ti = 1,\n\t\tlength = arguments.length,\n\t\tdeep = false;\n\n\t// Handle a deep copy situation\n\tif ( typeof target === \"boolean\" ) {\n\t\tdeep = target;\n\t\ttarget = arguments[1] || {};\n\t\t// skip the boolean and the target\n\t\ti = 2;\n\t}\n\n\t// Handle case when target is a string or something (possible in deep copy)\n\tif ( typeof target !== \"object\" && !jQuery.isFunction(target) ) {\n\t\ttarget = {};\n\t}\n\n\t// extend jQuery itself if only one argument is passed\n\tif ( length === i ) {\n\t\ttarget = this;\n\t\t--i;\n\t}\n\n\tfor ( ; i < length; i++ ) {\n\t\t// Only deal with non-null/undefined values\n\t\tif ( (options = arguments[ i ]) != null ) {\n\t\t\t// Extend the base object\n\t\t\tfor ( name in options ) {\n\t\t\t\tsrc = target[ name ];\n\t\t\t\tcopy = options[ name ];\n\n\t\t\t\t// Prevent never-ending loop\n\t\t\t\tif ( target === copy ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Recurse if we're merging plain objects or arrays\n\t\t\t\tif ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {\n\t\t\t\t\tif ( copyIsArray ) {\n\t\t\t\t\t\tcopyIsArray = false;\n\t\t\t\t\t\tclone = src && jQuery.isArray(src) ? src : [];\n\n\t\t\t\t\t} else {\n\t\t\t\t\t\tclone = src && jQuery.isPlainObject(src) ? src : {};\n\t\t\t\t\t}\n\n\t\t\t\t\t// Never move original objects, clone them\n\t\t\t\t\ttarget[ name ] = jQuery.extend( deep, clone, copy );\n\n\t\t\t\t// Don't bring in undefined values\n\t\t\t\t} else if ( copy !== undefined ) {\n\t\t\t\t\ttarget[ name ] = copy;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Return the modified object\n\treturn target;\n};\n\njQuery.extend({\n\tnoConflict: function( deep ) {\n\t\tif ( window.$ === jQuery ) {\n\t\t\twindow.$ = _$;\n\t\t}\n\n\t\tif ( deep && window.jQuery === jQuery ) {\n\t\t\twindow.jQuery = _jQuery;\n\t\t}\n\n\t\treturn jQuery;\n\t},\n\n\t// Is the DOM ready to be used? Set to true once it occurs.\n\tisReady: false,\n\n\t// A counter to track how many items to wait for before\n\t// the ready event fires. See #6781\n\treadyWait: 1,\n\n\t// Hold (or release) the ready event\n\tholdReady: function( hold ) {\n\t\tif ( hold ) {\n\t\t\tjQuery.readyWait++;\n\t\t} else {\n\t\t\tjQuery.ready( true );\n\t\t}\n\t},\n\n\t// Handle when the DOM is ready\n\tready: function( wait ) {\n\t\t// Either a released hold or an DOMready/load event and not yet ready\n\t\tif ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) {\n\t\t\t// Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).\n\t\t\tif ( !document.body ) {\n\t\t\t\treturn setTimeout( jQuery.ready, 1 );\n\t\t\t}\n\n\t\t\t// Remember that the DOM is ready\n\t\t\tjQuery.isReady = true;\n\n\t\t\t// If a normal DOM Ready event fired, decrement, and wait if need be\n\t\t\tif ( wait !== true && --jQuery.readyWait > 0 ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// If there are functions bound, to execute\n\t\t\treadyList.fireWith( document, [ jQuery ] );\n\n\t\t\t// Trigger any bound ready events\n\t\t\tif ( jQuery.fn.trigger ) {\n\t\t\t\tjQuery( document ).trigger( \"ready\" ).off( \"ready\" );\n\t\t\t}\n\t\t}\n\t},\n\n\tbindReady: function() {\n\t\tif ( readyList ) {\n\t\t\treturn;\n\t\t}\n\n\t\treadyList = jQuery.Callbacks( \"once memory\" );\n\n\t\t// Catch cases where $(document).ready() is called after the\n\t\t// browser event has already occurred.\n\t\tif ( document.readyState === \"complete\" ) {\n\t\t\t// Handle it asynchronously to allow scripts the opportunity to delay ready\n\t\t\treturn setTimeout( jQuery.ready, 1 );\n\t\t}\n\n\t\t// Mozilla, Opera and webkit nightlies currently support this event\n\t\tif ( document.addEventListener ) {\n\t\t\t// Use the handy event callback\n\t\t\tdocument.addEventListener( \"DOMContentLoaded\", DOMContentLoaded, false );\n\n\t\t\t// A fallback to window.onload, that will always work\n\t\t\twindow.addEventListener( \"load\", jQuery.ready, false );\n\n\t\t// If IE event model is used\n\t\t} else if ( document.attachEvent ) {\n\t\t\t// ensure firing before onload,\n\t\t\t// maybe late but safe also for iframes\n\t\t\tdocument.attachEvent( \"onreadystatechange\", DOMContentLoaded );\n\n\t\t\t// A fallback to window.onload, that will always work\n\t\t\twindow.attachEvent( \"onload\", jQuery.ready );\n\n\t\t\t// If IE and not a frame\n\t\t\t// continually check to see if the document is ready\n\t\t\tvar toplevel = false;\n\n\t\t\ttry {\n\t\t\t\ttoplevel = window.frameElement == null;\n\t\t\t} catch(e) {}\n\n\t\t\tif ( document.documentElement.doScroll && toplevel ) {\n\t\t\t\tdoScrollCheck();\n\t\t\t}\n\t\t}\n\t},\n\n\t// See test/unit/core.js for details concerning isFunction.\n\t// Since version 1.3, DOM methods and functions like alert\n\t// aren't supported. They return false on IE (#2968).\n\tisFunction: function( obj ) {\n\t\treturn jQuery.type(obj) === \"function\";\n\t},\n\n\tisArray: Array.isArray || function( obj ) {\n\t\treturn jQuery.type(obj) === \"array\";\n\t},\n\n\t// A crude way of determining if an object is a window\n\tisWindow: function( obj ) {\n\t\treturn obj && typeof obj === \"object\" && \"setInterval\" in obj;\n\t},\n\n\tisNumeric: function( obj ) {\n\t\treturn !isNaN( parseFloat(obj) ) && isFinite( obj );\n\t},\n\n\ttype: function( obj ) {\n\t\treturn obj == null ?\n\t\t\tString( obj ) :\n\t\t\tclass2type[ toString.call(obj) ] || \"object\";\n\t},\n\n\tisPlainObject: function( obj ) {\n\t\t// Must be an Object.\n\t\t// Because of IE, we also have to check the presence of the constructor property.\n\t\t// Make sure that DOM nodes and window objects don't pass through, as well\n\t\tif ( !obj || jQuery.type(obj) !== \"object\" || obj.nodeType || jQuery.isWindow( obj ) ) {\n\t\t\treturn false;\n\t\t}\n\n\t\ttry {\n\t\t\t// Not own constructor property must be Object\n\t\t\tif ( obj.constructor &&\n\t\t\t\t!hasOwn.call(obj, \"constructor\") &&\n\t\t\t\t!hasOwn.call(obj.constructor.prototype, \"isPrototypeOf\") ) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} catch ( e ) {\n\t\t\t// IE8,9 Will throw exceptions on certain host objects #9897\n\t\t\treturn false;\n\t\t}\n\n\t\t// Own properties are enumerated firstly, so to speed up,\n\t\t// if last one is own, then all properties are own.\n\n\t\tvar key;\n\t\tfor ( key in obj ) {}\n\n\t\treturn key === undefined || hasOwn.call( obj, key );\n\t},\n\n\tisEmptyObject: function( obj ) {\n\t\tfor ( var name in obj ) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t},\n\n\terror: function( msg ) {\n\t\tthrow new Error( msg );\n\t},\n\n\tparseJSON: function( data ) {\n\t\tif ( typeof data !== \"string\" || !data ) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Make sure leading/trailing whitespace is removed (IE can't handle it)\n\t\tdata = jQuery.trim( data );\n\n\t\t// Attempt to parse using the native JSON parser first\n\t\tif ( window.JSON && window.JSON.parse ) {\n\t\t\treturn window.JSON.parse( data );\n\t\t}\n\n\t\t// Make sure the incoming data is actual JSON\n\t\t// Logic borrowed from http://json.org/json2.js\n\t\tif ( rvalidchars.test( data.replace( rvalidescape, \"@\" )\n\t\t\t.replace( rvalidtokens, \"]\" )\n\t\t\t.replace( rvalidbraces, \"\")) ) {\n\n\t\t\treturn ( new Function( \"return \" + data ) )();\n\n\t\t}\n\t\tjQuery.error( \"Invalid JSON: \" + data );\n\t},\n\n\t// Cross-browser xml parsing\n\tparseXML: function( data ) {\n\t\tvar xml, tmp;\n\t\ttry {\n\t\t\tif ( window.DOMParser ) { // Standard\n\t\t\t\ttmp = new DOMParser();\n\t\t\t\txml = tmp.parseFromString( data , \"text/xml\" );\n\t\t\t} else { // IE\n\t\t\t\txml = new ActiveXObject( \"Microsoft.XMLDOM\" );\n\t\t\t\txml.async = \"false\";\n\t\t\t\txml.loadXML( data );\n\t\t\t}\n\t\t} catch( e ) {\n\t\t\txml = undefined;\n\t\t}\n\t\tif ( !xml || !xml.documentElement || xml.getElementsByTagName( \"parsererror\" ).length ) {\n\t\t\tjQuery.error( \"Invalid XML: \" + data );\n\t\t}\n\t\treturn xml;\n\t},\n\n\tnoop: function() {},\n\n\t// Evaluates a script in a global context\n\t// Workarounds based on findings by Jim Driscoll\n\t// http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context\n\tglobalEval: function( data ) {\n\t\tif ( data && rnotwhite.test( data ) ) {\n\t\t\t// We use execScript on Internet Explorer\n\t\t\t// We use an anonymous function so that context is window\n\t\t\t// rather than jQuery in Firefox\n\t\t\t( window.execScript || function( data ) {\n\t\t\t\twindow[ \"eval\" ].call( window, data );\n\t\t\t} )( data );\n\t\t}\n\t},\n\n\t// Convert dashed to camelCase; used by the css and data modules\n\t// Microsoft forgot to hump their vendor prefix (#9572)\n\tcamelCase: function( string ) {\n\t\treturn string.replace( rmsPrefix, \"ms-\" ).replace( rdashAlpha, fcamelCase );\n\t},\n\n\tnodeName: function( elem, name ) {\n\t\treturn elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase();\n\t},\n\n\t// args is for internal usage only\n\teach: function( object, callback, args ) {\n\t\tvar name, i = 0,\n\t\t\tlength = object.length,\n\t\t\tisObj = length === undefined || jQuery.isFunction( object );\n\n\t\tif ( args ) {\n\t\t\tif ( isObj ) {\n\t\t\t\tfor ( name in object ) {\n\t\t\t\t\tif ( callback.apply( object[ name ], args ) === false ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor ( ; i < length; ) {\n\t\t\t\t\tif ( callback.apply( object[ i++ ], args ) === false ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t// A special, fast, case for the most common use of each\n\t\t} else {\n\t\t\tif ( isObj ) {\n\t\t\t\tfor ( name in object ) {\n\t\t\t\t\tif ( callback.call( object[ name ], name, object[ name ] ) === false ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tfor ( ; i < length; ) {\n\t\t\t\t\tif ( callback.call( object[ i ], i, object[ i++ ] ) === false ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn object;\n\t},\n\n\t// Use native String.trim function wherever possible\n\ttrim: trim ?\n\t\tfunction( text ) {\n\t\t\treturn text == null ?\n\t\t\t\t\"\" :\n\t\t\t\ttrim.call( text );\n\t\t} :\n\n\t\t// Otherwise use our own trimming functionality\n\t\tfunction( text ) {\n\t\t\treturn text == null ?\n\t\t\t\t\"\" :\n\t\t\t\ttext.toString().replace( trimLeft, \"\" ).replace( trimRight, \"\" );\n\t\t},\n\n\t// results is for internal usage only\n\tmakeArray: function( array, results ) {\n\t\tvar ret = results || [];\n\n\t\tif ( array != null ) {\n\t\t\t// The window, strings (and functions) also have 'length'\n\t\t\t// Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930\n\t\t\tvar type = jQuery.type( array );\n\n\t\t\tif ( array.length == null || type === \"string\" || type === \"function\" || type === \"regexp\" || jQuery.isWindow( array ) ) {\n\t\t\t\tpush.call( ret, array );\n\t\t\t} else {\n\t\t\t\tjQuery.merge( ret, array );\n\t\t\t}\n\t\t}\n\n\t\treturn ret;\n\t},\n\n\tinArray: function( elem, array, i ) {\n\t\tvar len;\n\n\t\tif ( array ) {\n\t\t\tif ( indexOf ) {\n\t\t\t\treturn indexOf.call( array, elem, i );\n\t\t\t}\n\n\t\t\tlen = array.length;\n\t\t\ti = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;\n\n\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\t// Skip accessing in sparse arrays\n\t\t\t\tif ( i in array && array[ i ] === elem ) {\n\t\t\t\t\treturn i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn -1;\n\t},\n\n\tmerge: function( first, second ) {\n\t\tvar i = first.length,\n\t\t\tj = 0;\n\n\t\tif ( typeof second.length === \"number\" ) {\n\t\t\tfor ( var l = second.length; j < l; j++ ) {\n\t\t\t\tfirst[ i++ ] = second[ j ];\n\t\t\t}\n\n\t\t} else {\n\t\t\twhile ( second[j] !== undefined ) {\n\t\t\t\tfirst[ i++ ] = second[ j++ ];\n\t\t\t}\n\t\t}\n\n\t\tfirst.length = i;\n\n\t\treturn first;\n\t},\n\n\tgrep: function( elems, callback, inv ) {\n\t\tvar ret = [], retVal;\n\t\tinv = !!inv;\n\n\t\t// Go through the array, only saving the items\n\t\t// that pass the validator function\n\t\tfor ( var i = 0, length = elems.length; i < length; i++ ) {\n\t\t\tretVal = !!callback( elems[ i ], i );\n\t\t\tif ( inv !== retVal ) {\n\t\t\t\tret.push( elems[ i ] );\n\t\t\t}\n\t\t}\n\n\t\treturn ret;\n\t},\n\n\t// arg is for internal usage only\n\tmap: function( elems, callback, arg ) {\n\t\tvar value, key, ret = [],\n\t\t\ti = 0,\n\t\t\tlength = elems.length,\n\t\t\t// jquery objects are treated as arrays\n\t\t\tisArray = elems instanceof jQuery || length !== undefined && typeof length === \"number\" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ;\n\n\t\t// Go through the array, translating each of the items to their\n\t\tif ( isArray ) {\n\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\tvalue = callback( elems[ i ], i, arg );\n\n\t\t\t\tif ( value != null ) {\n\t\t\t\t\tret[ ret.length ] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t// Go through every key on the object,\n\t\t} else {\n\t\t\tfor ( key in elems ) {\n\t\t\t\tvalue = callback( elems[ key ], key, arg );\n\n\t\t\t\tif ( value != null ) {\n\t\t\t\t\tret[ ret.length ] = value;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Flatten any nested arrays\n\t\treturn ret.concat.apply( [], ret );\n\t},\n\n\t// A global GUID counter for objects\n\tguid: 1,\n\n\t// Bind a function to a context, optionally partially applying any\n\t// arguments.\n\tproxy: function( fn, context ) {\n\t\tif ( typeof context === \"string\" ) {\n\t\t\tvar tmp = fn[ context ];\n\t\t\tcontext = fn;\n\t\t\tfn = tmp;\n\t\t}\n\n\t\t// Quick check to determine if target is callable, in the spec\n\t\t// this throws a TypeError, but we will just return undefined.\n\t\tif ( !jQuery.isFunction( fn ) ) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\t// Simulated bind\n\t\tvar args = slice.call( arguments, 2 ),\n\t\t\tproxy = function() {\n\t\t\t\treturn fn.apply( context, args.concat( slice.call( arguments ) ) );\n\t\t\t};\n\n\t\t// Set the guid of unique handler to the same of original handler, so it can be removed\n\t\tproxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++;\n\n\t\treturn proxy;\n\t},\n\n\t// Mutifunctional method to get and set values to a collection\n\t// The value/s can optionally be executed if it's a function\n\taccess: function( elems, key, value, exec, fn, pass ) {\n\t\tvar length = elems.length;\n\n\t\t// Setting many attributes\n\t\tif ( typeof key === \"object\" ) {\n\t\t\tfor ( var k in key ) {\n\t\t\t\tjQuery.access( elems, k, key[k], exec, fn, value );\n\t\t\t}\n\t\t\treturn elems;\n\t\t}\n\n\t\t// Setting one attribute\n\t\tif ( value !== undefined ) {\n\t\t\t// Optionally, function values get executed if exec is true\n\t\t\texec = !pass && exec && jQuery.isFunction(value);\n\n\t\t\tfor ( var i = 0; i < length; i++ ) {\n\t\t\t\tfn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass );\n\t\t\t}\n\n\t\t\treturn elems;\n\t\t}\n\n\t\t// Getting an attribute\n\t\treturn length ? fn( elems[0], key ) : undefined;\n\t},\n\n\tnow: function() {\n\t\treturn ( new Date() ).getTime();\n\t},\n\n\t// Use of jQuery.browser is frowned upon.\n\t// More details: http://docs.jquery.com/Utilities/jQuery.browser\n\tuaMatch: function( ua ) {\n\t\tua = ua.toLowerCase();\n\n\t\tvar match = rwebkit.exec( ua ) ||\n\t\t\tropera.exec( ua ) ||\n\t\t\trmsie.exec( ua ) ||\n\t\t\tua.indexOf(\"compatible\") < 0 && rmozilla.exec( ua ) ||\n\t\t\t[];\n\n\t\treturn { browser: match[1] || \"\", version: match[2] || \"0\" };\n\t},\n\n\tsub: function() {\n\t\tfunction jQuerySub( selector, context ) {\n\t\t\treturn new jQuerySub.fn.init( selector, context );\n\t\t}\n\t\tjQuery.extend( true, jQuerySub, this );\n\t\tjQuerySub.superclass = this;\n\t\tjQuerySub.fn = jQuerySub.prototype = this();\n\t\tjQuerySub.fn.constructor = jQuerySub;\n\t\tjQuerySub.sub = this.sub;\n\t\tjQuerySub.fn.init = function init( selector, context ) {\n\t\t\tif ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) {\n\t\t\t\tcontext = jQuerySub( context );\n\t\t\t}\n\n\t\t\treturn jQuery.fn.init.call( this, selector, context, rootjQuerySub );\n\t\t};\n\t\tjQuerySub.fn.init.prototype = jQuerySub.fn;\n\t\tvar rootjQuerySub = jQuerySub(document);\n\t\treturn jQuerySub;\n\t},\n\n\tbrowser: {}\n});\n\n// Populate the class2type map\njQuery.each(\"Boolean Number String Function Array Date RegExp Object\".split(\" \"), function(i, name) {\n\tclass2type[ \"[object \" + name + \"]\" ] = name.toLowerCase();\n});\n\nbrowserMatch = jQuery.uaMatch( userAgent );\nif ( browserMatch.browser ) {\n\tjQuery.browser[ browserMatch.browser ] = true;\n\tjQuery.browser.version = browserMatch.version;\n}\n\n// Deprecated, use jQuery.browser.webkit instead\nif ( jQuery.browser.webkit ) {\n\tjQuery.browser.safari = true;\n}\n\n// IE doesn't match non-breaking spaces with \\s\nif ( rnotwhite.test( \"\\xA0\" ) ) {\n\ttrimLeft = /^[\\s\\xA0]+/;\n\ttrimRight = /[\\s\\xA0]+$/;\n}\n\n// All jQuery objects should point back to these\nrootjQuery = jQuery(document);\n\n// Cleanup functions for the document ready method\nif ( document.addEventListener ) {\n\tDOMContentLoaded = function() {\n\t\tdocument.removeEventListener( \"DOMContentLoaded\", DOMContentLoaded, false );\n\t\tjQuery.ready();\n\t};\n\n} else if ( document.attachEvent ) {\n\tDOMContentLoaded = function() {\n\t\t// Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).\n\t\tif ( document.readyState === \"complete\" ) {\n\t\t\tdocument.detachEvent( \"onreadystatechange\", DOMContentLoaded );\n\t\t\tjQuery.ready();\n\t\t}\n\t};\n}\n\n// The DOM ready check for Internet Explorer\nfunction doScrollCheck() {\n\tif ( jQuery.isReady ) {\n\t\treturn;\n\t}\n\n\ttry {\n\t\t// If IE is used, use the trick by Diego Perini\n\t\t// http://javascript.nwbox.com/IEContentLoaded/\n\t\tdocument.documentElement.doScroll(\"left\");\n\t} catch(e) {\n\t\tsetTimeout( doScrollCheck, 1 );\n\t\treturn;\n\t}\n\n\t// and execute any waiting functions\n\tjQuery.ready();\n}\n\nreturn jQuery;\n\n})();\n\n\n// String to Object flags format cache\nvar flagsCache = {};\n\n// Convert String-formatted flags into Object-formatted ones and store in cache\nfunction createFlags( flags ) {\n\tvar object = flagsCache[ flags ] = {},\n\t\ti, length;\n\tflags = flags.split( /\\s+/ );\n\tfor ( i = 0, length = flags.length; i < length; i++ ) {\n\t\tobject[ flags[i] ] = true;\n\t}\n\treturn object;\n}\n\n/*\n * Create a callback list using the following parameters:\n *\n *\tflags:\tan optional list of space-separated flags that will change how\n *\t\t\tthe callback list behaves\n *\n * By default a callback list will act like an event callback list and can be\n * \"fired\" multiple times.\n *\n * Possible flags:\n *\n *\tonce:\t\t\twill ensure the callback list can only be fired once (like a Deferred)\n *\n *\tmemory:\t\t\twill keep track of previous values and will call any callback added\n *\t\t\t\t\tafter the list has been fired right away with the latest \"memorized\"\n *\t\t\t\t\tvalues (like a Deferred)\n *\n *\tunique:\t\t\twill ensure a callback can only be added once (no duplicate in the list)\n *\n *\tstopOnFalse:\tinterrupt callings when a callback returns false\n *\n */\njQuery.Callbacks = function( flags ) {\n\n\t// Convert flags from String-formatted to Object-formatted\n\t// (we check in cache first)\n\tflags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {};\n\n\tvar // Actual callback list\n\t\tlist = [],\n\t\t// Stack of fire calls for repeatable lists\n\t\tstack = [],\n\t\t// Last fire value (for non-forgettable lists)\n\t\tmemory,\n\t\t// Flag to know if list is currently firing\n\t\tfiring,\n\t\t// First callback to fire (used internally by add and fireWith)\n\t\tfiringStart,\n\t\t// End of the loop when firing\n\t\tfiringLength,\n\t\t// Index of currently firing callback (modified by remove if needed)\n\t\tfiringIndex,\n\t\t// Add one or several callbacks to the list\n\t\tadd = function( args ) {\n\t\t\tvar i,\n\t\t\t\tlength,\n\t\t\t\telem,\n\t\t\t\ttype,\n\t\t\t\tactual;\n\t\t\tfor ( i = 0, length = args.length; i < length; i++ ) {\n\t\t\t\telem = args[ i ];\n\t\t\t\ttype = jQuery.type( elem );\n\t\t\t\tif ( type === \"array\" ) {\n\t\t\t\t\t// Inspect recursively\n\t\t\t\t\tadd( elem );\n\t\t\t\t} else if ( type === \"function\" ) {\n\t\t\t\t\t// Add if not in unique mode and callback is not in\n\t\t\t\t\tif ( !flags.unique || !self.has( elem ) ) {\n\t\t\t\t\t\tlist.push( elem );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t// Fire callbacks\n\t\tfire = function( context, args ) {\n\t\t\targs = args || [];\n\t\t\tmemory = !flags.memory || [ context, args ];\n\t\t\tfiring = true;\n\t\t\tfiringIndex = firingStart || 0;\n\t\t\tfiringStart = 0;\n\t\t\tfiringLength = list.length;\n\t\t\tfor ( ; list && firingIndex < firingLength; firingIndex++ ) {\n\t\t\t\tif ( list[ firingIndex ].apply( context, args ) === false && flags.stopOnFalse ) {\n\t\t\t\t\tmemory = true; // Mark as halted\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tfiring = false;\n\t\t\tif ( list ) {\n\t\t\t\tif ( !flags.once ) {\n\t\t\t\t\tif ( stack && stack.length ) {\n\t\t\t\t\t\tmemory = stack.shift();\n\t\t\t\t\t\tself.fireWith( memory[ 0 ], memory[ 1 ] );\n\t\t\t\t\t}\n\t\t\t\t} else if ( memory === true ) {\n\t\t\t\t\tself.disable();\n\t\t\t\t} else {\n\t\t\t\t\tlist = [];\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t// Actual Callbacks object\n\t\tself = {\n\t\t\t// Add a callback or a collection of callbacks to the list\n\t\t\tadd: function() {\n\t\t\t\tif ( list ) {\n\t\t\t\t\tvar length = list.length;\n\t\t\t\t\tadd( arguments );\n\t\t\t\t\t// Do we need to add the callbacks to the\n\t\t\t\t\t// current firing batch?\n\t\t\t\t\tif ( firing ) {\n\t\t\t\t\t\tfiringLength = list.length;\n\t\t\t\t\t// With memory, if we're not firing then\n\t\t\t\t\t// we should call right away, unless previous\n\t\t\t\t\t// firing was halted (stopOnFalse)\n\t\t\t\t\t} else if ( memory && memory !== true ) {\n\t\t\t\t\t\tfiringStart = length;\n\t\t\t\t\t\tfire( memory[ 0 ], memory[ 1 ] );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\t// Remove a callback from the list\n\t\t\tremove: function() {\n\t\t\t\tif ( list ) {\n\t\t\t\t\tvar args = arguments,\n\t\t\t\t\t\targIndex = 0,\n\t\t\t\t\t\targLength = args.length;\n\t\t\t\t\tfor ( ; argIndex < argLength ; argIndex++ ) {\n\t\t\t\t\t\tfor ( var i = 0; i < list.length; i++ ) {\n\t\t\t\t\t\t\tif ( args[ argIndex ] === list[ i ] ) {\n\t\t\t\t\t\t\t\t// Handle firingIndex and firingLength\n\t\t\t\t\t\t\t\tif ( firing ) {\n\t\t\t\t\t\t\t\t\tif ( i <= firingLength ) {\n\t\t\t\t\t\t\t\t\t\tfiringLength--;\n\t\t\t\t\t\t\t\t\t\tif ( i <= firingIndex ) {\n\t\t\t\t\t\t\t\t\t\t\tfiringIndex--;\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// Remove the element\n\t\t\t\t\t\t\t\tlist.splice( i--, 1 );\n\t\t\t\t\t\t\t\t// If we have some unicity property then\n\t\t\t\t\t\t\t\t// we only need to do this once\n\t\t\t\t\t\t\t\tif ( flags.unique ) {\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\t// Control if a given callback is in the list\n\t\t\thas: function( fn ) {\n\t\t\t\tif ( list ) {\n\t\t\t\t\tvar i = 0,\n\t\t\t\t\t\tlength = list.length;\n\t\t\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\t\t\tif ( fn === list[ i ] ) {\n\t\t\t\t\t\t\treturn true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t},\n\t\t\t// Remove all callbacks from the list\n\t\t\tempty: function() {\n\t\t\t\tlist = [];\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\t// Have the list do nothing anymore\n\t\t\tdisable: function() {\n\t\t\t\tlist = stack = memory = undefined;\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\t// Is it disabled?\n\t\t\tdisabled: function() {\n\t\t\t\treturn !list;\n\t\t\t},\n\t\t\t// Lock the list in its current state\n\t\t\tlock: function() {\n\t\t\t\tstack = undefined;\n\t\t\t\tif ( !memory || memory === true ) {\n\t\t\t\t\tself.disable();\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\t// Is it locked?\n\t\t\tlocked: function() {\n\t\t\t\treturn !stack;\n\t\t\t},\n\t\t\t// Call all callbacks with the given context and arguments\n\t\t\tfireWith: function( context, args ) {\n\t\t\t\tif ( stack ) {\n\t\t\t\t\tif ( firing ) {\n\t\t\t\t\t\tif ( !flags.once ) {\n\t\t\t\t\t\t\tstack.push( [ context, args ] );\n\t\t\t\t\t\t}\n\t\t\t\t\t} else if ( !( flags.once && memory ) ) {\n\t\t\t\t\t\tfire( context, args );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\t// Call all the callbacks with the given arguments\n\t\t\tfire: function() {\n\t\t\t\tself.fireWith( this, arguments );\n\t\t\t\treturn this;\n\t\t\t},\n\t\t\t// To know if the callbacks have already been called at least once\n\t\t\tfired: function() {\n\t\t\t\treturn !!memory;\n\t\t\t}\n\t\t};\n\n\treturn self;\n};\n\n\n\n\nvar // Static reference to slice\n\tsliceDeferred = [].slice;\n\njQuery.extend({\n\n\tDeferred: function( func ) {\n\t\tvar doneList = jQuery.Callbacks( \"once memory\" ),\n\t\t\tfailList = jQuery.Callbacks( \"once memory\" ),\n\t\t\tprogressList = jQuery.Callbacks( \"memory\" ),\n\t\t\tstate = \"pending\",\n\t\t\tlists = {\n\t\t\t\tresolve: doneList,\n\t\t\t\treject: failList,\n\t\t\t\tnotify: progressList\n\t\t\t},\n\t\t\tpromise = {\n\t\t\t\tdone: doneList.add,\n\t\t\t\tfail: failList.add,\n\t\t\t\tprogress: progressList.add,\n\n\t\t\t\tstate: function() {\n\t\t\t\t\treturn state;\n\t\t\t\t},\n\n\t\t\t\t// Deprecated\n\t\t\t\tisResolved: doneList.fired,\n\t\t\t\tisRejected: failList.fired,\n\n\t\t\t\tthen: function( doneCallbacks, failCallbacks, progressCallbacks ) {\n\t\t\t\t\tdeferred.done( doneCallbacks ).fail( failCallbacks ).progress( progressCallbacks );\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\t\t\t\talways: function() {\n\t\t\t\t\tdeferred.done.apply( deferred, arguments ).fail.apply( deferred, arguments );\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\t\t\t\tpipe: function( fnDone, fnFail, fnProgress ) {\n\t\t\t\t\treturn jQuery.Deferred(function( newDefer ) {\n\t\t\t\t\t\tjQuery.each( {\n\t\t\t\t\t\t\tdone: [ fnDone, \"resolve\" ],\n\t\t\t\t\t\t\tfail: [ fnFail, \"reject\" ],\n\t\t\t\t\t\t\tprogress: [ fnProgress, \"notify\" ]\n\t\t\t\t\t\t}, function( handler, data ) {\n\t\t\t\t\t\t\tvar fn = data[ 0 ],\n\t\t\t\t\t\t\t\taction = data[ 1 ],\n\t\t\t\t\t\t\t\treturned;\n\t\t\t\t\t\t\tif ( jQuery.isFunction( fn ) ) {\n\t\t\t\t\t\t\t\tdeferred[ handler ](function() {\n\t\t\t\t\t\t\t\t\treturned = fn.apply( this, arguments );\n\t\t\t\t\t\t\t\t\tif ( returned && jQuery.isFunction( returned.promise ) ) {\n\t\t\t\t\t\t\t\t\t\treturned.promise().then( newDefer.resolve, newDefer.reject, newDefer.notify );\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tnewDefer[ action + \"With\" ]( this === deferred ? newDefer : this, [ returned ] );\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tdeferred[ handler ]( newDefer[ action ] );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\t\t\t\t\t}).promise();\n\t\t\t\t},\n\t\t\t\t// Get a promise for this deferred\n\t\t\t\t// If obj is provided, the promise aspect is added to the object\n\t\t\t\tpromise: function( obj ) {\n\t\t\t\t\tif ( obj == null ) {\n\t\t\t\t\t\tobj = promise;\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfor ( var key in promise ) {\n\t\t\t\t\t\t\tobj[ key ] = promise[ key ];\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\treturn obj;\n\t\t\t\t}\n\t\t\t},\n\t\t\tdeferred = promise.promise({}),\n\t\t\tkey;\n\n\t\tfor ( key in lists ) {\n\t\t\tdeferred[ key ] = lists[ key ].fire;\n\t\t\tdeferred[ key + \"With\" ] = lists[ key ].fireWith;\n\t\t}\n\n\t\t// Handle state\n\t\tdeferred.done( function() {\n\t\t\tstate = \"resolved\";\n\t\t}, failList.disable, progressList.lock ).fail( function() {\n\t\t\tstate = \"rejected\";\n\t\t}, doneList.disable, progressList.lock );\n\n\t\t// Call given func if any\n\t\tif ( func ) {\n\t\t\tfunc.call( deferred, deferred );\n\t\t}\n\n\t\t// All done!\n\t\treturn deferred;\n\t},\n\n\t// Deferred helper\n\twhen: function( firstParam ) {\n\t\tvar args = sliceDeferred.call( arguments, 0 ),\n\t\t\ti = 0,\n\t\t\tlength = args.length,\n\t\t\tpValues = new Array( length ),\n\t\t\tcount = length,\n\t\t\tpCount = length,\n\t\t\tdeferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ?\n\t\t\t\tfirstParam :\n\t\t\t\tjQuery.Deferred(),\n\t\t\tpromise = deferred.promise();\n\t\tfunction resolveFunc( i ) {\n\t\t\treturn function( value ) {\n\t\t\t\targs[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value;\n\t\t\t\tif ( !( --count ) ) {\n\t\t\t\t\tdeferred.resolveWith( deferred, args );\n\t\t\t\t}\n\t\t\t};\n\t\t}\n\t\tfunction progressFunc( i ) {\n\t\t\treturn function( value ) {\n\t\t\t\tpValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value;\n\t\t\t\tdeferred.notifyWith( promise, pValues );\n\t\t\t};\n\t\t}\n\t\tif ( length > 1 ) {\n\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\tif ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) {\n\t\t\t\t\targs[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) );\n\t\t\t\t} else {\n\t\t\t\t\t--count;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif ( !count ) {\n\t\t\t\tdeferred.resolveWith( deferred, args );\n\t\t\t}\n\t\t} else if ( deferred !== firstParam ) {\n\t\t\tdeferred.resolveWith( deferred, length ? [ firstParam ] : [] );\n\t\t}\n\t\treturn promise;\n\t}\n});\n\n\n\n\njQuery.support = (function() {\n\n\tvar support,\n\t\tall,\n\t\ta,\n\t\tselect,\n\t\topt,\n\t\tinput,\n\t\tmarginDiv,\n\t\tfragment,\n\t\ttds,\n\t\tevents,\n\t\teventName,\n\t\ti,\n\t\tisSupported,\n\t\tdiv = document.createElement( \"div\" ),\n\t\tdocumentElement = document.documentElement;\n\n\t// Preliminary tests\n\tdiv.setAttribute(\"className\", \"t\");\n\tdiv.innerHTML = \"   <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>\";\n\n\tall = div.getElementsByTagName( \"*\" );\n\ta = div.getElementsByTagName( \"a\" )[ 0 ];\n\n\t// Can't get basic test support\n\tif ( !all || !all.length || !a ) {\n\t\treturn {};\n\t}\n\n\t// First batch of supports tests\n\tselect = document.createElement( \"select\" );\n\topt = select.appendChild( document.createElement(\"option\") );\n\tinput = div.getElementsByTagName( \"input\" )[ 0 ];\n\n\tsupport = {\n\t\t// IE strips leading whitespace when .innerHTML is used\n\t\tleadingWhitespace: ( div.firstChild.nodeType === 3 ),\n\n\t\t// Make sure that tbody elements aren't automatically inserted\n\t\t// IE will insert them into empty tables\n\t\ttbody: !div.getElementsByTagName(\"tbody\").length,\n\n\t\t// Make sure that link elements get serialized correctly by innerHTML\n\t\t// This requires a wrapper element in IE\n\t\thtmlSerialize: !!div.getElementsByTagName(\"link\").length,\n\n\t\t// Get the style information from getAttribute\n\t\t// (IE uses .cssText instead)\n\t\tstyle: /top/.test( a.getAttribute(\"style\") ),\n\n\t\t// Make sure that URLs aren't manipulated\n\t\t// (IE normalizes it by default)\n\t\threfNormalized: ( a.getAttribute(\"href\") === \"/a\" ),\n\n\t\t// Make sure that element opacity exists\n\t\t// (IE uses filter instead)\n\t\t// Use a regex to work around a WebKit issue. See #5145\n\t\topacity: /^0.55/.test( a.style.opacity ),\n\n\t\t// Verify style float existence\n\t\t// (IE uses styleFloat instead of cssFloat)\n\t\tcssFloat: !!a.style.cssFloat,\n\n\t\t// Make sure that if no value is specified for a checkbox\n\t\t// that it defaults to \"on\".\n\t\t// (WebKit defaults to \"\" instead)\n\t\tcheckOn: ( input.value === \"on\" ),\n\n\t\t// Make sure that a selected-by-default option has a working selected property.\n\t\t// (WebKit defaults to false instead of true, IE too, if it's in an optgroup)\n\t\toptSelected: opt.selected,\n\n\t\t// Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7)\n\t\tgetSetAttribute: div.className !== \"t\",\n\n\t\t// Tests for enctype support on a form(#6743)\n\t\tenctype: !!document.createElement(\"form\").enctype,\n\n\t\t// Makes sure cloning an html5 element does not cause problems\n\t\t// Where outerHTML is undefined, this still works\n\t\thtml5Clone: document.createElement(\"nav\").cloneNode( true ).outerHTML !== \"<:nav></:nav>\",\n\n\t\t// Will be defined later\n\t\tsubmitBubbles: true,\n\t\tchangeBubbles: true,\n\t\tfocusinBubbles: false,\n\t\tdeleteExpando: true,\n\t\tnoCloneEvent: true,\n\t\tinlineBlockNeedsLayout: false,\n\t\tshrinkWrapBlocks: false,\n\t\treliableMarginRight: true\n\t};\n\n\t// Make sure checked status is properly cloned\n\tinput.checked = true;\n\tsupport.noCloneChecked = input.cloneNode( true ).checked;\n\n\t// Make sure that the options inside disabled selects aren't marked as disabled\n\t// (WebKit marks them as disabled)\n\tselect.disabled = true;\n\tsupport.optDisabled = !opt.disabled;\n\n\t// Test to see if it's possible to delete an expando from an element\n\t// Fails in Internet Explorer\n\ttry {\n\t\tdelete div.test;\n\t} catch( e ) {\n\t\tsupport.deleteExpando = false;\n\t}\n\n\tif ( !div.addEventListener && div.attachEvent && div.fireEvent ) {\n\t\tdiv.attachEvent( \"onclick\", function() {\n\t\t\t// Cloning a node shouldn't copy over any\n\t\t\t// bound event handlers (IE does this)\n\t\t\tsupport.noCloneEvent = false;\n\t\t});\n\t\tdiv.cloneNode( true ).fireEvent( \"onclick\" );\n\t}\n\n\t// Check if a radio maintains its value\n\t// after being appended to the DOM\n\tinput = document.createElement(\"input\");\n\tinput.value = \"t\";\n\tinput.setAttribute(\"type\", \"radio\");\n\tsupport.radioValue = input.value === \"t\";\n\n\tinput.setAttribute(\"checked\", \"checked\");\n\tdiv.appendChild( input );\n\tfragment = document.createDocumentFragment();\n\tfragment.appendChild( div.lastChild );\n\n\t// WebKit doesn't clone checked state correctly in fragments\n\tsupport.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked;\n\n\t// Check if a disconnected checkbox will retain its checked\n\t// value of true after appended to the DOM (IE6/7)\n\tsupport.appendChecked = input.checked;\n\n\tfragment.removeChild( input );\n\tfragment.appendChild( div );\n\n\tdiv.innerHTML = \"\";\n\n\t// Check if div with explicit width and no margin-right incorrectly\n\t// gets computed margin-right based on width of container. For more\n\t// info see bug #3333\n\t// Fails in WebKit before Feb 2011 nightlies\n\t// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right\n\tif ( window.getComputedStyle ) {\n\t\tmarginDiv = document.createElement( \"div\" );\n\t\tmarginDiv.style.width = \"0\";\n\t\tmarginDiv.style.marginRight = \"0\";\n\t\tdiv.style.width = \"2px\";\n\t\tdiv.appendChild( marginDiv );\n\t\tsupport.reliableMarginRight =\n\t\t\t( parseInt( ( window.getComputedStyle( marginDiv, null ) || { marginRight: 0 } ).marginRight, 10 ) || 0 ) === 0;\n\t}\n\n\t// Technique from Juriy Zaytsev\n\t// http://perfectionkills.com/detecting-event-support-without-browser-sniffing/\n\t// We only care about the case where non-standard event systems\n\t// are used, namely in IE. Short-circuiting here helps us to\n\t// avoid an eval call (in setAttribute) which can cause CSP\n\t// to go haywire. See: https://developer.mozilla.org/en/Security/CSP\n\tif ( div.attachEvent ) {\n\t\tfor( i in {\n\t\t\tsubmit: 1,\n\t\t\tchange: 1,\n\t\t\tfocusin: 1\n\t\t}) {\n\t\t\teventName = \"on\" + i;\n\t\t\tisSupported = ( eventName in div );\n\t\t\tif ( !isSupported ) {\n\t\t\t\tdiv.setAttribute( eventName, \"return;\" );\n\t\t\t\tisSupported = ( typeof div[ eventName ] === \"function\" );\n\t\t\t}\n\t\t\tsupport[ i + \"Bubbles\" ] = isSupported;\n\t\t}\n\t}\n\n\tfragment.removeChild( div );\n\n\t// Null elements to avoid leaks in IE\n\tfragment = select = opt = marginDiv = div = input = null;\n\n\t// Run tests that need a body at doc ready\n\tjQuery(function() {\n\t\tvar container, outer, inner, table, td, offsetSupport,\n\t\t\tconMarginTop, ptlm, vb, style, html,\n\t\t\tbody = document.getElementsByTagName(\"body\")[0];\n\n\t\tif ( !body ) {\n\t\t\t// Return for frameset docs that don't have a body\n\t\t\treturn;\n\t\t}\n\n\t\tconMarginTop = 1;\n\t\tptlm = \"position:absolute;top:0;left:0;width:1px;height:1px;margin:0;\";\n\t\tvb = \"visibility:hidden;border:0;\";\n\t\tstyle = \"style='\" + ptlm + \"border:5px solid #000;padding:0;'\";\n\t\thtml = \"<div \" + style + \"><div></div></div>\" +\n\t\t\t\"<table \" + style + \" cellpadding='0' cellspacing='0'>\" +\n\t\t\t\"<tr><td></td></tr></table>\";\n\n\t\tcontainer = document.createElement(\"div\");\n\t\tcontainer.style.cssText = vb + \"width:0;height:0;position:static;top:0;margin-top:\" + conMarginTop + \"px\";\n\t\tbody.insertBefore( container, body.firstChild );\n\n\t\t// Construct the test element\n\t\tdiv = document.createElement(\"div\");\n\t\tcontainer.appendChild( div );\n\n\t\t// Check if table cells still have offsetWidth/Height when they are set\n\t\t// to display:none and there are still other visible table cells in a\n\t\t// table row; if so, offsetWidth/Height are not reliable for use when\n\t\t// determining if an element has been hidden directly using\n\t\t// display:none (it is still safe to use offsets if a parent element is\n\t\t// hidden; don safety goggles and see bug #4512 for more information).\n\t\t// (only IE 8 fails this test)\n\t\tdiv.innerHTML = \"<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>\";\n\t\ttds = div.getElementsByTagName( \"td\" );\n\t\tisSupported = ( tds[ 0 ].offsetHeight === 0 );\n\n\t\ttds[ 0 ].style.display = \"\";\n\t\ttds[ 1 ].style.display = \"none\";\n\n\t\t// Check if empty table cells still have offsetWidth/Height\n\t\t// (IE <= 8 fail this test)\n\t\tsupport.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 );\n\n\t\t// Figure out if the W3C box model works as expected\n\t\tdiv.innerHTML = \"\";\n\t\tdiv.style.width = div.style.paddingLeft = \"1px\";\n\t\tjQuery.boxModel = support.boxModel = div.offsetWidth === 2;\n\n\t\tif ( typeof div.style.zoom !== \"undefined\" ) {\n\t\t\t// Check if natively block-level elements act like inline-block\n\t\t\t// elements when setting their display to 'inline' and giving\n\t\t\t// them layout\n\t\t\t// (IE < 8 does this)\n\t\t\tdiv.style.display = \"inline\";\n\t\t\tdiv.style.zoom = 1;\n\t\t\tsupport.inlineBlockNeedsLayout = ( div.offsetWidth === 2 );\n\n\t\t\t// Check if elements with layout shrink-wrap their children\n\t\t\t// (IE 6 does this)\n\t\t\tdiv.style.display = \"\";\n\t\t\tdiv.innerHTML = \"<div style='width:4px;'></div>\";\n\t\t\tsupport.shrinkWrapBlocks = ( div.offsetWidth !== 2 );\n\t\t}\n\n\t\tdiv.style.cssText = ptlm + vb;\n\t\tdiv.innerHTML = html;\n\n\t\touter = div.firstChild;\n\t\tinner = outer.firstChild;\n\t\ttd = outer.nextSibling.firstChild.firstChild;\n\n\t\toffsetSupport = {\n\t\t\tdoesNotAddBorder: ( inner.offsetTop !== 5 ),\n\t\t\tdoesAddBorderForTableAndCells: ( td.offsetTop === 5 )\n\t\t};\n\n\t\tinner.style.position = \"fixed\";\n\t\tinner.style.top = \"20px\";\n\n\t\t// safari subtracts parent border width here which is 5px\n\t\toffsetSupport.fixedPosition = ( inner.offsetTop === 20 || inner.offsetTop === 15 );\n\t\tinner.style.position = inner.style.top = \"\";\n\n\t\touter.style.overflow = \"hidden\";\n\t\touter.style.position = \"relative\";\n\n\t\toffsetSupport.subtractsBorderForOverflowNotVisible = ( inner.offsetTop === -5 );\n\t\toffsetSupport.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== conMarginTop );\n\n\t\tbody.removeChild( container );\n\t\tdiv  = container = null;\n\n\t\tjQuery.extend( support, offsetSupport );\n\t});\n\n\treturn support;\n})();\n\n\n\n\nvar rbrace = /^(?:\\{.*\\}|\\[.*\\])$/,\n\trmultiDash = /([A-Z])/g;\n\njQuery.extend({\n\tcache: {},\n\n\t// Please use with caution\n\tuuid: 0,\n\n\t// Unique for each copy of jQuery on the page\n\t// Non-digits removed to match rinlinejQuery\n\texpando: \"jQuery\" + ( jQuery.fn.jquery + Math.random() ).replace( /\\D/g, \"\" ),\n\n\t// The following elements throw uncatchable exceptions if you\n\t// attempt to add expando properties to them.\n\tnoData: {\n\t\t\"embed\": true,\n\t\t// Ban all objects except for Flash (which handle expandos)\n\t\t\"object\": \"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000\",\n\t\t\"applet\": true\n\t},\n\n\thasData: function( elem ) {\n\t\telem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];\n\t\treturn !!elem && !isEmptyDataObject( elem );\n\t},\n\n\tdata: function( elem, name, data, pvt /* Internal Use Only */ ) {\n\t\tif ( !jQuery.acceptData( elem ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tvar privateCache, thisCache, ret,\n\t\t\tinternalKey = jQuery.expando,\n\t\t\tgetByName = typeof name === \"string\",\n\n\t\t\t// We have to handle DOM nodes and JS objects differently because IE6-7\n\t\t\t// can't GC object references properly across the DOM-JS boundary\n\t\t\tisNode = elem.nodeType,\n\n\t\t\t// Only DOM nodes need the global jQuery cache; JS object data is\n\t\t\t// attached directly to the object so GC can occur automatically\n\t\t\tcache = isNode ? jQuery.cache : elem,\n\n\t\t\t// Only defining an ID for JS objects if its cache already exists allows\n\t\t\t// the code to shortcut on the same path as a DOM node with no cache\n\t\t\tid = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey,\n\t\t\tisEvents = name === \"events\";\n\n\t\t// Avoid doing any more work than we need to when trying to get data on an\n\t\t// object that has no data at all\n\t\tif ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( !id ) {\n\t\t\t// Only DOM nodes need a new unique ID for each element since their data\n\t\t\t// ends up in the global cache\n\t\t\tif ( isNode ) {\n\t\t\t\telem[ internalKey ] = id = ++jQuery.uuid;\n\t\t\t} else {\n\t\t\t\tid = internalKey;\n\t\t\t}\n\t\t}\n\n\t\tif ( !cache[ id ] ) {\n\t\t\tcache[ id ] = {};\n\n\t\t\t// Avoids exposing jQuery metadata on plain JS objects when the object\n\t\t\t// is serialized using JSON.stringify\n\t\t\tif ( !isNode ) {\n\t\t\t\tcache[ id ].toJSON = jQuery.noop;\n\t\t\t}\n\t\t}\n\n\t\t// An object can be passed to jQuery.data instead of a key/value pair; this gets\n\t\t// shallow copied over onto the existing cache\n\t\tif ( typeof name === \"object\" || typeof name === \"function\" ) {\n\t\t\tif ( pvt ) {\n\t\t\t\tcache[ id ] = jQuery.extend( cache[ id ], name );\n\t\t\t} else {\n\t\t\t\tcache[ id ].data = jQuery.extend( cache[ id ].data, name );\n\t\t\t}\n\t\t}\n\n\t\tprivateCache = thisCache = cache[ id ];\n\n\t\t// jQuery data() is stored in a separate object inside the object's internal data\n\t\t// cache in order to avoid key collisions between internal data and user-defined\n\t\t// data.\n\t\tif ( !pvt ) {\n\t\t\tif ( !thisCache.data ) {\n\t\t\t\tthisCache.data = {};\n\t\t\t}\n\n\t\t\tthisCache = thisCache.data;\n\t\t}\n\n\t\tif ( data !== undefined ) {\n\t\t\tthisCache[ jQuery.camelCase( name ) ] = data;\n\t\t}\n\n\t\t// Users should not attempt to inspect the internal events object using jQuery.data,\n\t\t// it is undocumented and subject to change. But does anyone listen? No.\n\t\tif ( isEvents && !thisCache[ name ] ) {\n\t\t\treturn privateCache.events;\n\t\t}\n\n\t\t// Check for both converted-to-camel and non-converted data property names\n\t\t// If a data property was specified\n\t\tif ( getByName ) {\n\n\t\t\t// First Try to find as-is property data\n\t\t\tret = thisCache[ name ];\n\n\t\t\t// Test for null|undefined property data\n\t\t\tif ( ret == null ) {\n\n\t\t\t\t// Try to find the camelCased property\n\t\t\t\tret = thisCache[ jQuery.camelCase( name ) ];\n\t\t\t}\n\t\t} else {\n\t\t\tret = thisCache;\n\t\t}\n\n\t\treturn ret;\n\t},\n\n\tremoveData: function( elem, name, pvt /* Internal Use Only */ ) {\n\t\tif ( !jQuery.acceptData( elem ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tvar thisCache, i, l,\n\n\t\t\t// Reference to internal data cache key\n\t\t\tinternalKey = jQuery.expando,\n\n\t\t\tisNode = elem.nodeType,\n\n\t\t\t// See jQuery.data for more information\n\t\t\tcache = isNode ? jQuery.cache : elem,\n\n\t\t\t// See jQuery.data for more information\n\t\t\tid = isNode ? elem[ internalKey ] : internalKey;\n\n\t\t// If there is already no cache entry for this object, there is no\n\t\t// purpose in continuing\n\t\tif ( !cache[ id ] ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( name ) {\n\n\t\t\tthisCache = pvt ? cache[ id ] : cache[ id ].data;\n\n\t\t\tif ( thisCache ) {\n\n\t\t\t\t// Support array or space separated string names for data keys\n\t\t\t\tif ( !jQuery.isArray( name ) ) {\n\n\t\t\t\t\t// try the string as a key before any manipulation\n\t\t\t\t\tif ( name in thisCache ) {\n\t\t\t\t\t\tname = [ name ];\n\t\t\t\t\t} else {\n\n\t\t\t\t\t\t// split the camel cased version by spaces unless a key with the spaces exists\n\t\t\t\t\t\tname = jQuery.camelCase( name );\n\t\t\t\t\t\tif ( name in thisCache ) {\n\t\t\t\t\t\t\tname = [ name ];\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tname = name.split( \" \" );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tfor ( i = 0, l = name.length; i < l; i++ ) {\n\t\t\t\t\tdelete thisCache[ name[i] ];\n\t\t\t\t}\n\n\t\t\t\t// If there is no data left in the cache, we want to continue\n\t\t\t\t// and let the cache object itself get destroyed\n\t\t\t\tif ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// See jQuery.data for more information\n\t\tif ( !pvt ) {\n\t\t\tdelete cache[ id ].data;\n\n\t\t\t// Don't destroy the parent cache unless the internal data object\n\t\t\t// had been the only thing left in it\n\t\t\tif ( !isEmptyDataObject(cache[ id ]) ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\n\t\t// Browsers that fail expando deletion also refuse to delete expandos on\n\t\t// the window, but it will allow it on all other JS objects; other browsers\n\t\t// don't care\n\t\t// Ensure that `cache` is not a window object #10080\n\t\tif ( jQuery.support.deleteExpando || !cache.setInterval ) {\n\t\t\tdelete cache[ id ];\n\t\t} else {\n\t\t\tcache[ id ] = null;\n\t\t}\n\n\t\t// We destroyed the cache and need to eliminate the expando on the node to avoid\n\t\t// false lookups in the cache for entries that no longer exist\n\t\tif ( isNode ) {\n\t\t\t// IE does not allow us to delete expando properties from nodes,\n\t\t\t// nor does it have a removeAttribute function on Document nodes;\n\t\t\t// we must handle all of these cases\n\t\t\tif ( jQuery.support.deleteExpando ) {\n\t\t\t\tdelete elem[ internalKey ];\n\t\t\t} else if ( elem.removeAttribute ) {\n\t\t\t\telem.removeAttribute( internalKey );\n\t\t\t} else {\n\t\t\t\telem[ internalKey ] = null;\n\t\t\t}\n\t\t}\n\t},\n\n\t// For internal use only.\n\t_data: function( elem, name, data ) {\n\t\treturn jQuery.data( elem, name, data, true );\n\t},\n\n\t// A method for determining if a DOM node can handle the data expando\n\tacceptData: function( elem ) {\n\t\tif ( elem.nodeName ) {\n\t\t\tvar match = jQuery.noData[ elem.nodeName.toLowerCase() ];\n\n\t\t\tif ( match ) {\n\t\t\t\treturn !(match === true || elem.getAttribute(\"classid\") !== match);\n\t\t\t}\n\t\t}\n\n\t\treturn true;\n\t}\n});\n\njQuery.fn.extend({\n\tdata: function( key, value ) {\n\t\tvar parts, attr, name,\n\t\t\tdata = null;\n\n\t\tif ( typeof key === \"undefined\" ) {\n\t\t\tif ( this.length ) {\n\t\t\t\tdata = jQuery.data( this[0] );\n\n\t\t\t\tif ( this[0].nodeType === 1 && !jQuery._data( this[0], \"parsedAttrs\" ) ) {\n\t\t\t\t\tattr = this[0].attributes;\n\t\t\t\t\tfor ( var i = 0, l = attr.length; i < l; i++ ) {\n\t\t\t\t\t\tname = attr[i].name;\n\n\t\t\t\t\t\tif ( name.indexOf( \"data-\" ) === 0 ) {\n\t\t\t\t\t\t\tname = jQuery.camelCase( name.substring(5) );\n\n\t\t\t\t\t\t\tdataAttr( this[0], name, data[ name ] );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tjQuery._data( this[0], \"parsedAttrs\", true );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn data;\n\n\t\t} else if ( typeof key === \"object\" ) {\n\t\t\treturn this.each(function() {\n\t\t\t\tjQuery.data( this, key );\n\t\t\t});\n\t\t}\n\n\t\tparts = key.split(\".\");\n\t\tparts[1] = parts[1] ? \".\" + parts[1] : \"\";\n\n\t\tif ( value === undefined ) {\n\t\t\tdata = this.triggerHandler(\"getData\" + parts[1] + \"!\", [parts[0]]);\n\n\t\t\t// Try to fetch any internally stored data first\n\t\t\tif ( data === undefined && this.length ) {\n\t\t\t\tdata = jQuery.data( this[0], key );\n\t\t\t\tdata = dataAttr( this[0], key, data );\n\t\t\t}\n\n\t\t\treturn data === undefined && parts[1] ?\n\t\t\t\tthis.data( parts[0] ) :\n\t\t\t\tdata;\n\n\t\t} else {\n\t\t\treturn this.each(function() {\n\t\t\t\tvar self = jQuery( this ),\n\t\t\t\t\targs = [ parts[0], value ];\n\n\t\t\t\tself.triggerHandler( \"setData\" + parts[1] + \"!\", args );\n\t\t\t\tjQuery.data( this, key, value );\n\t\t\t\tself.triggerHandler( \"changeData\" + parts[1] + \"!\", args );\n\t\t\t});\n\t\t}\n\t},\n\n\tremoveData: function( key ) {\n\t\treturn this.each(function() {\n\t\t\tjQuery.removeData( this, key );\n\t\t});\n\t}\n});\n\nfunction dataAttr( elem, key, data ) {\n\t// If nothing was found internally, try to fetch any\n\t// data from the HTML5 data-* attribute\n\tif ( data === undefined && elem.nodeType === 1 ) {\n\n\t\tvar name = \"data-\" + key.replace( rmultiDash, \"-$1\" ).toLowerCase();\n\n\t\tdata = elem.getAttribute( name );\n\n\t\tif ( typeof data === \"string\" ) {\n\t\t\ttry {\n\t\t\t\tdata = data === \"true\" ? true :\n\t\t\t\tdata === \"false\" ? false :\n\t\t\t\tdata === \"null\" ? null :\n\t\t\t\tjQuery.isNumeric( data ) ? parseFloat( data ) :\n\t\t\t\t\trbrace.test( data ) ? jQuery.parseJSON( data ) :\n\t\t\t\t\tdata;\n\t\t\t} catch( e ) {}\n\n\t\t\t// Make sure we set the data so it isn't changed later\n\t\t\tjQuery.data( elem, key, data );\n\n\t\t} else {\n\t\t\tdata = undefined;\n\t\t}\n\t}\n\n\treturn data;\n}\n\n// checks a cache object for emptiness\nfunction isEmptyDataObject( obj ) {\n\tfor ( var name in obj ) {\n\n\t\t// if the public data object is empty, the private is still empty\n\t\tif ( name === \"data\" && jQuery.isEmptyObject( obj[name] ) ) {\n\t\t\tcontinue;\n\t\t}\n\t\tif ( name !== \"toJSON\" ) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\treturn true;\n}\n\n\n\n\nfunction handleQueueMarkDefer( elem, type, src ) {\n\tvar deferDataKey = type + \"defer\",\n\t\tqueueDataKey = type + \"queue\",\n\t\tmarkDataKey = type + \"mark\",\n\t\tdefer = jQuery._data( elem, deferDataKey );\n\tif ( defer &&\n\t\t( src === \"queue\" || !jQuery._data(elem, queueDataKey) ) &&\n\t\t( src === \"mark\" || !jQuery._data(elem, markDataKey) ) ) {\n\t\t// Give room for hard-coded callbacks to fire first\n\t\t// and eventually mark/queue something else on the element\n\t\tsetTimeout( function() {\n\t\t\tif ( !jQuery._data( elem, queueDataKey ) &&\n\t\t\t\t!jQuery._data( elem, markDataKey ) ) {\n\t\t\t\tjQuery.removeData( elem, deferDataKey, true );\n\t\t\t\tdefer.fire();\n\t\t\t}\n\t\t}, 0 );\n\t}\n}\n\njQuery.extend({\n\n\t_mark: function( elem, type ) {\n\t\tif ( elem ) {\n\t\t\ttype = ( type || \"fx\" ) + \"mark\";\n\t\t\tjQuery._data( elem, type, (jQuery._data( elem, type ) || 0) + 1 );\n\t\t}\n\t},\n\n\t_unmark: function( force, elem, type ) {\n\t\tif ( force !== true ) {\n\t\t\ttype = elem;\n\t\t\telem = force;\n\t\t\tforce = false;\n\t\t}\n\t\tif ( elem ) {\n\t\t\ttype = type || \"fx\";\n\t\t\tvar key = type + \"mark\",\n\t\t\t\tcount = force ? 0 : ( (jQuery._data( elem, key ) || 1) - 1 );\n\t\t\tif ( count ) {\n\t\t\t\tjQuery._data( elem, key, count );\n\t\t\t} else {\n\t\t\t\tjQuery.removeData( elem, key, true );\n\t\t\t\thandleQueueMarkDefer( elem, type, \"mark\" );\n\t\t\t}\n\t\t}\n\t},\n\n\tqueue: function( elem, type, data ) {\n\t\tvar q;\n\t\tif ( elem ) {\n\t\t\ttype = ( type || \"fx\" ) + \"queue\";\n\t\t\tq = jQuery._data( elem, type );\n\n\t\t\t// Speed up dequeue by getting out quickly if this is just a lookup\n\t\t\tif ( data ) {\n\t\t\t\tif ( !q || jQuery.isArray(data) ) {\n\t\t\t\t\tq = jQuery._data( elem, type, jQuery.makeArray(data) );\n\t\t\t\t} else {\n\t\t\t\t\tq.push( data );\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn q || [];\n\t\t}\n\t},\n\n\tdequeue: function( elem, type ) {\n\t\ttype = type || \"fx\";\n\n\t\tvar queue = jQuery.queue( elem, type ),\n\t\t\tfn = queue.shift(),\n\t\t\thooks = {};\n\n\t\t// If the fx queue is dequeued, always remove the progress sentinel\n\t\tif ( fn === \"inprogress\" ) {\n\t\t\tfn = queue.shift();\n\t\t}\n\n\t\tif ( fn ) {\n\t\t\t// Add a progress sentinel to prevent the fx queue from being\n\t\t\t// automatically dequeued\n\t\t\tif ( type === \"fx\" ) {\n\t\t\t\tqueue.unshift( \"inprogress\" );\n\t\t\t}\n\n\t\t\tjQuery._data( elem, type + \".run\", hooks );\n\t\t\tfn.call( elem, function() {\n\t\t\t\tjQuery.dequeue( elem, type );\n\t\t\t}, hooks );\n\t\t}\n\n\t\tif ( !queue.length ) {\n\t\t\tjQuery.removeData( elem, type + \"queue \" + type + \".run\", true );\n\t\t\thandleQueueMarkDefer( elem, type, \"queue\" );\n\t\t}\n\t}\n});\n\njQuery.fn.extend({\n\tqueue: function( type, data ) {\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tdata = type;\n\t\t\ttype = \"fx\";\n\t\t}\n\n\t\tif ( data === undefined ) {\n\t\t\treturn jQuery.queue( this[0], type );\n\t\t}\n\t\treturn this.each(function() {\n\t\t\tvar queue = jQuery.queue( this, type, data );\n\n\t\t\tif ( type === \"fx\" && queue[0] !== \"inprogress\" ) {\n\t\t\t\tjQuery.dequeue( this, type );\n\t\t\t}\n\t\t});\n\t},\n\tdequeue: function( type ) {\n\t\treturn this.each(function() {\n\t\t\tjQuery.dequeue( this, type );\n\t\t});\n\t},\n\t// Based off of the plugin by Clint Helfers, with permission.\n\t// http://blindsignals.com/index.php/2009/07/jquery-delay/\n\tdelay: function( time, type ) {\n\t\ttime = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time;\n\t\ttype = type || \"fx\";\n\n\t\treturn this.queue( type, function( next, hooks ) {\n\t\t\tvar timeout = setTimeout( next, time );\n\t\t\thooks.stop = function() {\n\t\t\t\tclearTimeout( timeout );\n\t\t\t};\n\t\t});\n\t},\n\tclearQueue: function( type ) {\n\t\treturn this.queue( type || \"fx\", [] );\n\t},\n\t// Get a promise resolved when queues of a certain type\n\t// are emptied (fx is the type by default)\n\tpromise: function( type, object ) {\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tobject = type;\n\t\t\ttype = undefined;\n\t\t}\n\t\ttype = type || \"fx\";\n\t\tvar defer = jQuery.Deferred(),\n\t\t\telements = this,\n\t\t\ti = elements.length,\n\t\t\tcount = 1,\n\t\t\tdeferDataKey = type + \"defer\",\n\t\t\tqueueDataKey = type + \"queue\",\n\t\t\tmarkDataKey = type + \"mark\",\n\t\t\ttmp;\n\t\tfunction resolve() {\n\t\t\tif ( !( --count ) ) {\n\t\t\t\tdefer.resolveWith( elements, [ elements ] );\n\t\t\t}\n\t\t}\n\t\twhile( i-- ) {\n\t\t\tif (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) ||\n\t\t\t\t\t( jQuery.data( elements[ i ], queueDataKey, undefined, true ) ||\n\t\t\t\t\t\tjQuery.data( elements[ i ], markDataKey, undefined, true ) ) &&\n\t\t\t\t\tjQuery.data( elements[ i ], deferDataKey, jQuery.Callbacks( \"once memory\" ), true ) )) {\n\t\t\t\tcount++;\n\t\t\t\ttmp.add( resolve );\n\t\t\t}\n\t\t}\n\t\tresolve();\n\t\treturn defer.promise();\n\t}\n});\n\n\n\n\nvar rclass = /[\\n\\t\\r]/g,\n\trspace = /\\s+/,\n\trreturn = /\\r/g,\n\trtype = /^(?:button|input)$/i,\n\trfocusable = /^(?:button|input|object|select|textarea)$/i,\n\trclickable = /^a(?:rea)?$/i,\n\trboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,\n\tgetSetAttribute = jQuery.support.getSetAttribute,\n\tnodeHook, boolHook, fixSpecified;\n\njQuery.fn.extend({\n\tattr: function( name, value ) {\n\t\treturn jQuery.access( this, name, value, true, jQuery.attr );\n\t},\n\n\tremoveAttr: function( name ) {\n\t\treturn this.each(function() {\n\t\t\tjQuery.removeAttr( this, name );\n\t\t});\n\t},\n\n\tprop: function( name, value ) {\n\t\treturn jQuery.access( this, name, value, true, jQuery.prop );\n\t},\n\n\tremoveProp: function( name ) {\n\t\tname = jQuery.propFix[ name ] || name;\n\t\treturn this.each(function() {\n\t\t\t// try/catch handles cases where IE balks (such as removing a property on window)\n\t\t\ttry {\n\t\t\t\tthis[ name ] = undefined;\n\t\t\t\tdelete this[ name ];\n\t\t\t} catch( e ) {}\n\t\t});\n\t},\n\n\taddClass: function( value ) {\n\t\tvar classNames, i, l, elem,\n\t\t\tsetClass, c, cl;\n\n\t\tif ( jQuery.isFunction( value ) ) {\n\t\t\treturn this.each(function( j ) {\n\t\t\t\tjQuery( this ).addClass( value.call(this, j, this.className) );\n\t\t\t});\n\t\t}\n\n\t\tif ( value && typeof value === \"string\" ) {\n\t\t\tclassNames = value.split( rspace );\n\n\t\t\tfor ( i = 0, l = this.length; i < l; i++ ) {\n\t\t\t\telem = this[ i ];\n\n\t\t\t\tif ( elem.nodeType === 1 ) {\n\t\t\t\t\tif ( !elem.className && classNames.length === 1 ) {\n\t\t\t\t\t\telem.className = value;\n\n\t\t\t\t\t} else {\n\t\t\t\t\t\tsetClass = \" \" + elem.className + \" \";\n\n\t\t\t\t\t\tfor ( c = 0, cl = classNames.length; c < cl; c++ ) {\n\t\t\t\t\t\t\tif ( !~setClass.indexOf( \" \" + classNames[ c ] + \" \" ) ) {\n\t\t\t\t\t\t\t\tsetClass += classNames[ c ] + \" \";\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\telem.className = jQuery.trim( setClass );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\tremoveClass: function( value ) {\n\t\tvar classNames, i, l, elem, className, c, cl;\n\n\t\tif ( jQuery.isFunction( value ) ) {\n\t\t\treturn this.each(function( j ) {\n\t\t\t\tjQuery( this ).removeClass( value.call(this, j, this.className) );\n\t\t\t});\n\t\t}\n\n\t\tif ( (value && typeof value === \"string\") || value === undefined ) {\n\t\t\tclassNames = ( value || \"\" ).split( rspace );\n\n\t\t\tfor ( i = 0, l = this.length; i < l; i++ ) {\n\t\t\t\telem = this[ i ];\n\n\t\t\t\tif ( elem.nodeType === 1 && elem.className ) {\n\t\t\t\t\tif ( value ) {\n\t\t\t\t\t\tclassName = (\" \" + elem.className + \" \").replace( rclass, \" \" );\n\t\t\t\t\t\tfor ( c = 0, cl = classNames.length; c < cl; c++ ) {\n\t\t\t\t\t\t\tclassName = className.replace(\" \" + classNames[ c ] + \" \", \" \");\n\t\t\t\t\t\t}\n\t\t\t\t\t\telem.className = jQuery.trim( className );\n\n\t\t\t\t\t} else {\n\t\t\t\t\t\telem.className = \"\";\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\ttoggleClass: function( value, stateVal ) {\n\t\tvar type = typeof value,\n\t\t\tisBool = typeof stateVal === \"boolean\";\n\n\t\tif ( jQuery.isFunction( value ) ) {\n\t\t\treturn this.each(function( i ) {\n\t\t\t\tjQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal );\n\t\t\t});\n\t\t}\n\n\t\treturn this.each(function() {\n\t\t\tif ( type === \"string\" ) {\n\t\t\t\t// toggle individual class names\n\t\t\t\tvar className,\n\t\t\t\t\ti = 0,\n\t\t\t\t\tself = jQuery( this ),\n\t\t\t\t\tstate = stateVal,\n\t\t\t\t\tclassNames = value.split( rspace );\n\n\t\t\t\twhile ( (className = classNames[ i++ ]) ) {\n\t\t\t\t\t// check each className given, space separated list\n\t\t\t\t\tstate = isBool ? state : !self.hasClass( className );\n\t\t\t\t\tself[ state ? \"addClass\" : \"removeClass\" ]( className );\n\t\t\t\t}\n\n\t\t\t} else if ( type === \"undefined\" || type === \"boolean\" ) {\n\t\t\t\tif ( this.className ) {\n\t\t\t\t\t// store className if set\n\t\t\t\t\tjQuery._data( this, \"__className__\", this.className );\n\t\t\t\t}\n\n\t\t\t\t// toggle whole className\n\t\t\t\tthis.className = this.className || value === false ? \"\" : jQuery._data( this, \"__className__\" ) || \"\";\n\t\t\t}\n\t\t});\n\t},\n\n\thasClass: function( selector ) {\n\t\tvar className = \" \" + selector + \" \",\n\t\t\ti = 0,\n\t\t\tl = this.length;\n\t\tfor ( ; i < l; i++ ) {\n\t\t\tif ( this[i].nodeType === 1 && (\" \" + this[i].className + \" \").replace(rclass, \" \").indexOf( className ) > -1 ) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t},\n\n\tval: function( value ) {\n\t\tvar hooks, ret, isFunction,\n\t\t\telem = this[0];\n\n\t\tif ( !arguments.length ) {\n\t\t\tif ( elem ) {\n\t\t\t\thooks = jQuery.valHooks[ elem.nodeName.toLowerCase() ] || jQuery.valHooks[ elem.type ];\n\n\t\t\t\tif ( hooks && \"get\" in hooks && (ret = hooks.get( elem, \"value\" )) !== undefined ) {\n\t\t\t\t\treturn ret;\n\t\t\t\t}\n\n\t\t\t\tret = elem.value;\n\n\t\t\t\treturn typeof ret === \"string\" ?\n\t\t\t\t\t// handle most common string cases\n\t\t\t\t\tret.replace(rreturn, \"\") :\n\t\t\t\t\t// handle cases where value is null/undef or number\n\t\t\t\t\tret == null ? \"\" : ret;\n\t\t\t}\n\n\t\t\treturn;\n\t\t}\n\n\t\tisFunction = jQuery.isFunction( value );\n\n\t\treturn this.each(function( i ) {\n\t\t\tvar self = jQuery(this), val;\n\n\t\t\tif ( this.nodeType !== 1 ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif ( isFunction ) {\n\t\t\t\tval = value.call( this, i, self.val() );\n\t\t\t} else {\n\t\t\t\tval = value;\n\t\t\t}\n\n\t\t\t// Treat null/undefined as \"\"; convert numbers to string\n\t\t\tif ( val == null ) {\n\t\t\t\tval = \"\";\n\t\t\t} else if ( typeof val === \"number\" ) {\n\t\t\t\tval += \"\";\n\t\t\t} else if ( jQuery.isArray( val ) ) {\n\t\t\t\tval = jQuery.map(val, function ( value ) {\n\t\t\t\t\treturn value == null ? \"\" : value + \"\";\n\t\t\t\t});\n\t\t\t}\n\n\t\t\thooks = jQuery.valHooks[ this.nodeName.toLowerCase() ] || jQuery.valHooks[ this.type ];\n\n\t\t\t// If set returns undefined, fall back to normal setting\n\t\t\tif ( !hooks || !(\"set\" in hooks) || hooks.set( this, val, \"value\" ) === undefined ) {\n\t\t\t\tthis.value = val;\n\t\t\t}\n\t\t});\n\t}\n});\n\njQuery.extend({\n\tvalHooks: {\n\t\toption: {\n\t\t\tget: function( elem ) {\n\t\t\t\t// attributes.value is undefined in Blackberry 4.7 but\n\t\t\t\t// uses .value. See #6932\n\t\t\t\tvar val = elem.attributes.value;\n\t\t\t\treturn !val || val.specified ? elem.value : elem.text;\n\t\t\t}\n\t\t},\n\t\tselect: {\n\t\t\tget: function( elem ) {\n\t\t\t\tvar value, i, max, option,\n\t\t\t\t\tindex = elem.selectedIndex,\n\t\t\t\t\tvalues = [],\n\t\t\t\t\toptions = elem.options,\n\t\t\t\t\tone = elem.type === \"select-one\";\n\n\t\t\t\t// Nothing was selected\n\t\t\t\tif ( index < 0 ) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\t// Loop through all the selected options\n\t\t\t\ti = one ? index : 0;\n\t\t\t\tmax = one ? index + 1 : options.length;\n\t\t\t\tfor ( ; i < max; i++ ) {\n\t\t\t\t\toption = options[ i ];\n\n\t\t\t\t\t// Don't return options that are disabled or in a disabled optgroup\n\t\t\t\t\tif ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute(\"disabled\") === null) &&\n\t\t\t\t\t\t\t(!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, \"optgroup\" )) ) {\n\n\t\t\t\t\t\t// Get the specific value for the option\n\t\t\t\t\t\tvalue = jQuery( option ).val();\n\n\t\t\t\t\t\t// We don't need an array for one selects\n\t\t\t\t\t\tif ( one ) {\n\t\t\t\t\t\t\treturn value;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Multi-Selects return an array\n\t\t\t\t\t\tvalues.push( value );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Fixes Bug #2551 -- select.val() broken in IE after form.reset()\n\t\t\t\tif ( one && !values.length && options.length ) {\n\t\t\t\t\treturn jQuery( options[ index ] ).val();\n\t\t\t\t}\n\n\t\t\t\treturn values;\n\t\t\t},\n\n\t\t\tset: function( elem, value ) {\n\t\t\t\tvar values = jQuery.makeArray( value );\n\n\t\t\t\tjQuery(elem).find(\"option\").each(function() {\n\t\t\t\t\tthis.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0;\n\t\t\t\t});\n\n\t\t\t\tif ( !values.length ) {\n\t\t\t\t\telem.selectedIndex = -1;\n\t\t\t\t}\n\t\t\t\treturn values;\n\t\t\t}\n\t\t}\n\t},\n\n\tattrFn: {\n\t\tval: true,\n\t\tcss: true,\n\t\thtml: true,\n\t\ttext: true,\n\t\tdata: true,\n\t\twidth: true,\n\t\theight: true,\n\t\toffset: true\n\t},\n\n\tattr: function( elem, name, value, pass ) {\n\t\tvar ret, hooks, notxml,\n\t\t\tnType = elem.nodeType;\n\n\t\t// don't get/set attributes on text, comment and attribute nodes\n\t\tif ( !elem || nType === 3 || nType === 8 || nType === 2 ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( pass && name in jQuery.attrFn ) {\n\t\t\treturn jQuery( elem )[ name ]( value );\n\t\t}\n\n\t\t// Fallback to prop when attributes are not supported\n\t\tif ( typeof elem.getAttribute === \"undefined\" ) {\n\t\t\treturn jQuery.prop( elem, name, value );\n\t\t}\n\n\t\tnotxml = nType !== 1 || !jQuery.isXMLDoc( elem );\n\n\t\t// All attributes are lowercase\n\t\t// Grab necessary hook if one is defined\n\t\tif ( notxml ) {\n\t\t\tname = name.toLowerCase();\n\t\t\thooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook );\n\t\t}\n\n\t\tif ( value !== undefined ) {\n\n\t\t\tif ( value === null ) {\n\t\t\t\tjQuery.removeAttr( elem, name );\n\t\t\t\treturn;\n\n\t\t\t} else if ( hooks && \"set\" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) {\n\t\t\t\treturn ret;\n\n\t\t\t} else {\n\t\t\t\telem.setAttribute( name, \"\" + value );\n\t\t\t\treturn value;\n\t\t\t}\n\n\t\t} else if ( hooks && \"get\" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) {\n\t\t\treturn ret;\n\n\t\t} else {\n\n\t\t\tret = elem.getAttribute( name );\n\n\t\t\t// Non-existent attributes return null, we normalize to undefined\n\t\t\treturn ret === null ?\n\t\t\t\tundefined :\n\t\t\t\tret;\n\t\t}\n\t},\n\n\tremoveAttr: function( elem, value ) {\n\t\tvar propName, attrNames, name, l,\n\t\t\ti = 0;\n\n\t\tif ( value && elem.nodeType === 1 ) {\n\t\t\tattrNames = value.toLowerCase().split( rspace );\n\t\t\tl = attrNames.length;\n\n\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\tname = attrNames[ i ];\n\n\t\t\t\tif ( name ) {\n\t\t\t\t\tpropName = jQuery.propFix[ name ] || name;\n\n\t\t\t\t\t// See #9699 for explanation of this approach (setting first, then removal)\n\t\t\t\t\tjQuery.attr( elem, name, \"\" );\n\t\t\t\t\telem.removeAttribute( getSetAttribute ? name : propName );\n\n\t\t\t\t\t// Set corresponding property to false for boolean attributes\n\t\t\t\t\tif ( rboolean.test( name ) && propName in elem ) {\n\t\t\t\t\t\telem[ propName ] = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\n\tattrHooks: {\n\t\ttype: {\n\t\t\tset: function( elem, value ) {\n\t\t\t\t// We can't allow the type property to be changed (since it causes problems in IE)\n\t\t\t\tif ( rtype.test( elem.nodeName ) && elem.parentNode ) {\n\t\t\t\t\tjQuery.error( \"type property can't be changed\" );\n\t\t\t\t} else if ( !jQuery.support.radioValue && value === \"radio\" && jQuery.nodeName(elem, \"input\") ) {\n\t\t\t\t\t// Setting the type on a radio button after the value resets the value in IE6-9\n\t\t\t\t\t// Reset value to it's default in case type is set after value\n\t\t\t\t\t// This is for element creation\n\t\t\t\t\tvar val = elem.value;\n\t\t\t\t\telem.setAttribute( \"type\", value );\n\t\t\t\t\tif ( val ) {\n\t\t\t\t\t\telem.value = val;\n\t\t\t\t\t}\n\t\t\t\t\treturn value;\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t// Use the value property for back compat\n\t\t// Use the nodeHook for button elements in IE6/7 (#1954)\n\t\tvalue: {\n\t\t\tget: function( elem, name ) {\n\t\t\t\tif ( nodeHook && jQuery.nodeName( elem, \"button\" ) ) {\n\t\t\t\t\treturn nodeHook.get( elem, name );\n\t\t\t\t}\n\t\t\t\treturn name in elem ?\n\t\t\t\t\telem.value :\n\t\t\t\t\tnull;\n\t\t\t},\n\t\t\tset: function( elem, value, name ) {\n\t\t\t\tif ( nodeHook && jQuery.nodeName( elem, \"button\" ) ) {\n\t\t\t\t\treturn nodeHook.set( elem, value, name );\n\t\t\t\t}\n\t\t\t\t// Does not return so that setAttribute is also used\n\t\t\t\telem.value = value;\n\t\t\t}\n\t\t}\n\t},\n\n\tpropFix: {\n\t\ttabindex: \"tabIndex\",\n\t\treadonly: \"readOnly\",\n\t\t\"for\": \"htmlFor\",\n\t\t\"class\": \"className\",\n\t\tmaxlength: \"maxLength\",\n\t\tcellspacing: \"cellSpacing\",\n\t\tcellpadding: \"cellPadding\",\n\t\trowspan: \"rowSpan\",\n\t\tcolspan: \"colSpan\",\n\t\tusemap: \"useMap\",\n\t\tframeborder: \"frameBorder\",\n\t\tcontenteditable: \"contentEditable\"\n\t},\n\n\tprop: function( elem, name, value ) {\n\t\tvar ret, hooks, notxml,\n\t\t\tnType = elem.nodeType;\n\n\t\t// don't get/set properties on text, comment and attribute nodes\n\t\tif ( !elem || nType === 3 || nType === 8 || nType === 2 ) {\n\t\t\treturn;\n\t\t}\n\n\t\tnotxml = nType !== 1 || !jQuery.isXMLDoc( elem );\n\n\t\tif ( notxml ) {\n\t\t\t// Fix name and attach hooks\n\t\t\tname = jQuery.propFix[ name ] || name;\n\t\t\thooks = jQuery.propHooks[ name ];\n\t\t}\n\n\t\tif ( value !== undefined ) {\n\t\t\tif ( hooks && \"set\" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) {\n\t\t\t\treturn ret;\n\n\t\t\t} else {\n\t\t\t\treturn ( elem[ name ] = value );\n\t\t\t}\n\n\t\t} else {\n\t\t\tif ( hooks && \"get\" in hooks && (ret = hooks.get( elem, name )) !== null ) {\n\t\t\t\treturn ret;\n\n\t\t\t} else {\n\t\t\t\treturn elem[ name ];\n\t\t\t}\n\t\t}\n\t},\n\n\tpropHooks: {\n\t\ttabIndex: {\n\t\t\tget: function( elem ) {\n\t\t\t\t// elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set\n\t\t\t\t// http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/\n\t\t\t\tvar attributeNode = elem.getAttributeNode(\"tabindex\");\n\n\t\t\t\treturn attributeNode && attributeNode.specified ?\n\t\t\t\t\tparseInt( attributeNode.value, 10 ) :\n\t\t\t\t\trfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ?\n\t\t\t\t\t\t0 :\n\t\t\t\t\t\tundefined;\n\t\t\t}\n\t\t}\n\t}\n});\n\n// Add the tabIndex propHook to attrHooks for back-compat (different case is intentional)\njQuery.attrHooks.tabindex = jQuery.propHooks.tabIndex;\n\n// Hook for boolean attributes\nboolHook = {\n\tget: function( elem, name ) {\n\t\t// Align boolean attributes with corresponding properties\n\t\t// Fall back to attribute presence where some booleans are not supported\n\t\tvar attrNode,\n\t\t\tproperty = jQuery.prop( elem, name );\n\t\treturn property === true || typeof property !== \"boolean\" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ?\n\t\t\tname.toLowerCase() :\n\t\t\tundefined;\n\t},\n\tset: function( elem, value, name ) {\n\t\tvar propName;\n\t\tif ( value === false ) {\n\t\t\t// Remove boolean attributes when set to false\n\t\t\tjQuery.removeAttr( elem, name );\n\t\t} else {\n\t\t\t// value is true since we know at this point it's type boolean and not false\n\t\t\t// Set boolean attributes to the same name and set the DOM property\n\t\t\tpropName = jQuery.propFix[ name ] || name;\n\t\t\tif ( propName in elem ) {\n\t\t\t\t// Only set the IDL specifically if it already exists on the element\n\t\t\t\telem[ propName ] = true;\n\t\t\t}\n\n\t\t\telem.setAttribute( name, name.toLowerCase() );\n\t\t}\n\t\treturn name;\n\t}\n};\n\n// IE6/7 do not support getting/setting some attributes with get/setAttribute\nif ( !getSetAttribute ) {\n\n\tfixSpecified = {\n\t\tname: true,\n\t\tid: true\n\t};\n\n\t// Use this for any attribute in IE6/7\n\t// This fixes almost every IE6/7 issue\n\tnodeHook = jQuery.valHooks.button = {\n\t\tget: function( elem, name ) {\n\t\t\tvar ret;\n\t\t\tret = elem.getAttributeNode( name );\n\t\t\treturn ret && ( fixSpecified[ name ] ? ret.nodeValue !== \"\" : ret.specified ) ?\n\t\t\t\tret.nodeValue :\n\t\t\t\tundefined;\n\t\t},\n\t\tset: function( elem, value, name ) {\n\t\t\t// Set the existing or create a new attribute node\n\t\t\tvar ret = elem.getAttributeNode( name );\n\t\t\tif ( !ret ) {\n\t\t\t\tret = document.createAttribute( name );\n\t\t\t\telem.setAttributeNode( ret );\n\t\t\t}\n\t\t\treturn ( ret.nodeValue = value + \"\" );\n\t\t}\n\t};\n\n\t// Apply the nodeHook to tabindex\n\tjQuery.attrHooks.tabindex.set = nodeHook.set;\n\n\t// Set width and height to auto instead of 0 on empty string( Bug #8150 )\n\t// This is for removals\n\tjQuery.each([ \"width\", \"height\" ], function( i, name ) {\n\t\tjQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], {\n\t\t\tset: function( elem, value ) {\n\t\t\t\tif ( value === \"\" ) {\n\t\t\t\t\telem.setAttribute( name, \"auto\" );\n\t\t\t\t\treturn value;\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t});\n\n\t// Set contenteditable to false on removals(#10429)\n\t// Setting to empty string throws an error as an invalid value\n\tjQuery.attrHooks.contenteditable = {\n\t\tget: nodeHook.get,\n\t\tset: function( elem, value, name ) {\n\t\t\tif ( value === \"\" ) {\n\t\t\t\tvalue = \"false\";\n\t\t\t}\n\t\t\tnodeHook.set( elem, value, name );\n\t\t}\n\t};\n}\n\n\n// Some attributes require a special call on IE\nif ( !jQuery.support.hrefNormalized ) {\n\tjQuery.each([ \"href\", \"src\", \"width\", \"height\" ], function( i, name ) {\n\t\tjQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], {\n\t\t\tget: function( elem ) {\n\t\t\t\tvar ret = elem.getAttribute( name, 2 );\n\t\t\t\treturn ret === null ? undefined : ret;\n\t\t\t}\n\t\t});\n\t});\n}\n\nif ( !jQuery.support.style ) {\n\tjQuery.attrHooks.style = {\n\t\tget: function( elem ) {\n\t\t\t// Return undefined in the case of empty string\n\t\t\t// Normalize to lowercase since IE uppercases css property names\n\t\t\treturn elem.style.cssText.toLowerCase() || undefined;\n\t\t},\n\t\tset: function( elem, value ) {\n\t\t\treturn ( elem.style.cssText = \"\" + value );\n\t\t}\n\t};\n}\n\n// Safari mis-reports the default selected property of an option\n// Accessing the parent's selectedIndex property fixes it\nif ( !jQuery.support.optSelected ) {\n\tjQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, {\n\t\tget: function( elem ) {\n\t\t\tvar parent = elem.parentNode;\n\n\t\t\tif ( parent ) {\n\t\t\t\tparent.selectedIndex;\n\n\t\t\t\t// Make sure that it also works with optgroups, see #5701\n\t\t\t\tif ( parent.parentNode ) {\n\t\t\t\t\tparent.parentNode.selectedIndex;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\t});\n}\n\n// IE6/7 call enctype encoding\nif ( !jQuery.support.enctype ) {\n\tjQuery.propFix.enctype = \"encoding\";\n}\n\n// Radios and checkboxes getter/setter\nif ( !jQuery.support.checkOn ) {\n\tjQuery.each([ \"radio\", \"checkbox\" ], function() {\n\t\tjQuery.valHooks[ this ] = {\n\t\t\tget: function( elem ) {\n\t\t\t\t// Handle the case where in Webkit \"\" is returned instead of \"on\" if a value isn't specified\n\t\t\t\treturn elem.getAttribute(\"value\") === null ? \"on\" : elem.value;\n\t\t\t}\n\t\t};\n\t});\n}\njQuery.each([ \"radio\", \"checkbox\" ], function() {\n\tjQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], {\n\t\tset: function( elem, value ) {\n\t\t\tif ( jQuery.isArray( value ) ) {\n\t\t\t\treturn ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 );\n\t\t\t}\n\t\t}\n\t});\n});\n\n\n\n\nvar rformElems = /^(?:textarea|input|select)$/i,\n\trtypenamespace = /^([^\\.]*)?(?:\\.(.+))?$/,\n\trhoverHack = /\\bhover(\\.\\S+)?\\b/,\n\trkeyEvent = /^key/,\n\trmouseEvent = /^(?:mouse|contextmenu)|click/,\n\trfocusMorph = /^(?:focusinfocus|focusoutblur)$/,\n\trquickIs = /^(\\w*)(?:#([\\w\\-]+))?(?:\\.([\\w\\-]+))?$/,\n\tquickParse = function( selector ) {\n\t\tvar quick = rquickIs.exec( selector );\n\t\tif ( quick ) {\n\t\t\t//   0  1    2   3\n\t\t\t// [ _, tag, id, class ]\n\t\t\tquick[1] = ( quick[1] || \"\" ).toLowerCase();\n\t\t\tquick[3] = quick[3] && new RegExp( \"(?:^|\\\\s)\" + quick[3] + \"(?:\\\\s|$)\" );\n\t\t}\n\t\treturn quick;\n\t},\n\tquickIs = function( elem, m ) {\n\t\tvar attrs = elem.attributes || {};\n\t\treturn (\n\t\t\t(!m[1] || elem.nodeName.toLowerCase() === m[1]) &&\n\t\t\t(!m[2] || (attrs.id || {}).value === m[2]) &&\n\t\t\t(!m[3] || m[3].test( (attrs[ \"class\" ] || {}).value ))\n\t\t);\n\t},\n\thoverHack = function( events ) {\n\t\treturn jQuery.event.special.hover ? events : events.replace( rhoverHack, \"mouseenter$1 mouseleave$1\" );\n\t};\n\n/*\n * Helper functions for managing events -- not part of the public interface.\n * Props to Dean Edwards' addEvent library for many of the ideas.\n */\njQuery.event = {\n\n\tadd: function( elem, types, handler, data, selector ) {\n\n\t\tvar elemData, eventHandle, events,\n\t\t\tt, tns, type, namespaces, handleObj,\n\t\t\thandleObjIn, quick, handlers, special;\n\n\t\t// Don't attach events to noData or text/comment nodes (allow plain objects tho)\n\t\tif ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Caller can pass in an object of custom data in lieu of the handler\n\t\tif ( handler.handler ) {\n\t\t\thandleObjIn = handler;\n\t\t\thandler = handleObjIn.handler;\n\t\t}\n\n\t\t// Make sure that the handler has a unique ID, used to find/remove it later\n\t\tif ( !handler.guid ) {\n\t\t\thandler.guid = jQuery.guid++;\n\t\t}\n\n\t\t// Init the element's event structure and main handler, if this is the first\n\t\tevents = elemData.events;\n\t\tif ( !events ) {\n\t\t\telemData.events = events = {};\n\t\t}\n\t\teventHandle = elemData.handle;\n\t\tif ( !eventHandle ) {\n\t\t\telemData.handle = eventHandle = function( e ) {\n\t\t\t\t// Discard the second event of a jQuery.event.trigger() and\n\t\t\t\t// when an event is called after a page has unloaded\n\t\t\t\treturn typeof jQuery !== \"undefined\" && (!e || jQuery.event.triggered !== e.type) ?\n\t\t\t\t\tjQuery.event.dispatch.apply( eventHandle.elem, arguments ) :\n\t\t\t\t\tundefined;\n\t\t\t};\n\t\t\t// Add elem as a property of the handle fn to prevent a memory leak with IE non-native events\n\t\t\teventHandle.elem = elem;\n\t\t}\n\n\t\t// Handle multiple events separated by a space\n\t\t// jQuery(...).bind(\"mouseover mouseout\", fn);\n\t\ttypes = jQuery.trim( hoverHack(types) ).split( \" \" );\n\t\tfor ( t = 0; t < types.length; t++ ) {\n\n\t\t\ttns = rtypenamespace.exec( types[t] ) || [];\n\t\t\ttype = tns[1];\n\t\t\tnamespaces = ( tns[2] || \"\" ).split( \".\" ).sort();\n\n\t\t\t// If event changes its type, use the special event handlers for the changed type\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\n\t\t\t// If selector defined, determine special event api type, otherwise given type\n\t\t\ttype = ( selector ? special.delegateType : special.bindType ) || type;\n\n\t\t\t// Update special based on newly reset type\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\n\t\t\t// handleObj is passed to all event handlers\n\t\t\thandleObj = jQuery.extend({\n\t\t\t\ttype: type,\n\t\t\t\torigType: tns[1],\n\t\t\t\tdata: data,\n\t\t\t\thandler: handler,\n\t\t\t\tguid: handler.guid,\n\t\t\t\tselector: selector,\n\t\t\t\tquick: quickParse( selector ),\n\t\t\t\tnamespace: namespaces.join(\".\")\n\t\t\t}, handleObjIn );\n\n\t\t\t// Init the event handler queue if we're the first\n\t\t\thandlers = events[ type ];\n\t\t\tif ( !handlers ) {\n\t\t\t\thandlers = events[ type ] = [];\n\t\t\t\thandlers.delegateCount = 0;\n\n\t\t\t\t// Only use addEventListener/attachEvent if the special events handler returns false\n\t\t\t\tif ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {\n\t\t\t\t\t// Bind the global event handler to the element\n\t\t\t\t\tif ( elem.addEventListener ) {\n\t\t\t\t\t\telem.addEventListener( type, eventHandle, false );\n\n\t\t\t\t\t} else if ( elem.attachEvent ) {\n\t\t\t\t\t\telem.attachEvent( \"on\" + type, eventHandle );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( special.add ) {\n\t\t\t\tspecial.add.call( elem, handleObj );\n\n\t\t\t\tif ( !handleObj.handler.guid ) {\n\t\t\t\t\thandleObj.handler.guid = handler.guid;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Add to the element's handler list, delegates in front\n\t\t\tif ( selector ) {\n\t\t\t\thandlers.splice( handlers.delegateCount++, 0, handleObj );\n\t\t\t} else {\n\t\t\t\thandlers.push( handleObj );\n\t\t\t}\n\n\t\t\t// Keep track of which events have ever been used, for event optimization\n\t\t\tjQuery.event.global[ type ] = true;\n\t\t}\n\n\t\t// Nullify elem to prevent memory leaks in IE\n\t\telem = null;\n\t},\n\n\tglobal: {},\n\n\t// Detach an event or set of events from an element\n\tremove: function( elem, types, handler, selector, mappedTypes ) {\n\n\t\tvar elemData = jQuery.hasData( elem ) && jQuery._data( elem ),\n\t\t\tt, tns, type, origType, namespaces, origCount,\n\t\t\tj, events, special, handle, eventType, handleObj;\n\n\t\tif ( !elemData || !(events = elemData.events) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Once for each type.namespace in types; type may be omitted\n\t\ttypes = jQuery.trim( hoverHack( types || \"\" ) ).split(\" \");\n\t\tfor ( t = 0; t < types.length; t++ ) {\n\t\t\ttns = rtypenamespace.exec( types[t] ) || [];\n\t\t\ttype = origType = tns[1];\n\t\t\tnamespaces = tns[2];\n\n\t\t\t// Unbind all events (on this namespace, if provided) for the element\n\t\t\tif ( !type ) {\n\t\t\t\tfor ( type in events ) {\n\t\t\t\t\tjQuery.event.remove( elem, type + types[ t ], handler, selector, true );\n\t\t\t\t}\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tspecial = jQuery.event.special[ type ] || {};\n\t\t\ttype = ( selector? special.delegateType : special.bindType ) || type;\n\t\t\teventType = events[ type ] || [];\n\t\t\torigCount = eventType.length;\n\t\t\tnamespaces = namespaces ? new RegExp(\"(^|\\\\.)\" + namespaces.split(\".\").sort().join(\"\\\\.(?:.*\\\\.)?\") + \"(\\\\.|$)\") : null;\n\n\t\t\t// Remove matching events\n\t\t\tfor ( j = 0; j < eventType.length; j++ ) {\n\t\t\t\thandleObj = eventType[ j ];\n\n\t\t\t\tif ( ( mappedTypes || origType === handleObj.origType ) &&\n\t\t\t\t\t ( !handler || handler.guid === handleObj.guid ) &&\n\t\t\t\t\t ( !namespaces || namespaces.test( handleObj.namespace ) ) &&\n\t\t\t\t\t ( !selector || selector === handleObj.selector || selector === \"**\" && handleObj.selector ) ) {\n\t\t\t\t\teventType.splice( j--, 1 );\n\n\t\t\t\t\tif ( handleObj.selector ) {\n\t\t\t\t\t\teventType.delegateCount--;\n\t\t\t\t\t}\n\t\t\t\t\tif ( special.remove ) {\n\t\t\t\t\t\tspecial.remove.call( elem, handleObj );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Remove generic event handler if we removed something and no more handlers exist\n\t\t\t// (avoids potential for endless recursion during removal of special event handlers)\n\t\t\tif ( eventType.length === 0 && origCount !== eventType.length ) {\n\t\t\t\tif ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) {\n\t\t\t\t\tjQuery.removeEvent( elem, type, elemData.handle );\n\t\t\t\t}\n\n\t\t\t\tdelete events[ type ];\n\t\t\t}\n\t\t}\n\n\t\t// Remove the expando if it's no longer used\n\t\tif ( jQuery.isEmptyObject( events ) ) {\n\t\t\thandle = elemData.handle;\n\t\t\tif ( handle ) {\n\t\t\t\thandle.elem = null;\n\t\t\t}\n\n\t\t\t// removeData also checks for emptiness and clears the expando if empty\n\t\t\t// so use it instead of delete\n\t\t\tjQuery.removeData( elem, [ \"events\", \"handle\" ], true );\n\t\t}\n\t},\n\n\t// Events that are safe to short-circuit if no handlers are attached.\n\t// Native DOM events should not be added, they may have inline handlers.\n\tcustomEvent: {\n\t\t\"getData\": true,\n\t\t\"setData\": true,\n\t\t\"changeData\": true\n\t},\n\n\ttrigger: function( event, data, elem, onlyHandlers ) {\n\t\t// Don't do events on text and comment nodes\n\t\tif ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Event object or event type\n\t\tvar type = event.type || event,\n\t\t\tnamespaces = [],\n\t\t\tcache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType;\n\n\t\t// focus/blur morphs to focusin/out; ensure we're not firing them right now\n\t\tif ( rfocusMorph.test( type + jQuery.event.triggered ) ) {\n\t\t\treturn;\n\t\t}\n\n\t\tif ( type.indexOf( \"!\" ) >= 0 ) {\n\t\t\t// Exclusive events trigger only for the exact event (no namespaces)\n\t\t\ttype = type.slice(0, -1);\n\t\t\texclusive = true;\n\t\t}\n\n\t\tif ( type.indexOf( \".\" ) >= 0 ) {\n\t\t\t// Namespaced trigger; create a regexp to match event type in handle()\n\t\t\tnamespaces = type.split(\".\");\n\t\t\ttype = namespaces.shift();\n\t\t\tnamespaces.sort();\n\t\t}\n\n\t\tif ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) {\n\t\t\t// No jQuery handlers for this event type, and it can't have inline handlers\n\t\t\treturn;\n\t\t}\n\n\t\t// Caller can pass in an Event, Object, or just an event type string\n\t\tevent = typeof event === \"object\" ?\n\t\t\t// jQuery.Event object\n\t\t\tevent[ jQuery.expando ] ? event :\n\t\t\t// Object literal\n\t\t\tnew jQuery.Event( type, event ) :\n\t\t\t// Just the event type (string)\n\t\t\tnew jQuery.Event( type );\n\n\t\tevent.type = type;\n\t\tevent.isTrigger = true;\n\t\tevent.exclusive = exclusive;\n\t\tevent.namespace = namespaces.join( \".\" );\n\t\tevent.namespace_re = event.namespace? new RegExp(\"(^|\\\\.)\" + namespaces.join(\"\\\\.(?:.*\\\\.)?\") + \"(\\\\.|$)\") : null;\n\t\tontype = type.indexOf( \":\" ) < 0 ? \"on\" + type : \"\";\n\n\t\t// Handle a global trigger\n\t\tif ( !elem ) {\n\n\t\t\t// TODO: Stop taunting the data cache; remove global events and always attach to document\n\t\t\tcache = jQuery.cache;\n\t\t\tfor ( i in cache ) {\n\t\t\t\tif ( cache[ i ].events && cache[ i ].events[ type ] ) {\n\t\t\t\t\tjQuery.event.trigger( event, data, cache[ i ].handle.elem, true );\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\t// Clean up the event in case it is being reused\n\t\tevent.result = undefined;\n\t\tif ( !event.target ) {\n\t\t\tevent.target = elem;\n\t\t}\n\n\t\t// Clone any incoming data and prepend the event, creating the handler arg list\n\t\tdata = data != null ? jQuery.makeArray( data ) : [];\n\t\tdata.unshift( event );\n\n\t\t// Allow special events to draw outside the lines\n\t\tspecial = jQuery.event.special[ type ] || {};\n\t\tif ( special.trigger && special.trigger.apply( elem, data ) === false ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Determine event propagation path in advance, per W3C events spec (#9951)\n\t\t// Bubble up to document, then to window; watch for a global ownerDocument var (#9724)\n\t\teventPath = [[ elem, special.bindType || type ]];\n\t\tif ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) {\n\n\t\t\tbubbleType = special.delegateType || type;\n\t\t\tcur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode;\n\t\t\told = null;\n\t\t\tfor ( ; cur; cur = cur.parentNode ) {\n\t\t\t\teventPath.push([ cur, bubbleType ]);\n\t\t\t\told = cur;\n\t\t\t}\n\n\t\t\t// Only add window if we got to document (e.g., not plain obj or detached DOM)\n\t\t\tif ( old && old === elem.ownerDocument ) {\n\t\t\t\teventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]);\n\t\t\t}\n\t\t}\n\n\t\t// Fire handlers on the event path\n\t\tfor ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) {\n\n\t\t\tcur = eventPath[i][0];\n\t\t\tevent.type = eventPath[i][1];\n\n\t\t\thandle = ( jQuery._data( cur, \"events\" ) || {} )[ event.type ] && jQuery._data( cur, \"handle\" );\n\t\t\tif ( handle ) {\n\t\t\t\thandle.apply( cur, data );\n\t\t\t}\n\t\t\t// Note that this is a bare JS function and not a jQuery handler\n\t\t\thandle = ontype && cur[ ontype ];\n\t\t\tif ( handle && jQuery.acceptData( cur ) && handle.apply( cur, data ) === false ) {\n\t\t\t\tevent.preventDefault();\n\t\t\t}\n\t\t}\n\t\tevent.type = type;\n\n\t\t// If nobody prevented the default action, do it now\n\t\tif ( !onlyHandlers && !event.isDefaultPrevented() ) {\n\n\t\t\tif ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) &&\n\t\t\t\t!(type === \"click\" && jQuery.nodeName( elem, \"a\" )) && jQuery.acceptData( elem ) ) {\n\n\t\t\t\t// Call a native DOM method on the target with the same name name as the event.\n\t\t\t\t// Can't use an .isFunction() check here because IE6/7 fails that test.\n\t\t\t\t// Don't do default actions on window, that's where global variables be (#6170)\n\t\t\t\t// IE<9 dies on focus/blur to hidden element (#1486)\n\t\t\t\tif ( ontype && elem[ type ] && ((type !== \"focus\" && type !== \"blur\") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) {\n\n\t\t\t\t\t// Don't re-trigger an onFOO event when we call its FOO() method\n\t\t\t\t\told = elem[ ontype ];\n\n\t\t\t\t\tif ( old ) {\n\t\t\t\t\t\telem[ ontype ] = null;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Prevent re-triggering of the same event, since we already bubbled it above\n\t\t\t\t\tjQuery.event.triggered = type;\n\t\t\t\t\telem[ type ]();\n\t\t\t\t\tjQuery.event.triggered = undefined;\n\n\t\t\t\t\tif ( old ) {\n\t\t\t\t\t\telem[ ontype ] = old;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn event.result;\n\t},\n\n\tdispatch: function( event ) {\n\n\t\t// Make a writable jQuery.Event from the native event object\n\t\tevent = jQuery.event.fix( event || window.event );\n\n\t\tvar handlers = ( (jQuery._data( this, \"events\" ) || {} )[ event.type ] || []),\n\t\t\tdelegateCount = handlers.delegateCount,\n\t\t\targs = [].slice.call( arguments, 0 ),\n\t\t\trun_all = !event.exclusive && !event.namespace,\n\t\t\thandlerQueue = [],\n\t\t\ti, j, cur, jqcur, ret, selMatch, matched, matches, handleObj, sel, related;\n\n\t\t// Use the fix-ed jQuery.Event rather than the (read-only) native event\n\t\targs[0] = event;\n\t\tevent.delegateTarget = this;\n\n\t\t// Determine handlers that should run if there are delegated events\n\t\t// Avoid disabled elements in IE (#6911) and non-left-click bubbling in Firefox (#3861)\n\t\tif ( delegateCount && !event.target.disabled && !(event.button && event.type === \"click\") ) {\n\n\t\t\t// Pregenerate a single jQuery object for reuse with .is()\n\t\t\tjqcur = jQuery(this);\n\t\t\tjqcur.context = this.ownerDocument || this;\n\n\t\t\tfor ( cur = event.target; cur != this; cur = cur.parentNode || this ) {\n\t\t\t\tselMatch = {};\n\t\t\t\tmatches = [];\n\t\t\t\tjqcur[0] = cur;\n\t\t\t\tfor ( i = 0; i < delegateCount; i++ ) {\n\t\t\t\t\thandleObj = handlers[ i ];\n\t\t\t\t\tsel = handleObj.selector;\n\n\t\t\t\t\tif ( selMatch[ sel ] === undefined ) {\n\t\t\t\t\t\tselMatch[ sel ] = (\n\t\t\t\t\t\t\thandleObj.quick ? quickIs( cur, handleObj.quick ) : jqcur.is( sel )\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t\tif ( selMatch[ sel ] ) {\n\t\t\t\t\t\tmatches.push( handleObj );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif ( matches.length ) {\n\t\t\t\t\thandlerQueue.push({ elem: cur, matches: matches });\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Add the remaining (directly-bound) handlers\n\t\tif ( handlers.length > delegateCount ) {\n\t\t\thandlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) });\n\t\t}\n\n\t\t// Run delegates first; they may want to stop propagation beneath us\n\t\tfor ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) {\n\t\t\tmatched = handlerQueue[ i ];\n\t\t\tevent.currentTarget = matched.elem;\n\n\t\t\tfor ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) {\n\t\t\t\thandleObj = matched.matches[ j ];\n\n\t\t\t\t// Triggered event must either 1) be non-exclusive and have no namespace, or\n\t\t\t\t// 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace).\n\t\t\t\tif ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) {\n\n\t\t\t\t\tevent.data = handleObj.data;\n\t\t\t\t\tevent.handleObj = handleObj;\n\n\t\t\t\t\tret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )\n\t\t\t\t\t\t\t.apply( matched.elem, args );\n\n\t\t\t\t\tif ( ret !== undefined ) {\n\t\t\t\t\t\tevent.result = ret;\n\t\t\t\t\t\tif ( ret === false ) {\n\t\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\t\tevent.stopPropagation();\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn event.result;\n\t},\n\n\t// Includes some event props shared by KeyEvent and MouseEvent\n\t// *** attrChange attrName relatedNode srcElement  are not normalized, non-W3C, deprecated, will be removed in 1.8 ***\n\tprops: \"attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which\".split(\" \"),\n\n\tfixHooks: {},\n\n\tkeyHooks: {\n\t\tprops: \"char charCode key keyCode\".split(\" \"),\n\t\tfilter: function( event, original ) {\n\n\t\t\t// Add which for key events\n\t\t\tif ( event.which == null ) {\n\t\t\t\tevent.which = original.charCode != null ? original.charCode : original.keyCode;\n\t\t\t}\n\n\t\t\treturn event;\n\t\t}\n\t},\n\n\tmouseHooks: {\n\t\tprops: \"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement\".split(\" \"),\n\t\tfilter: function( event, original ) {\n\t\t\tvar eventDoc, doc, body,\n\t\t\t\tbutton = original.button,\n\t\t\t\tfromElement = original.fromElement;\n\n\t\t\t// Calculate pageX/Y if missing and clientX/Y available\n\t\t\tif ( event.pageX == null && original.clientX != null ) {\n\t\t\t\teventDoc = event.target.ownerDocument || document;\n\t\t\t\tdoc = eventDoc.documentElement;\n\t\t\t\tbody = eventDoc.body;\n\n\t\t\t\tevent.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 );\n\t\t\t\tevent.pageY = original.clientY + ( doc && doc.scrollTop  || body && body.scrollTop  || 0 ) - ( doc && doc.clientTop  || body && body.clientTop  || 0 );\n\t\t\t}\n\n\t\t\t// Add relatedTarget, if necessary\n\t\t\tif ( !event.relatedTarget && fromElement ) {\n\t\t\t\tevent.relatedTarget = fromElement === event.target ? original.toElement : fromElement;\n\t\t\t}\n\n\t\t\t// Add which for click: 1 === left; 2 === middle; 3 === right\n\t\t\t// Note: button is not normalized, so don't use it\n\t\t\tif ( !event.which && button !== undefined ) {\n\t\t\t\tevent.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) );\n\t\t\t}\n\n\t\t\treturn event;\n\t\t}\n\t},\n\n\tfix: function( event ) {\n\t\tif ( event[ jQuery.expando ] ) {\n\t\t\treturn event;\n\t\t}\n\n\t\t// Create a writable copy of the event object and normalize some properties\n\t\tvar i, prop,\n\t\t\toriginalEvent = event,\n\t\t\tfixHook = jQuery.event.fixHooks[ event.type ] || {},\n\t\t\tcopy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;\n\n\t\tevent = jQuery.Event( originalEvent );\n\n\t\tfor ( i = copy.length; i; ) {\n\t\t\tprop = copy[ --i ];\n\t\t\tevent[ prop ] = originalEvent[ prop ];\n\t\t}\n\n\t\t// Fix target property, if necessary (#1925, IE 6/7/8 & Safari2)\n\t\tif ( !event.target ) {\n\t\t\tevent.target = originalEvent.srcElement || document;\n\t\t}\n\n\t\t// Target should not be a text node (#504, Safari)\n\t\tif ( event.target.nodeType === 3 ) {\n\t\t\tevent.target = event.target.parentNode;\n\t\t}\n\n\t\t// For mouse/key events; add metaKey if it's not there (#3368, IE6/7/8)\n\t\tif ( event.metaKey === undefined ) {\n\t\t\tevent.metaKey = event.ctrlKey;\n\t\t}\n\n\t\treturn fixHook.filter? fixHook.filter( event, originalEvent ) : event;\n\t},\n\n\tspecial: {\n\t\tready: {\n\t\t\t// Make sure the ready event is setup\n\t\t\tsetup: jQuery.bindReady\n\t\t},\n\n\t\tload: {\n\t\t\t// Prevent triggered image.load events from bubbling to window.load\n\t\t\tnoBubble: true\n\t\t},\n\n\t\tfocus: {\n\t\t\tdelegateType: \"focusin\"\n\t\t},\n\t\tblur: {\n\t\t\tdelegateType: \"focusout\"\n\t\t},\n\n\t\tbeforeunload: {\n\t\t\tsetup: function( data, namespaces, eventHandle ) {\n\t\t\t\t// We only want to do this special case on windows\n\t\t\t\tif ( jQuery.isWindow( this ) ) {\n\t\t\t\t\tthis.onbeforeunload = eventHandle;\n\t\t\t\t}\n\t\t\t},\n\n\t\t\tteardown: function( namespaces, eventHandle ) {\n\t\t\t\tif ( this.onbeforeunload === eventHandle ) {\n\t\t\t\t\tthis.onbeforeunload = null;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\n\tsimulate: function( type, elem, event, bubble ) {\n\t\t// Piggyback on a donor event to simulate a different one.\n\t\t// Fake originalEvent to avoid donor's stopPropagation, but if the\n\t\t// simulated event prevents default then we do the same on the donor.\n\t\tvar e = jQuery.extend(\n\t\t\tnew jQuery.Event(),\n\t\t\tevent,\n\t\t\t{ type: type,\n\t\t\t\tisSimulated: true,\n\t\t\t\toriginalEvent: {}\n\t\t\t}\n\t\t);\n\t\tif ( bubble ) {\n\t\t\tjQuery.event.trigger( e, null, elem );\n\t\t} else {\n\t\t\tjQuery.event.dispatch.call( elem, e );\n\t\t}\n\t\tif ( e.isDefaultPrevented() ) {\n\t\t\tevent.preventDefault();\n\t\t}\n\t}\n};\n\n// Some plugins are using, but it's undocumented/deprecated and will be removed.\n// The 1.7 special event interface should provide all the hooks needed now.\njQuery.event.handle = jQuery.event.dispatch;\n\njQuery.removeEvent = document.removeEventListener ?\n\tfunction( elem, type, handle ) {\n\t\tif ( elem.removeEventListener ) {\n\t\t\telem.removeEventListener( type, handle, false );\n\t\t}\n\t} :\n\tfunction( elem, type, handle ) {\n\t\tif ( elem.detachEvent ) {\n\t\t\telem.detachEvent( \"on\" + type, handle );\n\t\t}\n\t};\n\njQuery.Event = function( src, props ) {\n\t// Allow instantiation without the 'new' keyword\n\tif ( !(this instanceof jQuery.Event) ) {\n\t\treturn new jQuery.Event( src, props );\n\t}\n\n\t// Event object\n\tif ( src && src.type ) {\n\t\tthis.originalEvent = src;\n\t\tthis.type = src.type;\n\n\t\t// Events bubbling up the document may have been marked as prevented\n\t\t// by a handler lower down the tree; reflect the correct value.\n\t\tthis.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false ||\n\t\t\tsrc.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse;\n\n\t// Event type\n\t} else {\n\t\tthis.type = src;\n\t}\n\n\t// Put explicitly provided properties onto the event object\n\tif ( props ) {\n\t\tjQuery.extend( this, props );\n\t}\n\n\t// Create a timestamp if incoming event doesn't have one\n\tthis.timeStamp = src && src.timeStamp || jQuery.now();\n\n\t// Mark it as fixed\n\tthis[ jQuery.expando ] = true;\n};\n\nfunction returnFalse() {\n\treturn false;\n}\nfunction returnTrue() {\n\treturn true;\n}\n\n// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding\n// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html\njQuery.Event.prototype = {\n\tpreventDefault: function() {\n\t\tthis.isDefaultPrevented = returnTrue;\n\n\t\tvar e = this.originalEvent;\n\t\tif ( !e ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// if preventDefault exists run it on the original event\n\t\tif ( e.preventDefault ) {\n\t\t\te.preventDefault();\n\n\t\t// otherwise set the returnValue property of the original event to false (IE)\n\t\t} else {\n\t\t\te.returnValue = false;\n\t\t}\n\t},\n\tstopPropagation: function() {\n\t\tthis.isPropagationStopped = returnTrue;\n\n\t\tvar e = this.originalEvent;\n\t\tif ( !e ) {\n\t\t\treturn;\n\t\t}\n\t\t// if stopPropagation exists run it on the original event\n\t\tif ( e.stopPropagation ) {\n\t\t\te.stopPropagation();\n\t\t}\n\t\t// otherwise set the cancelBubble property of the original event to true (IE)\n\t\te.cancelBubble = true;\n\t},\n\tstopImmediatePropagation: function() {\n\t\tthis.isImmediatePropagationStopped = returnTrue;\n\t\tthis.stopPropagation();\n\t},\n\tisDefaultPrevented: returnFalse,\n\tisPropagationStopped: returnFalse,\n\tisImmediatePropagationStopped: returnFalse\n};\n\n// Create mouseenter/leave events using mouseover/out and event-time checks\njQuery.each({\n\tmouseenter: \"mouseover\",\n\tmouseleave: \"mouseout\"\n}, function( orig, fix ) {\n\tjQuery.event.special[ orig ] = {\n\t\tdelegateType: fix,\n\t\tbindType: fix,\n\n\t\thandle: function( event ) {\n\t\t\tvar target = this,\n\t\t\t\trelated = event.relatedTarget,\n\t\t\t\thandleObj = event.handleObj,\n\t\t\t\tselector = handleObj.selector,\n\t\t\t\tret;\n\n\t\t\t// For mousenter/leave call the handler if related is outside the target.\n\t\t\t// NB: No relatedTarget if the mouse left/entered the browser window\n\t\t\tif ( !related || (related !== target && !jQuery.contains( target, related )) ) {\n\t\t\t\tevent.type = handleObj.origType;\n\t\t\t\tret = handleObj.handler.apply( this, arguments );\n\t\t\t\tevent.type = fix;\n\t\t\t}\n\t\t\treturn ret;\n\t\t}\n\t};\n});\n\n// IE submit delegation\nif ( !jQuery.support.submitBubbles ) {\n\n\tjQuery.event.special.submit = {\n\t\tsetup: function() {\n\t\t\t// Only need this for delegated form submit events\n\t\t\tif ( jQuery.nodeName( this, \"form\" ) ) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// Lazy-add a submit handler when a descendant form may potentially be submitted\n\t\t\tjQuery.event.add( this, \"click._submit keypress._submit\", function( e ) {\n\t\t\t\t// Node name check avoids a VML-related crash in IE (#9807)\n\t\t\t\tvar elem = e.target,\n\t\t\t\t\tform = jQuery.nodeName( elem, \"input\" ) || jQuery.nodeName( elem, \"button\" ) ? elem.form : undefined;\n\t\t\t\tif ( form && !form._submit_attached ) {\n\t\t\t\t\tjQuery.event.add( form, \"submit._submit\", function( event ) {\n\t\t\t\t\t\t// If form was submitted by the user, bubble the event up the tree\n\t\t\t\t\t\tif ( this.parentNode && !event.isTrigger ) {\n\t\t\t\t\t\t\tjQuery.event.simulate( \"submit\", this.parentNode, event, true );\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t\tform._submit_attached = true;\n\t\t\t\t}\n\t\t\t});\n\t\t\t// return undefined since we don't need an event listener\n\t\t},\n\n\t\tteardown: function() {\n\t\t\t// Only need this for delegated form submit events\n\t\t\tif ( jQuery.nodeName( this, \"form\" ) ) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\t// Remove delegated handlers; cleanData eventually reaps submit handlers attached above\n\t\t\tjQuery.event.remove( this, \"._submit\" );\n\t\t}\n\t};\n}\n\n// IE change delegation and checkbox/radio fix\nif ( !jQuery.support.changeBubbles ) {\n\n\tjQuery.event.special.change = {\n\n\t\tsetup: function() {\n\n\t\t\tif ( rformElems.test( this.nodeName ) ) {\n\t\t\t\t// IE doesn't fire change on a check/radio until blur; trigger it on click\n\t\t\t\t// after a propertychange. Eat the blur-change in special.change.handle.\n\t\t\t\t// This still fires onchange a second time for check/radio after blur.\n\t\t\t\tif ( this.type === \"checkbox\" || this.type === \"radio\" ) {\n\t\t\t\t\tjQuery.event.add( this, \"propertychange._change\", function( event ) {\n\t\t\t\t\t\tif ( event.originalEvent.propertyName === \"checked\" ) {\n\t\t\t\t\t\t\tthis._just_changed = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t\tjQuery.event.add( this, \"click._change\", function( event ) {\n\t\t\t\t\t\tif ( this._just_changed && !event.isTrigger ) {\n\t\t\t\t\t\t\tthis._just_changed = false;\n\t\t\t\t\t\t\tjQuery.event.simulate( \"change\", this, event, true );\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t// Delegated event; lazy-add a change handler on descendant inputs\n\t\t\tjQuery.event.add( this, \"beforeactivate._change\", function( e ) {\n\t\t\t\tvar elem = e.target;\n\n\t\t\t\tif ( rformElems.test( elem.nodeName ) && !elem._change_attached ) {\n\t\t\t\t\tjQuery.event.add( elem, \"change._change\", function( event ) {\n\t\t\t\t\t\tif ( this.parentNode && !event.isSimulated && !event.isTrigger ) {\n\t\t\t\t\t\t\tjQuery.event.simulate( \"change\", this.parentNode, event, true );\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t\t\telem._change_attached = true;\n\t\t\t\t}\n\t\t\t});\n\t\t},\n\n\t\thandle: function( event ) {\n\t\t\tvar elem = event.target;\n\n\t\t\t// Swallow native change events from checkbox/radio, we already triggered them above\n\t\t\tif ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== \"radio\" && elem.type !== \"checkbox\") ) {\n\t\t\t\treturn event.handleObj.handler.apply( this, arguments );\n\t\t\t}\n\t\t},\n\n\t\tteardown: function() {\n\t\t\tjQuery.event.remove( this, \"._change\" );\n\n\t\t\treturn rformElems.test( this.nodeName );\n\t\t}\n\t};\n}\n\n// Create \"bubbling\" focus and blur events\nif ( !jQuery.support.focusinBubbles ) {\n\tjQuery.each({ focus: \"focusin\", blur: \"focusout\" }, function( orig, fix ) {\n\n\t\t// Attach a single capturing handler while someone wants focusin/focusout\n\t\tvar attaches = 0,\n\t\t\thandler = function( event ) {\n\t\t\t\tjQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true );\n\t\t\t};\n\n\t\tjQuery.event.special[ fix ] = {\n\t\t\tsetup: function() {\n\t\t\t\tif ( attaches++ === 0 ) {\n\t\t\t\t\tdocument.addEventListener( orig, handler, true );\n\t\t\t\t}\n\t\t\t},\n\t\t\tteardown: function() {\n\t\t\t\tif ( --attaches === 0 ) {\n\t\t\t\t\tdocument.removeEventListener( orig, handler, true );\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t});\n}\n\njQuery.fn.extend({\n\n\ton: function( types, selector, data, fn, /*INTERNAL*/ one ) {\n\t\tvar origFn, type;\n\n\t\t// Types can be a map of types/handlers\n\t\tif ( typeof types === \"object\" ) {\n\t\t\t// ( types-Object, selector, data )\n\t\t\tif ( typeof selector !== \"string\" ) {\n\t\t\t\t// ( types-Object, data )\n\t\t\t\tdata = selector;\n\t\t\t\tselector = undefined;\n\t\t\t}\n\t\t\tfor ( type in types ) {\n\t\t\t\tthis.on( type, selector, data, types[ type ], one );\n\t\t\t}\n\t\t\treturn this;\n\t\t}\n\n\t\tif ( data == null && fn == null ) {\n\t\t\t// ( types, fn )\n\t\t\tfn = selector;\n\t\t\tdata = selector = undefined;\n\t\t} else if ( fn == null ) {\n\t\t\tif ( typeof selector === \"string\" ) {\n\t\t\t\t// ( types, selector, fn )\n\t\t\t\tfn = data;\n\t\t\t\tdata = undefined;\n\t\t\t} else {\n\t\t\t\t// ( types, data, fn )\n\t\t\t\tfn = data;\n\t\t\t\tdata = selector;\n\t\t\t\tselector = undefined;\n\t\t\t}\n\t\t}\n\t\tif ( fn === false ) {\n\t\t\tfn = returnFalse;\n\t\t} else if ( !fn ) {\n\t\t\treturn this;\n\t\t}\n\n\t\tif ( one === 1 ) {\n\t\t\torigFn = fn;\n\t\t\tfn = function( event ) {\n\t\t\t\t// Can use an empty set, since event contains the info\n\t\t\t\tjQuery().off( event );\n\t\t\t\treturn origFn.apply( this, arguments );\n\t\t\t};\n\t\t\t// Use same guid so caller can remove using origFn\n\t\t\tfn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );\n\t\t}\n\t\treturn this.each( function() {\n\t\t\tjQuery.event.add( this, types, fn, data, selector );\n\t\t});\n\t},\n\tone: function( types, selector, data, fn ) {\n\t\treturn this.on.call( this, types, selector, data, fn, 1 );\n\t},\n\toff: function( types, selector, fn ) {\n\t\tif ( types && types.preventDefault && types.handleObj ) {\n\t\t\t// ( event )  dispatched jQuery.Event\n\t\t\tvar handleObj = types.handleObj;\n\t\t\tjQuery( types.delegateTarget ).off(\n\t\t\t\thandleObj.namespace? handleObj.type + \".\" + handleObj.namespace : handleObj.type,\n\t\t\t\thandleObj.selector,\n\t\t\t\thandleObj.handler\n\t\t\t);\n\t\t\treturn this;\n\t\t}\n\t\tif ( typeof types === \"object\" ) {\n\t\t\t// ( types-object [, selector] )\n\t\t\tfor ( var type in types ) {\n\t\t\t\tthis.off( type, selector, types[ type ] );\n\t\t\t}\n\t\t\treturn this;\n\t\t}\n\t\tif ( selector === false || typeof selector === \"function\" ) {\n\t\t\t// ( types [, fn] )\n\t\t\tfn = selector;\n\t\t\tselector = undefined;\n\t\t}\n\t\tif ( fn === false ) {\n\t\t\tfn = returnFalse;\n\t\t}\n\t\treturn this.each(function() {\n\t\t\tjQuery.event.remove( this, types, fn, selector );\n\t\t});\n\t},\n\n\tbind: function( types, data, fn ) {\n\t\treturn this.on( types, null, data, fn );\n\t},\n\tunbind: function( types, fn ) {\n\t\treturn this.off( types, null, fn );\n\t},\n\n\tlive: function( types, data, fn ) {\n\t\tjQuery( this.context ).on( types, this.selector, data, fn );\n\t\treturn this;\n\t},\n\tdie: function( types, fn ) {\n\t\tjQuery( this.context ).off( types, this.selector || \"**\", fn );\n\t\treturn this;\n\t},\n\n\tdelegate: function( selector, types, data, fn ) {\n\t\treturn this.on( types, selector, data, fn );\n\t},\n\tundelegate: function( selector, types, fn ) {\n\t\t// ( namespace ) or ( selector, types [, fn] )\n\t\treturn arguments.length == 1? this.off( selector, \"**\" ) : this.off( types, selector, fn );\n\t},\n\n\ttrigger: function( type, data ) {\n\t\treturn this.each(function() {\n\t\t\tjQuery.event.trigger( type, data, this );\n\t\t});\n\t},\n\ttriggerHandler: function( type, data ) {\n\t\tif ( this[0] ) {\n\t\t\treturn jQuery.event.trigger( type, data, this[0], true );\n\t\t}\n\t},\n\n\ttoggle: function( fn ) {\n\t\t// Save reference to arguments for access in closure\n\t\tvar args = arguments,\n\t\t\tguid = fn.guid || jQuery.guid++,\n\t\t\ti = 0,\n\t\t\ttoggler = function( event ) {\n\t\t\t\t// Figure out which function to execute\n\t\t\t\tvar lastToggle = ( jQuery._data( this, \"lastToggle\" + fn.guid ) || 0 ) % i;\n\t\t\t\tjQuery._data( this, \"lastToggle\" + fn.guid, lastToggle + 1 );\n\n\t\t\t\t// Make sure that clicks stop\n\t\t\t\tevent.preventDefault();\n\n\t\t\t\t// and execute the function\n\t\t\t\treturn args[ lastToggle ].apply( this, arguments ) || false;\n\t\t\t};\n\n\t\t// link all the functions, so any of them can unbind this click handler\n\t\ttoggler.guid = guid;\n\t\twhile ( i < args.length ) {\n\t\t\targs[ i++ ].guid = guid;\n\t\t}\n\n\t\treturn this.click( toggler );\n\t},\n\n\thover: function( fnOver, fnOut ) {\n\t\treturn this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );\n\t}\n});\n\njQuery.each( (\"blur focus focusin focusout load resize scroll unload click dblclick \" +\n\t\"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave \" +\n\t\"change select submit keydown keypress keyup error contextmenu\").split(\" \"), function( i, name ) {\n\n\t// Handle event binding\n\tjQuery.fn[ name ] = function( data, fn ) {\n\t\tif ( fn == null ) {\n\t\t\tfn = data;\n\t\t\tdata = null;\n\t\t}\n\n\t\treturn arguments.length > 0 ?\n\t\t\tthis.on( name, null, data, fn ) :\n\t\t\tthis.trigger( name );\n\t};\n\n\tif ( jQuery.attrFn ) {\n\t\tjQuery.attrFn[ name ] = true;\n\t}\n\n\tif ( rkeyEvent.test( name ) ) {\n\t\tjQuery.event.fixHooks[ name ] = jQuery.event.keyHooks;\n\t}\n\n\tif ( rmouseEvent.test( name ) ) {\n\t\tjQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks;\n\t}\n});\n\n\n\n/*!\n * Sizzle CSS Selector Engine\n *  Copyright 2016, The Dojo Foundation\n *  Released under the MIT, BSD, and GPL Licenses.\n *  More information: http://sizzlejs.com/\n */\n(function(){\n\nvar chunker = /((?:\\((?:\\([^()]+\\)|[^()]+)+\\)|\\[(?:\\[[^\\[\\]]*\\]|['\"][^'\"]*['\"]|[^\\[\\]'\"]+)+\\]|\\\\.|[^ >+~,(\\[\\\\]+)+|[>+~])(\\s*,\\s*)?((?:.|\\r|\\n)*)/g,\n\texpando = \"sizcache\" + (Math.random() + '').replace('.', ''),\n\tdone = 0,\n\ttoString = Object.prototype.toString,\n\thasDuplicate = false,\n\tbaseHasDuplicate = true,\n\trBackslash = /\\\\/g,\n\trReturn = /\\r\\n/g,\n\trNonWord = /\\W/;\n\n// Here we check if the JavaScript engine is using some sort of\n// optimization where it does not always call our comparison\n// function. If that is the case, discard the hasDuplicate value.\n//   Thus far that includes Google Chrome.\n[0, 0].sort(function() {\n\tbaseHasDuplicate = false;\n\treturn 0;\n});\n\nvar Sizzle = function( selector, context, results, seed ) {\n\tresults = results || [];\n\tcontext = context || document;\n\n\tvar origContext = context;\n\n\tif ( context.nodeType !== 1 && context.nodeType !== 9 ) {\n\t\treturn [];\n\t}\n\t\n\tif ( !selector || typeof selector !== \"string\" ) {\n\t\treturn results;\n\t}\n\n\tvar m, set, checkSet, extra, ret, cur, pop, i,\n\t\tprune = true,\n\t\tcontextXML = Sizzle.isXML( context ),\n\t\tparts = [],\n\t\tsoFar = selector;\n\t\n\t// Reset the position of the chunker regexp (start from head)\n\tdo {\n\t\tchunker.exec( \"\" );\n\t\tm = chunker.exec( soFar );\n\n\t\tif ( m ) {\n\t\t\tsoFar = m[3];\n\t\t\n\t\t\tparts.push( m[1] );\n\t\t\n\t\t\tif ( m[2] ) {\n\t\t\t\textra = m[3];\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t} while ( m );\n\n\tif ( parts.length > 1 && origPOS.exec( selector ) ) {\n\n\t\tif ( parts.length === 2 && Expr.relative[ parts[0] ] ) {\n\t\t\tset = posProcess( parts[0] + parts[1], context, seed );\n\n\t\t} else {\n\t\t\tset = Expr.relative[ parts[0] ] ?\n\t\t\t\t[ context ] :\n\t\t\t\tSizzle( parts.shift(), context );\n\n\t\t\twhile ( parts.length ) {\n\t\t\t\tselector = parts.shift();\n\n\t\t\t\tif ( Expr.relative[ selector ] ) {\n\t\t\t\t\tselector += parts.shift();\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tset = posProcess( selector, set, seed );\n\t\t\t}\n\t\t}\n\n\t} else {\n\t\t// Take a shortcut and set the context if the root selector is an ID\n\t\t// (but not if it'll be faster if the inner selector is an ID)\n\t\tif ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML &&\n\t\t\t\tExpr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) {\n\n\t\t\tret = Sizzle.find( parts.shift(), context, contextXML );\n\t\t\tcontext = ret.expr ?\n\t\t\t\tSizzle.filter( ret.expr, ret.set )[0] :\n\t\t\t\tret.set[0];\n\t\t}\n\n\t\tif ( context ) {\n\t\t\tret = seed ?\n\t\t\t\t{ expr: parts.pop(), set: makeArray(seed) } :\n\t\t\t\tSizzle.find( parts.pop(), parts.length === 1 && (parts[0] === \"~\" || parts[0] === \"+\") && context.parentNode ? context.parentNode : context, contextXML );\n\n\t\t\tset = ret.expr ?\n\t\t\t\tSizzle.filter( ret.expr, ret.set ) :\n\t\t\t\tret.set;\n\n\t\t\tif ( parts.length > 0 ) {\n\t\t\t\tcheckSet = makeArray( set );\n\n\t\t\t} else {\n\t\t\t\tprune = false;\n\t\t\t}\n\n\t\t\twhile ( parts.length ) {\n\t\t\t\tcur = parts.pop();\n\t\t\t\tpop = cur;\n\n\t\t\t\tif ( !Expr.relative[ cur ] ) {\n\t\t\t\t\tcur = \"\";\n\t\t\t\t} else {\n\t\t\t\t\tpop = parts.pop();\n\t\t\t\t}\n\n\t\t\t\tif ( pop == null ) {\n\t\t\t\t\tpop = context;\n\t\t\t\t}\n\n\t\t\t\tExpr.relative[ cur ]( checkSet, pop, contextXML );\n\t\t\t}\n\n\t\t} else {\n\t\t\tcheckSet = parts = [];\n\t\t}\n\t}\n\n\tif ( !checkSet ) {\n\t\tcheckSet = set;\n\t}\n\n\tif ( !checkSet ) {\n\t\tSizzle.error( cur || selector );\n\t}\n\n\tif ( toString.call(checkSet) === \"[object Array]\" ) {\n\t\tif ( !prune ) {\n\t\t\tresults.push.apply( results, checkSet );\n\n\t\t} else if ( context && context.nodeType === 1 ) {\n\t\t\tfor ( i = 0; checkSet[i] != null; i++ ) {\n\t\t\t\tif ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) {\n\t\t\t\t\tresults.push( set[i] );\n\t\t\t\t}\n\t\t\t}\n\n\t\t} else {\n\t\t\tfor ( i = 0; checkSet[i] != null; i++ ) {\n\t\t\t\tif ( checkSet[i] && checkSet[i].nodeType === 1 ) {\n\t\t\t\t\tresults.push( set[i] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t} else {\n\t\tmakeArray( checkSet, results );\n\t}\n\n\tif ( extra ) {\n\t\tSizzle( extra, origContext, results, seed );\n\t\tSizzle.uniqueSort( results );\n\t}\n\n\treturn results;\n};\n\nSizzle.uniqueSort = function( results ) {\n\tif ( sortOrder ) {\n\t\thasDuplicate = baseHasDuplicate;\n\t\tresults.sort( sortOrder );\n\n\t\tif ( hasDuplicate ) {\n\t\t\tfor ( var i = 1; i < results.length; i++ ) {\n\t\t\t\tif ( results[i] === results[ i - 1 ] ) {\n\t\t\t\t\tresults.splice( i--, 1 );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn results;\n};\n\nSizzle.matches = function( expr, set ) {\n\treturn Sizzle( expr, null, null, set );\n};\n\nSizzle.matchesSelector = function( node, expr ) {\n\treturn Sizzle( expr, null, null, [node] ).length > 0;\n};\n\nSizzle.find = function( expr, context, isXML ) {\n\tvar set, i, len, match, type, left;\n\n\tif ( !expr ) {\n\t\treturn [];\n\t}\n\n\tfor ( i = 0, len = Expr.order.length; i < len; i++ ) {\n\t\ttype = Expr.order[i];\n\t\t\n\t\tif ( (match = Expr.leftMatch[ type ].exec( expr )) ) {\n\t\t\tleft = match[1];\n\t\t\tmatch.splice( 1, 1 );\n\n\t\t\tif ( left.substr( left.length - 1 ) !== \"\\\\\" ) {\n\t\t\t\tmatch[1] = (match[1] || \"\").replace( rBackslash, \"\" );\n\t\t\t\tset = Expr.find[ type ]( match, context, isXML );\n\n\t\t\t\tif ( set != null ) {\n\t\t\t\t\texpr = expr.replace( Expr.match[ type ], \"\" );\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif ( !set ) {\n\t\tset = typeof context.getElementsByTagName !== \"undefined\" ?\n\t\t\tcontext.getElementsByTagName( \"*\" ) :\n\t\t\t[];\n\t}\n\n\treturn { set: set, expr: expr };\n};\n\nSizzle.filter = function( expr, set, inplace, not ) {\n\tvar match, anyFound,\n\t\ttype, found, item, filter, left,\n\t\ti, pass,\n\t\told = expr,\n\t\tresult = [],\n\t\tcurLoop = set,\n\t\tisXMLFilter = set && set[0] && Sizzle.isXML( set[0] );\n\n\twhile ( expr && set.length ) {\n\t\tfor ( type in Expr.filter ) {\n\t\t\tif ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) {\n\t\t\t\tfilter = Expr.filter[ type ];\n\t\t\t\tleft = match[1];\n\n\t\t\t\tanyFound = false;\n\n\t\t\t\tmatch.splice(1,1);\n\n\t\t\t\tif ( left.substr( left.length - 1 ) === \"\\\\\" ) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif ( curLoop === result ) {\n\t\t\t\t\tresult = [];\n\t\t\t\t}\n\n\t\t\t\tif ( Expr.preFilter[ type ] ) {\n\t\t\t\t\tmatch = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter );\n\n\t\t\t\t\tif ( !match ) {\n\t\t\t\t\t\tanyFound = found = true;\n\n\t\t\t\t\t} else if ( match === true ) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif ( match ) {\n\t\t\t\t\tfor ( i = 0; (item = curLoop[i]) != null; i++ ) {\n\t\t\t\t\t\tif ( item ) {\n\t\t\t\t\t\t\tfound = filter( item, match, i, curLoop );\n\t\t\t\t\t\t\tpass = not ^ found;\n\n\t\t\t\t\t\t\tif ( inplace && found != null ) {\n\t\t\t\t\t\t\t\tif ( pass ) {\n\t\t\t\t\t\t\t\t\tanyFound = true;\n\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tcurLoop[i] = false;\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t} else if ( pass ) {\n\t\t\t\t\t\t\t\tresult.push( item );\n\t\t\t\t\t\t\t\tanyFound = true;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif ( found !== undefined ) {\n\t\t\t\t\tif ( !inplace ) {\n\t\t\t\t\t\tcurLoop = result;\n\t\t\t\t\t}\n\n\t\t\t\t\texpr = expr.replace( Expr.match[ type ], \"\" );\n\n\t\t\t\t\tif ( !anyFound ) {\n\t\t\t\t\t\treturn [];\n\t\t\t\t\t}\n\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Improper expression\n\t\tif ( expr === old ) {\n\t\t\tif ( anyFound == null ) {\n\t\t\t\tSizzle.error( expr );\n\n\t\t\t} else {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\told = expr;\n\t}\n\n\treturn curLoop;\n};\n\nSizzle.error = function( msg ) {\n\tthrow new Error( \"Syntax error, unrecognized expression: \" + msg );\n};\n\n/**\n * Utility function for retrieving the text value of an array of DOM nodes\n * @param {Array|Element} elem\n */\nvar getText = Sizzle.getText = function( elem ) {\n    var i, node,\n\t\tnodeType = elem.nodeType,\n\t\tret = \"\";\n\n\tif ( nodeType ) {\n\t\tif ( nodeType === 1 || nodeType === 9 ) {\n\t\t\t// Use textContent || innerText for elements\n\t\t\tif ( typeof elem.textContent === 'string' ) {\n\t\t\t\treturn elem.textContent;\n\t\t\t} else if ( typeof elem.innerText === 'string' ) {\n\t\t\t\t// Replace IE's carriage returns\n\t\t\t\treturn elem.innerText.replace( rReturn, '' );\n\t\t\t} else {\n\t\t\t\t// Traverse it's children\n\t\t\t\tfor ( elem = elem.firstChild; elem; elem = elem.nextSibling) {\n\t\t\t\t\tret += getText( elem );\n\t\t\t\t}\n\t\t\t}\n\t\t} else if ( nodeType === 3 || nodeType === 4 ) {\n\t\t\treturn elem.nodeValue;\n\t\t}\n\t} else {\n\n\t\t// If no nodeType, this is expected to be an array\n\t\tfor ( i = 0; (node = elem[i]); i++ ) {\n\t\t\t// Do not traverse comment nodes\n\t\t\tif ( node.nodeType !== 8 ) {\n\t\t\t\tret += getText( node );\n\t\t\t}\n\t\t}\n\t}\n\treturn ret;\n};\n\nvar Expr = Sizzle.selectors = {\n\torder: [ \"ID\", \"NAME\", \"TAG\" ],\n\n\tmatch: {\n\t\tID: /#((?:[\\w\\u00c0-\\uFFFF\\-]|\\\\.)+)/,\n\t\tCLASS: /\\.((?:[\\w\\u00c0-\\uFFFF\\-]|\\\\.)+)/,\n\t\tNAME: /\\[name=['\"]*((?:[\\w\\u00c0-\\uFFFF\\-]|\\\\.)+)['\"]*\\]/,\n\t\tATTR: /\\[\\s*((?:[\\w\\u00c0-\\uFFFF\\-]|\\\\.)+)\\s*(?:(\\S?=)\\s*(?:(['\"])(.*?)\\3|(#?(?:[\\w\\u00c0-\\uFFFF\\-]|\\\\.)*)|)|)\\s*\\]/,\n\t\tTAG: /^((?:[\\w\\u00c0-\\uFFFF\\*\\-]|\\\\.)+)/,\n\t\tCHILD: /:(only|nth|last|first)-child(?:\\(\\s*(even|odd|(?:[+\\-]?\\d+|(?:[+\\-]?\\d*)?n\\s*(?:[+\\-]\\s*\\d+)?))\\s*\\))?/,\n\t\tPOS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\\((\\d*)\\))?(?=[^\\-]|$)/,\n\t\tPSEUDO: /:((?:[\\w\\u00c0-\\uFFFF\\-]|\\\\.)+)(?:\\((['\"]?)((?:\\([^\\)]+\\)|[^\\(\\)]*)+)\\2\\))?/\n\t},\n\n\tleftMatch: {},\n\n\tattrMap: {\n\t\t\"class\": \"className\",\n\t\t\"for\": \"htmlFor\"\n\t},\n\n\tattrHandle: {\n\t\thref: function( elem ) {\n\t\t\treturn elem.getAttribute( \"href\" );\n\t\t},\n\t\ttype: function( elem ) {\n\t\t\treturn elem.getAttribute( \"type\" );\n\t\t}\n\t},\n\n\trelative: {\n\t\t\"+\": function(checkSet, part){\n\t\t\tvar isPartStr = typeof part === \"string\",\n\t\t\t\tisTag = isPartStr && !rNonWord.test( part ),\n\t\t\t\tisPartStrNotTag = isPartStr && !isTag;\n\n\t\t\tif ( isTag ) {\n\t\t\t\tpart = part.toLowerCase();\n\t\t\t}\n\n\t\t\tfor ( var i = 0, l = checkSet.length, elem; i < l; i++ ) {\n\t\t\t\tif ( (elem = checkSet[i]) ) {\n\t\t\t\t\twhile ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {}\n\n\t\t\t\t\tcheckSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ?\n\t\t\t\t\t\telem || false :\n\t\t\t\t\t\telem === part;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( isPartStrNotTag ) {\n\t\t\t\tSizzle.filter( part, checkSet, true );\n\t\t\t}\n\t\t},\n\n\t\t\">\": function( checkSet, part ) {\n\t\t\tvar elem,\n\t\t\t\tisPartStr = typeof part === \"string\",\n\t\t\t\ti = 0,\n\t\t\t\tl = checkSet.length;\n\n\t\t\tif ( isPartStr && !rNonWord.test( part ) ) {\n\t\t\t\tpart = part.toLowerCase();\n\n\t\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\t\telem = checkSet[i];\n\n\t\t\t\t\tif ( elem ) {\n\t\t\t\t\t\tvar parent = elem.parentNode;\n\t\t\t\t\t\tcheckSet[i] = parent.nodeName.toLowerCase() === part ? parent : false;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t} else {\n\t\t\t\tfor ( ; i < l; i++ ) {\n\t\t\t\t\telem = checkSet[i];\n\n\t\t\t\t\tif ( elem ) {\n\t\t\t\t\t\tcheckSet[i] = isPartStr ?\n\t\t\t\t\t\t\telem.parentNode :\n\t\t\t\t\t\t\telem.parentNode === part;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif ( isPartStr ) {\n\t\t\t\t\tSizzle.filter( part, checkSet, true );\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\t\"\": function(checkSet, part, isXML){\n\t\t\tvar nodeCheck,\n\t\t\t\tdoneName = done++,\n\t\t\t\tcheckFn = dirCheck;\n\n\t\t\tif ( typeof part === \"string\" && !rNonWord.test( part ) ) {\n\t\t\t\tpart = part.toLowerCase();\n\t\t\t\tnodeCheck = part;\n\t\t\t\tcheckFn = dirNodeCheck;\n\t\t\t}\n\n\t\t\tcheckFn( \"parentNode\", part, doneName, checkSet, nodeCheck, isXML );\n\t\t},\n\n\t\t\"~\": function( checkSet, part, isXML ) {\n\t\t\tvar nodeCheck,\n\t\t\t\tdoneName = done++,\n\t\t\t\tcheckFn = dirCheck;\n\n\t\t\tif ( typeof part === \"string\" && !rNonWord.test( part ) ) {\n\t\t\t\tpart = part.toLowerCase();\n\t\t\t\tnodeCheck = part;\n\t\t\t\tcheckFn = dirNodeCheck;\n\t\t\t}\n\n\t\t\tcheckFn( \"previousSibling\", part, doneName, checkSet, nodeCheck, isXML );\n\t\t}\n\t},\n\n\tfind: {\n\t\tID: function( match, context, isXML ) {\n\t\t\tif ( typeof context.getElementById !== \"undefined\" && !isXML ) {\n\t\t\t\tvar m = context.getElementById(match[1]);\n\t\t\t\t// Check parentNode to catch when Blackberry 4.6 returns\n\t\t\t\t// nodes that are no longer in the document #6963\n\t\t\t\treturn m && m.parentNode ? [m] : [];\n\t\t\t}\n\t\t},\n\n\t\tNAME: function( match, context ) {\n\t\t\tif ( typeof context.getElementsByName !== \"undefined\" ) {\n\t\t\t\tvar ret = [],\n\t\t\t\t\tresults = context.getElementsByName( match[1] );\n\n\t\t\t\tfor ( var i = 0, l = results.length; i < l; i++ ) {\n\t\t\t\t\tif ( results[i].getAttribute(\"name\") === match[1] ) {\n\t\t\t\t\t\tret.push( results[i] );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn ret.length === 0 ? null : ret;\n\t\t\t}\n\t\t},\n\n\t\tTAG: function( match, context ) {\n\t\t\tif ( typeof context.getElementsByTagName !== \"undefined\" ) {\n\t\t\t\treturn context.getElementsByTagName( match[1] );\n\t\t\t}\n\t\t}\n\t},\n\tpreFilter: {\n\t\tCLASS: function( match, curLoop, inplace, result, not, isXML ) {\n\t\t\tmatch = \" \" + match[1].replace( rBackslash, \"\" ) + \" \";\n\n\t\t\tif ( isXML ) {\n\t\t\t\treturn match;\n\t\t\t}\n\n\t\t\tfor ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) {\n\t\t\t\tif ( elem ) {\n\t\t\t\t\tif ( not ^ (elem.className && (\" \" + elem.className + \" \").replace(/[\\t\\n\\r]/g, \" \").indexOf(match) >= 0) ) {\n\t\t\t\t\t\tif ( !inplace ) {\n\t\t\t\t\t\t\tresult.push( elem );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} else if ( inplace ) {\n\t\t\t\t\t\tcurLoop[i] = false;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn false;\n\t\t},\n\n\t\tID: function( match ) {\n\t\t\treturn match[1].replace( rBackslash, \"\" );\n\t\t},\n\n\t\tTAG: function( match, curLoop ) {\n\t\t\treturn match[1].replace( rBackslash, \"\" ).toLowerCase();\n\t\t},\n\n\t\tCHILD: function( match ) {\n\t\t\tif ( match[1] === \"nth\" ) {\n\t\t\t\tif ( !match[2] ) {\n\t\t\t\t\tSizzle.error( match[0] );\n\t\t\t\t}\n\n\t\t\t\tmatch[2] = match[2].replace(/^\\+|\\s*/g, '');\n\n\t\t\t\t// parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6'\n\t\t\t\tvar test = /(-?)(\\d*)(?:n([+\\-]?\\d*))?/.exec(\n\t\t\t\t\tmatch[2] === \"even\" && \"2n\" || match[2] === \"odd\" && \"2n+1\" ||\n\t\t\t\t\t!/\\D/.test( match[2] ) && \"0n+\" + match[2] || match[2]);\n\n\t\t\t\t// calculate the numbers (first)n+(last) including if they are negative\n\t\t\t\tmatch[2] = (test[1] + (test[2] || 1)) - 0;\n\t\t\t\tmatch[3] = test[3] - 0;\n\t\t\t}\n\t\t\telse if ( match[2] ) {\n\t\t\t\tSizzle.error( match[0] );\n\t\t\t}\n\n\t\t\t// TODO: Move to normal caching system\n\t\t\tmatch[0] = done++;\n\n\t\t\treturn match;\n\t\t},\n\n\t\tATTR: function( match, curLoop, inplace, result, not, isXML ) {\n\t\t\tvar name = match[1] = match[1].replace( rBackslash, \"\" );\n\t\t\t\n\t\t\tif ( !isXML && Expr.attrMap[name] ) {\n\t\t\t\tmatch[1] = Expr.attrMap[name];\n\t\t\t}\n\n\t\t\t// Handle if an un-quoted value was used\n\t\t\tmatch[4] = ( match[4] || match[5] || \"\" ).replace( rBackslash, \"\" );\n\n\t\t\tif ( match[2] === \"~=\" ) {\n\t\t\t\tmatch[4] = \" \" + match[4] + \" \";\n\t\t\t}\n\n\t\t\treturn match;\n\t\t},\n\n\t\tPSEUDO: function( match, curLoop, inplace, result, not ) {\n\t\t\tif ( match[1] === \"not\" ) {\n\t\t\t\t// If we're dealing with a complex expression, or a simple one\n\t\t\t\tif ( ( chunker.exec(match[3]) || \"\" ).length > 1 || /^\\w/.test(match[3]) ) {\n\t\t\t\t\tmatch[3] = Sizzle(match[3], null, null, curLoop);\n\n\t\t\t\t} else {\n\t\t\t\t\tvar ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not);\n\n\t\t\t\t\tif ( !inplace ) {\n\t\t\t\t\t\tresult.push.apply( result, ret );\n\t\t\t\t\t}\n\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t} else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\t\n\t\t\treturn match;\n\t\t},\n\n\t\tPOS: function( match ) {\n\t\t\tmatch.unshift( true );\n\n\t\t\treturn match;\n\t\t}\n\t},\n\t\n\tfilters: {\n\t\tenabled: function( elem ) {\n\t\t\treturn elem.disabled === false && elem.type !== \"hidden\";\n\t\t},\n\n\t\tdisabled: function( elem ) {\n\t\t\treturn elem.disabled === true;\n\t\t},\n\n\t\tchecked: function( elem ) {\n\t\t\treturn elem.checked === true;\n\t\t},\n\t\t\n\t\tselected: function( elem ) {\n\t\t\t// Accessing this property makes selected-by-default\n\t\t\t// options in Safari work properly\n\t\t\tif ( elem.parentNode ) {\n\t\t\t\telem.parentNode.selectedIndex;\n\t\t\t}\n\t\t\t\n\t\t\treturn elem.selected === true;\n\t\t},\n\n\t\tparent: function( elem ) {\n\t\t\treturn !!elem.firstChild;\n\t\t},\n\n\t\tempty: function( elem ) {\n\t\t\treturn !elem.firstChild;\n\t\t},\n\n\t\thas: function( elem, i, match ) {\n\t\t\treturn !!Sizzle( match[3], elem ).length;\n\t\t},\n\n\t\theader: function( elem ) {\n\t\t\treturn (/h\\d/i).test( elem.nodeName );\n\t\t},\n\n\t\ttext: function( elem ) {\n\t\t\tvar attr = elem.getAttribute( \"type\" ), type = elem.type;\n\t\t\t// IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) \n\t\t\t// use getAttribute instead to test this case\n\t\t\treturn elem.nodeName.toLowerCase() === \"input\" && \"text\" === type && ( attr === type || attr === null );\n\t\t},\n\n\t\tradio: function( elem ) {\n\t\t\treturn elem.nodeName.toLowerCase() === \"input\" && \"radio\" === elem.type;\n\t\t},\n\n\t\tcheckbox: function( elem ) {\n\t\t\treturn elem.nodeName.toLowerCase() === \"input\" && \"checkbox\" === elem.type;\n\t\t},\n\n\t\tfile: function( elem ) {\n\t\t\treturn elem.nodeName.toLowerCase() === \"input\" && \"file\" === elem.type;\n\t\t},\n\n\t\tpassword: function( elem ) {\n\t\t\treturn elem.nodeName.toLowerCase() === \"input\" && \"password\" === elem.type;\n\t\t},\n\n\t\tsubmit: function( elem ) {\n\t\t\tvar name = elem.nodeName.toLowerCase();\n\t\t\treturn (name === \"input\" || name === \"button\") && \"submit\" === elem.type;\n\t\t},\n\n\t\timage: function( elem ) {\n\t\t\treturn elem.nodeName.toLowerCase() === \"input\" && \"image\" === elem.type;\n\t\t},\n\n\t\treset: function( elem ) {\n\t\t\tvar name = elem.nodeName.toLowerCase();\n\t\t\treturn (name === \"input\" || name === \"button\") && \"reset\" === elem.type;\n\t\t},\n\n\t\tbutton: function( elem ) {\n\t\t\tvar name = elem.nodeName.toLowerCase();\n\t\t\treturn name === \"input\" && \"button\" === elem.type || name === \"button\";\n\t\t},\n\n\t\tinput: function( elem ) {\n\t\t\treturn (/input|select|textarea|button/i).test( elem.nodeName );\n\t\t},\n\n\t\tfocus: function( elem ) {\n\t\t\treturn elem === elem.ownerDocument.activeElement;\n\t\t}\n\t},\n\tsetFilters: {\n\t\tfirst: function( elem, i ) {\n\t\t\treturn i === 0;\n\t\t},\n\n\t\tlast: function( elem, i, match, array ) {\n\t\t\treturn i === array.length - 1;\n\t\t},\n\n\t\teven: function( elem, i ) {\n\t\t\treturn i % 2 === 0;\n\t\t},\n\n\t\todd: function( elem, i ) {\n\t\t\treturn i % 2 === 1;\n\t\t},\n\n\t\tlt: function( elem, i, match ) {\n\t\t\treturn i < match[3] - 0;\n\t\t},\n\n\t\tgt: function( elem, i, match ) {\n\t\t\treturn i > match[3] - 0;\n\t\t},\n\n\t\tnth: function( elem, i, match ) {\n\t\t\treturn match[3] - 0 === i;\n\t\t},\n\n\t\teq: function( elem, i, match ) {\n\t\t\treturn match[3] - 0 === i;\n\t\t}\n\t},\n\tfilter: {\n\t\tPSEUDO: function( elem, match, i, array ) {\n\t\t\tvar name = match[1],\n\t\t\t\tfilter = Expr.filters[ name ];\n\n\t\t\tif ( filter ) {\n\t\t\t\treturn filter( elem, i, match, array );\n\n\t\t\t} else if ( name === \"contains\" ) {\n\t\t\t\treturn (elem.textContent || elem.innerText || getText([ elem ]) || \"\").indexOf(match[3]) >= 0;\n\n\t\t\t} else if ( name === \"not\" ) {\n\t\t\t\tvar not = match[3];\n\n\t\t\t\tfor ( var j = 0, l = not.length; j < l; j++ ) {\n\t\t\t\t\tif ( not[j] === elem ) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn true;\n\n\t\t\t} else {\n\t\t\t\tSizzle.error( name );\n\t\t\t}\n\t\t},\n\n\t\tCHILD: function( elem, match ) {\n\t\t\tvar first, last,\n\t\t\t\tdoneName, parent, cache,\n\t\t\t\tcount, diff,\n\t\t\t\ttype = match[1],\n\t\t\t\tnode = elem;\n\n\t\t\tswitch ( type ) {\n\t\t\t\tcase \"only\":\n\t\t\t\tcase \"first\":\n\t\t\t\t\twhile ( (node = node.previousSibling) )\t {\n\t\t\t\t\t\tif ( node.nodeType === 1 ) { \n\t\t\t\t\t\t\treturn false; \n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( type === \"first\" ) { \n\t\t\t\t\t\treturn true; \n\t\t\t\t\t}\n\n\t\t\t\t\tnode = elem;\n\n\t\t\t\tcase \"last\":\n\t\t\t\t\twhile ( (node = node.nextSibling) )\t {\n\t\t\t\t\t\tif ( node.nodeType === 1 ) { \n\t\t\t\t\t\t\treturn false; \n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn true;\n\n\t\t\t\tcase \"nth\":\n\t\t\t\t\tfirst = match[2];\n\t\t\t\t\tlast = match[3];\n\n\t\t\t\t\tif ( first === 1 && last === 0 ) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tdoneName = match[0];\n\t\t\t\t\tparent = elem.parentNode;\n\t\n\t\t\t\t\tif ( parent && (parent[ expando ] !== doneName || !elem.nodeIndex) ) {\n\t\t\t\t\t\tcount = 0;\n\t\t\t\t\t\t\n\t\t\t\t\t\tfor ( node = parent.firstChild; node; node = node.nextSibling ) {\n\t\t\t\t\t\t\tif ( node.nodeType === 1 ) {\n\t\t\t\t\t\t\t\tnode.nodeIndex = ++count;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} \n\n\t\t\t\t\t\tparent[ expando ] = doneName;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tdiff = elem.nodeIndex - last;\n\n\t\t\t\t\tif ( first === 0 ) {\n\t\t\t\t\t\treturn diff === 0;\n\n\t\t\t\t\t} else {\n\t\t\t\t\t\treturn ( diff % first === 0 && diff / first >= 0 );\n\t\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tID: function( elem, match ) {\n\t\t\treturn elem.nodeType === 1 && elem.getAttribute(\"id\") === match;\n\t\t},\n\n\t\tTAG: function( elem, match ) {\n\t\t\treturn (match === \"*\" && elem.nodeType === 1) || !!elem.nodeName && elem.nodeName.toLowerCase() === match;\n\t\t},\n\t\t\n\t\tCLASS: function( elem, match ) {\n\t\t\treturn (\" \" + (elem.className || elem.getAttribute(\"class\")) + \" \")\n\t\t\t\t.indexOf( match ) > -1;\n\t\t},\n\n\t\tATTR: function( elem, match ) {\n\t\t\tvar name = match[1],\n\t\t\t\tresult = Sizzle.attr ?\n\t\t\t\t\tSizzle.attr( elem, name ) :\n\t\t\t\t\tExpr.attrHandle[ name ] ?\n\t\t\t\t\tExpr.attrHandle[ name ]( elem ) :\n\t\t\t\t\telem[ name ] != null ?\n\t\t\t\t\t\telem[ name ] :\n\t\t\t\t\t\telem.getAttribute( name ),\n\t\t\t\tvalue = result + \"\",\n\t\t\t\ttype = match[2],\n\t\t\t\tcheck = match[4];\n\n\t\t\treturn result == null ?\n\t\t\t\ttype === \"!=\" :\n\t\t\t\t!type && Sizzle.attr ?\n\t\t\t\tresult != null :\n\t\t\t\ttype === \"=\" ?\n\t\t\t\tvalue === check :\n\t\t\t\ttype === \"*=\" ?\n\t\t\t\tvalue.indexOf(check) >= 0 :\n\t\t\t\ttype === \"~=\" ?\n\t\t\t\t(\" \" + value + \" \").indexOf(check) >= 0 :\n\t\t\t\t!check ?\n\t\t\t\tvalue && result !== false :\n\t\t\t\ttype === \"!=\" ?\n\t\t\t\tvalue !== check :\n\t\t\t\ttype === \"^=\" ?\n\t\t\t\tvalue.indexOf(check) === 0 :\n\t\t\t\ttype === \"$=\" ?\n\t\t\t\tvalue.substr(value.length - check.length) === check :\n\t\t\t\ttype === \"|=\" ?\n\t\t\t\tvalue === check || value.substr(0, check.length + 1) === check + \"-\" :\n\t\t\t\tfalse;\n\t\t},\n\n\t\tPOS: function( elem, match, i, array ) {\n\t\t\tvar name = match[2],\n\t\t\t\tfilter = Expr.setFilters[ name ];\n\n\t\t\tif ( filter ) {\n\t\t\t\treturn filter( elem, i, match, array );\n\t\t\t}\n\t\t}\n\t}\n};\n\nvar origPOS = Expr.match.POS,\n\tfescape = function(all, num){\n\t\treturn \"\\\\\" + (num - 0 + 1);\n\t};\n\nfor ( var type in Expr.match ) {\n\tExpr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\\[]*\\])(?![^\\(]*\\))/.source) );\n\tExpr.leftMatch[ type ] = new RegExp( /(^(?:.|\\r|\\n)*?)/.source + Expr.match[ type ].source.replace(/\\\\(\\d+)/g, fescape) );\n}\n\nvar makeArray = function( array, results ) {\n\tarray = Array.prototype.slice.call( array, 0 );\n\n\tif ( results ) {\n\t\tresults.push.apply( results, array );\n\t\treturn results;\n\t}\n\t\n\treturn array;\n};\n\n// Perform a simple check to determine if the browser is capable of\n// converting a NodeList to an array using builtin methods.\n// Also verifies that the returned array holds DOM nodes\n// (which is not the case in the Blackberry browser)\ntry {\n\tArray.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType;\n\n// Provide a fallback method if it does not work\n} catch( e ) {\n\tmakeArray = function( array, results ) {\n\t\tvar i = 0,\n\t\t\tret = results || [];\n\n\t\tif ( toString.call(array) === \"[object Array]\" ) {\n\t\t\tArray.prototype.push.apply( ret, array );\n\n\t\t} else {\n\t\t\tif ( typeof array.length === \"number\" ) {\n\t\t\t\tfor ( var l = array.length; i < l; i++ ) {\n\t\t\t\t\tret.push( array[i] );\n\t\t\t\t}\n\n\t\t\t} else {\n\t\t\t\tfor ( ; array[i]; i++ ) {\n\t\t\t\t\tret.push( array[i] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn ret;\n\t};\n}\n\nvar sortOrder, siblingCheck;\n\nif ( document.documentElement.compareDocumentPosition ) {\n\tsortOrder = function( a, b ) {\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t\treturn 0;\n\t\t}\n\n\t\tif ( !a.compareDocumentPosition || !b.compareDocumentPosition ) {\n\t\t\treturn a.compareDocumentPosition ? -1 : 1;\n\t\t}\n\n\t\treturn a.compareDocumentPosition(b) & 4 ? -1 : 1;\n\t};\n\n} else {\n\tsortOrder = function( a, b ) {\n\t\t// The nodes are identical, we can exit early\n\t\tif ( a === b ) {\n\t\t\thasDuplicate = true;\n\t\t\treturn 0;\n\n\t\t// Fallback to using sourceIndex (in IE) if it's available on both nodes\n\t\t} else if ( a.sourceIndex && b.sourceIndex ) {\n\t\t\treturn a.sourceIndex - b.sourceIndex;\n\t\t}\n\n\t\tvar al, bl,\n\t\t\tap = [],\n\t\t\tbp = [],\n\t\t\taup = a.parentNode,\n\t\t\tbup = b.parentNode,\n\t\t\tcur = aup;\n\n\t\t// If the nodes are siblings (or identical) we can do a quick check\n\t\tif ( aup === bup ) {\n\t\t\treturn siblingCheck( a, b );\n\n\t\t// If no parents were found then the nodes are disconnected\n\t\t} else if ( !aup ) {\n\t\t\treturn -1;\n\n\t\t} else if ( !bup ) {\n\t\t\treturn 1;\n\t\t}\n\n\t\t// Otherwise they're somewhere else in the tree so we need\n\t\t// to build up a full list of the parentNodes for comparison\n\t\twhile ( cur ) {\n\t\t\tap.unshift( cur );\n\t\t\tcur = cur.parentNode;\n\t\t}\n\n\t\tcur = bup;\n\n\t\twhile ( cur ) {\n\t\t\tbp.unshift( cur );\n\t\t\tcur = cur.parentNode;\n\t\t}\n\n\t\tal = ap.length;\n\t\tbl = bp.length;\n\n\t\t// Start walking down the tree looking for a discrepancy\n\t\tfor ( var i = 0; i < al && i < bl; i++ ) {\n\t\t\tif ( ap[i] !== bp[i] ) {\n\t\t\t\treturn siblingCheck( ap[i], bp[i] );\n\t\t\t}\n\t\t}\n\n\t\t// We ended someplace up the tree so do a sibling check\n\t\treturn i === al ?\n\t\t\tsiblingCheck( a, bp[i], -1 ) :\n\t\t\tsiblingCheck( ap[i], b, 1 );\n\t};\n\n\tsiblingCheck = function( a, b, ret ) {\n\t\tif ( a === b ) {\n\t\t\treturn ret;\n\t\t}\n\n\t\tvar cur = a.nextSibling;\n\n\t\twhile ( cur ) {\n\t\t\tif ( cur === b ) {\n\t\t\t\treturn -1;\n\t\t\t}\n\n\t\t\tcur = cur.nextSibling;\n\t\t}\n\n\t\treturn 1;\n\t};\n}\n\n// Check to see if the browser returns elements by name when\n// querying by getElementById (and provide a workaround)\n(function(){\n\t// We're going to inject a fake input element with a specified name\n\tvar form = document.createElement(\"div\"),\n\t\tid = \"script\" + (new Date()).getTime(),\n\t\troot = document.documentElement;\n\n\tform.innerHTML = \"<a name='\" + id + \"'/>\";\n\n\t// Inject it into the root element, check its status, and remove it quickly\n\troot.insertBefore( form, root.firstChild );\n\n\t// The workaround has to do additional checks after a getElementById\n\t// Which slows things down for other browsers (hence the branching)\n\tif ( document.getElementById( id ) ) {\n\t\tExpr.find.ID = function( match, context, isXML ) {\n\t\t\tif ( typeof context.getElementById !== \"undefined\" && !isXML ) {\n\t\t\t\tvar m = context.getElementById(match[1]);\n\n\t\t\t\treturn m ?\n\t\t\t\t\tm.id === match[1] || typeof m.getAttributeNode !== \"undefined\" && m.getAttributeNode(\"id\").nodeValue === match[1] ?\n\t\t\t\t\t\t[m] :\n\t\t\t\t\t\tundefined :\n\t\t\t\t\t[];\n\t\t\t}\n\t\t};\n\n\t\tExpr.filter.ID = function( elem, match ) {\n\t\t\tvar node = typeof elem.getAttributeNode !== \"undefined\" && elem.getAttributeNode(\"id\");\n\n\t\t\treturn elem.nodeType === 1 && node && node.nodeValue === match;\n\t\t};\n\t}\n\n\troot.removeChild( form );\n\n\t// release memory in IE\n\troot = form = null;\n})();\n\n(function(){\n\t// Check to see if the browser returns only elements\n\t// when doing getElementsByTagName(\"*\")\n\n\t// Create a fake element\n\tvar div = document.createElement(\"div\");\n\tdiv.appendChild( document.createComment(\"\") );\n\n\t// Make sure no comments are found\n\tif ( div.getElementsByTagName(\"*\").length > 0 ) {\n\t\tExpr.find.TAG = function( match, context ) {\n\t\t\tvar results = context.getElementsByTagName( match[1] );\n\n\t\t\t// Filter out possible comments\n\t\t\tif ( match[1] === \"*\" ) {\n\t\t\t\tvar tmp = [];\n\n\t\t\t\tfor ( var i = 0; results[i]; i++ ) {\n\t\t\t\t\tif ( results[i].nodeType === 1 ) {\n\t\t\t\t\t\ttmp.push( results[i] );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tresults = tmp;\n\t\t\t}\n\n\t\t\treturn results;\n\t\t};\n\t}\n\n\t// Check to see if an attribute returns normalized href attributes\n\tdiv.innerHTML = \"<a href='#'></a>\";\n\n\tif ( div.firstChild && typeof div.firstChild.getAttribute !== \"undefined\" &&\n\t\t\tdiv.firstChild.getAttribute(\"href\") !== \"#\" ) {\n\n\t\tExpr.attrHandle.href = function( elem ) {\n\t\t\treturn elem.getAttribute( \"href\", 2 );\n\t\t};\n\t}\n\n\t// release memory in IE\n\tdiv = null;\n})();\n\nif ( document.querySelectorAll ) {\n\t(function(){\n\t\tvar oldSizzle = Sizzle,\n\t\t\tdiv = document.createElement(\"div\"),\n\t\t\tid = \"__sizzle__\";\n\n\t\tdiv.innerHTML = \"<p class='TEST'></p>\";\n\n\t\t// Safari can't handle uppercase or unicode characters when\n\t\t// in quirks mode.\n\t\tif ( div.querySelectorAll && div.querySelectorAll(\".TEST\").length === 0 ) {\n\t\t\treturn;\n\t\t}\n\t\n\t\tSizzle = function( query, context, extra, seed ) {\n\t\t\tcontext = context || document;\n\n\t\t\t// Only use querySelectorAll on non-XML documents\n\t\t\t// (ID selectors don't work in non-HTML documents)\n\t\t\tif ( !seed && !Sizzle.isXML(context) ) {\n\t\t\t\t// See if we find a selector to speed up\n\t\t\t\tvar match = /^(\\w+$)|^\\.([\\w\\-]+$)|^#([\\w\\-]+$)/.exec( query );\n\t\t\t\t\n\t\t\t\tif ( match && (context.nodeType === 1 || context.nodeType === 9) ) {\n\t\t\t\t\t// Speed-up: Sizzle(\"TAG\")\n\t\t\t\t\tif ( match[1] ) {\n\t\t\t\t\t\treturn makeArray( context.getElementsByTagName( query ), extra );\n\t\t\t\t\t\n\t\t\t\t\t// Speed-up: Sizzle(\".CLASS\")\n\t\t\t\t\t} else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) {\n\t\t\t\t\t\treturn makeArray( context.getElementsByClassName( match[2] ), extra );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif ( context.nodeType === 9 ) {\n\t\t\t\t\t// Speed-up: Sizzle(\"body\")\n\t\t\t\t\t// The body element only exists once, optimize finding it\n\t\t\t\t\tif ( query === \"body\" && context.body ) {\n\t\t\t\t\t\treturn makeArray( [ context.body ], extra );\n\t\t\t\t\t\t\n\t\t\t\t\t// Speed-up: Sizzle(\"#ID\")\n\t\t\t\t\t} else if ( match && match[3] ) {\n\t\t\t\t\t\tvar elem = context.getElementById( match[3] );\n\n\t\t\t\t\t\t// Check parentNode to catch when Blackberry 4.6 returns\n\t\t\t\t\t\t// nodes that are no longer in the document #6963\n\t\t\t\t\t\tif ( elem && elem.parentNode ) {\n\t\t\t\t\t\t\t// Handle the case where IE and Opera return items\n\t\t\t\t\t\t\t// by name instead of ID\n\t\t\t\t\t\t\tif ( elem.id === match[3] ) {\n\t\t\t\t\t\t\t\treturn makeArray( [ elem ], extra );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treturn makeArray( [], extra );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\ttry {\n\t\t\t\t\t\treturn makeArray( context.querySelectorAll(query), extra );\n\t\t\t\t\t} catch(qsaError) {}\n\n\t\t\t\t// qSA works strangely on Element-rooted queries\n\t\t\t\t// We can work around this by specifying an extra ID on the root\n\t\t\t\t// and working up from there (Thanks to Andrew Dupont for the technique)\n\t\t\t\t// IE 8 doesn't work on object elements\n\t\t\t\t} else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== \"object\" ) {\n\t\t\t\t\tvar oldContext = context,\n\t\t\t\t\t\told = context.getAttribute( \"id\" ),\n\t\t\t\t\t\tnid = old || id,\n\t\t\t\t\t\thasParent = context.parentNode,\n\t\t\t\t\t\trelativeHierarchySelector = /^\\s*[+~]/.test( query );\n\n\t\t\t\t\tif ( !old ) {\n\t\t\t\t\t\tcontext.setAttribute( \"id\", nid );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tnid = nid.replace( /'/g, \"\\\\$&\" );\n\t\t\t\t\t}\n\t\t\t\t\tif ( relativeHierarchySelector && hasParent ) {\n\t\t\t\t\t\tcontext = context.parentNode;\n\t\t\t\t\t}\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tif ( !relativeHierarchySelector || hasParent ) {\n\t\t\t\t\t\t\treturn makeArray( context.querySelectorAll( \"[id='\" + nid + \"'] \" + query ), extra );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} catch(pseudoError) {\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tif ( !old ) {\n\t\t\t\t\t\t\toldContext.removeAttribute( \"id\" );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\n\t\t\treturn oldSizzle(query, context, extra, seed);\n\t\t};\n\n\t\tfor ( var prop in oldSizzle ) {\n\t\t\tSizzle[ prop ] = oldSizzle[ prop ];\n\t\t}\n\n\t\t// release memory in IE\n\t\tdiv = null;\n\t})();\n}\n\n(function(){\n\tvar html = document.documentElement,\n\t\tmatches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector;\n\n\tif ( matches ) {\n\t\t// Check to see if it's possible to do matchesSelector\n\t\t// on a disconnected node (IE 9 fails this)\n\t\tvar disconnectedMatch = !matches.call( document.createElement( \"div\" ), \"div\" ),\n\t\t\tpseudoWorks = false;\n\n\t\ttry {\n\t\t\t// This should fail with an exception\n\t\t\t// Gecko does not error, returns false instead\n\t\t\tmatches.call( document.documentElement, \"[test!='']:sizzle\" );\n\t\n\t\t} catch( pseudoError ) {\n\t\t\tpseudoWorks = true;\n\t\t}\n\n\t\tSizzle.matchesSelector = function( node, expr ) {\n\t\t\t// Make sure that attribute selectors are quoted\n\t\t\texpr = expr.replace(/\\=\\s*([^'\"\\]]*)\\s*\\]/g, \"='$1']\");\n\n\t\t\tif ( !Sizzle.isXML( node ) ) {\n\t\t\t\ttry { \n\t\t\t\t\tif ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) {\n\t\t\t\t\t\tvar ret = matches.call( node, expr );\n\n\t\t\t\t\t\t// IE 9's matchesSelector returns false on disconnected nodes\n\t\t\t\t\t\tif ( ret || !disconnectedMatch ||\n\t\t\t\t\t\t\t\t// As well, disconnected nodes are said to be in a document\n\t\t\t\t\t\t\t\t// fragment in IE 9, so check for that\n\t\t\t\t\t\t\t\tnode.document && node.document.nodeType !== 11 ) {\n\t\t\t\t\t\t\treturn ret;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t} catch(e) {}\n\t\t\t}\n\n\t\t\treturn Sizzle(expr, null, null, [node]).length > 0;\n\t\t};\n\t}\n})();\n\n(function(){\n\tvar div = document.createElement(\"div\");\n\n\tdiv.innerHTML = \"<div class='test e'></div><div class='test'></div>\";\n\n\t// Opera can't find a second classname (in 9.6)\n\t// Also, make sure that getElementsByClassName actually exists\n\tif ( !div.getElementsByClassName || div.getElementsByClassName(\"e\").length === 0 ) {\n\t\treturn;\n\t}\n\n\t// Safari caches class attributes, doesn't catch changes (in 3.2)\n\tdiv.lastChild.className = \"e\";\n\n\tif ( div.getElementsByClassName(\"e\").length === 1 ) {\n\t\treturn;\n\t}\n\t\n\tExpr.order.splice(1, 0, \"CLASS\");\n\tExpr.find.CLASS = function( match, context, isXML ) {\n\t\tif ( typeof context.getElementsByClassName !== \"undefined\" && !isXML ) {\n\t\t\treturn context.getElementsByClassName(match[1]);\n\t\t}\n\t};\n\n\t// release memory in IE\n\tdiv = null;\n})();\n\nfunction dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {\n\tfor ( var i = 0, l = checkSet.length; i < l; i++ ) {\n\t\tvar elem = checkSet[i];\n\n\t\tif ( elem ) {\n\t\t\tvar match = false;\n\n\t\t\telem = elem[dir];\n\n\t\t\twhile ( elem ) {\n\t\t\t\tif ( elem[ expando ] === doneName ) {\n\t\t\t\t\tmatch = checkSet[elem.sizset];\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tif ( elem.nodeType === 1 && !isXML ){\n\t\t\t\t\telem[ expando ] = doneName;\n\t\t\t\t\telem.sizset = i;\n\t\t\t\t}\n\n\t\t\t\tif ( elem.nodeName.toLowerCase() === cur ) {\n\t\t\t\t\tmatch = elem;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\telem = elem[dir];\n\t\t\t}\n\n\t\t\tcheckSet[i] = match;\n\t\t}\n\t}\n}\n\nfunction dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) {\n\tfor ( var i = 0, l = checkSet.length; i < l; i++ ) {\n\t\tvar elem = checkSet[i];\n\n\t\tif ( elem ) {\n\t\t\tvar match = false;\n\t\t\t\n\t\t\telem = elem[dir];\n\n\t\t\twhile ( elem ) {\n\t\t\t\tif ( elem[ expando ] === doneName ) {\n\t\t\t\t\tmatch = checkSet[elem.sizset];\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tif ( elem.nodeType === 1 ) {\n\t\t\t\t\tif ( !isXML ) {\n\t\t\t\t\t\telem[ expando ] = doneName;\n\t\t\t\t\t\telem.sizset = i;\n\t\t\t\t\t}\n\n\t\t\t\t\tif ( typeof cur !== \"string\" ) {\n\t\t\t\t\t\tif ( elem === cur ) {\n\t\t\t\t\t\t\tmatch = true;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t} else if ( Sizzle.filter( cur, [elem] ).length > 0 ) {\n\t\t\t\t\t\tmatch = elem;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\telem = elem[dir];\n\t\t\t}\n\n\t\t\tcheckSet[i] = match;\n\t\t}\n\t}\n}\n\nif ( document.documentElement.contains ) {\n\tSizzle.contains = function( a, b ) {\n\t\treturn a !== b && (a.contains ? a.contains(b) : true);\n\t};\n\n} else if ( document.documentElement.compareDocumentPosition ) {\n\tSizzle.contains = function( a, b ) {\n\t\treturn !!(a.compareDocumentPosition(b) & 16);\n\t};\n\n} else {\n\tSizzle.contains = function() {\n\t\treturn false;\n\t};\n}\n\nSizzle.isXML = function( elem ) {\n\t// documentElement is verified for cases where it doesn't yet exist\n\t// (such as loading iframes in IE - #4833) \n\tvar documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement;\n\n\treturn documentElement ? documentElement.nodeName !== \"HTML\" : false;\n};\n\nvar posProcess = function( selector, context, seed ) {\n\tvar match,\n\t\ttmpSet = [],\n\t\tlater = \"\",\n\t\troot = context.nodeType ? [context] : context;\n\n\t// Position selectors must be done after the filter\n\t// And so must :not(positional) so we move all PSEUDOs to the end\n\twhile ( (match = Expr.match.PSEUDO.exec( selector )) ) {\n\t\tlater += match[0];\n\t\tselector = selector.replace( Expr.match.PSEUDO, \"\" );\n\t}\n\n\tselector = Expr.relative[selector] ? selector + \"*\" : selector;\n\n\tfor ( var i = 0, l = root.length; i < l; i++ ) {\n\t\tSizzle( selector, root[i], tmpSet, seed );\n\t}\n\n\treturn Sizzle.filter( later, tmpSet );\n};\n\n// EXPOSE\n// Override sizzle attribute retrieval\nSizzle.attr = jQuery.attr;\nSizzle.selectors.attrMap = {};\njQuery.find = Sizzle;\njQuery.expr = Sizzle.selectors;\njQuery.expr[\":\"] = jQuery.expr.filters;\njQuery.unique = Sizzle.uniqueSort;\njQuery.text = Sizzle.getText;\njQuery.isXMLDoc = Sizzle.isXML;\njQuery.contains = Sizzle.contains;\n\n\n})();\n\n\nvar runtil = /Until$/,\n\trparentsprev = /^(?:parents|prevUntil|prevAll)/,\n\t// Note: This RegExp should be improved, or likely pulled from Sizzle\n\trmultiselector = /,/,\n\tisSimple = /^.[^:#\\[\\.,]*$/,\n\tslice = Array.prototype.slice,\n\tPOS = jQuery.expr.match.POS,\n\t// methods guaranteed to produce a unique set when starting from a unique set\n\tguaranteedUnique = {\n\t\tchildren: true,\n\t\tcontents: true,\n\t\tnext: true,\n\t\tprev: true\n\t};\n\njQuery.fn.extend({\n\tfind: function( selector ) {\n\t\tvar self = this,\n\t\t\ti, l;\n\n\t\tif ( typeof selector !== \"string\" ) {\n\t\t\treturn jQuery( selector ).filter(function() {\n\t\t\t\tfor ( i = 0, l = self.length; i < l; i++ ) {\n\t\t\t\t\tif ( jQuery.contains( self[ i ], this ) ) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\tvar ret = this.pushStack( \"\", \"find\", selector ),\n\t\t\tlength, n, r;\n\n\t\tfor ( i = 0, l = this.length; i < l; i++ ) {\n\t\t\tlength = ret.length;\n\t\t\tjQuery.find( selector, this[i], ret );\n\n\t\t\tif ( i > 0 ) {\n\t\t\t\t// Make sure that the results are unique\n\t\t\t\tfor ( n = length; n < ret.length; n++ ) {\n\t\t\t\t\tfor ( r = 0; r < length; r++ ) {\n\t\t\t\t\t\tif ( ret[r] === ret[n] ) {\n\t\t\t\t\t\t\tret.splice(n--, 1);\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn ret;\n\t},\n\n\thas: function( target ) {\n\t\tvar targets = jQuery( target );\n\t\treturn this.filter(function() {\n\t\t\tfor ( var i = 0, l = targets.length; i < l; i++ ) {\n\t\t\t\tif ( jQuery.contains( this, targets[i] ) ) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t},\n\n\tnot: function( selector ) {\n\t\treturn this.pushStack( winnow(this, selector, false), \"not\", selector);\n\t},\n\n\tfilter: function( selector ) {\n\t\treturn this.pushStack( winnow(this, selector, true), \"filter\", selector );\n\t},\n\n\tis: function( selector ) {\n\t\treturn !!selector && ( \n\t\t\ttypeof selector === \"string\" ?\n\t\t\t\t// If this is a positional selector, check membership in the returned set\n\t\t\t\t// so $(\"p:first\").is(\"p:last\") won't return true for a doc with two \"p\".\n\t\t\t\tPOS.test( selector ) ? \n\t\t\t\t\tjQuery( selector, this.context ).index( this[0] ) >= 0 :\n\t\t\t\t\tjQuery.filter( selector, this ).length > 0 :\n\t\t\t\tthis.filter( selector ).length > 0 );\n\t},\n\n\tclosest: function( selectors, context ) {\n\t\tvar ret = [], i, l, cur = this[0];\n\t\t\n\t\t// Array (deprecated as of jQuery 1.7)\n\t\tif ( jQuery.isArray( selectors ) ) {\n\t\t\tvar level = 1;\n\n\t\t\twhile ( cur && cur.ownerDocument && cur !== context ) {\n\t\t\t\tfor ( i = 0; i < selectors.length; i++ ) {\n\n\t\t\t\t\tif ( jQuery( cur ).is( selectors[ i ] ) ) {\n\t\t\t\t\t\tret.push({ selector: selectors[ i ], elem: cur, level: level });\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcur = cur.parentNode;\n\t\t\t\tlevel++;\n\t\t\t}\n\n\t\t\treturn ret;\n\t\t}\n\n\t\t// String\n\t\tvar pos = POS.test( selectors ) || typeof selectors !== \"string\" ?\n\t\t\t\tjQuery( selectors, context || this.context ) :\n\t\t\t\t0;\n\n\t\tfor ( i = 0, l = this.length; i < l; i++ ) {\n\t\t\tcur = this[i];\n\n\t\t\twhile ( cur ) {\n\t\t\t\tif ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) {\n\t\t\t\t\tret.push( cur );\n\t\t\t\t\tbreak;\n\n\t\t\t\t} else {\n\t\t\t\t\tcur = cur.parentNode;\n\t\t\t\t\tif ( !cur || !cur.ownerDocument || cur === context || cur.nodeType === 11 ) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tret = ret.length > 1 ? jQuery.unique( ret ) : ret;\n\n\t\treturn this.pushStack( ret, \"closest\", selectors );\n\t},\n\n\t// Determine the position of an element within\n\t// the matched set of elements\n\tindex: function( elem ) {\n\n\t\t// No argument, return index in parent\n\t\tif ( !elem ) {\n\t\t\treturn ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1;\n\t\t}\n\n\t\t// index in selector\n\t\tif ( typeof elem === \"string\" ) {\n\t\t\treturn jQuery.inArray( this[0], jQuery( elem ) );\n\t\t}\n\n\t\t// Locate the position of the desired element\n\t\treturn jQuery.inArray(\n\t\t\t// If it receives a jQuery object, the first element is used\n\t\t\telem.jquery ? elem[0] : elem, this );\n\t},\n\n\tadd: function( selector, context ) {\n\t\tvar set = typeof selector === \"string\" ?\n\t\t\t\tjQuery( selector, context ) :\n\t\t\t\tjQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ),\n\t\t\tall = jQuery.merge( this.get(), set );\n\n\t\treturn this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ?\n\t\t\tall :\n\t\t\tjQuery.unique( all ) );\n\t},\n\n\tandSelf: function() {\n\t\treturn this.add( this.prevObject );\n\t}\n});\n\n// A painfully simple check to see if an element is disconnected\n// from a document (should be improved, where feasible).\nfunction isDisconnected( node ) {\n\treturn !node || !node.parentNode || node.parentNode.nodeType === 11;\n}\n\njQuery.each({\n\tparent: function( elem ) {\n\t\tvar parent = elem.parentNode;\n\t\treturn parent && parent.nodeType !== 11 ? parent : null;\n\t},\n\tparents: function( elem ) {\n\t\treturn jQuery.dir( elem, \"parentNode\" );\n\t},\n\tparentsUntil: function( elem, i, until ) {\n\t\treturn jQuery.dir( elem, \"parentNode\", until );\n\t},\n\tnext: function( elem ) {\n\t\treturn jQuery.nth( elem, 2, \"nextSibling\" );\n\t},\n\tprev: function( elem ) {\n\t\treturn jQuery.nth( elem, 2, \"previousSibling\" );\n\t},\n\tnextAll: function( elem ) {\n\t\treturn jQuery.dir( elem, \"nextSibling\" );\n\t},\n\tprevAll: function( elem ) {\n\t\treturn jQuery.dir( elem, \"previousSibling\" );\n\t},\n\tnextUntil: function( elem, i, until ) {\n\t\treturn jQuery.dir( elem, \"nextSibling\", until );\n\t},\n\tprevUntil: function( elem, i, until ) {\n\t\treturn jQuery.dir( elem, \"previousSibling\", until );\n\t},\n\tsiblings: function( elem ) {\n\t\treturn jQuery.sibling( elem.parentNode.firstChild, elem );\n\t},\n\tchildren: function( elem ) {\n\t\treturn jQuery.sibling( elem.firstChild );\n\t},\n\tcontents: function( elem ) {\n\t\treturn jQuery.nodeName( elem, \"iframe\" ) ?\n\t\t\telem.contentDocument || elem.contentWindow.document :\n\t\t\tjQuery.makeArray( elem.childNodes );\n\t}\n}, function( name, fn ) {\n\tjQuery.fn[ name ] = function( until, selector ) {\n\t\tvar ret = jQuery.map( this, fn, until );\n\n\t\tif ( !runtil.test( name ) ) {\n\t\t\tselector = until;\n\t\t}\n\n\t\tif ( selector && typeof selector === \"string\" ) {\n\t\t\tret = jQuery.filter( selector, ret );\n\t\t}\n\n\t\tret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret;\n\n\t\tif ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) {\n\t\t\tret = ret.reverse();\n\t\t}\n\n\t\treturn this.pushStack( ret, name, slice.call( arguments ).join(\",\") );\n\t};\n});\n\njQuery.extend({\n\tfilter: function( expr, elems, not ) {\n\t\tif ( not ) {\n\t\t\texpr = \":not(\" + expr + \")\";\n\t\t}\n\n\t\treturn elems.length === 1 ?\n\t\t\tjQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] :\n\t\t\tjQuery.find.matches(expr, elems);\n\t},\n\n\tdir: function( elem, dir, until ) {\n\t\tvar matched = [],\n\t\t\tcur = elem[ dir ];\n\n\t\twhile ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) {\n\t\t\tif ( cur.nodeType === 1 ) {\n\t\t\t\tmatched.push( cur );\n\t\t\t}\n\t\t\tcur = cur[dir];\n\t\t}\n\t\treturn matched;\n\t},\n\n\tnth: function( cur, result, dir, elem ) {\n\t\tresult = result || 1;\n\t\tvar num = 0;\n\n\t\tfor ( ; cur; cur = cur[dir] ) {\n\t\t\tif ( cur.nodeType === 1 && ++num === result ) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\n\t\treturn cur;\n\t},\n\n\tsibling: function( n, elem ) {\n\t\tvar r = [];\n\n\t\tfor ( ; n; n = n.nextSibling ) {\n\t\t\tif ( n.nodeType === 1 && n !== elem ) {\n\t\t\t\tr.push( n );\n\t\t\t}\n\t\t}\n\n\t\treturn r;\n\t}\n});\n\n// Implement the identical functionality for filter and not\nfunction winnow( elements, qualifier, keep ) {\n\n\t// Can't pass null or undefined to indexOf in Firefox 4\n\t// Set to 0 to skip string check\n\tqualifier = qualifier || 0;\n\n\tif ( jQuery.isFunction( qualifier ) ) {\n\t\treturn jQuery.grep(elements, function( elem, i ) {\n\t\t\tvar retVal = !!qualifier.call( elem, i, elem );\n\t\t\treturn retVal === keep;\n\t\t});\n\n\t} else if ( qualifier.nodeType ) {\n\t\treturn jQuery.grep(elements, function( elem, i ) {\n\t\t\treturn ( elem === qualifier ) === keep;\n\t\t});\n\n\t} else if ( typeof qualifier === \"string\" ) {\n\t\tvar filtered = jQuery.grep(elements, function( elem ) {\n\t\t\treturn elem.nodeType === 1;\n\t\t});\n\n\t\tif ( isSimple.test( qualifier ) ) {\n\t\t\treturn jQuery.filter(qualifier, filtered, !keep);\n\t\t} else {\n\t\t\tqualifier = jQuery.filter( qualifier, filtered );\n\t\t}\n\t}\n\n\treturn jQuery.grep(elements, function( elem, i ) {\n\t\treturn ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep;\n\t});\n}\n\n\n\n\nfunction createSafeFragment( document ) {\n\tvar list = nodeNames.split( \"|\" ),\n\tsafeFrag = document.createDocumentFragment();\n\n\tif ( safeFrag.createElement ) {\n\t\twhile ( list.length ) {\n\t\t\tsafeFrag.createElement(\n\t\t\t\tlist.pop()\n\t\t\t);\n\t\t}\n\t}\n\treturn safeFrag;\n}\n\nvar nodeNames = \"abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|\" +\n\t\t\"header|hgroup|mark|meter|nav|output|progress|section|summary|time|video\",\n\trinlinejQuery = / jQuery\\d+=\"(?:\\d+|null)\"/g,\n\trleadingWhitespace = /^\\s+/,\n\trxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\\w:]+)[^>]*)\\/>/ig,\n\trtagName = /<([\\w:]+)/,\n\trtbody = /<tbody/i,\n\trhtml = /<|&#?\\w+;/,\n\trnoInnerhtml = /<(?:script|style)/i,\n\trnocache = /<(?:script|object|embed|option|style)/i,\n\trnoshimcache = new RegExp(\"<(?:\" + nodeNames + \")\", \"i\"),\n\t// checked=\"checked\" or checked\n\trchecked = /checked\\s*(?:[^=]|=\\s*.checked.)/i,\n\trscriptType = /\\/(java|ecma)script/i,\n\trcleanScript = /^\\s*<!(?:\\[CDATA\\[|\\-\\-)/,\n\twrapMap = {\n\t\toption: [ 1, \"<select multiple='multiple'>\", \"</select>\" ],\n\t\tlegend: [ 1, \"<fieldset>\", \"</fieldset>\" ],\n\t\tthead: [ 1, \"<table>\", \"</table>\" ],\n\t\ttr: [ 2, \"<table><tbody>\", \"</tbody></table>\" ],\n\t\ttd: [ 3, \"<table><tbody><tr>\", \"</tr></tbody></table>\" ],\n\t\tcol: [ 2, \"<table><tbody></tbody><colgroup>\", \"</colgroup></table>\" ],\n\t\tarea: [ 1, \"<map>\", \"</map>\" ],\n\t\t_default: [ 0, \"\", \"\" ]\n\t},\n\tsafeFragment = createSafeFragment( document );\n\nwrapMap.optgroup = wrapMap.option;\nwrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;\nwrapMap.th = wrapMap.td;\n\n// IE can't serialize <link> and <script> tags normally\nif ( !jQuery.support.htmlSerialize ) {\n\twrapMap._default = [ 1, \"div<div>\", \"</div>\" ];\n}\n\njQuery.fn.extend({\n\ttext: function( text ) {\n\t\tif ( jQuery.isFunction(text) ) {\n\t\t\treturn this.each(function(i) {\n\t\t\t\tvar self = jQuery( this );\n\n\t\t\t\tself.text( text.call(this, i, self.text()) );\n\t\t\t});\n\t\t}\n\n\t\tif ( typeof text !== \"object\" && text !== undefined ) {\n\t\t\treturn this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) );\n\t\t}\n\n\t\treturn jQuery.text( this );\n\t},\n\n\twrapAll: function( html ) {\n\t\tif ( jQuery.isFunction( html ) ) {\n\t\t\treturn this.each(function(i) {\n\t\t\t\tjQuery(this).wrapAll( html.call(this, i) );\n\t\t\t});\n\t\t}\n\n\t\tif ( this[0] ) {\n\t\t\t// The elements to wrap the target around\n\t\t\tvar wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true);\n\n\t\t\tif ( this[0].parentNode ) {\n\t\t\t\twrap.insertBefore( this[0] );\n\t\t\t}\n\n\t\t\twrap.map(function() {\n\t\t\t\tvar elem = this;\n\n\t\t\t\twhile ( elem.firstChild && elem.firstChild.nodeType === 1 ) {\n\t\t\t\t\telem = elem.firstChild;\n\t\t\t\t}\n\n\t\t\t\treturn elem;\n\t\t\t}).append( this );\n\t\t}\n\n\t\treturn this;\n\t},\n\n\twrapInner: function( html ) {\n\t\tif ( jQuery.isFunction( html ) ) {\n\t\t\treturn this.each(function(i) {\n\t\t\t\tjQuery(this).wrapInner( html.call(this, i) );\n\t\t\t});\n\t\t}\n\n\t\treturn this.each(function() {\n\t\t\tvar self = jQuery( this ),\n\t\t\t\tcontents = self.contents();\n\n\t\t\tif ( contents.length ) {\n\t\t\t\tcontents.wrapAll( html );\n\n\t\t\t} else {\n\t\t\t\tself.append( html );\n\t\t\t}\n\t\t});\n\t},\n\n\twrap: function( html ) {\n\t\tvar isFunction = jQuery.isFunction( html );\n\n\t\treturn this.each(function(i) {\n\t\t\tjQuery( this ).wrapAll( isFunction ? html.call(this, i) : html );\n\t\t});\n\t},\n\n\tunwrap: function() {\n\t\treturn this.parent().each(function() {\n\t\t\tif ( !jQuery.nodeName( this, \"body\" ) ) {\n\t\t\t\tjQuery( this ).replaceWith( this.childNodes );\n\t\t\t}\n\t\t}).end();\n\t},\n\n\tappend: function() {\n\t\treturn this.domManip(arguments, true, function( elem ) {\n\t\t\tif ( this.nodeType === 1 ) {\n\t\t\t\tthis.appendChild( elem );\n\t\t\t}\n\t\t});\n\t},\n\n\tprepend: function() {\n\t\treturn this.domManip(arguments, true, function( elem ) {\n\t\t\tif ( this.nodeType === 1 ) {\n\t\t\t\tthis.insertBefore( elem, this.firstChild );\n\t\t\t}\n\t\t});\n\t},\n\n\tbefore: function() {\n\t\tif ( this[0] && this[0].parentNode ) {\n\t\t\treturn this.domManip(arguments, false, function( elem ) {\n\t\t\t\tthis.parentNode.insertBefore( elem, this );\n\t\t\t});\n\t\t} else if ( arguments.length ) {\n\t\t\tvar set = jQuery.clean( arguments );\n\t\t\tset.push.apply( set, this.toArray() );\n\t\t\treturn this.pushStack( set, \"before\", arguments );\n\t\t}\n\t},\n\n\tafter: function() {\n\t\tif ( this[0] && this[0].parentNode ) {\n\t\t\treturn this.domManip(arguments, false, function( elem ) {\n\t\t\t\tthis.parentNode.insertBefore( elem, this.nextSibling );\n\t\t\t});\n\t\t} else if ( arguments.length ) {\n\t\t\tvar set = this.pushStack( this, \"after\", arguments );\n\t\t\tset.push.apply( set, jQuery.clean(arguments) );\n\t\t\treturn set;\n\t\t}\n\t},\n\n\t// keepData is for internal use only--do not document\n\tremove: function( selector, keepData ) {\n\t\tfor ( var i = 0, elem; (elem = this[i]) != null; i++ ) {\n\t\t\tif ( !selector || jQuery.filter( selector, [ elem ] ).length ) {\n\t\t\t\tif ( !keepData && elem.nodeType === 1 ) {\n\t\t\t\t\tjQuery.cleanData( elem.getElementsByTagName(\"*\") );\n\t\t\t\t\tjQuery.cleanData( [ elem ] );\n\t\t\t\t}\n\n\t\t\t\tif ( elem.parentNode ) {\n\t\t\t\t\telem.parentNode.removeChild( elem );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\tempty: function() {\n\t\tfor ( var i = 0, elem; (elem = this[i]) != null; i++ ) {\n\t\t\t// Remove element nodes and prevent memory leaks\n\t\t\tif ( elem.nodeType === 1 ) {\n\t\t\t\tjQuery.cleanData( elem.getElementsByTagName(\"*\") );\n\t\t\t}\n\n\t\t\t// Remove any remaining nodes\n\t\t\twhile ( elem.firstChild ) {\n\t\t\t\telem.removeChild( elem.firstChild );\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t},\n\n\tclone: function( dataAndEvents, deepDataAndEvents ) {\n\t\tdataAndEvents = dataAndEvents == null ? false : dataAndEvents;\n\t\tdeepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents;\n\n\t\treturn this.map( function () {\n\t\t\treturn jQuery.clone( this, dataAndEvents, deepDataAndEvents );\n\t\t});\n\t},\n\n\thtml: function( value ) {\n\t\tif ( value === undefined ) {\n\t\t\treturn this[0] && this[0].nodeType === 1 ?\n\t\t\t\tthis[0].innerHTML.replace(rinlinejQuery, \"\") :\n\t\t\t\tnull;\n\n\t\t// See if we can take a shortcut and just use innerHTML\n\t\t} else if ( typeof value === \"string\" && !rnoInnerhtml.test( value ) &&\n\t\t\t(jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value )) &&\n\t\t\t!wrapMap[ (rtagName.exec( value ) || [\"\", \"\"])[1].toLowerCase() ] ) {\n\n\t\t\tvalue = value.replace(rxhtmlTag, \"<$1></$2>\");\n\n\t\t\ttry {\n\t\t\t\tfor ( var i = 0, l = this.length; i < l; i++ ) {\n\t\t\t\t\t// Remove element nodes and prevent memory leaks\n\t\t\t\t\tif ( this[i].nodeType === 1 ) {\n\t\t\t\t\t\tjQuery.cleanData( this[i].getElementsByTagName(\"*\") );\n\t\t\t\t\t\tthis[i].innerHTML = value;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t// If using innerHTML throws an exception, use the fallback method\n\t\t\t} catch(e) {\n\t\t\t\tthis.empty().append( value );\n\t\t\t}\n\n\t\t} else if ( jQuery.isFunction( value ) ) {\n\t\t\tthis.each(function(i){\n\t\t\t\tvar self = jQuery( this );\n\n\t\t\t\tself.html( value.call(this, i, self.html()) );\n\t\t\t});\n\n\t\t} else {\n\t\t\tthis.empty().append( value );\n\t\t}\n\n\t\treturn this;\n\t},\n\n\treplaceWith: function( value ) {\n\t\tif ( this[0] && this[0].parentNode ) {\n\t\t\t// Make sure that the elements are removed from the DOM before they are inserted\n\t\t\t// this can help fix replacing a parent with child elements\n\t\t\tif ( jQuery.isFunction( value ) ) {\n\t\t\t\treturn this.each(function(i) {\n\t\t\t\t\tvar self = jQuery(this), old = self.html();\n\t\t\t\t\tself.replaceWith( value.call( this, i, old ) );\n\t\t\t\t});\n\t\t\t}\n\n\t\t\tif ( typeof value !== \"string\" ) {\n\t\t\t\tvalue = jQuery( value ).detach();\n\t\t\t}\n\n\t\t\treturn this.each(function() {\n\t\t\t\tvar next = this.nextSibling,\n\t\t\t\t\tparent = this.parentNode;\n\n\t\t\t\tjQuery( this ).remove();\n\n\t\t\t\tif ( next ) {\n\t\t\t\t\tjQuery(next).before( value );\n\t\t\t\t} else {\n\t\t\t\t\tjQuery(parent).append( value );\n\t\t\t\t}\n\t\t\t});\n\t\t} else {\n\t\t\treturn this.length ?\n\t\t\t\tthis.pushStack( jQuery(jQuery.isFunction(value) ? value() : value), \"replaceWith\", value ) :\n\t\t\t\tthis;\n\t\t}\n\t},\n\n\tdetach: function( selector ) {\n\t\treturn this.remove( selector, true );\n\t},\n\n\tdomManip: function( args, table, callback ) {\n\t\tvar results, first, fragment, parent,\n\t\t\tvalue = args[0],\n\t\t\tscripts = [];\n\n\t\t// We can't cloneNode fragments that contain checked, in WebKit\n\t\tif ( !jQuery.support.checkClone && arguments.length === 3 && typeof value === \"string\" && rchecked.test( value ) ) {\n\t\t\treturn this.each(function() {\n\t\t\t\tjQuery(this).domManip( args, table, callback, true );\n\t\t\t});\n\t\t}\n\n\t\tif ( jQuery.isFunction(value) ) {\n\t\t\treturn this.each(function(i) {\n\t\t\t\tvar self = jQuery(this);\n\t\t\t\targs[0] = value.call(this, i, table ? self.html() : undefined);\n\t\t\t\tself.domManip( args, table, callback );\n\t\t\t});\n\t\t}\n\n\t\tif ( this[0] ) {\n\t\t\tparent = value && value.parentNode;\n\n\t\t\t// If we're in a fragment, just use that instead of building a new one\n\t\t\tif ( jQuery.support.parentNode && parent && parent.nodeType === 11 && parent.childNodes.length === this.length ) {\n\t\t\t\tresults = { fragment: parent };\n\n\t\t\t} else {\n\t\t\t\tresults = jQuery.buildFragment( args, this, scripts );\n\t\t\t}\n\n\t\t\tfragment = results.fragment;\n\n\t\t\tif ( fragment.childNodes.length === 1 ) {\n\t\t\t\tfirst = fragment = fragment.firstChild;\n\t\t\t} else {\n\t\t\t\tfirst = fragment.firstChild;\n\t\t\t}\n\n\t\t\tif ( first ) {\n\t\t\t\ttable = table && jQuery.nodeName( first, \"tr\" );\n\n\t\t\t\tfor ( var i = 0, l = this.length, lastIndex = l - 1; i < l; i++ ) {\n\t\t\t\t\tcallback.call(\n\t\t\t\t\t\ttable ?\n\t\t\t\t\t\t\troot(this[i], first) :\n\t\t\t\t\t\t\tthis[i],\n\t\t\t\t\t\t// Make sure that we do not leak memory by inadvertently discarding\n\t\t\t\t\t\t// the original fragment (which might have attached data) instead of\n\t\t\t\t\t\t// using it; in addition, use the original fragment object for the last\n\t\t\t\t\t\t// item instead of first because it can end up being emptied incorrectly\n\t\t\t\t\t\t// in certain situations (Bug #8070).\n\t\t\t\t\t\t// Fragments from the fragment cache must always be cloned and never used\n\t\t\t\t\t\t// in place.\n\t\t\t\t\t\tresults.cacheable || ( l > 1 && i < lastIndex ) ?\n\t\t\t\t\t\t\tjQuery.clone( fragment, true, true ) :\n\t\t\t\t\t\t\tfragment\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( scripts.length ) {\n\t\t\t\tjQuery.each( scripts, evalScript );\n\t\t\t}\n\t\t}\n\n\t\treturn this;\n\t}\n});\n\nfunction root( elem, cur ) {\n\treturn jQuery.nodeName(elem, \"table\") ?\n\t\t(elem.getElementsByTagName(\"tbody\")[0] ||\n\t\telem.appendChild(elem.ownerDocument.createElement(\"tbody\"))) :\n\t\telem;\n}\n\nfunction cloneCopyEvent( src, dest ) {\n\n\tif ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) {\n\t\treturn;\n\t}\n\n\tvar type, i, l,\n\t\toldData = jQuery._data( src ),\n\t\tcurData = jQuery._data( dest, oldData ),\n\t\tevents = oldData.events;\n\n\tif ( events ) {\n\t\tdelete curData.handle;\n\t\tcurData.events = {};\n\n\t\tfor ( type in events ) {\n\t\t\tfor ( i = 0, l = events[ type ].length; i < l; i++ ) {\n\t\t\t\tjQuery.event.add( dest, type + ( events[ type ][ i ].namespace ? \".\" : \"\" ) + events[ type ][ i ].namespace, events[ type ][ i ], events[ type ][ i ].data );\n\t\t\t}\n\t\t}\n\t}\n\n\t// make the cloned public data object a copy from the original\n\tif ( curData.data ) {\n\t\tcurData.data = jQuery.extend( {}, curData.data );\n\t}\n}\n\nfunction cloneFixAttributes( src, dest ) {\n\tvar nodeName;\n\n\t// We do not need to do anything for non-Elements\n\tif ( dest.nodeType !== 1 ) {\n\t\treturn;\n\t}\n\n\t// clearAttributes removes the attributes, which we don't want,\n\t// but also removes the attachEvent events, which we *do* want\n\tif ( dest.clearAttributes ) {\n\t\tdest.clearAttributes();\n\t}\n\n\t// mergeAttributes, in contrast, only merges back on the\n\t// original attributes, not the events\n\tif ( dest.mergeAttributes ) {\n\t\tdest.mergeAttributes( src );\n\t}\n\n\tnodeName = dest.nodeName.toLowerCase();\n\n\t// IE6-8 fail to clone children inside object elements that use\n\t// the proprietary classid attribute value (rather than the type\n\t// attribute) to identify the type of content to display\n\tif ( nodeName === \"object\" ) {\n\t\tdest.outerHTML = src.outerHTML;\n\n\t} else if ( nodeName === \"input\" && (src.type === \"checkbox\" || src.type === \"radio\") ) {\n\t\t// IE6-8 fails to persist the checked state of a cloned checkbox\n\t\t// or radio button. Worse, IE6-7 fail to give the cloned element\n\t\t// a checked appearance if the defaultChecked value isn't also set\n\t\tif ( src.checked ) {\n\t\t\tdest.defaultChecked = dest.checked = src.checked;\n\t\t}\n\n\t\t// IE6-7 get confused and end up setting the value of a cloned\n\t\t// checkbox/radio button to an empty string instead of \"on\"\n\t\tif ( dest.value !== src.value ) {\n\t\t\tdest.value = src.value;\n\t\t}\n\n\t// IE6-8 fails to return the selected option to the default selected\n\t// state when cloning options\n\t} else if ( nodeName === \"option\" ) {\n\t\tdest.selected = src.defaultSelected;\n\n\t// IE6-8 fails to set the defaultValue to the correct value when\n\t// cloning other types of input fields\n\t} else if ( nodeName === \"input\" || nodeName === \"textarea\" ) {\n\t\tdest.defaultValue = src.defaultValue;\n\t}\n\n\t// Event data gets referenced instead of copied if the expando\n\t// gets copied too\n\tdest.removeAttribute( jQuery.expando );\n}\n\njQuery.buildFragment = function( args, nodes, scripts ) {\n\tvar fragment, cacheable, cacheresults, doc,\n\tfirst = args[ 0 ];\n\n\t// nodes may contain either an explicit document object,\n\t// a jQuery collection or context object.\n\t// If nodes[0] contains a valid object to assign to doc\n\tif ( nodes && nodes[0] ) {\n\t\tdoc = nodes[0].ownerDocument || nodes[0];\n\t}\n\n\t// Ensure that an attr object doesn't incorrectly stand in as a document object\n\t// Chrome and Firefox seem to allow this to occur and will throw exception\n\t// Fixes #8950\n\tif ( !doc.createDocumentFragment ) {\n\t\tdoc = document;\n\t}\n\n\t// Only cache \"small\" (1/2 KB) HTML strings that are associated with the main document\n\t// Cloning options loses the selected state, so don't cache them\n\t// IE 6 doesn't like it when you put <object> or <embed> elements in a fragment\n\t// Also, WebKit does not clone 'checked' attributes on cloneNode, so don't cache\n\t// Lastly, IE6,7,8 will not correctly reuse cached fragments that were created from unknown elems #10501\n\tif ( args.length === 1 && typeof first === \"string\" && first.length < 512 && doc === document &&\n\t\tfirst.charAt(0) === \"<\" && !rnocache.test( first ) &&\n\t\t(jQuery.support.checkClone || !rchecked.test( first )) &&\n\t\t(jQuery.support.html5Clone || !rnoshimcache.test( first )) ) {\n\n\t\tcacheable = true;\n\n\t\tcacheresults = jQuery.fragments[ first ];\n\t\tif ( cacheresults && cacheresults !== 1 ) {\n\t\t\tfragment = cacheresults;\n\t\t}\n\t}\n\n\tif ( !fragment ) {\n\t\tfragment = doc.createDocumentFragment();\n\t\tjQuery.clean( args, doc, fragment, scripts );\n\t}\n\n\tif ( cacheable ) {\n\t\tjQuery.fragments[ first ] = cacheresults ? fragment : 1;\n\t}\n\n\treturn { fragment: fragment, cacheable: cacheable };\n};\n\njQuery.fragments = {};\n\njQuery.each({\n\tappendTo: \"append\",\n\tprependTo: \"prepend\",\n\tinsertBefore: \"before\",\n\tinsertAfter: \"after\",\n\treplaceAll: \"replaceWith\"\n}, function( name, original ) {\n\tjQuery.fn[ name ] = function( selector ) {\n\t\tvar ret = [],\n\t\t\tinsert = jQuery( selector ),\n\t\t\tparent = this.length === 1 && this[0].parentNode;\n\n\t\tif ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) {\n\t\t\tinsert[ original ]( this[0] );\n\t\t\treturn this;\n\n\t\t} else {\n\t\t\tfor ( var i = 0, l = insert.length; i < l; i++ ) {\n\t\t\t\tvar elems = ( i > 0 ? this.clone(true) : this ).get();\n\t\t\t\tjQuery( insert[i] )[ original ]( elems );\n\t\t\t\tret = ret.concat( elems );\n\t\t\t}\n\n\t\t\treturn this.pushStack( ret, name, insert.selector );\n\t\t}\n\t};\n});\n\nfunction getAll( elem ) {\n\tif ( typeof elem.getElementsByTagName !== \"undefined\" ) {\n\t\treturn elem.getElementsByTagName( \"*\" );\n\n\t} else if ( typeof elem.querySelectorAll !== \"undefined\" ) {\n\t\treturn elem.querySelectorAll( \"*\" );\n\n\t} else {\n\t\treturn [];\n\t}\n}\n\n// Used in clean, fixes the defaultChecked property\nfunction fixDefaultChecked( elem ) {\n\tif ( elem.type === \"checkbox\" || elem.type === \"radio\" ) {\n\t\telem.defaultChecked = elem.checked;\n\t}\n}\n// Finds all inputs and passes them to fixDefaultChecked\nfunction findInputs( elem ) {\n\tvar nodeName = ( elem.nodeName || \"\" ).toLowerCase();\n\tif ( nodeName === \"input\" ) {\n\t\tfixDefaultChecked( elem );\n\t// Skip scripts, get other children\n\t} else if ( nodeName !== \"script\" && typeof elem.getElementsByTagName !== \"undefined\" ) {\n\t\tjQuery.grep( elem.getElementsByTagName(\"input\"), fixDefaultChecked );\n\t}\n}\n\n// Derived From: http://www.iecss.com/shimprove/javascript/shimprove.1-0-1.js\nfunction shimCloneNode( elem ) {\n\tvar div = document.createElement( \"div\" );\n\tsafeFragment.appendChild( div );\n\n\tdiv.innerHTML = elem.outerHTML;\n\treturn div.firstChild;\n}\n\njQuery.extend({\n\tclone: function( elem, dataAndEvents, deepDataAndEvents ) {\n\t\tvar srcElements,\n\t\t\tdestElements,\n\t\t\ti,\n\t\t\t// IE<=8 does not properly clone detached, unknown element nodes\n\t\t\tclone = jQuery.support.html5Clone || !rnoshimcache.test( \"<\" + elem.nodeName ) ?\n\t\t\t\telem.cloneNode( true ) :\n\t\t\t\tshimCloneNode( elem );\n\n\t\tif ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) &&\n\t\t\t\t(elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) {\n\t\t\t// IE copies events bound via attachEvent when using cloneNode.\n\t\t\t// Calling detachEvent on the clone will also remove the events\n\t\t\t// from the original. In order to get around this, we use some\n\t\t\t// proprietary methods to clear the events. Thanks to MooTools\n\t\t\t// guys for this hotness.\n\n\t\t\tcloneFixAttributes( elem, clone );\n\n\t\t\t// Using Sizzle here is crazy slow, so we use getElementsByTagName instead\n\t\t\tsrcElements = getAll( elem );\n\t\t\tdestElements = getAll( clone );\n\n\t\t\t// Weird iteration because IE will replace the length property\n\t\t\t// with an element if you are cloning the body and one of the\n\t\t\t// elements on the page has a name or id of \"length\"\n\t\t\tfor ( i = 0; srcElements[i]; ++i ) {\n\t\t\t\t// Ensure that the destination node is not null; Fixes #9587\n\t\t\t\tif ( destElements[i] ) {\n\t\t\t\t\tcloneFixAttributes( srcElements[i], destElements[i] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Copy the events from the original to the clone\n\t\tif ( dataAndEvents ) {\n\t\t\tcloneCopyEvent( elem, clone );\n\n\t\t\tif ( deepDataAndEvents ) {\n\t\t\t\tsrcElements = getAll( elem );\n\t\t\t\tdestElements = getAll( clone );\n\n\t\t\t\tfor ( i = 0; srcElements[i]; ++i ) {\n\t\t\t\t\tcloneCopyEvent( srcElements[i], destElements[i] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tsrcElements = destElements = null;\n\n\t\t// Return the cloned set\n\t\treturn clone;\n\t},\n\n\tclean: function( elems, context, fragment, scripts ) {\n\t\tvar checkScriptType;\n\n\t\tcontext = context || document;\n\n\t\t// !context.createElement fails in IE with an error but returns typeof 'object'\n\t\tif ( typeof context.createElement === \"undefined\" ) {\n\t\t\tcontext = context.ownerDocument || context[0] && context[0].ownerDocument || document;\n\t\t}\n\n\t\tvar ret = [], j;\n\n\t\tfor ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {\n\t\t\tif ( typeof elem === \"number\" ) {\n\t\t\t\telem += \"\";\n\t\t\t}\n\n\t\t\tif ( !elem ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Convert html string into DOM nodes\n\t\t\tif ( typeof elem === \"string\" ) {\n\t\t\t\tif ( !rhtml.test( elem ) ) {\n\t\t\t\t\telem = context.createTextNode( elem );\n\t\t\t\t} else {\n\t\t\t\t\t// Fix \"XHTML\"-style tags in all browsers\n\t\t\t\t\telem = elem.replace(rxhtmlTag, \"<$1></$2>\");\n\n\t\t\t\t\t// Trim whitespace, otherwise indexOf won't work as expected\n\t\t\t\t\tvar tag = ( rtagName.exec( elem ) || [\"\", \"\"] )[1].toLowerCase(),\n\t\t\t\t\t\twrap = wrapMap[ tag ] || wrapMap._default,\n\t\t\t\t\t\tdepth = wrap[0],\n\t\t\t\t\t\tdiv = context.createElement(\"div\");\n\n\t\t\t\t\t// Append wrapper element to unknown element safe doc fragment\n\t\t\t\t\tif ( context === document ) {\n\t\t\t\t\t\t// Use the fragment we've already created for this document\n\t\t\t\t\t\tsafeFragment.appendChild( div );\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Use a fragment created with the owner document\n\t\t\t\t\t\tcreateSafeFragment( context ).appendChild( div );\n\t\t\t\t\t}\n\n\t\t\t\t\t// Go to html and back, then peel off extra wrappers\n\t\t\t\t\tdiv.innerHTML = wrap[1] + elem + wrap[2];\n\n\t\t\t\t\t// Move to the right depth\n\t\t\t\t\twhile ( depth-- ) {\n\t\t\t\t\t\tdiv = div.lastChild;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Remove IE's autoinserted <tbody> from table fragments\n\t\t\t\t\tif ( !jQuery.support.tbody ) {\n\n\t\t\t\t\t\t// String was a <table>, *may* have spurious <tbody>\n\t\t\t\t\t\tvar hasBody = rtbody.test(elem),\n\t\t\t\t\t\t\ttbody = tag === \"table\" && !hasBody ?\n\t\t\t\t\t\t\t\tdiv.firstChild && div.firstChild.childNodes :\n\n\t\t\t\t\t\t\t\t// String was a bare <thead> or <tfoot>\n\t\t\t\t\t\t\t\twrap[1] === \"<table>\" && !hasBody ?\n\t\t\t\t\t\t\t\t\tdiv.childNodes :\n\t\t\t\t\t\t\t\t\t[];\n\n\t\t\t\t\t\tfor ( j = tbody.length - 1; j >= 0 ; --j ) {\n\t\t\t\t\t\t\tif ( jQuery.nodeName( tbody[ j ], \"tbody\" ) && !tbody[ j ].childNodes.length ) {\n\t\t\t\t\t\t\t\ttbody[ j ].parentNode.removeChild( tbody[ j ] );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// IE completely kills leading whitespace when innerHTML is used\n\t\t\t\t\tif ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) {\n\t\t\t\t\t\tdiv.insertBefore( context.createTextNode( rleadingWhitespace.exec(elem)[0] ), div.firstChild );\n\t\t\t\t\t}\n\n\t\t\t\t\telem = div.childNodes;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Resets defaultChecked for any radios and checkboxes\n\t\t\t// about to be appended to the DOM in IE 6/7 (#8060)\n\t\t\tvar len;\n\t\t\tif ( !jQuery.support.appendChecked ) {\n\t\t\t\tif ( elem[0] && typeof (len = elem.length) === \"number\" ) {\n\t\t\t\t\tfor ( j = 0; j < len; j++ ) {\n\t\t\t\t\t\tfindInputs( elem[j] );\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tfindInputs( elem );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( elem.nodeType ) {\n\t\t\t\tret.push( elem );\n\t\t\t} else {\n\t\t\t\tret = jQuery.merge( ret, elem );\n\t\t\t}\n\t\t}\n\n\t\tif ( fragment ) {\n\t\t\tcheckScriptType = function( elem ) {\n\t\t\t\treturn !elem.type || rscriptType.test( elem.type );\n\t\t\t};\n\t\t\tfor ( i = 0; ret[i]; i++ ) {\n\t\t\t\tif ( scripts && jQuery.nodeName( ret[i], \"script\" ) && (!ret[i].type || ret[i].type.toLowerCase() === \"text/javascript\") ) {\n\t\t\t\t\tscripts.push( ret[i].parentNode ? ret[i].parentNode.removeChild( ret[i] ) : ret[i] );\n\n\t\t\t\t} else {\n\t\t\t\t\tif ( ret[i].nodeType === 1 ) {\n\t\t\t\t\t\tvar jsTags = jQuery.grep( ret[i].getElementsByTagName( \"script\" ), checkScriptType );\n\n\t\t\t\t\t\tret.splice.apply( ret, [i + 1, 0].concat( jsTags ) );\n\t\t\t\t\t}\n\t\t\t\t\tfragment.appendChild( ret[i] );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn ret;\n\t},\n\n\tcleanData: function( elems ) {\n\t\tvar data, id,\n\t\t\tcache = jQuery.cache,\n\t\t\tspecial = jQuery.event.special,\n\t\t\tdeleteExpando = jQuery.support.deleteExpando;\n\n\t\tfor ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {\n\t\t\tif ( elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()] ) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tid = elem[ jQuery.expando ];\n\n\t\t\tif ( id ) {\n\t\t\t\tdata = cache[ id ];\n\n\t\t\t\tif ( data && data.events ) {\n\t\t\t\t\tfor ( var type in data.events ) {\n\t\t\t\t\t\tif ( special[ type ] ) {\n\t\t\t\t\t\t\tjQuery.event.remove( elem, type );\n\n\t\t\t\t\t\t// This is a shortcut to avoid jQuery.event.remove's overhead\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tjQuery.removeEvent( elem, type, data.handle );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Null the DOM reference to avoid IE6/7/8 leak (#7054)\n\t\t\t\t\tif ( data.handle ) {\n\t\t\t\t\t\tdata.handle.elem = null;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif ( deleteExpando ) {\n\t\t\t\t\tdelete elem[ jQuery.expando ];\n\n\t\t\t\t} else if ( elem.removeAttribute ) {\n\t\t\t\t\telem.removeAttribute( jQuery.expando );\n\t\t\t\t}\n\n\t\t\t\tdelete cache[ id ];\n\t\t\t}\n\t\t}\n\t}\n});\n\nfunction evalScript( i, elem ) {\n\tif ( elem.src ) {\n\t\tjQuery.ajax({\n\t\t\turl: elem.src,\n\t\t\tasync: false,\n\t\t\tdataType: \"script\"\n\t\t});\n\t} else {\n\t\tjQuery.globalEval( ( elem.text || elem.textContent || elem.innerHTML || \"\" ).replace( rcleanScript, \"/*$0*/\" ) );\n\t}\n\n\tif ( elem.parentNode ) {\n\t\telem.parentNode.removeChild( elem );\n\t}\n}\n\n\n\n\nvar ralpha = /alpha\\([^)]*\\)/i,\n\tropacity = /opacity=([^)]*)/,\n\t// fixed for IE9, see #8346\n\trupper = /([A-Z]|^ms)/g,\n\trnumpx = /^-?\\d+(?:px)?$/i,\n\trnum = /^-?\\d/,\n\trrelNum = /^([\\-+])=([\\-+.\\de]+)/,\n\n\tcssShow = { position: \"absolute\", visibility: \"hidden\", display: \"block\" },\n\tcssWidth = [ \"Left\", \"Right\" ],\n\tcssHeight = [ \"Top\", \"Bottom\" ],\n\tcurCSS,\n\n\tgetComputedStyle,\n\tcurrentStyle;\n\njQuery.fn.css = function( name, value ) {\n\t// Setting 'undefined' is a no-op\n\tif ( arguments.length === 2 && value === undefined ) {\n\t\treturn this;\n\t}\n\n\treturn jQuery.access( this, name, value, true, function( elem, name, value ) {\n\t\treturn value !== undefined ?\n\t\t\tjQuery.style( elem, name, value ) :\n\t\t\tjQuery.css( elem, name );\n\t});\n};\n\njQuery.extend({\n\t// Add in style property hooks for overriding the default\n\t// behavior of getting and setting a style property\n\tcssHooks: {\n\t\topacity: {\n\t\t\tget: function( elem, computed ) {\n\t\t\t\tif ( computed ) {\n\t\t\t\t\t// We should always get a number back from opacity\n\t\t\t\t\tvar ret = curCSS( elem, \"opacity\", \"opacity\" );\n\t\t\t\t\treturn ret === \"\" ? \"1\" : ret;\n\n\t\t\t\t} else {\n\t\t\t\t\treturn elem.style.opacity;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t},\n\n\t// Exclude the following css properties to add px\n\tcssNumber: {\n\t\t\"fillOpacity\": true,\n\t\t\"fontWeight\": true,\n\t\t\"lineHeight\": true,\n\t\t\"opacity\": true,\n\t\t\"orphans\": true,\n\t\t\"widows\": true,\n\t\t\"zIndex\": true,\n\t\t\"zoom\": true\n\t},\n\n\t// Add in properties whose names you wish to fix before\n\t// setting or getting the value\n\tcssProps: {\n\t\t// normalize float css property\n\t\t\"float\": jQuery.support.cssFloat ? \"cssFloat\" : \"styleFloat\"\n\t},\n\n\t// Get and set the style property on a DOM Node\n\tstyle: function( elem, name, value, extra ) {\n\t\t// Don't set styles on text and comment nodes\n\t\tif ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Make sure that we're working with the right name\n\t\tvar ret, type, origName = jQuery.camelCase( name ),\n\t\t\tstyle = elem.style, hooks = jQuery.cssHooks[ origName ];\n\n\t\tname = jQuery.cssProps[ origName ] || origName;\n\n\t\t// Check if we're setting a value\n\t\tif ( value !== undefined ) {\n\t\t\ttype = typeof value;\n\n\t\t\t// convert relative number strings (+= or -=) to relative numbers. #7345\n\t\t\tif ( type === \"string\" && (ret = rrelNum.exec( value )) ) {\n\t\t\t\tvalue = ( +( ret[1] + 1) * +ret[2] ) + parseFloat( jQuery.css( elem, name ) );\n\t\t\t\t// Fixes bug #9237\n\t\t\t\ttype = \"number\";\n\t\t\t}\n\n\t\t\t// Make sure that NaN and null values aren't set. See: #7116\n\t\t\tif ( value == null || type === \"number\" && isNaN( value ) ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// If a number was passed in, add 'px' to the (except for certain CSS properties)\n\t\t\tif ( type === \"number\" && !jQuery.cssNumber[ origName ] ) {\n\t\t\t\tvalue += \"px\";\n\t\t\t}\n\n\t\t\t// If a hook was provided, use that value, otherwise just set the specified value\n\t\t\tif ( !hooks || !(\"set\" in hooks) || (value = hooks.set( elem, value )) !== undefined ) {\n\t\t\t\t// Wrapped to prevent IE from throwing errors when 'invalid' values are provided\n\t\t\t\t// Fixes bug #5509\n\t\t\t\ttry {\n\t\t\t\t\tstyle[ name ] = value;\n\t\t\t\t} catch(e) {}\n\t\t\t}\n\n\t\t} else {\n\t\t\t// If a hook was provided get the non-computed value from there\n\t\t\tif ( hooks && \"get\" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) {\n\t\t\t\treturn ret;\n\t\t\t}\n\n\t\t\t// Otherwise just get the value from the style object\n\t\t\treturn style[ name ];\n\t\t}\n\t},\n\n\tcss: function( elem, name, extra ) {\n\t\tvar ret, hooks;\n\n\t\t// Make sure that we're working with the right name\n\t\tname = jQuery.camelCase( name );\n\t\thooks = jQuery.cssHooks[ name ];\n\t\tname = jQuery.cssProps[ name ] || name;\n\n\t\t// cssFloat needs a special treatment\n\t\tif ( name === \"cssFloat\" ) {\n\t\t\tname = \"float\";\n\t\t}\n\n\t\t// If a hook was provided get the computed value from there\n\t\tif ( hooks && \"get\" in hooks && (ret = hooks.get( elem, true, extra )) !== undefined ) {\n\t\t\treturn ret;\n\n\t\t// Otherwise, if a way to get the computed value exists, use that\n\t\t} else if ( curCSS ) {\n\t\t\treturn curCSS( elem, name );\n\t\t}\n\t},\n\n\t// A method for quickly swapping in/out CSS properties to get correct calculations\n\tswap: function( elem, options, callback ) {\n\t\tvar old = {};\n\n\t\t// Remember the old values, and insert the new ones\n\t\tfor ( var name in options ) {\n\t\t\told[ name ] = elem.style[ name ];\n\t\t\telem.style[ name ] = options[ name ];\n\t\t}\n\n\t\tcallback.call( elem );\n\n\t\t// Revert the old values\n\t\tfor ( name in options ) {\n\t\t\telem.style[ name ] = old[ name ];\n\t\t}\n\t}\n});\n\n// DEPRECATED, Use jQuery.css() instead\njQuery.curCSS = jQuery.css;\n\njQuery.each([\"height\", \"width\"], function( i, name ) {\n\tjQuery.cssHooks[ name ] = {\n\t\tget: function( elem, computed, extra ) {\n\t\t\tvar val;\n\n\t\t\tif ( computed ) {\n\t\t\t\tif ( elem.offsetWidth !== 0 ) {\n\t\t\t\t\treturn getWH( elem, name, extra );\n\t\t\t\t} else {\n\t\t\t\t\tjQuery.swap( elem, cssShow, function() {\n\t\t\t\t\t\tval = getWH( elem, name, extra );\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\treturn val;\n\t\t\t}\n\t\t},\n\n\t\tset: function( elem, value ) {\n\t\t\tif ( rnumpx.test( value ) ) {\n\t\t\t\t// ignore negative width and height values #1599\n\t\t\t\tvalue = parseFloat( value );\n\n\t\t\t\tif ( value >= 0 ) {\n\t\t\t\t\treturn value + \"px\";\n\t\t\t\t}\n\n\t\t\t} else {\n\t\t\t\treturn value;\n\t\t\t}\n\t\t}\n\t};\n});\n\nif ( !jQuery.support.opacity ) {\n\tjQuery.cssHooks.opacity = {\n\t\tget: function( elem, computed ) {\n\t\t\t// IE uses filters for opacity\n\t\t\treturn ropacity.test( (computed && elem.currentStyle ? elem.currentStyle.filter : elem.style.filter) || \"\" ) ?\n\t\t\t\t( parseFloat( RegExp.$1 ) / 100 ) + \"\" :\n\t\t\t\tcomputed ? \"1\" : \"\";\n\t\t},\n\n\t\tset: function( elem, value ) {\n\t\t\tvar style = elem.style,\n\t\t\t\tcurrentStyle = elem.currentStyle,\n\t\t\t\topacity = jQuery.isNumeric( value ) ? \"alpha(opacity=\" + value * 100 + \")\" : \"\",\n\t\t\t\tfilter = currentStyle && currentStyle.filter || style.filter || \"\";\n\n\t\t\t// IE has trouble with opacity if it does not have layout\n\t\t\t// Force it by setting the zoom level\n\t\t\tstyle.zoom = 1;\n\n\t\t\t// if setting opacity to 1, and no other filters exist - attempt to remove filter attribute #6652\n\t\t\tif ( value >= 1 && jQuery.trim( filter.replace( ralpha, \"\" ) ) === \"\" ) {\n\n\t\t\t\t// Setting style.filter to null, \"\" & \" \" still leave \"filter:\" in the cssText\n\t\t\t\t// if \"filter:\" is present at all, clearType is disabled, we want to avoid this\n\t\t\t\t// style.removeAttribute is IE Only, but so apparently is this code path...\n\t\t\t\tstyle.removeAttribute( \"filter\" );\n\n\t\t\t\t// if there there is no filter style applied in a css rule, we are done\n\t\t\t\tif ( currentStyle && !currentStyle.filter ) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// otherwise, set new filter values\n\t\t\tstyle.filter = ralpha.test( filter ) ?\n\t\t\t\tfilter.replace( ralpha, opacity ) :\n\t\t\t\tfilter + \" \" + opacity;\n\t\t}\n\t};\n}\n\njQuery(function() {\n\t// This hook cannot be added until DOM ready because the support test\n\t// for it is not run until after DOM ready\n\tif ( !jQuery.support.reliableMarginRight ) {\n\t\tjQuery.cssHooks.marginRight = {\n\t\t\tget: function( elem, computed ) {\n\t\t\t\t// WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right\n\t\t\t\t// Work around by temporarily setting element display to inline-block\n\t\t\t\tvar ret;\n\t\t\t\tjQuery.swap( elem, { \"display\": \"inline-block\" }, function() {\n\t\t\t\t\tif ( computed ) {\n\t\t\t\t\t\tret = curCSS( elem, \"margin-right\", \"marginRight\" );\n\t\t\t\t\t} else {\n\t\t\t\t\t\tret = elem.style.marginRight;\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\treturn ret;\n\t\t\t}\n\t\t};\n\t}\n});\n\nif ( document.defaultView && document.defaultView.getComputedStyle ) {\n\tgetComputedStyle = function( elem, name ) {\n\t\tvar ret, defaultView, computedStyle;\n\n\t\tname = name.replace( rupper, \"-$1\" ).toLowerCase();\n\n\t\tif ( (defaultView = elem.ownerDocument.defaultView) &&\n\t\t\t\t(computedStyle = defaultView.getComputedStyle( elem, null )) ) {\n\t\t\tret = computedStyle.getPropertyValue( name );\n\t\t\tif ( ret === \"\" && !jQuery.contains( elem.ownerDocument.documentElement, elem ) ) {\n\t\t\t\tret = jQuery.style( elem, name );\n\t\t\t}\n\t\t}\n\n\t\treturn ret;\n\t};\n}\n\nif ( document.documentElement.currentStyle ) {\n\tcurrentStyle = function( elem, name ) {\n\t\tvar left, rsLeft, uncomputed,\n\t\t\tret = elem.currentStyle && elem.currentStyle[ name ],\n\t\t\tstyle = elem.style;\n\n\t\t// Avoid setting ret to empty string here\n\t\t// so we don't default to auto\n\t\tif ( ret === null && style && (uncomputed = style[ name ]) ) {\n\t\t\tret = uncomputed;\n\t\t}\n\n\t\t// From the awesome hack by Dean Edwards\n\t\t// http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291\n\n\t\t// If we're not dealing with a regular pixel number\n\t\t// but a number that has a weird ending, we need to convert it to pixels\n\t\tif ( !rnumpx.test( ret ) && rnum.test( ret ) ) {\n\n\t\t\t// Remember the original values\n\t\t\tleft = style.left;\n\t\t\trsLeft = elem.runtimeStyle && elem.runtimeStyle.left;\n\n\t\t\t// Put in the new values to get a computed value out\n\t\t\tif ( rsLeft ) {\n\t\t\t\telem.runtimeStyle.left = elem.currentStyle.left;\n\t\t\t}\n\t\t\tstyle.left = name === \"fontSize\" ? \"1em\" : ( ret || 0 );\n\t\t\tret = style.pixelLeft + \"px\";\n\n\t\t\t// Revert the changed values\n\t\t\tstyle.left = left;\n\t\t\tif ( rsLeft ) {\n\t\t\t\telem.runtimeStyle.left = rsLeft;\n\t\t\t}\n\t\t}\n\n\t\treturn ret === \"\" ? \"auto\" : ret;\n\t};\n}\n\ncurCSS = getComputedStyle || currentStyle;\n\nfunction getWH( elem, name, extra ) {\n\n\t// Start with offset property\n\tvar val = name === \"width\" ? elem.offsetWidth : elem.offsetHeight,\n\t\twhich = name === \"width\" ? cssWidth : cssHeight,\n\t\ti = 0,\n\t\tlen = which.length;\n\n\tif ( val > 0 ) {\n\t\tif ( extra !== \"border\" ) {\n\t\t\tfor ( ; i < len; i++ ) {\n\t\t\t\tif ( !extra ) {\n\t\t\t\t\tval -= parseFloat( jQuery.css( elem, \"padding\" + which[ i ] ) ) || 0;\n\t\t\t\t}\n\t\t\t\tif ( extra === \"margin\" ) {\n\t\t\t\t\tval += parseFloat( jQuery.css( elem, extra + which[ i ] ) ) || 0;\n\t\t\t\t} else {\n\t\t\t\t\tval -= parseFloat( jQuery.css( elem, \"border\" + which[ i ] + \"Width\" ) ) || 0;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn val + \"px\";\n\t}\n\n\t// Fall back to computed then uncomputed css if necessary\n\tval = curCSS( elem, name, name );\n\tif ( val < 0 || val == null ) {\n\t\tval = elem.style[ name ] || 0;\n\t}\n\t// Normalize \"\", auto, and prepare for extra\n\tval = parseFloat( val ) || 0;\n\n\t// Add padding, border, margin\n\tif ( extra ) {\n\t\tfor ( ; i < len; i++ ) {\n\t\t\tval += parseFloat( jQuery.css( elem, \"padding\" + which[ i ] ) ) || 0;\n\t\t\tif ( extra !== \"padding\" ) {\n\t\t\t\tval += parseFloat( jQuery.css( elem, \"border\" + which[ i ] + \"Width\" ) ) || 0;\n\t\t\t}\n\t\t\tif ( extra === \"margin\" ) {\n\t\t\t\tval += parseFloat( jQuery.css( elem, extra + which[ i ] ) ) || 0;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn val + \"px\";\n}\n\nif ( jQuery.expr && jQuery.expr.filters ) {\n\tjQuery.expr.filters.hidden = function( elem ) {\n\t\tvar width = elem.offsetWidth,\n\t\t\theight = elem.offsetHeight;\n\n\t\treturn ( width === 0 && height === 0 ) || (!jQuery.support.reliableHiddenOffsets && ((elem.style && elem.style.display) || jQuery.css( elem, \"display\" )) === \"none\");\n\t};\n\n\tjQuery.expr.filters.visible = function( elem ) {\n\t\treturn !jQuery.expr.filters.hidden( elem );\n\t};\n}\n\n\n\n\nvar r20 = /%20/g,\n\trbracket = /\\[\\]$/,\n\trCRLF = /\\r?\\n/g,\n\trhash = /#.*$/,\n\trheaders = /^(.*?):[ \\t]*([^\\r\\n]*)\\r?$/mg, // IE leaves an \\r character at EOL\n\trinput = /^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,\n\t// #7653, #8125, #8152: local protocol detection\n\trlocalProtocol = /^(?:about|app|app\\-storage|.+\\-extension|file|res|widget):$/,\n\trnoContent = /^(?:GET|HEAD)$/,\n\trprotocol = /^\\/\\//,\n\trquery = /\\?/,\n\trscript = /<script\\b[^<]*(?:(?!<\\/script>)<[^<]*)*<\\/script>/gi,\n\trselectTextarea = /^(?:select|textarea)/i,\n\trspacesAjax = /\\s+/,\n\trts = /([?&])_=[^&]*/,\n\trurl = /^([\\w\\+\\.\\-]+:)(?:\\/\\/([^\\/?#:]*)(?::(\\d+))?)?/,\n\n\t// Keep a copy of the old load method\n\t_load = jQuery.fn.load,\n\n\t/* Prefilters\n\t * 1) They are useful to introduce custom dataTypes (see ajax/jsonp.js for an example)\n\t * 2) These are called:\n\t *    - BEFORE asking for a transport\n\t *    - AFTER param serialization (s.data is a string if s.processData is true)\n\t * 3) key is the dataType\n\t * 4) the catchall symbol \"*\" can be used\n\t * 5) execution will start with transport dataType and THEN continue down to \"*\" if needed\n\t */\n\tprefilters = {},\n\n\t/* Transports bindings\n\t * 1) key is the dataType\n\t * 2) the catchall symbol \"*\" can be used\n\t * 3) selection will start with transport dataType and THEN go to \"*\" if needed\n\t */\n\ttransports = {},\n\n\t// Document location\n\tajaxLocation,\n\n\t// Document location segments\n\tajaxLocParts,\n\n\t// Avoid comment-prolog char sequence (#10098); must appease lint and evade compression\n\tallTypes = [\"*/\"] + [\"*\"];\n\n// #8138, IE may throw an exception when accessing\n// a field from window.location if document.domain has been set\ntry {\n\tajaxLocation = location.href;\n} catch( e ) {\n\t// Use the href attribute of an A element\n\t// since IE will modify it given document.location\n\tajaxLocation = document.createElement( \"a\" );\n\tajaxLocation.href = \"\";\n\tajaxLocation = ajaxLocation.href;\n}\n\n// Segment location into parts\najaxLocParts = rurl.exec( ajaxLocation.toLowerCase() ) || [];\n\n// Base \"constructor\" for jQuery.ajaxPrefilter and jQuery.ajaxTransport\nfunction addToPrefiltersOrTransports( structure ) {\n\n\t// dataTypeExpression is optional and defaults to \"*\"\n\treturn function( dataTypeExpression, func ) {\n\n\t\tif ( typeof dataTypeExpression !== \"string\" ) {\n\t\t\tfunc = dataTypeExpression;\n\t\t\tdataTypeExpression = \"*\";\n\t\t}\n\n\t\tif ( jQuery.isFunction( func ) ) {\n\t\t\tvar dataTypes = dataTypeExpression.toLowerCase().split( rspacesAjax ),\n\t\t\t\ti = 0,\n\t\t\t\tlength = dataTypes.length,\n\t\t\t\tdataType,\n\t\t\t\tlist,\n\t\t\t\tplaceBefore;\n\n\t\t\t// For each dataType in the dataTypeExpression\n\t\t\tfor ( ; i < length; i++ ) {\n\t\t\t\tdataType = dataTypes[ i ];\n\t\t\t\t// We control if we're asked to add before\n\t\t\t\t// any existing element\n\t\t\t\tplaceBefore = /^\\+/.test( dataType );\n\t\t\t\tif ( placeBefore ) {\n\t\t\t\t\tdataType = dataType.substr( 1 ) || \"*\";\n\t\t\t\t}\n\t\t\t\tlist = structure[ dataType ] = structure[ dataType ] || [];\n\t\t\t\t// then we add to the structure accordingly\n\t\t\t\tlist[ placeBefore ? \"unshift\" : \"push\" ]( func );\n\t\t\t}\n\t\t}\n\t};\n}\n\n// Base inspection function for prefilters and transports\nfunction inspectPrefiltersOrTransports( structure, options, originalOptions, jqXHR,\n\t\tdataType /* internal */, inspected /* internal */ ) {\n\n\tdataType = dataType || options.dataTypes[ 0 ];\n\tinspected = inspected || {};\n\n\tinspected[ dataType ] = true;\n\n\tvar list = structure[ dataType ],\n\t\ti = 0,\n\t\tlength = list ? list.length : 0,\n\t\texecuteOnly = ( structure === prefilters ),\n\t\tselection;\n\n\tfor ( ; i < length && ( executeOnly || !selection ); i++ ) {\n\t\tselection = list[ i ]( options, originalOptions, jqXHR );\n\t\t// If we got redirected to another dataType\n\t\t// we try there if executing only and not done already\n\t\tif ( typeof selection === \"string\" ) {\n\t\t\tif ( !executeOnly || inspected[ selection ] ) {\n\t\t\t\tselection = undefined;\n\t\t\t} else {\n\t\t\t\toptions.dataTypes.unshift( selection );\n\t\t\t\tselection = inspectPrefiltersOrTransports(\n\t\t\t\t\t\tstructure, options, originalOptions, jqXHR, selection, inspected );\n\t\t\t}\n\t\t}\n\t}\n\t// If we're only executing or nothing was selected\n\t// we try the catchall dataType if not done already\n\tif ( ( executeOnly || !selection ) && !inspected[ \"*\" ] ) {\n\t\tselection = inspectPrefiltersOrTransports(\n\t\t\t\tstructure, options, originalOptions, jqXHR, \"*\", inspected );\n\t}\n\t// unnecessary when only executing (prefilters)\n\t// but it'll be ignored by the caller in that case\n\treturn selection;\n}\n\n// A special extend for ajax options\n// that takes \"flat\" options (not to be deep extended)\n// Fixes #9887\nfunction ajaxExtend( target, src ) {\n\tvar key, deep,\n\t\tflatOptions = jQuery.ajaxSettings.flatOptions || {};\n\tfor ( key in src ) {\n\t\tif ( src[ key ] !== undefined ) {\n\t\t\t( flatOptions[ key ] ? target : ( deep || ( deep = {} ) ) )[ key ] = src[ key ];\n\t\t}\n\t}\n\tif ( deep ) {\n\t\tjQuery.extend( true, target, deep );\n\t}\n}\n\njQuery.fn.extend({\n\tload: function( url, params, callback ) {\n\t\tif ( typeof url !== \"string\" && _load ) {\n\t\t\treturn _load.apply( this, arguments );\n\n\t\t// Don't do a request if no elements are being requested\n\t\t} else if ( !this.length ) {\n\t\t\treturn this;\n\t\t}\n\n\t\tvar off = url.indexOf( \" \" );\n\t\tif ( off >= 0 ) {\n\t\t\tvar selector = url.slice( off, url.length );\n\t\t\turl = url.slice( 0, off );\n\t\t}\n\n\t\t// Default to a GET request\n\t\tvar type = \"GET\";\n\n\t\t// If the second parameter was provided\n\t\tif ( params ) {\n\t\t\t// If it's a function\n\t\t\tif ( jQuery.isFunction( params ) ) {\n\t\t\t\t// We assume that it's the callback\n\t\t\t\tcallback = params;\n\t\t\t\tparams = undefined;\n\n\t\t\t// Otherwise, build a param string\n\t\t\t} else if ( typeof params === \"object\" ) {\n\t\t\t\tparams = jQuery.param( params, jQuery.ajaxSettings.traditional );\n\t\t\t\ttype = \"POST\";\n\t\t\t}\n\t\t}\n\n\t\tvar self = this;\n\n\t\t// Request the remote document\n\t\tjQuery.ajax({\n\t\t\turl: url,\n\t\t\ttype: type,\n\t\t\tdataType: \"html\",\n\t\t\tdata: params,\n\t\t\t// Complete callback (responseText is used internally)\n\t\t\tcomplete: function( jqXHR, status, responseText ) {\n\t\t\t\t// Store the response as specified by the jqXHR object\n\t\t\t\tresponseText = jqXHR.responseText;\n\t\t\t\t// If successful, inject the HTML into all the matched elements\n\t\t\t\tif ( jqXHR.isResolved() ) {\n\t\t\t\t\t// #4825: Get the actual response in case\n\t\t\t\t\t// a dataFilter is present in ajaxSettings\n\t\t\t\t\tjqXHR.done(function( r ) {\n\t\t\t\t\t\tresponseText = r;\n\t\t\t\t\t});\n\t\t\t\t\t// See if a selector was specified\n\t\t\t\t\tself.html( selector ?\n\t\t\t\t\t\t// Create a dummy div to hold the results\n\t\t\t\t\t\tjQuery(\"<div>\")\n\t\t\t\t\t\t\t// inject the contents of the document in, removing the scripts\n\t\t\t\t\t\t\t// to avoid any 'Permission Denied' errors in IE\n\t\t\t\t\t\t\t.append(responseText.replace(rscript, \"\"))\n\n\t\t\t\t\t\t\t// Locate the specified elements\n\t\t\t\t\t\t\t.find(selector) :\n\n\t\t\t\t\t\t// If not, just inject the full result\n\t\t\t\t\t\tresponseText );\n\t\t\t\t}\n\n\t\t\t\tif ( callback ) {\n\t\t\t\t\tself.each( callback, [ responseText, status, jqXHR ] );\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\treturn this;\n\t},\n\n\tserialize: function() {\n\t\treturn jQuery.param( this.serializeArray() );\n\t},\n\n\tserializeArray: function() {\n\t\treturn this.map(function(){\n\t\t\treturn this.elements ? jQuery.makeArray( this.elements ) : this;\n\t\t})\n\t\t.filter(function(){\n\t\t\treturn this.name && !this.disabled &&\n\t\t\t\t( this.checked || rselectTextarea.test( this.nodeName ) ||\n\t\t\t\t\trinput.test( this.type ) );\n\t\t})\n\t\t.map(function( i, elem ){\n\t\t\tvar val = jQuery( this ).val();\n\n\t\t\treturn val == null ?\n\t\t\t\tnull :\n\t\t\t\tjQuery.isArray( val ) ?\n\t\t\t\t\tjQuery.map( val, function( val, i ){\n\t\t\t\t\t\treturn { name: elem.name, value: val.replace( rCRLF, \"\\r\\n\" ) };\n\t\t\t\t\t}) :\n\t\t\t\t\t{ name: elem.name, value: val.replace( rCRLF, \"\\r\\n\" ) };\n\t\t}).get();\n\t}\n});\n\n// Attach a bunch of functions for handling common AJAX events\njQuery.each( \"ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend\".split( \" \" ), function( i, o ){\n\tjQuery.fn[ o ] = function( f ){\n\t\treturn this.on( o, f );\n\t};\n});\n\njQuery.each( [ \"get\", \"post\" ], function( i, method ) {\n\tjQuery[ method ] = function( url, data, callback, type ) {\n\t\t// shift arguments if data argument was omitted\n\t\tif ( jQuery.isFunction( data ) ) {\n\t\t\ttype = type || callback;\n\t\t\tcallback = data;\n\t\t\tdata = undefined;\n\t\t}\n\n\t\treturn jQuery.ajax({\n\t\t\ttype: method,\n\t\t\turl: url,\n\t\t\tdata: data,\n\t\t\tsuccess: callback,\n\t\t\tdataType: type\n\t\t});\n\t};\n});\n\njQuery.extend({\n\n\tgetScript: function( url, callback ) {\n\t\treturn jQuery.get( url, undefined, callback, \"script\" );\n\t},\n\n\tgetJSON: function( url, data, callback ) {\n\t\treturn jQuery.get( url, data, callback, \"json\" );\n\t},\n\n\t// Creates a full fledged settings object into target\n\t// with both ajaxSettings and settings fields.\n\t// If target is omitted, writes into ajaxSettings.\n\tajaxSetup: function( target, settings ) {\n\t\tif ( settings ) {\n\t\t\t// Building a settings object\n\t\t\tajaxExtend( target, jQuery.ajaxSettings );\n\t\t} else {\n\t\t\t// Extending ajaxSettings\n\t\t\tsettings = target;\n\t\t\ttarget = jQuery.ajaxSettings;\n\t\t}\n\t\tajaxExtend( target, settings );\n\t\treturn target;\n\t},\n\n\tajaxSettings: {\n\t\turl: ajaxLocation,\n\t\tisLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ),\n\t\tglobal: true,\n\t\ttype: \"GET\",\n\t\tcontentType: \"application/x-www-form-urlencoded\",\n\t\tprocessData: true,\n\t\tasync: true,\n\t\t/*\n\t\ttimeout: 0,\n\t\tdata: null,\n\t\tdataType: null,\n\t\tusername: null,\n\t\tpassword: null,\n\t\tcache: null,\n\t\ttraditional: false,\n\t\theaders: {},\n\t\t*/\n\n\t\taccepts: {\n\t\t\txml: \"application/xml, text/xml\",\n\t\t\thtml: \"text/html\",\n\t\t\ttext: \"text/plain\",\n\t\t\tjson: \"application/json, text/javascript\",\n\t\t\t\"*\": allTypes\n\t\t},\n\n\t\tcontents: {\n\t\t\txml: /xml/,\n\t\t\thtml: /html/,\n\t\t\tjson: /json/\n\t\t},\n\n\t\tresponseFields: {\n\t\t\txml: \"responseXML\",\n\t\t\ttext: \"responseText\"\n\t\t},\n\n\t\t// List of data converters\n\t\t// 1) key format is \"source_type destination_type\" (a single space in-between)\n\t\t// 2) the catchall symbol \"*\" can be used for source_type\n\t\tconverters: {\n\n\t\t\t// Convert anything to text\n\t\t\t\"* text\": window.String,\n\n\t\t\t// Text to html (true = no transformation)\n\t\t\t\"text html\": true,\n\n\t\t\t// Evaluate text as a json expression\n\t\t\t\"text json\": jQuery.parseJSON,\n\n\t\t\t// Parse text as xml\n\t\t\t\"text xml\": jQuery.parseXML\n\t\t},\n\n\t\t// For options that shouldn't be deep extended:\n\t\t// you can add your own custom options here if\n\t\t// and when you create one that shouldn't be\n\t\t// deep extended (see ajaxExtend)\n\t\tflatOptions: {\n\t\t\tcontext: true,\n\t\t\turl: true\n\t\t}\n\t},\n\n\tajaxPrefilter: addToPrefiltersOrTransports( prefilters ),\n\tajaxTransport: addToPrefiltersOrTransports( transports ),\n\n\t// Main method\n\tajax: function( url, options ) {\n\n\t\t// If url is an object, simulate pre-1.5 signature\n\t\tif ( typeof url === \"object\" ) {\n\t\t\toptions = url;\n\t\t\turl = undefined;\n\t\t}\n\n\t\t// Force options to be an object\n\t\toptions = options || {};\n\n\t\tvar // Create the final options object\n\t\t\ts = jQuery.ajaxSetup( {}, options ),\n\t\t\t// Callbacks context\n\t\t\tcallbackContext = s.context || s,\n\t\t\t// Context for global events\n\t\t\t// It's the callbackContext if one was provided in the options\n\t\t\t// and if it's a DOM node or a jQuery collection\n\t\t\tglobalEventContext = callbackContext !== s &&\n\t\t\t\t( callbackContext.nodeType || callbackContext instanceof jQuery ) ?\n\t\t\t\t\t\tjQuery( callbackContext ) : jQuery.event,\n\t\t\t// Deferreds\n\t\t\tdeferred = jQuery.Deferred(),\n\t\t\tcompleteDeferred = jQuery.Callbacks( \"once memory\" ),\n\t\t\t// Status-dependent callbacks\n\t\t\tstatusCode = s.statusCode || {},\n\t\t\t// ifModified key\n\t\t\tifModifiedKey,\n\t\t\t// Headers (they are sent all at once)\n\t\t\trequestHeaders = {},\n\t\t\trequestHeadersNames = {},\n\t\t\t// Response headers\n\t\t\tresponseHeadersString,\n\t\t\tresponseHeaders,\n\t\t\t// transport\n\t\t\ttransport,\n\t\t\t// timeout handle\n\t\t\ttimeoutTimer,\n\t\t\t// Cross-domain detection vars\n\t\t\tparts,\n\t\t\t// The jqXHR state\n\t\t\tstate = 0,\n\t\t\t// To know if global events are to be dispatched\n\t\t\tfireGlobals,\n\t\t\t// Loop variable\n\t\t\ti,\n\t\t\t// Fake xhr\n\t\t\tjqXHR = {\n\n\t\t\t\treadyState: 0,\n\n\t\t\t\t// Caches the header\n\t\t\t\tsetRequestHeader: function( name, value ) {\n\t\t\t\t\tif ( !state ) {\n\t\t\t\t\t\tvar lname = name.toLowerCase();\n\t\t\t\t\t\tname = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name;\n\t\t\t\t\t\trequestHeaders[ name ] = value;\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Raw string\n\t\t\t\tgetAllResponseHeaders: function() {\n\t\t\t\t\treturn state === 2 ? responseHeadersString : null;\n\t\t\t\t},\n\n\t\t\t\t// Builds headers hashtable if needed\n\t\t\t\tgetResponseHeader: function( key ) {\n\t\t\t\t\tvar match;\n\t\t\t\t\tif ( state === 2 ) {\n\t\t\t\t\t\tif ( !responseHeaders ) {\n\t\t\t\t\t\t\tresponseHeaders = {};\n\t\t\t\t\t\t\twhile( ( match = rheaders.exec( responseHeadersString ) ) ) {\n\t\t\t\t\t\t\t\tresponseHeaders[ match[1].toLowerCase() ] = match[ 2 ];\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tmatch = responseHeaders[ key.toLowerCase() ];\n\t\t\t\t\t}\n\t\t\t\t\treturn match === undefined ? null : match;\n\t\t\t\t},\n\n\t\t\t\t// Overrides response content-type header\n\t\t\t\toverrideMimeType: function( type ) {\n\t\t\t\t\tif ( !state ) {\n\t\t\t\t\t\ts.mimeType = type;\n\t\t\t\t\t}\n\t\t\t\t\treturn this;\n\t\t\t\t},\n\n\t\t\t\t// Cancel the request\n\t\t\t\tabort: function( statusText ) {\n\t\t\t\t\tstatusText = statusText || \"abort\";\n\t\t\t\t\tif ( transport ) {\n\t\t\t\t\t\ttransport.abort( statusText );\n\t\t\t\t\t}\n\t\t\t\t\tdone( 0, statusText );\n\t\t\t\t\treturn this;\n\t\t\t\t}\n\t\t\t};\n\n\t\t// Callback for when everything is done\n\t\t// It is defined here because jslint complains if it is declared\n\t\t// at the end of the function (which would be more logical and readable)\n\t\tfunction done( status, nativeStatusText, responses, headers ) {\n\n\t\t\t// Called once\n\t\t\tif ( state === 2 ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// State is \"done\" now\n\t\t\tstate = 2;\n\n\t\t\t// Clear timeout if it exists\n\t\t\tif ( timeoutTimer ) {\n\t\t\t\tclearTimeout( timeoutTimer );\n\t\t\t}\n\n\t\t\t// Dereference transport for early garbage collection\n\t\t\t// (no matter how long the jqXHR object will be used)\n\t\t\ttransport = undefined;\n\n\t\t\t// Cache response headers\n\t\t\tresponseHeadersString = headers || \"\";\n\n\t\t\t// Set readyState\n\t\t\tjqXHR.readyState = status > 0 ? 4 : 0;\n\n\t\t\tvar isSuccess,\n\t\t\t\tsuccess,\n\t\t\t\terror,\n\t\t\t\tstatusText = nativeStatusText,\n\t\t\t\tresponse = responses ? ajaxHandleResponses( s, jqXHR, responses ) : undefined,\n\t\t\t\tlastModified,\n\t\t\t\tetag;\n\n\t\t\t// If successful, handle type chaining\n\t\t\tif ( status >= 200 && status < 300 || status === 304 ) {\n\n\t\t\t\t// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.\n\t\t\t\tif ( s.ifModified ) {\n\n\t\t\t\t\tif ( ( lastModified = jqXHR.getResponseHeader( \"Last-Modified\" ) ) ) {\n\t\t\t\t\t\tjQuery.lastModified[ ifModifiedKey ] = lastModified;\n\t\t\t\t\t}\n\t\t\t\t\tif ( ( etag = jqXHR.getResponseHeader( \"Etag\" ) ) ) {\n\t\t\t\t\t\tjQuery.etag[ ifModifiedKey ] = etag;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// If not modified\n\t\t\t\tif ( status === 304 ) {\n\n\t\t\t\t\tstatusText = \"notmodified\";\n\t\t\t\t\tisSuccess = true;\n\n\t\t\t\t// If we have data\n\t\t\t\t} else {\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tsuccess = ajaxConvert( s, response );\n\t\t\t\t\t\tstatusText = \"success\";\n\t\t\t\t\t\tisSuccess = true;\n\t\t\t\t\t} catch(e) {\n\t\t\t\t\t\t// We have a parsererror\n\t\t\t\t\t\tstatusText = \"parsererror\";\n\t\t\t\t\t\terror = e;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// We extract error from statusText\n\t\t\t\t// then normalize statusText and status for non-aborts\n\t\t\t\terror = statusText;\n\t\t\t\tif ( !statusText || status ) {\n\t\t\t\t\tstatusText = \"error\";\n\t\t\t\t\tif ( status < 0 ) {\n\t\t\t\t\t\tstatus = 0;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Set data for the fake xhr object\n\t\t\tjqXHR.status = status;\n\t\t\tjqXHR.statusText = \"\" + ( nativeStatusText || statusText );\n\n\t\t\t// Success/Error\n\t\t\tif ( isSuccess ) {\n\t\t\t\tdeferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );\n\t\t\t} else {\n\t\t\t\tdeferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );\n\t\t\t}\n\n\t\t\t// Status-dependent callbacks\n\t\t\tjqXHR.statusCode( statusCode );\n\t\t\tstatusCode = undefined;\n\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( \"ajax\" + ( isSuccess ? \"Success\" : \"Error\" ),\n\t\t\t\t\t\t[ jqXHR, s, isSuccess ? success : error ] );\n\t\t\t}\n\n\t\t\t// Complete\n\t\t\tcompleteDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );\n\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( \"ajaxComplete\", [ jqXHR, s ] );\n\t\t\t\t// Handle the global AJAX counter\n\t\t\t\tif ( !( --jQuery.active ) ) {\n\t\t\t\t\tjQuery.event.trigger( \"ajaxStop\" );\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Attach deferreds\n\t\tdeferred.promise( jqXHR );\n\t\tjqXHR.success = jqXHR.done;\n\t\tjqXHR.error = jqXHR.fail;\n\t\tjqXHR.complete = completeDeferred.add;\n\n\t\t// Status-dependent callbacks\n\t\tjqXHR.statusCode = function( map ) {\n\t\t\tif ( map ) {\n\t\t\t\tvar tmp;\n\t\t\t\tif ( state < 2 ) {\n\t\t\t\t\tfor ( tmp in map ) {\n\t\t\t\t\t\tstatusCode[ tmp ] = [ statusCode[tmp], map[tmp] ];\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\ttmp = map[ jqXHR.status ];\n\t\t\t\t\tjqXHR.then( tmp, tmp );\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn this;\n\t\t};\n\n\t\t// Remove hash character (#7531: and string promotion)\n\t\t// Add protocol if not provided (#5866: IE7 issue with protocol-less urls)\n\t\t// We also use the url parameter if available\n\t\ts.url = ( ( url || s.url ) + \"\" ).replace( rhash, \"\" ).replace( rprotocol, ajaxLocParts[ 1 ] + \"//\" );\n\n\t\t// Extract dataTypes list\n\t\ts.dataTypes = jQuery.trim( s.dataType || \"*\" ).toLowerCase().split( rspacesAjax );\n\n\t\t// Determine if a cross-domain request is in order\n\t\tif ( s.crossDomain == null ) {\n\t\t\tparts = rurl.exec( s.url.toLowerCase() );\n\t\t\ts.crossDomain = !!( parts &&\n\t\t\t\t( parts[ 1 ] != ajaxLocParts[ 1 ] || parts[ 2 ] != ajaxLocParts[ 2 ] ||\n\t\t\t\t\t( parts[ 3 ] || ( parts[ 1 ] === \"http:\" ? 80 : 443 ) ) !=\n\t\t\t\t\t\t( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === \"http:\" ? 80 : 443 ) ) )\n\t\t\t);\n\t\t}\n\n\t\t// Convert data if not already a string\n\t\tif ( s.data && s.processData && typeof s.data !== \"string\" ) {\n\t\t\ts.data = jQuery.param( s.data, s.traditional );\n\t\t}\n\n\t\t// Apply prefilters\n\t\tinspectPrefiltersOrTransports( prefilters, s, options, jqXHR );\n\n\t\t// If request was aborted inside a prefiler, stop there\n\t\tif ( state === 2 ) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// We can fire global events as of now if asked to\n\t\tfireGlobals = s.global;\n\n\t\t// Uppercase the type\n\t\ts.type = s.type.toUpperCase();\n\n\t\t// Determine if request has content\n\t\ts.hasContent = !rnoContent.test( s.type );\n\n\t\t// Watch for a new set of requests\n\t\tif ( fireGlobals && jQuery.active++ === 0 ) {\n\t\t\tjQuery.event.trigger( \"ajaxStart\" );\n\t\t}\n\n\t\t// More options handling for requests with no content\n\t\tif ( !s.hasContent ) {\n\n\t\t\t// If data is available, append data to url\n\t\t\tif ( s.data ) {\n\t\t\t\ts.url += ( rquery.test( s.url ) ? \"&\" : \"?\" ) + s.data;\n\t\t\t\t// #9682: remove data so that it's not used in an eventual retry\n\t\t\t\tdelete s.data;\n\t\t\t}\n\n\t\t\t// Get ifModifiedKey before adding the anti-cache parameter\n\t\t\tifModifiedKey = s.url;\n\n\t\t\t// Add anti-cache in url if needed\n\t\t\tif ( s.cache === false ) {\n\n\t\t\t\tvar ts = jQuery.now(),\n\t\t\t\t\t// try replacing _= if it is there\n\t\t\t\t\tret = s.url.replace( rts, \"$1_=\" + ts );\n\n\t\t\t\t// if nothing was replaced, add timestamp to the end\n\t\t\t\ts.url = ret + ( ( ret === s.url ) ? ( rquery.test( s.url ) ? \"&\" : \"?\" ) + \"_=\" + ts : \"\" );\n\t\t\t}\n\t\t}\n\n\t\t// Set the correct header, if data is being sent\n\t\tif ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {\n\t\t\tjqXHR.setRequestHeader( \"Content-Type\", s.contentType );\n\t\t}\n\n\t\t// Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.\n\t\tif ( s.ifModified ) {\n\t\t\tifModifiedKey = ifModifiedKey || s.url;\n\t\t\tif ( jQuery.lastModified[ ifModifiedKey ] ) {\n\t\t\t\tjqXHR.setRequestHeader( \"If-Modified-Since\", jQuery.lastModified[ ifModifiedKey ] );\n\t\t\t}\n\t\t\tif ( jQuery.etag[ ifModifiedKey ] ) {\n\t\t\t\tjqXHR.setRequestHeader( \"If-None-Match\", jQuery.etag[ ifModifiedKey ] );\n\t\t\t}\n\t\t}\n\n\t\t// Set the Accepts header for the server, depending on the dataType\n\t\tjqXHR.setRequestHeader(\n\t\t\t\"Accept\",\n\t\t\ts.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ?\n\t\t\t\ts.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== \"*\" ? \", \" + allTypes + \"; q=0.01\" : \"\" ) :\n\t\t\t\ts.accepts[ \"*\" ]\n\t\t);\n\n\t\t// Check for headers option\n\t\tfor ( i in s.headers ) {\n\t\t\tjqXHR.setRequestHeader( i, s.headers[ i ] );\n\t\t}\n\n\t\t// Allow custom headers/mimetypes and early abort\n\t\tif ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) {\n\t\t\t\t// Abort if not done already\n\t\t\t\tjqXHR.abort();\n\t\t\t\treturn false;\n\n\t\t}\n\n\t\t// Install callbacks on deferreds\n\t\tfor ( i in { success: 1, error: 1, complete: 1 } ) {\n\t\t\tjqXHR[ i ]( s[ i ] );\n\t\t}\n\n\t\t// Get transport\n\t\ttransport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );\n\n\t\t// If no transport, we auto-abort\n\t\tif ( !transport ) {\n\t\t\tdone( -1, \"No Transport\" );\n\t\t} else {\n\t\t\tjqXHR.readyState = 1;\n\t\t\t// Send global event\n\t\t\tif ( fireGlobals ) {\n\t\t\t\tglobalEventContext.trigger( \"ajaxSend\", [ jqXHR, s ] );\n\t\t\t}\n\t\t\t// Timeout\n\t\t\tif ( s.async && s.timeout > 0 ) {\n\t\t\t\ttimeoutTimer = setTimeout( function(){\n\t\t\t\t\tjqXHR.abort( \"timeout\" );\n\t\t\t\t}, s.timeout );\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tstate = 1;\n\t\t\t\ttransport.send( requestHeaders, done );\n\t\t\t} catch (e) {\n\t\t\t\t// Propagate exception as error if not done\n\t\t\t\tif ( state < 2 ) {\n\t\t\t\t\tdone( -1, e );\n\t\t\t\t// Simply rethrow otherwise\n\t\t\t\t} else {\n\t\t\t\t\tthrow e;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn jqXHR;\n\t},\n\n\t// Serialize an array of form elements or a set of\n\t// key/values into a query string\n\tparam: function( a, traditional ) {\n\t\tvar s = [],\n\t\t\tadd = function( key, value ) {\n\t\t\t\t// If value is a function, invoke it and return its value\n\t\t\t\tvalue = jQuery.isFunction( value ) ? value() : value;\n\t\t\t\ts[ s.length ] = encodeURIComponent( key ) + \"=\" + encodeURIComponent( value );\n\t\t\t};\n\n\t\t// Set traditional to true for jQuery <= 1.3.2 behavior.\n\t\tif ( traditional === undefined ) {\n\t\t\ttraditional = jQuery.ajaxSettings.traditional;\n\t\t}\n\n\t\t// If an array was passed in, assume that it is an array of form elements.\n\t\tif ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) {\n\t\t\t// Serialize the form elements\n\t\t\tjQuery.each( a, function() {\n\t\t\t\tadd( this.name, this.value );\n\t\t\t});\n\n\t\t} else {\n\t\t\t// If traditional, encode the \"old\" way (the way 1.3.2 or older\n\t\t\t// did it), otherwise encode params recursively.\n\t\t\tfor ( var prefix in a ) {\n\t\t\t\tbuildParams( prefix, a[ prefix ], traditional, add );\n\t\t\t}\n\t\t}\n\n\t\t// Return the resulting serialization\n\t\treturn s.join( \"&\" ).replace( r20, \"+\" );\n\t}\n});\n\nfunction buildParams( prefix, obj, traditional, add ) {\n\tif ( jQuery.isArray( obj ) ) {\n\t\t// Serialize array item.\n\t\tjQuery.each( obj, function( i, v ) {\n\t\t\tif ( traditional || rbracket.test( prefix ) ) {\n\t\t\t\t// Treat each array item as a scalar.\n\t\t\t\tadd( prefix, v );\n\n\t\t\t} else {\n\t\t\t\t// If array item is non-scalar (array or object), encode its\n\t\t\t\t// numeric index to resolve deserialization ambiguity issues.\n\t\t\t\t// Note that rack (as of 1.0.0) can't currently deserialize\n\t\t\t\t// nested arrays properly, and attempting to do so may cause\n\t\t\t\t// a server error. Possible fixes are to modify rack's\n\t\t\t\t// deserialization algorithm or to provide an option or flag\n\t\t\t\t// to force array serialization to be shallow.\n\t\t\t\tbuildParams( prefix + \"[\" + ( typeof v === \"object\" || jQuery.isArray(v) ? i : \"\" ) + \"]\", v, traditional, add );\n\t\t\t}\n\t\t});\n\n\t} else if ( !traditional && obj != null && typeof obj === \"object\" ) {\n\t\t// Serialize object item.\n\t\tfor ( var name in obj ) {\n\t\t\tbuildParams( prefix + \"[\" + name + \"]\", obj[ name ], traditional, add );\n\t\t}\n\n\t} else {\n\t\t// Serialize scalar item.\n\t\tadd( prefix, obj );\n\t}\n}\n\n// This is still on the jQuery object... for now\n// Want to move this to jQuery.ajax some day\njQuery.extend({\n\n\t// Counter for holding the number of active queries\n\tactive: 0,\n\n\t// Last-Modified header cache for next request\n\tlastModified: {},\n\tetag: {}\n\n});\n\n/* Handles responses to an ajax request:\n * - sets all responseXXX fields accordingly\n * - finds the right dataType (mediates between content-type and expected dataType)\n * - returns the corresponding response\n */\nfunction ajaxHandleResponses( s, jqXHR, responses ) {\n\n\tvar contents = s.contents,\n\t\tdataTypes = s.dataTypes,\n\t\tresponseFields = s.responseFields,\n\t\tct,\n\t\ttype,\n\t\tfinalDataType,\n\t\tfirstDataType;\n\n\t// Fill responseXXX fields\n\tfor ( type in responseFields ) {\n\t\tif ( type in responses ) {\n\t\t\tjqXHR[ responseFields[type] ] = responses[ type ];\n\t\t}\n\t}\n\n\t// Remove auto dataType and get content-type in the process\n\twhile( dataTypes[ 0 ] === \"*\" ) {\n\t\tdataTypes.shift();\n\t\tif ( ct === undefined ) {\n\t\t\tct = s.mimeType || jqXHR.getResponseHeader( \"content-type\" );\n\t\t}\n\t}\n\n\t// Check if we're dealing with a known content-type\n\tif ( ct ) {\n\t\tfor ( type in contents ) {\n\t\t\tif ( contents[ type ] && contents[ type ].test( ct ) ) {\n\t\t\t\tdataTypes.unshift( type );\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check to see if we have a response for the expected dataType\n\tif ( dataTypes[ 0 ] in responses ) {\n\t\tfinalDataType = dataTypes[ 0 ];\n\t} else {\n\t\t// Try convertible dataTypes\n\t\tfor ( type in responses ) {\n\t\t\tif ( !dataTypes[ 0 ] || s.converters[ type + \" \" + dataTypes[0] ] ) {\n\t\t\t\tfinalDataType = type;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif ( !firstDataType ) {\n\t\t\t\tfirstDataType = type;\n\t\t\t}\n\t\t}\n\t\t// Or just use first one\n\t\tfinalDataType = finalDataType || firstDataType;\n\t}\n\n\t// If we found a dataType\n\t// We add the dataType to the list if needed\n\t// and return the corresponding response\n\tif ( finalDataType ) {\n\t\tif ( finalDataType !== dataTypes[ 0 ] ) {\n\t\t\tdataTypes.unshift( finalDataType );\n\t\t}\n\t\treturn responses[ finalDataType ];\n\t}\n}\n\n// Chain conversions given the request and the original response\nfunction ajaxConvert( s, response ) {\n\n\t// Apply the dataFilter if provided\n\tif ( s.dataFilter ) {\n\t\tresponse = s.dataFilter( response, s.dataType );\n\t}\n\n\tvar dataTypes = s.dataTypes,\n\t\tconverters = {},\n\t\ti,\n\t\tkey,\n\t\tlength = dataTypes.length,\n\t\ttmp,\n\t\t// Current and previous dataTypes\n\t\tcurrent = dataTypes[ 0 ],\n\t\tprev,\n\t\t// Conversion expression\n\t\tconversion,\n\t\t// Conversion function\n\t\tconv,\n\t\t// Conversion functions (transitive conversion)\n\t\tconv1,\n\t\tconv2;\n\n\t// For each dataType in the chain\n\tfor ( i = 1; i < length; i++ ) {\n\n\t\t// Create converters map\n\t\t// with lowercased keys\n\t\tif ( i === 1 ) {\n\t\t\tfor ( key in s.converters ) {\n\t\t\t\tif ( typeof key === \"string\" ) {\n\t\t\t\t\tconverters[ key.toLowerCase() ] = s.converters[ key ];\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Get the dataTypes\n\t\tprev = current;\n\t\tcurrent = dataTypes[ i ];\n\n\t\t// If current is auto dataType, update it to prev\n\t\tif ( current === \"*\" ) {\n\t\t\tcurrent = prev;\n\t\t// If no auto and dataTypes are actually different\n\t\t} else if ( prev !== \"*\" && prev !== current ) {\n\n\t\t\t// Get the converter\n\t\t\tconversion = prev + \" \" + current;\n\t\t\tconv = converters[ conversion ] || converters[ \"* \" + current ];\n\n\t\t\t// If there is no direct converter, search transitively\n\t\t\tif ( !conv ) {\n\t\t\t\tconv2 = undefined;\n\t\t\t\tfor ( conv1 in converters ) {\n\t\t\t\t\ttmp = conv1.split( \" \" );\n\t\t\t\t\tif ( tmp[ 0 ] === prev || tmp[ 0 ] === \"*\" ) {\n\t\t\t\t\t\tconv2 = converters[ tmp[1] + \" \" + current ];\n\t\t\t\t\t\tif ( conv2 ) {\n\t\t\t\t\t\t\tconv1 = converters[ conv1 ];\n\t\t\t\t\t\t\tif ( conv1 === true ) {\n\t\t\t\t\t\t\t\tconv = conv2;\n\t\t\t\t\t\t\t} else if ( conv2 === true ) {\n\t\t\t\t\t\t\t\tconv = conv1;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// If we found no converter, dispatch an error\n\t\t\tif ( !( conv || conv2 ) ) {\n\t\t\t\tjQuery.error( \"No conversion from \" + conversion.replace(\" \",\" to \") );\n\t\t\t}\n\t\t\t// If found converter is not an equivalence\n\t\t\tif ( conv !== true ) {\n\t\t\t\t// Convert with 1 or 2 converters accordingly\n\t\t\t\tresponse = conv ? conv( response ) : conv2( conv1(response) );\n\t\t\t}\n\t\t}\n\t}\n\treturn response;\n}\n\n\n\n\nvar jsc = jQuery.now(),\n\tjsre = /(\\=)\\?(&|$)|\\?\\?/i;\n\n// Default jsonp settings\njQuery.ajaxSetup({\n\tjsonp: \"callback\",\n\tjsonpCallback: function() {\n\t\treturn jQuery.expando + \"_\" + ( jsc++ );\n\t}\n});\n\n// Detect, normalize options and install callbacks for jsonp requests\njQuery.ajaxPrefilter( \"json jsonp\", function( s, originalSettings, jqXHR ) {\n\n\tvar inspectData = s.contentType === \"application/x-www-form-urlencoded\" &&\n\t\t( typeof s.data === \"string\" );\n\n\tif ( s.dataTypes[ 0 ] === \"jsonp\" ||\n\t\ts.jsonp !== false && ( jsre.test( s.url ) ||\n\t\t\t\tinspectData && jsre.test( s.data ) ) ) {\n\n\t\tvar responseContainer,\n\t\t\tjsonpCallback = s.jsonpCallback =\n\t\t\t\tjQuery.isFunction( s.jsonpCallback ) ? s.jsonpCallback() : s.jsonpCallback,\n\t\t\tprevious = window[ jsonpCallback ],\n\t\t\turl = s.url,\n\t\t\tdata = s.data,\n\t\t\treplace = \"$1\" + jsonpCallback + \"$2\";\n\n\t\tif ( s.jsonp !== false ) {\n\t\t\turl = url.replace( jsre, replace );\n\t\t\tif ( s.url === url ) {\n\t\t\t\tif ( inspectData ) {\n\t\t\t\t\tdata = data.replace( jsre, replace );\n\t\t\t\t}\n\t\t\t\tif ( s.data === data ) {\n\t\t\t\t\t// Add callback manually\n\t\t\t\t\turl += (/\\?/.test( url ) ? \"&\" : \"?\") + s.jsonp + \"=\" + jsonpCallback;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ts.url = url;\n\t\ts.data = data;\n\n\t\t// Install callback\n\t\twindow[ jsonpCallback ] = function( response ) {\n\t\t\tresponseContainer = [ response ];\n\t\t};\n\n\t\t// Clean-up function\n\t\tjqXHR.always(function() {\n\t\t\t// Set callback back to previous value\n\t\t\twindow[ jsonpCallback ] = previous;\n\t\t\t// Call if it was a function and we have a response\n\t\t\tif ( responseContainer && jQuery.isFunction( previous ) ) {\n\t\t\t\twindow[ jsonpCallback ]( responseContainer[ 0 ] );\n\t\t\t}\n\t\t});\n\n\t\t// Use data converter to retrieve json after script execution\n\t\ts.converters[\"script json\"] = function() {\n\t\t\tif ( !responseContainer ) {\n\t\t\t\tjQuery.error( jsonpCallback + \" was not called\" );\n\t\t\t}\n\t\t\treturn responseContainer[ 0 ];\n\t\t};\n\n\t\t// force json dataType\n\t\ts.dataTypes[ 0 ] = \"json\";\n\n\t\t// Delegate to script\n\t\treturn \"script\";\n\t}\n});\n\n\n\n\n// Install script dataType\njQuery.ajaxSetup({\n\taccepts: {\n\t\tscript: \"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript\"\n\t},\n\tcontents: {\n\t\tscript: /javascript|ecmascript/\n\t},\n\tconverters: {\n\t\t\"text script\": function( text ) {\n\t\t\tjQuery.globalEval( text );\n\t\t\treturn text;\n\t\t}\n\t}\n});\n\n// Handle cache's special case and global\njQuery.ajaxPrefilter( \"script\", function( s ) {\n\tif ( s.cache === undefined ) {\n\t\ts.cache = false;\n\t}\n\tif ( s.crossDomain ) {\n\t\ts.type = \"GET\";\n\t\ts.global = false;\n\t}\n});\n\n// Bind script tag hack transport\njQuery.ajaxTransport( \"script\", function(s) {\n\n\t// This transport only deals with cross domain requests\n\tif ( s.crossDomain ) {\n\n\t\tvar script,\n\t\t\thead = document.head || document.getElementsByTagName( \"head\" )[0] || document.documentElement;\n\n\t\treturn {\n\n\t\t\tsend: function( _, callback ) {\n\n\t\t\t\tscript = document.createElement( \"script\" );\n\n\t\t\t\tscript.async = \"async\";\n\n\t\t\t\tif ( s.scriptCharset ) {\n\t\t\t\t\tscript.charset = s.scriptCharset;\n\t\t\t\t}\n\n\t\t\t\tscript.src = s.url;\n\n\t\t\t\t// Attach handlers for all browsers\n\t\t\t\tscript.onload = script.onreadystatechange = function( _, isAbort ) {\n\n\t\t\t\t\tif ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {\n\n\t\t\t\t\t\t// Handle memory leak in IE\n\t\t\t\t\t\tscript.onload = script.onreadystatechange = null;\n\n\t\t\t\t\t\t// Remove the script\n\t\t\t\t\t\tif ( head && script.parentNode ) {\n\t\t\t\t\t\t\thead.removeChild( script );\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Dereference the script\n\t\t\t\t\t\tscript = undefined;\n\n\t\t\t\t\t\t// Callback if not abort\n\t\t\t\t\t\tif ( !isAbort ) {\n\t\t\t\t\t\t\tcallback( 200, \"success\" );\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t};\n\t\t\t\t// Use insertBefore instead of appendChild  to circumvent an IE6 bug.\n\t\t\t\t// This arises when a base node is used (#2709 and #4378).\n\t\t\t\thead.insertBefore( script, head.firstChild );\n\t\t\t},\n\n\t\t\tabort: function() {\n\t\t\t\tif ( script ) {\n\t\t\t\t\tscript.onload( 0, 1 );\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\t}\n});\n\n\n\n\nvar // #5280: Internet Explorer will keep connections alive if we don't abort on unload\n\txhrOnUnloadAbort = window.ActiveXObject ? function() {\n\t\t// Abort all pending requests\n\t\tfor ( var key in xhrCallbacks ) {\n\t\t\txhrCallbacks[ key ]( 0, 1 );\n\t\t}\n\t} : false,\n\txhrId = 0,\n\txhrCallbacks;\n\n// Functions to create xhrs\nfunction createStandardXHR() {\n\ttry {\n\t\treturn new window.XMLHttpRequest();\n\t} catch( e ) {}\n}\n\nfunction createActiveXHR() {\n\ttry {\n\t\treturn new window.ActiveXObject( \"Microsoft.XMLHTTP\" );\n\t} catch( e ) {}\n}\n\n// Create the request object\n// (This is still attached to ajaxSettings for backward compatibility)\njQuery.ajaxSettings.xhr = window.ActiveXObject ?\n\t/* Microsoft failed to properly\n\t * implement the XMLHttpRequest in IE7 (can't request local files),\n\t * so we use the ActiveXObject when it is available\n\t * Additionally XMLHttpRequest can be disabled in IE7/IE8 so\n\t * we need a fallback.\n\t */\n\tfunction() {\n\t\treturn !this.isLocal && createStandardXHR() || createActiveXHR();\n\t} :\n\t// For all other browsers, use the standard XMLHttpRequest object\n\tcreateStandardXHR;\n\n// Determine support properties\n(function( xhr ) {\n\tjQuery.extend( jQuery.support, {\n\t\tajax: !!xhr,\n\t\tcors: !!xhr && ( \"withCredentials\" in xhr )\n\t});\n})( jQuery.ajaxSettings.xhr() );\n\n// Create transport if the browser can provide an xhr\nif ( jQuery.support.ajax ) {\n\n\tjQuery.ajaxTransport(function( s ) {\n\t\t// Cross domain only allowed if supported through XMLHttpRequest\n\t\tif ( !s.crossDomain || jQuery.support.cors ) {\n\n\t\t\tvar callback;\n\n\t\t\treturn {\n\t\t\t\tsend: function( headers, complete ) {\n\n\t\t\t\t\t// Get a new xhr\n\t\t\t\t\tvar xhr = s.xhr(),\n\t\t\t\t\t\thandle,\n\t\t\t\t\t\ti;\n\n\t\t\t\t\t// Open the socket\n\t\t\t\t\t// Passing null username, generates a login popup on Opera (#2865)\n\t\t\t\t\tif ( s.username ) {\n\t\t\t\t\t\txhr.open( s.type, s.url, s.async, s.username, s.password );\n\t\t\t\t\t} else {\n\t\t\t\t\t\txhr.open( s.type, s.url, s.async );\n\t\t\t\t\t}\n\n\t\t\t\t\t// Apply custom fields if provided\n\t\t\t\t\tif ( s.xhrFields ) {\n\t\t\t\t\t\tfor ( i in s.xhrFields ) {\n\t\t\t\t\t\t\txhr[ i ] = s.xhrFields[ i ];\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Override mime type if needed\n\t\t\t\t\tif ( s.mimeType && xhr.overrideMimeType ) {\n\t\t\t\t\t\txhr.overrideMimeType( s.mimeType );\n\t\t\t\t\t}\n\n\t\t\t\t\t// X-Requested-With header\n\t\t\t\t\t// For cross-domain requests, seeing as conditions for a preflight are\n\t\t\t\t\t// akin to a jigsaw puzzle, we simply never set it to be sure.\n\t\t\t\t\t// (it can always be set on a per-request basis or even using ajaxSetup)\n\t\t\t\t\t// For same-domain requests, won't change header if already provided.\n\t\t\t\t\tif ( !s.crossDomain && !headers[\"X-Requested-With\"] ) {\n\t\t\t\t\t\theaders[ \"X-Requested-With\" ] = \"XMLHttpRequest\";\n\t\t\t\t\t}\n\n\t\t\t\t\t// Need an extra try/catch for cross domain requests in Firefox 3\n\t\t\t\t\ttry {\n\t\t\t\t\t\tfor ( i in headers ) {\n\t\t\t\t\t\t\txhr.setRequestHeader( i, headers[ i ] );\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch( _ ) {}\n\n\t\t\t\t\t// Do send the request\n\t\t\t\t\t// This may raise an exception which is actually\n\t\t\t\t\t// handled in jQuery.ajax (so no try/catch here)\n\t\t\t\t\txhr.send( ( s.hasContent && s.data ) || null );\n\n\t\t\t\t\t// Listener\n\t\t\t\t\tcallback = function( _, isAbort ) {\n\n\t\t\t\t\t\tvar status,\n\t\t\t\t\t\t\tstatusText,\n\t\t\t\t\t\t\tresponseHeaders,\n\t\t\t\t\t\t\tresponses,\n\t\t\t\t\t\t\txml;\n\n\t\t\t\t\t\t// Firefox throws exceptions when accessing properties\n\t\t\t\t\t\t// of an xhr when a network error occurred\n\t\t\t\t\t\t// http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE)\n\t\t\t\t\t\ttry {\n\n\t\t\t\t\t\t\t// Was never called and is aborted or complete\n\t\t\t\t\t\t\tif ( callback && ( isAbort || xhr.readyState === 4 ) ) {\n\n\t\t\t\t\t\t\t\t// Only called once\n\t\t\t\t\t\t\t\tcallback = undefined;\n\n\t\t\t\t\t\t\t\t// Do not keep as active anymore\n\t\t\t\t\t\t\t\tif ( handle ) {\n\t\t\t\t\t\t\t\t\txhr.onreadystatechange = jQuery.noop;\n\t\t\t\t\t\t\t\t\tif ( xhrOnUnloadAbort ) {\n\t\t\t\t\t\t\t\t\t\tdelete xhrCallbacks[ handle ];\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t// If it's an abort\n\t\t\t\t\t\t\t\tif ( isAbort ) {\n\t\t\t\t\t\t\t\t\t// Abort it manually if needed\n\t\t\t\t\t\t\t\t\tif ( xhr.readyState !== 4 ) {\n\t\t\t\t\t\t\t\t\t\txhr.abort();\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tstatus = xhr.status;\n\t\t\t\t\t\t\t\t\tresponseHeaders = xhr.getAllResponseHeaders();\n\t\t\t\t\t\t\t\t\tresponses = {};\n\t\t\t\t\t\t\t\t\txml = xhr.responseXML;\n\n\t\t\t\t\t\t\t\t\t// Construct response list\n\t\t\t\t\t\t\t\t\tif ( xml && xml.documentElement /* #4958 */ ) {\n\t\t\t\t\t\t\t\t\t\tresponses.xml = xml;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tresponses.text = xhr.responseText;\n\n\t\t\t\t\t\t\t\t\t// Firefox throws an exception when accessing\n\t\t\t\t\t\t\t\t\t// statusText for faulty cross-domain requests\n\t\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\t\tstatusText = xhr.statusText;\n\t\t\t\t\t\t\t\t\t} catch( e ) {\n\t\t\t\t\t\t\t\t\t\t// We normalize with Webkit giving an empty statusText\n\t\t\t\t\t\t\t\t\t\tstatusText = \"\";\n\t\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\t\t// Filter status for non standard behaviors\n\n\t\t\t\t\t\t\t\t\t// If the request is local and we have data: assume a success\n\t\t\t\t\t\t\t\t\t// (success with no data won't get notified, that's the best we\n\t\t\t\t\t\t\t\t\t// can do given current implementations)\n\t\t\t\t\t\t\t\t\tif ( !status && s.isLocal && !s.crossDomain ) {\n\t\t\t\t\t\t\t\t\t\tstatus = responses.text ? 200 : 404;\n\t\t\t\t\t\t\t\t\t// IE - #1450: sometimes returns 1223 when it should be 204\n\t\t\t\t\t\t\t\t\t} else if ( status === 1223 ) {\n\t\t\t\t\t\t\t\t\t\tstatus = 204;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch( firefoxAccessException ) {\n\t\t\t\t\t\t\tif ( !isAbort ) {\n\t\t\t\t\t\t\t\tcomplete( -1, firefoxAccessException );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Call complete if needed\n\t\t\t\t\t\tif ( responses ) {\n\t\t\t\t\t\t\tcomplete( status, statusText, responses, responseHeaders );\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\n\t\t\t\t\t// if we're in sync mode or it's in cache\n\t\t\t\t\t// and has been retrieved directly (IE6 & IE7)\n\t\t\t\t\t// we need to manually fire the callback\n\t\t\t\t\tif ( !s.async || xhr.readyState === 4 ) {\n\t\t\t\t\t\tcallback();\n\t\t\t\t\t} else {\n\t\t\t\t\t\thandle = ++xhrId;\n\t\t\t\t\t\tif ( xhrOnUnloadAbort ) {\n\t\t\t\t\t\t\t// Create the active xhrs callbacks list if needed\n\t\t\t\t\t\t\t// and attach the unload handler\n\t\t\t\t\t\t\tif ( !xhrCallbacks ) {\n\t\t\t\t\t\t\t\txhrCallbacks = {};\n\t\t\t\t\t\t\t\tjQuery( window ).unload( xhrOnUnloadAbort );\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t// Add to list of active xhrs callbacks\n\t\t\t\t\t\t\txhrCallbacks[ handle ] = callback;\n\t\t\t\t\t\t}\n\t\t\t\t\t\txhr.onreadystatechange = callback;\n\t\t\t\t\t}\n\t\t\t\t},\n\n\t\t\t\tabort: function() {\n\t\t\t\t\tif ( callback ) {\n\t\t\t\t\t\tcallback(0,1);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t}\n\t});\n}\n\n\n\n\nvar elemdisplay = {},\n\tiframe, iframeDoc,\n\trfxtypes = /^(?:toggle|show|hide)$/,\n\trfxnum = /^([+\\-]=)?([\\d+.\\-]+)([a-z%]*)$/i,\n\ttimerId,\n\tfxAttrs = [\n\t\t// height animations\n\t\t[ \"height\", \"marginTop\", \"marginBottom\", \"paddingTop\", \"paddingBottom\" ],\n\t\t// width animations\n\t\t[ \"width\", \"marginLeft\", \"marginRight\", \"paddingLeft\", \"paddingRight\" ],\n\t\t// opacity animations\n\t\t[ \"opacity\" ]\n\t],\n\tfxNow;\n\njQuery.fn.extend({\n\tshow: function( speed, easing, callback ) {\n\t\tvar elem, display;\n\n\t\tif ( speed || speed === 0 ) {\n\t\t\treturn this.animate( genFx(\"show\", 3), speed, easing, callback );\n\n\t\t} else {\n\t\t\tfor ( var i = 0, j = this.length; i < j; i++ ) {\n\t\t\t\telem = this[ i ];\n\n\t\t\t\tif ( elem.style ) {\n\t\t\t\t\tdisplay = elem.style.display;\n\n\t\t\t\t\t// Reset the inline display of this element to learn if it is\n\t\t\t\t\t// being hidden by cascaded rules or not\n\t\t\t\t\tif ( !jQuery._data(elem, \"olddisplay\") && display === \"none\" ) {\n\t\t\t\t\t\tdisplay = elem.style.display = \"\";\n\t\t\t\t\t}\n\n\t\t\t\t\t// Set elements which have been overridden with display: none\n\t\t\t\t\t// in a stylesheet to whatever the default browser style is\n\t\t\t\t\t// for such an element\n\t\t\t\t\tif ( display === \"\" && jQuery.css(elem, \"display\") === \"none\" ) {\n\t\t\t\t\t\tjQuery._data( elem, \"olddisplay\", defaultDisplay(elem.nodeName) );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Set the display of most of the elements in a second loop\n\t\t\t// to avoid the constant reflow\n\t\t\tfor ( i = 0; i < j; i++ ) {\n\t\t\t\telem = this[ i ];\n\n\t\t\t\tif ( elem.style ) {\n\t\t\t\t\tdisplay = elem.style.display;\n\n\t\t\t\t\tif ( display === \"\" || display === \"none\" ) {\n\t\t\t\t\t\telem.style.display = jQuery._data( elem, \"olddisplay\" ) || \"\";\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn this;\n\t\t}\n\t},\n\n\thide: function( speed, easing, callback ) {\n\t\tif ( speed || speed === 0 ) {\n\t\t\treturn this.animate( genFx(\"hide\", 3), speed, easing, callback);\n\n\t\t} else {\n\t\t\tvar elem, display,\n\t\t\t\ti = 0,\n\t\t\t\tj = this.length;\n\n\t\t\tfor ( ; i < j; i++ ) {\n\t\t\t\telem = this[i];\n\t\t\t\tif ( elem.style ) {\n\t\t\t\t\tdisplay = jQuery.css( elem, \"display\" );\n\n\t\t\t\t\tif ( display !== \"none\" && !jQuery._data( elem, \"olddisplay\" ) ) {\n\t\t\t\t\t\tjQuery._data( elem, \"olddisplay\", display );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Set the display of the elements in a second loop\n\t\t\t// to avoid the constant reflow\n\t\t\tfor ( i = 0; i < j; i++ ) {\n\t\t\t\tif ( this[i].style ) {\n\t\t\t\t\tthis[i].style.display = \"none\";\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn this;\n\t\t}\n\t},\n\n\t// Save the old toggle function\n\t_toggle: jQuery.fn.toggle,\n\n\ttoggle: function( fn, fn2, callback ) {\n\t\tvar bool = typeof fn === \"boolean\";\n\n\t\tif ( jQuery.isFunction(fn) && jQuery.isFunction(fn2) ) {\n\t\t\tthis._toggle.apply( this, arguments );\n\n\t\t} else if ( fn == null || bool ) {\n\t\t\tthis.each(function() {\n\t\t\t\tvar state = bool ? fn : jQuery(this).is(\":hidden\");\n\t\t\t\tjQuery(this)[ state ? \"show\" : \"hide\" ]();\n\t\t\t});\n\n\t\t} else {\n\t\t\tthis.animate(genFx(\"toggle\", 3), fn, fn2, callback);\n\t\t}\n\n\t\treturn this;\n\t},\n\n\tfadeTo: function( speed, to, easing, callback ) {\n\t\treturn this.filter(\":hidden\").css(\"opacity\", 0).show().end()\n\t\t\t\t\t.animate({opacity: to}, speed, easing, callback);\n\t},\n\n\tanimate: function( prop, speed, easing, callback ) {\n\t\tvar optall = jQuery.speed( speed, easing, callback );\n\n\t\tif ( jQuery.isEmptyObject( prop ) ) {\n\t\t\treturn this.each( optall.complete, [ false ] );\n\t\t}\n\n\t\t// Do not change referenced properties as per-property easing will be lost\n\t\tprop = jQuery.extend( {}, prop );\n\n\t\tfunction doAnimation() {\n\t\t\t// XXX 'this' does not always have a nodeName when running the\n\t\t\t// test suite\n\n\t\t\tif ( optall.queue === false ) {\n\t\t\t\tjQuery._mark( this );\n\t\t\t}\n\n\t\t\tvar opt = jQuery.extend( {}, optall ),\n\t\t\t\tisElement = this.nodeType === 1,\n\t\t\t\thidden = isElement && jQuery(this).is(\":hidden\"),\n\t\t\t\tname, val, p, e,\n\t\t\t\tparts, start, end, unit,\n\t\t\t\tmethod;\n\n\t\t\t// will store per property easing and be used to determine when an animation is complete\n\t\t\topt.animatedProperties = {};\n\n\t\t\tfor ( p in prop ) {\n\n\t\t\t\t// property name normalization\n\t\t\t\tname = jQuery.camelCase( p );\n\t\t\t\tif ( p !== name ) {\n\t\t\t\t\tprop[ name ] = prop[ p ];\n\t\t\t\t\tdelete prop[ p ];\n\t\t\t\t}\n\n\t\t\t\tval = prop[ name ];\n\n\t\t\t\t// easing resolution: per property > opt.specialEasing > opt.easing > 'swing' (default)\n\t\t\t\tif ( jQuery.isArray( val ) ) {\n\t\t\t\t\topt.animatedProperties[ name ] = val[ 1 ];\n\t\t\t\t\tval = prop[ name ] = val[ 0 ];\n\t\t\t\t} else {\n\t\t\t\t\topt.animatedProperties[ name ] = opt.specialEasing && opt.specialEasing[ name ] || opt.easing || 'swing';\n\t\t\t\t}\n\n\t\t\t\tif ( val === \"hide\" && hidden || val === \"show\" && !hidden ) {\n\t\t\t\t\treturn opt.complete.call( this );\n\t\t\t\t}\n\n\t\t\t\tif ( isElement && ( name === \"height\" || name === \"width\" ) ) {\n\t\t\t\t\t// Make sure that nothing sneaks out\n\t\t\t\t\t// Record all 3 overflow attributes because IE does not\n\t\t\t\t\t// change the overflow attribute when overflowX and\n\t\t\t\t\t// overflowY are set to the same value\n\t\t\t\t\topt.overflow = [ this.style.overflow, this.style.overflowX, this.style.overflowY ];\n\n\t\t\t\t\t// Set display property to inline-block for height/width\n\t\t\t\t\t// animations on inline elements that are having width/height animated\n\t\t\t\t\tif ( jQuery.css( this, \"display\" ) === \"inline\" &&\n\t\t\t\t\t\t\tjQuery.css( this, \"float\" ) === \"none\" ) {\n\n\t\t\t\t\t\t// inline-level elements accept inline-block;\n\t\t\t\t\t\t// block-level elements need to be inline with layout\n\t\t\t\t\t\tif ( !jQuery.support.inlineBlockNeedsLayout || defaultDisplay( this.nodeName ) === \"inline\" ) {\n\t\t\t\t\t\t\tthis.style.display = \"inline-block\";\n\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tthis.style.zoom = 1;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( opt.overflow != null ) {\n\t\t\t\tthis.style.overflow = \"hidden\";\n\t\t\t}\n\n\t\t\tfor ( p in prop ) {\n\t\t\t\te = new jQuery.fx( this, opt, p );\n\t\t\t\tval = prop[ p ];\n\n\t\t\t\tif ( rfxtypes.test( val ) ) {\n\n\t\t\t\t\t// Tracks whether to show or hide based on private\n\t\t\t\t\t// data attached to the element\n\t\t\t\t\tmethod = jQuery._data( this, \"toggle\" + p ) || ( val === \"toggle\" ? hidden ? \"show\" : \"hide\" : 0 );\n\t\t\t\t\tif ( method ) {\n\t\t\t\t\t\tjQuery._data( this, \"toggle\" + p, method === \"show\" ? \"hide\" : \"show\" );\n\t\t\t\t\t\te[ method ]();\n\t\t\t\t\t} else {\n\t\t\t\t\t\te[ val ]();\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\t\t\t\t\tparts = rfxnum.exec( val );\n\t\t\t\t\tstart = e.cur();\n\n\t\t\t\t\tif ( parts ) {\n\t\t\t\t\t\tend = parseFloat( parts[2] );\n\t\t\t\t\t\tunit = parts[3] || ( jQuery.cssNumber[ p ] ? \"\" : \"px\" );\n\n\t\t\t\t\t\t// We need to compute starting value\n\t\t\t\t\t\tif ( unit !== \"px\" ) {\n\t\t\t\t\t\t\tjQuery.style( this, p, (end || 1) + unit);\n\t\t\t\t\t\t\tstart = ( (end || 1) / e.cur() ) * start;\n\t\t\t\t\t\t\tjQuery.style( this, p, start + unit);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// If a +=/-= token was provided, we're doing a relative animation\n\t\t\t\t\t\tif ( parts[1] ) {\n\t\t\t\t\t\t\tend = ( (parts[ 1 ] === \"-=\" ? -1 : 1) * end ) + start;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\te.custom( start, end, unit );\n\n\t\t\t\t\t} else {\n\t\t\t\t\t\te.custom( start, val, \"\" );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// For JS strict compliance\n\t\t\treturn true;\n\t\t}\n\n\t\treturn optall.queue === false ?\n\t\t\tthis.each( doAnimation ) :\n\t\t\tthis.queue( optall.queue, doAnimation );\n\t},\n\n\tstop: function( type, clearQueue, gotoEnd ) {\n\t\tif ( typeof type !== \"string\" ) {\n\t\t\tgotoEnd = clearQueue;\n\t\t\tclearQueue = type;\n\t\t\ttype = undefined;\n\t\t}\n\t\tif ( clearQueue && type !== false ) {\n\t\t\tthis.queue( type || \"fx\", [] );\n\t\t}\n\n\t\treturn this.each(function() {\n\t\t\tvar index,\n\t\t\t\thadTimers = false,\n\t\t\t\ttimers = jQuery.timers,\n\t\t\t\tdata = jQuery._data( this );\n\n\t\t\t// clear marker counters if we know they won't be\n\t\t\tif ( !gotoEnd ) {\n\t\t\t\tjQuery._unmark( true, this );\n\t\t\t}\n\n\t\t\tfunction stopQueue( elem, data, index ) {\n\t\t\t\tvar hooks = data[ index ];\n\t\t\t\tjQuery.removeData( elem, index, true );\n\t\t\t\thooks.stop( gotoEnd );\n\t\t\t}\n\n\t\t\tif ( type == null ) {\n\t\t\t\tfor ( index in data ) {\n\t\t\t\t\tif ( data[ index ] && data[ index ].stop && index.indexOf(\".run\") === index.length - 4 ) {\n\t\t\t\t\t\tstopQueue( this, data, index );\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if ( data[ index = type + \".run\" ] && data[ index ].stop ){\n\t\t\t\tstopQueue( this, data, index );\n\t\t\t}\n\n\t\t\tfor ( index = timers.length; index--; ) {\n\t\t\t\tif ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) {\n\t\t\t\t\tif ( gotoEnd ) {\n\n\t\t\t\t\t\t// force the next step to be the last\n\t\t\t\t\t\ttimers[ index ]( true );\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttimers[ index ].saveState();\n\t\t\t\t\t}\n\t\t\t\t\thadTimers = true;\n\t\t\t\t\ttimers.splice( index, 1 );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// start the next in the queue if the last step wasn't forced\n\t\t\t// timers currently will call their complete callbacks, which will dequeue\n\t\t\t// but only if they were gotoEnd\n\t\t\tif ( !( gotoEnd && hadTimers ) ) {\n\t\t\t\tjQuery.dequeue( this, type );\n\t\t\t}\n\t\t});\n\t}\n\n});\n\n// Animations created synchronously will run synchronously\nfunction createFxNow() {\n\tsetTimeout( clearFxNow, 0 );\n\treturn ( fxNow = jQuery.now() );\n}\n\nfunction clearFxNow() {\n\tfxNow = undefined;\n}\n\n// Generate parameters to create a standard animation\nfunction genFx( type, num ) {\n\tvar obj = {};\n\n\tjQuery.each( fxAttrs.concat.apply([], fxAttrs.slice( 0, num )), function() {\n\t\tobj[ this ] = type;\n\t});\n\n\treturn obj;\n}\n\n// Generate shortcuts for custom animations\njQuery.each({\n\tslideDown: genFx( \"show\", 1 ),\n\tslideUp: genFx( \"hide\", 1 ),\n\tslideToggle: genFx( \"toggle\", 1 ),\n\tfadeIn: { opacity: \"show\" },\n\tfadeOut: { opacity: \"hide\" },\n\tfadeToggle: { opacity: \"toggle\" }\n}, function( name, props ) {\n\tjQuery.fn[ name ] = function( speed, easing, callback ) {\n\t\treturn this.animate( props, speed, easing, callback );\n\t};\n});\n\njQuery.extend({\n\tspeed: function( speed, easing, fn ) {\n\t\tvar opt = speed && typeof speed === \"object\" ? jQuery.extend( {}, speed ) : {\n\t\t\tcomplete: fn || !fn && easing ||\n\t\t\t\tjQuery.isFunction( speed ) && speed,\n\t\t\tduration: speed,\n\t\t\teasing: fn && easing || easing && !jQuery.isFunction( easing ) && easing\n\t\t};\n\n\t\topt.duration = jQuery.fx.off ? 0 : typeof opt.duration === \"number\" ? opt.duration :\n\t\t\topt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;\n\n\t\t// normalize opt.queue - true/undefined/null -> \"fx\"\n\t\tif ( opt.queue == null || opt.queue === true ) {\n\t\t\topt.queue = \"fx\";\n\t\t}\n\n\t\t// Queueing\n\t\topt.old = opt.complete;\n\n\t\topt.complete = function( noUnmark ) {\n\t\t\tif ( jQuery.isFunction( opt.old ) ) {\n\t\t\t\topt.old.call( this );\n\t\t\t}\n\n\t\t\tif ( opt.queue ) {\n\t\t\t\tjQuery.dequeue( this, opt.queue );\n\t\t\t} else if ( noUnmark !== false ) {\n\t\t\t\tjQuery._unmark( this );\n\t\t\t}\n\t\t};\n\n\t\treturn opt;\n\t},\n\n\teasing: {\n\t\tlinear: function( p, n, firstNum, diff ) {\n\t\t\treturn firstNum + diff * p;\n\t\t},\n\t\tswing: function( p, n, firstNum, diff ) {\n\t\t\treturn ( ( -Math.cos( p*Math.PI ) / 2 ) + 0.5 ) * diff + firstNum;\n\t\t}\n\t},\n\n\ttimers: [],\n\n\tfx: function( elem, options, prop ) {\n\t\tthis.options = options;\n\t\tthis.elem = elem;\n\t\tthis.prop = prop;\n\n\t\toptions.orig = options.orig || {};\n\t}\n\n});\n\njQuery.fx.prototype = {\n\t// Simple function for setting a style value\n\tupdate: function() {\n\t\tif ( this.options.step ) {\n\t\t\tthis.options.step.call( this.elem, this.now, this );\n\t\t}\n\n\t\t( jQuery.fx.step[ this.prop ] || jQuery.fx.step._default )( this );\n\t},\n\n\t// Get the current size\n\tcur: function() {\n\t\tif ( this.elem[ this.prop ] != null && (!this.elem.style || this.elem.style[ this.prop ] == null) ) {\n\t\t\treturn this.elem[ this.prop ];\n\t\t}\n\n\t\tvar parsed,\n\t\t\tr = jQuery.css( this.elem, this.prop );\n\t\t// Empty strings, null, undefined and \"auto\" are converted to 0,\n\t\t// complex values such as \"rotate(1rad)\" are returned as is,\n\t\t// simple values such as \"10px\" are parsed to Float.\n\t\treturn isNaN( parsed = parseFloat( r ) ) ? !r || r === \"auto\" ? 0 : r : parsed;\n\t},\n\n\t// Start an animation from one number to another\n\tcustom: function( from, to, unit ) {\n\t\tvar self = this,\n\t\t\tfx = jQuery.fx;\n\n\t\tthis.startTime = fxNow || createFxNow();\n\t\tthis.end = to;\n\t\tthis.now = this.start = from;\n\t\tthis.pos = this.state = 0;\n\t\tthis.unit = unit || this.unit || ( jQuery.cssNumber[ this.prop ] ? \"\" : \"px\" );\n\n\t\tfunction t( gotoEnd ) {\n\t\t\treturn self.step( gotoEnd );\n\t\t}\n\n\t\tt.queue = this.options.queue;\n\t\tt.elem = this.elem;\n\t\tt.saveState = function() {\n\t\t\tif ( self.options.hide && jQuery._data( self.elem, \"fxshow\" + self.prop ) === undefined ) {\n\t\t\t\tjQuery._data( self.elem, \"fxshow\" + self.prop, self.start );\n\t\t\t}\n\t\t};\n\n\t\tif ( t() && jQuery.timers.push(t) && !timerId ) {\n\t\t\ttimerId = setInterval( fx.tick, fx.interval );\n\t\t}\n\t},\n\n\t// Simple 'show' function\n\tshow: function() {\n\t\tvar dataShow = jQuery._data( this.elem, \"fxshow\" + this.prop );\n\n\t\t// Remember where we started, so that we can go back to it later\n\t\tthis.options.orig[ this.prop ] = dataShow || jQuery.style( this.elem, this.prop );\n\t\tthis.options.show = true;\n\n\t\t// Begin the animation\n\t\t// Make sure that we start at a small width/height to avoid any flash of content\n\t\tif ( dataShow !== undefined ) {\n\t\t\t// This show is picking up where a previous hide or show left off\n\t\t\tthis.custom( this.cur(), dataShow );\n\t\t} else {\n\t\t\tthis.custom( this.prop === \"width\" || this.prop === \"height\" ? 1 : 0, this.cur() );\n\t\t}\n\n\t\t// Start by showing the element\n\t\tjQuery( this.elem ).show();\n\t},\n\n\t// Simple 'hide' function\n\thide: function() {\n\t\t// Remember where we started, so that we can go back to it later\n\t\tthis.options.orig[ this.prop ] = jQuery._data( this.elem, \"fxshow\" + this.prop ) || jQuery.style( this.elem, this.prop );\n\t\tthis.options.hide = true;\n\n\t\t// Begin the animation\n\t\tthis.custom( this.cur(), 0 );\n\t},\n\n\t// Each step of an animation\n\tstep: function( gotoEnd ) {\n\t\tvar p, n, complete,\n\t\t\tt = fxNow || createFxNow(),\n\t\t\tdone = true,\n\t\t\telem = this.elem,\n\t\t\toptions = this.options;\n\n\t\tif ( gotoEnd || t >= options.duration + this.startTime ) {\n\t\t\tthis.now = this.end;\n\t\t\tthis.pos = this.state = 1;\n\t\t\tthis.update();\n\n\t\t\toptions.animatedProperties[ this.prop ] = true;\n\n\t\t\tfor ( p in options.animatedProperties ) {\n\t\t\t\tif ( options.animatedProperties[ p ] !== true ) {\n\t\t\t\t\tdone = false;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( done ) {\n\t\t\t\t// Reset the overflow\n\t\t\t\tif ( options.overflow != null && !jQuery.support.shrinkWrapBlocks ) {\n\n\t\t\t\t\tjQuery.each( [ \"\", \"X\", \"Y\" ], function( index, value ) {\n\t\t\t\t\t\telem.style[ \"overflow\" + value ] = options.overflow[ index ];\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\t// Hide the element if the \"hide\" operation was done\n\t\t\t\tif ( options.hide ) {\n\t\t\t\t\tjQuery( elem ).hide();\n\t\t\t\t}\n\n\t\t\t\t// Reset the properties, if the item has been hidden or shown\n\t\t\t\tif ( options.hide || options.show ) {\n\t\t\t\t\tfor ( p in options.animatedProperties ) {\n\t\t\t\t\t\tjQuery.style( elem, p, options.orig[ p ] );\n\t\t\t\t\t\tjQuery.removeData( elem, \"fxshow\" + p, true );\n\t\t\t\t\t\t// Toggle data is no longer needed\n\t\t\t\t\t\tjQuery.removeData( elem, \"toggle\" + p, true );\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Execute the complete function\n\t\t\t\t// in the event that the complete function throws an exception\n\t\t\t\t// we must ensure it won't be called twice. #5684\n\n\t\t\t\tcomplete = options.complete;\n\t\t\t\tif ( complete ) {\n\n\t\t\t\t\toptions.complete = false;\n\t\t\t\t\tcomplete.call( elem );\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn false;\n\n\t\t} else {\n\t\t\t// classical easing cannot be used with an Infinity duration\n\t\t\tif ( options.duration == Infinity ) {\n\t\t\t\tthis.now = t;\n\t\t\t} else {\n\t\t\t\tn = t - this.startTime;\n\t\t\t\tthis.state = n / options.duration;\n\n\t\t\t\t// Perform the easing function, defaults to swing\n\t\t\t\tthis.pos = jQuery.easing[ options.animatedProperties[this.prop] ]( this.state, n, 0, 1, options.duration );\n\t\t\t\tthis.now = this.start + ( (this.end - this.start) * this.pos );\n\t\t\t}\n\t\t\t// Perform the next step of the animation\n\t\t\tthis.update();\n\t\t}\n\n\t\treturn true;\n\t}\n};\n\njQuery.extend( jQuery.fx, {\n\ttick: function() {\n\t\tvar timer,\n\t\t\ttimers = jQuery.timers,\n\t\t\ti = 0;\n\n\t\tfor ( ; i < timers.length; i++ ) {\n\t\t\ttimer = timers[ i ];\n\t\t\t// Checks the timer has not already been removed\n\t\t\tif ( !timer() && timers[ i ] === timer ) {\n\t\t\t\ttimers.splice( i--, 1 );\n\t\t\t}\n\t\t}\n\n\t\tif ( !timers.length ) {\n\t\t\tjQuery.fx.stop();\n\t\t}\n\t},\n\n\tinterval: 13,\n\n\tstop: function() {\n\t\tclearInterval( timerId );\n\t\ttimerId = null;\n\t},\n\n\tspeeds: {\n\t\tslow: 600,\n\t\tfast: 200,\n\t\t// Default speed\n\t\t_default: 400\n\t},\n\n\tstep: {\n\t\topacity: function( fx ) {\n\t\t\tjQuery.style( fx.elem, \"opacity\", fx.now );\n\t\t},\n\n\t\t_default: function( fx ) {\n\t\t\tif ( fx.elem.style && fx.elem.style[ fx.prop ] != null ) {\n\t\t\t\tfx.elem.style[ fx.prop ] = fx.now + fx.unit;\n\t\t\t} else {\n\t\t\t\tfx.elem[ fx.prop ] = fx.now;\n\t\t\t}\n\t\t}\n\t}\n});\n\n// Adds width/height step functions\n// Do not set anything below 0\njQuery.each([ \"width\", \"height\" ], function( i, prop ) {\n\tjQuery.fx.step[ prop ] = function( fx ) {\n\t\tjQuery.style( fx.elem, prop, Math.max(0, fx.now) + fx.unit );\n\t};\n});\n\nif ( jQuery.expr && jQuery.expr.filters ) {\n\tjQuery.expr.filters.animated = function( elem ) {\n\t\treturn jQuery.grep(jQuery.timers, function( fn ) {\n\t\t\treturn elem === fn.elem;\n\t\t}).length;\n\t};\n}\n\n// Try to restore the default display value of an element\nfunction defaultDisplay( nodeName ) {\n\n\tif ( !elemdisplay[ nodeName ] ) {\n\n\t\tvar body = document.body,\n\t\t\telem = jQuery( \"<\" + nodeName + \">\" ).appendTo( body ),\n\t\t\tdisplay = elem.css( \"display\" );\n\t\telem.remove();\n\n\t\t// If the simple way fails,\n\t\t// get element's real default display by attaching it to a temp iframe\n\t\tif ( display === \"none\" || display === \"\" ) {\n\t\t\t// No iframe to use yet, so create it\n\t\t\tif ( !iframe ) {\n\t\t\t\tiframe = document.createElement( \"iframe\" );\n\t\t\t\tiframe.frameBorder = iframe.width = iframe.height = 0;\n\t\t\t}\n\n\t\t\tbody.appendChild( iframe );\n\n\t\t\t// Create a cacheable copy of the iframe document on first call.\n\t\t\t// IE and Opera will allow us to reuse the iframeDoc without re-writing the fake HTML\n\t\t\t// document to it; WebKit & Firefox won't allow reusing the iframe document.\n\t\t\tif ( !iframeDoc || !iframe.createElement ) {\n\t\t\t\tiframeDoc = ( iframe.contentWindow || iframe.contentDocument ).document;\n\t\t\t\tiframeDoc.write( ( document.compatMode === \"CSS1Compat\" ? \"<!doctype html>\" : \"\" ) + \"<html><body>\" );\n\t\t\t\tiframeDoc.close();\n\t\t\t}\n\n\t\t\telem = iframeDoc.createElement( nodeName );\n\n\t\t\tiframeDoc.body.appendChild( elem );\n\n\t\t\tdisplay = jQuery.css( elem, \"display\" );\n\t\t\tbody.removeChild( iframe );\n\t\t}\n\n\t\t// Store the correct default display\n\t\telemdisplay[ nodeName ] = display;\n\t}\n\n\treturn elemdisplay[ nodeName ];\n}\n\n\n\n\nvar rtable = /^t(?:able|d|h)$/i,\n\trroot = /^(?:body|html)$/i;\n\nif ( \"getBoundingClientRect\" in document.documentElement ) {\n\tjQuery.fn.offset = function( options ) {\n\t\tvar elem = this[0], box;\n\n\t\tif ( options ) {\n\t\t\treturn this.each(function( i ) {\n\t\t\t\tjQuery.offset.setOffset( this, options, i );\n\t\t\t});\n\t\t}\n\n\t\tif ( !elem || !elem.ownerDocument ) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif ( elem === elem.ownerDocument.body ) {\n\t\t\treturn jQuery.offset.bodyOffset( elem );\n\t\t}\n\n\t\ttry {\n\t\t\tbox = elem.getBoundingClientRect();\n\t\t} catch(e) {}\n\n\t\tvar doc = elem.ownerDocument,\n\t\t\tdocElem = doc.documentElement;\n\n\t\t// Make sure we're not dealing with a disconnected DOM node\n\t\tif ( !box || !jQuery.contains( docElem, elem ) ) {\n\t\t\treturn box ? { top: box.top, left: box.left } : { top: 0, left: 0 };\n\t\t}\n\n\t\tvar body = doc.body,\n\t\t\twin = getWindow(doc),\n\t\t\tclientTop  = docElem.clientTop  || body.clientTop  || 0,\n\t\t\tclientLeft = docElem.clientLeft || body.clientLeft || 0,\n\t\t\tscrollTop  = win.pageYOffset || jQuery.support.boxModel && docElem.scrollTop  || body.scrollTop,\n\t\t\tscrollLeft = win.pageXOffset || jQuery.support.boxModel && docElem.scrollLeft || body.scrollLeft,\n\t\t\ttop  = box.top  + scrollTop  - clientTop,\n\t\t\tleft = box.left + scrollLeft - clientLeft;\n\n\t\treturn { top: top, left: left };\n\t};\n\n} else {\n\tjQuery.fn.offset = function( options ) {\n\t\tvar elem = this[0];\n\n\t\tif ( options ) {\n\t\t\treturn this.each(function( i ) {\n\t\t\t\tjQuery.offset.setOffset( this, options, i );\n\t\t\t});\n\t\t}\n\n\t\tif ( !elem || !elem.ownerDocument ) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif ( elem === elem.ownerDocument.body ) {\n\t\t\treturn jQuery.offset.bodyOffset( elem );\n\t\t}\n\n\t\tvar computedStyle,\n\t\t\toffsetParent = elem.offsetParent,\n\t\t\tprevOffsetParent = elem,\n\t\t\tdoc = elem.ownerDocument,\n\t\t\tdocElem = doc.documentElement,\n\t\t\tbody = doc.body,\n\t\t\tdefaultView = doc.defaultView,\n\t\t\tprevComputedStyle = defaultView ? defaultView.getComputedStyle( elem, null ) : elem.currentStyle,\n\t\t\ttop = elem.offsetTop,\n\t\t\tleft = elem.offsetLeft;\n\n\t\twhile ( (elem = elem.parentNode) && elem !== body && elem !== docElem ) {\n\t\t\tif ( jQuery.support.fixedPosition && prevComputedStyle.position === \"fixed\" ) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tcomputedStyle = defaultView ? defaultView.getComputedStyle(elem, null) : elem.currentStyle;\n\t\t\ttop  -= elem.scrollTop;\n\t\t\tleft -= elem.scrollLeft;\n\n\t\t\tif ( elem === offsetParent ) {\n\t\t\t\ttop  += elem.offsetTop;\n\t\t\t\tleft += elem.offsetLeft;\n\n\t\t\t\tif ( jQuery.support.doesNotAddBorder && !(jQuery.support.doesAddBorderForTableAndCells && rtable.test(elem.nodeName)) ) {\n\t\t\t\t\ttop  += parseFloat( computedStyle.borderTopWidth  ) || 0;\n\t\t\t\t\tleft += parseFloat( computedStyle.borderLeftWidth ) || 0;\n\t\t\t\t}\n\n\t\t\t\tprevOffsetParent = offsetParent;\n\t\t\t\toffsetParent = elem.offsetParent;\n\t\t\t}\n\n\t\t\tif ( jQuery.support.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== \"visible\" ) {\n\t\t\t\ttop  += parseFloat( computedStyle.borderTopWidth  ) || 0;\n\t\t\t\tleft += parseFloat( computedStyle.borderLeftWidth ) || 0;\n\t\t\t}\n\n\t\t\tprevComputedStyle = computedStyle;\n\t\t}\n\n\t\tif ( prevComputedStyle.position === \"relative\" || prevComputedStyle.position === \"static\" ) {\n\t\t\ttop  += body.offsetTop;\n\t\t\tleft += body.offsetLeft;\n\t\t}\n\n\t\tif ( jQuery.support.fixedPosition && prevComputedStyle.position === \"fixed\" ) {\n\t\t\ttop  += Math.max( docElem.scrollTop, body.scrollTop );\n\t\t\tleft += Math.max( docElem.scrollLeft, body.scrollLeft );\n\t\t}\n\n\t\treturn { top: top, left: left };\n\t};\n}\n\njQuery.offset = {\n\n\tbodyOffset: function( body ) {\n\t\tvar top = body.offsetTop,\n\t\t\tleft = body.offsetLeft;\n\n\t\tif ( jQuery.support.doesNotIncludeMarginInBodyOffset ) {\n\t\t\ttop  += parseFloat( jQuery.css(body, \"marginTop\") ) || 0;\n\t\t\tleft += parseFloat( jQuery.css(body, \"marginLeft\") ) || 0;\n\t\t}\n\n\t\treturn { top: top, left: left };\n\t},\n\n\tsetOffset: function( elem, options, i ) {\n\t\tvar position = jQuery.css( elem, \"position\" );\n\n\t\t// set position first, in-case top/left are set even on static elem\n\t\tif ( position === \"static\" ) {\n\t\t\telem.style.position = \"relative\";\n\t\t}\n\n\t\tvar curElem = jQuery( elem ),\n\t\t\tcurOffset = curElem.offset(),\n\t\t\tcurCSSTop = jQuery.css( elem, \"top\" ),\n\t\t\tcurCSSLeft = jQuery.css( elem, \"left\" ),\n\t\t\tcalculatePosition = ( position === \"absolute\" || position === \"fixed\" ) && jQuery.inArray(\"auto\", [curCSSTop, curCSSLeft]) > -1,\n\t\t\tprops = {}, curPosition = {}, curTop, curLeft;\n\n\t\t// need to be able to calculate position if either top or left is auto and position is either absolute or fixed\n\t\tif ( calculatePosition ) {\n\t\t\tcurPosition = curElem.position();\n\t\t\tcurTop = curPosition.top;\n\t\t\tcurLeft = curPosition.left;\n\t\t} else {\n\t\t\tcurTop = parseFloat( curCSSTop ) || 0;\n\t\t\tcurLeft = parseFloat( curCSSLeft ) || 0;\n\t\t}\n\n\t\tif ( jQuery.isFunction( options ) ) {\n\t\t\toptions = options.call( elem, i, curOffset );\n\t\t}\n\n\t\tif ( options.top != null ) {\n\t\t\tprops.top = ( options.top - curOffset.top ) + curTop;\n\t\t}\n\t\tif ( options.left != null ) {\n\t\t\tprops.left = ( options.left - curOffset.left ) + curLeft;\n\t\t}\n\n\t\tif ( \"using\" in options ) {\n\t\t\toptions.using.call( elem, props );\n\t\t} else {\n\t\t\tcurElem.css( props );\n\t\t}\n\t}\n};\n\n\njQuery.fn.extend({\n\n\tposition: function() {\n\t\tif ( !this[0] ) {\n\t\t\treturn null;\n\t\t}\n\n\t\tvar elem = this[0],\n\n\t\t// Get *real* offsetParent\n\t\toffsetParent = this.offsetParent(),\n\n\t\t// Get correct offsets\n\t\toffset       = this.offset(),\n\t\tparentOffset = rroot.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset();\n\n\t\t// Subtract element margins\n\t\t// note: when an element has margin: auto the offsetLeft and marginLeft\n\t\t// are the same in Safari causing offset.left to incorrectly be 0\n\t\toffset.top  -= parseFloat( jQuery.css(elem, \"marginTop\") ) || 0;\n\t\toffset.left -= parseFloat( jQuery.css(elem, \"marginLeft\") ) || 0;\n\n\t\t// Add offsetParent borders\n\t\tparentOffset.top  += parseFloat( jQuery.css(offsetParent[0], \"borderTopWidth\") ) || 0;\n\t\tparentOffset.left += parseFloat( jQuery.css(offsetParent[0], \"borderLeftWidth\") ) || 0;\n\n\t\t// Subtract the two offsets\n\t\treturn {\n\t\t\ttop:  offset.top  - parentOffset.top,\n\t\t\tleft: offset.left - parentOffset.left\n\t\t};\n\t},\n\n\toffsetParent: function() {\n\t\treturn this.map(function() {\n\t\t\tvar offsetParent = this.offsetParent || document.body;\n\t\t\twhile ( offsetParent && (!rroot.test(offsetParent.nodeName) && jQuery.css(offsetParent, \"position\") === \"static\") ) {\n\t\t\t\toffsetParent = offsetParent.offsetParent;\n\t\t\t}\n\t\t\treturn offsetParent;\n\t\t});\n\t}\n});\n\n\n// Create scrollLeft and scrollTop methods\njQuery.each( [\"Left\", \"Top\"], function( i, name ) {\n\tvar method = \"scroll\" + name;\n\n\tjQuery.fn[ method ] = function( val ) {\n\t\tvar elem, win;\n\n\t\tif ( val === undefined ) {\n\t\t\telem = this[ 0 ];\n\n\t\t\tif ( !elem ) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\twin = getWindow( elem );\n\n\t\t\t// Return the scroll offset\n\t\t\treturn win ? (\"pageXOffset\" in win) ? win[ i ? \"pageYOffset\" : \"pageXOffset\" ] :\n\t\t\t\tjQuery.support.boxModel && win.document.documentElement[ method ] ||\n\t\t\t\t\twin.document.body[ method ] :\n\t\t\t\telem[ method ];\n\t\t}\n\n\t\t// Set the scroll offset\n\t\treturn this.each(function() {\n\t\t\twin = getWindow( this );\n\n\t\t\tif ( win ) {\n\t\t\t\twin.scrollTo(\n\t\t\t\t\t!i ? val : jQuery( win ).scrollLeft(),\n\t\t\t\t\t i ? val : jQuery( win ).scrollTop()\n\t\t\t\t);\n\n\t\t\t} else {\n\t\t\t\tthis[ method ] = val;\n\t\t\t}\n\t\t});\n\t};\n});\n\nfunction getWindow( elem ) {\n\treturn jQuery.isWindow( elem ) ?\n\t\telem :\n\t\telem.nodeType === 9 ?\n\t\t\telem.defaultView || elem.parentWindow :\n\t\t\tfalse;\n}\n\n\n\n\n// Create width, height, innerHeight, innerWidth, outerHeight and outerWidth methods\njQuery.each([ \"Height\", \"Width\" ], function( i, name ) {\n\n\tvar type = name.toLowerCase();\n\n\t// innerHeight and innerWidth\n\tjQuery.fn[ \"inner\" + name ] = function() {\n\t\tvar elem = this[0];\n\t\treturn elem ?\n\t\t\telem.style ?\n\t\t\tparseFloat( jQuery.css( elem, type, \"padding\" ) ) :\n\t\t\tthis[ type ]() :\n\t\t\tnull;\n\t};\n\n\t// outerHeight and outerWidth\n\tjQuery.fn[ \"outer\" + name ] = function( margin ) {\n\t\tvar elem = this[0];\n\t\treturn elem ?\n\t\t\telem.style ?\n\t\t\tparseFloat( jQuery.css( elem, type, margin ? \"margin\" : \"border\" ) ) :\n\t\t\tthis[ type ]() :\n\t\t\tnull;\n\t};\n\n\tjQuery.fn[ type ] = function( size ) {\n\t\t// Get window width or height\n\t\tvar elem = this[0];\n\t\tif ( !elem ) {\n\t\t\treturn size == null ? null : this;\n\t\t}\n\n\t\tif ( jQuery.isFunction( size ) ) {\n\t\t\treturn this.each(function( i ) {\n\t\t\t\tvar self = jQuery( this );\n\t\t\t\tself[ type ]( size.call( this, i, self[ type ]() ) );\n\t\t\t});\n\t\t}\n\n\t\tif ( jQuery.isWindow( elem ) ) {\n\t\t\t// Everyone else use document.documentElement or document.body depending on Quirks vs Standards mode\n\t\t\t// 3rd condition allows Nokia support, as it supports the docElem prop but not CSS1Compat\n\t\t\tvar docElemProp = elem.document.documentElement[ \"client\" + name ],\n\t\t\t\tbody = elem.document.body;\n\t\t\treturn elem.document.compatMode === \"CSS1Compat\" && docElemProp ||\n\t\t\t\tbody && body[ \"client\" + name ] || docElemProp;\n\n\t\t// Get document width or height\n\t\t} else if ( elem.nodeType === 9 ) {\n\t\t\t// Either scroll[Width/Height] or offset[Width/Height], whichever is greater\n\t\t\treturn Math.max(\n\t\t\t\telem.documentElement[\"client\" + name],\n\t\t\t\telem.body[\"scroll\" + name], elem.documentElement[\"scroll\" + name],\n\t\t\t\telem.body[\"offset\" + name], elem.documentElement[\"offset\" + name]\n\t\t\t);\n\n\t\t// Get or set width or height on the element\n\t\t} else if ( size === undefined ) {\n\t\t\tvar orig = jQuery.css( elem, type ),\n\t\t\t\tret = parseFloat( orig );\n\n\t\t\treturn jQuery.isNumeric( ret ) ? ret : orig;\n\n\t\t// Set the width or height on the element (default to pixels if value is unitless)\n\t\t} else {\n\t\t\treturn this.css( type, typeof size === \"string\" ? size : size + \"px\" );\n\t\t}\n\t};\n\n});\n\n\n\n\n// Expose jQuery to the global object\nwindow.jQuery = window.$ = jQuery;\n\n// Expose jQuery as an AMD module, but only for AMD loaders that\n// understand the issues with loading multiple versions of jQuery\n// in a page that all might call define(). The loader will indicate\n// they have special allowances for multiple jQuery versions by\n// specifying define.amd.jQuery = true. Register as a named module,\n// since jQuery can be concatenated with other files that may use define,\n// but not use a proper concatenation script that understands anonymous\n// AMD modules. A named AMD is safest and most robust way to register.\n// Lowercase jquery is used because AMD module names are derived from\n// file names, and jQuery is normally delivered in a lowercase file name.\n// Do this after creating the global so that if an AMD module wants to call\n// noConflict to hide this version of jQuery, it will work.\nif ( typeof define === \"function\" && define.amd && define.amd.jQuery ) {\n\tdefine( \"jquery\", [], function () { return jQuery; } );\n}\n\n\n\n})( window );\n"
  },
  {
    "path": "beetsplug/web/static/underscore.js",
    "content": "//     Underscore.js 1.2.2\n//     (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.\n//     Underscore is freely distributable under the MIT license.\n//     Portions of Underscore are inspired or borrowed from Prototype,\n//     Oliver Steele's Functional, and John Resig's Micro-Templating.\n//     For all details and documentation:\n//     http://documentcloud.github.com/underscore\n\n(function() {\n\n  // Baseline setup\n  // --------------\n\n  // Establish the root object, `window` in the browser, or `global` on the server.\n  var root = this;\n\n  // Save the previous value of the `_` variable.\n  var previousUnderscore = root._;\n\n  // Establish the object that gets returned to break out of a loop iteration.\n  var breaker = {};\n\n  // Save bytes in the minified (but not gzipped) version:\n  var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;\n\n  // Create quick reference variables for speed access to core prototypes.\n  var slice            = ArrayProto.slice,\n      unshift          = ArrayProto.unshift,\n      toString         = ObjProto.toString,\n      hasOwnProperty   = ObjProto.hasOwnProperty;\n\n  // All **ECMAScript 5** native function implementations that we hope to use\n  // are declared here.\n  var\n    nativeForEach      = ArrayProto.forEach,\n    nativeMap          = ArrayProto.map,\n    nativeReduce       = ArrayProto.reduce,\n    nativeReduceRight  = ArrayProto.reduceRight,\n    nativeFilter       = ArrayProto.filter,\n    nativeEvery        = ArrayProto.every,\n    nativeSome         = ArrayProto.some,\n    nativeIndexOf      = ArrayProto.indexOf,\n    nativeLastIndexOf  = ArrayProto.lastIndexOf,\n    nativeIsArray      = Array.isArray,\n    nativeKeys         = Object.keys,\n    nativeBind         = FuncProto.bind;\n\n  // Create a safe reference to the Underscore object for use below.\n  var _ = function(obj) { return new wrapper(obj); };\n\n  // Export the Underscore object for **Node.js** and **\"CommonJS\"**, with\n  // backwards-compatibility for the old `require()` API. If we're not in\n  // CommonJS, add `_` to the global object.\n  if (typeof exports !== 'undefined') {\n    if (typeof module !== 'undefined' && module.exports) {\n      exports = module.exports = _;\n    }\n    exports._ = _;\n  } else if (typeof define === 'function' && define.amd) {\n    // Register as a named module with AMD.\n    define('underscore', function() {\n      return _;\n    });\n  } else {\n    // Exported as a string, for Closure Compiler \"advanced\" mode.\n    root['_'] = _;\n  }\n\n  // Current version.\n  _.VERSION = '1.2.2';\n\n  // Collection Functions\n  // --------------------\n\n  // The cornerstone, an `each` implementation, aka `forEach`.\n  // Handles objects with the built-in `forEach`, arrays, and raw objects.\n  // Delegates to **ECMAScript 5**'s native `forEach` if available.\n  var each = _.each = _.forEach = function(obj, iterator, context) {\n    if (obj == null) return;\n    if (nativeForEach && obj.forEach === nativeForEach) {\n      obj.forEach(iterator, context);\n    } else if (obj.length === +obj.length) {\n      for (var i = 0, l = obj.length; i < l; i++) {\n        if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return;\n      }\n    } else {\n      for (var key in obj) {\n        if (hasOwnProperty.call(obj, key)) {\n          if (iterator.call(context, obj[key], key, obj) === breaker) return;\n        }\n      }\n    }\n  };\n\n  // Return the results of applying the iterator to each element.\n  // Delegates to **ECMAScript 5**'s native `map` if available.\n  _.map = function(obj, iterator, context) {\n    var results = [];\n    if (obj == null) return results;\n    if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);\n    each(obj, function(value, index, list) {\n      results[results.length] = iterator.call(context, value, index, list);\n    });\n    return results;\n  };\n\n  // **Reduce** builds up a single result from a list of values, aka `inject`,\n  // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.\n  _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {\n    var initial = memo !== void 0;\n    if (obj == null) obj = [];\n    if (nativeReduce && obj.reduce === nativeReduce) {\n      if (context) iterator = _.bind(iterator, context);\n      return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);\n    }\n    each(obj, function(value, index, list) {\n      if (!initial) {\n        memo = value;\n        initial = true;\n      } else {\n        memo = iterator.call(context, memo, value, index, list);\n      }\n    });\n    if (!initial) throw new TypeError(\"Reduce of empty array with no initial value\");\n    return memo;\n  };\n\n  // The right-associative version of reduce, also known as `foldr`.\n  // Delegates to **ECMAScript 5**'s native `reduceRight` if available.\n  _.reduceRight = _.foldr = function(obj, iterator, memo, context) {\n    if (obj == null) obj = [];\n    if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {\n      if (context) iterator = _.bind(iterator, context);\n      return memo !== void 0 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);\n    }\n    var reversed = (_.isArray(obj) ? obj.slice() : _.toArray(obj)).reverse();\n    return _.reduce(reversed, iterator, memo, context);\n  };\n\n  // Return the first value which passes a truth test. Aliased as `detect`.\n  _.find = _.detect = function(obj, iterator, context) {\n    var result;\n    any(obj, function(value, index, list) {\n      if (iterator.call(context, value, index, list)) {\n        result = value;\n        return true;\n      }\n    });\n    return result;\n  };\n\n  // Return all the elements that pass a truth test.\n  // Delegates to **ECMAScript 5**'s native `filter` if available.\n  // Aliased as `select`.\n  _.filter = _.select = function(obj, iterator, context) {\n    var results = [];\n    if (obj == null) return results;\n    if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);\n    each(obj, function(value, index, list) {\n      if (iterator.call(context, value, index, list)) results[results.length] = value;\n    });\n    return results;\n  };\n\n  // Return all the elements for which a truth test fails.\n  _.reject = function(obj, iterator, context) {\n    var results = [];\n    if (obj == null) return results;\n    each(obj, function(value, index, list) {\n      if (!iterator.call(context, value, index, list)) results[results.length] = value;\n    });\n    return results;\n  };\n\n  // Determine whether all of the elements match a truth test.\n  // Delegates to **ECMAScript 5**'s native `every` if available.\n  // Aliased as `all`.\n  _.every = _.all = function(obj, iterator, context) {\n    var result = true;\n    if (obj == null) return result;\n    if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);\n    each(obj, function(value, index, list) {\n      if (!(result = result && iterator.call(context, value, index, list))) return breaker;\n    });\n    return result;\n  };\n\n  // Determine if at least one element in the object matches a truth test.\n  // Delegates to **ECMAScript 5**'s native `some` if available.\n  // Aliased as `any`.\n  var any = _.some = _.any = function(obj, iterator, context) {\n    iterator = iterator || _.identity;\n    var result = false;\n    if (obj == null) return result;\n    if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);\n    each(obj, function(value, index, list) {\n      if (result || (result = iterator.call(context, value, index, list))) return breaker;\n    });\n    return !!result;\n  };\n\n  // Determine if a given value is included in the array or object using `===`.\n  // Aliased as `contains`.\n  _.include = _.contains = function(obj, target) {\n    var found = false;\n    if (obj == null) return found;\n    if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;\n    found = any(obj, function(value) {\n      return value === target;\n    });\n    return found;\n  };\n\n  // Invoke a method (with arguments) on every item in a collection.\n  _.invoke = function(obj, method) {\n    var args = slice.call(arguments, 2);\n    return _.map(obj, function(value) {\n      return (method.call ? method || value : value[method]).apply(value, args);\n    });\n  };\n\n  // Convenience version of a common use case of `map`: fetching a property.\n  _.pluck = function(obj, key) {\n    return _.map(obj, function(value){ return value[key]; });\n  };\n\n  // Return the maximum element or (element-based computation).\n  _.max = function(obj, iterator, context) {\n    if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj);\n    if (!iterator && _.isEmpty(obj)) return -Infinity;\n    var result = {computed : -Infinity};\n    each(obj, function(value, index, list) {\n      var computed = iterator ? iterator.call(context, value, index, list) : value;\n      computed >= result.computed && (result = {value : value, computed : computed});\n    });\n    return result.value;\n  };\n\n  // Return the minimum element (or element-based computation).\n  _.min = function(obj, iterator, context) {\n    if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj);\n    if (!iterator && _.isEmpty(obj)) return Infinity;\n    var result = {computed : Infinity};\n    each(obj, function(value, index, list) {\n      var computed = iterator ? iterator.call(context, value, index, list) : value;\n      computed < result.computed && (result = {value : value, computed : computed});\n    });\n    return result.value;\n  };\n\n  // Shuffle an array.\n  _.shuffle = function(obj) {\n    var shuffled = [], rand;\n    each(obj, function(value, index, list) {\n      if (index == 0) {\n        shuffled[0] = value;\n      } else {\n        rand = Math.floor(Math.random() * (index + 1));\n        shuffled[index] = shuffled[rand];\n        shuffled[rand] = value;\n      }\n    });\n    return shuffled;\n  };\n\n  // Sort the object's values by a criterion produced by an iterator.\n  _.sortBy = function(obj, iterator, context) {\n    return _.pluck(_.map(obj, function(value, index, list) {\n      return {\n        value : value,\n        criteria : iterator.call(context, value, index, list)\n      };\n    }).sort(function(left, right) {\n      var a = left.criteria, b = right.criteria;\n      return a < b ? -1 : a > b ? 1 : 0;\n    }), 'value');\n  };\n\n  // Groups the object's values by a criterion. Pass either a string attribute\n  // to group by, or a function that returns the criterion.\n  _.groupBy = function(obj, val) {\n    var result = {};\n    var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; };\n    each(obj, function(value, index) {\n      var key = iterator(value, index);\n      (result[key] || (result[key] = [])).push(value);\n    });\n    return result;\n  };\n\n  // Use a comparator function to figure out at what index an object should\n  // be inserted so as to maintain order. Uses binary search.\n  _.sortedIndex = function(array, obj, iterator) {\n    iterator || (iterator = _.identity);\n    var low = 0, high = array.length;\n    while (low < high) {\n      var mid = (low + high) >> 1;\n      iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid;\n    }\n    return low;\n  };\n\n  // Safely convert anything iterable into a real, live array.\n  _.toArray = function(iterable) {\n    if (!iterable)                return [];\n    if (iterable.toArray)         return iterable.toArray();\n    if (_.isArray(iterable))      return slice.call(iterable);\n    if (_.isArguments(iterable))  return slice.call(iterable);\n    return _.values(iterable);\n  };\n\n  // Return the number of elements in an object.\n  _.size = function(obj) {\n    return _.toArray(obj).length;\n  };\n\n  // Array Functions\n  // ---------------\n\n  // Get the first element of an array. Passing **n** will return the first N\n  // values in the array. Aliased as `head`. The **guard** check allows it to work\n  // with `_.map`.\n  _.first = _.head = function(array, n, guard) {\n    return (n != null) && !guard ? slice.call(array, 0, n) : array[0];\n  };\n\n  // Returns everything but the last entry of the array. Especcialy useful on\n  // the arguments object. Passing **n** will return all the values in\n  // the array, excluding the last N. The **guard** check allows it to work with\n  // `_.map`.\n  _.initial = function(array, n, guard) {\n    return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));\n  };\n\n  // Get the last element of an array. Passing **n** will return the last N\n  // values in the array. The **guard** check allows it to work with `_.map`.\n  _.last = function(array, n, guard) {\n    if ((n != null) && !guard) {\n      return slice.call(array, Math.max(array.length - n, 0));\n    } else {\n      return array[array.length - 1];\n    }\n  };\n\n  // Returns everything but the first entry of the array. Aliased as `tail`.\n  // Especially useful on the arguments object. Passing an **index** will return\n  // the rest of the values in the array from that index onward. The **guard**\n  // check allows it to work with `_.map`.\n  _.rest = _.tail = function(array, index, guard) {\n    return slice.call(array, (index == null) || guard ? 1 : index);\n  };\n\n  // Trim out all falsy values from an array.\n  _.compact = function(array) {\n    return _.filter(array, function(value){ return !!value; });\n  };\n\n  // Return a completely flattened version of an array.\n  _.flatten = function(array, shallow) {\n    return _.reduce(array, function(memo, value) {\n      if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value));\n      memo[memo.length] = value;\n      return memo;\n    }, []);\n  };\n\n  // Return a version of the array that does not contain the specified value(s).\n  _.without = function(array) {\n    return _.difference(array, slice.call(arguments, 1));\n  };\n\n  // Produce a duplicate-free version of the array. If the array has already\n  // been sorted, you have the option of using a faster algorithm.\n  // Aliased as `unique`.\n  _.uniq = _.unique = function(array, isSorted, iterator) {\n    var initial = iterator ? _.map(array, iterator) : array;\n    var result = [];\n    _.reduce(initial, function(memo, el, i) {\n      if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) {\n        memo[memo.length] = el;\n        result[result.length] = array[i];\n      }\n      return memo;\n    }, []);\n    return result;\n  };\n\n  // Produce an array that contains the union: each distinct element from all of\n  // the passed-in arrays.\n  _.union = function() {\n    return _.uniq(_.flatten(arguments, true));\n  };\n\n  // Produce an array that contains every item shared between all the\n  // passed-in arrays. (Aliased as \"intersect\" for back-compat.)\n  _.intersection = _.intersect = function(array) {\n    var rest = slice.call(arguments, 1);\n    return _.filter(_.uniq(array), function(item) {\n      return _.every(rest, function(other) {\n        return _.indexOf(other, item) >= 0;\n      });\n    });\n  };\n\n  // Take the difference between one array and another.\n  // Only the elements present in just the first array will remain.\n  _.difference = function(array, other) {\n    return _.filter(array, function(value){ return !_.include(other, value); });\n  };\n\n  // Zip together multiple lists into a single array -- elements that share\n  // an index go together.\n  _.zip = function() {\n    var args = slice.call(arguments);\n    var length = _.max(_.pluck(args, 'length'));\n    var results = new Array(length);\n    for (var i = 0; i < length; i++) results[i] = _.pluck(args, \"\" + i);\n    return results;\n  };\n\n  // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),\n  // we need this function. Return the position of the first occurrence of an\n  // item in an array, or -1 if the item is not included in the array.\n  // Delegates to **ECMAScript 5**'s native `indexOf` if available.\n  // If the array is large and already in sort order, pass `true`\n  // for **isSorted** to use binary search.\n  _.indexOf = function(array, item, isSorted) {\n    if (array == null) return -1;\n    var i, l;\n    if (isSorted) {\n      i = _.sortedIndex(array, item);\n      return array[i] === item ? i : -1;\n    }\n    if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);\n    for (i = 0, l = array.length; i < l; i++) if (array[i] === item) return i;\n    return -1;\n  };\n\n  // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.\n  _.lastIndexOf = function(array, item) {\n    if (array == null) return -1;\n    if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item);\n    var i = array.length;\n    while (i--) if (array[i] === item) return i;\n    return -1;\n  };\n\n  // Generate an integer Array containing an arithmetic progression. A port of\n  // the native Python `range()` function. See\n  // [the Python documentation](http://docs.python.org/library/functions.html#range).\n  _.range = function(start, stop, step) {\n    if (arguments.length <= 1) {\n      stop = start || 0;\n      start = 0;\n    }\n    step = arguments[2] || 1;\n\n    var len = Math.max(Math.ceil((stop - start) / step), 0);\n    var idx = 0;\n    var range = new Array(len);\n\n    while(idx < len) {\n      range[idx++] = start;\n      start += step;\n    }\n\n    return range;\n  };\n\n  // Function (ahem) Functions\n  // ------------------\n\n  // Reusable constructor function for prototype setting.\n  var ctor = function(){};\n\n  // Create a function bound to a given object (assigning `this`, and arguments,\n  // optionally). Binding with arguments is also known as `curry`.\n  // Delegates to **ECMAScript 5**'s native `Function.bind` if available.\n  // We check for `func.bind` first, to fail fast when `func` is undefined.\n  _.bind = function bind(func, context) {\n    var bound, args;\n    if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));\n    if (!_.isFunction(func)) throw new TypeError;\n    args = slice.call(arguments, 2);\n    return bound = function() {\n      if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));\n      ctor.prototype = func.prototype;\n      var self = new ctor;\n      var result = func.apply(self, args.concat(slice.call(arguments)));\n      if (Object(result) === result) return result;\n      return self;\n    };\n  };\n\n  // Bind all of an object's methods to that object. Useful for ensuring that\n  // all callbacks defined on an object belong to it.\n  _.bindAll = function(obj) {\n    var funcs = slice.call(arguments, 1);\n    if (funcs.length == 0) funcs = _.functions(obj);\n    each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });\n    return obj;\n  };\n\n  // Memoize an expensive function by storing its results.\n  _.memoize = function(func, hasher) {\n    var memo = {};\n    hasher || (hasher = _.identity);\n    return function() {\n      var key = hasher.apply(this, arguments);\n      return hasOwnProperty.call(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));\n    };\n  };\n\n  // Delays a function for the given number of milliseconds, and then calls\n  // it with the arguments supplied.\n  _.delay = function(func, wait) {\n    var args = slice.call(arguments, 2);\n    return setTimeout(function(){ return func.apply(func, args); }, wait);\n  };\n\n  // Defers a function, scheduling it to run after the current call stack has\n  // cleared.\n  _.defer = function(func) {\n    return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));\n  };\n\n  // Returns a function, that, when invoked, will only be triggered at most once\n  // during a given window of time.\n  _.throttle = function(func, wait) {\n    var context, args, timeout, throttling, more;\n    var whenDone = _.debounce(function(){ more = throttling = false; }, wait);\n    return function() {\n      context = this; args = arguments;\n      var later = function() {\n        timeout = null;\n        if (more) func.apply(context, args);\n        whenDone();\n      };\n      if (!timeout) timeout = setTimeout(later, wait);\n      if (throttling) {\n        more = true;\n      } else {\n        func.apply(context, args);\n      }\n      whenDone();\n      throttling = true;\n    };\n  };\n\n  // Returns a function, that, as long as it continues to be invoked, will not\n  // be triggered. The function will be called after it stops being called for\n  // N milliseconds.\n  _.debounce = function(func, wait) {\n    var timeout;\n    return function() {\n      var context = this, args = arguments;\n      var later = function() {\n        timeout = null;\n        func.apply(context, args);\n      };\n      clearTimeout(timeout);\n      timeout = setTimeout(later, wait);\n    };\n  };\n\n  // Returns a function that will be executed at most one time, no matter how\n  // often you call it. Useful for lazy initialization.\n  _.once = function(func) {\n    var ran = false, memo;\n    return function() {\n      if (ran) return memo;\n      ran = true;\n      return memo = func.apply(this, arguments);\n    };\n  };\n\n  // Returns the first function passed as an argument to the second,\n  // allowing you to adjust arguments, run code before and after, and\n  // conditionally execute the original function.\n  _.wrap = function(func, wrapper) {\n    return function() {\n      var args = [func].concat(slice.call(arguments));\n      return wrapper.apply(this, args);\n    };\n  };\n\n  // Returns a function that is the composition of a list of functions, each\n  // consuming the return value of the function that follows.\n  _.compose = function() {\n    var funcs = slice.call(arguments);\n    return function() {\n      var args = slice.call(arguments);\n      for (var i = funcs.length - 1; i >= 0; i--) {\n        args = [funcs[i].apply(this, args)];\n      }\n      return args[0];\n    };\n  };\n\n  // Returns a function that will only be executed after being called N times.\n  _.after = function(times, func) {\n    if (times <= 0) return func();\n    return function() {\n      if (--times < 1) { return func.apply(this, arguments); }\n    };\n  };\n\n  // Object Functions\n  // ----------------\n\n  // Retrieve the names of an object's properties.\n  // Delegates to **ECMAScript 5**'s native `Object.keys`\n  _.keys = nativeKeys || function(obj) {\n    if (obj !== Object(obj)) throw new TypeError('Invalid object');\n    var keys = [];\n    for (var key in obj) if (hasOwnProperty.call(obj, key)) keys[keys.length] = key;\n    return keys;\n  };\n\n  // Retrieve the values of an object's properties.\n  _.values = function(obj) {\n    return _.map(obj, _.identity);\n  };\n\n  // Return a sorted list of the function names available on the object.\n  // Aliased as `methods`\n  _.functions = _.methods = function(obj) {\n    var names = [];\n    for (var key in obj) {\n      if (_.isFunction(obj[key])) names.push(key);\n    }\n    return names.sort();\n  };\n\n  // Extend a given object with all the properties in passed-in object(s).\n  _.extend = function(obj) {\n    each(slice.call(arguments, 1), function(source) {\n      for (var prop in source) {\n        if (source[prop] !== void 0) obj[prop] = source[prop];\n      }\n    });\n    return obj;\n  };\n\n  // Fill in a given object with default properties.\n  _.defaults = function(obj) {\n    each(slice.call(arguments, 1), function(source) {\n      for (var prop in source) {\n        if (obj[prop] == null) obj[prop] = source[prop];\n      }\n    });\n    return obj;\n  };\n\n  // Create a (shallow-cloned) duplicate of an object.\n  _.clone = function(obj) {\n    if (!_.isObject(obj)) return obj;\n    return _.isArray(obj) ? obj.slice() : _.extend({}, obj);\n  };\n\n  // Invokes interceptor with the obj, and then returns obj.\n  // The primary purpose of this method is to \"tap into\" a method chain, in\n  // order to perform operations on intermediate results within the chain.\n  _.tap = function(obj, interceptor) {\n    interceptor(obj);\n    return obj;\n  };\n\n  // Internal recursive comparison function.\n  function eq(a, b, stack) {\n    // Identical objects are equal. `0 === -0`, but they aren't identical.\n    // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal.\n    if (a === b) return a !== 0 || 1 / a == 1 / b;\n    // A strict comparison is necessary because `null == undefined`.\n    if (a == null || b == null) return a === b;\n    // Unwrap any wrapped objects.\n    if (a._chain) a = a._wrapped;\n    if (b._chain) b = b._wrapped;\n    // Invoke a custom `isEqual` method if one is provided.\n    if (_.isFunction(a.isEqual)) return a.isEqual(b);\n    if (_.isFunction(b.isEqual)) return b.isEqual(a);\n    // Compare `[[Class]]` names.\n    var className = toString.call(a);\n    if (className != toString.call(b)) return false;\n    switch (className) {\n      // Strings, numbers, dates, and booleans are compared by value.\n      case '[object String]':\n        // Primitives and their corresponding object wrappers are equivalent; thus, `\"5\"` is\n        // equivalent to `new String(\"5\")`.\n        return String(a) == String(b);\n      case '[object Number]':\n        a = +a;\n        b = +b;\n        // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for\n        // other numeric values.\n        return a != a ? b != b : (a == 0 ? 1 / a == 1 / b : a == b);\n      case '[object Date]':\n      case '[object Boolean]':\n        // Coerce dates and booleans to numeric primitive values. Dates are compared by their\n        // millisecond representations. Note that invalid dates with millisecond representations\n        // of `NaN` are not equivalent.\n        return +a == +b;\n      // RegExps are compared by their source patterns and flags.\n      case '[object RegExp]':\n        return a.source == b.source &&\n               a.global == b.global &&\n               a.multiline == b.multiline &&\n               a.ignoreCase == b.ignoreCase;\n    }\n    if (typeof a != 'object' || typeof b != 'object') return false;\n    // Assume equality for cyclic structures. The algorithm for detecting cyclic\n    // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.\n    var length = stack.length;\n    while (length--) {\n      // Linear search. Performance is inversely proportional to the number of\n      // unique nested structures.\n      if (stack[length] == a) return true;\n    }\n    // Add the first object to the stack of traversed objects.\n    stack.push(a);\n    var size = 0, result = true;\n    // Recursively compare objects and arrays.\n    if (className == '[object Array]') {\n      // Compare array lengths to determine if a deep comparison is necessary.\n      size = a.length;\n      result = size == b.length;\n      if (result) {\n        // Deep compare the contents, ignoring non-numeric properties.\n        while (size--) {\n          // Ensure commutative equality for sparse arrays.\n          if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break;\n        }\n      }\n    } else {\n      // Objects with different constructors are not equivalent.\n      if (\"constructor\" in a != \"constructor\" in b || a.constructor != b.constructor) return false;\n      // Deep compare objects.\n      for (var key in a) {\n        if (hasOwnProperty.call(a, key)) {\n          // Count the expected number of properties.\n          size++;\n          // Deep compare each member.\n          if (!(result = hasOwnProperty.call(b, key) && eq(a[key], b[key], stack))) break;\n        }\n      }\n      // Ensure that both objects contain the same number of properties.\n      if (result) {\n        for (key in b) {\n          if (hasOwnProperty.call(b, key) && !(size--)) break;\n        }\n        result = !size;\n      }\n    }\n    // Remove the first object from the stack of traversed objects.\n    stack.pop();\n    return result;\n  }\n\n  // Perform a deep comparison to check if two objects are equal.\n  _.isEqual = function(a, b) {\n    return eq(a, b, []);\n  };\n\n  // Is a given array, string, or object empty?\n  // An \"empty\" object has no enumerable own-properties.\n  _.isEmpty = function(obj) {\n    if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;\n    for (var key in obj) if (hasOwnProperty.call(obj, key)) return false;\n    return true;\n  };\n\n  // Is a given value a DOM element?\n  _.isElement = function(obj) {\n    return !!(obj && obj.nodeType == 1);\n  };\n\n  // Is a given value an array?\n  // Delegates to ECMA5's native Array.isArray\n  _.isArray = nativeIsArray || function(obj) {\n    return toString.call(obj) == '[object Array]';\n  };\n\n  // Is a given variable an object?\n  _.isObject = function(obj) {\n    return obj === Object(obj);\n  };\n\n  // Is a given variable an arguments object?\n  if (toString.call(arguments) == '[object Arguments]') {\n    _.isArguments = function(obj) {\n      return toString.call(obj) == '[object Arguments]';\n    };\n  } else {\n    _.isArguments = function(obj) {\n      return !!(obj && hasOwnProperty.call(obj, 'callee'));\n    };\n  }\n\n  // Is a given value a function?\n  _.isFunction = function(obj) {\n    return toString.call(obj) == '[object Function]';\n  };\n\n  // Is a given value a string?\n  _.isString = function(obj) {\n    return toString.call(obj) == '[object String]';\n  };\n\n  // Is a given value a number?\n  _.isNumber = function(obj) {\n    return toString.call(obj) == '[object Number]';\n  };\n\n  // Is the given value `NaN`?\n  _.isNaN = function(obj) {\n    // `NaN` is the only value for which `===` is not reflexive.\n    return obj !== obj;\n  };\n\n  // Is a given value a boolean?\n  _.isBoolean = function(obj) {\n    return obj === true || obj === false || toString.call(obj) == '[object Boolean]';\n  };\n\n  // Is a given value a date?\n  _.isDate = function(obj) {\n    return toString.call(obj) == '[object Date]';\n  };\n\n  // Is the given value a regular expression?\n  _.isRegExp = function(obj) {\n    return toString.call(obj) == '[object RegExp]';\n  };\n\n  // Is a given value equal to null?\n  _.isNull = function(obj) {\n    return obj === null;\n  };\n\n  // Is a given variable undefined?\n  _.isUndefined = function(obj) {\n    return obj === void 0;\n  };\n\n  // Utility Functions\n  // -----------------\n\n  // Run Underscore.js in *noConflict* mode, returning the `_` variable to its\n  // previous owner. Returns a reference to the Underscore object.\n  _.noConflict = function() {\n    root._ = previousUnderscore;\n    return this;\n  };\n\n  // Keep the identity function around for default iterators.\n  _.identity = function(value) {\n    return value;\n  };\n\n  // Run a function **n** times.\n  _.times = function (n, iterator, context) {\n    for (var i = 0; i < n; i++) iterator.call(context, i);\n  };\n\n  // Escape a string for HTML interpolation.\n  _.escape = function(string) {\n    return (''+string).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;').replace(/'/g, '&#x27;').replace(/\\//g,'&#x2F;');\n  };\n\n  // Add your own custom functions to the Underscore object, ensuring that\n  // they're correctly added to the OOP wrapper as well.\n  _.mixin = function(obj) {\n    each(_.functions(obj), function(name){\n      addToWrapper(name, _[name] = obj[name]);\n    });\n  };\n\n  // Generate a unique integer id (unique within the entire client session).\n  // Useful for temporary DOM ids.\n  var idCounter = 0;\n  _.uniqueId = function(prefix) {\n    var id = idCounter++;\n    return prefix ? prefix + id : id;\n  };\n\n  // By default, Underscore uses ERB-style template delimiters, change the\n  // following template settings to use alternative delimiters.\n  _.templateSettings = {\n    evaluate    : /<%([\\s\\S]+?)%>/g,\n    interpolate : /<%=([\\s\\S]+?)%>/g,\n    escape      : /<%-([\\s\\S]+?)%>/g\n  };\n\n  // JavaScript micro-templating, similar to John Resig's implementation.\n  // Underscore templating handles arbitrary delimiters, preserves whitespace,\n  // and correctly escapes quotes within interpolated code.\n  _.template = function(str, data) {\n    var c  = _.templateSettings;\n    var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' +\n      'with(obj||{}){__p.push(\\'' +\n      str.replace(/\\\\/g, '\\\\\\\\')\n         .replace(/'/g, \"\\\\'\")\n         .replace(c.escape, function(match, code) {\n           return \"',_.escape(\" + code.replace(/\\\\'/g, \"'\") + \"),'\";\n         })\n         .replace(c.interpolate, function(match, code) {\n           return \"',\" + code.replace(/\\\\'/g, \"'\") + \",'\";\n         })\n         .replace(c.evaluate || null, function(match, code) {\n           return \"');\" + code.replace(/\\\\'/g, \"'\")\n                              .replace(/[\\r\\n\\t]/g, ' ') + \";__p.push('\";\n         })\n         .replace(/\\r/g, '\\\\r')\n         .replace(/\\n/g, '\\\\n')\n         .replace(/\\t/g, '\\\\t')\n         + \"');}return __p.join('');\";\n    var func = new Function('obj', '_', tmpl);\n    return data ? func(data, _) : function(data) { return func(data, _) };\n  };\n\n  // The OOP Wrapper\n  // ---------------\n\n  // If Underscore is called as a function, it returns a wrapped object that\n  // can be used OO-style. This wrapper holds altered versions of all the\n  // underscore functions. Wrapped objects may be chained.\n  var wrapper = function(obj) { this._wrapped = obj; };\n\n  // Expose `wrapper.prototype` as `_.prototype`\n  _.prototype = wrapper.prototype;\n\n  // Helper function to continue chaining intermediate results.\n  var result = function(obj, chain) {\n    return chain ? _(obj).chain() : obj;\n  };\n\n  // A method to easily add functions to the OOP wrapper.\n  var addToWrapper = function(name, func) {\n    wrapper.prototype[name] = function() {\n      var args = slice.call(arguments);\n      unshift.call(args, this._wrapped);\n      return result(func.apply(_, args), this._chain);\n    };\n  };\n\n  // Add all of the Underscore functions to the wrapper object.\n  _.mixin(_);\n\n  // Add all mutator Array functions to the wrapper.\n  each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {\n    var method = ArrayProto[name];\n    wrapper.prototype[name] = function() {\n      method.apply(this._wrapped, arguments);\n      return result(this._wrapped, this._chain);\n    };\n  });\n\n  // Add all accessor Array functions to the wrapper.\n  each(['concat', 'join', 'slice'], function(name) {\n    var method = ArrayProto[name];\n    wrapper.prototype[name] = function() {\n      return result(method.apply(this._wrapped, arguments), this._chain);\n    };\n  });\n\n  // Start chaining a wrapped Underscore object.\n  wrapper.prototype.chain = function() {\n    this._chain = true;\n    return this;\n  };\n\n  // Extracts the result from a wrapped and chained object.\n  wrapper.prototype.value = function() {\n    return this._wrapped;\n  };\n\n}).call(this);\n"
  },
  {
    "path": "beetsplug/web/templates/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n    <head>\n        <meta charset=\"utf-8\">\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n        <meta name=\"description\" content=\"the music geek’s media organizer\">\n        <meta name=\"keywords\"\n              content=\"beets, media, music, library, metadata, player, tagger, grep, transcoder, organizer\">\n        <title>beets</title>\n        <link rel=\"stylesheet\"\n              href=\"{{ url_for('static', filename='beets.css') }}\"\n              type=\"text/css\">\n        <script src=\"{{ url_for('static', filename='jquery.js') }}\"></script>\n        <script src=\"{{ url_for('static', filename='underscore.js') }}\"></script>\n        <script src=\"{{ url_for('static', filename='backbone.js') }}\"></script>\n        <script src=\"{{ url_for('static', filename='beets.js') }}\"></script>\n    </head>\n    <body>\n        <div id=\"header\">\n            <h1>beets</h1>\n            <div id=\"player\">\n                <audio></audio>\n                <button class=\"disabled\">&#9654;</button>\n                <button class=\"play\">&#9654;</button>\n                <button class=\"pause\" style=\"letter-spacing: 1px;\">&#10073;&#10073;</button>\n                <span class=\"times\">\n                    <span class=\"currentTime\"></span>\n                </span>\n            </div>\n        </div>\n        <div id=\"entities\">\n            <form id=\"queryForm\">\n                <input type=\"search\" id=\"query\" placeholder=\"Query\">\n            </form>\n            <ul id=\"results\">\n            </ul>\n        </div>\n        <div id=\"main-detail\"></div>\n        <div id=\"extra-detail\"></div>\n        <!-- Templates. -->\n        <script type=\"text/template\" id=\"item-entry-template\">\n            <% if (artist) { %><%= artist %><% } %>\n            <% if (artist && album) { %> &ndash; <% } %>\n            <% if (album) { %><%= album %><% } %>\n            <% if ((artist || album) && title) { %> &ndash; <% } %>\n            <%= title %>\n            <span class=\"playing\">&#9654;</span>\n        </script>\n        <script type=\"text/template\" id=\"item-main-detail-template\">\n            <span class=\"artist\"><%= artist %></span>\n            <span class=\"album\">\n                <span class=\"albumtitle\"><%= album %></span>\n                <span class=\"year\">(<%= year %>)</span>\n            </span>\n            <span class=\"title\"><%= title %></span>\n\n            <button class=\"play\">&#9654;</button>\n\n            \n        </script>\n        <script type=\"text/template\" id=\"item-extra-detail-template\">\n            <dl>\n                <dt>Track</dt>\n                <dd><%= track %>/<%= tracktotal %></dd>\n                <% if (disc) { %>\n                    <dt>Disc</dt>\n                    <dd><%= disc %>/<%= disctotal %></dd>\n                <% } %>\n                <dt>Length</dt>\n                <dd><%= timeFormat(length) %></dd>\n                <dt>Format</dt>\n                <dd><%= format %></dd>\n                <dt>Bitrate</dt>\n                <dd><%= Math.round(bitrate/1000) %> kbps</dd>\n                <% if (mb_trackid) { %>\n                    <dt>MusicBrainz entry</dt>\n                    <dd>\n                        <a target=\"_blank\" href=\"http://musicbrainz.org/recording/<%= mb_trackid %>\">view</a>\n                    </dd>\n                <% } %>\n                <dt>File</dt>\n                <dd>\n                    <a target=\"_blank\" class=\"download\" href=\"item/<%= id %>/file\">download</a>\n                </dd>\n                <% if (lyrics) { %>\n                    <dt>Lyrics</dt>\n                    <dd class=\"lyrics\"><%= lyrics %></dd>\n                <% } %>\n                <% if (comments) { %>\n                    <dt>Comments</dt>\n                    <dd><%= comments %></dd>\n                <% } %>\n            </dl>\n        </script>\n    </body>\n</html>\n"
  },
  {
    "path": "beetsplug/zero.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Blemjhoo Tezoulbr <baobab@heresiarch.info>.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Clears tag fields in media files.\"\"\"\n\nimport re\n\nimport confuse\nfrom mediafile import MediaFile\n\nfrom beets.importer import Action\nfrom beets.plugins import BeetsPlugin\nfrom beets.ui import Subcommand, input_yn\n\n__author__ = \"baobab@heresiarch.info\"\n\n\nclass ZeroPlugin(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n\n        self.register_listener(\"write\", self.write_event)\n        self.register_listener(\n            \"import_task_choice\", self.import_task_choice_event\n        )\n\n        self.config.add(\n            {\n                \"auto\": True,\n                \"fields\": [],\n                \"keep_fields\": [],\n                \"update_database\": False,\n                \"omit_single_disc\": False,\n            }\n        )\n\n        self.fields_to_progs = {}\n        self.warned = False\n\n        \"\"\"Read the bulk of the config into `self.fields_to_progs`.\n        After construction, `fields_to_progs` contains all the fields that\n        should be zeroed as keys and maps each of those to a list of compiled\n        regexes (progs) as values.\n        A field is zeroed if its value matches one of the associated progs. If\n        progs is empty, then the associated field is always zeroed.\n        \"\"\"\n        if self.config[\"fields\"] and self.config[\"keep_fields\"]:\n            self._log.warning(\"cannot blacklist and whitelist at the same time\")\n        # Blacklist mode.\n        elif self.config[\"fields\"]:\n            for field in self.config[\"fields\"].as_str_seq():\n                self._set_pattern(field)\n        # Whitelist mode.\n        elif self.config[\"keep_fields\"]:\n            for field in MediaFile.fields():\n                if (\n                    field not in self.config[\"keep_fields\"].as_str_seq()\n                    and\n                    # These fields should always be preserved.\n                    field not in (\"id\", \"path\", \"album_id\")\n                ):\n                    self._set_pattern(field)\n\n    def commands(self):\n        zero_command = Subcommand(\"zero\", help=\"set fields to null\")\n\n        def zero_fields(lib, opts, args):\n            if not args and not input_yn(\n                \"Remove fields for all items? (Y/n)\", True\n            ):\n                return\n            for item in lib.items(args):\n                self.process_item(item)\n\n        zero_command.func = zero_fields\n        return [zero_command]\n\n    def _set_pattern(self, field):\n        \"\"\"Populate `self.fields_to_progs` for a given field.\n        Do some sanity checks then compile the regexes.\n        \"\"\"\n        if field not in MediaFile.fields():\n            self._log.error(\"invalid field: {}\", field)\n        elif field in (\"id\", \"path\", \"album_id\"):\n            self._log.warning(\n                \"field '{}' ignored, zeroing it would be dangerous\", field\n            )\n        else:\n            try:\n                for pattern in self.config[field].as_str_seq():\n                    prog = re.compile(pattern, re.IGNORECASE)\n                    self.fields_to_progs.setdefault(field, []).append(prog)\n            except confuse.NotFoundError:\n                # Matches everything\n                self.fields_to_progs[field] = []\n\n    def import_task_choice_event(self, session, task):\n        if task.choice_flag == Action.ASIS and not self.warned:\n            self._log.warning('cannot zero in \"as-is\" mode')\n            self.warned = True\n        # TODO request write in as-is mode\n\n    def write_event(self, item, path, tags):\n        if self.config[\"auto\"]:\n            self.set_fields(item, tags)\n\n    def set_fields(self, item, tags):\n        \"\"\"Set values in `tags` to `None` if the field is in\n        `self.fields_to_progs` and any of the corresponding `progs` matches the\n        field value.\n        Also update the `item` itself if `update_database` is set in the\n        config.\n        \"\"\"\n        fields_set = False\n        if self.config[\"omit_single_disc\"].get(bool) and item.disctotal == 1:\n            for tag in {\"disc\", \"disctotal\"} & set(tags):\n                tags[tag] = None\n                fields_set = True\n\n        if not self.fields_to_progs:\n            self._log.warning(\"no fields list to remove\")\n\n        for field, progs in self.fields_to_progs.items():\n            if field in tags:\n                value = tags[field]\n                match = _match_progs(tags[field], progs)\n            else:\n                value = \"\"\n                match = not progs\n\n            if match:\n                fields_set = True\n                self._log.debug(\"{}: {} -> None\", field, value)\n                tags[field] = None\n                if self.config[\"update_database\"]:\n                    item[field] = None\n\n        return fields_set\n\n    def process_item(self, item):\n        tags = dict(item)\n\n        if self.set_fields(item, tags):\n            item.write(tags=tags)\n            if self.config[\"update_database\"]:\n                item.store(fields=tags)\n\n\ndef _match_progs(value, progs):\n    \"\"\"Check if `value` (as string) is matching any of the compiled regexes in\n    the `progs` list.\n    \"\"\"\n    if not progs:\n        return True\n    for prog in progs:\n        if prog.search(str(value)):\n            return True\n    return False\n"
  },
  {
    "path": "codecov.yml",
    "content": "comment:\n  layout: \"header, diff, files\"\n  require_changes: true\n\n# Sets non-blocking status checks\n# https://docs.codecov.com/docs/commit-status#informational\ncoverage:\n  status:\n    project:\n      default:\n        informational: true\n    patch:\n      default:\n        informational: true\n    changes: false\n"
  },
  {
    "path": "docs/.gitignore",
    "content": "_build\ngenerated/"
  },
  {
    "path": "docs/Makefile",
    "content": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nPAPER         =\nBUILDDIR      = _build\nSOURCEDIR\t  = .\n\n# When both are available, use Sphinx 2.x for autodoc compatibility.\nifeq ($(shell which sphinx-build2 >/dev/null 2>&1 ; echo $$?),0)\n\tSPHINXBUILD = sphinx-build2\nendif\n\n# Internal variables.\nPAPEROPT_a4     = -D latex_paper_size=a4\nPAPEROPT_letter = -D latex_paper_size=letter\nALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .\n\n.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest auto\n\nhelp:\n\t@echo \"Please use \\`make <target>' where <target> is one of\"\n\t@echo \"  html       to make standalone HTML files\"\n\t@echo \"  dirhtml    to make HTML files named index.html in directories\"\n\t@echo \"  singlehtml to make a single large HTML file\"\n\t@echo \"  pickle     to make pickle files\"\n\t@echo \"  json       to make JSON files\"\n\t@echo \"  htmlhelp   to make HTML files and a HTML help project\"\n\t@echo \"  qthelp     to make HTML files and a qthelp project\"\n\t@echo \"  devhelp    to make HTML files and a Devhelp project\"\n\t@echo \"  epub       to make an epub\"\n\t@echo \"  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter\"\n\t@echo \"  latexpdf   to make LaTeX files and run them through pdflatex\"\n\t@echo \"  text       to make text files\"\n\t@echo \"  man        to make manual pages\"\n\t@echo \"  changes    to make an overview of all changed/added/deprecated items\"\n\t@echo \"  linkcheck  to check all external links for integrity\"\n\t@echo \"  doctest    to run all doctests embedded in the documentation (if enabled)\"\n\nclean:\n\t-rm -rf $(BUILDDIR)/* $(SOURCEDIR)/api/generated/*\n\nhtml:\n\t$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/html.\"\n\ndirhtml:\n\t$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/dirhtml.\"\n\nsinglehtml:\n\t$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml\n\t@echo\n\t@echo \"Build finished. The HTML page is in $(BUILDDIR)/singlehtml.\"\n\npickle:\n\t$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle\n\t@echo\n\t@echo \"Build finished; now you can process the pickle files.\"\n\njson:\n\t$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json\n\t@echo\n\t@echo \"Build finished; now you can process the JSON files.\"\n\nhtmlhelp:\n\t$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp\n\t@echo\n\t@echo \"Build finished; now you can run HTML Help Workshop with the\" \\\n\t      \".hhp project file in $(BUILDDIR)/htmlhelp.\"\n\nqthelp:\n\t$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp\n\t@echo\n\t@echo \"Build finished; now you can run \"qcollectiongenerator\" with the\" \\\n\t      \".qhcp project file in $(BUILDDIR)/qthelp, like this:\"\n\t@echo \"# qcollectiongenerator $(BUILDDIR)/qthelp/beets.qhcp\"\n\t@echo \"To view the help file:\"\n\t@echo \"# assistant -collectionFile $(BUILDDIR)/qthelp/beets.qhc\"\n\ndevhelp:\n\t$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp\n\t@echo\n\t@echo \"Build finished.\"\n\t@echo \"To view the help file:\"\n\t@echo \"# mkdir -p $$HOME/.local/share/devhelp/beets\"\n\t@echo \"# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/beets\"\n\t@echo \"# devhelp\"\n\nepub:\n\t$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub\n\t@echo\n\t@echo \"Build finished. The epub file is in $(BUILDDIR)/epub.\"\n\nlatex:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo\n\t@echo \"Build finished; the LaTeX files are in $(BUILDDIR)/latex.\"\n\t@echo \"Run \\`make' in that directory to run these through (pdf)latex\" \\\n\t      \"(use \\`make latexpdf' here to do that automatically).\"\n\nlatexpdf:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through pdflatex...\"\n\tmake -C $(BUILDDIR)/latex all-pdf\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\ntext:\n\t$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text\n\t@echo\n\t@echo \"Build finished. The text files are in $(BUILDDIR)/text.\"\n\nman:\n\t$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man\n\t@echo\n\t@echo \"Build finished. The manual pages are in $(BUILDDIR)/man.\"\n\nchanges:\n\t$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes\n\t@echo\n\t@echo \"The overview file is in $(BUILDDIR)/changes.\"\n\nlinkcheck:\n\t$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck\n\t@echo\n\t@echo \"Link check complete; look for any errors in the above output \" \\\n\t      \"or in $(BUILDDIR)/linkcheck/output.txt.\"\n\ndoctest:\n\t$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest\n\t@echo \"Testing of doctests in the sources finished, look at the \" \\\n\t      \"results in $(BUILDDIR)/doctest/output.txt.\"\n"
  },
  {
    "path": "docs/_static/beets.css",
    "content": "html[data-theme=\"light\"] {\n    --pst-color-secondary: #a23632;\n}\nhtml[data-theme=\"light\"] {\n    --pst-color-inline-code: #a23632;\n}\n\n/* beetroot red: #a23632 */\n/* beetroot green: #1B5801 */\n/* beetroot green light: rgb(27, 150, 50) */\n/* pydata teal (primary): #126A7E */\n/* pydata violet (secondary): #7D0E70 */\n"
  },
  {
    "path": "docs/_templates/autosummary/base.rst",
    "content": "{{ fullname | escape | underline}}\n.. currentmodule:: {{ module }}\n.. auto{{ objtype }}:: {{ objname }}\n"
  },
  {
    "path": "docs/_templates/autosummary/class.rst",
    "content": "{{ name | escape | underline}}\n\n.. currentmodule:: {{ module }}\n\n.. autoclass:: {{ objname }}\n   :members:                 <-- add at least this line\n   :private-members:\n   :show-inheritance:        <-- plus I want to show inheritance...\n   :inherited-members:       <-- ...and inherited members too\n\n   {% block methods %}\n   .. automethod:: __init__\n\n   {% if methods %}\n   .. rubric:: {{ _('Public methods summary') }}\n\n   .. autosummary::\n   {% for item in methods %}\n      ~{{ name }}.{{ item }}\n   {%- endfor %}\n   {% for item in _methods %}\n      ~{{ name }}.{{ item }}\n   {%- endfor %}\n   {% endif %}\n   {% endblock %}\n\n   .. rubric:: {{ _('Methods definition') }}\n\n{% if objname in related_typeddicts %}\nRelated TypedDicts\n------------------\n\n{% for typeddict in related_typeddicts[objname] %}\n.. autotypeddict:: {{ typeddict }}\n   :show-inheritance:\n\n{% endfor %}\n{% endif %}\n"
  },
  {
    "path": "docs/_templates/autosummary/module.rst",
    "content": "{{ fullname | escape | underline}}\n{% block modules %}\n{% if modules %}\n.. rubric:: Modules\n\n{% for item in modules %}\n{{ item }}\n\n{%- endfor %}\n{% endif %}\n{% endblock %}\n"
  },
  {
    "path": "docs/_templates/autosummary/namedtuple.rst",
    "content": "{{ name | escape | underline }}\n\n.. currentmodule:: {{ module }}\n\n.. autonamedtuple:: {{ objname }}\n"
  },
  {
    "path": "docs/api/database.rst",
    "content": "Database\n========\n\n.. currentmodule:: beets.library\n\nLibrary\n-------\n\n.. autosummary::\n    :toctree: generated/\n\n    Library\n\nModels\n------\n\n.. autosummary::\n    :toctree: generated/\n\n    LibModel\n    Album\n    Item\n\nTransactions\n------------\n\n.. currentmodule:: beets.dbcore.db\n\n.. autosummary::\n    :toctree: generated/\n\n    Migration\n    Transaction\n\nQueries\n-------\n\n.. currentmodule:: beets.dbcore.query\n\n.. autosummary::\n    :toctree: generated/\n\n    Query\n    FieldQuery\n    AndQuery\n"
  },
  {
    "path": "docs/api/index.rst",
    "content": "API Reference\n=============\n\n.. toctree::\n    :maxdepth: 2\n    :titlesonly:\n\n    plugins\n    plugin_utilities\n    database\n"
  },
  {
    "path": "docs/api/plugin_utilities.rst",
    "content": "Plugin Utilities\n================\n\n.. currentmodule:: beetsplug._utils.requests\n\n.. autosummary::\n    :toctree: generated/\n\n    RequestHandler\n\n.. currentmodule:: beetsplug._utils.musicbrainz\n\n.. autosummary::\n    :toctree: generated/\n\n    MusicBrainzAPI\n"
  },
  {
    "path": "docs/api/plugins.rst",
    "content": "Plugins\n=======\n\n.. currentmodule:: beets.plugins\n\n.. autosummary::\n    :toctree: generated/\n\n    BeetsPlugin\n\n.. currentmodule:: beets.metadata_plugins\n\n.. autosummary::\n    :toctree: generated/\n\n    MetadataSourcePlugin\n    SearchApiMetadataSourcePlugin\n    SearchParams\n"
  },
  {
    "path": "docs/changelog.rst",
    "content": "Changelog\n=========\n\nChangelog goes here! Please add your entry to the bottom of one of the lists\nbelow!\n\n.. Uncomment the relevant section when you add the first entry\n\nUnreleased\n----------\n\nNew features\n~~~~~~~~~~~~\n\n- :doc:`plugins/discogs`: Add :conf:`plugins.discogs:extra_tags` option to use\n  additional tags (such as ``barcode``, ``catalognum``, ``country``, ``label``,\n  ``media``, and ``year``) in Discogs search queries.\n- :doc:`plugins/smartplaylist`: Add new configuration option ``dest_regen`` to\n  regenerate items' path in the generated playlist instead of using those in the\n  library. This is useful when items have been imported in don't copy-move (``-C\n  -M``) mode in the library but are later passed through the ``convert`` plugin\n  which will regenerate new paths according to the Beets path format.\n- :doc:`plugins/missing`: When running in missing album mode, allows users to\n  specify MusicBrainz release types to show using the ``--release-type`` flag.\n  The default behavior is also changed to just show releases of type ``album``.\n  :bug:`2661`\n- :doc:`plugins/play`: Added ``-R``/``--randomize`` flag to shuffle the playlist\n  order before passing it to the player.\n\nBug fixes\n~~~~~~~~~\n\n- :doc:`plugins/missing`: Fix ``--album`` mode incorrectly reporting albums\n  already in the library as missing. The comparison now correctly uses\n  ``mb_releasegroupid``.\n- :ref:`replace`: Made ``drive_sep_replace`` regex logic more precise to prevent\n  edge-case mismatches (e.g., a song titled \"1:00 AM\" would incorrectly be\n  considered a Windows drive path).\n- :doc:`plugins/fish`: Fix AttributeError. :bug:`6340`\n- :ref:`import-cmd` Autotagging by explicit release or recording IDs now keeps\n  candidates from all enabled metadata sources instead of dropping matches when\n  different providers share the same ID. :bug:`6178` :bug:`6181`\n- :doc:`plugins/mbsync` and :doc:`plugins/missing` now use each item's stored\n  ``data_source`` for ID lookups, with a fallback to ``MusicBrainz``.\n- :doc:`plugins/musicbrainz`: Use ``va_name`` config for ``albumartist_sort``,\n  ``albumartists_sort``, ``albumartist_credit``, ``albumartists_credit``, and\n  ``albumartists`` on VA releases instead of hardcoded \"Various Artists\".\n  :bug:`6316`\n- :doc:`plugins/beatport`: Use ``va_name`` config for the album artist on VA\n  releases instead of hardcoded \"Various Artists\". :bug:`6316`\n- :ref:`config-cmd` on Windows now uses ``cmd /c start \"\"`` for the default\n  editor fallback so ``beet config -e`` works when ``VISUAL`` and ``EDITOR`` are\n  unset. :bug:`6436`\n- :doc:`plugins/lastimport`: Rename flexible field ``play_count`` to\n  ``lastfm_play_count`` to avoid conflicts with :doc:`plugins/mpdstats`.\n  **Migration**: This cannot be migrated automatically because of the field\n  clash. If you use ``lastimport`` without ``mpdstats``, migrate manually with\n  ``beet modify lastfm_play_count='$play_count'``.\n\nFor plugin developers\n~~~~~~~~~~~~~~~~~~~~~\n\n- :py:func:`beets.metadata_plugins.album_for_id` and\n  :py:func:`beets.metadata_plugins.track_for_id` now require a ``data_source``\n  argument and query only that provider.\n- Colorisation, diff and layout utility helpers previously imported from\n  :mod:`beets.ui` now live in :mod:`beets.util.color`, :mod:`beets.util.diff`,\n  and :mod:`beets.util.layout`. Update external imports accordingly.\n- The ``tunelog`` logging helper that was exclusively available to the lastgenre\n  plugin is now usable througout beets and was renamed to ``extra_debug``.\n  Import it from the ``beets.logging`` module to use it.\n\nOther changes\n~~~~~~~~~~~~~\n\n- Deprecate the :doc:`plugins/beatport` and :doc:`plugins/bpsync` plugins.\n  Beatport has retired the API these plugins rely on, making them\n  non-functional. :bug:`3862`\n- API-backed metadata source plugins can now use\n  :py:class:`~beets.metadata_plugins.SearchApiMetadataSourcePlugin` for shared\n  search orchestration. Implement provider behavior in\n  :py:meth:`~beets.metadata_plugins.SearchApiMetadataSourcePlugin.get_search_query_with_filters`\n  and\n  :py:meth:`~beets.metadata_plugins.SearchApiMetadataSourcePlugin.get_search_response`.\n- :doc:`guides/installation`: Remove redundant macOS section from the\n  installation guide. :bug:`5993`\n- :doc:`guides/installation`: Update installation guide to document plugin\n  management with pipx and move package manager instructions to the FAQ.\n- :doc:`guides/main`: Update quick installation section to reflect current\n  installation guide structure.\n\n2.7.1 (March 08, 2026)\n----------------------\n\nBug fixes\n~~~~~~~~~\n\n- Tests that depend on the optional ``langdetect`` package are now skipped when\n  the package is not installed. :bug:`6421`\n\n2.7.0 (March 07, 2026)\n----------------------\n\nNew features\n~~~~~~~~~~~~\n\n- :doc:`plugins/lastgenre`: Added ``cleanup_existing`` configuration flag to\n  allow whitelist canonicalization of existing genres.\n- Add native support for multiple genres per album/track. The ``genres`` field\n  now stores genres as a list and is written to files as multiple individual\n  genre tags (e.g., separate GENRE tags for FLAC/MP3). The\n  :doc:`plugins/musicbrainz`, :doc:`plugins/beatport`, :doc:`plugins/discogs`\n  and :doc:`plugins/lastgenre` plugins have been updated to populate the\n  ``genres`` field as a list.\n\n  **Migration**: Existing libraries with comma-separated, semicolon-separated,\n  or slash-separated genre strings (e.g., ``\"Rock, Alternative, Indie\"``) are\n  automatically migrated to the ``genres`` list when you first run beets after\n  upgrading. The migration runs once when the database schema is updated,\n  splitting genre strings and writing the changes to the database. The updated\n  ``genres`` values will be written to media files the next time you run a\n  command that writes tags (such as ``beet write`` or during import). No manual\n  action or ``mbsync`` is required.\n\n  The ``genre`` field is split by the first separator found in the string, in\n  the following order of precedence:\n\n  1. :doc:`plugins/lastgenre` ``separator`` configuration\n  2. Semicolon followed by a space\n  3. Comma followed by a space\n  4. Slash wrapped by spaces\n\n- :doc:`plugins/lyrics`: With ``synced`` enabled, existing synced lyrics are no\n  longer replaced by newly fetched plain lyrics, even when ``force`` is enabled.\n- :doc:`plugins/lyrics`: Remove ``Source: <lyrics-url>`` suffix from lyrics.\n  Store the backend name in ``lyrics_backend``, URL in ``lyrics_url``, language\n  in ``lyrics_language`` and translation language (if translations present) in\n  ``lyrics_translation_language`` flexible attributes. Lyrics are automatically\n  migrated on the first beets run. :bug:`6370`\n\nBug fixes\n~~~~~~~~~\n\n- :doc:`plugins/ftintitle`: Fix handling of multiple featured artists with\n  ampersand.\n- :doc:`plugins/zero`: When the ``omit_single_disc`` option is set,\n  ``disctotal`` is zeroed alongside ``disc``.\n- :doc:`plugins/fetchart`: Prevent deletion of configured fallback cover art\n- :ref:`import-cmd` When autotagging, initialise empty multi-valued fields with\n  ``None`` instead of empty list, which caused beets to overwrite existing\n  metadata with empty list values instead of leaving them unchanged. :bug:`6403`\n- :doc:`plugins/fuzzy`: Improve fuzzy matching when the query is shorter than\n  the field value so substring-style searches produce more useful results.\n  :bug:`2043`\n- :doc:`plugins/fuzzy`: Force slow query evaluation whenever the fuzzy prefix is\n  used (for example ``~foo`` or ``%%foo``), so fuzzy matching is applied\n  consistently. :bug:`5638`\n- :ref:`import-cmd` Duplicate detection now works for as-is imports (when\n  ``autotag`` is disabled). Previously, ``duplicate_keys`` and\n  ``duplicate_action`` config options were silently ignored for as-is imports.\n- :doc:`/plugins/convert`: Fix extension substitution inside path of the\n  exported playlist.\n\nFor plugin developers\n~~~~~~~~~~~~~~~~~~~~~\n\n- If you maintain a metadata source plugin that populates the ``genre`` field,\n  please update it to populate a list of ``genres`` instead. You will see a\n  deprecation warning for now, but support for populating the single ``genre``\n  field will be removed in version ``3.0.0``.\n\nOther changes\n~~~~~~~~~~~~~\n\n- :ref:`modify-cmd`: Use the following separator to delimit multiple field\n  values: |semicolon_space|. For example ``beet modify albumtypes=\"album; ep\"``.\n  Previously, ``\\␀`` was used as a separator. This applies to fields such as\n  ``artists``, ``albumtypes`` etc.\n- Improve highlighting of multi-valued fields changes.\n- :doc:`plugins/edit`: Editing multi-valued fields now behaves more naturally,\n  with list values handled directly to make metadata edits smoother and more\n  predictable.\n- :doc:`plugins/lastgenre`: The ``separator`` configuration option is removed.\n  Since genres are now stored as a list in the ``genres`` field and written to\n  files as individual genre tags, this option has no effect and has been\n  removed.\n- :doc:`plugins/lyrics`: To cut down noise from the ``lrclib`` lyrics source,\n  synced lyrics are now checked to ensure the final verse falls within the\n  track's duration.\n- Updated URLs in the documentation to use HTTPS where possible and updated\n  outdated links.\n\n2.6.2 (February 22, 2026)\n-------------------------\n\nBug fixes\n~~~~~~~~~\n\n- :doc:`plugins/musicbrainz`: Fix crash when release mediums lack the ``tracks``\n  key. :bug:`6302`\n- :doc:`plugins/musicbrainz`: Fix search terms escaping. :bug:`6347`\n- :doc:`plugins/musicbrainz`: Fix support for ``alias`` and ``tracks``\n  :conf:`plugins.musicbrainz:extra_tags`.\n- :doc:`plugins/musicbrainz`: Fix fetching very large releases that have more\n  than 500 tracks. :bug:`6355`\n- :doc:`plugins/badfiles`: Fix number of found errors in log message\n- :doc:`plugins/replaygain`: Avoid magic Windows prefix in calls to command\n  backends, such as ``mp3gain``. :bug:`2946`\n- :doc:`plugins/mbpseudo`: Fix crash due to missing ``artist_credit`` field in\n  the MusicBrainz API response. :bug:`6339`\n- :ref:`config-cmd`: Improved error message when user-configured editor does not\n  exist. :bug:`6176`\n\nOther changes\n~~~~~~~~~~~~~\n\n- :doc:`plugins/lyrics`: Disable ``tekstowo`` by default because it blocks the\n  beets User-Agent.\n\n2.6.1 (February 02, 2026)\n-------------------------\n\nBug fixes\n~~~~~~~~~\n\n- Make ``packaging`` a required dependency. :bug:`6332`\n\n2.6.0 (February 01, 2026)\n-------------------------\n\nBeets now requires Python 3.10 or later since support for EOL Python 3.9 has\nbeen dropped.\n\nNew features\n~~~~~~~~~~~~\n\n- :doc:`plugins/fetchart`: Added config setting for a fallback cover art image.\n- :doc:`plugins/ftintitle`: Added argument for custom feat. words in ftintitle.\n- :doc:`plugins/ftintitle`: Added album template value ``album_artist_no_feat``.\n- :doc:`plugins/musicbrainz`: Allow selecting tags or genres to populate the\n  genres tag.\n- :doc:`plugins/ftintitle`: Added argument to skip the processing of artist and\n  album artist are the same in ftintitle.\n- :doc:`plugins/play`: Added ``$playlist`` marker to precisely edit the playlist\n  filepath into the command calling the player program.\n- :doc:`plugins/lastgenre`: For tuning plugin settings ``-vvv`` can be passed to\n  receive extra verbose logging around last.fm results and how they are\n  resolved. The ``extended_debug`` config setting and ``--debug`` option have\n  been removed.\n- :doc:`plugins/importsource`: Added new plugin that tracks original import\n  paths and optionally suggests removing source files when items are removed\n  from the library.\n- :doc:`plugins/mbpseudo`: Add a new ``mbpseudo`` plugin to proactively receive\n  MusicBrainz pseudo-releases as recommendations during import.\n- Added support for Python 3.13.\n- :doc:`/plugins/convert`: ``force`` can be passed to override checks like\n  no_convert, never_convert_lossy_files, same format, and max_bitrate\n- :doc:`plugins/titlecase`: Add the ``titlecase`` plugin to allow users to\n  resolve differences in metadata source styles.\n- :doc:`plugins/spotify`: Added support for multi-artist albums and tracks,\n  saving all contributing artists to the respective fields.\n- :doc:`plugins/fetchart`: Fix colorized output text.\n- :doc:`plugins/ftintitle`: Featured artists are now inserted before brackets\n  containing remix/edit-related keywords (e.g., \"Remix\", \"Live\", \"Edit\") instead\n  of being appended at the end. This improves formatting for titles like \"Song 1\n  (Carol Remix) ft. Bob\" which becomes \"Song 1 ft. Bob (Carol Remix)\". A variety\n  of brackets are supported and a new ``bracket_keywords`` configuration option\n  allows customizing the keywords. Setting ``bracket_keywords`` to an empty list\n  matches any bracket content regardless of keywords.\n- :doc:`plugins/discogs`: Added support for multi value fields. :bug:`6068`\n- :doc:`plugins/embedart`: Embedded arts can now be cleared during import with\n  the ``clearart_on_import`` config option. Also, ``beet clearart`` is only\n  going to update the files matching the query and with an embedded art, leaving\n  untouched the files without.\n- :doc:`plugins/fish`: Filenames are now completed in more places, like after\n  ``beet import``.\n- :doc:`plugins/random`: Added ``--field`` option to specify which field to use\n  for equal-chance sampling (default: ``albumartist``).\n- :doc:`plugins/musicbrainz`: Use title aliases for releases, release groups,\n  and recordings.\n\nBug fixes\n~~~~~~~~~\n\n- :doc:`/plugins/lastgenre`: Canonicalize genres when ``force`` and\n  ``keep_existing`` are ``on``, yet no genre info on lastfm could be found.\n  :bug:`6303`\n- Handle potential OSError when unlinking temporary files in ArtResizer.\n  :bug:`5615`\n- :doc:`/plugins/spotify`: Updated Spotify API credentials. :bug:`6270`\n- :doc:`/plugins/smartplaylist`: Fixed an issue where multiple queries in a\n  playlist configuration were not preserving their order, causing items to\n  appear in database order rather than the order specified in the config.\n  :bug:`6183`\n- :doc:`plugins/inline`: Fix recursion error when an inline field definition\n  shadows a built-in item field (e.g., redefining ``track_no``). Inline\n  expressions now skip self-references during evaluation to avoid infinite\n  recursion. :bug:`6115`\n- When hardlinking from a symlink (e.g. importing a symlink with hardlinking\n  enabled), dereference the symlink then hardlink, rather than creating a new\n  (potentially broken) symlink :bug:`5676`\n- :doc:`/plugins/spotify`: The plugin now gracefully handles audio-features API\n  deprecation (HTTP 403 errors). When a 403 error is encountered from the\n  audio-features endpoint, the plugin logs a warning once and skips audio\n  features for all remaining tracks in the session, avoiding unnecessary API\n  calls and rate limit exhaustion.\n- Running ``beet --config <mypath> config -e`` now edits ``<mypath>`` rather\n  than the default config path. :bug:`5652`\n- :doc:`plugins/lyrics`: Accepts strings for lyrics sources (previously only\n  accepted a list of strings). :bug:`5962`\n- Fix a bug introduced in release 2.4.0 where import from any valid\n  import-log-file always threw a \"none of the paths are importable\" error.\n- :doc:`/plugins/web`: repair broken ``/item/values/…`` and `/albums/values/…`\n  endpoints. Previously, due to single-quotes (ie. string literal) in the SQL\n  query, the query eg. ``GET /item/values/albumartist`` would return the literal\n  \"albumartist\" instead of a list of unique album artists.\n- Sanitize log messages by removing control characters preventing terminal\n  rendering issues.\n- When using :doc:`plugins/fromfilename` together with :doc:`plugins/edit`,\n  temporary tags extracted from filenames are no longer lost when discarding or\n  cancelling an edit session during import. :bug:`6104`\n- :ref:`update-cmd` :doc:`plugins/edit` fix display formatting of field changes\n  to clearly show added and removed flexible fields.\n- :doc:`plugins/lastgenre`: Fix the issue where last.fm doesn't return any\n  result in the artist genre stage because \"concatenation\" words in the artist\n  name (like \"feat.\", \"+\", or \"&\") prevent it. Using the albumartists list field\n  and fetching a genre for each artist separately improves the chance of\n  receiving valid results in that stage.\n- :doc:`/plugins/ftintitle`: Fixed artist name splitting to prioritize explicit\n  featuring tokens (feat, ft, featuring) over generic separators (&, and),\n  preventing incorrect splits when both are present.\n- :doc:`reference/cli`: Fix 'from_scratch' option for singleton imports: delete\n  all (old) metadata when new metadata is applied. :bug:`3706`\n- :doc:`/plugins/convert`: ``auto_keep`` now respects ``no_convert`` and\n  ``never_convert_lossy_files`` when deciding whether to copy/transcode items,\n  avoiding extra lossy duplicates.\n- :doc:`plugins/discogs`: Fixed unexpected flex attr from the Discogs plugin.\n  :bug:`6177`\n- Errors in metadata plugins during autotage process will now be logged but\n  won't crash beets anymore. If you want to raise exceptions instead, set the\n  new configuration option ``raise_on_error`` to ``yes`` :bug:`5903`,\n  :bug:`4789`.\n\nFor plugin developers\n~~~~~~~~~~~~~~~~~~~~~\n\n- A new plugin event, ``album_matched``, is sent when an album that is being\n  imported has been matched to its metadata and the corresponding distance has\n  been calculated.\n- Added a reusable requests handler which can be used by plugins to make HTTP\n  requests with built-in retry and backoff logic. It uses beets user-agent and\n  configures timeouts. See :class:`~beetsplug._utils.requests.RequestHandler`\n  for documentation.\n- Replaced dependency on ``python-musicbrainzngs`` with a lightweight custom\n  MusicBrainz client implementation and updated relevant plugins accordingly:\n\n  - :doc:`plugins/listenbrainz`\n  - :doc:`plugins/mbcollection`\n  - :doc:`plugins/mbpseudo`\n  - :doc:`plugins/missing`\n  - :doc:`plugins/musicbrainz`\n  - :doc:`plugins/parentwork`\n\n  See :class:`~beetsplug._utils.musicbrainz.MusicBrainzAPI` for documentation.\n\nFor packagers\n~~~~~~~~~~~~~\n\n- The minimum supported Python version is now 3.10.\n- An unused dependency on ``mock`` has been removed.\n\nOther changes\n~~~~~~~~~~~~~\n\n- The documentation chapter :doc:`dev/paths` has been moved to the \"For\n  Developers\" section and revised to reflect current best practices (pathlib\n  usage).\n- Refactored the ``beets/ui/commands.py`` monolithic file (2000+ lines) into\n  multiple modules within the ``beets/ui/commands`` directory for better\n  maintainability.\n- :doc:`plugins/bpd`: Raise ImportError instead of ValueError when GStreamer is\n  unavailable, enabling ``importorskip`` usage in pytest setup.\n- Finally removed gmusic plugin and all related code/docs as the Google Play\n  Music service was shut down in 2020.\n- Updated color documentation with ``bright_*`` and ``bg_bright_*`` entries.\n- Moved ``beets/random.py`` into ``beetsplug/random.py`` to cleanup core module.\n- dbcore: Allow models to declare SQL indices; add an ``items.album_id`` index\n  to speed up ``album.items()`` queries. :bug:`5809`\n\n2.5.1 (October 14, 2025)\n------------------------\n\nNew features\n~~~~~~~~~~~~\n\n- :doc:`plugins/zero`: Add new configuration option, ``omit_single_disc``, to\n  allow zeroing the disc number on write for single-disc albums. Defaults to\n  False.\n\nBug fixes\n~~~~~~~~~\n\n- |BeetsPlugin|: load the last plugin class defined in the plugin namespace.\n  :bug:`6093`\n\nFor packagers\n~~~~~~~~~~~~~\n\n- Fixed issue with legacy metadata plugins not copying properties from the base\n  class.\n- Reverted the following: When installing ``beets`` via git or locally the\n  version string now reflects the current git branch and commit hash.\n  :bug:`6089`\n\nOther changes\n~~~~~~~~~~~~~\n\n- Removed outdated mailing list contact information from the documentation\n  :bug:`5462`.\n- :doc:`guides/main`: Modernized the *Getting Started* guide with tabbed\n  sections and dropdown menus. Installation instructions have been streamlined,\n  and a new subpage now provides additional setup details.\n- Documentation: introduced a new role ``conf`` for documenting configuration\n  options. This role provides consistent formatting and creates references\n  automatically. Applied it to :doc:`plugins/deezer`, :doc:`plugins/discogs`,\n  :doc:`plugins/musicbrainz` and :doc:`plugins/spotify` plugins documentation.\n\n2.5.0 (October 11, 2025)\n------------------------\n\nNew features\n~~~~~~~~~~~~\n\n- :doc:`plugins/lastgenre`: Add a ``--pretend`` option to preview genre changes\n  without storing or writing them.\n- :doc:`plugins/convert`: Add a config option to disable writing metadata to\n  converted files.\n- :doc:`plugins/discogs`: New config option\n  :conf:`plugins.discogs:strip_disambiguation` to toggle stripping discogs\n  numeric disambiguation on artist and label fields.\n- :doc:`plugins/discogs` Added support for featured artists. :bug:`6038`\n- :doc:`plugins/discogs` New configuration option\n  :conf:`plugins.discogs:featured_string` to change the default string used to\n  join featured artists. The default string is ``Feat.``.\n- :doc:`plugins/discogs` Support for ``artist_credit`` in Discogs tags.\n  :bug:`3354`\n- :doc:`plugins/discogs` Support for name variations and config options to\n  specify where the variations are written. :bug:`3354`\n- :doc:`plugins/web` Support for ``nexttrack`` keyboard press\n\nBug fixes\n~~~~~~~~~\n\n- :doc:`plugins/musicbrainz` Refresh flexible MusicBrainz metadata on reimport\n  so format changes are applied. :bug:`6036`\n- :doc:`plugins/spotify` Ensure ``spotifysync`` keeps popularity, ISRC, and\n  related fields current even when audio features requests fail. :bug:`6061`\n- :doc:`plugins/spotify` Fixed an issue where track matching and lookups could\n  return incorrect or misleading results when using the Spotify plugin. The\n  problem occurred primarily when no album was provided or when the album field\n  was an empty string. :bug:`5189`\n- :doc:`plugins/spotify` Removed old and undocumented config options\n  ``artist_field``, ``album_field`` and ``track`` that were causing issues with\n  track matching. :bug:`5189`\n- :doc:`plugins/spotify` Fixed an issue where candidate lookup would not find\n  matches due to query escaping (single vs double quotes).\n- :doc:`plugins/discogs` Fixed inconsistency in stripping disambiguation from\n  artists but not labels. :bug:`5366`\n- :doc:`plugins/chroma` :doc:`plugins/bpsync` Fix plugin loading issue caused by\n  an import of another |BeetsPlugin| class. :bug:`6033`\n- :doc:`/plugins/fromfilename`: Fix :bug:`5218`, improve the code (refactor\n  regexps, allow for more cases, add some logging), add tests.\n- Metadata source plugins: Fixed data source penalty calculation that was\n  incorrectly applied during import matching. The\n  :conf:`plugins.index:source_weight` configuration option has been renamed to\n  :conf:`plugins.index:data_source_mismatch_penalty` to better reflect its\n  purpose. :bug:`6066`\n\nOther changes\n~~~~~~~~~~~~~\n\n- :doc:`plugins/index`: Clarify that musicbrainz must be mentioned if plugin\n  list modified :bug:`6020`\n- :doc:`/faq`: Add check for musicbrainz plugin if auto-tagger can't find a\n  match :bug:`6020`\n- :doc:`guides/tagger`: Section on no matching release found, related to\n  possibly disabled musicbrainz plugin :bug:`6020`\n- Moved ``art.py`` utility module from ``beets`` into ``beetsplug`` namespace as\n  it is not used in the core beets codebase. It can now be found in\n  ``beetsplug._utils``.\n- Moved ``vfs.py`` utility module from ``beets`` into ``beetsplug`` namespace as\n  it is not used in the core beets codebase. It can now be found in\n  ``beetsplug._utils``.\n- :class:`beets.metadata_plugins.MetadataSourcePlugin`: Remove discogs specific\n  disambiguation stripping.\n- When installing ``beets`` via git or locally the version string now reflects\n  the current git branch and commit hash. :bug:`4448`\n- :ref:`match-config`: ``match.distance_weights.source`` configuration has been\n  renamed to ``match.distance_weights.data_source`` for consistency with the\n  name of the field it refers to.\n\nFor developers and plugin authors\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- Typing improvements in ``beets/logging.py``: ``getLogger`` now returns\n  ``BeetsLogger`` when called with a name, or ``RootLogger`` when called without\n  a name.\n- The ``track_distance()`` and ``album_distance()`` methods have been removed\n  from ``MetadataSourcePlugin``. Distance calculation for data source mismatches\n  is now handled automatically by the core matching logic. This change\n  simplifies the plugin architecture and fixes incorrect penalty calculations.\n  :bug:`6066`\n- Metadata source plugins are now registered globally when instantiated, which\n  makes their handling slightly more efficient.\n\n2.4.0 (September 13, 2025)\n--------------------------\n\nNew features\n~~~~~~~~~~~~\n\n- :doc:`plugins/musicbrainz`: The MusicBrainz autotagger has been moved to a\n  separate plugin. The default :ref:`plugins-config` includes ``musicbrainz``,\n  but if you've customized your ``plugins`` list in your configuration, you'll\n  need to explicitly add ``musicbrainz`` to continue using this functionality.\n  Configuration option :conf:`plugins.musicbrainz:enabled` has thus been\n  deprecated. :bug:`2686` :bug:`4605`\n- :doc:`plugins/web`: Show notifications when a track plays. This uses the Media\n  Session API to customize media notifications.\n- :doc:`plugins/discogs`: Add configurable :conf:`plugins.discogs:search_limit`\n  option to limit the number of results returned by the Discogs metadata search\n  queries.\n- :doc:`plugins/discogs`: Implement ``track_for_id`` method to allow retrieving\n  singletons by their Discogs ID. :bug:`4661`\n- :doc:`plugins/replace`: Add new plugin.\n- :doc:`plugins/duplicates`: Add ``--remove`` option, allowing to remove from\n  the library without deleting media files. :bug:`5832`\n- :doc:`plugins/playlist`: Support files with the ``.m3u8`` extension.\n  :bug:`5829`\n- :doc:`plugins/mbcollection`: When getting the user collections, only consider\n  collections of releases, and ignore collections of other entity types.\n- :doc:`plugins/mpdstats`: Add new configuration option,\n  ``played_ratio_threshold``, to allow configuring the percentage the song must\n  be played for it to be counted as played instead of skipped.\n- :doc:`plugins/web`: Display artist and album as part of the search results.\n- :doc:`plugins/spotify` :doc:`plugins/deezer`: Add new configuration option\n  :conf:`plugins.index:search_limit` to limit the number of results returned by\n  search queries.\n\nBug fixes\n~~~~~~~~~\n\n- :doc:`plugins/musicbrainz`: fix regression where user configured\n  :conf:`plugins.musicbrainz:extra_tags` have been read incorrectly. :bug:`5788`\n- tests: Fix library tests failing on Windows when run from outside ``D:/``.\n  :bug:`5802`\n- Fix an issue where calling ``Library.add`` would cause the ``database_change``\n  event to be sent twice, not once. :bug:`5560`\n- Fix ``HiddenFileTest`` by using ``bytestring_path()``.\n- tests: Fix tests failing without ``langdetect`` (by making it required).\n  :bug:`5797`\n- :doc:`plugins/musicbrainz`: Fix the MusicBrainz search not taking into account\n  the album/recording aliases\n- :doc:`/plugins/spotify`: Fix the issue with that every query to spotify was\n  ascii encoded. This resulted in bad matches for queries that contained special\n  e.g. non latin characters as 盗作. If you want to keep the legacy behavior set\n  the config option ``spotify.search_query_ascii: yes``. :bug:`5699`\n- :doc:`plugins/discogs`: Beets will no longer crash if a release has been\n  deleted, and returns a 404.\n- :doc:`plugins/lastgenre`: Fix the issue introduced in Beets 2.3.0 where\n  non-whitelisted last.fm genres were not canonicalized to parent genres.\n  :bug:`5930`\n- :doc:`plugins/chroma`: AcoustID lookup HTTP requests will now time out after\n  10 seconds, rather than hanging the entire import process.\n- :doc:`/plugins/deezer`: Fix the issue with that every query to deezer was\n  ascii encoded. This resulted in bad matches for queries that contained special\n  e.g. non latin characters as 盗作. If you want to keep the legacy behavior set\n  the config option ``deezer.search_query_ascii: yes``. :bug:`5860`\n- Fixed regression with :doc:`/plugins/listenbrainz` where the plugin could not\n  be loaded :bug:`5975`\n- :doc:`/plugins/fromfilename`: Beets will no longer crash if a track's title\n  field is missing.\n\nFor packagers\n~~~~~~~~~~~~~\n\n- Optional :conf:`plugins.musicbrainz:extra_tags` parameter has been removed\n  from ``BeetsPlugin.candidates`` method signature since it is never passed in.\n  If you override this method in your plugin, feel free to remove this\n  parameter.\n- Loosened ``typing_extensions`` dependency in pyproject.toml to apply to every\n  python version.\n\nFor plugin developers\n~~~~~~~~~~~~~~~~~~~~~\n\n- The ``fetchart`` plugins has seen a few changes to function signatures and\n  source registration in the process of introducing typings to the code. Custom\n  art sources might need to be adapted.\n- We split the responsibilities of plugins into two base classes\n\n  1. |BeetsPlugin| is the base class for all plugins, any plugin needs to\n     inherit from this class.\n  2. :class:`beets.metadata_plugins.MetadataSourcePlugin` allows plugins to act\n     like metadata sources. E.g. used by the MusicBrainz plugin. All plugins in\n     the beets repo are opted into this class where applicable. If you are\n     maintaining a plugin that acts like a metadata source, i.e. you expose any\n     of ``track_for_id``, ``album_for_id``, ``candidates``, ``item_candidates``,\n     ``album_distance``, ``track_distance`` methods, please update your plugin\n     to inherit from the new baseclass, as otherwise your plugin will stop\n     working with the next major release.\n\n- Several definitions have been moved:\n\n  - ``BLOB_TYPE`` constant, ``PathQuery`` and ``SingletonQuery`` queries have\n    moved from ``beets.library`` to ``beets.dbcore.query`` module\n  - ``DateType``, ``DurationType``, ``PathType`` types and ``MusicalKey`` class\n    have moved from ``beets.library`` to ``beets.dbcore.types`` module.\n  - ``Distance`` has moved from ``beets.autotag`` to ``beets.autotag.distance``\n    module.\n  - ``beets.autotag.current_metadata`` has been renamed to\n    ``beets.util.get_most_common_tags``.\n\n  Old imports are now deprecated and will be removed in version ``3.0.0``.\n\n- ``beets.ui.decargs`` is deprecated and will be removed in version ``3.0.0``.\n- Beets is now PEP 561 compliant, which means that it provides type hints for\n  all public APIs. This allows IDEs to provide better autocompletion and type\n  checking for downstream users of the beets API.\n- ``plugins.find_plugins`` function does not anymore load plugins. You need to\n  explicitly call ``plugins.load_plugins()`` to load them.\n- ``plugins.load_plugins`` function does not anymore accept the list of plugins\n  to load. Instead, it loads all plugins that are configured by\n  :ref:`plugins-config` configuration.\n- Flexible fields, which can be used by plugins to store additional metadata,\n  now also support list values. Previously, beets would throw an error while\n  storing the data in the SQL database due to missing type conversion.\n  :bug:`5698`\n\nOther changes\n~~~~~~~~~~~~~\n\n- Refactor: Split responsibilities of Plugins into MetaDataPlugins and general\n  Plugins.\n- Documentation structure for auto generated API references changed slightly.\n  Autogenerated API references are now located in the ``docs/api`` subdirectory.\n- :doc:`/plugins/substitute`: Fix rST formatting for example cases so that each\n  case is shown on separate lines.\n- :doc:`/plugins/ftintitle`: Process items whose albumartist is not contained in\n  the artist field, including compilations using Various Artists as an\n  albumartist and album tracks by guest artists featuring a third artist.\n- Refactored library.py file by splitting it into multiple modules within the\n  beets/library directory.\n- Added a test to check that all plugins can be imported without errors.\n- :doc:`/guides/main`: Add instructions to install beets on Void Linux.\n- :doc:`plugins/lastgenre`: Refactor loading whitelist and canonicalization\n  file. :bug:`5979`\n- :doc:`plugins/lastgenre`: Updated and streamlined the genre whitelist and\n  canonicalization tree :bug:`5977`\n- UI: Update default ``text_diff_added`` color from **bold red** to **bold\n  green.**\n- UI: Use ``text_diff_added`` and ``text_diff_removed`` colors in **all** diff\n  comparisons, including case differences.\n\n2.3.1 (May 14, 2025)\n--------------------\n\nBug fixes\n~~~~~~~~~\n\n- :doc:`/reference/pathformat`: Fixed a regression where path legalization\n  incorrectly removed parts of user-configured path formats that followed a dot\n  (**.**). :bug:`5771`\n\nFor packagers\n~~~~~~~~~~~~~\n\n- Force ``poetry`` version below 2 to avoid it mangling file modification times\n  in ``sdist`` package. :bug:`5770`\n\n2.3.0 (May 07, 2025)\n--------------------\n\nBeets now requires Python 3.9 or later since support for EOL Python 3.8 has been\ndropped.\n\nNew features\n~~~~~~~~~~~~\n\n- :doc:`plugins/lastgenre`: The new configuration option, ``keep_existing``,\n  provides more fine-grained control over how pre-populated genre tags are\n  handled. The ``force`` option now behaves in a more conventional manner.\n  :bug:`4982`\n- :doc:`plugins/lyrics`: Add new configuration option ``dist_thresh`` to control\n  the maximum allowed distance between the lyrics search result and the tagged\n  item's artist and title. This is useful for preventing false positives when\n  fetching lyrics.\n- :doc:`plugins/lyrics`: Rewrite lyrics translation functionality to use Azure\n  AI Translator API and add relevant instructions to the documentation.\n- :doc:`plugins/missing`: Add support for all metadata sources.\n- :doc:`plugins/mbsync`: Add support for all metadata sorces.\n\nBug fixes\n~~~~~~~~~\n\n- :doc:`plugins/thumbnails`: Fix API call to GIO on big endian architectures\n  (like s390x) in thumbnails plugin. :bug:`5708`\n- :doc:`plugins/listenbrainz`: Fix rST formatting for URLs of Listenbrainz API\n  Key documentation and config.yaml.\n- :doc:`plugins/listenbrainz`: Fix ``UnboundLocalError`` in cases where 'mbid'\n  is not defined.\n- :doc:`plugins/fetchart`: Fix fetchart bug where a tempfile could not be\n  deleted due to never being properly closed. :bug:`5521`\n- :doc:`plugins/lyrics`: LRCLib will fallback to plain lyrics if synced lyrics\n  are not found and ``synced`` flag is set to ``yes``.\n- Synchronise files included in the source distribution with what we used to\n  have before the introduction of Poetry. :bug:`5531` :bug:`5526`\n- :ref:`write-cmd`: Fix the issue where for certain files differences in\n  ``mb_artistid``, ``mb_albumartistid`` and ``albumtype`` fields are shown on\n  every attempt to write tags. Note: your music needs to be reimported with\n  ``beet import -LI`` or synchronised with ``beet mbsync`` in order to fix this!\n  :bug:`5265` :bug:`5371` :bug:`4715`\n- :ref:`import-cmd`: Fix ``MemoryError`` and improve performance tagging large\n  albums by replacing ``munkres`` library with ``lap.lapjv``. :bug:`5207`\n- :ref:`query-sort`: Fix a bug that would raise an exception when sorting on a\n  non-string field that is not populated in all items. :bug:`5512`\n- :doc:`plugins/lastgenre`: Fix track-level genre handling. Now when an\n  album-level genre is set already, single tracks don't fall back to the album's\n  genre and request their own last.fm genre. Also log messages regarding what's\n  been tagged are now more polished. :bug:`5582`\n- Fix ambiguous column name ``sqlite3.OperationalError`` that occured in album\n  queries that filtered album track titles, for example ``beet list -a keyword\n  title:foo``.\n- :doc:`plugins/lyrics`: Rewrite lyrics tests using pytest to provide isolated\n  configuration for each test case. This fixes the issue where some tests failed\n  because they read developers' local lyrics configuration. :bug:`5133`\n- :doc:`plugins/lyrics`: Do not attempt to search for lyrics if either the\n  artist or title is missing and ignore ``artist_sort`` value if it is empty.\n  :bug:`2635`\n- :doc:`plugins/lyrics`: Fix fetching lyrics from ``lrclib`` source. If we\n  cannot find lyrics for a specific album, artist, title combination, the plugin\n  now tries to search for the artist and title and picks the most relevant\n  result. Update the default ``sources`` configuration to prioritize ``lrclib``\n  over other sources since it returns reliable results quicker than others.\n  :bug:`5102`\n- :doc:`plugins/lyrics`: Fix the issue with ``genius`` backend not being able to\n  match lyrics when there is a slight variation in the artist name. :bug:`4791`\n- :doc:`plugins/lyrics`: Fix plugin crash when ``genius`` backend returns empty\n  lyrics. :bug:`5583`\n- ImageMagick 7.1.1-44 is now supported.\n- :doc:`plugins/parentwork`: Only output parentwork changes when running in\n  verbose mode.\n\nFor packagers\n~~~~~~~~~~~~~\n\n- The minimum supported Python version is now 3.9.\n- External plugin developers: ``beetsplug/__init__.py`` file can be removed from\n  your plugin as beets now uses native/implicit namespace package setup.\n\nOther changes\n~~~~~~~~~~~~~\n\n- Release workflow: fix the issue where the new release tag is created for the\n  wrong (outdated) commit. Now the tag is created in the same workflow step\n  right after committing the version update. :bug:`5539`\n- :doc:`/plugins/smartplaylist`: URL-encode additional item ``fields`` within\n  generated EXTM3U playlists instead of JSON-encoding them.\n- typehints: ``./beets/importer.py`` file now has improved typehints.\n- typehints: ``./beets/plugins.py`` file now includes typehints.\n- :doc:`plugins/ftintitle`: Optimize the plugin by avoiding unnecessary writes\n  to the database.\n- Database models are now serializable with pickle.\n\n2.2.0 (December 02, 2024)\n-------------------------\n\nNew features\n~~~~~~~~~~~~\n\n- :doc:`/plugins/substitute`: Allow the replacement string to use capture groups\n  from the match. It is thus possible to create more general rules, applying to\n  many different artists at once.\n\nBug fixes\n~~~~~~~~~\n\n- Check if running python from the Microsoft Store and provide feedback to\n  install from python.org. :bug:`5467`\n- Fix bug where matcher doesn't consider medium number when importing. This\n  makes it difficult to import hybrid SACDs and other releases with duplicate\n  tracks. :bug:`5148`\n- Bring back test files and the manual to the source distribution tarball.\n  :bug:`5513`\n\nOther changes\n~~~~~~~~~~~~~\n\n- Changed ``bitesize`` label to ``good first issue``. Our contribute_ page is\n  now automatically populated with these issues. :bug:`4855`\n\n.. _contribute: https://github.com/beetbox/beets/contribute\n\n2.1.0 (November 22, 2024)\n-------------------------\n\nNew features\n~~~~~~~~~~~~\n\n- New template function added: ``%capitalize``. Converts the first letter of the\n  text to uppercase and the rest to lowercase.\n- Ability to query albums with track db fields and vice-versa, for example\n  ``beet list -a title:something`` or ``beet list artpath:cover``. Consequently\n  album queries involving ``path`` field have been sped up, like ``beet list -a\n  path:/path/``.\n- :doc:`plugins/ftintitle`: New ``keep_in_artist`` option for the plugin, which\n  allows keeping the \"feat.\" part in the artist metadata while still changing\n  the title.\n- :doc:`plugins/autobpm`: Add new configuration option ``beat_track_kwargs``\n  which enables adjusting keyword arguments supplied to librosa's ``beat_track``\n  function call.\n- Beets now uses ``platformdirs`` to determine the default music directory. This\n  location varies between systems -- for example, users can configure it on Unix\n  systems via ``user-dirs.dirs(5)``.\n\nBug fixes\n~~~~~~~~~\n\n- :doc:`plugins/ftintitle`: The detection of a \"feat. X\" part in a song title\n  does not produce any false positives caused by words like \"and\" or \"with\"\n  anymore. :bug:`5441`\n- :doc:`plugins/ftintitle`: The detection of a \"feat. X\" part now also matches\n  such parts if they are in parentheses or brackets. :bug:`5436`\n- Improve naming of temporary files by separating the random part with the file\n  extension.\n- Fix the ``auto`` value for the :ref:`reflink` config option.\n- Fix lyrics plugin only getting part of the lyrics from ``Genius.com``\n  :bug:`4815`\n- Album flexible fields are now correctly saved. For instance MusicBrainz\n  external links such as ``bandcamp_album_id`` will be available on albums in\n  addition to tracks. For albums already in your library, a re-import is\n  required for the fields to be added. Such a re-import can be done with, in\n  this case, ``beet import -L data_source:=MusicBrainz``.\n- :doc:`plugins/autobpm`: Fix the ``TypeError`` where tempo was being returned\n  as a numpy array. Update ``librosa`` dependency constraint to prevent similar\n  issues in the future. :bug:`5289`\n- :doc:`plugins/discogs`: Fix the ``TypeError`` when there is no description.\n- Use single quotes in all SQL queries :bug:`4709`\n- :doc:`plugins/lyrics`: Update ``tekstowo`` backend to fetch lyrics directly\n  since recent updates to their website made it unsearchable. :bug:`5456`\n- :doc:`plugins/convert`: Fixed the convert plugin ``no_convert`` option so that\n  it no longer treats \"and\" and \"or\" queries the same. To maintain previous\n  behaviour add commas between your query keywords. For help see\n  :ref:`combiningqueries`.\n- Fix the ``TypeError`` when :ref:`set_fields` is provided non-string values.\n  :bug:`4840`\n\nFor packagers\n~~~~~~~~~~~~~\n\n- The minimum supported Python version is now 3.8.\n- The ``beet`` script has been removed from the repository.\n- The ``typing_extensions`` is required for Python 3.10 and below.\n\nOther changes\n~~~~~~~~~~~~~\n\n- :doc:`contributing`: The project now uses ``poetry`` for packaging and\n  dependency management. This change affects project management and mostly\n  affects beets developers. Please see updates in :ref:`getting-the-source` and\n  :ref:`testing` for more information.\n- :doc:`contributing`: Since ``poetry`` now manages local virtual environments,\n  ``tox`` has been replaced by a task runner ``poethepoet``. This change affects\n  beets developers and contributors. Please see updates in the\n  :ref:`development-tools` section for more details. Type ``poe`` while in the\n  project directory to see the available commands.\n- Installation instructions have been made consistent across plugins\n  documentation. Users should simply install ``beets`` with an ``extra`` of the\n  corresponding plugin name in order to install extra dependencies for that\n  plugin.\n- GitHub workflows have been reorganised for clarity: style, linting, type and\n  docs checks now live in separate jobs and are named accordingly.\n- Added caching for dependency installation in all CI jobs which speeds them up\n  a bit, especially the tests.\n- The linting workflow has been made to run only when Python files or\n  documentation is changed, and they only check the changed files. When\n  dependencies are updated (``poetry.lock``), then the entire code base is\n  checked.\n- The long-deprecated ``beets.util.confit`` module has been removed. This may\n  cause extremely outdated external plugins to fail to load.\n- :doc:`plugins/autobpm`: Add plugin dependencies to ``pyproject.toml`` under\n  the ``autobpm`` extra and update the plugin installation instructions in the\n  docs. Since importing the bpm calculation functionality from ``librosa`` takes\n  around 4 seconds, update the plugin to only do so when it actually needs to\n  calculate the bpm. Previously this import was being done immediately, so every\n  ``beet`` invocation was being delayed by a couple of seconds. :bug:`5185`\n\n2.0.0 (May 30, 2024)\n--------------------\n\nWith this release, beets now requires Python 3.7 or later (it removes support\nfor Python 3.6).\n\nMajor new features\n~~~~~~~~~~~~~~~~~~\n\n- The beets importer UI received a major overhaul. Several new configuration\n  options are available for customizing layout and colors: :ref:`ui_options`.\n  :bug:`3721` :bug:`5028`\n\nNew features\n~~~~~~~~~~~~\n\n- :doc:`/plugins/edit`: Prefer editor from ``VISUAL`` environment variable over\n  ``EDITOR``.\n- :ref:`config-cmd`: Prefer editor from ``VISUAL`` environment variable over\n  ``EDITOR``.\n- :doc:`/plugins/listenbrainz`: Add initial support for importing history and\n  playlists from ``ListenBrainz`` :bug:`1719`\n- :doc:`plugins/mbsubmit`: add new prompt choices helping further to submit\n  unmatched tracks to MusicBrainz faster.\n- :doc:`plugins/spotify`: We now fetch track's ISRC, EAN, and UPC identifiers\n  from Spotify when using the ``spotifysync`` command. :bug:`4992`\n- :doc:`plugins/discogs`: supply a value for the ``cover_art_url`` attribute,\n  for use by ``fetchart``. :bug:`429`\n- :ref:`update-cmd`: added ``-e`` flag for excluding fields from being updated.\n- :doc:`/plugins/deezer`: Import rank and other attributes from Deezer during\n  import and add a function to update the rank of existing items. :bug:`4841`\n- resolve transl-tracklisting relations for pseudo releases and merge data with\n  the actual release :bug:`654`\n- Fetchart: Use the right field (``spotify_album_id``) to obtain the Spotify\n  album id :bug:`4803`\n- Prevent reimporting album if it is permanently removed from Spotify\n  :bug:`4800`\n- Added option to use ``cover_art_url`` as an album art source in the\n  ``fetchart`` plugin. :bug:`4707`\n- :doc:`/plugins/fetchart`: The plugin can now get album art from ``spotify``.\n- Added option to specify a URL in the ``embedart`` plugin. :bug:`83`\n- :ref:`list-cmd` ``singleton:true`` queries have been made faster\n- :ref:`list-cmd` ``singleton:1`` and ``singleton:0`` can now alternatively be\n  used in queries, same as ``comp``\n- --from-logfile now parses log files using a UTF-8 encoding in\n  ``beets/beets/ui/commands.py``. :bug:`4693`\n- :doc:`/plugins/bareasc` lookups have been made faster\n- :ref:`list-cmd` lookups using the pattern operator ``::`` have been made\n  faster\n- Added additional error handling for ``spotify`` plugin. :bug:`4686`\n- We now import the remixer field from Musicbrainz into the library. :bug:`4428`\n- :doc:`/plugins/mbsubmit`: Added a new ``mbsubmit`` command to print track\n  information to be submitted to MusicBrainz after initial import. :bug:`4455`\n- Added ``spotify_updated`` field to track when the information was last\n  updated.\n- We now import and tag the ``album`` information when importing singletons\n  using Spotify source. :bug:`4398`\n- :doc:`/plugins/spotify`: The plugin now provides an additional command\n  ``spotifysync`` that allows getting track popularity and audio features\n  information from Spotify. :bug:`4094`\n- :doc:`/plugins/spotify`: The plugin now records Spotify-specific IDs in the\n  ``spotify_album_id``, ``spotify_artist_id``, and ``spotify_track_id`` fields.\n  :bug:`4348`\n- Create the parental directories for database if they do not exist. :bug:`3808`\n  :bug:`4327`\n- :ref:`musicbrainz-config`: a new :conf:`plugins.musicbrainz:enabled` option\n  allows disabling the MusicBrainz metadata source during the autotagging\n  process\n- :doc:`/plugins/kodiupdate`: Now supports multiple kodi instances :bug:`4101`\n- Add the item fields ``bitrate_mode``, ``encoder_info`` and\n  ``encoder_settings``.\n- Add query prefixes ``=`` and ``~``.\n- A new configuration option, :ref:`duplicate_keys`, lets you change which\n  fields the beets importer uses to identify duplicates. :bug:`1133` :bug:`4199`\n- Add :ref:`exact match <exact-match>` queries, using the prefixes ``=`` and\n  ``=~``. :bug:`4251`\n- :doc:`/plugins/discogs`: Permit appending style to genre.\n- :doc:`plugins/discogs`: Implement item_candidates for matching singletons.\n- :doc:`plugins/discogs`: Check for compliant discogs_client module.\n- :doc:`/plugins/convert`: Add a new ``auto_keep`` option that automatically\n  converts files but keeps the *originals* in the library. :bug:`1840`\n  :bug:`4302`\n- Added a ``-P`` (or ``--disable-plugins``) flag to specify one/multiple\n  plugin(s) to be disabled at startup.\n- :ref:`import-options`: Add support for re-running the importer on paths in log\n  files that were created with the ``-l`` (or ``--logfile``) argument.\n  :bug:`4379` :bug:`4387`\n- Preserve mtimes from archives :bug:`4392`\n- Add :ref:`%sunique{} <sunique>` template to disambiguate between singletons.\n  :bug:`4438`\n- Add a new ``import.ignored_alias_types`` config option to allow for specific\n  alias types to be skipped over when importing items/albums.\n- :doc:`/plugins/smartplaylist`: A new ``--pretend`` option lets the user see\n  what a new or changed smart playlist saved in the config is actually\n  returning. :bug:`4573`\n- :doc:`/plugins/fromfilename`: Add debug log messages that inform when the\n  plugin replaced bad (missing) artist, title or tracknumber metadata.\n  :bug:`4561` :bug:`4600`\n- :ref:`musicbrainz-config`: MusicBrainz release pages often link to related\n  metadata sources like Discogs, Bandcamp, Spotify, Deezer and Beatport. When\n  enabled via the :conf:`plugins.musicbrainz:external_ids` options, release ID's\n  will be extracted from those URL's and imported to the library. :bug:`4220`\n- :doc:`/plugins/convert`: Add support for generating m3u8 playlists together\n  with converted media files. :bug:`4373`\n- Fetch the ``release_group_title`` field from MusicBrainz. :bug:`4809`\n- :doc:`plugins/discogs`: Add support for applying album information on\n  singleton imports. :bug:`4716`\n- :doc:`/plugins/smartplaylist`: During explicit runs of the ``splupdate``\n  command, the log message \"Creating playlist ...\"\" is now displayed instead of\n  hidden in the debug log, which states some form of progress through the UI.\n  :bug:`4861`\n- :doc:`plugins/subsonicupdate`: Updates are now triggered whenever either the\n  beets database is changed or a smart playlist is created/updated. :bug:`4862`\n- :doc:`plugins/importfeeds`: Add a new output format allowing to save a\n  playlist once per import session. :bug:`4863`\n- Make ArtResizer work with :pypi:`PIL`/:pypi:`pillow` 10.0.0 removals.\n  :bug:`4869`\n- A new configuration option, :ref:`duplicate_verbose_prompt`, allows changing\n  how duplicates are presented during import. :bug:`4866`\n- :doc:`/plugins/embyupdate`: Add handling for private users by adding\n  ``userid`` config option. :bug:`4402`\n- :doc:`/plugins/substitute`: Add the new plugin ``substitute`` as an\n  alternative to the ``rewrite`` plugin. The main difference between them being\n  that ``rewrite`` modifies files' metadata and ``substitute`` does not.\n  :bug:`2786`\n- Add support for ``artists`` and ``albumartists`` multi-valued tags. :bug:`505`\n- :doc:`/plugins/autobpm`: Add the ``autobpm`` plugin which uses Librosa to\n  calculate the BPM of the audio. :bug:`3856`\n- :doc:`/plugins/fetchart`: Fix the error with CoverArtArchive where the\n  ``maxwidth`` option would not be used to download a pre-sized thumbnail for\n  release groups, as is already done with releases.\n- :doc:`/plugins/fetchart`: Fix the error with CoverArtArchive where no cover\n  would be found when the ``maxwidth`` option matches a pre-sized thumbnail\n  size, but no thumbnail is provided by CAA. We now fallback to the raw image.\n- :doc:`/plugins/advancedrewrite`: Add an advanced version of the ``rewrite``\n  plugin which allows to replace fields based on a given library query.\n- :doc:`/plugins/lyrics`: Add LRCLIB as a new lyrics provider and a new\n  ``synced`` option to prefer synced lyrics over plain lyrics.\n- :ref:`import-cmd`: Expose import.quiet_fallback as CLI option.\n- :ref:`import-cmd`: Expose ``import.incremental_skip_later`` as CLI option.\n- :doc:`/plugins/smartplaylist`: Expose config options as CLI options.\n- :doc:`/plugins/smartplaylist`: Add new option ``smartplaylist.output``.\n- :doc:`/plugins/smartplaylist`: Add new option ``smartplaylist.uri_format``.\n- Sorted the default configuration file into categories. :bug:`4987`\n- :doc:`/plugins/convert`: Don't treat WAVE (``.wav``) files as lossy anymore\n  when using the ``never_convert_lossy_files`` option. They will get transcoded\n  like the other lossless formats.\n- Add support for ``barcode`` field. :bug:`3172`\n- :doc:`/plugins/smartplaylist`: Add new config option ``smartplaylist.fields``.\n- :doc:`/plugins/fetchart`: Defer source removal config option evaluation to the\n  point where they are used really, supporting temporary config changes.\n\nBug fixes\n~~~~~~~~~\n\n- Improve ListenBrainz error handling. :bug:`5459`\n- :doc:`/plugins/deezer`: Improve requests error handling.\n- :doc:`/plugins/lastimport`: Improve error handling in the ``process_tracks``\n  function and enable it to be used with other plugins.\n- :doc:`/plugins/spotify`: Improve handling of ConnectionError.\n- :doc:`/plugins/deezer`: Improve Deezer plugin error handling and set requests\n  timeout to 10 seconds. :bug:`4983`\n- :doc:`/plugins/spotify`: Add bad gateway (502) error handling.\n- :doc:`/plugins/spotify`: Add a limit of 3 retries, instead of retrying\n  endlessly when the API is not available.\n- Fix a crash when the Spotify API timeouts or does not return a ``Retry-After``\n  interval. :bug:`4942`\n- :doc:`/plugins/scrub`: Fixed the import behavior where scrubbed database tags\n  were restored to newly imported tracks with config settings ``scrub.auto:\n  yes`` and ``import.write: no``. :bug:`4326`\n- :doc:`/plugins/deezer`: Fixed the error where Deezer plugin would crash if\n  non-Deezer id is passed during import.\n- :doc:`/plugins/fetchart`: Fix fetching from Cover Art Archive when the\n  ``maxwidth`` option is set to one of the supported Cover Art Archive widths.\n- :doc:`/plugins/discogs`: Fix \"Discogs plugin replacing Feat. or Ft. with a\n  comma\" by fixing an oversight that removed a functionality from the code base\n  when the MetadataSourcePlugin abstract class was introduced in PR's #3335 and\n  #3371. :bug:`4401`\n- :doc:`/plugins/convert`: Set default ``max_bitrate`` value to ``None`` to\n  avoid transcoding when this parameter is not set. :bug:`4472`\n- :doc:`/plugins/replaygain`: Avoid a crash when errors occur in the analysis\n  backend. :bug:`4506`\n- We now use Python's defaults for command-line argument encoding, which should\n  reduce the chance for errors and \"file not found\" failures when invoking other\n  command-line tools, especially on Windows. :bug:`4507`\n- We now respect the Spotify API's rate limiting, which avoids crashing when the\n  API reports code 429 (too many requests). :bug:`4370`\n- Fix implicit paths OR queries (e.g. ``beet list /path/ , /other-path/``) which\n  have previously been returning the entire library. :bug:`1865`\n- The Discogs release ID is now populated correctly to the discogs_albumid field\n  again (it was no longer working after Discogs changed their release URL\n  format). :bug:`4225`\n- The autotagger no longer considers all matches without a MusicBrainz ID as\n  duplicates of each other. :bug:`4299`\n- :doc:`/plugins/convert`: Resize album art when embedding :bug:`2116`\n- :doc:`/plugins/deezer`: Fix auto tagger pagination issues (fetch beyond the\n  first 25 tracks of a release).\n- :doc:`/plugins/spotify`: Fix auto tagger pagination issues (fetch beyond the\n  first 50 tracks of a release).\n- :doc:`/plugins/lyrics`: Fix Genius search by using query params instead of\n  body.\n- :doc:`/plugins/unimported`: The new ``ignore_subdirectories`` configuration\n  option added in 1.6.0 now has a default value if it hasn't been set.\n- :doc:`/plugins/deezer`: Tolerate missing fields when searching for singleton\n  tracks. :bug:`4116`\n- :doc:`/plugins/replaygain`: The type of the internal ``r128_track_gain`` and\n  ``r128_album_gain`` fields was changed from integer to float to fix loss of\n  precision due to truncation. :bug:`4169`\n- Fix a regression in the previous release that caused a ``TypeError`` when\n  moving files across filesystems. :bug:`4168`\n- :doc:`/plugins/convert`: Deleting the original files during conversion no\n  longer logs output when the ``quiet`` flag is enabled.\n- :doc:`plugins/web`: Fix handling of \"query\" requests. Previously queries\n  consisting of more than one token (separated by a slash) always returned an\n  empty result.\n- :doc:`/plugins/discogs`: Skip Discogs query on insufficiently tagged files\n  (artist and album tags missing) to prevent arbitrary candidate results.\n  :bug:`4227`\n- :doc:`plugins/lyrics`: Fixed issues with the Tekstowo.pl and Genius backends\n  where some non-lyrics content got included in the lyrics\n- :doc:`plugins/limit`: Better header formatting to improve index\n- :doc:`plugins/replaygain`: Correctly handle the ``overwrite`` config option,\n  which forces recomputing ReplayGain values on import even for tracks that\n  already have the tags.\n- :doc:`plugins/embedart`: Fix a crash when using recent versions of ImageMagick\n  and the ``compare_threshold`` option. :bug:`4272`\n- :doc:`plugins/lyrics`: Fixed issue with Genius header being included in\n  lyrics, added test case of up-to-date Genius html\n- :doc:`plugins/importadded`: Fix a bug with recently added reflink import\n  option that causes a crash when ImportAdded plugin enabled. :bug:`4389`\n- :doc:`plugins/convert`: Fix a bug with the ``wma`` format alias.\n- :doc:`/plugins/web`: Fix get file from item.\n- :doc:`/plugins/lastgenre`: Fix a duplicated entry for trip hop in the default\n  genre list. :bug:`4510`\n- :doc:`plugins/lyrics`: Fixed issue with Tekstowo backend not actually checking\n  if the found song matches. :bug:`4406`\n- :doc:`plugins/embedart`: Add support for ImageMagick 7.1.1-12 :bug:`4836`\n- :doc:`/plugins/fromfilename`: Fix failed detection of <track> <title> filename\n  patterns. :bug:`4561` :bug:`4600`\n- Fix issue where deletion of flexible fields on an album doesn't cascade to\n  items :bug:`4662`\n- Fix issue where ``beet write`` continuously retags the ``albumtypes`` metadata\n  field in files. Additionally broken data could have been added to the library\n  when the tag was read from file back into the library using ``beet update``.\n  It is required for all users to **check if such broken data is present in the\n  library**. Following the instructions `described here\n  <https://github.com/beetbox/beets/pull/4582#issuecomment-1445023493>`_, a\n  sanity check and potential fix is easily possible. :bug:`4528`\n- Fix updating \"data_source\" on re-imports and improve logging when flexible\n  attributes are being re-imported. :bug:`4726`\n- :doc:`/plugins/fetchart`: Correctly select the cover art from fanart.tv with\n  the highest number of likes\n- :doc:`/plugins/lyrics`: Fix a crash with the Google backend when processing\n  some web pages. :bug:`4875`\n- Modifying flexible attributes of albums now cascade to the individual album\n  tracks, similar to how fixed album attributes have been cascading to tracks\n  already. A new option ``--noinherit/-I`` to :ref:`modify <modify-cmd>` allows\n  changing this behaviour. :bug:`4822`\n- Fix bug where an interrupted import process poisons the database, causing a\n  null path that can't be removed. :bug:`4906`\n- :doc:`/plugins/discogs`: Fix bug where empty artist and title fields would\n  return None instead of an empty list. :bug:`4973`\n- Fix bug regarding displaying tracks that have been changed not being displayed\n  unless the detail configuration is enabled.\n- :doc:`/plugins/web`: Fix range request support, allowing to play large audio/\n  opus files using e.g. a browser/firefox or gstreamer/mopidy directly.\n- Fix bug where ``zsh`` completion script made assumptions about the specific\n  variant of ``awk`` installed and required specific settings for ``sqlite3``\n  and caching in ``zsh``. :bug:`3546`\n- Remove unused functions :bug:`5103`\n- Fix bug where all media types are reported as the first media type when\n  importing with MusicBrainz as the data source :bug:`4947`\n- Fix bug where unimported plugin would not ignore children directories of\n  ignored directories. :bug:`5130`\n- Fix bug where some plugin commands hang indefinitely due to a missing\n  ``requests`` timeout.\n- Fix cover art resizing logic to support multiple steps of resizing :bug:`5151`\n- :doc:`/plugins/convert`: Fix attempt to convert and perform side-effects if\n  library file is not readable.\n\nFor plugin developers\n~~~~~~~~~~~~~~~~~~~~~\n\n- beets now explicitly prevents multiple plugins to define replacement functions\n  for the same field. When previously defining ``template_fields`` for the same\n  field in two plugins, the last loaded plugin would silently overwrite the\n  function defined by the other plugin. Now, beets will raise an exception when\n  this happens. :bug:`5002`\n- Allow reuse of some parts of beets' testing components. This may ease the work\n  for externally developed plugins or related software (e.g. the beets plugin\n  for Mopidy), if they need to create an in-memory instance of a beets music\n  library for their tests.\n\nFor packagers\n~~~~~~~~~~~~~\n\n- As noted above, the minimum Python version is now 3.7.\n- We fixed a version for the dependency on the Confuse_ library. :bug:`4167`\n- The minimum required version of :pypi:`mediafile` is now 0.9.0.\n\nOther changes\n~~~~~~~~~~~~~\n\n- Add ``sphinx`` and ``sphinx_rtd_theme`` as dependencies for a new ``docs``\n  extra :bug:`4643`\n- :doc:`/plugins/absubmit`: Deprecate the ``absubmit`` plugin since\n  AcousticBrainz has stopped accepting new submissions. :bug:`4627`\n- :doc:`/plugins/acousticbrainz`: Deprecate the ``acousticbrainz`` plugin since\n  the AcousticBrainz project has shut down. :bug:`4627`\n- :doc:`/plugins/limit`: Limit query results to head or tail (``lslimit``\n  command only)\n- :doc:`/plugins/fish`: Add ``--output`` option.\n- :doc:`/plugins/lyrics`: Remove Musixmatch from default enabled sources as they\n  are currently blocking requests from the beets user agent. :bug:`4585`\n- :doc:`/faq`: :ref:`multidisc`: Elaborated the multi-disc FAQ :bug:`4806`\n- :doc:`/faq`: :ref:`src`: Removed some long lines.\n- Refactor the test cases to avoid test smells.\n\n1.6.0 (November 27, 2021)\n-------------------------\n\nThis release is our first experiment with time-based releases! We are aiming to\npublish a new release of beets every 3 months. We therefore have a healthy but\nnot dizzyingly long list of new features and fixes.\n\nWith this release, beets now requires Python 3.6 or later (it removes support\nfor Python 2.7, 3.4, and 3.5). There are also a few other dependency\nchanges---if you're a maintainer of a beets package for a package manager, thank\nyou for your ongoing efforts, and please see the list of notes below.\n\nMajor new features\n~~~~~~~~~~~~~~~~~~\n\n- When fetching genres from MusicBrainz, we now include genres from the release\n  group (in addition to the release). We also prioritize genres based on the\n  number of votes. Thanks to :user:`aereaux`.\n- Primary and secondary release types from MusicBrainz are now stored in a new\n  ``albumtypes`` field. Thanks to :user:`edgars-supe`. :bug:`2200`\n- An accompanying new :doc:`/plugins/albumtypes` includes some options for\n  formatting this new ``albumtypes`` field. Thanks to :user:`edgars-supe`.\n- The :ref:`modify-cmd` and :ref:`import-cmd` can now use\n  :doc:`/reference/pathformat` formats when setting fields. For example, you can\n  now do ``beet modify title='$track $title'`` to put track numbers into songs'\n  titles. :bug:`488`\n\nOther new things\n~~~~~~~~~~~~~~~~\n\n- :doc:`/plugins/permissions`: The plugin now sets cover art permissions to\n  match the audio file permissions.\n- :doc:`/plugins/unimported`: A new configuration option supports excluding\n  specific subdirectories in library.\n- :doc:`/plugins/info`: Add support for an ``--album`` flag.\n- :doc:`/plugins/export`: Similarly add support for an ``--album`` flag.\n- ``beet move`` now highlights path differences in color (when enabled).\n- When moving files and a direct rename of a file is not possible (for example,\n  when crossing filesystems), beets now copies to a temporary file in the target\n  folder first and then moves to the destination instead of directly copying the\n  target path. This gets us closer to always updating files atomically. Thanks\n  to :user:`catap`. :bug:`4060`\n- :doc:`/plugins/fetchart`: Add a new option to store cover art as\n  non-progressive image. This is useful for DAPs that do not support progressive\n  images. Set ``deinterlace: yes`` in your configuration to enable this\n  conversion.\n- :doc:`/plugins/fetchart`: Add a new option to change the file format of cover\n  art images. This may also be useful for DAPs that only support some image\n  formats.\n- Support flexible attributes in ``%aunique``. :bug:`2678` :bug:`3553`\n- Make ``%aunique`` faster, especially when using inline fields. :bug:`4145`\n\nBug fixes\n~~~~~~~~~\n\n- :doc:`/plugins/lyrics`: Fix a crash when Beautiful Soup is not installed.\n  :bug:`4027`\n- :doc:`/plugins/discogs`: Support a new Discogs URL format for IDs. :bug:`4080`\n- :doc:`/plugins/discogs`: Remove built-in rate-limiting because the Discogs\n  Python library we use now has its own rate-limiting. :bug:`4108`\n- :doc:`/plugins/export`: Fix some duplicated output.\n- :doc:`/plugins/aura`: Fix a potential security hole when serving image files.\n  :bug:`4160`\n\nFor plugin developers\n~~~~~~~~~~~~~~~~~~~~~\n\n- :py:meth:`beets.library.Item.destination` now accepts a ``replacements``\n  argument to be used in favor of the default.\n- The ``pluginload`` event is now sent after plugin types and queries are\n  available, not before.\n- A new plugin event, ``album_removed``, is called when an album is removed from\n  the library (even when its file is not deleted from disk).\n\nHere are some notes for packagers\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- As noted above, the minimum Python version is now 3.6.\n- We fixed a flaky test, named ``test_album_art`` in the ``test_zero.py`` file,\n  that some distributions had disabled. Disabling this test should no longer be\n  necessary. :bug:`4037` :bug:`4038`\n- This version of beets no longer depends on the six_ library. :bug:`4030`\n- The ``gmusic`` plugin was removed since Google Play Music has been shut down.\n  Thus, the optional dependency on ``gmusicapi`` does not exist anymore.\n  :bug:`4089`\n\n1.5.0 (August 19, 2021)\n-----------------------\n\nThis long overdue release of beets includes far too many exciting and useful\nfeatures than could ever be satisfactorily enumerated. As a technical detail, it\nalso introduces two new external libraries: MediaFile_ and Confuse_ used to be\npart of beets but are now reusable dependencies---packagers, please take note.\nFinally, this is the last version of beets where we intend to support Python 2.x\nand 3.5; future releases will soon require Python 3.6.\n\nOne non-technical change is that we moved our official ``#beets`` home on IRC\nfrom freenode to Libera.Chat_.\n\n.. _libera.chat: https://libera.chat/\n\nMajor new features\n~~~~~~~~~~~~~~~~~~\n\n- Fields in queries now fall back to an item's album and check its fields too.\n  Notably, this allows querying items by an album's attribute: in other words,\n  ``beet list foo:bar`` will not only find tracks with the ``foo`` attribute; it\n  will also find tracks *on albums* that have the ``foo`` attribute. This may be\n  particularly useful in the :ref:`path-format-config`, which matches individual\n  items to decide which path to use. Thanks to :user:`FichteFoll`. :bug:`2797`\n  :bug:`2988`\n- A new :ref:`reflink` config option instructs the importer to create fast,\n  copy-on-write file clones on filesystems that support them. Thanks to\n  :user:`rubdos`.\n- A new :doc:`/plugins/unimported` lets you find untracked files in your library\n  directory.\n- The :doc:`/plugins/aura` has arrived! Try out the future of remote music\n  library access today.\n- We now fetch information about works_ from MusicBrainz. MusicBrainz matches\n  provide the fields ``work`` (the title), ``mb_workid`` (the MBID), and\n  ``work_disambig`` (the disambiguation string). Thanks to :user:`dosoe`.\n  :bug:`2580` :bug:`3272`\n- A new :doc:`/plugins/parentwork` gets information about the original work,\n  which is useful for classical music. Thanks to :user:`dosoe`. :bug:`2580`\n  :bug:`3279`\n- :doc:`/plugins/bpd`: BPD now supports most of the features of version 0.16 of\n  the MPD protocol. This is enough to get it talking to more complicated clients\n  like ncmpcpp, but there are still some incompatibilities, largely due to MPD\n  commands we don't support yet. (Let us know if you find an MPD client that\n  doesn't get along with BPD!) :bug:`3214` :bug:`800`\n- A new :doc:`/plugins/deezer` can autotag tracks and albums using the Deezer_\n  database. Thanks to :user:`rhlahuja`. :bug:`3355`\n- A new :doc:`/plugins/bareasc` provides a new query type: \"bare ASCII\" queries\n  that ignore accented characters, treating them as though they were plain ASCII\n  characters. Use the ``#`` prefix with :ref:`list-cmd` or other commands.\n  :bug:`3882`\n- :doc:`/plugins/fetchart`: The plugin can now get album art from last.fm_.\n  :bug:`3530`\n- :doc:`/plugins/web`: The API now supports the HTTP ``DELETE`` and ``PATCH``\n  methods for modifying items. They are disabled by default; set ``readonly:\n  no`` in your configuration file to enable modification via the API.\n  :bug:`3870`\n\nOther new things\n~~~~~~~~~~~~~~~~\n\n- ``beet remove`` now also allows interactive selection of items from the query,\n  similar to ``beet modify``.\n- Enable HTTPS for MusicBrainz by default and add configuration option\n  :conf:`plugins.musicbrainz:https` for custom servers. See\n  :ref:`musicbrainz-config` for more details.\n- :doc:`/plugins/mpdstats`: Add a new ``strip_path`` option to help build the\n  right local path from MPD information.\n- :doc:`/plugins/convert`: Conversion can now parallelize conversion jobs on\n  Python 3.\n- :doc:`/plugins/lastgenre`: Add a new ``title_case`` config option to make\n  title-case formatting optional.\n- There's a new message when running ``beet config`` when there's no available\n  configuration file. :bug:`3779`\n- When importing a duplicate album, the prompt now says \"keep all\" instead of\n  \"keep both\" to reflect that there may be more than two albums involved.\n  :bug:`3569`\n- :doc:`/plugins/chroma`: The plugin now updates file metadata after generating\n  fingerprints through the ``submit`` command.\n- :doc:`/plugins/lastgenre`: Added more heavy metal genres to the built-in genre\n  filter lists.\n- A new :doc:`/plugins/subsonicplaylist` can import playlists from a Subsonic\n  server.\n- :doc:`/plugins/subsonicupdate`: The plugin now automatically chooses between\n  token- and password-based authentication based on the server version.\n- A new :conf:`plugins.musicbrainz:extra_tags` configuration option lets you use\n  more metadata in MusicBrainz queries to further narrow the search.\n- A new :doc:`/plugins/fish` adds `Fish shell`_ tab autocompletion to beets.\n- :doc:`plugins/fetchart` and :doc:`plugins/embedart`: Added a new ``quality``\n  option that controls the quality of the image output when the image is\n  resized.\n- :doc:`plugins/keyfinder`: Added support for keyfinder-cli_. Thanks to\n  :user:`BrainDamage`.\n- :doc:`plugins/fetchart`: Added a new ``high_resolution`` config option to\n  allow downloading of higher resolution iTunes artwork (at the expense of file\n  size). :bug:`3391`\n- :doc:`plugins/discogs`: The plugin applies two new fields: ``discogs_labelid``\n  and ``discogs_artistid``. :bug:`3413`\n- :doc:`/plugins/export`: Added a new ``-f`` (``--format``) flag, which can\n  export your data as JSON, JSON lines, CSV, or XML. Thanks to :user:`austinmm`.\n  :bug:`3402`\n- :doc:`/plugins/convert`: Added a new ``-l`` (``--link``) flag and ``link``\n  option as well as the ``-H`` (``--hardlink``) flag and ``hardlink`` option,\n  which symlink or hardlink files that do not need to be converted (instead of\n  copying them). :bug:`2324`\n- :doc:`/plugins/replaygain`: The plugin now supports a ``per_disc`` option that\n  enables calculation of album ReplayGain on disc level instead of album level.\n  Thanks to :user:`samuelnilsson`. :bug:`293`\n- :doc:`/plugins/replaygain`: The new ``ffmpeg`` ReplayGain backend supports\n  ``R128_`` tags. :bug:`3056`\n- :doc:`plugins/replaygain`: A new ``r128_targetlevel`` configuration option\n  defines the reference volume for files using ``R128_`` tags. ``targetlevel``\n  only configures the reference volume for ``REPLAYGAIN_`` files. :bug:`3065`\n- :doc:`/plugins/discogs`: The plugin now collects the \"style\" field. Thanks to\n  :user:`thedevilisinthedetails`. :bug:`2579` :bug:`3251`\n- :doc:`/plugins/absubmit`: By default, the plugin now avoids re-analyzing files\n  that already have AcousticBrainz data. There are new ``force`` and ``pretend``\n  options to help control this new behavior. Thanks to :user:`SusannaMaria`.\n  :bug:`3318`\n- :doc:`/plugins/discogs`: The plugin now also gets genre information and a new\n  ``discogs_albumid`` field from the Discogs API. Thanks to\n  :user:`thedevilisinthedetails`. :bug:`465` :bug:`3322`\n- :doc:`/plugins/acousticbrainz`: The plugin now fetches two more additional\n  fields: ``moods_mirex`` and ``timbre``. Thanks to :user:`malcops`. :bug:`2860`\n- :doc:`/plugins/playlist` and :doc:`/plugins/smartplaylist`: A new\n  ``forward_slash`` config option facilitates compatibility with MPD on Windows.\n  Thanks to :user:`MartyLake`. :bug:`3331` :bug:`3334`\n- The ``data_source`` field, which indicates which metadata source was used\n  during an autotagging import, is now also applied as an album-level flexible\n  attribute. :bug:`3350` :bug:`1693`\n- :doc:`/plugins/beatport`: The plugin now gets the musical key, BPM, and genre\n  for each track. :bug:`2080`\n- A new :doc:`/plugins/bpsync` can synchronize metadata changes from the\n  Beatport database (like the existing :doc:`/plugins/mbsync` for MusicBrainz).\n- :doc:`/plugins/hook`: The plugin now treats non-zero exit codes as errors.\n  :bug:`3409`\n- :doc:`/plugins/subsonicupdate`: A new ``url`` configuration replaces the older\n  (and now deprecated) separate ``host``, ``port``, and ``contextpath`` config\n  options. As a consequence, the plugin can now talk to Subsonic over HTTPS.\n  Thanks to :user:`jef`. :bug:`3449`\n- :doc:`/plugins/discogs`: The new :conf:`plugins.discogs:index_tracks` option\n  enables incorporation of work names and intra-work divisions into imported\n  track titles. Thanks to :user:`cole-miller`. :bug:`3459`\n- :doc:`/plugins/web`: The query API now interprets backslashes as path\n  separators to support path queries. Thanks to :user:`nmeum`. :bug:`3567`\n- ``beet import`` now handles tar archives with bzip2 or gzip compression.\n  :bug:`3606`\n- ``beet import`` *also* now handles 7z archives, via the py7zr_ library. Thanks\n  to :user:`arogl`. :bug:`3906`\n- :doc:`/plugins/plexupdate`: Added an option to use a secure connection to Plex\n  server, and to ignore certificate validation errors if necessary. :bug:`2871`\n- :doc:`/plugins/convert`: A new ``delete_originals`` configuration option can\n  delete the source files after conversion during import. Thanks to\n  :user:`logan-arens`. :bug:`2947`\n- There is a new ``--plugins`` (or ``-p``) CLI flag to specify a list of plugins\n  to load.\n- A new :conf:`plugins.musicbrainz:genres` option fetches genre information from\n  MusicBrainz. This functionality depends on functionality that is currently\n  unreleased in the python-musicbrainzngs_ library: see PR `#266\n  <https://github.com/alastair/python-musicbrainzngs/pull/266>`_. Thanks to\n  :user:`aereaux`.\n- :doc:`/plugins/replaygain`: Analysis now happens in parallel using the\n  ``command`` and ``ffmpeg`` backends. :bug:`3478`\n- :doc:`plugins/replaygain`: The bs1770gain backend is removed. Thanks to\n  :user:`SamuelCook`.\n- Added ``trackdisambig`` which stores the recording disambiguation from\n  MusicBrainz for each track. :bug:`1904`\n- :doc:`plugins/fetchart`: The new ``max_filesize`` configuration sets a maximum\n  target image file size.\n- :doc:`/plugins/badfiles`: Checkers can now run during import with the\n  ``check_on_import`` config option.\n- :doc:`/plugins/export`: The plugin is now much faster when using the\n  ``--include-keys`` option is used. Thanks to :user:`ssssam`.\n- The importer's :ref:`set_fields` option now saves all updated fields to\n  on-disk metadata. :bug:`3925` :bug:`3927`\n- We now fetch ISRC identifiers from MusicBrainz. Thanks to :user:`aereaux`.\n- :doc:`/plugins/metasync`: The plugin now also fetches the \"Date Added\" field\n  from iTunes databases and stores it in the ``itunes_dateadded`` field. Thanks\n  to :user:`sandersantema`.\n- :doc:`/plugins/lyrics`: Added a new Tekstowo.pl lyrics provider. Thanks to\n  various people for the implementation and for reporting issues with the\n  initial version. :bug:`3344` :bug:`3904` :bug:`3905` :bug:`3994`\n- ``beet update`` will now confirm that the user still wants to update if their\n  library folder cannot be found, preventing the user from accidentally wiping\n  out their beets database. Thanks to user: ``logan-arens``. :bug:`1934`\n\nFixes\n~~~~~\n\n- Adapt to breaking changes in Python's ``ast`` module in Python 3.8.\n- :doc:`/plugins/beatport`: Fix the assignment of the ``genre`` field, and\n  rename ``musical_key`` to ``initial_key``. :bug:`3387`\n- :doc:`/plugins/lyrics`: Fixed the Musixmatch backend for lyrics pages when\n  lyrics are divided into multiple elements on the webpage, and when the lyrics\n  are missing.\n- :doc:`/plugins/web`: Allow use of the backslash character in regex queries.\n  :bug:`3867`\n- :doc:`/plugins/web`: Fixed a small bug that caused the album art path to be\n  redacted even when ``include_paths`` option is set. :bug:`3866`\n- :doc:`/plugins/discogs`: Fixed a bug with the\n  :conf:`plugins.discogs:index_tracks` option that sometimes caused the index to\n  be discarded. Also, remove the extra semicolon that was added when there is no\n  index track.\n- :doc:`/plugins/subsonicupdate`: The API client was using the ``POST`` method\n  rather the ``GET`` method. Also includes better exception handling, response\n  parsing, and tests.\n- :doc:`/plugins/the`: Fixed incorrect regex for \"the\" that matched any 3-letter\n  combination of the letters t, h, e. :bug:`3701`\n- :doc:`/plugins/fetchart`: Fixed a bug that caused the plugin to not take\n  environment variables, such as proxy servers, into account when making\n  requests. :bug:`3450`\n- :doc:`/plugins/fetchart`: Temporary files for fetched album art that fail\n  validation are now removed.\n- :doc:`/plugins/inline`: In function-style field definitions that refer to\n  flexible attributes, values could stick around from one function invocation to\n  the next. This meant that, when displaying a list of objects, later objects\n  could seem to reuse values from earlier objects when they were missing a value\n  for a given field. These values are now properly undefined. :bug:`2406`\n- :doc:`/plugins/bpd`: Seeking by fractions of a second now works as intended,\n  fixing crashes in MPD clients like mpDris2 on seek. The ``playlistid`` command\n  now works properly in its zero-argument form. :bug:`3214`\n- :doc:`/plugins/replaygain`: Fix a Python 3 incompatibility in the Python Audio\n  Tools backend. :bug:`3305`\n- :doc:`/plugins/importadded`: Fixed a crash that occurred when the\n  ``after_write`` signal was emitted. :bug:`3301`\n- :doc:`plugins/replaygain`: Fix the storage format for R128 gain tags.\n  :bug:`3311` :bug:`3314`\n- :doc:`/plugins/discogs`: Fixed a crash that occurred when the master URI isn't\n  set in the API response. :bug:`2965` :bug:`3239`\n- :doc:`/plugins/spotify`: Fix handling of year-only release dates returned by\n  the Spotify albums API. Thanks to :user:`rhlahuja`. :bug:`3343`\n- Fixed a bug that caused the UI to display incorrect track numbers for tracks\n  with index 0 when the ``per_disc_numbering`` option was set. :bug:`3346`\n- ``none_rec_action`` does not import automatically when ``timid`` is enabled.\n  Thanks to :user:`RollingStar`. :bug:`3242`\n- Fix a bug that caused a crash when tagging items with the beatport plugin.\n  :bug:`3374`\n- ``beet import`` now logs which files are ignored when in debug mode.\n  :bug:`3764`\n- :doc:`/plugins/bpd`: Fix the transition to next track when in consume mode.\n  Thanks to :user:`aereaux`. :bug:`3437`\n- :doc:`/plugins/lyrics`: Fix a corner-case with Genius lowercase artist names\n  :bug:`3446`\n- :doc:`/plugins/parentwork`: Don't save tracks when nothing has changed.\n  :bug:`3492`\n- Added a warning when configuration files defined in the ``include`` directive\n  of the configuration file fail to be imported. :bug:`3498`\n- Added normalization to integer values in the database, which should avoid\n  problems where fields like ``bpm`` would sometimes store non-integer values.\n  :bug:`762` :bug:`3507` :bug:`3508`\n- Fix a crash when querying for null values. :bug:`3516` :bug:`3517`\n- :doc:`/plugins/lyrics`: Tolerate a missing lyrics div in the Genius scraper.\n  Thanks to :user:`thejli21`. :bug:`3535` :bug:`3554`\n- :doc:`/plugins/lyrics`: Use the artist sort name to search for lyrics, which\n  can help find matches when the artist name has special characters. Thanks to\n  :user:`hashhar`. :bug:`3340` :bug:`3558`\n- :doc:`/plugins/replaygain`: Trying to calculate volume gain for an album\n  consisting of some formats using ``ReplayGain`` and some using ``R128`` will\n  no longer crash; instead it is skipped and and a message is logged. The log\n  message has also been rewritten for to improve clarity. Thanks to\n  :user:`autrimpo`. :bug:`3533`\n- :doc:`/plugins/lyrics`: Adapt the Genius backend to changes in markup to\n  reduce the scraping failure rate. :bug:`3535` :bug:`3594`\n- :doc:`/plugins/lyrics`: Fix a crash when writing ReST files for a query\n  without results or fetched lyrics. :bug:`2805`\n- :doc:`/plugins/fetchart`: Attempt to fetch pre-resized thumbnails from Cover\n  Art Archive if the ``maxwidth`` option matches one of the sizes supported by\n  the Cover Art Archive API. Thanks to :user:`trolley`. :bug:`3637`\n- :doc:`/plugins/ipfs`: Fix Python 3 compatibility. Thanks to :user:`musoke`.\n  :bug:`2554`\n- Fix a bug that caused metadata starting with something resembling a drive\n  letter to be incorrectly split into an extra directory after the colon.\n  :bug:`3685`\n- :doc:`/plugins/mpdstats`: Don't record a skip when stopping MPD, as MPD keeps\n  the current track in the queue. Thanks to :user:`aereaux`. :bug:`3722`\n- String-typed fields are now normalized to string values, avoiding an\n  occasional crash when using both the :doc:`/plugins/fetchart` and the\n  :doc:`/plugins/discogs` together. :bug:`3773` :bug:`3774`\n- Fix a bug causing PIL to generate poor quality JPEGs when resizing artwork.\n  :bug:`3743`\n- :doc:`plugins/keyfinder`: Catch output from ``keyfinder-cli`` that is missing\n  key. :bug:`2242`\n- :doc:`plugins/replaygain`: Disable parallel analysis on import by default.\n  :bug:`3819`\n- :doc:`/plugins/mpdstats`: Fix Python 2/3 compatibility :bug:`3798`\n- :doc:`/plugins/discogs`: Replace the deprecated official ``discogs-client``\n  library with the community supported python3-discogs-client_ library.\n  :bug:`3608`\n- :doc:`/plugins/chroma`: Fixed submitting AcoustID information for tracks that\n  already have a fingerprint. :bug:`3834`\n- Allow equals within the value part of the ``--set`` option to the ``beet\n  import`` command. :bug:`2984`\n- Duplicates can now generate checksums. Thanks :user:`wisp3rwind` for the\n  pointer to how to solve. Thanks to :user:`arogl`. :bug:`2873`\n- Templates that use ``%ifdef`` now produce the expected behavior when used in\n  conjunction with non-string fields from the :doc:`/plugins/types`. :bug:`3852`\n- :doc:`/plugins/lyrics`: Fix crashes when a website could not be retrieved,\n  affecting at least the Genius source. :bug:`3970`\n- :doc:`/plugins/duplicates`: Fix a crash when running the ``dup`` command with\n  a query that returns no results. :bug:`3943`\n- :doc:`/plugins/beatport`: Fix the default assignment of the musical key.\n  :bug:`3377`\n- :doc:`/plugins/lyrics`: Improved searching on the Genius backend when the\n  artist contains special characters. :bug:`3634`\n- :doc:`/plugins/parentwork`: Also get the composition date of the parent work,\n  instead of just the child work. Thanks to :user:`aereaux`. :bug:`3650`\n- :doc:`/plugins/lyrics`: Fix a bug in the heuristic for detecting valid lyrics\n  in the Google source. :bug:`2969`\n- :doc:`/plugins/thumbnails`: Fix a crash due to an incorrect string type on\n  Python 3. :bug:`3360`\n- :doc:`/plugins/fetchart`: The Cover Art Archive source now iterates over all\n  front images instead of blindly selecting the first one.\n- :doc:`/plugins/lyrics`: Removed the LyricWiki source (the site shut down on\n  21/09/2020).\n- :doc:`/plugins/subsonicupdate`: The plugin is now functional again. A new\n  ``auth`` configuration option is required in the configuration to specify the\n  flavor of authentication to use. :bug:`4002`\n\nFor plugin developers\n~~~~~~~~~~~~~~~~~~~~~\n\n- MediaFile_ has been split into a standalone project. Where you used to do\n  ``from beets import mediafile``, now just do ``import mediafile``. Beets\n  re-exports MediaFile at the old location for backwards-compatibility, but a\n  deprecation warning is raised if you do this since we might drop this wrapper\n  in a future release.\n- Similarly, we've replaced beets' configuration library (previously called\n  Confit) with a standalone version called Confuse_. Where you used to do ``from\n  beets.util import confit``, now just do ``import confuse``. The code is almost\n  identical apart from the name change. Again, we'll re-export at the old\n  location (with a deprecation warning) for backwards compatibility, but we\n  might stop doing this in a future release.\n- ``beets.util.command_output`` now returns a named tuple containing both the\n  standard output and the standard error data instead of just stdout alone.\n  Client code will need to access the ``stdout`` attribute on the return value.\n  Thanks to :user:`zsinskri`. :bug:`3329`\n- There were sporadic failures in ``test.test_player``. Hopefully these are\n  fixed. If they resurface, please reopen the relevant issue. :bug:`3309`\n  :bug:`3330`\n- The ``beets.plugins.MetadataSourcePlugin`` base class has been added to\n  simplify development of plugins which query album, track, and search APIs to\n  provide metadata matches for the importer. Refer to the\n  :doc:`/plugins/spotify` and the :doc:`/plugins/deezer` for examples of using\n  this template class. :bug:`3355`\n- Accessing fields on an ``Item`` now falls back to the album's attributes. So,\n  for example, ``item.foo`` will first look for a field ``foo`` on ``item`` and,\n  if it doesn't exist, next tries looking for a field named ``foo`` on the album\n  that contains ``item``. If you specifically want to access an item's\n  attributes, use ``Item.get(key, with_album=False)``. :bug:`2988`\n- ``Item.keys`` also has a ``with_album`` argument now, defaulting to ``True``.\n- A ``revision`` attribute has been added to ``Database``. It is increased on\n  every transaction that mutates it. :bug:`2988`\n- The classes ``AlbumInfo`` and ``TrackInfo`` now convey arbitrary attributes\n  instead of a fixed, built-in set of field names (which was important to\n  address :bug:`1547`). Thanks to :user:`dosoe`.\n- Two new events, ``mb_album_extract`` and ``mb_track_extract``, let plugins add\n  new fields based on MusicBrainz data. Thanks to :user:`dosoe`.\n\nFor packagers\n~~~~~~~~~~~~~\n\n- Beets' library for manipulating media file metadata has now been split to a\n  standalone project called MediaFile_, released as :pypi:`mediafile`. Beets now\n  depends on this new package. Beets now depends on Mutagen transitively through\n  MediaFile rather than directly, except in the case of one of beets' plugins\n  (in particular, the :doc:`/plugins/scrub`).\n- Beets' library for configuration has been split into a standalone project\n  called Confuse_, released as :pypi:`confuse`. Beets now depends on this\n  package. Confuse has existed separately for some time and is used by unrelated\n  projects, but until now we've been bundling a copy within beets.\n- We attempted to fix an unreliable test, so a patch to skip-broken-test_ or\n  repairing_ may no longer be necessary.\n- This version drops support for Python 3.4.\n- We have removed an optional dependency on bs1770gain.\n\n.. _confuse: https://github.com/beetbox/confuse\n\n.. _deezer: https://www.deezer.com/en/\n\n.. _fish shell: https://fishshell.com/\n\n.. _keyfinder-cli: https://github.com/EvanPurkhiser/keyfinder-cli\n\n.. _last.fm: https://www.last.fm/\n\n.. _mediafile: https://github.com/beetbox/mediafile\n\n.. _py7zr: https://pypi.org/project/py7zr/\n\n.. _python3-discogs-client: https://github.com/joalla/discogs_client\n\n.. _repairing: https://build.opensuse.org/package/view_file/openSUSE:Factory/beets/fix_test_command_line_option_relative_to_working_dir.diff?expand=1\n\n.. _skip-broken-test: https://sources.debian.org/src/beets/1.4.7-2/debian/patches/skip-broken-test/\n\n.. _works: https://musicbrainz.org/doc/Work\n\n1.4.9 (May 30, 2019)\n--------------------\n\nThis small update is part of our attempt to release new versions more often!\nThere are a few important fixes, and we're clearing the deck for a change to\nbeets' dependencies in the next version.\n\nThe new feature is\n~~~~~~~~~~~~~~~~~~\n\n- You can use the NO_COLOR_ environment variable to disable terminal colors.\n  :bug:`3273`\n\nThere are some fixes in this release\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- Fix a regression in the last release that made the image resizer fail to\n  detect older versions of ImageMagick. :bug:`3269`\n- ``/plugins/gmusic``: The ``oauth_file`` config option now supports more\n  flexible path values, including ``~`` for the home directory. :bug:`3270`\n- ``/plugins/gmusic``: Fix a crash when using version 12.0.0 or later of the\n  ``gmusicapi`` module. :bug:`3270`\n- Fix an incompatibility with Python 3.8's AST changes. :bug:`3278`\n\nHere's a note for packagers\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- ``pathlib`` is now an optional test dependency on Python 3.4+, removing the\n  need for `Debian pathlib patch`_ :bug:`3275`\n\n.. _debian pathlib patch: https://sources.debian.org/src/beets/1.4.7-2/debian/patches/pathlib-is-stdlib/\n\n.. _no_color: https://no-color.org\n\n1.4.8 (May 16, 2019)\n--------------------\n\nThis release is far too long in coming, but it's a good one. There is the usual\ntorrent of new features and a ridiculously long line of fixes, but there are\nalso some crucial maintenance changes. We officially support Python 3.7 and 3.8,\nand some performance optimizations can (anecdotally) make listing your library\nmore than three times faster than in the previous version.\n\nThe new core features are\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- A new :ref:`config-aunique` configuration option allows setting default\n  options for the :ref:`aunique` template function.\n- The ``albumdisambig`` field no longer includes the MusicBrainz release group\n  disambiguation comment. A new ``releasegroupdisambig`` field has been added.\n  :bug:`3024`\n- The :ref:`modify-cmd` command now allows resetting fixed attributes. For\n  example, ``beet modify -a artist:beatles artpath!`` resets ``artpath``\n  attribute from matching albums back to the default value. :bug:`2497`\n- A new importer option, :ref:`ignore_data_tracks`, lets you skip audio tracks\n  contained in data files. :bug:`3021`\n\nThere are some new plugins\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- The :doc:`/plugins/playlist` can query the beets library using M3U playlists.\n  Thanks to :user:`Holzhaus` and :user:`Xenopathic`. :bug:`123` :bug:`3145`\n- The :doc:`/plugins/loadext` allows loading of SQLite extensions, primarily for\n  use with the ICU SQLite extension for internationalization. :bug:`3160`\n  :bug:`3226`\n- The :doc:`/plugins/subsonicupdate` can automatically update your Subsonic\n  library. Thanks to :user:`maffo999`. :bug:`3001`\n\nAnd many improvements to existing plugins\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- :doc:`/plugins/lastgenre`: Added option ``-A`` to match individual tracks and\n  singletons. :bug:`3220` :bug:`3219`\n- :doc:`/plugins/play`: The plugin can now emit a UTF-8 BOM, fixing some issues\n  with foobar2000 and Winamp. Thanks to :user:`mz2212`. :bug:`2944`\n- ``/plugins/gmusic``:\n\n  - Add a new option to automatically upload to Google Play Music library on\n    track import. Thanks to :user:`shuaiscott`.\n  - Add new options for Google Play Music authentication. Thanks to\n    :user:`thetarkus`. :bug:`3002`\n\n- :doc:`/plugins/replaygain`: ``albumpeak`` on large collections is calculated\n  as the average, not the maximum. :bug:`3008` :bug:`3009`\n- :doc:`/plugins/chroma`:\n\n  - Now optionally has a bias toward looking up more relevant releases according\n    to the :ref:`preferred` configuration options. Thanks to :user:`archer4499`.\n    :bug:`3017`\n  - Fingerprint values are now properly stored as strings, which prevents\n    strange repeated output when running ``beet write``. Thanks to\n    :user:`Holzhaus`. :bug:`3097` :bug:`2942`\n\n- :doc:`/plugins/convert`: The plugin now has an ``id3v23`` option that allows\n  you to override the global ``id3v23`` option. Thanks to :user:`Holzhaus`.\n  :bug:`3104`\n- :doc:`/plugins/spotify`:\n\n  - The plugin now uses OAuth for authentication to the Spotify API. Thanks to\n    :user:`rhlahuja`. :bug:`2694` :bug:`3123`\n  - The plugin now works as an import metadata provider: you can match tracks\n    and albums using the Spotify database. Thanks to :user:`rhlahuja`.\n    :bug:`3123`\n\n- :doc:`/plugins/ipfs`: The plugin now supports a ``nocopy`` option which passes\n  that flag to ipfs. Thanks to :user:`wildthyme`.\n- :doc:`/plugins/discogs`: The plugin now has rate limiting for the Discogs API.\n  :bug:`3081`\n- :doc:`/plugins/mpdstats`, :doc:`/plugins/mpdupdate`: These plugins now use the\n  ``MPD_PORT`` environment variable if no port is specified in the configuration\n  file. :bug:`3223`\n- :doc:`/plugins/bpd`:\n\n  - MPD protocol commands ``consume`` and ``single`` are now supported along\n    with updated semantics for ``repeat`` and ``previous`` and new fields for\n    ``status``. The bpd server now understands and ignores some additional\n    commands. :bug:`3200` :bug:`800`\n  - MPD protocol command ``idle`` is now supported, allowing the MPD version to\n    be bumped to 0.14. :bug:`3205` :bug:`800`\n  - MPD protocol command ``decoders`` is now supported. :bug:`3222`\n  - The plugin now uses the main beets logging system. The special-purpose\n    ``--debug`` flag has been removed. Thanks to :user:`arcresu`. :bug:`3196`\n\n- :doc:`/plugins/mbsync`: The plugin no longer queries MusicBrainz when either\n  the ``mb_albumid`` or ``mb_trackid`` field is invalid. See also the discussion\n  on `Google Groups`_ Thanks to :user:`arogl`.\n- :doc:`/plugins/export`: The plugin now also exports ``path`` field if the user\n  explicitly specifies it with ``-i`` parameter. This only works when exporting\n  library fields. :bug:`3084`\n- :doc:`/plugins/acousticbrainz`: The plugin now declares types for all its\n  fields, which enables easier querying and avoids a problem where very small\n  numbers would be stored as strings. Thanks to :user:`rain0r`. :bug:`2790`\n  :bug:`3238`\n\n.. _google groups: https://groups.google.com/forum/#!searchin/beets-users/mbsync|sort:date/beets-users/iwCF6bNdh9A/i1xl4Gx8BQAJ\n\nSome improvements have been focused on improving beets' performance\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- Querying the library is now faster:\n\n  - We only convert fields that need to be displayed. Thanks to :user:`pprkut`.\n    :bug:`3089`\n  - We now compile templates once and reuse them instead of recompiling them to\n    print out each matching object. Thanks to :user:`SimonPersson`. :bug:`3258`\n  - Querying the library for items is now faster, for all queries that do not\n    need to access album level properties. This was implemented by lazily\n    fetching the album only when needed. Thanks to :user:`SimonPersson`.\n    :bug:`3260`\n\n- :doc:`/plugins/absubmit`, :doc:`/plugins/badfiles`: Analysis now works in\n  parallel (on Python 3 only). Thanks to :user:`bemeurer`. :bug:`2442`\n  :bug:`3003`\n- :doc:`/plugins/mpdstats`: Use the ``currentsong`` MPD command instead of\n  ``playlist`` to get the current song, improving performance when the playlist\n  is long. Thanks to :user:`ray66`. :bug:`3207` :bug:`2752`\n\nSeveral improvements are related to usability\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- The disambiguation string for identifying albums in the importer now shows the\n  catalog number. Thanks to :user:`8h2a`. :bug:`2951`\n- Added whitespace padding to missing tracks dialog to improve readability.\n  Thanks to :user:`jams2`. :bug:`2962`\n- The :ref:`move-cmd` command now lists the number of items already in-place.\n  Thanks to :user:`RollingStar`. :bug:`3117`\n- Modify selection can now be applied early without selecting every item.\n  :bug:`3083`\n- Beets now emits more useful messages during startup if SQLite returns an\n  error. The SQLite error message is now attached to the beets message.\n  :bug:`3005`\n- Fixed a confusing typo when the :doc:`/plugins/convert` plugin copies the art\n  covers. :bug:`3063`\n\nMany fixes have been focused on issues where beets would previously crash\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- Avoid a crash when archive extraction fails during import. :bug:`3041`\n- Missing album art file during an update no longer causes a fatal exception\n  (instead, an error is logged and the missing file path is removed from the\n  library). :bug:`3030`\n- When updating the database, beets no longer tries to move album art twice.\n  :bug:`3189`\n- Fix an unhandled exception when pruning empty directories. :bug:`1996`\n  :bug:`3209`\n- :doc:`/plugins/fetchart`: Added network connection error handling to backends\n  so that beets won't crash if a request fails. Thanks to :user:`Holzhaus`.\n  :bug:`1579`\n- :doc:`/plugins/badfiles`: Avoid a crash when the underlying tool emits\n  undecodable output. :bug:`3165`\n- :doc:`/plugins/beatport`: Avoid a crash when the server produces an error.\n  :bug:`3184`\n- :doc:`/plugins/bpd`: Fix crashes in the bpd server during exception handling.\n  :bug:`3200`\n- :doc:`/plugins/bpd`: Fix a crash triggered when certain clients tried to list\n  the albums belonging to a particular artist. :bug:`3007` :bug:`3215`\n- :doc:`/plugins/replaygain`: Avoid a crash when the ``bs1770gain`` tool emits\n  malformed XML. :bug:`2983` :bug:`3247`\n\nThere are many fixes related to compatibility with our dependencies including\naddressing changes interfaces:\n\n- On Python 2, pin the :pypi:`jellyfish` requirement to version 0.6.0 for\n  compatibility.\n- Fix compatibility with Python 3.7 and its change to a name in the :stdlib:`re`\n  module. :bug:`2978`\n- Fix several uses of deprecated standard-library features on Python 3.7. Thanks\n  to :user:`arcresu`. :bug:`3197`\n- Fix compatibility with pre-release versions of Python 3.8. :bug:`3201`\n  :bug:`3202`\n- :doc:`/plugins/web`: Fix an error when using more recent versions of Flask\n  with CORS enabled. Thanks to :user:`rveachkc`. :bug:`2979`: :bug:`2980`\n- Avoid some deprecation warnings with certain versions of the MusicBrainz\n  library. Thanks to :user:`zhelezov`. :bug:`2826` :bug:`3092`\n- Restore iTunes Store album art source, and remove the dependency on\n  :pypi:`python-itunes`, which had gone unmaintained and was not\n  Python-3-compatible. Thanks to :user:`ocelma` for creating\n  :pypi:`python-itunes` in the first place. Thanks to :user:`nathdwek`.\n  :bug:`2371` :bug:`2551` :bug:`2718`\n- :doc:`/plugins/lastgenre`, :doc:`/plugins/edit`: Avoid a deprecation warnings\n  from the :pypi:`PyYAML` library by switching to the safe loader. Thanks to\n  :user:`translit` and :user:`sbraz`. :bug:`3192` :bug:`3225`\n- Fix a problem when resizing images with :pypi:`PIL`/:pypi:`pillow` on Python\n  3. Thanks to :user:`architek`. :bug:`2504` :bug:`3029`\n\nAnd there are many other fixes\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- R128 normalization tags are now properly deleted from files when the values\n  are missing. Thanks to :user:`autrimpo`. :bug:`2757`\n- Display the artist credit when matching albums if the :ref:`artist_credit`\n  configuration option is set. :bug:`2953`\n- With the :ref:`from_scratch` configuration option set, only writable fields\n  are cleared. Beets now no longer ignores the format your music is saved in.\n  :bug:`2972`\n- The ``%aunique`` template function now works correctly with the\n  ``-f/--format`` option. :bug:`3043`\n- Fixed the ordering of items when manually selecting changes while updating\n  tags Thanks to :user:`TaizoSimpson`. :bug:`3501`\n- The ``%title`` template function now works correctly with apostrophes. Thanks\n  to :user:`GuilhermeHideki`. :bug:`3033`\n- :doc:`/plugins/lastgenre`: It's now possible to set the ``prefer_specific``\n  option without also setting ``canonical``. :bug:`2973`\n- :doc:`/plugins/fetchart`: The plugin now respects the ``ignore`` and\n  ``ignore_hidden`` settings. :bug:`1632`\n- :doc:`/plugins/hook`: Fix byte string interpolation in hook commands.\n  :bug:`2967` :bug:`3167`\n- :doc:`/plugins/the`: Log a message when something has changed, not when it\n  hasn't. Thanks to :user:`arcresu`. :bug:`3195`\n- :doc:`/plugins/lastgenre`: The ``force`` config option now actually works.\n  :bug:`2704` :bug:`3054`\n- Resizing image files with ImageMagick now avoids problems on systems where\n  there is a ``convert`` command that is *not* ImageMagick's by using the\n  ``magick`` executable when it is available. Thanks to :user:`ababyduck`.\n  :bug:`2093` :bug:`3236`\n\nThere is one new thing for plugin developers to know about\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- In addition to prefix-based field queries, plugins can now define *named\n  queries* that are not associated with any specific field. For example, the new\n  :doc:`/plugins/playlist` supports queries like ``playlist:name`` although\n  there is no field named ``playlist``. See :ref:`extend-query` for details.\n\nAnd some messages for packagers\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- Note the changes to the dependencies on :pypi:`jellyfish` and :pypi:`munkres`.\n- The optional :pypi:`python-itunes` dependency has been removed.\n- Python versions 3.7 and 3.8 are now supported.\n\n1.4.7 (May 29, 2018)\n--------------------\n\nThis new release includes lots of new features in the importer and the metadata\nsource backends that it uses. We've changed how the beets importer handles\nnon-audio tracks listed in metadata sources like MusicBrainz:\n\n- The importer now ignores non-audio tracks (namely, data and video tracks)\n  listed in MusicBrainz. Also, a new option, :ref:`ignore_video_tracks`, lets\n  you return to the old behavior and include these video tracks. :bug:`1210`\n- A new importer option, :ref:`ignored_media`, can let you skip certain media\n  formats. :bug:`2688`\n\nThere are other subtle improvements to metadata handling in the importer\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- In the MusicBrainz backend, beets now imports the\n  ``musicbrainz_releasetrackid`` field. This is a first step toward :bug:`406`.\n  Thanks to :user:`Rawrmonkeys`.\n- A new importer configuration option, :ref:`artist_credit`, will tell beets to\n  prefer the artist credit over the artist when autotagging. :bug:`1249`\n\nAnd there are even more new features\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- :doc:`/plugins/replaygain`: The ``beet replaygain`` command now has\n  ``--force``, ``--write`` and ``--nowrite`` options. :bug:`2778`\n- A new importer configuration option, :ref:`incremental_skip_later`, lets you\n  avoid recording skipped directories to the list of \"processed\" directories in\n  :ref:`incremental` mode. This way, you can revisit them later with another\n  import. Thanks to :user:`sekjun9878`. :bug:`2773`\n- :doc:`/plugins/fetchart`: The configuration options now support finer-grained\n  control via the ``sources`` option. You can now specify the search order for\n  different *matching strategies* within different backends.\n- :doc:`/plugins/web`: A new ``cors_supports_credentials`` configuration option\n  lets in-browser clients communicate with the server even when it is protected\n  by an authorization mechanism (a proxy with HTTP authentication enabled, for\n  example).\n- A new :doc:`/plugins/sonosupdate` plugin automatically notifies Sonos\n  controllers to update the music library when the beets library changes. Thanks\n  to :user:`cgtobi`.\n- :doc:`/plugins/discogs`: The plugin now stores master release IDs into\n  ``mb_releasegroupid``. It also \"simulates\" track IDs using the release ID and\n  the track list position. Thanks to :user:`dbogdanov`. :bug:`2336`\n- :doc:`/plugins/discogs`: Fetch the original year from master releases.\n  :bug:`1122`\n\nThere are lots and lots of fixes\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- :doc:`/plugins/replaygain`: Fix a corner-case with the ``bs1770gain`` backend\n  where ReplayGain values were assigned to the wrong files. The plugin now\n  requires version 0.4.6 or later of the ``bs1770gain`` tool. :bug:`2777`\n- :doc:`/plugins/lyrics`: The plugin no longer crashes in the Genius source when\n  BeautifulSoup is not found. Instead, it just logs a message and disables the\n  source. :bug:`2911`\n- :doc:`/plugins/lyrics`: Handle network and API errors when communicating with\n  Genius. :bug:`2771`\n- :doc:`/plugins/lyrics`: The ``lyrics`` command previously wrote ReST files by\n  default, even when you didn't ask for them. This default has been fixed.\n- :doc:`/plugins/lyrics`: When writing ReST files, the ``lyrics`` command now\n  groups lyrics by the ``albumartist`` field, rather than ``artist``.\n  :bug:`2924`\n- Plugins can now see updated import task state, such as when rejecting the\n  initial candidates and finding new ones via a manual search. Notably, this\n  means that the importer prompt options that the :doc:`/plugins/edit` provides\n  show up more reliably after doing a secondary import search. :bug:`2441`\n  :bug:`2731`\n- :doc:`/plugins/importadded`: Fix a crash on non-autotagged imports. Thanks to\n  :user:`m42i`. :bug:`2601` :bug:`1918`\n- :doc:`/plugins/plexupdate`: The Plex token is now redacted in configuration\n  output. Thanks to :user:`Kovrinic`. :bug:`2804`\n- Avoid a crash when importing a non-ASCII filename when using an ASCII locale\n  on Unix under Python 3. :bug:`2793` :bug:`2803`\n- Fix a problem caused by time zone misalignment that could make date queries\n  fail to match certain dates that are near the edges of a range. For example,\n  querying for dates within a certain month would fail to match dates within\n  hours of the end of that month. :bug:`2652`\n- :doc:`/plugins/convert`: The plugin now runs before other plugin-provided\n  import stages, which addresses an issue with generating ReplayGain data\n  incompatible between the source and target file formats. Thanks to\n  :user:`autrimpo`. :bug:`2814`\n- :doc:`/plugins/ftintitle`: The ``drop`` config option had no effect; it now\n  does what it says it should do. :bug:`2817`\n- Importing a release with multiple release events now selects the event based\n  on the order of your :ref:`preferred` countries rather than the order of\n  release events in MusicBrainz. :bug:`2816`\n- :doc:`/plugins/web`: The time display in the web interface would incorrectly\n  jump at the 30-second mark of every minute. Now, it correctly changes over at\n  zero seconds. :bug:`2822`\n- :doc:`/plugins/web`: Fetching album art now works (instead of throwing an\n  exception) under Python 3. Additionally, the server will now return a 404\n  response when the album ID is unknown (instead of throwing an exception and\n  producing a 500 response). :bug:`2823`\n- :doc:`/plugins/web`: Fix an exception on Python 3 for filenames with\n  non-Latin1 characters. (These characters are now converted to their ASCII\n  equivalents.) :bug:`2815`\n- Partially fix bash completion for subcommand names that contain hyphens.\n  Thanks to :user:`jhermann`. :bug:`2836` :bug:`2837`\n- :doc:`/plugins/replaygain`: Really fix album gain calculation using the\n  GStreamer backend. :bug:`2846`\n- Avoid an error when doing a \"no-op\" move on non-existent files (i.e., moving a\n  file onto itself). :bug:`2863`\n- :doc:`/plugins/discogs`: Fix the ``medium`` and ``medium_index`` values, which\n  were occasionally incorrect for releases with two-sided mediums such as vinyl.\n  Also fix the ``medium_total`` value, which now contains total number of tracks\n  on the medium to which a track belongs, not the total number of different\n  mediums present on the release. Thanks to :user:`dbogdanov`. :bug:`2887`\n- The importer now supports audio files contained in data tracks when they are\n  listed in MusicBrainz: the corresponding audio tracks are now merged into the\n  main track list. Thanks to :user:`jdetrey`. :bug:`1638`\n- :doc:`/plugins/keyfinder`: Avoid a crash when trying to process unmatched\n  tracks. :bug:`2537`\n- :doc:`/plugins/mbsync`: Support MusicBrainz recording ID changes, relying on\n  release track IDs instead. Thanks to :user:`jdetrey`. :bug:`1234`\n- :doc:`/plugins/mbsync`: We can now successfully update albums even when the\n  first track has a missing MusicBrainz recording ID. :bug:`2920`\n\nThere are a couple of changes for developers\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- Plugins can now run their import stages *early*, before other plugins. Use the\n  ``early_import_stages`` list instead of plain ``import_stages`` to request\n  this behavior. :bug:`2814`\n- We again properly send ``albuminfo_received`` and ``trackinfo_received`` in\n  all cases, most notably when using the ``mbsync`` plugin. This was a\n  regression since version 1.4.1. :bug:`2921`\n\n1.4.6 (December 21, 2017)\n-------------------------\n\nThe highlight of this release is \"album merging,\" an oft-requested option in the\nimporter to add new tracks to an existing album you already have in your\nlibrary. This way, you no longer need to resort to removing the partial album\nfrom your library, combining the files manually, and importing again.\n\nHere are the larger new features in this release\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- When the importer finds duplicate albums, you can now merge all the\n  tracks---old and new---together and try importing them as a single, combined\n  album. Thanks to :user:`udiboy1209`. :bug:`112` :bug:`2725`\n- :doc:`/plugins/lyrics`: The plugin can now produce reStructuredText files for\n  beautiful, readable books of lyrics. Thanks to :user:`anarcat`. :bug:`2628`\n- A new :ref:`from_scratch` configuration option makes the importer remove old\n  metadata before applying new metadata. This new feature complements the\n  :doc:`zero </plugins/zero>` and :doc:`scrub </plugins/scrub>` plugins but is\n  slightly different: beets clears out all the old tags it knows about and only\n  keeps the new data it gets from the remote metadata source. Thanks to\n  :user:`tummychow`. :bug:`934` :bug:`2755`\n\nThere are also somewhat littler, but still great, new features\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- :doc:`/plugins/convert`: A new ``no_convert`` option lets you skip transcoding\n  items matching a query. Instead, the files are just copied as-is. Thanks to\n  :user:`Stunner`. :bug:`2732` :bug:`2751`\n- :doc:`/plugins/fetchart`: A new quiet switch that only prints out messages\n  when album art is missing. Thanks to :user:`euri10`. :bug:`2683`\n- :doc:`/plugins/mbcollection`: You can configure a custom MusicBrainz\n  collection via the new ``collection`` configuration option. :bug:`2685`\n- :doc:`/plugins/mbcollection`: The collection update command can now remove\n  albums from collections that are longer in the beets library.\n- :doc:`/plugins/fetchart`: The ``clearart`` command now asks for confirmation\n  before touching your files. Thanks to :user:`konman2`. :bug:`2708` :bug:`2427`\n- :doc:`/plugins/mpdstats`: The plugin now correctly updates song statistics\n  when MPD switches from a song to a stream and when it plays the same song\n  multiple times consecutively. :bug:`2707`\n- :doc:`/plugins/acousticbrainz`: The plugin can now be configured to write only\n  a specific list of tags. Thanks to :user:`woparry`.\n\nThere are lots and lots of bug fixes\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- :doc:`/plugins/hook`: Fixed a problem where accessing non-string properties of\n  ``item`` or ``album`` (e.g., ``item.track``) would cause a crash. Thanks to\n  :user:`broddo`. :bug:`2740`\n- :doc:`/plugins/play`: When ``relative_to`` is set, the plugin correctly emits\n  relative paths even when querying for albums rather than tracks. Thanks to\n  :user:`j000`. :bug:`2702`\n- We suppress a spurious Python warning about a ``BrokenPipeError`` being\n  ignored. This was an issue when using beets in simple shell scripts. Thanks to\n  :user:`Azphreal`. :bug:`2622` :bug:`2631`\n- :doc:`/plugins/replaygain`: Fix a regression in the previous release related\n  to the new R128 tags. :bug:`2615` :bug:`2623`\n- :doc:`/plugins/lyrics`: The MusixMatch backend now detects and warns when the\n  server has blocked the client. Thanks to :user:`anarcat`. :bug:`2634`\n  :bug:`2632`\n- :doc:`/plugins/importfeeds`: Fix an error on Python 3 in certain\n  configurations. Thanks to :user:`djl`. :bug:`2467` :bug:`2658`\n- :doc:`/plugins/edit`: Fix a bug when editing items during a re-import with the\n  ``-L`` flag. Previously, diffs against against unrelated items could be shown\n  or beets could crash. :bug:`2659`\n- :doc:`/plugins/kodiupdate`: Fix the server URL and add better error reporting.\n  :bug:`2662`\n- Fixed a problem where \"no-op\" modifications would reset files' mtimes,\n  resulting in unnecessary writes. This most prominently affected the\n  :doc:`/plugins/edit` when saving the text file without making changes to some\n  music. :bug:`2667`\n- :doc:`/plugins/chroma`: Fix a crash when running the ``submit`` command on\n  Python 3 on Windows with non-ASCII filenames. :bug:`2671`\n- :doc:`/plugins/absubmit`: Fix an occasional crash on Python 3 when the AB\n  analysis tool produced non-ASCII metadata. :bug:`2673`\n- :doc:`/plugins/duplicates`: Use the default tiebreak for items or albums when\n  the configuration only specifies a tiebreak for the other kind of entity.\n  Thanks to :user:`cgevans`. :bug:`2758`\n- :doc:`/plugins/duplicates`: Fix the ``--key`` command line option, which was\n  ignored.\n- :doc:`/plugins/replaygain`: Fix album ReplayGain calculation with the\n  GStreamer backend. :bug:`2636`\n- :doc:`/plugins/scrub`: Handle errors when manipulating files using newer\n  versions of Mutagen. :bug:`2716`\n- :doc:`/plugins/fetchart`: The plugin no longer gets skipped during import when\n  the \"Edit Candidates\" option is used from the :doc:`/plugins/edit`.\n  :bug:`2734`\n- Fix a crash when numeric metadata fields contain just a minus or plus sign\n  with no following numbers. Thanks to :user:`eigengrau`. :bug:`2741`\n- :doc:`/plugins/fromfilename`: Recognize file names that contain *only* a track\n  number, such as ``01.mp3``. Also, the plugin now allows underscores as a\n  separator between fields. Thanks to :user:`Vrihub`. :bug:`2738` :bug:`2759`\n- Fixed an issue where images would be resized according to their longest edge,\n  instead of their width, when using the ``maxwidth`` config option in the\n  :doc:`/plugins/fetchart` and :doc:`/plugins/embedart`. Thanks to\n  :user:`sekjun9878`. :bug:`2729`\n\nThere are some changes for developers\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- \"Fixed fields\" in Album and Item objects are now more strict about translating\n  missing values into type-specific null-like values. This should help in cases\n  where a string field is unexpectedly ``None`` sometimes instead of just\n  showing up as an empty string. :bug:`2605`\n- Refactored the move functions the ``beets.library`` module and the\n  ``manipulate_files`` function in ``beets.importer`` to use a single parameter\n  describing the file operation instead of multiple Boolean flags. There is a\n  new numerated type describing how to move, copy, or link files. :bug:`2682`\n\n1.4.5 (June 20, 2017)\n---------------------\n\nVersion 1.4.5 adds some oft-requested features. When you're importing files, you\ncan now manually set fields on the new music. Date queries have gotten much more\npowerful: you can write precise queries down to the second, and we now have\n*relative* queries like ``-1w``, which means *one week ago*.\n\nHere are the new features\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- You can now set fields to certain values during :ref:`import-cmd`, using\n  either a ``--set field=value`` command-line flag or a new :ref:`set_fields`\n  configuration option under the ``importer`` section. Thanks to :user:`bartkl`.\n  :bug:`1881` :bug:`2581`\n- :ref:`Date queries <datequery>` can now include times, so you can filter your\n  music down to the second. Thanks to :user:`discopatrick`. :bug:`2506`\n  :bug:`2528`\n- :ref:`Date queries <datequery>` can also be *relative*. You can say\n  ``added:-1w..`` to match music added in the last week, for example. Thanks to\n  :user:`euri10`. :bug:`2598`\n- A new ``/plugins/gmusic`` lets you interact with your Google Play Music\n  library. Thanks to :user:`tigranl`. :bug:`2553` :bug:`2586`\n- :doc:`/plugins/replaygain`: We now keep R128 data in separate tags from\n  classic ReplayGain data for formats that need it (namely, Ogg Opus). A new\n  ``r128`` configuration option enables this behavior for specific formats.\n  Thanks to :user:`autrimpo`. :bug:`2557` :bug:`2560`\n- The :ref:`move-cmd` command gained a new ``--export`` flag, which copies files\n  to an external location without changing their paths in the library database.\n  Thanks to :user:`SpirosChadoulos`. :bug:`435` :bug:`2510`\n\nThere are also some bug fixes\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- :doc:`/plugins/lastgenre`: Fix a crash when using the ``prefer_specific`` and\n  ``canonical`` options together. Thanks to :user:`yacoob`. :bug:`2459`\n  :bug:`2583`\n- :doc:`/plugins/web`: Fix a crash on Windows under Python 2 when serving\n  non-ASCII filenames. Thanks to :user:`robot3498712`. :bug:`2592` :bug:`2593`\n- :doc:`/plugins/metasync`: Fix a crash in the Amarok backend when filenames\n  contain quotes. Thanks to :user:`aranc23`. :bug:`2595` :bug:`2596`\n- More informative error messages are displayed when the file format is not\n  recognized. :bug:`2599`\n\n1.4.4 (June 10, 2017)\n---------------------\n\nThis release built up a longer-than-normal list of nifty new features. We now\nsupport DSF audio files and the importer can hard-link your files, for example.\n\nHere's a full list of new features\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- Added support for DSF files, once a future version of Mutagen is released that\n  supports them. Thanks to :user:`docbobo`. :bug:`459` :bug:`2379`\n- A new :ref:`hardlink` config option instructs the importer to create hard\n  links on filesystems that support them. Thanks to :user:`jacobwgillespie`.\n  :bug:`2445`\n- A new :doc:`/plugins/kodiupdate` lets you keep your Kodi library in sync with\n  beets. Thanks to :user:`Pauligrinder`. :bug:`2411`\n- A new :ref:`bell` configuration option under the ``import`` section enables a\n  terminal bell when input is required. Thanks to :user:`SpirosChadoulos`.\n  :bug:`2366` :bug:`2495`\n- A new field, ``composer_sort``, is now supported and fetched from MusicBrainz.\n  Thanks to :user:`dosoe`. :bug:`2519` :bug:`2529`\n- The MusicBrainz backend and :doc:`/plugins/discogs` now both provide a new\n  attribute called ``track_alt`` that stores more nuanced, possibly non-numeric\n  track index data. For example, some vinyl or tape media will report the side\n  of the record using a letter instead of a number in that field. :bug:`1831`\n  :bug:`2363`\n- :doc:`/plugins/web`: Added a new endpoint, ``/item/path/foo``, which will\n  return the item info for the file at the given path, or 404.\n- :doc:`/plugins/web`: Added a new config option, ``include_paths``, which will\n  cause paths to be included in item API responses if set to true.\n- The ``%aunique`` template function for :ref:`aunique` now takes a third\n  argument that specifies which brackets to use around the disambiguator value.\n  The argument can be any two characters that represent the left and right\n  brackets. It defaults to ``[]`` and can also be blank to turn off bracketing.\n  :bug:`2397` :bug:`2399`\n- Added a ``--move`` or ``-m`` option to the importer so that the files can be\n  moved to the library instead of being copied or added \"in place.\" :bug:`2252`\n  :bug:`2429`\n- :doc:`/plugins/badfiles`: Added a ``--verbose`` or ``-v`` option. Results are\n  now displayed only for corrupted files by default and for all the files when\n  the verbose option is set. :bug:`1654` :bug:`2434`\n- :doc:`/plugins/embedart`: The explicit ``embedart`` command now asks for\n  confirmation before embedding art into music files. Thanks to :user:`Stunner`.\n  :bug:`1999`\n- You can now run beets by typing ``python -m beets``. :bug:`2453`\n- :doc:`/plugins/smartplaylist`: Different playlist specifications that generate\n  identically-named playlist files no longer conflict; instead, the resulting\n  lists of tracks are concatenated. :bug:`2468`\n- :doc:`/plugins/missing`: A new mode lets you see missing albums from artists\n  you have in your library. Thanks to :user:`qlyoung`. :bug:`2481`\n- :doc:`/plugins/web` : Add new ``reverse_proxy`` config option to allow serving\n  the web plugins under a reverse proxy.\n- Importing a release with multiple release events now selects the event based\n  on your :ref:`preferred` countries. :bug:`2501`\n- :doc:`/plugins/play`: A new ``-y`` or ``--yes`` parameter lets you skip the\n  warning message if you enqueue more items than the warning threshold usually\n  allows.\n- Fix a bug where commands which forked subprocesses would sometimes prevent\n  further inputs. This bug mainly affected :doc:`/plugins/convert`. Thanks to\n  :user:`jansol`. :bug:`2488` :bug:`2524`\n\nThere are also quite a few fixes\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- In the :ref:`replace` configuration option, we now replace a leading hyphen\n  (-) with an underscore. :bug:`549` :bug:`2509`\n- :doc:`/plugins/absubmit`: We no longer filter audio files for specific\n  formats---we will attempt the submission process for all formats. :bug:`2471`\n- :doc:`/plugins/mpdupdate`: Fix Python 3 compatibility. :bug:`2381`\n- :doc:`/plugins/replaygain`: Fix Python 3 compatibility in the ``bs1770gain``\n  backend. :bug:`2382`\n- :doc:`/plugins/bpd`: Report playback times as integers. :bug:`2394`\n- :doc:`/plugins/mpdstats`: Fix Python 3 compatibility. The plugin also now\n  requires version 0.4.2 or later of the ``python-mpd2`` library. :bug:`2405`\n- :doc:`/plugins/mpdstats`: Improve handling of MPD status queries.\n- :doc:`/plugins/badfiles`: Fix Python 3 compatibility.\n- Fix some cases where album-level ReplayGain/SoundCheck metadata would be\n  written to files incorrectly. :bug:`2426`\n- :doc:`/plugins/badfiles`: The command no longer bails out if the validator\n  command is not found or exits with an error. :bug:`2430` :bug:`2433`\n- :doc:`/plugins/lyrics`: The Google search backend no longer crashes when the\n  server responds with an error. :bug:`2437`\n- :doc:`/plugins/discogs`: You can now authenticate with Discogs using a\n  personal access token. :bug:`2447`\n- Fix Python 3 compatibility when extracting rar archives in the importer.\n  Thanks to :user:`Lompik`. :bug:`2443` :bug:`2448`\n- :doc:`/plugins/duplicates`: Fix Python 3 compatibility when using the ``copy``\n  and ``move`` options. :bug:`2444`\n- :doc:`/plugins/mbsubmit`: The tracks are now sorted properly. Thanks to\n  :user:`awesomer`. :bug:`2457`\n- :doc:`/plugins/thumbnails`: Fix a string-related crash on Python 3.\n  :bug:`2466`\n- :doc:`/plugins/beatport`: More than just 10 songs are now fetched per album.\n  :bug:`2469`\n- On Python 3, the :ref:`terminal_encoding` setting is respected again for\n  output and printing will no longer crash on systems configured with a limited\n  encoding.\n- :doc:`/plugins/convert`: The default configuration uses FFmpeg's built-in AAC\n  codec instead of faac. Thanks to :user:`jansol`. :bug:`2484`\n- Fix the importer's detection of multi-disc albums when other subdirectories\n  are present. :bug:`2493`\n- Invalid date queries now print an error message instead of being silently\n  ignored. Thanks to :user:`discopatrick`. :bug:`2513` :bug:`2517`\n- When the SQLite database stops being accessible, we now print a friendly error\n  message. Thanks to :user:`Mary011196`. :bug:`1676` :bug:`2508`\n- :doc:`/plugins/web`: Avoid a crash when sending binary data, such as\n  Chromaprint fingerprints, in music attributes. :bug:`2542` :bug:`2532`\n- Fix a hang when parsing templates that end in newlines. :bug:`2562`\n- Fix a crash when reading non-ASCII characters in configuration files on\n  Windows under Python 3. :bug:`2456` :bug:`2565` :bug:`2566`\n\nWe removed backends from two metadata plugins because of bitrot\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- :doc:`/plugins/lyrics`: The Lyrics.com backend has been removed. (It stopped\n  working because of changes to the site's URL structure.) :bug:`2548`\n  :bug:`2549`\n- :doc:`/plugins/fetchart`: The documentation no longer recommends iTunes Store\n  artwork lookup because the unmaintained python-itunes_ is broken. Want to\n  adopt it? :bug:`2371` :bug:`1610`\n\n.. _python-itunes: https://github.com/ocelma/python-itunes\n\n1.4.3 (January 9, 2017)\n-----------------------\n\nHappy new year! This new version includes a cornucopia of new features from\ncontributors, including new tags related to classical music and a new\n:doc:`/plugins/absubmit` for performing acoustic analysis on your music. The\n:doc:`/plugins/random` has a new mode that lets you generate time-limited\nmusic---for example, you might generate a random playlist that lasts the perfect\nlength for your walk to work. We also access as many Web services as possible\nover secure connections now---HTTPS everywhere!\n\nThe most visible new features are\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- We now support the composer, lyricist, and arranger tags. The MusicBrainz data\n  source will fetch data for these fields when the next version of\n  python-musicbrainzngs_ is released. Thanks to :user:`ibmibmibm`. :bug:`506`\n  :bug:`507` :bug:`1547` :bug:`2333`\n- A new :doc:`/plugins/absubmit` lets you run acoustic analysis software and\n  upload the results for others to use. Thanks to :user:`inytar`. :bug:`2253`\n  :bug:`2342`\n- :doc:`/plugins/play`: The plugin now provides an importer prompt choice to\n  play the music you're about to import. Thanks to :user:`diomekes`. :bug:`2008`\n  :bug:`2360`\n- We now use SSL to access Web services whenever possible. That includes\n  MusicBrainz itself, several album art sources, some lyrics sources, and other\n  servers. Thanks to :user:`tigranl`. :bug:`2307`\n- :doc:`/plugins/random`: A new ``--time`` option lets you generate a random\n  playlist that takes a given amount of time. Thanks to :user:`diomekes`.\n  :bug:`2305` :bug:`2322`\n\nSome smaller new features\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- :doc:`/plugins/zero`: A new ``zero`` command manually triggers the zero\n  plugin. Thanks to :user:`SJoshBrown`. :bug:`2274` :bug:`2329`\n- :doc:`/plugins/acousticbrainz`: The plugin will avoid re-downloading data for\n  files that already have it by default. You can override this behavior using a\n  new ``force`` option. Thanks to :user:`SusannaMaria`. :bug:`2347` :bug:`2349`\n- :doc:`/plugins/bpm`: The ``import.write`` configuration option now decides\n  whether or not to write tracks after updating their BPM. :bug:`1992`\n\nAnd the fixes\n~~~~~~~~~~~~~\n\n- :doc:`/plugins/bpd`: Fix a crash on non-ASCII MPD commands. :bug:`2332`\n- :doc:`/plugins/scrub`: Avoid a crash when files cannot be read or written.\n  :bug:`2351`\n- :doc:`/plugins/scrub`: The image type values on scrubbed files are preserved\n  instead of being reset to \"other.\" :bug:`2339`\n- :doc:`/plugins/web`: Fix a crash on Python 3 when serving files from the\n  filesystem. :bug:`2353`\n- :doc:`/plugins/discogs`: Improve the handling of releases that contain\n  subtracks. :bug:`2318`\n- :doc:`/plugins/discogs`: Fix a crash when a release does not contain format\n  information, and increase robustness when other fields are missing.\n  :bug:`2302`\n- :doc:`/plugins/lyrics`: The plugin now reports a beets-specific User-Agent\n  header when requesting lyrics. :bug:`2357`\n- :doc:`/plugins/embyupdate`: The plugin now checks whether an API key or a\n  password is provided in the configuration.\n- :doc:`/plugins/play`: The misspelled configuration option ``warning_treshold``\n  is no longer supported.\n\nFor plugin developers\n~~~~~~~~~~~~~~~~~~~~~\n\n- :ref:`append_prompt_choices`: When providing new importer prompt choices, you\n  can now provide new candidates for the user to consider. For example, you\n  might provide an alternative strategy for picking between the available\n  alternatives or for looking up a release on MusicBrainz.\n\n1.4.2 (December 16, 2016)\n-------------------------\n\nThis is just a little bug fix release. With 1.4.2, we're also confident enough\nto recommend that anyone who's interested give Python 3 a try: bugs may still\nlurk, but we've deemed things safe enough for broad adoption. If you can, please\ninstall beets with ``pip3`` instead of ``pip2`` this time and let us know how it\ngoes!\n\nHere are the fixes\n~~~~~~~~~~~~~~~~~~\n\n- :doc:`/plugins/badfiles`: Fix a crash on non-ASCII filenames. :bug:`2299`\n- The ``%asciify{}`` path formatting function and the :ref:`asciify-paths`\n  setting properly substitute path separators generated by converting some\n  Unicode characters, such as ½ and ¢, into ASCII.\n- :doc:`/plugins/convert`: Fix a logging-related crash when filenames contain\n  curly braces. Thanks to :user:`kierdavis`. :bug:`2323`\n- We've rolled back some changes to the included zsh completion script that were\n  causing problems for some users. :bug:`2266`\n\nAlso, we've removed some special handling for logging in the\n:doc:`/plugins/discogs` that we believe was unnecessary. If spurious log\nmessages appear in this version, please let us know by filing a bug.\n\n1.4.1 (November 25, 2016)\n-------------------------\n\nVersion 1.4 has **alpha-level** Python 3 support. Thanks to the heroic efforts\nof :user:`jrobeson`, beets should run both under Python 2.7, as before, and now\nunder Python 3.4 and above. The support is still new: it undoubtedly contains\nbugs, so it may replace all your music with Limp Bizkit---but if you're brave\nand you have backups, please try installing on Python 3. Let us know how it\ngoes.\n\nIf you package beets for distribution, here's what you'll want to know\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- This version of beets now depends on the six_ library.\n- We also bumped our minimum required version of Mutagen_ to 1.33 (from 1.27).\n- Please don't package beets as a Python 3 application *yet*, even though most\n  things work under Python 3.4 and later.\n\nThis version also makes a few changes to the command-line interface and\nconfiguration that you may need to know about:\n\n- :doc:`/plugins/duplicates`: The ``duplicates`` command no longer accepts\n  multiple field arguments in the form ``-k title albumartist album``. Each\n  argument must be prefixed with ``-k``, as in ``-k title -k albumartist -k\n  album``.\n- The old top-level ``colors`` configuration option has been removed (the\n  setting is now under ``ui``).\n- The deprecated ``list_format_album`` and ``list_format_item`` configuration\n  options have been removed (see :ref:`format_album` and :ref:`format_item`).\n\nThe are a few new features\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- :doc:`/plugins/mpdupdate`, :doc:`/plugins/mpdstats`: When the ``host`` option\n  is not set, these plugins will now look for the ``$MPD_HOST`` environment\n  variable before falling back to ``localhost``. Thanks to :user:`tarruda`.\n  :bug:`2175`\n- :doc:`/plugins/web`: Added an ``expand`` option to show the items of an album.\n  :bug:`2050`\n- :doc:`/plugins/embyupdate`: The plugin can now use an API key instead of a\n  password to authenticate with Emby. :bug:`2045` :bug:`2117`\n- :doc:`/plugins/acousticbrainz`: The plugin now adds a ``bpm`` field.\n- ``beet --version`` now includes the Python version used to run beets.\n- :doc:`/reference/pathformat` can now include unescaped commas (``,``) when\n  they are not part of a function call. :bug:`2166` :bug:`2213`\n- The :ref:`update-cmd` command takes a new ``-F`` flag to specify the fields to\n  update. Thanks to :user:`dangmai`. :bug:`2229` :bug:`2231`\n\nAnd there are a few bug fixes too\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- :doc:`/plugins/convert`: The plugin no longer asks for confirmation if the\n  query did not return anything to convert. :bug:`2260` :bug:`2262`\n- :doc:`/plugins/embedart`: The plugin now uses ``jpg`` as an extension rather\n  than ``jpeg``, to ensure consistency with the :doc:`plugins/fetchart`. Thanks\n  to :user:`tweitzel`. :bug:`2254` :bug:`2255`\n- :doc:`/plugins/embedart`: The plugin now works for all jpeg files, including\n  those that are only recognizable by their magic bytes. :bug:`1545` :bug:`2255`\n- :doc:`/plugins/web`: The JSON output is no longer pretty-printed (for a space\n  savings). :bug:`2050`\n- :doc:`/plugins/permissions`: Fix a regression in the previous release where\n  the plugin would always fail to set permissions (and log a warning).\n  :bug:`2089`\n- :doc:`/plugins/beatport`: Use track numbers from Beatport (instead of\n  determining them from the order of tracks) and set the ``medium_index`` value.\n- With :ref:`per_disc_numbering` enabled, some metadata sources (notably, the\n  :doc:`/plugins/beatport`) would not set the track number at all. This is\n  fixed. :bug:`2085`\n- :doc:`/plugins/play`: Fix ``$args`` getting passed verbatim to the play\n  command if it was set in the configuration but ``-A`` or ``--args`` was\n  omitted.\n- With :ref:`ignore_hidden` enabled, non-UTF-8 filenames would cause a crash.\n  This is fixed. :bug:`2168`\n- :doc:`/plugins/embyupdate`: Fixes authentication header problem that caused a\n  problem that it was not possible to get tokens from the Emby API.\n- :doc:`/plugins/lyrics`: Some titles use a colon to separate the main title\n  from a subtitle. To find more matches, the plugin now also searches for lyrics\n  using the part part preceding the colon character. :bug:`2206`\n- Fix a crash when a query uses a date field and some items are missing that\n  field. :bug:`1938`\n- :doc:`/plugins/discogs`: Subtracks are now detected and combined into a single\n  track, two-sided mediums are treated as single discs, and tracks have\n  ``media``, ``medium_total`` and ``medium`` set correctly. :bug:`2222`\n  :bug:`2228`.\n- :doc:`/plugins/missing`: ``missing`` is now treated as an integer, allowing\n  the use of (for example) ranges in queries.\n- :doc:`/plugins/smartplaylist`: Playlist names will be sanitized to ensure\n  valid filenames. :bug:`2258`\n- The ID3 APIC tag now uses the Latin-1 encoding when possible instead of a\n  Unicode encoding. This should increase compatibility with other software,\n  especially with iTunes and when using ID3v2.3. Thanks to :user:`lazka`.\n  :bug:`899` :bug:`2264` :bug:`2270`\n\nThe last release, 1.3.19, also erroneously reported its version as \"1.3.18\" when\nyou typed ``beet version``. This has been corrected.\n\n.. _six: https://pypi.org/project/six/\n\n1.3.19 (June 25, 2016)\n----------------------\n\nThis is primarily a bug fix release: it cleans up a couple of regressions that\nappeared in the last version. But it also features the triumphant return of the\n:doc:`/plugins/beatport` and a modernized :doc:`/plugins/bpd`.\n\nIt's also the first version where beets passes all its tests on Windows! May\nthis herald a new age of cross-platform reliability for beets.\n\nNew features\n~~~~~~~~~~~~\n\n- :doc:`/plugins/beatport`: This metadata source plugin has arisen from the\n  dead! It now works with Beatport's new OAuth-based API. Thanks to\n  :user:`jbaiter`. :bug:`1989` :bug:`2067`\n- :doc:`/plugins/bpd`: The plugin now uses the modern GStreamer 1.0 instead of\n  the old 0.10. Thanks to :user:`philippbeckmann`. :bug:`2057` :bug:`2062`\n- A new ``--force`` option for the :ref:`remove-cmd` command allows removal of\n  items without prompting beforehand. :bug:`2042`\n- A new :ref:`duplicate_action` importer config option controls how duplicate\n  albums or tracks treated in import task. :bug:`185`\n\nSome fixes for Windows\n~~~~~~~~~~~~~~~~~~~~~~\n\n- Queries are now detected as paths when they contain backslashes (in addition\n  to forward slashes). This only applies on Windows.\n- :doc:`/plugins/embedart`: Image similarity comparison with ImageMagick should\n  now work on Windows.\n- :doc:`/plugins/fetchart`: The plugin should work more reliably with non-ASCII\n  paths.\n\nAnd other fixes\n~~~~~~~~~~~~~~~\n\n- :doc:`/plugins/replaygain`: The ``bs1770gain`` backend now correctly\n  calculates sample peak instead of true peak. This comes with a major speed\n  increase. :bug:`2031`\n- :doc:`/plugins/lyrics`: Avoid a crash and a spurious warning introduced in the\n  last version about a Google API key, which appeared even when you hadn't\n  enabled the Google lyrics source.\n- Fix a hard-coded path to ``bash-completion`` to work better with Homebrew\n  installations. Thanks to :user:`bismark`. :bug:`2038`\n- Fix a crash introduced in the previous version when the standard input was\n  connected to a Unix pipe. :bug:`2041`\n- Fix a crash when specifying non-ASCII format strings on the command line with\n  the ``-f`` option for many commands. :bug:`2063`\n- :doc:`/plugins/fetchart`: Determine the file extension for downloaded images\n  based on the image's magic bytes. The plugin prints a warning if result is not\n  consistent with the server-supplied ``Content-Type`` header. In previous\n  versions, the plugin would use a ``.jpg`` extension for all images.\n  :bug:`2053`\n\n1.3.18 (May 31, 2016)\n---------------------\n\nThis update adds a new :doc:`/plugins/hook` that lets you integrate beets with\ncommand-line tools and an :doc:`/plugins/export` that can dump data from the\nbeets database as JSON. You can also automatically translate lyrics using a\nmachine translation service.\n\nThe ``echonest`` plugin has been removed in this version because the API it used\nis `shutting down`_. You might want to try the :doc:`/plugins/acousticbrainz`\ninstead.\n\n.. _shutting down: https://web.archive.org/web/20260000000000*/developer.spotify.com/news-stories/2016/03/29/api-improvements-update\n\nSome of the larger new features\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- The new :doc:`/plugins/hook` lets you execute commands in response to beets\n  events.\n- The new :doc:`/plugins/export` can export data from beets' database as JSON.\n  Thanks to :user:`GuilhermeHideki`.\n- :doc:`/plugins/lyrics`: The plugin can now translate the fetched lyrics to\n  your native language using the Bing translation API. Thanks to\n  :user:`Kraymer`.\n- :doc:`/plugins/fetchart`: Album art can now be fetched from fanart.tv_.\n\nSmaller new things\n~~~~~~~~~~~~~~~~~~\n\n- There are two new functions available in templates: ``%first`` and ``%ifdef``.\n  See :ref:`template-functions`.\n- :doc:`/plugins/convert`: A new ``album_art_maxwidth`` setting lets you resize\n  album art while copying it.\n- :doc:`/plugins/convert`: The ``extension`` setting is now optional for\n  conversion formats. By default, the extension is the same as the name of the\n  configured format.\n- :doc:`/plugins/importadded`: A new ``preserve_write_mtimes`` option lets you\n  preserve mtime of files even when beets updates their metadata.\n- :doc:`/plugins/fetchart`: The ``enforce_ratio`` option now lets you tolerate\n  images that are *almost* square but differ slightly from an exact 1:1 aspect\n  ratio.\n- :doc:`/plugins/fetchart`: The plugin can now optionally save the artwork's\n  source in an attribute in the database.\n- The :ref:`terminal_encoding` configuration option can now also override the\n  *input* encoding. (Previously, it only affected the encoding of the standard\n  *output* stream.)\n- A new :ref:`ignore_hidden` configuration option lets you ignore files that\n  your OS marks as invisible.\n- :doc:`/plugins/web`: A new ``values`` endpoint lets you get the distinct\n  values of a field. Thanks to :user:`sumpfralle`. :bug:`2010`\n\n.. _fanart.tv: https://fanart.tv/\n\nFixes\n~~~~~\n\n- Fix a problem with the :ref:`stats-cmd` command in exact mode when filenames\n  on Windows use non-ASCII characters. :bug:`1891`\n- Fix a crash when iTunes Sound Check tags contained invalid data. :bug:`1895`\n- :doc:`/plugins/mbcollection`: The plugin now redacts your MusicBrainz password\n  in the ``beet config`` output. :bug:`1907`\n- :doc:`/plugins/scrub`: Fix an occasional problem where scrubbing on import\n  could undo the :ref:`id3v23` setting. :bug:`1903`\n- :doc:`/plugins/lyrics`: Add compatibility with some changes to the LyricsWiki\n  page markup. :bug:`1912` :bug:`1909`\n- :doc:`/plugins/lyrics`: Fix retrieval from Musixmatch by improving the way we\n  guess the URL for lyrics on that service. :bug:`1880`\n- :doc:`/plugins/edit`: Fail gracefully when the configured text editor command\n  can't be invoked. :bug:`1927`\n- :doc:`/plugins/fetchart`: Fix a crash in the Wikipedia backend on non-ASCII\n  artist and album names. :bug:`1960`\n- :doc:`/plugins/convert`: Change the default ``ogg`` encoding quality from 2 to\n  3 (to fit the default from the ``oggenc(1)`` manpage). :bug:`1982`\n- :doc:`/plugins/convert`: The ``never_convert_lossy_files`` option now\n  considers AIFF a lossless format. :bug:`2005`\n- :doc:`/plugins/web`: A proper 404 error, instead of an internal exception, is\n  returned when missing album art is requested. Thanks to :user:`sumpfralle`.\n  :bug:`2011`\n- Tolerate more malformed floating-point numbers in metadata tags. :bug:`2014`\n- The :ref:`ignore` configuration option now includes the ``lost+found``\n  directory by default.\n- :doc:`/plugins/acousticbrainz`: AcousticBrainz lookups are now done over\n  HTTPS. Thanks to :user:`Freso`. :bug:`2007`\n\n1.3.17 (February 7, 2016)\n-------------------------\n\nThis release introduces one new plugin to fetch audio information from the\nAcousticBrainz_ project and another plugin to make it easier to submit your\nhandcrafted metadata back to MusicBrainz. The importer also gained two\noft-requested features: a way to skip the initial search process by specifying\nan ID ahead of time, and a way to *manually* provide metadata in the middle of\nthe import process (via the :doc:`/plugins/edit`).\n\nAlso, as of this release, the beets project has some new Internet homes! Our new\ndomain name is beets.io_, and we have a shiny new GitHub organization: beetbox_.\n\nHere are the big new features\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- A new :doc:`/plugins/acousticbrainz` fetches acoustic-analysis information\n  from the AcousticBrainz_ project. Thanks to :user:`opatel99`, and thanks to\n  `Google Code-In`_! :bug:`1784`\n- A new :doc:`/plugins/mbsubmit` lets you print music's current metadata in a\n  format that the MusicBrainz data parser can understand. You can trigger it\n  during an interactive import session. :bug:`1779`\n- A new ``--search-id`` importer option lets you manually specify IDs (i.e.,\n  MBIDs or Discogs IDs) for imported music. Doing this skips the initial\n  candidate search, which can be important for huge albums where this initial\n  lookup is slow. Also, the ``enter Id`` prompt choice now accepts several IDs,\n  separated by spaces. :bug:`1808`\n- :doc:`/plugins/edit`: You can now edit metadata *on the fly* during the import\n  process. The plugin provides two new interactive options: one to edit *your\n  music's* metadata, and one to edit the *matched metadata* retrieved from\n  MusicBrainz (or another data source). This feature is still in its early\n  stages, so please send feedback if you find anything missing. :bug:`1846`\n  :bug:`396`\n\nThere are even more new features\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- :doc:`/plugins/fetchart`: The Google Images backend has been restored. It now\n  requires an API key from Google. Thanks to :user:`lcharlick`. :bug:`1778`\n- :doc:`/plugins/info`: A new option will print only fields' names and not their\n  values. Thanks to :user:`GuilhermeHideki`. :bug:`1812`\n- The :ref:`fields-cmd` command now displays flexible attributes. Thanks to\n  :user:`GuilhermeHideki`. :bug:`1818`\n- The :ref:`modify-cmd` command lets you interactively select which albums or\n  items you want to change. :bug:`1843`\n- The :ref:`move-cmd` command gained a new ``--timid`` flag to print and confirm\n  which files you want to move. :bug:`1843`\n- The :ref:`move-cmd` command no longer prints filenames for files that don't\n  actually need to be moved. :bug:`1583`\n\n.. _acousticbrainz: https://acousticbrainz.org/\n\n.. _google code-in: https://codein.withgoogle.com/archive/\n\nFixes\n~~~~~\n\n- :doc:`/plugins/play`: Fix a regression in the last version where there was no\n  default command. :bug:`1793`\n- :doc:`/plugins/lastimport`: The plugin now works again after being broken by\n  some unannounced changes to the Last.fm API. :bug:`1574`\n- :doc:`/plugins/play`: Fixed a typo in a configuration option. The option is\n  now ``warning_threshold`` instead of ``warning_treshold``, but we kept the old\n  name around for compatibility. Thanks to :user:`JesseWeinstein`. :bug:`1802`\n  :bug:`1803`\n- :doc:`/plugins/edit`: Editing metadata now moves files, when appropriate (like\n  the :ref:`modify-cmd` command). :bug:`1804`\n- The :ref:`stats-cmd` command no longer crashes when files are missing or\n  inaccessible. :bug:`1806`\n- :doc:`/plugins/fetchart`: Possibly fix a Unicode-related crash when using some\n  versions of pyOpenSSL. :bug:`1805`\n- :doc:`/plugins/replaygain`: Fix an intermittent crash with the GStreamer\n  backend. :bug:`1855`\n- :doc:`/plugins/lastimport`: The plugin now works with the beets API key by\n  default. You can still provide a different key the configuration.\n- :doc:`/plugins/replaygain`: Fix a crash using the Python Audio Tools backend.\n  :bug:`1873`\n\n.. _beetbox: https://github.com/beetbox\n\n.. _beets.io: https://beets.io/\n\n1.3.16 (December 28, 2015)\n--------------------------\n\nThe big news in this release is a new :doc:`interactive editor plugin\n</plugins/edit>`. It's really nifty: you can now change your music's metadata by\nmaking changes in a visual text editor, which can sometimes be far more\nefficient than the built-in :ref:`modify-cmd` command. No more carefully\nretyping the same artist name with slight capitalization changes.\n\nThis version also adds an oft-requested \"not\" operator to beets' queries, so you\ncan exclude music from any operation. It also brings friendlier formatting (and\nquerying!) of song durations.\n\nThe big new stuff\n~~~~~~~~~~~~~~~~~\n\n- A new :doc:`/plugins/edit` lets you manually edit your music's metadata using\n  your favorite text editor. :bug:`164` :bug:`1706`\n- Queries can now use \"not\" logic. Type a ``^`` before part of a query to\n  *exclude* matching music from the results. For example, ``beet list -a beatles\n  ^album:1`` will find all your albums by the Beatles except for their singles\n  compilation, \"1.\" See :ref:`not_query`. :bug:`819` :bug:`1728`\n- A new :doc:`/plugins/embyupdate` can trigger a library refresh on an Emby_\n  server when your beets database changes.\n- Track length is now displayed as \"M:SS\" rather than a raw number of seconds.\n  Queries on track length also accept this format: for example, ``beet list\n  length:5:30..`` will find all your tracks that have a duration over 5 minutes\n  and 30 seconds. You can turn off this new behavior using the\n  ``format_raw_length`` configuration option. :bug:`1749`\n\nSmaller changes\n~~~~~~~~~~~~~~~\n\n- Three commands, ``modify``, ``update``, and ``mbsync``, would previously move\n  files by default after changing their metadata. Now, these commands will only\n  move files if you have the :ref:`config-import-copy` or\n  :ref:`config-import-move` options enabled in your importer configuration. This\n  way, if you configure the importer not to touch your filenames, other commands\n  will respect that decision by default too. Each command also sprouted a\n  ``--move`` command-line option to override this default (in addition to the\n  ``--nomove`` flag they already had). :bug:`1697`\n- A new configuration option, ``va_name``, controls the album artist name for\n  various-artists albums. The setting defaults to \"Various Artists,\" the\n  MusicBrainz standard. In order to match MusicBrainz, the\n  :doc:`/plugins/discogs` also adopts the same setting.\n- :doc:`/plugins/info`: The ``info`` command now accepts a ``-f/--format``\n  option for customizing how items are displayed, just like the built-in\n  ``list`` command. :bug:`1737`\n\nSome changes for developers\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- Two new :ref:`plugin hooks <plugin_events>`, ``albuminfo_received`` and\n  ``trackinfo_received``, let plugins intercept metadata as soon as it is\n  received, before it is applied to music in the database. :bug:`872`\n- Plugins can now add options to the interactive importer prompts. See\n  :ref:`append_prompt_choices`. :bug:`1758`\n\nFixes\n~~~~~\n\n- :doc:`/plugins/plexupdate`: Fix a crash when Plex libraries use non-ASCII\n  collection names. :bug:`1649`\n- :doc:`/plugins/discogs`: Maybe fix a crash when using some versions of the\n  ``requests`` library. :bug:`1656`\n- Fix a race in the importer when importing two albums with the same artist and\n  name in quick succession. The importer would fail to detect them as\n  duplicates, claiming that there were \"empty albums\" in the database even when\n  there were not. :bug:`1652`\n- :doc:`plugins/lastgenre`: Clean up the reggae-related genres somewhat. Thanks\n  to :user:`Freso`. :bug:`1661`\n- The importer now correctly moves album art files when re-importing. :bug:`314`\n- :doc:`/plugins/fetchart`: In auto mode, the plugin now skips albums that\n  already have art attached to them so as not to interfere with re-imports.\n  :bug:`314`\n- :doc:`plugins/fetchart`: The plugin now only resizes album art if necessary,\n  rather than always by default. :bug:`1264`\n- :doc:`plugins/fetchart`: Fix a bug where a database reference to a\n  non-existent album art file would prevent the command from fetching new art.\n  :bug:`1126`\n- :doc:`/plugins/thumbnails`: Fix a crash with Unicode paths. :bug:`1686`\n- :doc:`/plugins/embedart`: The ``remove_art_file`` option now works on import\n  (as well as with the explicit command). :bug:`1662` :bug:`1675`\n- :doc:`/plugins/metasync`: Fix a crash when syncing with recent versions of\n  iTunes. :bug:`1700`\n- :doc:`/plugins/duplicates`: Fix a crash when merging items. :bug:`1699`\n- :doc:`/plugins/smartplaylist`: More gracefully handle malformed queries and\n  missing configuration.\n- Fix a crash with some files with unreadable iTunes SoundCheck metadata.\n  :bug:`1666`\n- :doc:`/plugins/thumbnails`: Fix a nasty segmentation fault crash that arose\n  with some library versions. :bug:`1433`\n- :doc:`/plugins/convert`: Fix a crash with Unicode paths in ``--pretend`` mode.\n  :bug:`1735`\n- Fix a crash when sorting by nonexistent fields on queries. :bug:`1734`\n- Probably fix some mysterious errors when dealing with images using ImageMagick\n  on Windows. :bug:`1721`\n- Fix a crash when writing some Unicode comment strings to MP3s that used older\n  encodings. The encoding is now always updated to UTF-8. :bug:`879`\n- :doc:`/plugins/fetchart`: The Google Images backend has been removed. It used\n  an API that has been shut down. :bug:`1760`\n- :doc:`/plugins/lyrics`: Fix a crash in the Google backend when searching for\n  bands with regular-expression characters in their names, like Sunn O))).\n  :bug:`1673`\n- :doc:`/plugins/scrub`: In ``auto`` mode, the plugin now *actually* only scrubs\n  files on import, as the documentation always claimed it did---not every time\n  files were written, as it previously did. :bug:`1657`\n- :doc:`/plugins/scrub`: Also in ``auto`` mode, album art is now correctly\n  restored. :bug:`1657`\n- Possibly allow flexible attributes to be used with the ``%aunique`` template\n  function. :bug:`1775`\n- :doc:`/plugins/lyrics`: The Genius backend is now more robust to communication\n  errors. The backend has also been disabled by default, since the API it\n  depends on is currently down. :bug:`1770`\n\n.. _emby: https://emby.media\n\n1.3.15 (October 17, 2015)\n-------------------------\n\nThis release adds a new plugin for checking file quality and a new source for\nlyrics. The larger features are:\n\n- A new :doc:`/plugins/badfiles` helps you scan for corruption in your music\n  collection. Thanks to :user:`fxthomas`. :bug:`1568`\n- :doc:`/plugins/lyrics`: You can now fetch lyrics from Genius.com. Thanks to\n  :user:`sadatay`. :bug:`1626` :bug:`1639`\n- :doc:`/plugins/zero`: The plugin can now use a \"whitelist\" policy as an\n  alternative to the (default) \"blacklist\" mode. Thanks to :user:`adkow`.\n  :bug:`1621` :bug:`1641`\n\nAnd there are smaller new features too\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- Add new color aliases for standard terminal color names (e.g., cyan and\n  magenta). Thanks to :user:`mathstuf`. :bug:`1548`\n- :doc:`/plugins/play`: A new ``--args`` option lets you specify options for the\n  player command. :bug:`1532`\n- :doc:`/plugins/play`: A new ``raw`` configuration option lets the command work\n  with players (such as VLC) that expect music filenames as arguments, rather\n  than in a playlist. Thanks to :user:`nathdwek`. :bug:`1578`\n- :doc:`/plugins/play`: You can now configure the number of tracks that trigger\n  a \"lots of music\" warning. :bug:`1577`\n- :doc:`/plugins/embedart`: A new ``remove_art_file`` option lets you clean up\n  if you prefer *only* embedded album art. Thanks to :user:`jackwilsdon`.\n  :bug:`1591` :bug:`733`\n- :doc:`/plugins/plexupdate`: A new ``library_name`` option allows you to select\n  which Plex library to update. :bug:`1572` :bug:`1595`\n- A new ``include`` option lets you import external configuration files.\n\nThis release has plenty of fixes\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- :doc:`/plugins/lastgenre`: Fix a bug that prevented tag popularity from being\n  considered. Thanks to :user:`svoos`. :bug:`1559`\n- Fixed a bug where plugins wouldn't be notified of the deletion of an item's\n  art, for example with the ``clearart`` command from the\n  :doc:`/plugins/embedart`. Thanks to :user:`nathdwek`. :bug:`1565`\n- :doc:`/plugins/fetchart`: The Google Images source is disabled by default (as\n  it was before beets 1.3.9), as is the Wikipedia source (which was causing lots\n  of unnecessary delays due to DBpedia downtime). To re-enable these sources,\n  add ``wikipedia google`` to your ``sources`` configuration option.\n- The :ref:`list-cmd` command's help output now has a small query and format\n  string example. Thanks to :user:`pkess`. :bug:`1582`\n- :doc:`/plugins/fetchart`: The plugin now fetches PNGs but not GIFs. (It still\n  fetches JPEGs.) This avoids an error when trying to embed images, since not\n  all formats support GIFs. :bug:`1588`\n- Date fields are now written in the correct order (year-month-day), which\n  eliminates an intermittent bug where the latter two fields would not get\n  written to files. Thanks to :user:`jdetrey`. :bug:`1303` :bug:`1589`\n- :doc:`/plugins/replaygain`: Avoid a crash when the PyAudioTools backend\n  encounters an error. :bug:`1592`\n- The case sensitivity of path queries is more useful now: rather than just\n  guessing based on the platform, we now check the case sensitivity of your\n  filesystem. :bug:`1586`\n- Case-insensitive path queries might have returned nothing because of a wrong\n  SQL query.\n- Fix a crash when a query contains a \"+\" or \"-\" alone in a component.\n  :bug:`1605`\n- Fixed unit of file size to powers of two (MiB, GiB, etc.) instead of powers of\n  ten (MB, GB, etc.). :bug:`1623`\n\n1.3.14 (August 2, 2015)\n-----------------------\n\nThis is mainly a bugfix release, but we also have a nifty new plugin for ipfs_\nand a bunch of new configuration options.\n\nThe new features\n~~~~~~~~~~~~~~~~\n\n- A new :doc:`/plugins/ipfs` lets you share music via a new, global,\n  decentralized filesystem. :bug:`1397`\n- :doc:`/plugins/duplicates`: You can now merge duplicate track metadata (when\n  detecting duplicate items), or duplicate album tracks (when detecting\n  duplicate albums).\n- :doc:`/plugins/duplicates`: Duplicate resolution now uses an ordering to\n  prioritize duplicates. By default, it prefers music with more complete\n  metadata, but you can configure it to use any list of attributes.\n- :doc:`/plugins/metasync`: Added a new backend to fetch metadata from iTunes.\n  This plugin is still in an experimental phase. :bug:`1450`\n- The ``move`` command has a new ``--pretend`` option, making the command show\n  how the items will be moved without actually changing anything.\n- The importer now supports matching of \"pregap\" or HTOA (hidden track-one\n  audio) tracks when they are listed in MusicBrainz. (This feature depends on a\n  new version of the python-musicbrainzngs_ library that is not yet released,\n  but will start working when it is available.) Thanks to :user:`ruippeixotog`.\n  :bug:`1104` :bug:`1493`\n- :doc:`/plugins/plexupdate`: A new ``token`` configuration option lets you\n  specify a key for Plex Home setups. Thanks to :user:`edcarroll`. :bug:`1494`\n\nFixes\n~~~~~\n\n- :doc:`/plugins/fetchart`: Complain when the ``enforce_ratio`` or ``min_width``\n  options are enabled but no local imaging backend is available to carry them\n  out. :bug:`1460`\n- :doc:`/plugins/importfeeds`: Avoid generating incorrect m3u filename when both\n  of the ``m3u`` and ``m3u_multi`` options are enabled. :bug:`1490`\n- :doc:`/plugins/duplicates`: Avoid a crash when misconfigured. :bug:`1457`\n- :doc:`/plugins/mpdstats`: Avoid a crash when the music played is not in the\n  beets library. Thanks to :user:`CodyReichert`. :bug:`1443`\n- Fix a crash with ArtResizer on Windows systems (affecting\n  :doc:`/plugins/embedart`, :doc:`/plugins/fetchart`, and\n  :doc:`/plugins/thumbnails`). :bug:`1448`\n- :doc:`/plugins/permissions`: Fix an error with non-ASCII paths. :bug:`1449`\n- Fix sorting by paths when the :ref:`sort_case_insensitive` option is enabled.\n  :bug:`1451`\n- :doc:`/plugins/embedart`: Avoid an error when trying to embed invalid images\n  into MPEG-4 files.\n- :doc:`/plugins/fetchart`: The Wikipedia source can now better deal artists\n  that use non-standard capitalization (e.g., alt-J, dEUS).\n- :doc:`/plugins/web`: Fix searching for non-ASCII queries. Thanks to\n  :user:`oldtopman`. :bug:`1470`\n- :doc:`/plugins/mpdupdate`: We now recommend the newer ``python-mpd2`` library\n  instead of its unmaintained parent. Thanks to :user:`Somasis`. :bug:`1472`\n- The importer interface and log file now output a useful list of files (instead\n  of the word \"None\") when in album-grouping mode. :bug:`1475` :bug:`825`\n- Fix some logging errors when filenames and other user-provided strings contain\n  curly braces. :bug:`1481`\n- Regular expression queries over paths now work more reliably with non-ASCII\n  characters in filenames. :bug:`1482`\n- Fix a bug where the autotagger's :ref:`ignored` setting was sometimes, well,\n  ignored. :bug:`1487`\n- Fix a bug with Unicode strings when generating image thumbnails. :bug:`1485`\n- :doc:`/plugins/keyfinder`: Fix handling of Unicode paths. :bug:`1502`\n- :doc:`/plugins/fetchart`: When album art is already present, the message is\n  now printed in the ``text_highlight_minor`` color (light gray). Thanks to\n  :user:`Somasis`. :bug:`1512`\n- Some messages in the console UI now use plural nouns correctly. Thanks to\n  :user:`JesseWeinstein`. :bug:`1521`\n- Sorting numerical fields (such as track) now works again. :bug:`1511`\n- :doc:`/plugins/replaygain`: Missing GStreamer plugins now cause a helpful\n  error message instead of a crash. :bug:`1518`\n- Fix an edge case when producing sanitized filenames where the maximum path\n  length conflicted with the :ref:`replace` rules. Thanks to Ben Ockmore.\n  :bug:`496` :bug:`1361`\n- Fix an incompatibility with OS X 10.11 (where ``/usr/sbin`` seems not to be on\n  the user's path by default).\n- Fix an incompatibility with certain JPEG files. Here's a relevant `Python\n  bug`_. Thanks to :user:`nathdwek`. :bug:`1545`\n- Fix the :ref:`group_albums` importer mode so that it works correctly when\n  files are not already in order by album. :bug:`1550`\n- The ``fields`` command no longer separates built-in fields from\n  plugin-provided ones. This distinction was becoming increasingly unreliable.\n- :doc:`/plugins/duplicates`: Fix a Unicode warning when paths contained\n  non-ASCII characters. :bug:`1551`\n- :doc:`/plugins/fetchart`: Work around a urllib3 bug that could cause a crash.\n  :bug:`1555` :bug:`1556`\n- When you edit the configuration file with ``beet config -e`` and the file does\n  not exist, beets creates an empty file before editing it. This fixes an error\n  on OS X, where the ``open`` command does not work with non-existent files.\n  :bug:`1480`\n- :doc:`/plugins/convert`: Fix a problem with filename encoding on Windows under\n  Python 3. :bug:`2515` :bug:`2516`\n\n.. _ipfs: https://about.ipfs.io/\n\n.. _python bug: https://bugs.python.org/issue16512\n\n1.3.13 (April 24, 2015)\n-----------------------\n\nThis is a tiny bug-fix release. It copes with a dependency upgrade that broke\nbeets. There are just two fixes:\n\n- Fix compatibility with Jellyfish_ version 0.5.0.\n- :doc:`/plugins/embedart`: In ``auto`` mode (the import hook), the plugin now\n  respects the ``write`` config option under ``import``. If this is disabled,\n  album art is no longer embedded on import in order to leave files\n  untouched---in effect, ``auto`` is implicitly disabled. :bug:`1427`\n\n1.3.12 (April 18, 2015)\n-----------------------\n\nThis little update makes queries more powerful, sorts music more intelligently,\nand removes a performance bottleneck. There's an experimental new plugin for\nsynchronizing metadata with music players.\n\nPackagers should also note a new dependency in this version: the Jellyfish_\nPython library makes our text comparisons (a big part of the auto-tagging\nprocess) go much faster.\n\nNew features\n~~~~~~~~~~~~\n\n- Queries can now use **\"or\" logic**: if you use a comma to separate parts of a\n  query, items and albums will match *either* side of the comma. For example,\n  ``beet ls foo , bar`` will get all the items matching ``foo`` or matching\n  ``bar``. See :ref:`combiningqueries`. :bug:`1423`\n- The autotagger's **matching algorithm is faster**. We now use the Jellyfish_\n  library to compute string similarity, which is better optimized than our\n  hand-rolled edit distance implementation. :bug:`1389`\n- Sorting is now **case insensitive** by default. This means that artists will\n  be sorted lexicographically regardless of case. For example, the artist alt-J\n  will now properly sort before YACHT. (Previously, it would have ended up at\n  the end of the list, after all the capital-letter artists.) You can turn this\n  new behavior off using the :ref:`sort_case_insensitive` configuration option.\n  See :ref:`query-sort`. :bug:`1429`\n- An experimental new :doc:`/plugins/metasync` lets you get metadata from your\n  favorite music players, starting with Amarok. :bug:`1386`\n- :doc:`/plugins/fetchart`: There are new settings to control what constitutes\n  \"acceptable\" images. The ``minwidth`` option constrains the minimum image\n  width in pixels and the ``enforce_ratio`` option requires that images be\n  square. :bug:`1394`\n\nLittle fixes and improvements\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- :doc:`/plugins/fetchart`: Remove a hard size limit when fetching from the\n  Cover Art Archive.\n- The output of the :ref:`fields-cmd` command is now sorted. Thanks to\n  :user:`multikatt`. :bug:`1402`\n- :doc:`/plugins/replaygain`: Fix a number of issues with the new ``bs1770gain``\n  backend on Windows. Also, fix missing debug output in import mode. :bug:`1398`\n- Beets should now be better at guessing the appropriate output encoding on\n  Windows. (Specifically, the console output encoding is guessed separately from\n  the encoding for command-line arguments.) A bug was also fixed where beets\n  would ignore the locale settings and use UTF-8 by default. :bug:`1419`\n- :doc:`/plugins/discogs`: Better error handling when we can't communicate with\n  Discogs on setup. :bug:`1417`\n- :doc:`/plugins/importadded`: Fix a crash when importing singletons in-place.\n  :bug:`1416`\n- :doc:`/plugins/fuzzy`: Fix a regression causing a crash in the last release.\n  :bug:`1422`\n- Fix a crash when the importer cannot open its log file. Thanks to\n  :user:`barsanuphe`. :bug:`1426`\n- Fix an error when trying to write tags for items with flexible fields called\n  ``date`` and ``original_date`` (which are not built-in beets fields).\n  :bug:`1404`\n\n.. _jellyfish: https://github.com/jamesturk/jellyfish\n\n1.3.11 (April 5, 2015)\n----------------------\n\nIn this release, we refactored the logging system to be more flexible and more\nuseful. There are more granular levels of verbosity, the output from plugins\nshould be more consistent, and several kinds of logging bugs should be\nimpossible in the future.\n\nThere are also two new plugins: one for filtering the files you import and an\nevolved plugin for using album art as directory thumbnails in file managers.\nThere's a new source for album art, and the importer now records the source of\nmatch data. This is a particularly huge release---there's lots more below.\n\nThere's one big change with this release: **Python 2.6 is no longer supported**.\nYou'll need Python 2.7. Please trust us when we say this let us remove a\nsurprising number of ugly hacks throughout the code.\n\nMajor new features and bigger changes\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- There are now **multiple levels of output verbosity**. On the command line,\n  you can make beets somewhat verbose with ``-v`` or very verbose with ``-vv``.\n  For the importer especially, this makes the first verbose mode much more\n  manageable, while still preserving an option for overwhelmingly verbose debug\n  output. :bug:`1244`\n- A new :doc:`/plugins/filefilter` lets you write regular expressions to\n  automatically **avoid importing** certain files. Thanks to :user:`mried`.\n  :bug:`1186`\n- A new :doc:`/plugins/thumbnails` generates cover-art **thumbnails for album\n  folders** for Freedesktop.org-compliant file managers. (This replaces the\n  :doc:`/plugins/freedesktop`, which only worked with the Dolphin file manager.)\n- :doc:`/plugins/replaygain`: There is a new backend that uses the bs1770gain_\n  analysis tool. Thanks to :user:`jmwatte`. :bug:`1343`\n- A new ``filesize`` field on items indicates the number of bytes in the file.\n  :bug:`1291`\n- A new :conf:`plugins.index:search_limit` configuration option allows you to\n  specify how many search results you wish to see when looking up releases at\n  MusicBrainz during import. :bug:`1245`\n- The importer now records the data source for a match in a new flexible\n  attribute ``data_source`` on items and albums. :bug:`1311`\n- The colors used in the terminal interface are now configurable via the new\n  config option ``colors``, nested under the option ``ui``. (Also, the ``color``\n  config option has been moved from top-level to under ``ui``. Beets will\n  respect the old color setting, but will warn the user with a deprecation\n  message.) :bug:`1238`\n- :doc:`/plugins/fetchart`: There's a new Wikipedia image source that uses\n  DBpedia to find albums. Thanks to Tom Jaspers. :bug:`1194`\n- In the :ref:`config-cmd` command, the output is now redacted by default.\n  Sensitive information like passwords and API keys is not included. The new\n  ``--clear`` option disables redaction. :bug:`1376`\n\nYou should probably also know about these core changes to the way beets works\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- As mentioned above, Python 2.6 is no longer supported.\n- The ``tracktotal`` attribute is now a *track-level field* instead of an\n  album-level one. This field stores the total number of tracks on the album, or\n  if the :ref:`per_disc_numbering` config option is set, the total number of\n  tracks on a particular medium (i.e., disc). The field was causing problems\n  with that :ref:`per_disc_numbering` mode: different discs on the same album\n  needed different track totals. The field can now work correctly in either\n  mode.\n- To replace ``tracktotal`` as an album-level field, there is a new\n  ``albumtotal`` computed attribute that provides the total number of tracks on\n  the album. (The :ref:`per_disc_numbering` option has no influence on this\n  field.)\n- The ``list_format_album`` and ``list_format_item`` configuration keys now\n  affect (almost) every place where objects are printed and logged. (Previously,\n  they only controlled the :ref:`list-cmd` command and a few other scattered\n  pieces.) :bug:`1269`\n- Relatedly, the ``beet`` program now accept top-level options ``--format-item``\n  and ``--format-album`` before any subcommand to control how items and albums\n  are displayed. :bug:`1271`\n- ``list_format_album`` and ``list_format_album`` have respectively been renamed\n  :ref:`format_album` and :ref:`format_item`. The old names still work but each\n  triggers a warning message. :bug:`1271`\n- :ref:`Path queries <pathquery>` are automatically triggered only if the path\n  targeted by the query exists. Previously, just having a slash somewhere in the\n  query was enough, so ``beet ls AC/DC`` wouldn't work to refer to the artist.\n\nThere are also lots of medium-sized features in this update\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- :doc:`/plugins/duplicates`: The command has a new ``--strict`` option that\n  will only report duplicates if all attributes are explicitly set. :bug:`1000`\n- :doc:`/plugins/smartplaylist`: Playlist updating should now be faster: the\n  plugin detects, for each playlist, whether it needs to be regenerated, instead\n  of obliviously regenerating all of them. The ``splupdate`` command can now\n  also take additional parameters that indicate the names of the playlists to\n  regenerate.\n- :doc:`/plugins/play`: The command shows the output of the underlying player\n  command and lets you interact with it. :bug:`1321`\n- The summary shown to compare duplicate albums during import now displays the\n  old and new filesizes. :bug:`1291`\n- :doc:`/plugins/lastgenre`: Add *comedy*, *humor*, and *stand-up* as well as a\n  longer list of classical music genre tags to the built-in whitelist and\n  canonicalization tree. :bug:`1206` :bug:`1239` :bug:`1240`\n- :doc:`/plugins/web`: Add support for *cross-origin resource sharing* for more\n  flexible in-browser clients. Thanks to Andre Miller. :bug:`1236` :bug:`1237`\n- :doc:`plugins/mbsync`: A new ``-f/--format`` option controls the output format\n  when listing unrecognized items. The output is also now more helpful by\n  default. :bug:`1246`\n- :doc:`/plugins/fetchart`: A new option, ``-n``, extracts the cover art of all\n  matched albums into their respective directories. Another new flag, ``-a``,\n  associates the extracted files with the albums in the database. :bug:`1261`\n- :doc:`/plugins/info`: A new option, ``-i``, can display only a specified\n  subset of properties. :bug:`1287`\n- The number of missing/unmatched tracks is shown during import. :bug:`1088`\n- :doc:`/plugins/permissions`: The plugin now also adjusts the permissions of\n  the directories. (Previously, it only affected files.) :bug:`1308` :bug:`1324`\n- :doc:`/plugins/ftintitle`: You can now configure the format that the plugin\n  uses to add the artist to the title. Thanks to :user:`amishb`. :bug:`1377`\n\nAnd many little fixes and improvements\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- :doc:`/plugins/replaygain`: Stop applying replaygain directly to source files\n  when using the mp3gain backend. :bug:`1316`\n- Path queries are case-sensitive on non-Windows OSes. :bug:`1165`\n- :doc:`/plugins/lyrics`: Silence a warning about insecure requests in the new\n  MusixMatch backend. :bug:`1204`\n- Fix a crash when ``beet`` is invoked without arguments. :bug:`1205`\n  :bug:`1207`\n- :doc:`/plugins/fetchart`: Do not attempt to import directories as album art.\n  :bug:`1177` :bug:`1211`\n- :doc:`/plugins/mpdstats`: Avoid double-counting some play events. :bug:`773`\n  :bug:`1212`\n- Fix a crash when the importer deals with Unicode metadata in ``--pretend``\n  mode. :bug:`1214`\n- :doc:`/plugins/smartplaylist`: Fix ``album_query`` so that individual files\n  are added to the playlist instead of directories. :bug:`1225`\n- Remove the ``beatport`` plugin. Beatport_ has shut off public access to their\n  API and denied our request for an account. We have not heard from the company\n  since 2013, so we are assuming access will not be restored.\n- Incremental imports now (once again) show a \"skipped N directories\" message.\n- :doc:`/plugins/embedart`: Handle errors in ImageMagick's output. :bug:`1241`\n- :doc:`/plugins/keyfinder`: Parse the underlying tool's output more robustly.\n  :bug:`1248`\n- :doc:`/plugins/embedart`: We now show a comprehensible error message when\n  ``beet embedart -f FILE`` is given a non-existent path. :bug:`1252`\n- Fix a crash when a file has an unrecognized image type tag. Thanks to Matthias\n  Kiefer. :bug:`1260`\n- :doc:`/plugins/importfeeds` and :doc:`/plugins/smartplaylist`: Automatically\n  create parent directories for playlist files (instead of crashing when the\n  parent directory does not exist). :bug:`1266`\n- The :ref:`write-cmd` command no longer tries to \"write\" non-writable fields,\n  such as the bitrate. :bug:`1268`\n- The error message when MusicBrainz is not reachable on the network is now much\n  clearer. Thanks to Tom Jaspers. :bug:`1190` :bug:`1272`\n- Improve error messages when parsing query strings with shlex. :bug:`1290`\n- :doc:`/plugins/embedart`: Fix a crash that occurred when used together with\n  the *check* plugin. :bug:`1241`\n- :doc:`/plugins/scrub`: Log an error instead of stopping when the ``beet\n  scrub`` command cannot write a file. Also, avoid problems on Windows with\n  Unicode filenames. :bug:`1297`\n- :doc:`/plugins/discogs`: Handle and log more kinds of communication errors.\n  :bug:`1299` :bug:`1305`\n- :doc:`/plugins/lastgenre`: Bugs in the ``pylast`` library can no longer crash\n  beets.\n- :doc:`/plugins/convert`: You can now configure the temporary directory for\n  conversions. Thanks to :user:`autochthe`. :bug:`1382` :bug:`1383`\n- :doc:`/plugins/rewrite`: Fix a regression that prevented the plugin's\n  rewriting from applying to album-level fields like ``$albumartist``.\n  :bug:`1393`\n- :doc:`/plugins/play`: The plugin now sorts items according to the\n  configuration in album mode.\n- :doc:`/plugins/fetchart`: The name for extracted art files is taken from the\n  ``art_filename`` configuration option. :bug:`1258`\n- When there's a parse error in a query (for example, when you type a malformed\n  date in a :ref:`date query <datequery>`), beets now stops with an error\n  instead of silently ignoring the query component.\n- :doc:`/plugins/smartplaylist`: Stream-friendly smart playlists. The\n  ``splupdate`` command can now also add a URL-encodable prefix to every path in\n  the playlist file.\n\nFor developers\n~~~~~~~~~~~~~~\n\n- The ``database_change`` event now sends the item or album that is subject to a\n  change.\n- The ``OptionParser`` is now a ``CommonOptionsParser`` that offers facilities\n  for adding usual options (``--album``, ``--path`` and ``--format``). See\n  :ref:`add_subcommands`. :bug:`1271`\n- The logging system in beets has been overhauled. Plugins now each have their\n  own logger, which helps by automatically adjusting the verbosity level in\n  import mode and by prefixing the plugin's name. Logging levels are dynamically\n  set when a plugin is called, depending on how it is called (import stage,\n  event or direct command). Finally, logging calls can (and should!) use modern\n  ``{}``-style string formatting lazily. See :ref:`plugin-logging` in the plugin\n  API docs.\n- A new ``import_task_created`` event lets you manipulate import tasks\n  immediately after they are initialized. It's also possible to replace the\n  originally created tasks by returning new ones using this event.\n\n.. _bs1770gain: https://sourceforge.net/projects/bs1770gain/\n\n1.3.10 (January 5, 2015)\n------------------------\n\nThis version adds a healthy helping of new features and fixes a critical\nMPEG-4--related bug. There are more lyrics sources, there new plugins for\nmanaging permissions and integrating with Plex_, and the importer has a new\n``--pretend`` flag that shows which music *would* be imported.\n\nOne backwards-compatibility note: the :doc:`/plugins/lyrics` now requires the\nrequests_ library. If you use this plugin, you will need to install the library\nby typing ``pip install requests`` or the equivalent for your OS.\n\nAlso, as an advance warning, this will be one of the last releases to support\nPython 2.6. If you have a system that cannot run Python 2.7, please consider\nupgrading soon.\n\nThe new features are\n~~~~~~~~~~~~~~~~~~~~\n\n- A new :doc:`/plugins/permissions` makes it easy to fix permissions on music\n  files as they are imported. Thanks to :user:`xsteadfastx`. :bug:`1098`\n- A new :doc:`/plugins/plexupdate` lets you notify a Plex_ server when the\n  database changes. Thanks again to xsteadfastx. :bug:`1120`\n- The :ref:`import-cmd` command now has a ``--pretend`` flag that lists the\n  files that will be imported. Thanks to :user:`mried`. :bug:`1162`\n- :doc:`/plugins/lyrics`: Add Musixmatch_ source and introduce a new ``sources``\n  config option that lets you choose exactly where to look for lyrics and in\n  which order.\n- :doc:`/plugins/lyrics`: Add Brazilian and Spanish sources to Google custom\n  search engine.\n- Add a warning when importing a directory that contains no music. :bug:`1116`\n  :bug:`1127`\n- :doc:`/plugins/zero`: Can now remove embedded images. :bug:`1129` :bug:`1100`\n- The :ref:`config-cmd` command can now be used to edit the configuration even\n  when it has syntax errors. :bug:`1123` :bug:`1128`\n- :doc:`/plugins/lyrics`: Added a new ``force`` config option. :bug:`1150`\n\nAs usual, there are loads of little fixes and improvements\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- Fix a new crash with the latest version of Mutagen (1.26).\n- :doc:`/plugins/lyrics`: Avoid fetching truncated lyrics from the Google backed\n  by merging text blocks separated by empty ``<div>`` tags before scraping.\n- We now print a better error message when the database file is corrupted.\n- :doc:`/plugins/discogs`: Only prompt for authentication when running the\n  :ref:`import-cmd` command. :bug:`1123`\n- When deleting fields with the :ref:`modify-cmd` command, do not crash when the\n  field cannot be removed (i.e., when it does not exist, when it is a built-in\n  field, or when it is a computed field). :bug:`1124`\n- The deprecated ``echonest_tempo`` plugin has been removed. Please use the\n  ``echonest`` plugin instead.\n- ``echonest`` plugin: Fingerprint-based lookup has been removed in accordance\n  with `API changes`_. :bug:`1121`\n- ``echonest`` plugin: Avoid a crash when the song has no duration information.\n  :bug:`896`\n- :doc:`/plugins/lyrics`: Avoid a crash when retrieving non-ASCII lyrics from\n  the Google backend. :bug:`1135` :bug:`1136`\n- :doc:`/plugins/smartplaylist`: Sort specifiers are now respected in queries.\n  Thanks to :user:`djl`. :bug:`1138` :bug:`1137`\n- :doc:`/plugins/ftintitle` and :doc:`/plugins/lyrics`: Featuring artists can\n  now be detected when they use the Spanish word *con*. :bug:`1060` :bug:`1143`\n- :doc:`/plugins/mbcollection`: Fix an \"HTTP 400\" error caused by a change in\n  the MusicBrainz API. :bug:`1152`\n- The ``%`` and ``_`` characters in path queries do not invoke their special SQL\n  meaning anymore. :bug:`1146`\n- :doc:`/plugins/convert`: Command-line argument construction now works on\n  Windows. Thanks to :user:`mluds`. :bug:`1026` :bug:`1157` :bug:`1158`\n- :doc:`/plugins/embedart`: Fix an erroneous missing-art error on Windows.\n  Thanks to :user:`mluds`. :bug:`1163`\n- :doc:`/plugins/importadded`: Now works with in-place and symlinked imports.\n  :bug:`1170`\n- :doc:`/plugins/ftintitle`: The plugin is now quiet when it runs as part of the\n  import process. Thanks to :user:`Freso`. :bug:`1176` :bug:`1172`\n- :doc:`/plugins/ftintitle`: Fix weird behavior when the same artist appears\n  twice in the artist string. Thanks to Marc Addeo. :bug:`1179` :bug:`1181`\n- :doc:`/plugins/lastgenre`: Match songs more robustly when they contain dashes.\n  Thanks to :user:`djl`. :bug:`1156`\n- The :ref:`config-cmd` command can now use ``$EDITOR`` variables with\n  arguments.\n\n.. _api changes: https://web.archive.org/web/20160814092627/https://developer.echonest.com/forums/thread/3650\n\n.. _musixmatch: https://www.musixmatch.com/\n\n.. _plex: https://watch.plex.tv/\n\n1.3.9 (November 17, 2014)\n-------------------------\n\nThis release adds two new standard plugins to beets: one for synchronizing\nLast.fm listening data and one for integrating with Linux desktops. And at long\nlast, imports can now create symbolic links to music files instead of copying or\nmoving them. We also gained the ability to search for album art on the iTunes\nStore and a new way to compute ReplayGain levels.\n\nThe major new features are\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- A new :doc:`/plugins/lastimport` lets you download your play count data from\n  Last.fm into a flexible attribute. Thanks to Rafael Bodill.\n- A new :doc:`/plugins/freedesktop` creates metadata files for\n  Freedesktop.org--compliant file managers. Thanks to :user:`kerobaros`.\n  :bug:`1056`, :bug:`707`\n- A new :ref:`link` option in the ``import`` section creates symbolic links\n  during import instead of moving or copying. Thanks to Rovanion Luckey.\n  :bug:`710`, :bug:`114`\n- :doc:`/plugins/fetchart`: You can now search for art on the iTunes Store.\n  There's also a new ``sources`` config option that lets you choose exactly\n  where to look for images and in which order.\n- :doc:`/plugins/replaygain`: A new Python Audio Tools backend was added. Thanks\n  to Francesco Rubino. :bug:`1070`\n- :doc:`/plugins/embedart`: You can now automatically check that new art looks\n  similar to existing art---ensuring that you only get a better \"version\" of the\n  art you already have. See :ref:`image-similarity-check`.\n- :doc:`/plugins/ftintitle`: The plugin now runs automatically on import. To\n  disable this, unset the ``auto`` config flag.\n\nThere are also core improvements and other substantial additions\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- The ``media`` attribute is now a *track-level field* instead of an album-level\n  one. This field stores the delivery mechanism for the music, so in its\n  album-level incarnation, it could not represent heterogeneous releases---for\n  example, an album consisting of a CD and a DVD. Now, tracks accurately\n  indicate the media they appear on. Thanks to Heinz Wiesinger.\n- Re-imports of your existing music (see :ref:`reimport`) now preserve its added\n  date and flexible attributes. Thanks to Stig Inge Lea Bjørnsen.\n- Slow queries, such as those over flexible attributes, should now be much\n  faster when used with certain commands---notably, the :doc:`/plugins/play`.\n- :doc:`/plugins/bpd`: Add a new configuration option for setting the default\n  volume. Thanks to IndiGit.\n- :doc:`/plugins/embedart`: A new ``ifempty`` config option lets you only embed\n  album art when no album art is present. Thanks to kerobaros.\n- :doc:`/plugins/discogs`: Authenticate with the Discogs server. The plugin now\n  requires a Discogs account due to new API restrictions. Thanks to\n  :user:`multikatt`. :bug:`1027`, :bug:`1040`\n\nAnd countless little improvements and fixes\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- Standard cover art in APEv2 metadata is now supported. Thanks to Matthias\n  Kiefer. :bug:`1042`\n- :doc:`/plugins/convert`: Avoid a crash when embedding cover art fails.\n- :doc:`/plugins/mpdstats`: Fix an error on start (introduced in the previous\n  version). Thanks to Zach Denton.\n- :doc:`/plugins/convert`: The ``--yes`` command-line flag no longer expects an\n  argument.\n- :doc:`/plugins/play`: Remove the temporary .m3u file after sending it to the\n  player.\n- The importer no longer tries to highlight partial differences in numeric\n  quantities (track numbers and durations), which was often confusing.\n- Date-based queries that are malformed (not parse-able) no longer crash beets\n  and instead fail silently.\n- :doc:`/plugins/duplicates`: Emit an error when the ``checksum`` config option\n  is set incorrectly.\n- The migration from pre-1.1, non-YAML configuration files has been removed. If\n  you need to upgrade an old config file, use an older version of beets\n  temporarily.\n- :doc:`/plugins/discogs`: Recover from HTTP errors when communicating with the\n  Discogs servers. Thanks to Dustin Rodriguez.\n- :doc:`/plugins/embedart`: Do not log \"embedding album art into...\" messages\n  during the import process.\n- Fix a crash in the autotagger when files had only whitespace in their\n  metadata.\n- :doc:`/plugins/play`: Fix a potential crash when the command outputs special\n  characters. :bug:`1041`\n- :doc:`/plugins/web`: Queries typed into the search field are now treated as\n  separate query components. :bug:`1045`\n- Date tags that use slashes instead of dashes as separators are now interpreted\n  correctly. And WMA (ASF) files now map the ``comments`` field to the\n  \"Description\" tag (in addition to \"WM/Comments\"). Thanks to Matthias Kiefer.\n  :bug:`1043`\n- :doc:`/plugins/embedart`: Avoid resizing the image multiple times when\n  embedding into an album. Thanks to :user:`kerobaros`. :bug:`1028`, :bug:`1036`\n- :doc:`/plugins/discogs`: Avoid a situation where a trailing comma could be\n  appended to some artist names. :bug:`1049`\n- The output of the :ref:`stats-cmd` command is slightly different: the\n  approximate size is now marked as such, and the total number of seconds only\n  appears in exact mode.\n- :doc:`/plugins/convert`: A new ``copy_album_art`` option puts images alongside\n  converted files. Thanks to Ángel Alonso. :bug:`1050`, :bug:`1055`\n- There is no longer a \"conflict\" between two plugins that declare the same\n  field with the same type. Thanks to Peter Schnebel. :bug:`1059` :bug:`1061`\n- :doc:`/plugins/chroma`: Limit the number of releases and recordings fetched as\n  the result of an Acoustid match to avoid extremely long processing times for\n  very popular music. :bug:`1068`\n- Fix an issue where modifying an album's field without actually changing it\n  would not update the corresponding tracks to bring differing tracks back in\n  line with the album. :bug:`856`\n- ``echonest`` plugin: When communicating with the Echo Nest servers fails\n  repeatedly, log an error instead of exiting. :bug:`1096`\n- :doc:`/plugins/lyrics`: Avoid an error when the Google source returns a result\n  without a title. Thanks to Alberto Leal. :bug:`1097`\n- Importing an archive will no longer leave temporary files behind in ``/tmp``.\n  Thanks to :user:`multikatt`. :bug:`1067`, :bug:`1091`\n\n1.3.8 (September 17, 2014)\n--------------------------\n\nThis release has two big new chunks of functionality. Queries now support\n**sorting** and user-defined fields can now have **types**.\n\nIf you want to see all your songs in reverse chronological order, just type\n``beet list year-``. It couldn't be easier. For details, see :ref:`query-sort`.\n\nFlexible field types mean that some functionality that has previously only\nworked for built-in fields, like range queries, can now work with plugin- and\nuser-defined fields too. For starters, the ``echonest`` plugin and\n:doc:`/plugins/mpdstats` now mark the types of the fields they provide---so you\ncan now say, for example, ``beet ls liveness:0.5..1.5`` for the Echo Nest\n\"liveness\" attribute. The :doc:`/plugins/types` makes it easy to specify field\ntypes in your config file.\n\nOne upgrade note: if you use the :doc:`/plugins/discogs`, you will need to\nupgrade the Discogs client library to use this version. Just type ``pip install\n-U discogs-client``.\n\nOther new features\n~~~~~~~~~~~~~~~~~~\n\n- :doc:`/plugins/info`: Target files can now be specified through library\n  queries (in addition to filenames). The ``--library`` option prints library\n  fields instead of tags. Multiple files can be summarized together with the new\n  ``--summarize`` option.\n- :doc:`/plugins/mbcollection`: A new option lets you automatically update your\n  collection on import. Thanks to Olin Gay.\n- :doc:`/plugins/convert`: A new ``never_convert_lossy_files`` option can\n  prevent lossy transcoding. Thanks to Simon Kohlmeyer.\n- :doc:`/plugins/convert`: A new ``--yes`` command-line flag skips the\n  confirmation.\n\nStill more fixes and little improvements\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- Invalid state files don't crash the importer.\n- :doc:`/plugins/lyrics`: Only strip featured artists and parenthesized title\n  suffixes if no lyrics for the original artist and title were found.\n- Fix a crash when reading some files with missing tags.\n- :doc:`/plugins/discogs`: Compatibility with the new 2.0 version of the\n  discogs_client_ Python library. If you were using the old version, you will\n  need to upgrade to the latest version of the library to use the\n  correspondingly new version of the plugin (e.g., with ``pip install -U\n  discogs-client``). Thanks to Andriy Kohut.\n- Fix a crash when writing files that can't be read. Thanks to Jocelyn De La\n  Rosa.\n- The :ref:`stats-cmd` command now counts album artists. The album count also\n  more accurately reflects the number of albums in the database.\n- :doc:`/plugins/convert`: Avoid crashes when tags cannot be written to newly\n  converted files.\n- Formatting templates with item data no longer confusingly shows album-level\n  data when the two are inconsistent.\n- Resuming imports and beginning incremental imports should now be much faster\n  when there is a lot of previously-imported music to skip.\n- :doc:`/plugins/lyrics`: Remove ``<script>`` tags from scraped lyrics. Thanks\n  to Bombardment.\n- :doc:`/plugins/play`: Add a ``relative_to`` config option. Thanks to\n  BrainDamage.\n- Fix a crash when a MusicBrainz release has zero tracks.\n- The ``--version`` flag now works as an alias for the ``version`` command.\n- :doc:`/plugins/lastgenre`: Remove some unhelpful genres from the default\n  whitelist. Thanks to gwern.\n- :doc:`/plugins/importfeeds`: A new ``echo`` output mode prints files' paths to\n  standard error. Thanks to robotanarchy.\n- :doc:`/plugins/replaygain`: Restore some error handling when ``mp3gain``\n  output cannot be parsed. The verbose log now contains the bad tool output in\n  this case.\n- :doc:`/plugins/convert`: Fix filename extensions when converting\n  automatically.\n- The ``write`` plugin event allows plugins to change the tags that are written\n  to a media file.\n- :doc:`/plugins/zero`: Do not delete database values; only media file tags are\n  affected.\n\n.. _discogs_client: https://github.com/discogs/discogs_client\n\n1.3.7 (August 22, 2014)\n-----------------------\n\nThis release of beets fixes all the bugs, and you can be confident that you will\nnever again find any bugs in beets, ever. It also adds support for plain old\nAIFF files and adds three more plugins, including a nifty one that lets you\nmeasure a song's tempo by tapping out the beat on your keyboard. The importer\ndeals more elegantly with duplicates and you can broaden your cover art search\nto the entire web with Google Image Search.\n\nThe big new features are\n~~~~~~~~~~~~~~~~~~~~~~~~\n\n- Support for AIFF files. Tags are stored as ID3 frames in one of the file's IFF\n  chunks. Thanks to Evan Purkhiser for contributing support to Mutagen_.\n- The new :doc:`/plugins/importadded` reads files' modification times to set\n  their \"added\" date. Thanks to Stig Inge Lea Bjørnsen.\n- The new :doc:`/plugins/bpm` lets you manually measure the tempo of a playing\n  song. Thanks to aroquen.\n- The new :doc:`/plugins/spotify` generates playlists for your Spotify_ account.\n  Thanks to Olin Gay.\n- A new :ref:`required` configuration option for the importer skips matches that\n  are missing certain data. Thanks to oprietop.\n- When the importer detects duplicates, it now shows you some details about the\n  potentially-replaced music so you can make an informed decision. Thanks to\n  Howard Jones.\n- :doc:`/plugins/fetchart`: You can now optionally search for cover art on\n  Google Image Search. Thanks to Lemutar.\n- A new :ref:`asciify-paths` configuration option replaces all non-ASCII\n  characters in paths.\n\n.. _mutagen: https://github.com/quodlibet/mutagen\n\n.. _spotify: https://open.spotify.com/\n\nAnd the multitude of little improvements and fixes\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- Compatibility with the latest version of Mutagen_, 1.23.\n- :doc:`/plugins/web`: Lyrics now display readably with correct line breaks.\n  Also, the detail view scrolls to reveal all of the lyrics. Thanks to Meet\n  Udeshi.\n- :doc:`/plugins/play`: The ``command`` config option can now contain arguments\n  (rather than just an executable). Thanks to Alessandro Ghedini.\n- Fix an error when using the :ref:`modify-cmd` command to remove a flexible\n  attribute. Thanks to Pierre Rust.\n- :doc:`/plugins/info`: The command now shows audio properties (e.g., bitrate)\n  in addition to metadata. Thanks Alessandro Ghedini.\n- Avoid a crash on Windows when writing to files with special characters in\n  their names.\n- :doc:`/plugins/play`: Playing albums now generates filenames by default (as\n  opposed to directories) for better compatibility. The ``use_folders`` option\n  restores the old behavior. Thanks to Lucas Duailibe.\n- Fix an error when importing an empty directory with the ``--flat`` option.\n- :doc:`/plugins/mpdstats`: The last song in a playlist is now correctly counted\n  as played. Thanks to Johann Klähn.\n- :doc:`/plugins/zero`: Prevent accidental nulling of dangerous fields (IDs and\n  paths). Thanks to brunal.\n- The :ref:`remove-cmd` command now shows the paths of files that will be\n  deleted. Thanks again to brunal.\n- Don't display changes for fields that are not in the restricted field set.\n  This fixes :ref:`write-cmd` showing changes for fields that are not written to\n  the file.\n- The :ref:`write-cmd` command avoids displaying the item name if there are no\n  changes for it.\n- When using both the :doc:`/plugins/convert` and the :doc:`/plugins/scrub`,\n  avoid scrubbing the source file of conversions. (Fix a regression introduced\n  in the previous release.)\n- :doc:`/plugins/replaygain`: Logging is now quieter during import. Thanks to\n  Yevgeny Bezman.\n- :doc:`/plugins/fetchart`: When loading art from the filesystem, we now\n  prioritize covers with more keywords in them. This means that\n  ``cover-front.jpg`` will now be taken before ``cover-back.jpg`` because it\n  contains two keywords rather than one. Thanks to Fabrice Laporte.\n- :doc:`/plugins/lastgenre`: Remove duplicates from canonicalized genre lists.\n  Thanks again to Fabrice Laporte.\n- The importer now records its progress when skipping albums. This means that\n  incremental imports will no longer try to import albums again after you've\n  chosen to skip them, and erroneous invitations to resume \"interrupted\" imports\n  should be reduced. Thanks to jcassette.\n- :doc:`/plugins/bucket`: You can now customize the definition of alphanumeric\n  \"ranges\" using regular expressions. And the heuristic for detecting years has\n  been improved. Thanks to sotho.\n- Already-imported singleton tracks are skipped when resuming an import.\n- :doc:`/plugins/chroma`: A new ``auto`` configuration option disables\n  fingerprinting on import. Thanks to ddettrittus.\n- :doc:`/plugins/convert`: A new ``--format`` option to can select the\n  transcoding preset from the command-line.\n- :doc:`/plugins/convert`: Transcoding presets can now omit their filename\n  extensions (extensions default to the name of the preset).\n- :doc:`/plugins/convert`: A new ``--pretend`` option lets you preview the\n  commands the plugin will execute without actually taking any action. Thanks to\n  Dietrich Daroch.\n- Fix a crash when a float-valued tag field only contained a ``+`` or ``-``\n  character.\n- Fixed a regression in the core that caused the :doc:`/plugins/scrub` not to\n  work in ``auto`` mode. Thanks to Harry Khanna.\n- The :ref:`write-cmd` command now has a ``--force`` flag. Thanks again to Harry\n  Khanna.\n- :doc:`/plugins/mbsync`: Track alignment now works with albums that have\n  multiple copies of the same recording. Thanks to Rui Gonçalves.\n\n1.3.6 (May 10, 2014)\n--------------------\n\nThis is primarily a bugfix release, but it also brings two new plugins: one for\nplaying music in desktop players and another for organizing your directories\ninto \"buckets.\" It also brings huge performance optimizations to queries---your\n``beet ls`` commands will now go much faster.\n\nNew features\n~~~~~~~~~~~~\n\n- The new :doc:`/plugins/play` lets you start your desktop music player with the\n  songs that match a query. Thanks to David Hamp-Gonsalves.\n- The new :doc:`/plugins/bucket` provides a ``%bucket{}`` function for path\n  formatting to generate folder names representing ranges of years or initial\n  letter. Thanks to Fabrice Laporte.\n- Item and album queries are much faster.\n- :doc:`/plugins/ftintitle`: A new option lets you remove featured artists\n  entirely instead of moving them to the title. Thanks to SUTJael.\n\nAnd those all-important bug fixes\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- :doc:`/plugins/mbsync`: Fix a regression in 1.3.5 that broke the plugin\n  entirely.\n- :ref:`Shell completion <completion>` now searches more common paths for its\n  ``bash_completion`` dependency.\n- Fix encoding-related logging errors in :doc:`/plugins/convert` and\n  :doc:`/plugins/replaygain`.\n- :doc:`/plugins/replaygain`: Suppress a deprecation warning emitted by later\n  versions of PyGI.\n- Fix a crash when reading files whose iTunes SoundCheck tags contain non-ASCII\n  characters.\n- The ``%if{}`` template function now appropriately interprets the condition as\n  false when it contains the string \"false\". Thanks to Ayberk Yilmaz.\n- :doc:`/plugins/convert`: Fix conversion for files that include a video stream\n  by ignoring it. Thanks to brunal.\n- :doc:`/plugins/fetchart`: Log an error instead of crashing when tag\n  manipulation fails.\n- :doc:`/plugins/convert`: Log an error instead of crashing when embedding album\n  art fails.\n- :doc:`/plugins/convert`: Embed cover art into converted files. Previously they\n  were embedded into the source files.\n- New plugin event: ``before_item_moved``. Thanks to Robert Speicher.\n\n1.3.5 (April 15, 2014)\n----------------------\n\nThis is a short-term release that adds some great new stuff to beets. There's\nsupport for tracking and calculating musical keys, the ReplayGain plugin was\nexpanded to work with more music formats via GStreamer, we can now import\ndirectly from compressed archives, and the lyrics plugin is more robust.\n\nOne note for upgraders and packagers: this version of beets has a new dependency\nin enum34_, which is a backport of the new enum_ standard library module.\n\nThe major new features are\n~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- Beets can now import ``zip``, ``tar``, and ``rar`` archives. Just type ``beet\n  import music.zip`` to have beets transparently extract the files to import.\n- :doc:`/plugins/replaygain`: Added support for calculating ReplayGain values\n  with GStreamer as well the mp3gain program. This enables ReplayGain\n  calculation for any audio format. Thanks to Yevgeny Bezman.\n- :doc:`/plugins/lyrics`: Lyrics should now be found for more songs. Searching\n  is now sensitive to featured artists and parenthesized title suffixes. When a\n  song has multiple titles, lyrics from all the named songs are now\n  concatenated. Thanks to Fabrice Laporte and Paul Phillips.\n\nIn particular, a full complement of features for supporting musical keys are new\nin this release:\n\n- A new ``initial_key`` field is available in the database and files' tags. You\n  can set the field manually using a command like ``beet modify\n  initial_key=Am``.\n- The ``echonest`` plugin sets the ``initial_key`` field if the data is\n  available.\n- A new :doc:`/plugins/keyfinder` runs a command-line tool to get the key from\n  audio data and store it in the ``initial_key`` field.\n\nThere are also many bug fixes and little enhancements\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- ``echonest`` plugin: Truncate files larger than 50MB before uploading for\n  analysis.\n- :doc:`/plugins/fetchart`: Fix a crash when the server does not specify a\n  content type. Thanks to Lee Reinhardt.\n- :doc:`/plugins/convert`: The ``--keep-new`` flag now works correctly and the\n  library includes the converted item.\n- The importer now logs a message instead of crashing when errors occur while\n  opening the files to be imported.\n- :doc:`/plugins/embedart`: Better error messages in exceptional conditions.\n- Silenced some confusing error messages when searching for a non-MusicBrainz\n  ID. Using an invalid ID (of any kind---Discogs IDs can be used there too) at\n  the \"Enter ID:\" importer prompt now just silently returns no results. More\n  info is in the verbose logs.\n- :doc:`/plugins/mbsync`: Fix application of album-level metadata. Due to a\n  regression a few releases ago, only track-level metadata was being updated.\n- On Windows, paths on network shares (UNC paths) no longer cause \"invalid\n  filename\" errors.\n- :doc:`/plugins/replaygain`: Fix crashes when attempting to log errors.\n- The :ref:`modify-cmd` command can now accept query arguments that contain =\n  signs. An argument is considered a query part when a : appears before any =s.\n  Thanks to mook.\n\n.. _enum: https://docs.python.org/3.4/library/enum.html\n\n.. _enum34: https://pypi.org/project/enum34/\n\n1.3.4 (April 5, 2014)\n---------------------\n\nThis release brings a hodgepodge of medium-sized conveniences to beets. A new\n:ref:`config-cmd` command manages your configuration, we now have :ref:`bash\ncompletion <completion>`, and the :ref:`modify-cmd` command can delete\nattributes. There are also some significant performance optimizations to the\nautotagger's matching logic.\n\nOne note for upgraders: if you use the :doc:`/plugins/fetchart`, it has a new\ndependency, the requests_ module.\n\nNew stuff\n~~~~~~~~~\n\n- Added a :ref:`config-cmd` command to manage your configuration. It can show\n  you what you currently have in your config file, point you at where the file\n  should be, or launch your text editor to let you modify the file. Thanks to\n  geigerzaehler.\n- Beets now ships with a shell command completion script! See :ref:`completion`.\n  Thanks to geigerzaehler.\n- The :ref:`modify-cmd` command now allows removing flexible attributes. For\n  example, ``beet modify artist:beatles oldies!`` deletes the ``oldies``\n  attribute from matching items. Thanks to brilnius.\n- Internally, beets has laid the groundwork for supporting multi-valued fields.\n  Thanks to geigerzaehler.\n- The importer interface now shows the URL for MusicBrainz matches. Thanks to\n  johtso.\n- :doc:`/plugins/smartplaylist`: Playlists can now be generated from multiple\n  queries (combined with \"or\" logic). Album-level queries are also now possible\n  and automatic playlist regeneration can now be disabled. Thanks to brilnius.\n- ``echonest`` plugin: Echo Nest similarity now weights the tempo in better\n  proportion to other metrics. Also, options were added to specify custom\n  thresholds and output formats. Thanks to Adam M.\n- Added the :ref:`after_write <plugin_events>` plugin event.\n- :doc:`/plugins/lastgenre`: Separator in genre lists can now be configured.\n  Thanks to brilnius.\n- We now only use \"primary\" aliases for artist names from MusicBrainz. This\n  eliminates some strange naming that could occur when the ``languages`` config\n  option was set. Thanks to Filipe Fortes.\n- The performance of the autotagger's matching mechanism is vastly improved.\n  This should be noticeable when matching against very large releases such as\n  box sets.\n- The :ref:`import-cmd` command can now accept individual files as arguments\n  even in non-singleton mode. Files are imported as one-track albums.\n\nFixes\n~~~~~\n\n- Error messages involving paths no longer escape non-ASCII characters (for\n  legibility).\n- Fixed a regression that made it impossible to use the :ref:`modify-cmd`\n  command to add new flexible fields. Thanks to brilnius.\n- ``echonest`` plugin: Avoid crashing when the audio analysis fails. Thanks to\n  Pedro Silva.\n- :doc:`/plugins/duplicates`: Fix checksumming command execution for files with\n  quotation marks in their names. Thanks again to Pedro Silva.\n- Fix a crash when importing with both of the :ref:`group_albums` and\n  :ref:`incremental` options enabled. Thanks to geigerzaehler.\n- Give a sensible error message when ``BEETSDIR`` points to a file. Thanks again\n  to geigerzaehler.\n- Fix a crash when reading WMA files whose boolean-valued fields contain\n  strings. Thanks to johtso.\n- :doc:`/plugins/fetchart`: The plugin now sends \"beets\" as the User-Agent when\n  making scraping requests. This helps resolve some blocked requests. The plugin\n  now also depends on the requests_ Python library.\n- The :ref:`write-cmd` command now only shows the changes to fields that will\n  actually be written to a file.\n- :doc:`/plugins/duplicates`: Spurious reports are now avoided for tracks with\n  missing values (e.g., no MBIDs). Thanks to Pedro Silva.\n- The default :ref:`replace` sanitation options now remove leading whitespace by\n  default. Thanks to brilnius.\n- :doc:`/plugins/importfeeds`: Fix crash when importing albums containing ``/``\n  with the ``m3u_multi`` format.\n- Avoid crashing on Mutagen bugs while writing files' tags.\n- :doc:`/plugins/convert`: Display a useful error message when the FFmpeg\n  executable can't be found.\n\n.. _requests: https://requests.readthedocs.io/en/latest/\n\n1.3.3 (February 26, 2014)\n-------------------------\n\nVersion 1.3.3 brings a bunch changes to how item and album fields work\ninternally. Along with laying the groundwork for some great things in the\nfuture, this brings a number of improvements to how you interact with beets.\nHere's what's new with fields in particular:\n\n- Plugin-provided fields can now be used in queries. For example, if you use the\n  :doc:`/plugins/inline` to define a field called ``era``, you can now filter\n  your library based on that field by typing something like ``beet list\n  era:goldenage``.\n- Album-level flexible attributes and plugin-provided attributes can now be used\n  in path formats (and other item-level templates).\n- :ref:`Date-based queries <datequery>` are now possible. Try getting every\n  track you added in February 2014 with ``beet ls added:2014-02`` or in the\n  whole decade with ``added:2010..``. Thanks to Stig Inge Lea Bjørnsen.\n- The :ref:`modify-cmd` command is now better at parsing and formatting fields.\n  You can assign to boolean fields like ``comp``, for example, using either the\n  words \"true\" or \"false\" or the numerals 1 and 0. Any boolean-esque value is\n  normalized to a real boolean. The :ref:`update-cmd` and :ref:`write-cmd`\n  commands also got smarter at formatting and colorizing changes.\n\nFor developers, the short version of the story is that Item and Album objects\nprovide *uniform access* across fixed, flexible, and computed attributes. You\ncan write ``item.foo`` to access the ``foo`` field without worrying about where\nthe data comes from.\n\nUnrelated new stuff\n~~~~~~~~~~~~~~~~~~~\n\n- The importer has a new interactive option (*G* for \"Group albums\"),\n  command-line flag (``--group-albums``), and config option\n  (:ref:`group_albums`) that lets you split apart albums that are mixed together\n  in a single directory. Thanks to geigerzaehler.\n- A new ``--config`` command-line option lets you specify an additional\n  configuration file. This option *combines* config settings with your default\n  config file. (As part of this change, the ``BEETSDIR`` environment variable no\n  longer combines---it *replaces* your default config file.) Thanks again to\n  geigerzaehler.\n- :doc:`/plugins/ihate`: The plugin's configuration interface was overhauled.\n  Its configuration is now much simpler---it uses beets queries instead of an\n  ad-hoc per-field configuration. This is *backwards-incompatible*---if you use\n  this plugin, you will need to update your configuration. Thanks to\n  BrainDamage.\n\nOther little fixes\n~~~~~~~~~~~~~~~~~~\n\n- ``echonest`` plugin: Tempo (BPM) is now always stored as an integer. Thanks to\n  Heinz Wiesinger.\n- Fix Python 2.6 compatibility in some logging statements in\n  :doc:`/plugins/chroma` and :doc:`/plugins/lastgenre`.\n- Prevent some crashes when things go really wrong when writing file metadata at\n  the end of the import process.\n- New plugin events: ``item_removed`` (thanks to Romuald Conty) and\n  ``item_copied`` (thanks to Stig Inge Lea Bjørnsen).\n- The ``pluginpath`` config option can now point to the directory containing\n  plugin code. (Previously, it awkwardly needed to point at a directory\n  containing a ``beetsplug`` directory, which would then contain your code. This\n  is preserved as an option for backwards compatibility.) This change should\n  also work around a long-standing issue when using ``pluginpath`` when beets is\n  installed using pip. Many thanks to geigerzaehler.\n- :doc:`/plugins/web`: The ``/item/`` and ``/album/`` API endpoints now produce\n  full details about albums and items, not just lists of IDs. Thanks to\n  geigerzaehler.\n- Fix a potential crash when using image resizing with the\n  :doc:`/plugins/fetchart` or :doc:`/plugins/embedart` without ImageMagick\n  installed.\n- Also, when invoking ``convert`` for image resizing fails, we now log an error\n  instead of crashing.\n- :doc:`/plugins/fetchart`: The ``beet fetchart`` command can now associate\n  local images with albums (unless ``--force`` is provided). Thanks to brilnius.\n- :doc:`/plugins/fetchart`: Command output is now colorized. Thanks again to\n  brilnius.\n- The :ref:`modify-cmd` command avoids writing files and committing to the\n  database when nothing has changed. Thanks once more to brilnius.\n- The importer now uses the album artist field when guessing existing metadata\n  for albums (rather than just the track artist field). Thanks to geigerzaehler.\n- :doc:`/plugins/fromfilename`: Fix a crash when a filename contained only a\n  track number (e.g., ``02.mp3``).\n- :doc:`/plugins/convert`: Transcoding should now work on Windows.\n- :doc:`/plugins/duplicates`: The ``move`` and ``copy`` destination arguments\n  are now treated as directories. Thanks to Pedro Silva.\n- The :ref:`modify-cmd` command now skips confirmation and prints a message if\n  no changes are necessary. Thanks to brilnius.\n- :doc:`/plugins/fetchart`: When using the ``remote_priority`` config option,\n  local image files are no longer completely ignored.\n- ``echonest`` plugin: Fix an issue causing the plugin to appear twice in the\n  output of the ``beet version`` command.\n- :doc:`/plugins/lastgenre`: Fix an occasional crash when no tag weight was\n  returned by Last.fm.\n- :doc:`/plugins/mpdstats`: Restore the ``last_played`` field. Thanks to Johann\n  Klähn.\n- The :ref:`modify-cmd` command's output now clearly shows when a file has been\n  deleted.\n- Album art in files with Vorbis Comments is now marked with the \"front cover\"\n  type. Thanks to Jason Lefley.\n\n1.3.2 (December 22, 2013)\n-------------------------\n\nThis update brings new plugins for fetching acoustic metrics and listening\nstatistics, many more options for the duplicate detection plugin, and flexible\noptions for fetching multiple genres.\n\nThe \"core\" of beets gained a new built-in command: :ref:`beet write <write-cmd>`\nupdates the metadata tags for files, bringing them back into sync with your\ndatabase. Thanks to Heinz Wiesinger.\n\nWe added some plugins and overhauled some existing ones\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- The new ``echonest`` plugin plugin can fetch a wide range of `acoustic\n  attributes`_ from `The Echo Nest`_, including the \"speechiness\" and \"liveness\"\n  of each track. The new plugin supersedes an older version (``echonest_tempo``)\n  that only fetched the BPM field. Thanks to Pedro Silva and Peter Schnebel.\n- The :doc:`/plugins/duplicates` got a number of new features, thanks to Pedro\n  Silva:\n\n  - The ``keys`` option lets you specify the fields used detect duplicates.\n  - You can now use checksumming (via an external command) to find duplicates\n    instead of metadata via the ``checksum`` option.\n  - The plugin can perform actions on the duplicates it find. The new ``copy``,\n    ``move``, ``delete``, ``delete_file``, and ``tag`` options perform those\n    actions.\n\n- The new :doc:`/plugins/mpdstats` collects statistics about your listening\n  habits from MPD_. Thanks to Peter Schnebel and Johann Klähn.\n- :doc:`/plugins/lastgenre`: The new ``multiple`` option has been replaced with\n  the ``count`` option, which lets you limit the number of genres added to your\n  music. (No more thousand-character genre fields!) Also, the ``min_weight``\n  field filters out nonsense tags to make your genres more relevant. Thanks to\n  Peter Schnebel and rashley60.\n- :doc:`/plugins/lyrics`: A new ``--force`` option optionally re-downloads\n  lyrics even when files already have them. Thanks to Bitdemon.\n\nAs usual, there are also innumerable little fixes and improvements\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- When writing ID3 tags for ReplayGain normalization, tags are written with both\n  upper-case and lower-case TXXX frame descriptions. Previous versions of beets\n  used only the upper-case style, which seems to be more standard, but some\n  players (namely, Quod Libet and foobar2000) seem to only use lower-case names.\n- :doc:`/plugins/missing`: Avoid a possible error when an album's ``tracktotal``\n  field is missing.\n- :doc:`/plugins/ftintitle`: Fix an error when the sort artist is missing.\n- ``echonest_tempo``: The plugin should now match songs more reliably (i.e.,\n  fewer \"no tempo found\" messages). Thanks to Peter Schnebel.\n- :doc:`/plugins/convert`: Fix an \"Item has no library\" error when using the\n  ``auto`` config option.\n- :doc:`/plugins/convert`: Fix an issue where files of the wrong format would\n  have their transcoding skipped (and files with the right format would be\n  needlessly transcoded). Thanks to Jakob Schnitzer.\n- Fix an issue that caused the :ref:`id3v23` option to work only occasionally.\n- Also fix using :ref:`id3v23` in conjunction with the ``scrub`` and\n  ``embedart`` plugins. Thanks to Chris Cogburn.\n- :doc:`/plugins/ihate`: Fix an error when importing singletons. Thanks to\n  Mathijs de Bruin.\n- The :ref:`clutter` option can now be a whitespace-separated list in addition\n  to a YAML list.\n- Values for the :ref:`replace` option can now be empty (i.e., null is\n  equivalent to the empty string).\n- :doc:`/plugins/lastgenre`: Fix a conflict between canonicalization and\n  multiple genres.\n- When a match has a year but not a month or day, the autotagger now \"zeros out\"\n  the month and day fields after applying the year.\n- For plugin developers: added an ``optparse`` callback utility function for\n  performing actions based on arguments. Thanks to Pedro Silva.\n- :doc:`/plugins/scrub`: Fix scrubbing of MPEG-4 files. Thanks to Yevgeny\n  Bezman.\n\n.. _acoustic attributes: https://web.archive.org/web/20160701063109/http://developer.echonest.com/acoustic-attributes.html\n\n.. _mpd: https://www.musicpd.org/\n\n1.3.1 (October 12, 2013)\n------------------------\n\nThis release boasts a host of new little features, many of them contributed by\nbeets' amazing and prolific community. It adds support for Opus_ files,\ntranscoding to any format, and two new plugins: one that guesses metadata for\n\"blank\" files based on their filenames and one that moves featured artists into\nthe title field.\n\nHere's the new stuff\n~~~~~~~~~~~~~~~~~~~~\n\n- Add Opus_ audio support. Thanks to Rowan Lewis.\n- :doc:`/plugins/convert`: You can now transcode files to any audio format,\n  rather than just MP3. Thanks again to Rowan Lewis.\n- The new :doc:`/plugins/fromfilename` guesses tags from the filenames during\n  import when metadata tags themselves are missing. Thanks to Jan-Erik Dahlin.\n- The :doc:`/plugins/ftintitle`, by `@Verrus`_, is now distributed with beets.\n  It helps you rewrite tags to move \"featured\" artists from the artist field to\n  the title field.\n- The MusicBrainz data source now uses track artists over recording artists.\n  This leads to better metadata when tagging classical music. Thanks to Henrique\n  Ferreiro.\n- :doc:`/plugins/lastgenre`: You can now get multiple genres per album or track\n  using the ``multiple`` config option. Thanks to rashley60 on GitHub.\n- A new :ref:`id3v23` config option makes beets write MP3 files' tags using the\n  older ID3v2.3 metadata standard. Use this if you want your tags to be visible\n  to Windows and some older players.\n\nAnd some fixes\n~~~~~~~~~~~~~~\n\n- :doc:`/plugins/fetchart`: Better error message when the image file has an\n  unrecognized type.\n- :doc:`/plugins/mbcollection`: Detect, log, and skip invalid MusicBrainz IDs\n  (instead of failing with an API error).\n- :doc:`/plugins/info`: Fail gracefully when used erroneously with a directory.\n- ``echonest_tempo``: Fix an issue where the plugin could use the tempo from the\n  wrong song when the API did not contain the requested song.\n- Fix a crash when a file's metadata included a very large number (one wider\n  than 64 bits). These huge numbers are now replaced with zeroes in the\n  database.\n- When a track on a MusicBrainz release has a different length from the\n  underlying recording's length, the track length is now used instead.\n- With :ref:`per_disc_numbering` enabled, the ``tracktotal`` field is now set\n  correctly (i.e., to the number of tracks on the disc).\n- :doc:`/plugins/scrub`: The ``scrub`` command now restores album art in\n  addition to other (database-backed) tags.\n- :doc:`/plugins/mpdupdate`: Domain sockets can now begin with a tilde (which is\n  correctly expanded to ``$HOME``) as well as a slash. Thanks to Johann Klähn.\n- :doc:`/plugins/lastgenre`: Fix a regression that could cause new genres found\n  during import not to be persisted.\n- Fixed a crash when imported album art was also marked as \"clutter\" where the\n  art would be deleted before it could be moved into place. This led to a\n  \"image.jpg not found during copy\" error. Now clutter is removed (and\n  directories pruned) much later in the process, after the ``import_task_files``\n  hook.\n- :doc:`/plugins/missing`: Fix an error when printing missing track names.\n  Thanks to Pedro Silva.\n- Fix an occasional KeyError in the :ref:`update-cmd` command introduced in\n  1.3.0.\n- :doc:`/plugins/scrub`: Avoid preserving certain non-standard ID3 tags such as\n  NCON.\n\n.. _@verrus: https://github.com/Verrus\n\n.. _opus: https://www.opus-codec.org/\n\n1.3.0 (September 11, 2013)\n--------------------------\n\nAlbums and items now have **flexible attributes**. This means that, when you\nwant to store information about your music in the beets database, you're no\nlonger constrained to the set of fields it supports out of the box (title,\nartist, track, etc.). Instead, you can use any field name you can think of and\ntreat it just like the built-in fields.\n\nFor example, you can use the :ref:`modify-cmd` command to set a new field on a\ntrack:\n\n::\n\n    $ beet modify mood=sexy artist:miguel\n\nand then query your music based on that field:\n\n::\n\n    $ beet ls mood:sunny\n\nor use templates to see the value of the field:\n\n::\n\n    $ beet ls -f '$title: $mood'\n\nWhile this feature is nifty when used directly with the usual command-line\nsuspects, it's especially useful for plugin authors and for future beets\nfeatures. Stay tuned for great things built on this flexible attribute\ninfrastructure.\n\nOne side effect of this change: queries that include unknown fields will now\nmatch *nothing* instead of *everything*. So if you type ``beet ls\nfieldThatDoesNotExist:foo``, beets will now return no results, whereas previous\nversions would spit out a warning and then list your entire library.\n\nThere's more detail than you could ever need `on the beets blog`_.\n\n.. _on the beets blog: https://beets.io/blog/flexattr.html\n\n1.2.2 (August 27, 2013)\n-----------------------\n\nThis is a bugfix release. We're in the midst of preparing for a large change in\nbeets 1.3, so 1.2.2 resolves some issues that came up over the last few weeks.\nStay tuned!\n\nThe improvements in this release are\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- A new plugin event, ``item_moved``, is sent when files are moved on disk.\n  Thanks to dsedivec.\n- :doc:`/plugins/lyrics`: More improvements to the Google backend by Fabrice\n  Laporte.\n- :doc:`/plugins/bpd`: Fix for a crash when searching, thanks to Simon Chopin.\n- Regular expression queries (and other query types) over paths now work.\n  (Previously, special query types were ignored for the ``path`` field.)\n- :doc:`/plugins/fetchart`: Look for images in the Cover Art Archive for the\n  release group in addition to the specific release. Thanks to Filipe Fortes.\n- Fix a race in the importer that could cause files to be deleted before they\n  were imported. This happened when importing one album, importing a duplicate\n  album, and then asking for the first album to be replaced with the second. The\n  situation could only arise when importing music from the library directory and\n  when the two albums are imported close in time.\n\n1.2.1 (June 22, 2013)\n---------------------\n\nThis release introduces a major internal change in the way that similarity\nscores are handled. It means that the importer interface can now show you\nexactly why a match is assigned its score and that the autotagger gained a few\nnew options that let you customize how matches are prioritized and recommended.\n\nThe refactoring work is due to the continued efforts of Tai Lee. The changes\nyou'll notice while using the autotagger are:\n\n- The top 3 distance penalties are now displayed on the release listing, and all\n  album and track penalties are now displayed on the track changes list. This\n  should make it clear exactly which metadata is contributing to a low\n  similarity score.\n- When displaying differences, the colorization has been made more consistent\n  and helpful: red for an actual difference, yellow to indicate that a distance\n  penalty is being applied, and light gray for no penalty (e.g., case changes)\n  or disambiguation data.\n\nThere are also three new (or overhauled) configuration options that let you\ncustomize the way that matches are selected:\n\n- The :ref:`ignored` setting lets you instruct the importer not to show you\n  matches that have a certain penalty applied.\n- The :ref:`preferred` collection of settings specifies a sorted list of\n  preferred countries and media types, or prioritizes releases closest to the\n  original year for an album.\n- The :ref:`max_rec` settings can now be used for any distance penalty\n  component. The recommendation will be downgraded if a non-zero penalty is\n  being applied to the specified field.\n\nAnd some little enhancements and bug fixes\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- Multi-disc directory names can now contain \"disk\" (in addition to \"disc\").\n  Thanks to John Hawthorn.\n- :doc:`/plugins/web`: Item and album counts are now exposed through the API for\n  use with the Tomahawk resolver. Thanks to Uwe L. Korn.\n- Python 2.6 compatibility for ``beatport``, :doc:`/plugins/missing`, and\n  :doc:`/plugins/duplicates`. Thanks to Wesley Bitter and Pedro Silva.\n- Don't move the config file during a null migration. Thanks to Theofilos\n  Intzoglou.\n- Fix an occasional crash in the ``beatport`` when a length field was missing\n  from the API response. Thanks to Timothy Appnel.\n- :doc:`/plugins/scrub`: Handle and log I/O errors.\n- :doc:`/plugins/lyrics`: The Google backend should now turn up more results.\n  Thanks to Fabrice Laporte.\n- :doc:`/plugins/random`: Fix compatibility with Python 2.6. Thanks to Matthias\n  Drochner.\n\n1.2.0 (June 5, 2013)\n--------------------\n\nThere's a *lot* of new stuff in this release: new data sources for the\nautotagger, new plugins to look for problems in your library, tracking the date\nthat you acquired new music, an awesome new syntax for doing queries over\nnumeric fields, support for ALAC files, and major enhancements to the importer's\nUI and distance calculations. A special thanks goes out to all the contributors\nwho helped make this release awesome.\n\nFor the first time, beets can now tag your music using additional **data\nsources** to augment the matches from MusicBrainz. When you enable either of\nthese plugins, the importer will start showing you new kinds of matches:\n\n- New :doc:`/plugins/discogs`: Get matches from the Discogs_ database. Thanks to\n  Artem Ponomarenko and Tai Lee.\n- New ``beatport`` plugin: Get matches from the Beatport_ database. Thanks to\n  Johannes Baiter.\n\nWe also have two other new plugins that can scan your library to check for\ncommon problems, both by Pedro Silva:\n\n- New :doc:`/plugins/duplicates`: Find tracks or albums in your library that are\n  **duplicated**.\n- New :doc:`/plugins/missing`: Find albums in your library that are **missing\n  tracks**.\n\nThere are also three more big features added to beets core\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- Your library now keeps track of **when music was added** to it. The new\n  ``added`` field is a timestamp reflecting when each item and album was\n  imported and the new ``%time{}`` template function lets you format this\n  timestamp for humans. Thanks to Lucas Duailibe.\n- When using queries to match on quantitative fields, you can now use **numeric\n  ranges**. For example, you can get a list of albums from the '90s by typing\n  ``beet ls year:1990..1999`` or find high-bitrate music with\n  ``bitrate:128000..``. See :ref:`numericquery`. Thanks to Michael Schuerig.\n- **ALAC files** are now marked as ALAC instead of being conflated with AAC\n  audio. Thanks to Simon Luijk.\n\nIn addition, the importer saw various UI enhancements, thanks to Tai Lee\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- More consistent format and colorization of album and track metadata.\n- Display data source URL for matches from the new data source plugins. This\n  should make it easier to migrate data from Discogs or Beatport into\n  MusicBrainz.\n- Display album disambiguation and disc titles in the track listing, when\n  available.\n- Track changes are highlighted in yellow when they indicate a change in format\n  to or from the style of :ref:`per_disc_numbering`. (As before, no penalty is\n  applied because the track number is still \"correct\", just in a different\n  format.)\n- Sort missing and unmatched tracks by index and title and group them together\n  for better readability.\n- Indicate MusicBrainz ID mismatches.\n\nThe calculation of the similarity score for autotagger matches was also\nimproved, again thanks to Tai Lee. These changes, in general, help deal with the\nnew metadata sources and help disambiguate between similar releases in the same\nMusicBrainz release group:\n\n- Strongly prefer releases with a matching MusicBrainz album ID. This helps\n  beets re-identify the same release when re-importing existing files.\n- Prefer releases that are closest to the tagged ``year``. Tolerate files tagged\n  with release or original year.\n- The new ``preferred_media`` config option lets you prefer a certain media type\n  when the ``media`` field is unset on an album.\n- Apply minor penalties across a range of fields to differentiate between nearly\n  identical releases: ``disctotal``, ``label``, ``catalognum``, ``country`` and\n  ``albumdisambig``.\n\nAs usual, there were also lots of other great littler enhancements\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- :doc:`/plugins/random`: A new ``-e`` option gives an equal chance to each\n  artist in your collection to avoid biasing random samples to prolific artists.\n  Thanks to Georges Dubus.\n- The :ref:`modify-cmd` now correctly converts types when modifying non-string\n  fields. You can now safely modify the \"comp\" flag and the \"year\" field, for\n  example. Thanks to Lucas Duailibe.\n- :doc:`/plugins/convert`: You can now configure the path formats for converted\n  files separately from your main library. Thanks again to Lucas Duailibe.\n- The importer output now shows the number of audio files in each album. Thanks\n  to jayme on GitHub.\n- Plugins can now provide fields for both Album and Item templates, thanks to\n  Pedro Silva. Accordingly, the :doc:`/plugins/inline` can also now define album\n  fields. For consistency, the ``pathfields`` configuration section has been\n  renamed ``item_fields`` (although the old name will still work for\n  compatibility).\n- Plugins can also provide metadata matches for ID searches. For example, the\n  new Discogs plugin lets you search for an album by its Discogs ID from the\n  same prompt that previously just accepted MusicBrainz IDs. Thanks to Johannes\n  Baiter.\n- The :ref:`fields-cmd` command shows template fields provided by plugins.\n  Thanks again to Pedro Silva.\n- :doc:`/plugins/mpdupdate`: You can now communicate with MPD over a Unix domain\n  socket. Thanks to John Hawthorn.\n\nAnd a batch of fixes\n~~~~~~~~~~~~~~~~~~~~\n\n- Album art filenames now respect the :ref:`replace` configuration.\n- Friendly error messages are now printed when trying to read or write files\n  that go missing.\n- The :ref:`modify-cmd` command can now change albums' album art paths (i.e.,\n  ``beet modify artpath=...`` works). Thanks to Lucas Duailibe.\n- :doc:`/plugins/zero`: Fix a crash when nulling out a field that contains None.\n- Templates can now refer to non-tag item fields (e.g., ``$id`` and\n  ``$album_id``).\n- :doc:`/plugins/lyrics`: Lyrics searches should now turn up more results due to\n  some fixes in dealing with special characters.\n\n.. _beatport: https://www.beatport.com/\n\n.. _discogs: https://discogs.com/\n\n1.1.0 (April 29, 2013)\n----------------------\n\nThis final release of 1.1 brings a little polish to the betas that introduced\nthe new configuration system. The album art and lyrics plugins also got a little\nlove.\n\nIf you're upgrading from 1.0.0 or earlier, this release (like the 1.1 betas)\nwill automatically migrate your configuration to the new system.\n\n- :doc:`/plugins/embedart`: The ``embedart`` command now embeds each album's\n  associated art by default. The ``--file`` option invokes the old behavior, in\n  which a specific image file is used.\n- :doc:`/plugins/lyrics`: A new (optional) Google Custom Search backend was\n  added for finding lyrics on a wide array of sites. Thanks to Fabrice Laporte.\n- When automatically detecting the filesystem's maximum filename length, never\n  guess more than 200 characters. This prevents errors on systems where the\n  maximum length was misreported. You can, of course, override this default with\n  the :ref:`max_filename_length` option.\n- :doc:`/plugins/fetchart`: Two new configuration options were added:\n  ``cover_names``, the list of keywords used to identify preferred images, and\n  ``cautious``, which lets you avoid falling back to images that don't contain\n  those keywords. Thanks to Fabrice Laporte.\n- Avoid some error cases in the ``update`` command and the ``embedart`` and\n  ``mbsync`` plugins. Invalid or missing files now cause error logs instead of\n  crashing beets. Thanks to Lucas Duailibe.\n- :doc:`/plugins/lyrics`: Searches now strip \"featuring\" artists when searching\n  for lyrics, which should increase the hit rate for these tracks. Thanks to\n  Fabrice Laporte.\n- When listing the items in an album, the items are now always in track-number\n  order. This should lead to more predictable listings from the\n  :doc:`/plugins/importfeeds`.\n- :doc:`/plugins/smartplaylist`: Queries are now split using shell-like syntax\n  instead of just whitespace, so you can now construct terms that contain\n  spaces.\n- :doc:`/plugins/lastgenre`: The ``force`` config option now defaults to true\n  and controls the behavior of the import hook. (Previously, new genres were\n  always forced during import.)\n- :doc:`/plugins/web`: Fix an error when specifying the hostname on the command\n  line.\n- :doc:`/plugins/web`: The underlying API was expanded slightly to support\n  Tomahawk_ collections. And file transfers now have a \"Content-Length\" header.\n  Thanks to Uwe L. Korn.\n- :doc:`/plugins/lastgenre`: Fix an error when using genre canonicalization.\n\n.. _tomahawk: https://github.com/tomahawk-player/tomahawk\n\n1.1b3 (March 16, 2013)\n----------------------\n\nThis third beta of beets 1.1 brings a hodgepodge of little new features (and\ninternal overhauls that will make improvements easier in the future). There are\nnew options for getting metadata in a particular language and seeing more detail\nduring the import process. There's also a new plugin for synchronizing your\nmetadata with MusicBrainz. Under the hood, plugins can now extend the query\nsyntax.\n\nNew configuration options\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- :ref:`languages` controls the preferred languages when selecting an alias from\n  MusicBrainz. This feature requires python-musicbrainzngs_ 0.3 or later. Thanks\n  to Sam Doshi.\n- :ref:`detail` enables a mode where all tracks are listed in the importer UI,\n  as opposed to only changed tracks.\n- The ``--flat`` option to the ``beet import`` command treats an entire\n  directory tree of music files as a single album. This can help in situations\n  where a multi-disc album is split across multiple directories.\n- :doc:`/plugins/importfeeds`: An option was added to use absolute, rather than\n  relative, paths. Thanks to Lucas Duailibe.\n\nOther stuff\n~~~~~~~~~~~\n\n- A new :doc:`/plugins/mbsync` provides a command that looks up each item and\n  track in MusicBrainz and updates your library to reflect it. This can help you\n  easily correct errors that have been fixed in the MB database. Thanks to Jakob\n  Schnitzer.\n- :doc:`/plugins/fuzzy`: The ``fuzzy`` command was removed and replaced with a\n  new query type. To perform fuzzy searches, use the ``~`` prefix with\n  :ref:`list-cmd` or other commands. Thanks to Philippe Mongeau.\n- As part of the above, plugins can now extend the query syntax and new kinds of\n  matching capabilities to beets. See :ref:`extend-query`. Thanks again to\n  Philippe Mongeau.\n- :doc:`/plugins/convert`: A new ``--keep-new`` option lets you store transcoded\n  files in your library while backing up the originals (instead of vice-versa).\n  Thanks to Lucas Duailibe.\n- :doc:`/plugins/convert`: Also, a new ``auto`` config option will transcode\n  audio files automatically during import. Thanks again to Lucas Duailibe.\n- :doc:`/plugins/chroma`: A new ``fingerprint`` command lets you generate and\n  store fingerprints for items that don't yet have them. One more round of\n  applause for Lucas Duailibe.\n- ``echonest_tempo``: API errors now issue a warning instead of exiting with an\n  exception. We also avoid an error when track metadata contains newlines.\n- When the importer encounters an error (insufficient permissions, for example)\n  when walking a directory tree, it now logs an error instead of crashing.\n- In path formats, null database values now expand to the empty string instead\n  of the string \"None\".\n- Add \"System Volume Information\" (an internal directory found on some Windows\n  filesystems) to the default ignore list.\n- Fix a crash when ReplayGain values were set to null.\n- Fix a crash when iTunes Sound Check tags contained invalid data.\n- Fix an error when the configuration file (``config.yaml``) is completely\n  empty.\n- Fix an error introduced in 1.1b1 when importing using timid mode. Thanks to\n  Sam Doshi.\n- :doc:`/plugins/convert`: Fix a bug when creating files with Unicode pathnames.\n- Fix a spurious warning from the Unidecode module when matching albums that are\n  missing all metadata.\n- Fix Unicode errors when a directory or file doesn't exist when invoking the\n  import command. Thanks to Lucas Duailibe.\n- :doc:`/plugins/mbcollection`: Show friendly, human-readable errors when\n  MusicBrainz exceptions occur.\n- ``echonest_tempo``: Catch socket errors that are not handled by the Echo Nest\n  library.\n- :doc:`/plugins/chroma`: Catch Acoustid Web service errors when submitting\n  fingerprints.\n\n1.1b2 (February 16, 2013)\n-------------------------\n\nThe second beta of beets 1.1 uses the fancy new configuration infrastructure to\nadd many, many new config options. The import process is more flexible;\nfilenames can be customized in more detail; and more. This release also supports\nWindows Media (ASF) files and iTunes Sound Check volume normalization.\n\nThis version introduces one **change to the default behavior** that you should\nbe aware of. Previously, when importing new albums matched in MusicBrainz, the\ndate fields (``year``, ``month``, and ``day``) would be set to the release date\nof the *original* version of the album, as opposed to the specific date of the\nrelease selected. Now, these fields reflect the specific release and\n``original_year``, etc., reflect the earlier release date. If you want the old\nbehavior, just set :ref:`original_date` to true in your config file.\n\nNew configuration options\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- :ref:`default_action` lets you determine the default (just-hit-return) option\n  is when considering a candidate.\n- :ref:`none_rec_action` lets you skip the prompt, and automatically choose an\n  action, when there is no good candidate. Thanks to Tai Lee.\n- :ref:`max_rec` lets you define a maximum recommendation for albums with\n  missing/extra tracks or differing track lengths/numbers. Thanks again to Tai\n  Lee.\n- :ref:`original_date` determines whether, when importing new albums, the\n  ``year``, ``month``, and ``day`` fields should reflect the specific (e.g.,\n  reissue) release date or the original release date. Note that the original\n  release date is always available as ``original_year``, etc.\n- :ref:`clutter` controls which files should be ignored when cleaning up empty\n  directories. Thanks to Steinþór Pálsson.\n- :doc:`/plugins/lastgenre`: A new configuration option lets you choose to\n  retrieve artist-level tags as genres instead of album- or track-level tags.\n  Thanks to Peter Fern and Peter Schnebel.\n- :ref:`max_filename_length` controls truncation of long filenames. Also, beets\n  now tries to determine the filesystem's maximum length automatically if you\n  leave this option unset.\n- :doc:`/plugins/fetchart`: The ``remote_priority`` option searches remote (Web)\n  art sources even when local art is present.\n- You can now customize the character substituted for path separators (e.g., /)\n  in filenames via ``path_sep_replace``. The default is an underscore. Use this\n  setting with caution.\n\nOther new stuff\n~~~~~~~~~~~~~~~\n\n- Support for Windows Media/ASF audio files. Thanks to Dave Hayes.\n- New :doc:`/plugins/smartplaylist`: generate and maintain m3u playlist files\n  based on beets queries. Thanks to Dang Mai Hai.\n- ReplayGain tags on MPEG-4/AAC files are now supported. And, even more\n  astonishingly, ReplayGain values in MP3 and AAC files are now compatible with\n  `iTunes Sound Check`_. Thanks to Dave Hayes.\n- Track titles in the importer UI's difference display are now either aligned\n  vertically or broken across two lines for readability. Thanks to Tai Lee.\n- Albums and items have new fields reflecting the *original* release date\n  (``original_year``, ``original_month``, and ``original_day``). Previously,\n  when tagging from MusicBrainz, *only* the original date was stored; now, the\n  old fields refer to the *specific* release date (e.g., when the album was\n  reissued).\n- Some changes to the way candidates are recommended for selection, thanks to\n  Tai Lee:\n\n  - According to the new :ref:`max_rec` configuration option, partial album\n    matches are downgraded to a \"low\" recommendation by default.\n  - When a match isn't great but is either better than all the others or the\n    only match, it is given a \"low\" (rather than \"medium\") recommendation.\n  - There is no prompt default (i.e., input is required) when matches are bad:\n    \"low\" or \"none\" recommendations or when choosing a candidate other than the\n    first.\n\n- The importer's heuristic for coalescing the directories in a multi-disc album\n  has been improved. It can now detect when two directories alongside each other\n  share a similar prefix but a different number (e.g., \"Album Disc 1\" and \"Album\n  Disc 2\") even when they are not alone in a common parent directory. Thanks\n  once again to Tai Lee.\n- Album listings in the importer UI now show the release medium (CD, Vinyl,\n  3xCD, etc.) as well as the disambiguation string. Thanks to Peter Schnebel.\n- :doc:`/plugins/lastgenre`: The plugin can now get different genres for\n  individual tracks on an album. Thanks to Peter Schnebel.\n- When getting data from MusicBrainz, the album disambiguation string\n  (``albumdisambig``) now reflects both the release and the release group.\n- :doc:`/plugins/mpdupdate`: Sends an update message whenever *anything* in the\n  database changes---not just when importing. Thanks to Dang Mai Hai.\n- When the importer UI shows a difference in track numbers or durations, they\n  are now colorized based on the *suffixes* that differ. For example, when\n  showing the difference between 2:01 and 2:09, only the last digit will be\n  highlighted.\n- The importer UI no longer shows a change when the track length difference is\n  less than 10 seconds. (This threshold was previously 2 seconds.)\n- Two new plugin events were added: *database_change* and *cli_exit*. Thanks\n  again to Dang Mai Hai.\n- Plugins are now loaded in the order they appear in the config file. Thanks to\n  Dang Mai Hai.\n- :doc:`/plugins/bpd`: Browse by album artist and album artist sort name. Thanks\n  to Steinþór Pálsson.\n- ``echonest_tempo``: Don't attempt a lookup when the artist or track title is\n  missing.\n- Fix an error when migrating the ``.beetsstate`` file on Windows.\n- A nicer error message is now given when the configuration file contains tabs.\n  (YAML doesn't like tabs.)\n- Fix the ``-l`` (log path) command-line option for the ``import`` command.\n\n.. _itunes sound check: https://support.apple.com/itunes\n\n1.1b1 (January 29, 2013)\n------------------------\n\nThis release entirely revamps beets' configuration system. The configuration\nfile is now a YAML_ document and is located, along with other support files, in\na common directory (e.g., ``~/.config/beets`` on Unix-like systems).\n\n.. _yaml: https://en.wikipedia.org/wiki/YAML\n\n- Renamed plugins: The ``rdm`` plugin has been renamed to ``random`` and\n  ``fuzzy_search`` has been renamed to ``fuzzy``.\n- Renamed config options: Many plugins have a flag dictating whether their\n  action runs at import time. This option had many names (``autofetch``,\n  ``autoembed``, etc.) but is now consistently called ``auto``.\n- Reorganized import config options: The various ``import_*`` options are now\n  organized under an ``import:`` heading and their prefixes have been removed.\n- New default file locations: The default filename of the library database is\n  now ``library.db`` in the same directory as the config file, as opposed to\n  ``~/.beetsmusic.blb`` previously. Similarly, the runtime state file is now\n  called ``state.pickle`` in the same directory instead of ``~/.beetsstate``.\n\nIt also adds some new features\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- :doc:`/plugins/inline`: Inline definitions can now contain statements or\n  blocks in addition to just expressions. Thanks to Florent Thoumie.\n- Add a configuration option, :ref:`terminal_encoding`, controlling the text\n  encoding used to print messages to standard output.\n- The MusicBrainz hostname (and rate limiting) are now configurable. See\n  :ref:`musicbrainz-config`.\n- You can now configure the similarity thresholds used to determine when the\n  autotagger automatically accepts a metadata match. See :ref:`match-config`.\n- :doc:`/plugins/importfeeds`: Added a new configuration option that controls\n  the base for relative paths used in m3u files. Thanks to Philippe Mongeau.\n\n1.0.0 (January 29, 2013)\n------------------------\n\nAfter fifteen betas and two release candidates, beets has finally hit\none-point-oh. Congratulations to everybody involved. This version of beets will\nremain stable and receive only bug fixes from here on out. New development is\nongoing in the betas of version 1.1.\n\n- :doc:`/plugins/scrub`: Fix an incompatibility with Python 2.6.\n- :doc:`/plugins/lyrics`: Fix an issue that failed to find lyrics when metadata\n  contained \"real\" apostrophes.\n- :doc:`/plugins/replaygain`: On Windows, emit a warning instead of crashing\n  when analyzing non-ASCII filenames.\n- Silence a spurious warning from version 0.04.12 of the Unidecode module.\n\n1.0rc2 (December 31, 2012)\n--------------------------\n\nThis second release candidate follows quickly after rc1 and fixes a few small\nbugs found since that release. There were a couple of regressions and some bugs\nin a newly added plugin.\n\n- ``echonest_tempo``: If the Echo Nest API limit is exceeded or a communication\n  error occurs, the plugin now waits and tries again instead of crashing. Thanks\n  to Zach Denton.\n- :doc:`/plugins/fetchart`: Fix a regression that caused crashes when art was\n  not available from some sources.\n- Fix a regression on Windows that caused all relative paths to be \"not found\".\n\n1.0rc1 (December 17, 2012)\n--------------------------\n\nThe first release candidate for beets 1.0 includes a deluge of new features\ncontributed by beets users. The vast majority of the credit for this release\ngoes to the growing and vibrant beets community. A million thanks to everybody\nwho contributed to this release.\n\nThere are new plugins for transcoding music, fuzzy searches, tempo collection,\nand fiddling with metadata. The ReplayGain plugin has been rebuilt from scratch.\nAlbum art images can now be resized automatically. Many other smaller\nrefinements make things \"just work\" as smoothly as possible.\n\nWith this release candidate, beets 1.0 is feature-complete. We'll be fixing bugs\non the road to 1.0 but no new features will be added. Concurrently, work begins\ntoday on features for version 1.1.\n\n- New plugin: :doc:`/plugins/convert` **transcodes** music and embeds album art\n  while copying to a separate directory. Thanks to Jakob Schnitzer and Andrew G.\n  Dunn.\n- New plugin: :doc:`/plugins/fuzzy` lets you find albums and tracks using\n  **fuzzy string matching** so you don't have to type (or even remember) their\n  exact names. Thanks to Philippe Mongeau.\n- New plugin: ``echonest_tempo`` fetches **tempo** (BPM) information from `The\n  Echo Nest`_. Thanks to David Brenner.\n- New plugin: :doc:`/plugins/the` adds a template function that helps format\n  text for nicely-sorted directory listings. Thanks to Blemjhoo Tezoulbr.\n- New plugin: :doc:`/plugins/zero` **filters out undesirable fields** before\n  they are written to your tags. Thanks again to Blemjhoo Tezoulbr.\n- New plugin: :doc:`/plugins/ihate` automatically skips (or warns you about)\n  importing albums that match certain criteria. Thanks once again to Blemjhoo\n  Tezoulbr.\n- :doc:`/plugins/replaygain`: This plugin has been completely overhauled to use\n  the mp3gain_ or aacgain_ command-line tools instead of the failure-prone\n  Gstreamer ReplayGain implementation. Thanks to Fabrice Laporte.\n- :doc:`/plugins/fetchart` and :doc:`/plugins/embedart`: Both plugins can now\n  **resize album art** to avoid excessively large images. Use the ``maxwidth``\n  config option with either plugin. Thanks to Fabrice Laporte.\n- :doc:`/plugins/scrub`: Scrubbing now removes *all* types of tags from a file\n  rather than just one. For example, if your FLAC file has both ordinary FLAC\n  tags and ID3 tags, the ID3 tags are now also removed.\n- :ref:`stats-cmd` command: New ``--exact`` switch to make the file size\n  calculation more accurate (thanks to Jakob Schnitzer).\n- :ref:`list-cmd` command: Templates given with ``-f`` can now show items' and\n  albums' paths (using ``$path``).\n- The output of the :ref:`update-cmd`, :ref:`remove-cmd`, and :ref:`modify-cmd`\n  commands now respects the :ref:`list_format_album` and :ref:`list_format_item`\n  config options. Thanks to Mike Kazantsev.\n- The :ref:`art-filename` option can now be a template rather than a simple\n  string. Thanks to Jarrod Beardwood.\n- Fix album queries for ``artpath`` and other non-item fields.\n- Null values in the database can now be matched with the empty-string regular\n  expression, ``^$``.\n- Queries now correctly match non-string values in path format predicates.\n- When autotagging a various-artists album, the album artist field is now used\n  instead of the majority track artist.\n- :doc:`/plugins/lastgenre`: Use the albums' existing genre tags if they pass\n  the whitelist (thanks to Fabrice Laporte).\n- :doc:`/plugins/lastgenre`: Add a ``lastgenre`` command for fetching genres\n  post facto (thanks to Jakob Schnitzer).\n- :doc:`/plugins/fetchart`: Local image filenames are now used in alphabetical\n  order.\n- :doc:`/plugins/fetchart`: Fix a bug where cover art filenames could lack a\n  ``.jpg`` extension.\n- :doc:`/plugins/lyrics`: Fix an exception with non-ASCII lyrics.\n- :doc:`/plugins/web`: The API now reports file sizes (for use with the\n  `Tomahawk resolver`_).\n- :doc:`/plugins/web`: Files now download with a reasonable filename rather than\n  just being called \"file\" (thanks to Zach Denton).\n- :doc:`/plugins/importfeeds`: Fix error in symlink mode with non-ASCII\n  filenames.\n- :doc:`/plugins/mbcollection`: Fix an error when submitting a large number of\n  releases (we now submit only 200 releases at a time instead of 350). Thanks to\n  Jonathan Towne.\n- :doc:`/plugins/embedart`: Made the method for embedding art into FLAC files\n  `standard\n  <https://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE>`_-compliant.\n  Thanks to Daniele Sluijters.\n- Add the track mapping dictionary to the ``album_distance`` plugin function.\n- When an exception is raised while reading a file, the path of the file in\n  question is now logged (thanks to Mike Kazantsev).\n- Truncate long filenames based on their *bytes* rather than their Unicode\n  *characters*, fixing situations where encoded names could be too long.\n- Filename truncation now incorporates the length of the extension.\n- Fix an assertion failure when the MusicBrainz main database and search server\n  disagree.\n- Fix a bug that caused the :doc:`/plugins/lastgenre` and other plugins not to\n  modify files' tags even when they successfully change the database.\n- Fix a VFS bug leading to a crash in the :doc:`/plugins/bpd` when files had\n  non-ASCII extensions.\n- Fix for changing date fields (like \"year\") with the :ref:`modify-cmd` command.\n- Fix a crash when input is read from a pipe without a specified encoding.\n- Fix some problem with identifying files on Windows with Unicode directory\n  names in their path.\n- Fix a crash when Unicode queries were used with ``import -L`` re-imports.\n- Fix an error when fingerprinting files with Unicode filenames on Windows.\n- Warn instead of crashing when importing a specific file in singleton mode.\n- Add human-readable error messages when writing files' tags fails or when a\n  directory can't be created.\n- Changed plugin loading so that modules can be imported without unintentionally\n  loading the plugins they contain.\n\n.. _aacgain: https://github.com/dgilman/aacgain\n\n.. _mp3gain: https://sourceforge.net/projects/mp3gain/\n\n.. _the echo nest: https://web.archive.org/web/20180329103558/http://the.echonest.com/\n\n.. _tomahawk resolver: https://beets.io/blog/tomahawk-resolver.html\n\n1.0b15 (July 26, 2012)\n----------------------\n\nThe fifteenth (!) beta of beets is compendium of small fixes and features, most\nof which represent long-standing requests. The improvements include matching\nalbums with extra tracks, per-disc track numbering in multi-disc albums, an\noverhaul of the album art downloader, and robustness enhancements that should\nkeep beets running even when things go wrong. All these smaller changes should\nhelp us focus on some larger changes coming before 1.0.\n\nPlease note that this release contains one backwards-incompatible change: album\nart fetching, which was previously baked into the import workflow, is now\nencapsulated in a plugin (the :doc:`/plugins/fetchart`). If you want to continue\nfetching cover art for your music, enable this plugin after upgrading to beets\n1.0b15.\n\n- The autotagger can now find matches for albums when you have **extra tracks**\n  on your filesystem that aren't present in the MusicBrainz catalog. Previously,\n  if you tried to match album with 15 audio files but the MusicBrainz entry had\n  only 14 tracks, beets would ignore this match. Now, beets will show you\n  matches even when they are \"too short\" and indicate which tracks from your\n  disk are unmatched.\n- Tracks on multi-disc albums can now be **numbered per-disc** instead of\n  per-album via the :ref:`per_disc_numbering` config option.\n- The default output format for the ``beet list`` command is now configurable\n  via the :ref:`list_format_item` and :ref:`list_format_album` config options.\n  Thanks to Fabrice Laporte.\n- Album **cover art fetching** is now encapsulated in the\n  :doc:`/plugins/fetchart`. Be sure to enable this plugin if you're using this\n  functionality. As a result of this new organization, the new plugin has gained\n  a few new features:\n\n  - \"As-is\" and non-autotagged imports can now have album art imported from the\n    local filesystem (although Web repositories are still not searched in these\n    cases).\n  - A new command, ``beet fetchart``, allows you to download album art\n    post-import. If you only want to fetch art manually, not automatically\n    during import, set the new plugin's ``autofetch`` option to ``no``.\n  - New album art sources have been added.\n\n- Errors when communicating with MusicBrainz now log an error message instead of\n  halting the importer.\n- Similarly, filesystem manipulation errors now print helpful error messages\n  instead of a messy traceback. They still interrupt beets, but they should now\n  be easier for users to understand. Tracebacks are still available in verbose\n  mode.\n- New metadata fields for `artist credits`_: ``artist_credit`` and\n  ``albumartist_credit`` can now contain release- and recording-specific\n  variations of the artist's name. See :ref:`itemfields`.\n- Revamped the way beets handles concurrent database access to avoid\n  nondeterministic SQLite-related crashes when using the multithreaded importer.\n  On systems where SQLite was compiled without ``usleep(3)`` support,\n  multithreaded database access could cause an internal error (with the message\n  \"database is locked\"). This release synchronizes access to the database to\n  avoid internal SQLite contention, which should avoid this error.\n- Plugins can now add parallel stages to the import pipeline. See\n  :ref:`basic-plugin-setup`.\n- Beets now prints out an error when you use an unrecognized field name in a\n  query: for example, when running ``beet ls -a artist:foo`` (because ``artist``\n  is an item-level field).\n- New plugin events:\n\n  - ``import_task_choice`` is called after an import task has an action\n    assigned.\n  - ``import_task_files`` is called after a task's file manipulation has\n    finished (copying or moving files, writing metadata tags).\n  - ``library_opened`` is called when beets starts up and opens the library\n    database.\n\n- :doc:`/plugins/lastgenre`: Fixed a problem where path formats containing\n  ``$genre`` would use the old genre instead of the newly discovered one.\n- Fix a crash when moving files to a Samba share.\n- :doc:`/plugins/mpdupdate`: Fix TypeError crash (thanks to Philippe Mongeau).\n- When re-importing files with ``import_copy`` enabled, only files inside the\n  library directory are moved. Files outside the library directory are still\n  copied. This solves a problem (introduced in 1.0b14) where beets could crash\n  after adding files to the library but before finishing copying them; during\n  the next import, the (external) files would be moved instead of copied.\n- Artist sort names are now populated correctly for multi-artist tracks and\n  releases. (Previously, they only reflected the first artist.)\n- When previewing changes during import, differences in track duration are now\n  shown as \"2:50 vs. 3:10\" rather than separated with ``->`` like track numbers.\n  This should clarify that beets isn't doing anything to modify lengths.\n- Fix a problem with query-based path format matching where a field-qualified\n  pattern, like ``albumtype_soundtrack``, would match everything.\n- :doc:`/plugins/chroma`: Fix matching with ambiguous Acoustids. Some Acoustids\n  are identified with multiple recordings; beets now considers any associated\n  recording a valid match. This should reduce some cases of errant track\n  reordering when using chroma.\n- Fix the ID3 tag name for the catalog number field.\n- :doc:`/plugins/chroma`: Fix occasional crash at end of fingerprint submission\n  and give more context to \"failed fingerprint generation\" errors.\n- Interactive prompts are sent to stdout instead of stderr.\n- :doc:`/plugins/embedart`: Fix crash when audio files are unreadable.\n- :doc:`/plugins/bpd`: Fix crash when sockets disconnect (thanks to Matteo\n  Mecucci).\n- Fix an assertion failure while importing with moving enabled when the file was\n  already at its destination.\n- Fix Unicode values in the ``replace`` config option (thanks to Jakob Borg).\n- Use a nicer error message when input is requested but stdin is closed.\n- Fix errors on Windows for certain Unicode characters that can't be represented\n  in the MBCS encoding. This required a change to the way that paths are\n  represented in the database on Windows; if you find that beets' paths are out\n  of sync with your filesystem with this release, delete and recreate your\n  database with ``beet import -AWC /path/to/music``.\n- Fix ``import`` with relative path arguments on Windows.\n\n.. _artist credits: https://wiki.musicbrainz.org/Artist_Credit\n\n1.0b14 (May 12, 2012)\n---------------------\n\nThe centerpiece of this beets release is the graceful handling of\nsimilarly-named albums. It's now possible to import two albums with the same\nartist and title and to keep them from conflicting in the filesystem. Many other\nawesome new features were contributed by the beets community, including regular\nexpression queries, artist sort names, moving files on import. There are three\nnew plugins: random song/album selection; MusicBrainz \"collection\" integration;\nand a plugin for interoperability with other music library systems.\n\nA million thanks to the (growing) beets community for making this a huge\nrelease.\n\n- The importer now gives you **choices when duplicates are detected**.\n  Previously, when beets found an existing album or item in your library\n  matching the metadata on a newly-imported one, it would just skip the new\n  music to avoid introducing duplicates into your library. Now, you have three\n  choices: skip the new music (the previous behavior), keep both, or remove the\n  old music. See the :ref:`guide-duplicates` section in the autotagging guide\n  for details.\n- Beets can now avoid storing identically-named albums in the same directory.\n  The new ``%aunique{}`` template function, which is included in the default\n  path formats, ensures that Crystal Castles' albums will be placed into\n  different directories. See :ref:`aunique` for details.\n- Beets queries can now use **regular expressions**. Use an additional ``:`` in\n  your query to enable regex matching. See :ref:`regex` for the full details.\n  Thanks to Matteo Mecucci.\n- Artist **sort names** are now fetched from MusicBrainz. There are two new data\n  fields, ``artist_sort`` and ``albumartist_sort``, that contain sortable artist\n  names like \"Beatles, The\". These fields are also used to sort albums and items\n  when using the ``list`` command. Thanks to Paul Provost.\n- Many other **new metadata fields** were added, including ASIN, label catalog\n  number, disc title, encoder, and MusicBrainz release group ID. For a full list\n  of fields, see :ref:`itemfields`.\n- :doc:`/plugins/chroma`: A new command, ``beet submit``, will **submit\n  fingerprints** to the Acoustid database. Submitting your library helps\n  increase the coverage and accuracy of Acoustid fingerprinting. The Chromaprint\n  fingerprint and Acoustid ID are also now stored for all fingerprinted tracks.\n  This version of beets *requires* at least version 0.6 of pyacoustid_ for\n  fingerprinting to work.\n- The importer can now **move files**. Previously, beets could only copy files\n  and delete the originals, which is inefficient if the source and destination\n  are on the same filesystem. Use the ``import_move`` configuration option and\n  see :doc:`/reference/config` for more details. Thanks to Domen Kožar.\n- New :doc:`/plugins/random`: Randomly select albums and tracks from your\n  library. Thanks to Philippe Mongeau.\n- The :doc:`/plugins/mbcollection` by Jeffrey Aylesworth was added to the core\n  beets distribution.\n- New :doc:`/plugins/importfeeds`: Catalog imported files in ``m3u`` playlist\n  files or as symlinks for easy importing to other systems. Thanks to Fabrice\n  Laporte.\n- The ``-f`` (output format) option to the ``beet list`` command can now contain\n  template functions as well as field references. Thanks to Steve Dougherty.\n- A new command ``beet fields`` displays the available metadata fields (thanks\n  to Matteo Mecucci).\n- The ``import`` command now has a ``--noincremental`` or ``-I`` flag to disable\n  incremental imports (thanks to Matteo Mecucci).\n- When the autotagger fails to find a match, it now displays the number of\n  tracks on the album (to help you guess what might be going wrong) and a link\n  to the FAQ.\n- The default filename character substitutions were changed to be more\n  conservative. The Windows \"reserved characters\" are substituted by default\n  even on Unix platforms (this causes less surprise when using Samba shares to\n  store music). To customize your character substitutions, see :ref:`the replace\n  config option <replace>`.\n- :doc:`/plugins/lastgenre`: Added a \"fallback\" option when no suitable genre\n  can be found (thanks to Fabrice Laporte).\n- :doc:`/plugins/rewrite`: Unicode rewriting rules are now allowed (thanks to\n  Nicolas Dietrich).\n- Filename collisions are now avoided when moving album art.\n- :doc:`/plugins/bpd`: Print messages to show when directory tree is being\n  constructed.\n- :doc:`/plugins/bpd`: Use Gstreamer's ``playbin2`` element instead of the\n  deprecated ``playbin``.\n- :doc:`/plugins/bpd`: Random and repeat modes are now supported (thanks to\n  Matteo Mecucci).\n- :doc:`/plugins/bpd`: Listings are now sorted (thanks once again to Matteo\n  Mecucci).\n- Filenames are normalized with Unicode Normal Form D (NFD) on Mac OS X and NFC\n  on all other platforms.\n- Significant internal restructuring to avoid SQLite locking errors. As part of\n  these changes, the not-very-useful \"save\" plugin event has been removed.\n\n.. _pyacoustid: https://github.com/beetbox/pyacoustid\n\n1.0b13 (March 16, 2012)\n-----------------------\n\nBeets 1.0b13 consists of a plethora of small but important fixes and\nrefinements. A lyrics plugin is now included with beets; new audio properties\nare catalogged; the ``list`` command has been made more powerful; the autotagger\nis more tolerant of different tagging styles; and importing with original file\ndeletion now cleans up after itself more thoroughly. Many, many bugs—including\nseveral crashers—were fixed. This release lays the foundation for more features\nto come in the next couple of releases.\n\n- The :doc:`/plugins/lyrics`, originally by `Peter Brunner`_, is revamped and\n  included with beets, making it easy to fetch **song lyrics**.\n- Items now expose their audio **sample rate**, number of **channels**, and\n  **bits per sample** (bitdepth). See :doc:`/reference/pathformat` for a list of\n  all available audio properties. Thanks to Andrew Dunn.\n- The ``beet list`` command now accepts a \"format\" argument that lets you **show\n  specific information about each album or track**. For example, run ``beet ls\n  -af '$album: $tracktotal' beatles`` to see how long each Beatles album is.\n  Thanks to Philippe Mongeau.\n- The autotagger now tolerates tracks on multi-disc albums that are numbered\n  per-disc. For example, if track 24 on a release is the first track on the\n  second disc, then it is not penalized for having its track number set to 1\n  instead of 24.\n- The autotagger sets the disc number and disc total fields on autotagged\n  albums.\n- The autotagger now also tolerates tracks whose track artists tags are set to\n  \"Various Artists\".\n- Terminal colors are now supported on Windows via Colorama_ (thanks to Karl).\n- When previewing metadata differences, the importer now shows discrepancies in\n  track length.\n- Importing with ``import_delete`` enabled now cleans up empty directories that\n  contained deleting imported music files.\n- Similarly, ``import_delete`` now causes original album art imported from the\n  disk to be deleted.\n- Plugin-supplied template values, such as those created by ``rewrite``, are now\n  properly sanitized (for example, ``AC/DC`` properly becomes ``AC_DC``).\n- Filename extensions are now always lower-cased when copying and moving files.\n- The ``inline`` plugin now prints a more comprehensible error when exceptions\n  occur in Python snippets.\n- The ``replace`` configuration option can now remove characters entirely (in\n  addition to replacing them) if the special string ``<strip>`` is specified as\n  the replacement.\n- New plugin API: plugins can now add fields to the MediaFile tag abstraction\n  layer. See :ref:`basic-plugin-setup`.\n- A reasonable error message is now shown when the import log file cannot be\n  opened.\n- The import log file is now flushed and closed properly so that it can be used\n  to monitor import progress, even when the import crashes.\n- Duplicate track matches are no longer shown when autotagging singletons.\n- The ``chroma`` plugin now logs errors when fingerprinting fails.\n- The ``lastgenre`` plugin suppresses more errors when dealing with the Last.fm\n  API.\n- Fix a bug in the ``rewrite`` plugin that broke the use of multiple rules for a\n  single field.\n- Fix a crash with non-ASCII characters in bytestring metadata fields (e.g.,\n  MusicBrainz IDs).\n- Fix another crash with non-ASCII characters in the configuration paths.\n- Fix a divide-by-zero crash on zero-length audio files.\n- Fix a crash in the ``chroma`` plugin when the Acoustid database had no\n  recording associated with a fingerprint.\n- Fix a crash when an autotagging with an artist or album containing \"AND\" or\n  \"OR\" (upper case).\n- Fix an error in the ``rewrite`` and ``inline`` plugins when the corresponding\n  config sections did not exist.\n- Fix bitrate estimation for AAC files whose headers are missing the relevant\n  data.\n- Fix the ``list`` command in BPD (thanks to Simon Chopin).\n\n.. _colorama: https://pypi.org/project/colorama/\n\n1.0b12 (January 16, 2012)\n-------------------------\n\nThis release focuses on making beets' path formatting vastly more powerful. It\nadds a function syntax for transforming text. Via a new plugin, arbitrary Python\ncode can also be used to define new path format fields. Each path format\ntemplate can now be activated conditionally based on a query. Character set\nsubstitutions are also now configurable.\n\nIn addition, beets avoids problematic filename conflicts by appending numbers to\nfilenames that would otherwise conflict. Three new plugins (``inline``,\n``scrub``, and ``rewrite``) are included in this release.\n\n- **Functions in path formats** provide a simple way to write complex file\n  naming rules: for example, ``%upper{%left{$artist,1}}`` will insert the\n  capitalized first letter of the track's artist. For more details, see\n  :doc:`/reference/pathformat`. If you're interested in adding your own template\n  functions via a plugin, see :ref:`basic-plugin-setup`.\n- Plugins can also now define new path *fields* in addition to functions.\n- The new :doc:`/plugins/inline` lets you **use Python expressions to customize\n  path formats** by defining new fields in the config file.\n- The configuration can **condition path formats based on queries**. That is,\n  you can write a path format that is only used if an item matches a given\n  query. (This supersedes the earlier functionality that only allowed\n  conditioning on album type; if you used this feature in a previous version,\n  you will need to replace, for example, ``soundtrack:`` with\n  ``albumtype_soundtrack:``.) See :ref:`path-format-config`.\n- **Filename substitutions are now configurable** via the ``replace`` config\n  value. You can choose which characters you think should be allowed in your\n  directory and music file names. See :doc:`/reference/config`.\n- Beets now ensures that files have **unique filenames** by appending a number\n  to any filename that would otherwise conflict with an existing file.\n- The new :doc:`/plugins/scrub` can remove extraneous metadata either manually\n  or automatically.\n- The new :doc:`/plugins/rewrite` can canonicalize names for path formats.\n- The autotagging heuristics have been tweaked in situations where the\n  MusicBrainz database did not contain track lengths. Previously, beets\n  penalized matches where this was the case, leading to situations where\n  seemingly good matches would have poor similarity. This penalty has been\n  removed.\n- Fix an incompatibility in BPD with libmpc (the library that powers mpc and\n  ncmpc).\n- Fix a crash when importing a partial match whose first track was missing.\n- The ``lastgenre`` plugin now correctly writes discovered genres to imported\n  files (when tag-writing is enabled).\n- Add a message when skipping directories during an incremental import.\n- The default ignore settings now ignore all files beginning with a dot.\n- Date values in path formats (``$year``, ``$month``, and ``$day``) are now\n  appropriately zero-padded.\n- Removed the ``--path-format`` global flag for ``beet``.\n- Removed the ``lastid`` plugin, which was deprecated in the previous version.\n\n1.0b11 (December 12, 2011)\n--------------------------\n\nThis version of beets focuses on transitioning the autotagger to the new version\nof the MusicBrainz database (called NGS). This transition brings with it a\nnumber of long-overdue improvements: most notably, predictable behavior when\ntagging multi-disc albums and integration with the new Acoustid_ acoustic\nfingerprinting technology.\n\nThe importer can also now tag *incomplete* albums when you're missing a few\ntracks from a given release. Two other new plugins are also included with this\nrelease: one for assigning genres and another for ReplayGain analysis.\n\n- Beets now communicates with MusicBrainz via the new `Next Generation Schema`_\n  (NGS) service via python-musicbrainzngs_. The bindings are included with this\n  version of beets, but a future version will make them an external dependency.\n- The importer now detects **multi-disc albums** and tags them together. Using a\n  heuristic based on the names of directories, certain structures are classified\n  as multi-disc albums: for example, if a directory contains subdirectories\n  labeled \"disc 1\" and \"disc 2\", these subdirectories will be coalesced into a\n  single album for tagging.\n- The new :doc:`/plugins/chroma` uses the Acoustid_ **open-source acoustic\n  fingerprinting** service. This replaces the old ``lastid`` plugin, which used\n  Last.fm fingerprinting and is now deprecated. Fingerprinting with this library\n  should be faster and more reliable.\n- The importer can now perform **partial matches**. This means that, if you're\n  missing a few tracks from an album, beets can still tag the remaining tracks\n  as a single album. (Thanks to `Simon Chopin`_.)\n- The new :doc:`/plugins/lastgenre` automatically **assigns genres to imported\n  albums** and items based on Last.fm tags and an internal whitelist. (Thanks to\n  KraYmer_.)\n- The :doc:`/plugins/replaygain`, written by `Peter Brunner`_, has been merged\n  into the core beets distribution. Use it to analyze audio and **adjust\n  playback levels** in ReplayGain-aware music players.\n- Albums are now tagged with their *original* release date rather than the date\n  of any reissue, remaster, \"special edition\", or the like.\n- The config file and library databases are now given better names and locations\n  on Windows. Namely, both files now reside in ``%APPDATA%``; the config file is\n  named ``beetsconfig.ini`` and the database is called ``beetslibrary.blb``\n  (neither has a leading dot as on Unix). For backwards compatibility, beets\n  will check the old locations first.\n- When entering an ID manually during tagging, beets now searches for anything\n  that looks like an MBID in the entered string. This means that full\n  MusicBrainz URLs now work as IDs at the prompt. (Thanks to derwin.)\n- The importer now ignores certain \"clutter\" files like ``.AppleDouble``\n  directories and ``._*`` files. The list of ignored patterns is configurable\n  via the ``ignore`` setting; see :doc:`/reference/config`.\n- The database now keeps track of files' modification times so that, during an\n  ``update``, unmodified files can be skipped. (Thanks to Jos van der Til.)\n- The album art fetcher now uses albumart.org_ as a fallback when the Amazon art\n  downloader fails.\n- A new ``timeout`` config value avoids database locking errors on slow systems.\n- Fix a crash after using the \"as Tracks\" option during import.\n- Fix a Unicode error when tagging items with missing titles.\n- Fix a crash when the state file (``~/.beetsstate``) became emptied or\n  corrupted.\n\n.. _acoustid: https://acoustid.org/\n\n.. _albumart.org: https://web.archive.org/web/20191217041318/http://www.albumart.org/\n\n.. _kraymer: https://github.com/KraYmer\n\n.. _next generation schema: https://musicbrainz.org/doc/MusicBrainz_API\n\n.. _peter brunner: https://github.com/Lugoues\n\n.. _python-musicbrainzngs: https://github.com/alastair/python-musicbrainzngs\n\n.. _simon chopin: https://github.com/laarmen\n\n1.0b10 (September 22, 2011)\n---------------------------\n\nThis version of beets focuses on making it easier to manage your metadata\n*after* you've imported it. A bumper crop of new commands has been added: a\nmanual tag editor (``modify``), a tool to pick up out-of-band deletions and\nmodifications (``update``), and functionality for moving and copying files\naround (``move``). Furthermore, the concept of \"re-importing\" is new: you can\nchoose to re-run beets' advanced autotagger on any files you already have in\nyour library if you change your mind after you finish the initial import.\n\nAs a couple of added bonuses, imports can now automatically skip\npreviously-imported directories (with the ``-i`` flag) and there's an\n:doc:`experimental Web interface </plugins/web>` to beets in a new standard\nplugin.\n\n- A new ``beet modify`` command enables **manual, command-line-based\n  modification** of music metadata. Pass it a query along with ``field=value``\n  pairs that specify the changes you want to make.\n- A new ``beet update`` command updates the database to reflect **changes in the\n  on-disk metadata**. You can now use an external program to edit tags on files,\n  remove files and directories, etc., and then run ``beet update`` to make sure\n  your beets library is in sync. This will also rename files to reflect their\n  new metadata.\n- A new ``beet move`` command can **copy or move files** into your library\n  directory or to another specified directory.\n- When importing files that are already in the library database, the items are\n  no longer duplicated---instead, the library is updated to reflect the new\n  metadata. This way, the import command can be transparently used as a\n  **re-import**.\n- Relatedly, the ``-L`` flag to the \"import\" command makes it take a query as\n  its argument instead of a list of directories. The matched albums (or items,\n  depending on the ``-s`` flag) are then re-imported.\n- A new flag ``-i`` to the import command runs **incremental imports**, keeping\n  track of and skipping previously-imported directories. This has the effect of\n  making repeated import commands pick up only newly-added directories. The\n  ``import_incremental`` config option makes this the default.\n- When pruning directories, \"clutter\" files such as ``.DS_Store`` and\n  ``Thumbs.db`` are ignored (and removed with otherwise-empty directories).\n- The :doc:`/plugins/web` encapsulates a simple **Web-based GUI for beets**. The\n  current iteration can browse the library and play music in browsers that\n  support HTML5 Audio.\n- When moving items that are part of an album, the album art implicitly moves\n  too.\n- Files are no longer silently overwritten when moving and copying files.\n- Handle exceptions thrown when running Mutagen.\n- Fix a missing ``__future__`` import in ``embed art`` on Python 2.5.\n- Fix ID3 and MPEG-4 tag names for the album-artist field.\n- Fix Unicode encoding of album artist, album type, and label.\n- Fix crash when \"copying\" an art file that's already in place.\n\n1.0b9 (July 9, 2011)\n--------------------\n\nThis release focuses on a large number of small fixes and improvements that turn\nbeets into a well-oiled, music-devouring machine. See the full release notes,\nbelow, for a plethora of new features.\n\n- **Queries can now contain whitespace.** Spaces passed as shell arguments are\n  now preserved, so you can use your shell's escaping syntax (quotes or\n  backslashes, for instance) to include spaces in queries. For example, ``beet\n  ls \"the knife\"`` or ``beet ls theknife``. Read more in\n  :doc:`/reference/query`.\n- Queries can **match items from the library by directory**. A ``path:`` prefix\n  is optional; any query containing a path separator (/ on POSIX systems) is\n  assumed to be a path query. Running ``beet ls path/to/music`` will show all\n  the music in your library under the specified directory. The\n  :doc:`/reference/query` reference again has more details.\n- **Local album art** is now automatically discovered and copied from the\n  imported directories when available.\n- When choosing the \"as-is\" import album (or doing a non-autotagged import),\n  **every album either has an \"album artist\" set or is marked as a compilation\n  (Various Artists)**. The choice is made based on the homogeneity of the\n  tracks' artists. This prevents compilations that are imported as-is from being\n  scattered across many directories after they are imported.\n- The release **label** for albums and tracks is now fetched from !MusicBrainz,\n  written to files, and stored in the database.\n- The \"list\" command now accepts a ``-p`` switch that causes it to **show\n  paths** instead of titles. This makes the output of ``beet ls -p`` suitable\n  for piping into another command such as xargs_.\n- Release year and label are now shown in the candidate selection list to help\n  disambiguate different releases of the same album.\n- Prompts in the importer interface are now colorized for easy reading. The\n  default option is always highlighted.\n- The importer now provides the option to specify a MusicBrainz ID manually if\n  the built-in searching isn't working for a particular album or track.\n- ``$bitrate`` in path formats is now formatted as a human-readable kbps value\n  instead of as a raw integer.\n- The import logger has been improved for \"always-on\" use. First, it is now\n  possible to specify a log file in .beetsconfig. Also, logs are now appended\n  rather than overwritten and contain timestamps.\n- Album art fetching and plugin events are each now run in separate pipeline\n  stages during imports. This should bring additional performance when using\n  album art plugins like embedart or beets-lyrics.\n- Accents and other Unicode decorators on characters are now treated more fairly\n  by the autotagger. For example, if you're missing the acute accent on the \"e\"\n  in \"café\", that change won't be penalized. This introduces a new dependency on\n  the unidecode_ Python module.\n- When tagging a track with no title set, the track's filename is now shown\n  (instead of nothing at all).\n- The bitrate of lossless files is now calculated from their file size (rather\n  than being fixed at 0 or reflecting the uncompressed audio bitrate).\n- Fixed a problem where duplicate albums or items imported at the same time\n  would fail to be detected.\n- BPD now uses a persistent \"virtual filesystem\" in order to fake a directory\n  structure. This means that your path format settings are respected in BPD's\n  browsing hierarchy. This may come at a performance cost, however. The virtual\n  filesystem used by BPD is available for reuse by plugins (e.g., the FUSE\n  plugin).\n- Singleton imports (``beet import -s``) can now take individual files as\n  arguments as well as directories.\n- Fix Unicode queries given on the command line.\n- Fix crasher in quiet singleton imports (``import -qs``).\n- Fix crash when autotagging files with no metadata.\n- Fix a rare deadlock when finishing the import pipeline.\n- Fix an issue that was causing mpdupdate to run twice for every album.\n- Fix a bug that caused release dates/years not to be fetched.\n- Fix a crasher when setting MBIDs on MP3s file metadata.\n- Fix a \"broken pipe\" error when piping beets' standard output.\n- A better error message is given when the database file is unopenable.\n- Suppress errors due to timeouts and bad responses from MusicBrainz.\n- Fix a crash on album queries with item-only field names.\n\n.. _unidecode: https://pypi.org/project/Unidecode/0.04.1/\n\n.. _xargs: https://en.wikipedia.org/wiki/Xargs\n\n1.0b8 (April 28, 2011)\n----------------------\n\nThis release of beets brings two significant new features. First, beets now has\nfirst-class support for \"singleton\" tracks. Previously, it was only really meant\nto manage whole albums, but many of us have lots of non-album tracks to keep\ntrack of alongside our collections of albums. So now beets makes it easy to tag,\ncatalog, and manipulate your individual tracks. Second, beets can now\n(optionally) embed album art directly into file metadata rather than only\nstoring it in a \"file on the side.\" Check out the :doc:`/plugins/embedart` for\nthat functionality.\n\n- Better support for **singleton (non-album) tracks**. Whereas beets previously\n  only really supported full albums, now it can also keep track of individual,\n  off-album songs. The \"singleton\" path format can be used to customize where\n  these tracks are stored. To import singleton tracks, provide the -s switch to\n  the import command or, while doing a normal full-album import, choose the \"as\n  Tracks\" (T) option to add singletons to your library. To list only singleton\n  or only album tracks, use the new ``singleton:`` query term: the query\n  ``singleton:true`` matches only singleton tracks; ``singleton:false`` matches\n  only album tracks. The ``lastid`` plugin has been extended to support matching\n  individual items as well.\n- The importer/autotagger system has been heavily refactored in this release. If\n  anything breaks as a result, please get in touch or just file a bug.\n- Support for **album art embedded in files**. A new :doc:`/plugins/embedart`\n  implements this functionality. Enable the plugin to automatically embed\n  downloaded album art into your music files' metadata. The plugin also provides\n  the \"embedart\" and \"extractart\" commands for moving image files in and out of\n  metadata. See the wiki for more details. (Thanks, daenney!)\n- The \"distance\" number, which quantifies how different an album's current and\n  proposed metadata are, is now displayed as \"similarity\" instead. This should\n  be less noisy and confusing; you'll now see 99.5% instead of 0.00489323.\n- A new \"timid mode\" in the importer asks the user every time, even when it\n  makes a match with very high confidence. The ``-t`` flag on the command line\n  and the ``import_timid`` config option control this mode. (Thanks to mdecker\n  on GitHub!)\n- The multithreaded importer should now abort (either by selecting aBort or by\n  typing ^C) much more quickly. Previously, it would try to get a lot of work\n  done before quitting; now it gives up as soon as it can.\n- Added a new plugin event, ``album_imported``, which is called every time an\n  album is added to the library. (Thanks, Lugoues!)\n- A new plugin method, ``register_listener``, is an imperative alternative to\n  the ``@listen`` decorator (Thanks again, Lugoues!)\n- In path formats, ``$albumartist`` now falls back to ``$artist`` (as well as\n  the other way around).\n- The importer now prints \"(unknown album)\" when no tags are present.\n- When autotagging, \"and\" is considered equal to \"&\".\n- Fix some crashes when deleting files that don't exist.\n- Fix adding individual tracks in BPD.\n- Fix crash when ``~/.beetsconfig`` does not exist.\n\n1.0b7 (April 5, 2011)\n---------------------\n\nBeta 7's focus is on better support for \"various artists\" releases. These albums\ncan be treated differently via the new ``[paths]`` config section and the\nautotagger is better at handling them. It also includes a number of\noft-requested improvements to the ``beet`` command-line tool, including several\nnew configuration options and the ability to clean up empty directory subtrees.\n\n- **\"Various artists\" releases** are handled much more gracefully. The\n  autotagger now sets the ``comp`` flag on albums whenever the album is\n  identified as a \"various artists\" release by !MusicBrainz. Also, there is now\n  a distinction between the \"album artist\" and the \"track artist\", the latter of\n  which is never \"Various Artists\" or other such bogus stand-in. *(Thanks to\n  Jonathan for the bulk of the implementation work on this feature!)*\n- The directory hierarchy can now be **customized based on release type**. In\n  particular, the ``path_format`` setting in .beetsconfig has been replaced with\n  a new ``[paths]`` section, which allows you to specify different path formats\n  for normal and \"compilation\" (various artists) releases as well as for each\n  album type (see below). The default path formats have been changed to use\n  ``$albumartist`` instead of ``$artist``.\n- A **new** ``albumtype`` **field** reflects the release type `as specified by\n  MusicBrainz`_.\n- When deleting files, beets now appropriately \"prunes\" the directory\n  tree---empty directories are automatically cleaned up. *(Thanks to wlof on\n  GitHub for this!)*\n- The tagger's output now always shows the album directory that is currently\n  being tagged. This should help in situations where files' current tags are\n  missing or useless.\n- The logging option (``-l``) to the ``import`` command now logs duplicate\n  albums.\n- A new ``import_resume`` configuration option can be used to disable the\n  importer's resuming feature or force it to resume without asking. This option\n  may be either ``yes``, ``no``, or ``ask``, with the obvious meanings. The\n  ``-p`` and ``-P`` command-line flags override this setting and correspond to\n  the \"yes\" and \"no\" settings.\n- Resuming is automatically disabled when the importer is in quiet (``-q``)\n  mode. Progress is still saved, however, and the ``-p`` flag (above) can be\n  used to force resuming.\n- The ``BEETSCONFIG`` environment variable can now be used to specify the\n  location of the config file that is at ~/.beetsconfig by default.\n- A new ``import_quiet_fallback`` config option specifies what should happen in\n  quiet mode when there is no strong recommendation. The options are ``skip``\n  (the default) and \"asis\".\n- When importing with the \"delete\" option and importing files that are already\n  at their destination, files could be deleted (leaving zero copies afterward).\n  This is fixed.\n- The ``version`` command now lists all the loaded plugins.\n- A new plugin, called ``info``, just prints out audio file metadata.\n- Fix a bug where some files would be erroneously interpreted as MPEG-4 audio.\n- Fix permission bits applied to album art files.\n- Fix malformed !MusicBrainz queries caused by null characters.\n- Fix a bug with old versions of the Monkey's Audio format.\n- Fix a crash on broken symbolic links.\n- Retry in more cases when !MusicBrainz servers are slow/overloaded.\n- The old \"albumify\" plugin for upgrading databases was removed.\n\n.. _as specified by musicbrainz: https://wiki.musicbrainz.org/ReleaseType\n\n1.0b6 (January 20, 2011)\n------------------------\n\nThis version consists primarily of bug fixes and other small improvements. It's\nin preparation for a more feature-ful release in beta 7. The most important\nissue involves correct ordering of autotagged albums.\n\n- **Quiet import:** a new \"-q\" command line switch for the import command\n  suppresses all prompts for input; it pessimistically skips all albums that the\n  importer is not completely confident about.\n- Added support for the **WavPack** and **Musepack** formats. Unfortunately, due\n  to a limitation in the Mutagen library (used by beets for metadata\n  manipulation), Musepack SV8 is not yet supported. Here's the `upstream bug`_\n  in question.\n- BPD now uses a pure-Python socket library and no longer requires\n  eventlet/greenlet (the latter of which is a C extension). For the curious, the\n  socket library in question is called Bluelet_.\n- Non-autotagged imports are now resumable (just like autotagged imports).\n- Fix a terrible and long-standing bug where track orderings were never applied.\n  This manifested when the tagger appeared to be applying a reasonable ordering\n  to the tracks but, later, the database reflects a completely wrong association\n  of track names to files. The order applied was always just alphabetical by\n  filename, which is frequently but not always what you want.\n- We now use Windows' \"long filename\" support. This API is fairly tricky,\n  though, so some instability may still be present---please file a bug if you\n  run into pathname weirdness on Windows. Also, filenames on Windows now never\n  end in spaces.\n- Fix crash in lastid when the artist name is not available.\n- Fixed a spurious crash when ``LANG`` or a related environment variable is set\n  to an invalid value (such as ``'UTF-8'`` on some installations of Mac OS X).\n- Fixed an error when trying to copy a file that is already at its destination.\n- When copying read-only files, the importer now tries to make the copy\n  writable. (Previously, this would just crash the import.)\n- Fixed an ``UnboundLocalError`` when no matches are found during autotag.\n- Fixed a Unicode encoding error when entering special characters into the\n  \"manual search\" prompt.\n- Added ``beet version`` command that just shows the current release version.\n\n.. _bluelet: https://github.com/sampsyo/bluelet\n\n.. _upstream bug: https://github.com/quodlibet/mutagen/issues/7\n\n1.0b5 (September 28, 2010)\n--------------------------\n\nThis version of beets focuses on increasing the accuracy of the autotagger. The\nmain addition is an included plugin that uses acoustic fingerprinting to match\nbased on the audio content (rather than existing metadata). Additional\nheuristics were also added to the metadata-based tagger as well that should make\nit more reliable. This release also greatly expands the capabilities of beets'\n:doc:`plugin API </plugins/index>`. A host of other little features and fixes\nare also rolled into this release.\n\n- The ``lastid`` plugin adds Last.fm **acoustic fingerprinting support** to the\n  autotagger. Similar to the PUIDs used by !MusicBrainz Picard, this system\n  allows beets to recognize files that don't have any metadata at all. You'll\n  need to install some dependencies for this plugin to work.\n- To support the above, there's also a new system for **extending the autotagger\n  via plugins**. Plugins can currently add components to the track and album\n  distance functions as well as augment the MusicBrainz search. The new API is\n  documented at :doc:`/plugins/index`.\n- **String comparisons** in the autotagger have been augmented to act more\n  intuitively. Previously, if your album had the title \"Something (EP)\" and it\n  was officially called \"Something\", then beets would think this was a fairly\n  significant change. It now checks for and appropriately reweights certain\n  parts of each string. As another example, the title \"The Great Album\" is\n  considered equal to \"Great Album, The\".\n- New **event system for plugins** (thanks, Jeff!). Plugins can now get\n  callbacks from beets when certain events occur in the core. Again, the API is\n  documented in :doc:`/plugins/index`.\n- The BPD plugin is now disabled by default. This greatly simplifies\n  installation of the beets core, which is now 100% pure Python. To use BPD,\n  though, you'll need to set ``plugins: bpd`` in your .beetsconfig.\n- The ``import`` command can now remove original files when it copies items into\n  your library. (This might be useful if you're low on disk space.) Set the\n  ``import_delete`` option in your .beetsconfig to ``yes``.\n- Importing without autotagging (``beet import -A``) now prints out album names\n  as it imports them to indicate progress.\n- The new :doc:`/plugins/mpdupdate` will automatically update your MPD server's\n  index whenever your beets library changes.\n- Efficiency tweak should reduce the number of !MusicBrainz queries per\n  autotagged album.\n- A new ``-v`` command line switch enables debugging output.\n- Fixed bug that completely broke non-autotagged imports (``import -A``).\n- Fixed bug that logged the wrong paths when using ``import -l``.\n- Fixed autotagging for the creatively-named band `!!!`_.\n- Fixed normalization of relative paths.\n- Fixed escaping of ``/`` characters in paths on Windows.\n\n.. _!!!: https://musicbrainz.org/artist/f26c72d3-e52c-467b-b651-679c73d8e1a7\n\n1.0b4 (August 9, 2010)\n----------------------\n\nThis thrilling new release of beets focuses on making the tagger more usable in\na variety of ways. First and foremost, it should now be much faster: the tagger\nnow uses a multithreaded algorithm by default (although, because the new tagger\nis experimental, a single-threaded version is still available via a config\noption). Second, the tagger output now uses a little bit of ANSI terminal\ncoloring to make changes stand out. This way, it should be faster to decide what\nto do with a proposed match: the more red you see, the worse the match is.\nFinally, the tagger can be safely interrupted (paused) and restarted later at\nthe same point. Just enter ``b`` for aBort at any prompt to stop the tagging\nprocess and save its progress. (The progress-saving also works in the\nunthinkable event that beets crashes while tagging.)\n\nAmong the under-the-hood changes in 1.0b4 is a major change to the way beets\nhandles paths (filenames). This should make the whole system more tolerant to\nspecial characters in filenames, but it may break things (especially databases\ncreated with older versions of beets). As always, let me know if you run into\nweird problems with this release.\n\nFinally, this release's ``setup.py`` should install a ``beet.exe`` startup stub\nfor Windows users. This should make running beets much easier: just type\n``beet`` if you have your ``PATH`` environment variable set up correctly. The\n:doc:`/guides/main` guide has some tips on installing beets on Windows.\n\nHere's the detailed list of changes\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n- **Parallel tagger.** The autotagger has been reimplemented to use multiple\n  threads. This means that it can concurrently read files from disk, talk to the\n  user, communicate with MusicBrainz, and write data back to disk. Not only does\n  this make the tagger much faster because independent work may be performed in\n  parallel, but it makes the tagging process much more pleasant for large\n  imports. The user can let albums queue up in the background while making a\n  decision rather than waiting for beets between each question it asks. The\n  parallel tagger is on by default but a sequential (single- threaded) version\n  is still available by setting the ``threaded`` config value to ``no`` (because\n  the parallel version is still quite experimental).\n- **Colorized tagger output.** The autotagger interface now makes it a little\n  easier to see what's going on at a glance by highlighting changes with\n  terminal colors. This feature is on by default, but you can turn it off by\n  setting ``color`` to ``no`` in your ``.beetsconfig`` (if, for example, your\n  terminal doesn't understand colors and garbles the output).\n- **Pause and resume imports.** The ``import`` command now keeps track of its\n  progress, so if you're interrupted (beets crashes, you abort the process, an\n  alien devours your motherboard, etc.), beets will try to resume from the point\n  where you left off. The next time you run ``import`` on the same directory, it\n  will ask if you want to resume. It accomplishes this by \"fast-forwarding\"\n  through the albums in the directory until it encounters the last one it saw.\n  (This means it might fail if that album can't be found.) Also, you can now\n  abort the tagging process by entering ``b`` (for aBort) at any of the prompts.\n- Overhauled methods for handling filesystem paths to allow filenames that have\n  badly encoded special characters. These changes are pretty fragile, so please\n  report any bugs involving ``UnicodeError`` or SQLite ``ProgrammingError``\n  messages in this version.\n- The destination paths (the library directory structure) now respect\n  album-level metadata. This means that if you have an album in which two tracks\n  have different album-level attributes (like year, for instance), they will\n  still wind up in the same directory together. (There's currently not a very\n  smart method for picking the \"correct\" album-level metadata, but we'll fix\n  that later.)\n- Fixed a bug where the CLI would fail completely if the ``LANG`` environment\n  variable was not set.\n- Fixed removal of albums (``beet remove -a``): previously, the album record\n  would stay around although the items were deleted.\n- The setup script now makes a ``beet.exe`` startup stub on Windows; Windows\n  users can now just type ``beet`` at the prompt to run beets.\n- Fixed an occasional bug where Mutagen would complain that a tag was already\n  present.\n- Fixed a bug with reading invalid integers from ID3 tags.\n- The tagger should now be a little more reluctant to reorder tracks that\n  already have indices.\n\n1.0b3 (July 22, 2010)\n---------------------\n\nThis release features two major additions to the autotagger's functionality:\nalbum art fetching and MusicBrainz ID tags. It also contains some important\nunder-the-hood improvements: a new plugin architecture is introduced and the\ndatabase schema is extended with explicit support for albums.\n\nThis release has one major backwards-incompatibility. Because of the new way\nbeets handles albums in the library, databases created with an old version of\nbeets might have trouble with operations that deal with albums (like the ``-a``\nswitch to ``beet list`` and ``beet remove``, as well as the file browser for\nBPD). To \"upgrade\" an old database, you can use the included ``albumify`` plugin\n(see the fourth bullet point below).\n\n- **Album art.** The tagger now, by default, downloads album art from Amazon\n  that is referenced in the MusicBrainz database. It places the album art\n  alongside the audio files in a file called (for example) ``cover.jpg``. The\n  ``import_art`` config option controls this behavior, as do the ``-r`` and\n  ``-R`` options to the import command. You can set the name (minus extension)\n  of the album art file with the ``art_filename`` config option. (See\n  :doc:`/reference/config` for more information about how to configure the album\n  art downloader.)\n- **Support for MusicBrainz ID tags.** The autotagger now keeps track of the\n  MusicBrainz track, album, and artist IDs it matched for each file. It also\n  looks for album IDs in new files it's importing and uses those to look up data\n  in MusicBrainz. Furthermore, track IDs are used as a component of the tagger's\n  distance metric now. (This obviously lays the groundwork for a utility that\n  can update tags if the MB database changes, but that's `for the future`_.)\n  Tangentially, this change required the database code to support a lightweight\n  form of migrations so that new columns could be added to old databases--this\n  is a delicate feature, so it would be very wise to make a backup of your\n  database before upgrading to this version.\n- **Plugin architecture.** Add-on modules can now add new commands to the beets\n  command-line interface. The ``bpd`` and ``dadd`` commands were removed from\n  the beets core and turned into plugins; BPD is loaded by default. To load the\n  non-default plugins, use the config options ``plugins`` (a space-separated\n  list of plugin names) and ``pluginpath`` (a colon-separated list of\n  directories to search beyond ``sys.path``). Plugins are just Python modules\n  under the ``beetsplug`` namespace package containing subclasses of\n  |BeetsPlugin|. See `the beetsplug directory`_ for examples or\n  :doc:`/plugins/index` for instructions.\n- As a consequence of adding album art, the database was significantly\n  refactored to keep track of some information at an album (rather than item)\n  granularity. Databases created with earlier versions of beets should work\n  fine, but they won't have any \"albums\" in them--they'll just be a bag of\n  items. This means that commands like ``beet ls -a`` and ``beet rm -a`` won't\n  match anything. To \"upgrade\" your database, you can use the included\n  ``albumify`` plugin. Running ``beets albumify`` with the plugin activated (set\n  ``plugins=albumify`` in your config file) will group all your items into\n  albums, making beets behave more or less as it did before.\n- Fixed some bugs with encoding paths on Windows. Also, ``:`` is now replaced\n  with ``-`` in path names (instead of ``_``) for readability.\n- ``MediaFile`` now has a ``format`` attribute, so you can use ``$format`` in\n  your library path format strings like ``$artist - $album ($format)`` to get\n  directories with names like ``Paul Simon - Graceland (FLAC)``.\n\n.. _for the future: https://github.com/google-code-export/beets/issues/69\n\n.. _the beetsplug directory: https://github.com/beetbox/beets/tree/master/beetsplug\n\nBeets also now has its first third-party plugin: beetfs_, by Martin Eve! It\nexposes your music in a FUSE filesystem using a custom directory structure. Even\ncooler: it lets you keep your files intact on-disk while correcting their tags\nwhen accessed through FUSE. Check it out!\n\n.. _beetfs: https://github.com/jbaiter/beetfs\n\n1.0b2 (July 7, 2010)\n--------------------\n\nThis release focuses on high-priority fixes and conspicuously missing features.\nHighlights include support for two new audio formats (Monkey's Audio and Ogg\nVorbis) and an option to log untaggable albums during import.\n\n- **Support for Ogg Vorbis and Monkey's Audio** files and their tags. (This\n  support should be considered preliminary: I haven't tested it heavily because\n  I don't use either of these formats regularly.)\n- An option to the ``beet import`` command for **logging albums that are\n  untaggable** (i.e., are skipped or taken \"as-is\"). Use ``beet import -l\n  LOGFILE PATHS``. The log format is very simple: it's just a status (either\n  \"skip\" or \"asis\") followed by the path to the album in question. The idea is\n  that you can tag a large collection and automatically keep track of the albums\n  that weren't found in MusicBrainz so you can come back and look at them later.\n- Fixed a ``UnicodeEncodeError`` on terminals that don't (or don't claim to)\n  support UTF-8.\n- Importing without autotagging (``beet import -A``) is now faster and doesn't\n  print out a bunch of whitespace. It also lets you specify single files on the\n  command line (rather than just directories).\n- Fixed importer crash when attempting to read a corrupt file.\n- Reorganized code for CLI in preparation for adding pluggable subcommands. Also\n  removed dependency on the aging ``cmdln`` module in favor of `a hand-rolled\n  solution`_.\n\n.. _a hand-rolled solution: https://gist.github.com/sampsyo/462717\n\n1.0b1 (June 17, 2010)\n---------------------\n\nInitial release.\n"
  },
  {
    "path": "docs/code_of_conduct.rst",
    "content": ".. code_of_conduct:\n\n.. include:: ../CODE_OF_CONDUCT.rst\n"
  },
  {
    "path": "docs/conf.py",
    "content": "# Configuration file for the Sphinx documentation builder.\n#\n# For the full list of built-in configuration values, see the documentation:\n# https://www.sphinx-doc.org/en/master/usage/configuration.html\n\n# -- Project information -----------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information\n\nimport sys\nfrom pathlib import Path\n\n# Add custom extensions directory to path\nsys.path.insert(0, str(Path(__file__).parent / \"extensions\"))\n\nproject = \"beets\"\nAUTHOR = \"Adrian Sampson\"\ncopyright = \"2016, Adrian Sampson\"\n\nmaster_doc = \"index\"\nlanguage = \"en\"\nversion = \"2.7\"\nrelease = \"2.7.1\"\n\n# -- General configuration ---------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration\n\nextensions = [\n    \"sphinx.ext.autodoc\",\n    \"sphinx.ext.autosummary\",\n    \"sphinx.ext.extlinks\",\n    \"sphinx.ext.viewcode\",\n    \"sphinx_design\",\n    \"sphinx_copybutton\",\n    \"conf\",\n    \"sphinx_toolbox.more_autodoc.autotypeddict\",\n    \"sphinx_toolbox.more_autodoc.autonamedtuple\",\n]\n\nautosummary_generate = True\nautosummary_context = {\n    \"related_typeddicts\": {\n        \"MusicBrainzAPI\": [\n            \"beetsplug._utils.musicbrainz.LookupKwargs\",\n            \"beetsplug._utils.musicbrainz.SearchKwargs\",\n            \"beetsplug._utils.musicbrainz.BrowseKwargs\",\n            \"beetsplug._utils.musicbrainz.BrowseRecordingsKwargs\",\n            \"beetsplug._utils.musicbrainz.BrowseReleaseGroupsKwargs\",\n        ],\n    }\n}\nautodoc_member_order = \"bysource\"\nexclude_patterns = [\"_build\"]\ntemplates_path = [\"_templates\"]\nsource_suffix = {\".rst\": \"restructuredtext\", \".md\": \"markdown\"}\n\npygments_style = \"sphinx\"\n\n# External links to the bug tracker and other sites.\nextlinks = {\n    \"bug\": (\"https://github.com/beetbox/beets/issues/%s\", \"#%s\"),\n    \"user\": (\"https://github.com/%s\", \"%s\"),\n    \"pypi\": (\"https://pypi.org/project/%s/\", \"%s\"),\n    \"stdlib\": (\"https://docs.python.org/3/library/%s.html\", \"%s\"),\n}\n\nlinkcheck_ignore = [\n    r\"https://github.com/beetbox/beets/issues/\",\n    r\"https://github.com/[^/]+$\",  # ignore user pages\n    r\".*localhost.*\",\n    r\"https?://127\\.0\\.0\\.1\",\n    r\"https://www.musixmatch.com/\",  # blocks requests\n    r\"https://genius.com/\",  # blocks requests\n    r\"https://sourceforge\\.net/\",  # blocks requests\n    r\"https://[^/]*fanart\\.tv/\",  # blocks requests\n    r\"https://[^/]*fandom\\.com/\",  # blocks requests\n    r\"https://imgur\\.com/\",  # not accessible from the UK\n    r\"https://www.discogs.com/settings/developers\",  # requires login\n]\n\n# Options for HTML output\nhtmlhelp_basename = \"beetsdoc\"\n\n# Options for LaTeX output\nlatex_documents = [\n    (\"index\", \"beets.tex\", \"beets Documentation\", AUTHOR, \"manual\"),\n]\n\n# Options for manual page output\nman_pages = [\n    (\n        \"reference/cli\",\n        \"beet\",\n        \"music tagger and library organizer\",\n        [AUTHOR],\n        1,\n    ),\n    (\n        \"reference/config\",\n        \"beetsconfig\",\n        \"beets configuration file\",\n        [AUTHOR],\n        5,\n    ),\n]\n\n# Global substitutions that can be used anywhere in the documentation.\nrst_epilog = r\"\"\"\n.. |Album| replace:: :class:`~beets.library.models.Album`\n.. |AlbumInfo| replace:: :class:`beets.autotag.hooks.AlbumInfo`\n.. |BeetsPlugin| replace:: :class:`beets.plugins.BeetsPlugin`\n.. |ImportSession| replace:: :class:`~beets.importer.session.ImportSession`\n.. |ImportTask| replace:: :class:`~beets.importer.tasks.ImportTask`\n.. |Item| replace:: :class:`~beets.library.models.Item`\n.. |Library| replace:: :class:`~beets.library.library.Library`\n.. |Model| replace:: :class:`~beets.dbcore.db.Model`\n.. |TrackInfo| replace:: :class:`beets.autotag.hooks.TrackInfo`\n.. |semicolon_space| replace:: :literal:`; \\ `\n\"\"\"\n\n# -- Options for HTML output -------------------------------------------------\n# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output\n\n\nhtml_theme = \"pydata_sphinx_theme\"\nhtml_theme_options = {\n    \"collapse_navigation\": False,\n    \"logo\": {\"text\": \"beets\"},\n    \"show_nav_level\": 2,  # How many levels in left sidebar to show automatically\n    \"navigation_depth\": 4,  # How many levels of navigation to expand\n}\nhtml_title = \"beets\"\nhtml_logo = \"_static/beets_logo_nobg.png\"\nhtml_static_path = [\"_static\"]\nhtml_css_files = [\"beets.css\"]\n\n\ndef skip_member(app, what, name, obj, skip, options):\n    if name.startswith(\"_\"):\n        return True\n    return skip\n\n\ndef setup(app):\n    app.connect(\"autodoc-skip-member\", skip_member)\n"
  },
  {
    "path": "docs/contributing.rst",
    "content": ".. contributing:\n\n.. include:: ../CONTRIBUTING.rst\n"
  },
  {
    "path": "docs/dev/cli.rst",
    "content": "Providing a CLI\n===============\n\nThe ``beets.ui`` module houses interactions with the user via a terminal, the\n:doc:`/reference/cli`. The main function is called when the user types beet on\nthe command line. The CLI functionality is organized into commands, some of\nwhich are built-in and some of which are provided by plugins. The built-in\ncommands are all implemented in the ``beets.ui.commands`` submodule.\n"
  },
  {
    "path": "docs/dev/importer.rst",
    "content": "Music Importer\n==============\n\nThe importer component is responsible for the user-centric workflow that adds\nmusic to a library. This is one of the first aspects that a user experiences\nwhen using beets: it finds music in the filesystem, groups it into albums, finds\ncorresponding metadata in MusicBrainz, asks the user for intervention, applies\nchanges, and moves/copies files. A description of its user interface is given in\n:doc:`/guides/tagger`.\n\nThe workflow is implemented in the ``beets.importer`` module and is distinct\nfrom the core logic for matching MusicBrainz metadata (in the ``beets.autotag``\nmodule). The workflow is also decoupled from the command-line interface with the\nhope that, eventually, other (graphical) interfaces can be bolted onto the same\nimporter implementation.\n\nThe importer is multithreaded and follows the pipeline pattern. Each pipeline\nstage is a Python coroutine. The ``beets.util.pipeline`` module houses a\ngeneric, reusable implementation of a multithreaded pipeline.\n"
  },
  {
    "path": "docs/dev/index.rst",
    "content": "For Developers\n==============\n\nThis section contains information for developers. Read on if you're interested\nin hacking beets itself or creating plugins for it.\n\nSee also the documentation for the MediaFile_ and Confuse_ libraries. These are\nmaintained by the beets team and used to read and write metadata tags and manage\nconfiguration files, respectively.\n\n.. _confuse: https://confuse.readthedocs.io/en/latest/\n\n.. _mediafile: https://mediafile.readthedocs.io/en/latest/\n\n.. toctree::\n    :maxdepth: 3\n    :titlesonly:\n\n    plugins/index\n    library\n    paths\n    importer\n    cli\n    ../api/index\n"
  },
  {
    "path": "docs/dev/library.rst",
    "content": "Library Database API\n====================\n\n.. currentmodule:: beets.library\n\nThis page describes the internal API of beets' core database features. It\ndoesn't exhaustively document the API, but is aimed at giving an overview of the\narchitecture to orient anyone who wants to dive into the code.\n\nThe |Library| object is the central repository for data in beets. It represents\na database containing songs, which are |Item| instances, and groups of items,\nwhich are |Album| instances.\n\nThe Library Class\n-----------------\n\nThe |Library| is typically instantiated as a singleton. A single invocation of\nbeets usually has only one |Library|. It's powered by :class:`dbcore.Database`\nunder the hood, which handles the SQLite_ abstraction, something like a very\nminimal ORM_. The library is also responsible for handling queries to retrieve\nstored objects.\n\nOverview\n~~~~~~~~\n\nYou can add new items or albums to the library via the :py:meth:`Library.add`\nand :py:meth:`Library.add_album` methods.\n\nYou may also query the library for items and albums using the\n:py:meth:`Library.items`, :py:meth:`Library.albums`, :py:meth:`Library.get_item`\nand :py:meth:`Library.get_album` methods.\n\nAny modifications to the library must go through a :class:`Transaction` object,\nwhich you can get using the :py:meth:`Library.transaction` context manager.\n\n.. _orm: https://en.wikipedia.org/wiki/Object-relational_mapping\n\n.. _sqlite: https://sqlite.org/index.html\n\nModel Classes\n-------------\n\nThe two model entities in beets libraries, |Item| and |Album|, share a base\nclass, :class:`LibModel`, that provides common functionality. That class itself\nspecialises :class:`beets.dbcore.Model` which provides an ORM-like abstraction.\n\nTo get or change the metadata of a model (an item or album), either access its\nattributes (e.g., ``print(album.year)`` or ``album.year = 2012``) or use the\n``dict``-like interface (e.g. ``item['artist']``).\n\nModel base\n~~~~~~~~~~\n\nModels use dirty-flags to track when the object's metadata goes out of sync with\nthe database. The dirty dictionary maps field names to booleans indicating\nwhether the field has been written since the object was last synchronized (via\nload or store) with the database. This logic is implemented in the model base\nclass :class:`LibModel` and is inherited by both |Item| and |Album|.\n\nWe provide CRUD-like methods for interacting with the database:\n\n- :py:meth:`LibModel.store`\n- :py:meth:`LibModel.load`\n- :py:meth:`LibModel.remove`\n- :py:meth:`LibModel.add`\n\nThe base class :class:`beets.dbcore.Model` has a ``dict``-like interface, so\nnormal the normal mapping API is supported:\n\n- :py:meth:`LibModel.keys`\n- :py:meth:`LibModel.update`\n- :py:meth:`LibModel.items`\n- :py:meth:`LibModel.get`\n\nItem\n~~~~\n\nEach |Item| object represents a song or track. (We use the more generic term\nitem because, one day, beets might support non-music media.) An item can either\nbe purely abstract, in which case it's just a bag of metadata fields, or it can\nhave an associated file (indicated by ``item.path``).\n\nIn terms of the underlying SQLite database, items are backed by a single table\ncalled items with one column per metadata fields. The metadata fields currently\nin use are listed in ``library.py`` in ``Item._fields``.\n\nTo read and write a file's tags, we use the MediaFile_ library. To make changes\nto either the database or the tags on a file, you update an item's fields (e.g.,\n``item.title = \"Let It Be\"``) and then call ``item.write()``.\n\n.. _mediafile: https://mediafile.readthedocs.io/en/latest/\n\nItems also track their modification times (mtimes) to help detect when they\nbecome out of sync with on-disk metadata, mainly to speed up the\n:ref:`update-cmd` (which needs to check whether the database is in sync with the\nfilesystem). This feature turns out to be sort of complicated.\n\nFor any |Item|, there are two mtimes: the on-disk mtime (maintained by the OS)\nand the database mtime (maintained by beets). Correspondingly, there is on-disk\nmetadata (ID3 tags, for example) and DB metadata. The goal with the mtime is to\nensure that the on-disk and DB mtimes match when the on-disk and DB metadata are\nin sync; this lets beets do a quick mtime check and avoid rereading files in\nsome circumstances.\n\nSpecifically, beets attempts to maintain the following invariant:\n\n    If the on-disk metadata differs from the DB metadata, then the on-disk mtime\n    must be greater than the DB mtime.\n\nAs a result, it is always valid for the DB mtime to be zero (assuming that real\ndisk mtimes are always positive). However, whenever possible, beets tries to set\n``db_mtime = disk_mtime`` at points where it knows the metadata is synchronized.\nWhen it is possible that the metadata is out of sync, beets can then just set\n``db_mtime = 0`` to return to a consistent state.\n\nThis leads to the following implementation policy:\n\n    - On every write of disk metadata (``Item.write()``), the DB mtime is\n      updated to match the post-write disk mtime.\n    - Same for metadata reads (``Item.read()``).\n    - On every modification to DB metadata (``item.field = ...``), the DB mtime\n      is reset to zero.\n\nAlbum\n~~~~~\n\nAn |Album| is a collection of Items in the database. Every item in the database\nhas either zero or one associated albums (accessible via ``item.album_id``). An\nitem that has no associated album is called a singleton. Changing fields on an\nalbum (e.g. ``album.year = 2012``) updates the album itself and also changes the\nsame field in all associated items.\n\nAn |Album| object keeps track of album-level metadata, which is (mostly) a\nsubset of the track-level metadata. The album-level metadata fields are listed\nin ``Album._fields``. For those fields that are both item-level and album-level\n(e.g., ``year`` or ``albumartist``), every item in an album should share the\nsame value. Albums use an SQLite table called ``albums``, in which each column\nis an album metadata field.\n\n.. note::\n\n    The :py:meth:`Album.items` method is not inherited from\n    :py:meth:`LibModel.items` for historical reasons.\n\nTransactions\n~~~~~~~~~~~~\n\nThe |Library| class provides the basic methods necessary to access and\nmanipulate its contents. To perform more complicated operations atomically, or\nto interact directly with the underlying SQLite database, you must use a\n*transaction* (see this `blog post`_ for motivation). For example\n\n.. code-block:: python\n\n    lib = Library()\n    with lib.transaction() as tx:\n        items = lib.items(query)\n        lib.add_album(list(items))\n\n.. currentmodule:: beets.dbcore.db\n\nThe :class:`Transaction` class is a context manager that provides a\ntransactional interface to the underlying SQLite database. It is responsible for\nmanaging the transaction's lifecycle, including beginning, committing, and\nrolling back the transaction if an error occurs.\n\n.. _blog post: https://beets.io/blog/sqlite-nightmare.html\n\nMigrations\n~~~~~~~~~~\n\nThe database layer includes a first-class migration system for data changes that\nmust happen alongside schema evolution. This keeps compatibility work explicit,\ntestable, and isolated from normal query and model code.\n\nEach database subclass declares its migrations in ``_migrations`` as pairs of a\nmigration class and the model classes it applies to. During startup, the\ndatabase creates required tables and columns first, then executes configured\nmigrations.\n\nMigration completion is tracked in a dedicated ``migrations`` table keyed by\nmigration name and table name. This means each migration runs at most once per\ntable, so large one-time data rewrites can be safely coordinated across\nrestarts.\n\nThe migration name is derived from the migration class name. Because that name\nis the persisted identity in the ``migrations`` table, renaming a released\nmigration class changes its identity and can cause the migration to run again.\nTreat migration class names as stable once shipped.\n\nFor example, ``MultiGenreFieldMigration`` becomes ``multi_genre_field``. After\nit runs for the ``items`` table, beets records a row equivalent to:\n\n.. code-block:: text\n\n    name = \"multi_genre_field\", table_name = \"items\"\n\nCommon use cases include:\n\n1. Backfilling a newly introduced canonical field from older data.\n2. Normalizing legacy free-form values into a structured representation.\n3. Splitting mixed-content legacy fields into cleaned primary content plus\n   auxiliary metadata stored as flexible attributes.\n\nTo add a migration:\n\n1. Create a :class:`beets.dbcore.db.Migration` subclass.\n2. Implement the table-specific data rewrite logic in ``_migrate_data``.\n3. Register the migration in the database subclass ``_migrations`` list for the\n   target models.\n\nIn practice, migrations should be idempotent and conservative: gate behavior on\nthe current schema when needed, keep writes transactional, and batch large\nupdates so startup remains predictable for real libraries.\n\nQueries\n-------\n\n.. currentmodule:: beets.dbcore.query\n\nTo access albums and items in a library, we use :doc:`/reference/query`. In\nbeets, the :class:`Query` abstract base class represents a criterion that\nmatches items or albums in the database. Every subclass of :class:`Query` must\nimplement two methods, which implement two different ways of identifying\nmatching items/albums.\n\nThe ``clause()`` method should return an SQLite ``WHERE`` clause that matches\nappropriate albums/items. This allows for efficient batch queries.\nCorrespondingly, the ``match(item)`` method should take an |Item| object and\nreturn a boolean, indicating whether or not a specific item matches the\ncriterion. This alternate implementation allows clients to determine whether\nitems that have already been fetched from the database match the query.\n\nThere are many different types of queries. Just as an example,\n:class:`FieldQuery` determines whether a certain field matches a certain value\n(an equality query). :class:`AndQuery` (like its abstract superclass,\n:class:`CollectionQuery`) takes a set of other query objects and bundles them\ntogether, matching only albums/items that match all constituent queries.\n\nBeets has a human-writable plain-text query syntax that can be parsed into\n:class:`Query` objects. Calling ``AndQuery.from_strings`` parses a list of query\nparts into a query object that can then be used with |Library| objects.\n"
  },
  {
    "path": "docs/dev/paths.rst",
    "content": "Handling Paths\n==============\n\n``pathlib`` provides a clean, cross-platform API for working with filesystem\npaths.\n\nUse the ``.filepath`` property on ``Item`` and ``Album`` library objects to\naccess paths as ``pathlib.Path`` objects. This produces a readable, native\nrepresentation suitable for printing, logging, or further processing.\n\nNormalize paths using ``Path(...).expanduser().resolve()``, which expands ``~``\nand resolves symlinks.\n\nCross-platform differences—such as path separators, Unicode handling, and\nlong-path support (Windows) are automatically managed by ``pathlib``.\n\nWhen storing paths in the database, however, convert them to bytes with\n``bytestring_path()``. Paths in Beets are currently stored as bytes, although\nthere are plans to eventually store ``pathlib.Path`` objects directly. To access\nmedia file paths in their stored form, use the ``.path`` property on ``Item``\nand ``Album``.\n\nLegacy utilities\n----------------\n\nHistorically, Beets used custom utilities to ensure consistent behavior across\nLinux, macOS, and Windows before ``pathlib`` became reliable:\n\n- ``syspath()``: worked around Windows Unicode and long-path limitations by\n  converting to a system-safe string (adding the ``\\\\?\\`` prefix where needed).\n- ``normpath()``: normalized slashes and removed ``./`` or ``..`` parts but did\n  not expand ``~``.\n- ``bytestring_path()``: converted paths to bytes for database storage (still\n  used for that purpose today).\n- ``displayable_path()``: converted byte paths to Unicode for display or\n  logging.\n\nThese functions remain safe to use in legacy code, but new code should rely\nsolely on ``pathlib.Path``.\n\nExamples\n--------\n\nOld style\n\n.. code-block:: python\n\n    displayable_path(item.path)\n    normpath(\"~/Music/../Artist\")\n    syspath(path)\n\nNew style\n\n.. code-block:: python\n\n    item.filepath\n    Path(\"~/Music/../Artist\").expanduser().resolve()\n    Path(path)\n\nWhen storing paths in the database\n\n.. code-block:: python\n\n    path_bytes = bytestring_path(Path(\"/some/path/to/file.mp3\"))\n"
  },
  {
    "path": "docs/dev/plugins/autotagger.rst",
    "content": "Extending the Autotagger\n========================\n\n.. currentmodule:: beets.metadata_plugins\n\nBeets supports **metadata source plugins**, which allow it to fetch and match\nmetadata from external services (such as Spotify, Discogs, or Deezer). This\nguide explains how to build your own metadata source plugin by extending either\n:py:class:`MetadataSourcePlugin` or :py:class:`SearchApiMetadataSourcePlugin`.\n\nThese plugins integrate directly with the autotagger, providing candidate\nmetadata during lookups. To implement one, you must subclass\n:py:class:`MetadataSourcePlugin` (or the search API helper base class) and\nimplement its abstract methods.\n\nOverview\n--------\n\nCreating a metadata source plugin is very similar to writing a standard plugin\n(see :ref:`basic-plugin-setup`). The main difference is that your plugin must:\n\n1. Subclass :py:class:`MetadataSourcePlugin` or\n   :py:class:`SearchApiMetadataSourcePlugin`.\n2. Implement all required abstract methods.\n\nHere`s a minimal example:\n\n.. code-block:: python\n\n    # beetsplug/myawesomeplugin.py\n    from typing import Sequence\n    from beets.autotag.hooks import Item\n    from beets.metadata_plugins import MetadataSourcePlugin\n\n\n    class MyAwesomePlugin(MetadataSourcePlugin):\n\n        def candidates(\n            self,\n            items: Sequence[Item],\n            artist: str,\n            album: str,\n            va_likely: bool,\n        ): ...\n\n        def item_candidates(self, item: Item, artist: str, title: str): ...\n\n        def track_for_id(self, track_id: str): ...\n\n        def album_for_id(self, album_id: str): ...\n\nFor API-backed metadata sources, prefer\n:py:class:`SearchApiMetadataSourcePlugin` to reuse shared search orchestration:\n\n.. code-block:: python\n\n    from beets.metadata_plugins import SearchApiMetadataSourcePlugin, SearchParams\n\n\n    class MyApiPlugin(SearchApiMetadataSourcePlugin):\n\n        def get_search_query_with_filters(self, query_type, items, artist, name, va_likely):\n            query = f'album:\"{name}\"' if query_type == \"album\" else name\n            if query_type == \"track\" or not va_likely:\n                query += f' artist:\"{artist}\"'\n            return query, {}\n\n        def get_search_response(self, params: SearchParams):\n            # Execute provider API request and return results with \"id\" fields.\n            ...\n\n        def track_for_id(self, track_id: str): ...\n\n        def album_for_id(self, album_id: str): ...\n\nThe shared base class centralizes query normalization, ``search_limit``\nhandling, candidate wiring, and consistent error logging for search requests.\nProvider-specific behavior is implemented in\n:py:meth:`~SearchApiMetadataSourcePlugin.get_search_query_with_filters` and\n:py:meth:`~SearchApiMetadataSourcePlugin.get_search_response`.\n\nEach metadata source plugin automatically gets a unique identifier. You can\naccess this identifier using the :py:meth:`~MetadataSourcePlugin.data_source`\nclass property to tell plugins apart.\n\nMetadata lookup\n---------------\n\nWhen beets runs the autotagger, it queries **all enabled metadata source\nplugins** for potential matches:\n\n- For **albums**, it calls :py:meth:`~MetadataSourcePlugin.candidates`.\n- For **singletons**, it calls :py:meth:`~MetadataSourcePlugin.item_candidates`.\n\nThe results are combined and scored. By default, candidate ranking is handled\nautomatically by the beets core, but you can customize weighting by overriding:\n\n- :py:meth:`~MetadataSourcePlugin.album_distance`\n- :py:meth:`~MetadataSourcePlugin.track_distance`\n\nThis is optional, if not overridden, both methods return a constant distance of\n`0.5`.\n\nID-based lookups\n----------------\n\nYour plugin must also define:\n\n- :py:meth:`~MetadataSourcePlugin.album_for_id` — fetch album metadata by ID.\n- :py:meth:`~MetadataSourcePlugin.track_for_id` — fetch track metadata by ID.\n\nIDs are expected to be strings. If your source uses specific formats, consider\ncontributing an extractor regex to the core module:\n:py:mod:`beets.util.id_extractors`.\n\nWhen beets matches by explicit IDs (for example via ``--search-id`` or existing\n``mb_*id`` fields), it asks every enabled metadata source plugin for candidates\nusing :py:meth:`~MetadataSourcePlugin.albums_for_ids` and\n:py:meth:`~MetadataSourcePlugin.tracks_for_ids`. Candidate identity is tracked\nby ``(data_source, id)``, so identical IDs from different providers remain\nseparate options.\n\nIf you need to query one specific provider, use the module helpers\n:py:func:`beets.metadata_plugins.album_for_id` and\n:py:func:`beets.metadata_plugins.track_for_id` and pass both the ID and the\nprovider ``data_source`` name.\n\nBest practices\n--------------\n\nBeets already ships with several metadata source plugins. Studying these\nimplementations can help you follow conventions and avoid pitfalls. Good\nstarting points include:\n\n- ``spotify``\n- ``deezer``\n- ``discogs``\n\nMigration guidance\n------------------\n\nOlder metadata plugins that extend |BeetsPlugin| should be migrated to\n:py:class:`MetadataSourcePlugin`. API-backed sources should generally migrate to\n:py:class:`SearchApiMetadataSourcePlugin` to avoid duplicating search\norchestration. Legacy support will be removed in **beets v3.0.0**.\n\n.. seealso::\n\n    - :py:mod:`beets.autotag`\n    - :py:mod:`beets.metadata_plugins`\n    - :ref:`autotagger_extensions`\n    - :ref:`using-the-auto-tagger`\n"
  },
  {
    "path": "docs/dev/plugins/commands.rst",
    "content": ".. _add_subcommands:\n\nAdd Commands to the CLI\n=======================\n\nPlugins can add new subcommands to the ``beet`` command-line interface. Define\nthe plugin class' ``commands()`` method to return a list of ``Subcommand``\nobjects. (The ``Subcommand`` class is defined in the ``beets.ui`` module.)\nHere's an example plugin that adds a simple command:\n\n.. code-block:: python\n\n    from beets.plugins import BeetsPlugin\n    from beets.ui import Subcommand\n\n    my_super_command = Subcommand(\"super\", help=\"do something super\")\n\n\n    def say_hi(lib, opts, args):\n        print(\"Hello everybody! I'm a plugin!\")\n\n\n    my_super_command.func = say_hi\n\n\n    class SuperPlug(BeetsPlugin):\n        def commands(self):\n            return [my_super_command]\n\nTo make a subcommand, invoke the constructor like so: ``Subcommand(name, parser,\nhelp, aliases)``. The ``name`` parameter is the only required one and should\njust be the name of your command. ``parser`` can be an `OptionParser instance`_,\nbut it defaults to an empty parser (you can extend it later). ``help`` is a\ndescription of your command, and ``aliases`` is a list of shorthand versions of\nyour command name.\n\n.. _optionparser instance: https://docs.python.org/3/library/optparse.html\n\nYou'll need to add a function to your command by saying ``mycommand.func =\nmyfunction``. This function should take the following parameters: ``lib`` (a\nbeets ``Library`` object) and ``opts`` and ``args`` (command-line options and\narguments as returned by OptionParser.parse_args_).\n\n.. _optionparser.parse_args: https://docs.python.org/3/library/optparse.html#parsing-arguments\n\nThe function should use any of the utility functions defined in ``beets.ui``.\nTry running ``pydoc beets.ui`` to see what's available.\n\nYou can add command-line options to your new command using the ``parser`` member\nof the ``Subcommand`` class, which is a ``CommonOptionsParser`` instance. Just\nuse it like you would a normal ``OptionParser`` in an independent script. Note\nthat it offers several methods to add common options: ``--album``, ``--path``\nand ``--format``. This feature is versatile and extensively documented, try\n``pydoc beets.ui.CommonOptionsParser`` for more information.\n"
  },
  {
    "path": "docs/dev/plugins/events.rst",
    "content": ".. _plugin_events:\n\nListen for Events\n=================\n\n.. currentmodule:: beets.plugins\n\nEvent handlers allow plugins to hook into whenever something happens in beets'\noperations. For instance, a plugin could write a log message every time an album\nis successfully autotagged or update MPD's index whenever the database is\nchanged.\n\nYou can \"listen\" for events using :py:meth:`BeetsPlugin.register_listener`.\nHere's an example:\n\n.. code-block:: python\n\n    from beets.plugins import BeetsPlugin\n\n\n    def loaded():\n        print(\"Plugin loaded!\")\n\n\n    class SomePlugin(BeetsPlugin):\n        def __init__(self):\n            super().__init__()\n            self.register_listener(\"pluginload\", loaded)\n\nNote that if you want to access an attribute of your plugin (e.g. ``config`` or\n``log``) you'll have to define a method and not a function. Here is the usual\nregistration process in this case:\n\n.. code-block:: python\n\n    from beets.plugins import BeetsPlugin\n\n\n    class SomePlugin(BeetsPlugin):\n        def __init__(self):\n            super().__init__()\n            self.register_listener(\"pluginload\", self.loaded)\n\n        def loaded(self):\n            self._log.info(\"Plugin loaded!\")\n\n.. rubric:: Plugin Events\n\n``pluginload``\n    :Parameters: (none)\n    :Description: Called after all plugins have been loaded after the ``beet``\n        command starts.\n\n``import``\n    :Parameters: ``lib`` (|Library|), ``paths`` (list of path strings)\n    :Description: Called after the ``import`` command finishes.\n\n``album_imported``\n    :Parameters: ``lib`` (|Library|), ``album`` (|Album|)\n    :Description: Called every time the importer finishes adding an album to the\n        library.\n\n``album_removed``\n    :Parameters: ``lib`` (|Library|), ``album`` (|Album|)\n    :Description: Called every time an album is removed from the library (even\n        when its files are not deleted from disk).\n\n``item_copied``\n    :Parameters: ``item`` (|Item|), ``source`` (path), ``destination`` (path)\n    :Description: Called whenever an item file is copied.\n\n``item_imported``\n    :Parameters: ``lib`` (|Library|), ``item`` (|Item|)\n    :Description: Called every time the importer adds a singleton to the library\n        (not called for full-album imports).\n\n``before_item_imported``\n    :Parameters: ``item`` (|Item|), ``source`` (path), ``destination`` (path)\n    :Description: Called with an ``Item`` object immediately before it is\n        imported.\n\n``before_item_moved``\n    :Parameters: ``item`` (|Item|), ``source`` (path), ``destination`` (path)\n    :Description: Called with an ``Item`` object immediately before its file is\n        moved.\n\n``item_moved``\n    :Parameters: ``item`` (|Item|), ``source`` (path), ``destination`` (path)\n    :Description: Called with an ``Item`` object whenever its file is moved.\n\n``item_linked``\n    :Parameters: ``item`` (|Item|), ``source`` (path), ``destination`` (path)\n    :Description: Called with an ``Item`` object whenever a symlink is created\n        for a file.\n\n``item_hardlinked``\n    :Parameters: ``item`` (|Item|), ``source`` (path), ``destination`` (path)\n    :Description: Called with an ``Item`` object whenever a hardlink is created\n        for a file.\n\n``item_reflinked``\n    :Parameters: ``item`` (|Item|), ``source`` (path), ``destination`` (path)\n    :Description: Called with an ``Item`` object whenever a reflink is created\n        for a file.\n\n``item_removed``\n    :Parameters: ``item`` (|Item|)\n    :Description: Called with an ``Item`` object every time an item (singleton\n        or part of an album) is removed from the library (even when its file is\n        not deleted from disk).\n\n``write``\n    :Parameters: ``item`` (|Item|), ``path`` (path), ``tags`` (dict)\n    :Description: Called just before a file's metadata is written to disk.\n        Handlers may modify ``tags`` or raise ``library.FileOperationError`` to\n        abort.\n\n``after_write``\n    :Parameters: ``item`` (|Item|)\n    :Description: Called after a file's metadata is written to disk.\n\n``import_task_created``\n    :Parameters: ``task`` (|ImportTask|), ``session`` (|ImportSession|)\n    :Description: Called immediately after an import task is initialized. May\n        return a list (possibly empty) of replacement tasks.\n\n``import_task_start``\n    :Parameters: ``task`` (|ImportTask|), ``session`` (|ImportSession|)\n    :Description: Called before an import task begins processing.\n\n``import_task_apply``\n    :Parameters: ``task`` (|ImportTask|), ``session`` (|ImportSession|)\n    :Description: Called after metadata changes have been applied in an import\n        task (on the UI thread; keep fast). Prefer a pipeline stage otherwise\n        (see :ref:`plugin-stage`).\n\n``import_task_before_choice``\n    :Parameters: ``task`` (|ImportTask|), ``session`` (|ImportSession|)\n    :Description: Called after candidate search and before deciding how to\n        import. May return an importer action (only one handler may return\n        non-None).\n\n``import_task_choice``\n    :Parameters: ``task`` (|ImportTask|), ``session`` (|ImportSession|)\n    :Description: Called after a decision has been made about an import task.\n        Use ``task.choice_flag`` to inspect or change the action.\n\n``import_task_files``\n    :Parameters: ``task`` (|ImportTask|), ``session`` (|ImportSession|)\n    :Description: Called after filesystem manipulation (copy/move/write) for an\n        import task.\n\n``library_opened``\n    :Parameters: ``lib`` (|Library|)\n    :Description: Called after beets starts and initializes the main Library\n        object.\n\n``database_change``\n    :Parameters: ``lib`` (|Library|), ``model`` (|Model|)\n    :Description: A modification has been made to the library database (may not\n        yet be committed).\n\n``cli_exit``\n    :Parameters: ``lib`` (|Library|)\n    :Description: Called just before the ``beet`` command-line program exits.\n\n``import_begin``\n    :Parameters: ``session`` (|ImportSession|)\n    :Description: Called just before a ``beet import`` session starts.\n\n``trackinfo_received``\n    :Parameters: ``info`` (|TrackInfo|)\n    :Description: Called after metadata for a track is fetched (e.g., from\n        MusicBrainz). Handlers can modify the tags seen by later pipeline stages\n        or adjustments (e.g., ``mbsync``).\n\n``albuminfo_received``\n    :Parameters: ``info`` (|AlbumInfo|)\n    :Description: Like ``trackinfo_received`` but for album-level metadata.\n\n``album_matched``\n    :Parameters: ``match`` (``AlbumMatch``)\n    :Description: Called each time an ``AlbumMatch`` candidate is created while\n        importing. This applies to both ID-driven and text-search matching.\n        Missing and extra tracks, if any, are included in the match.\n\n``before_choose_candidate``\n    :Parameters: ``task`` (|ImportTask|), ``session`` (|ImportSession|)\n    :Description: Called before prompting the user during interactive import.\n        May return a list of ``PromptChoices`` to append to the prompt (see\n        :ref:`append_prompt_choices`).\n\n``mb_track_extract``\n    :Parameters: ``data`` (dict)\n    :Description: Called after metadata is obtained from MusicBrainz for a\n        track. Must return a (possibly empty) dict of additional ``field:\n        value`` pairs to apply (overwriting existing fields).\n\n``mb_album_extract``\n    :Parameters: ``data`` (dict)\n    :Description: Like ``mb_track_extract`` but for album tags. Overwrites tags\n        set at the track level with the same field.\n\nThe included ``mpdupdate`` plugin provides an example use case for event\nlisteners.\n"
  },
  {
    "path": "docs/dev/plugins/index.rst",
    "content": "Plugin Development\n==================\n\nBeets plugins are Python modules or packages that extend the core functionality\nof beets. The plugin system is designed to be flexible, allowing developers to\nadd virtually any type of features to beets.\n\nFor instance you can create plugins that add new commands to the command-line\ninterface, listen for events in the beets lifecycle or extend the autotagger\nwith new metadata sources.\n\n.. _basic-plugin-setup:\n\nBasic Plugin Setup\n------------------\n\nA beets plugin is just a Python module or package inside the ``beetsplug``\nnamespace [1]_ package. To create the basic plugin layout, create a directory\ncalled ``beetsplug`` and add either your plugin module:\n\n.. code-block:: shell\n\n    beetsplug/\n    └── myawesomeplugin.py\n\nor your plugin subpackage\n\n.. code-block:: shell\n\n    beetsplug/\n    └── myawesomeplugin/\n        ├── __init__.py\n        └── myawesomeplugin.py\n\n.. attention::\n\n    You do not need to add an ``__init__.py`` file to the ``beetsplug``\n    directory. Python treats your plugin as a namespace package automatically,\n    thus we do not depend on ``pkgutil``-based setup in the ``__init__.py`` file\n    anymore.\n\nThe meat of your plugin goes in ``myawesomeplugin.py``. Every plugin has to\nextend the |BeetsPlugin| abstract base class [2]_ . For instance, a minimal\nplugin without any functionality would look like this:\n\n.. code-block:: python\n\n    # beetsplug/myawesomeplugin.py\n    from beets.plugins import BeetsPlugin\n\n\n    class MyAwesomePlugin(BeetsPlugin):\n        pass\n\n.. attention::\n\n    If your plugin is composed of intermediate |BeetsPlugin| subclasses, make\n    sure that your plugin is defined *last* in the namespace. We only load the\n    last subclass of |BeetsPlugin| we find in your plugin namespace.\n\nTo use your new plugin, you need to package [3]_ your plugin and install it into\nyour ``beets`` (virtual) environment. To enable your plugin, add it it to the\nbeets configuration\n\n.. code-block:: yaml\n\n    # config.yaml\n    plugins:\n      - myawesomeplugin\n\nand you're good to go!\n\n.. [1] Check out `this article`_ and `this Stack Overflow question`_ if you\n    haven't heard about namespace packages.\n\n.. [2] Abstract base classes allow us to define a contract which any plugin must\n    follow. This is a common paradigm in object-oriented programming, and it\n    helps to ensure that plugins are implemented in a consistent way. For more\n    information, see for example pep-3119_.\n\n.. [3] There are a variety of packaging tools available for python, for example\n    you can use poetry_, setuptools_ or hatchling_.\n\n.. _hatchling: https://hatch.pypa.io/latest/config/build/#build-system\n\n.. _pep-3119: https://peps.python.org/pep-3119/#rationale\n\n.. _poetry: https://python-poetry.org/docs/pyproject/#packages\n\n.. _setuptools: https://setuptools.pypa.io/en/latest/userguide/package_discovery.html#finding-simple-packages\n\n.. _this article: https://realpython.com/python-namespace-package/#setting-up-some-namespace-packages\n\n.. _this stack overflow question: https://stackoverflow.com/questions/1675734/how-do-i-create-a-namespace-package-in-python/27586272#27586272\n\nMore information\n----------------\n\nFor more information on writing plugins, feel free to check out the following\nresources:\n\n.. toctree::\n    :maxdepth: 3\n    :includehidden:\n\n    commands\n    events\n    autotagger\n    other/index\n"
  },
  {
    "path": "docs/dev/plugins/other/config.rst",
    "content": "Read Configuration Options\n==========================\n\nPlugins can configure themselves using the ``config.yaml`` file. You can read\nconfiguration values in two ways. The first is to use ``self.config`` within\nyour plugin class. This gives you a view onto the configuration values in a\nsection with the same name as your plugin's module. For example, if your plugin\nis in ``greatplugin.py``, then ``self.config`` will refer to options under the\n``greatplugin:`` section of the config file.\n\nFor example, if you have a configuration value called \"foo\", then users can put\nthis in their ``config.yaml``:\n\n::\n\n    greatplugin:\n        foo: bar\n\nTo access this value, say ``self.config['foo'].get()`` at any point in your\nplugin's code. The ``self.config`` object is a *view* as defined by the Confuse_\nlibrary.\n\n.. _confuse: https://confuse.readthedocs.io/en/latest/\n\nIf you want to access configuration values *outside* of your plugin's section,\nimport the ``config`` object from the ``beets`` module. That is, just put ``from\nbeets import config`` at the top of your plugin and access values from there.\n\nIf your plugin provides configuration values for sensitive data (e.g.,\npasswords, API keys, ...), you should add these to the config so they can be\nredacted automatically when users dump their config. This can be done by setting\neach value's ``redact`` flag, like so:\n\n::\n\n    self.config['password'].redact = True\n"
  },
  {
    "path": "docs/dev/plugins/other/fields.rst",
    "content": "Flexible Field Types\n====================\n\nIf your plugin uses flexible fields to store numbers or other non-string values,\nyou can specify the types of those fields. A rating plugin, for example, might\nwant to declare that the ``rating`` field should have an integer type:\n\n.. code-block:: python\n\n    from beets.plugins import BeetsPlugin\n    from beets.dbcore import types\n\n\n    class RatingPlugin(BeetsPlugin):\n        item_types = {\"rating\": types.INTEGER}\n\n        @property\n        def album_types(self):\n            return {\"rating\": types.INTEGER}\n\nA plugin may define two attributes: ``item_types`` and ``album_types``. Each of\nthose attributes is a dictionary mapping a flexible field name to a type\ninstance. You can find the built-in types in the ``beets.dbcore.types`` and\n``beets.library`` modules or implement your own type by inheriting from the\n``Type`` class.\n\nSpecifying types has several advantages:\n\n- Code that accesses the field like ``item['my_field']`` gets the right type\n  (instead of just a string).\n- You can use advanced queries (like :ref:`ranges <numericquery>`) from the\n  command line.\n- User input for flexible fields may be validated and converted.\n- Items missing the given field can use an appropriate null value for querying\n  and sorting purposes.\n"
  },
  {
    "path": "docs/dev/plugins/other/import.rst",
    "content": ".. _plugin-stage:\n\nAdd Import Pipeline Stages\n==========================\n\nMany plugins need to add high-latency operations to the import workflow. For\nexample, a plugin that fetches lyrics from the Web would, ideally, not block the\nprogress of the rest of the importer. Beets allows plugins to add stages to the\nparallel import pipeline.\n\nEach stage is run in its own thread. Plugin stages run after metadata changes\nhave been applied to a unit of music (album or track) and before file\nmanipulation has occurred (copying and moving files, writing tags to disk).\nMultiple stages run in parallel but each stage processes only one task at a time\nand each task is processed by only one stage at a time.\n\nPlugins provide stages as functions that take two arguments: ``config`` and\n``task``, which are ``ImportSession`` and ``ImportTask`` objects (both defined\nin ``beets.importer``). Add such a function to the plugin's ``import_stages``\nfield to register it:\n\n.. code-block:: python\n\n    from beets.importer import ImportSession, ImportTask\n    from beets.plugins import BeetsPlugin\n\n\n    class ExamplePlugin(BeetsPlugin):\n\n        def __init__(self):\n            super().__init__()\n            self.import_stages = [self.stage]\n\n        def stage(self, session: ImportSession, task: ImportTask):\n            print(\"Importing something!\")\n\nIt is also possible to request your function to run early in the pipeline by\nadding the function to the plugin's ``early_import_stages`` field instead:\n\n.. code-block:: python\n\n    self.early_import_stages = [self.stage]\n\n.. _extend-query:\n\nExtend the Query Syntax\n-----------------------\n\nYou can add new kinds of queries to beets' :doc:`query syntax\n</reference/query>`. There are two ways to add custom queries: using a prefix\nand using a name. Prefix-based query extension can apply to *any* field, while\nnamed queries are not associated with any field. For example, beets already\nsupports regular expression queries, which are indicated by a colon\nprefix---plugins can do the same.\n\nFor either kind of query extension, define a subclass of the ``Query`` type from\nthe ``beets.dbcore.query`` module. Then:\n\n- To define a prefix-based query, define a ``queries`` method in your plugin\n  class. Return from this method a dictionary mapping prefix strings to query\n  classes.\n- To define a named query, defined dictionaries named either ``item_queries`` or\n  ``album_queries``. These should map names to query types. So if you use ``{\n  \"foo\": FooQuery }``, then the query ``foo:bar`` will construct a query like\n  ``FooQuery(\"bar\")``.\n\nFor prefix-based queries, you will want to extend ``FieldQuery``, which\nimplements string comparisons on fields. To use it, create a subclass inheriting\nfrom that class and override the ``value_match`` class method. (Remember the\n``@classmethod`` decorator!) The following example plugin declares a query using\nthe ``@`` prefix to delimit exact string matches. The plugin will be used if we\nissue a command like ``beet ls @something`` or ``beet ls artist:@something``:\n\n.. code-block:: python\n\n    from beets.plugins import BeetsPlugin\n    from beets.dbcore import FieldQuery\n\n\n    class ExactMatchQuery(FieldQuery):\n        @classmethod\n        def value_match(self, pattern, val):\n            return pattern == val\n\n\n    class ExactMatchPlugin(BeetsPlugin):\n        def queries(self):\n            return {\"@\": ExactMatchQuery}\n"
  },
  {
    "path": "docs/dev/plugins/other/index.rst",
    "content": "Further Reading\n===============\n\nFor more information on writing plugins, feel free to check out the following\nresources:\n\n.. toctree::\n    :maxdepth: 2\n\n    config\n    templates\n    mediafile\n    import\n    fields\n    logging\n    prompts\n"
  },
  {
    "path": "docs/dev/plugins/other/logging.rst",
    "content": ".. _plugin-logging:\n\nLogging\n=======\n\nEach plugin object has a ``_log`` attribute, which is a ``Logger`` from the\n`standard Python logging module`_. The logger is set up to `PEP 3101`_,\nstr.format-style string formatting. So you can write logging calls like this:\n\n.. code-block:: python\n\n    self._log.debug(\"Processing {0.title} by {0.artist}\", item)\n\n.. _pep 3101: https://peps.python.org/pep-3101/\n\n.. _standard python logging module: https://docs.python.org/3/library/logging.html\n\nWhen beets is in verbose mode, plugin messages are prefixed with the plugin name\nto make them easier to see.\n\nWhich messages will be logged depends on the logging level and the action\nperformed:\n\n- Inside import stages and event handlers, the default is ``WARNING`` messages\n  and above.\n- Everywhere else, the default is ``INFO`` or above.\n\nThe verbosity can be increased with ``--verbose`` (``-v``) flags: each flags\nlowers the level by a notch. That means that, with a single ``-v`` flag, event\nhandlers won't have their ``DEBUG`` messages displayed, but command functions\n(for example) will. With ``-vv`` on the command line, ``DEBUG`` messages will be\ndisplayed everywhere.\n\nThis addresses a common pattern where plugins need to use the same code for a\ncommand and an import stage, but the command needs to print more messages than\nthe import stage. (For example, you'll want to log \"found lyrics for this song\"\nwhen you're run explicitly as a command, but you don't want to noisily interrupt\nthe importer interface when running automatically.)\n"
  },
  {
    "path": "docs/dev/plugins/other/mediafile.rst",
    "content": "Extend MediaFile\n================\n\nMediaFile_ is the file tag abstraction layer that beets uses to make\ncross-format metadata manipulation simple. Plugins can add fields to MediaFile\nto extend the kinds of metadata that they can easily manage.\n\nThe ``MediaFile`` class uses ``MediaField`` descriptors to provide access to\nfile tags. If you have created a descriptor you can add it through your plugins\n:py:meth:`beets.plugins.BeetsPlugin.add_media_field` method.\n\n.. _mediafile: https://mediafile.readthedocs.io/en/latest/\n\nHere's an example plugin that provides a meaningless new field \"foo\":\n\n.. code-block:: python\n\n    class FooPlugin(BeetsPlugin):\n        def __init__(self):\n            field = mediafile.MediaField(\n                mediafile.MP3DescStorageStyle(\"foo\"), mediafile.StorageStyle(\"foo\")\n            )\n            self.add_media_field(\"foo\", field)\n\n\n    FooPlugin()\n    item = Item.from_path(\"/path/to/foo/tag.mp3\")\n    assert item[\"foo\"] == \"spam\"\n\n    item[\"foo\"] == \"ham\"\n    item.write()\n    # The \"foo\" tag of the file is now \"ham\"\n"
  },
  {
    "path": "docs/dev/plugins/other/prompts.rst",
    "content": ".. _append_prompt_choices:\n\nAppend Prompt Choices\n=====================\n\nPlugins can also append choices to the prompt presented to the user during an\nimport session.\n\nTo do so, add a listener for the ``before_choose_candidate`` event, and return a\nlist of ``PromptChoices`` that represent the additional choices that your plugin\nshall expose to the user:\n\n.. code-block:: python\n\n    from beets.plugins import BeetsPlugin\n    from beets.util import PromptChoice\n\n\n    class ExamplePlugin(BeetsPlugin):\n        def __init__(self):\n            super().__init__()\n            self.register_listener(\n                \"before_choose_candidate\", self.before_choose_candidate_event\n            )\n\n        def before_choose_candidate_event(self, session, task):\n            return [\n                PromptChoice(\"p\", \"Print foo\", self.foo),\n                PromptChoice(\"d\", \"Do bar\", self.bar),\n            ]\n\n        def foo(self, session, task):\n            print('User has chosen \"Print foo\"!')\n\n        def bar(self, session, task):\n            print('User has chosen \"Do bar\"!')\n\nThe previous example modifies the standard prompt:\n\n.. code-block:: shell\n\n    # selection (default 1), Skip, Use as-is, as Tracks, Group albums,\n    Enter search, enter Id, aBort?\n\nby appending two additional options (``Print foo`` and ``Do bar``):\n\n.. code-block:: shell\n\n    # selection (default 1), Skip, Use as-is, as Tracks, Group albums,\n    Enter search, enter Id, aBort, Print foo, Do bar?\n\nIf the user selects a choice, the ``callback`` attribute of the corresponding\n``PromptChoice`` will be called. It is the responsibility of the plugin to check\nfor the status of the import session and decide the choices to be appended: for\nexample, if a particular choice should only be presented if the album has no\ncandidates, the relevant checks against ``task.candidates`` should be performed\ninside the plugin's ``before_choose_candidate_event`` accordingly.\n\nPlease make sure that the short letter for each of the choices provided by the\nplugin is not already in use: the importer will emit a warning and discard all\nbut one of the choices using the same letter, giving priority to the core\nimporter prompt choices. As a reference, the following characters are used by\nthe choices on the core importer prompt, and hence should not be used: ``a``,\n``s``, ``u``, ``t``, ``g``, ``e``, ``i``, ``b``.\n\nAdditionally, the callback function can optionally specify the next action to be\nperformed by returning a ``importer.Action`` value. It may also return a\n``autotag.Proposal`` value to update the set of current proposals to be\nconsidered.\n"
  },
  {
    "path": "docs/dev/plugins/other/templates.rst",
    "content": "Add Path Format Functions and Fields\n====================================\n\nBeets supports *function calls* in its path format syntax (see\n:doc:`/reference/pathformat`). Beets includes a few built-in functions, but\nplugins can register new functions by adding them to the ``template_funcs``\ndictionary.\n\nHere's an example:\n\n.. code-block:: python\n\n    class MyPlugin(BeetsPlugin):\n        def __init__(self):\n            super().__init__()\n            self.template_funcs[\"initial\"] = _tmpl_initial\n\n\n    def _tmpl_initial(text: str) -> str:\n        if text:\n            return text[0].upper()\n        else:\n            return \"\"\n\nThis plugin provides a function ``%initial`` to path templates where\n``%initial{$artist}`` expands to the artist's initial (its capitalized first\ncharacter).\n\nPlugins can also add template *fields*, which are computed values referenced as\n``$name`` in templates. To add a new field, add a function that takes an\n``Item`` object to the ``template_fields`` dictionary on the plugin object.\nHere's an example that adds a ``$disc_and_track`` field:\n\n.. code-block:: python\n\n    class MyPlugin(BeetsPlugin):\n        def __init__(self):\n            super().__init__()\n            self.template_fields[\"disc_and_track\"] = _tmpl_disc_and_track\n\n\n    def _tmpl_disc_and_track(item: Item) -> str:\n        \"\"\"Expand to the disc number and track number if this is a\n        multi-disc release. Otherwise, just expands to the track\n        number.\n        \"\"\"\n        if item.disctotal > 1:\n            return \"%02i.%02i\" % (item.disc, item.track)\n        else:\n            return \"%02i\" % (item.track)\n\nWith this plugin enabled, templates can reference ``$disc_and_track`` as they\ncan any standard metadata field.\n\nThis field works for *item* templates. Similarly, you can register *album*\ntemplate fields by adding a function accepting an ``Album`` argument to the\n``album_template_fields`` dict.\n"
  },
  {
    "path": "docs/extensions/conf.py",
    "content": "\"\"\"Sphinx extension for simple configuration value documentation.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Any, ClassVar\n\nfrom docutils import nodes\nfrom docutils.parsers.rst import directives\nfrom sphinx import addnodes\nfrom sphinx.directives import ObjectDescription\nfrom sphinx.domains import Domain, ObjType\nfrom sphinx.roles import XRefRole\nfrom sphinx.util.nodes import make_refnode\n\nif TYPE_CHECKING:\n    from collections.abc import Iterable, Sequence\n\n    from docutils.nodes import Element\n    from docutils.parsers.rst.states import Inliner\n    from sphinx.addnodes import desc_signature, pending_xref\n    from sphinx.application import Sphinx\n    from sphinx.builders import Builder\n    from sphinx.environment import BuildEnvironment\n    from sphinx.util.typing import ExtensionMetadata, OptionSpec\n\n\nclass Conf(ObjectDescription[str]):\n    \"\"\"Directive for documenting a single configuration value.\"\"\"\n\n    option_spec: ClassVar[OptionSpec] = {\n        \"default\": directives.unchanged,\n    }\n\n    def handle_signature(self, sig: str, signode: desc_signature) -> str:\n        \"\"\"Process the directive signature (the config name).\"\"\"\n        signode += addnodes.desc_name(sig, sig)\n\n        # Add default value if provided\n        if \"default\" in self.options:\n            signode += nodes.Text(\" \")\n            default_container = nodes.inline(\"\", \"\")\n            default_container += nodes.Text(\"(default: \")\n            default_container += nodes.literal(\"\", self.options[\"default\"])\n            default_container += nodes.Text(\")\")\n            signode += default_container\n\n        return sig\n\n    def add_target_and_index(\n        self, name: str, sig: str, signode: desc_signature\n    ) -> None:\n        \"\"\"Add cross-reference target and index entry.\"\"\"\n        target = f\"conf-{name}\"\n        if target not in self.state.document.ids:\n            signode[\"ids\"].append(target)\n            self.state.document.note_explicit_target(signode)\n\n            # A unique full name which includes the document name\n            index_name = f\"{self.env.docname.replace('/', '.')}:{name}\"\n            # Register with the conf domain\n            domain = self.env.get_domain(\"conf\")\n            domain.data[\"objects\"][index_name] = (self.env.docname, target)\n\n            # Add to index\n            self.indexnode[\"entries\"].append(\n                (\"single\", f\"{name} (configuration value)\", target, \"\", None)\n            )\n\n\nclass ConfDomain(Domain):\n    \"\"\"Domain for simple configuration values.\"\"\"\n\n    name = \"conf\"\n    label = \"Simple Configuration\"\n    object_types = {\"conf\": ObjType(\"conf\", \"conf\")}  # noqa: RUF012\n    directives = {\"conf\": Conf}  # noqa: RUF012\n    roles = {\"conf\": XRefRole()}  # noqa: RUF012\n    initial_data: dict[str, Any] = {\"objects\": {}}  # noqa: RUF012\n\n    def get_objects(self) -> Iterable[tuple[str, str, str, str, str, int]]:\n        \"\"\"Return an iterable of object tuples for the inventory.\"\"\"\n        for name, (docname, targetname) in self.data[\"objects\"].items():\n            # Remove the document name prefix for display\n            display_name = name.split(\":\")[-1]\n            yield (name, display_name, \"conf\", docname, targetname, 1)\n\n    def resolve_xref(\n        self,\n        env: BuildEnvironment,\n        fromdocname: str,\n        builder: Builder,\n        typ: str,\n        target: str,\n        node: pending_xref,\n        contnode: Element,\n    ) -> Element | None:\n        if entry := self.data[\"objects\"].get(target):\n            docname, targetid = entry\n            return make_refnode(\n                builder, fromdocname, docname, targetid, contnode\n            )\n\n        return None\n\n\n# sphinx.util.typing.RoleFunction\ndef conf_role(\n    name: str,\n    rawtext: str,\n    text: str,\n    lineno: int,\n    inliner: Inliner,\n    /,\n    options: dict[str, Any] | None = None,\n    content: Sequence[str] = (),\n) -> tuple[list[nodes.Node], list[nodes.system_message]]:\n    \"\"\"Role for referencing configuration values.\"\"\"\n    node = addnodes.pending_xref(\n        \"\",\n        refdomain=\"conf\",\n        reftype=\"conf\",\n        reftarget=text,\n        refwarn=True,\n        **(options or {}),\n    )\n    node += nodes.literal(text, text.split(\":\")[-1])\n    return [node], []\n\n\ndef setup(app: Sphinx) -> ExtensionMetadata:\n    app.add_domain(ConfDomain)\n\n    # register a top-level directive so users can use \".. conf:: ...\"\n    app.add_directive(\"conf\", Conf)\n\n    # Register role with short name\n    app.add_role(\"conf\", conf_role)\n    return {\n        \"version\": \"0.1\",\n        \"parallel_read_safe\": True,\n        \"parallel_write_safe\": True,\n    }\n"
  },
  {
    "path": "docs/faq.rst",
    "content": "FAQ\n===\n\nHere are some answers to frequently-asked questions from IRC and elsewhere. Got\na question that isn't answered here? Try the `discussion board`_, or\n:ref:`filing an issue <bugs>` in the bug tracker.\n\n.. _discussion board: https://github.com/beetbox/beets/discussions/\n\n.. _mailing list: https://groups.google.com/group/beets-users\n\n.. contents::\n    :local:\n    :depth: 2\n\nHow do I…\n---------\n\n.. _move:\n\n…rename my files according to a new path format configuration?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nJust run the :ref:`move-cmd` command. Use a :doc:`query </reference/query>` to\nrename a subset of your music or leave the query off to rename everything.\n\n.. _asispostfacto:\n\n…find all the albums I imported \"as-is\"?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nEnable the :ref:`import log <import_log>` to automatically record whenever you\nskip an album or accept one \"as-is\".\n\nAlternatively, you can find all the albums in your library that are missing\nMBIDs using a command like this:\n\n::\n\n    beet ls -a mb_albumid::^$\n\nAssuming your files didn't have MBIDs already, then this will roughly correspond\nto those albums that didn't get autotagged.\n\n.. _discdir:\n\n…create \"Disc N\" directories for multi-disc albums?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nUse the :doc:`/plugins/inline` along with the ``%if{}`` function to accomplish\nthis:\n\n::\n\n    plugins: inline\n    paths:\n        default: $albumartist/$album%aunique{}/%if{$multidisc,Disc $disc/}$track $title\n    item_fields:\n        multidisc: 1 if disctotal > 1 else 0\n\nThis ``paths`` configuration only contains the ``default`` key: it leaves the\n``comp`` and ``singleton`` keys as their default values, as documented in\n:ref:`path-format-config`. To create \"Disc N\" directories for compilations and\nsingletons, you will need to specify similar templates for those keys as well.\n\n.. _multidisc:\n\n…import a multi-disc album?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nAs of 1.0b11, beets tags multi-disc albums as a *single unit*. To get a good\nmatch, it needs to treat all of the album's parts together as a single release.\n\nTo help with this, the importer uses a simple heuristic to guess when a\ndirectory represents a multi-disc album that's been divided into multiple\nsubdirectories. When it finds a situation like this, it collapses all of the\nitems in the subdirectories into a single release for tagging.\n\nThe heuristic works by looking at the names of directories. If multiple\nsubdirectories of a common parent directory follow the pattern \"(title) disc\n(number) (...)\" and the *prefix* (everything up to the number) is the same, the\ndirectories are collapsed together. One of the key words \"disc\" or \"CD\" must be\npresent to make this work.\n\nIf you have trouble tagging a multi-disc album, consider the ``--flat`` flag\n(which treats a whole tree as a single album) or just putting all the tracks\ninto a single directory to force them to be tagged together.\n\n.. _mbid:\n\n…enter a MusicBrainz ID?\n~~~~~~~~~~~~~~~~~~~~~~~~\n\nAn MBID looks like one of these:\n\n- ``https://musicbrainz.org/release/ded77dcf-7279-457e-955d-625bd3801b87``\n- ``d569deba-8c6b-4d08-8c43-d0e5a1b8c7f3``\n\nBeets can recognize either the hex-with-dashes UUID-style string or the full URL\nthat contains it (as of 1.0b11).\n\nYou can get these IDs by `searching on the MusicBrainz web site\n<https://musicbrainz.org/>`__ and going to a *release* page (when tagging full\nalbums) or a *recording* page (when tagging singletons). Then, copy the URL of\nthe page and paste it into beets.\n\nNote that MusicBrainz has both \"releases\" and \"release groups,\" which link\ntogether different versions of the same album. Use *release* IDs here.\n\n.. _upgrade:\n\n…upgrade to the latest version of beets?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nRun a command like this:\n\n::\n\n    pip install -U beets\n\nThe ``-U`` flag tells pip_ to upgrade beets to the latest version. If you want a\nspecific version, you can specify with using ``==`` like so:\n\n::\n\n    pip install beets==1.0rc2\n\n.. _src:\n\n…run the latest source version of beets?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nBeets sees regular releases (about every six weeks or so), but sometimes it's\nhelpful to run on the \"bleeding edge\". To run the latest source:\n\n1. Uninstall beets. If you installed using ``pip``, you can just run ``pip\n   uninstall beets``.\n2. Install from source. Choose one of these methods:\n\n   - Directly from GitHub using ``python -m pip install\n     git+https://github.com/beetbox/beets.git`` command. Depending on your\n     system, you may need to use ``pip3`` and ``python3`` instead of ``pip`` and\n     ``python`` respectively.\n   - Use ``pip`` to install the latest snapshot tarball. Type: ``pip install\n     https://github.com/beetbox/beets/tarball/master``\n   - Use ``pip`` to install an \"editable\" version of beets based on an automatic\n     source checkout. For example, run ``pip install -e\n     git+https://github.com/beetbox/beets#egg=beets`` to clone beets and install\n     it, allowing you to modify the source in-place to try out changes.\n   - Clone source code and install it in editable mode\n\n     .. code-block:: shell\n\n         git clone https://github.com/beetbox/beets.git\n         poetry install\n\n     This approach lets you decide where the source is stored, with any changes\n     immediately reflected in your environment.\n\nMore details about the beets source are available on the :doc:`developer\ndocumentation </dev/index>` pages.\n\n.. _bugs:\n\n…report a bug in beets?\n-----------------------\n\nWe use the `issue tracker`_ on GitHub where you can `open a new ticket`_. Please\nfollow these guidelines when reporting an issue:\n\n- Most importantly: if beets is crashing, please `include the traceback\n  <https://imgur.com/jacoj>`__. Tracebacks can be more readable if you put them\n  in a pastebin (e.g., `Gist <https://gist.github.com/>`__ or `Hastebin\n  <https://www.toptal.com/developers/hastebin>`__), especially when\n  communicating over IRC.\n- Turn on beets' debug output (using the -v option: for example, ``beet -v\n  import ...``) and include that with your bug report. Look through this verbose\n  output for any red flags that might point to the problem.\n- If you can, try installing the latest beets source code to see if the bug is\n  fixed in an unreleased version. You can also look at the :doc:`latest\n  changelog entries </changelog>` for descriptions of the problem you're seeing.\n- Try to narrow your problem down to something specific. Is a particular plugin\n  causing the problem? (You can disable plugins to see whether the problem goes\n  away.) Is a some music file or a single album leading to the crash? (Try\n  importing individual albums to determine which one is causing the problem.) Is\n  some entry in your configuration file causing it? Et cetera.\n- If you do narrow the problem down to a particular audio file or album, include\n  it with your bug report so the developers can run tests.\n\nIf you've never reported a bug before, Mozilla has some well-written `general\nguidelines for good bug reports`_.\n\n.. _find-config:\n\n.. _general guidelines for good bug reports: https://bugzilla.mozilla.org/page.cgi?id=bug-writing.html\n\n.. _issue tracker: https://github.com/beetbox/beets/issues\n\n…find the configuration file (config.yaml)?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nYou create this file yourself; beets just reads it. See\n:doc:`/reference/config`.\n\n.. _special-chars:\n\n…avoid using special characters in my filenames?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nUse the ``%asciify{}`` function in your path formats. See\n:ref:`template-functions`.\n\n.. _move-dir:\n\n…point beets at a new music directory?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nIf you want to move your music from one directory to another, the best way is to\nlet beets do it for you. First, edit your configuration and set the\n``directory`` setting to the new place. Then, type ``beet move`` to have beets\nmove all your files.\n\nIf you've already moved your music *outside* of beets, you have a few options:\n\n- Move the music back (with an ordinary ``mv``) and then use the above steps.\n- Delete your database and re-create it from the new paths using ``beet import\n  -AWC``.\n- Resort to manually modifying the SQLite database (not recommended).\n\nWhy does beets…\n---------------\n\n.. _nomatch:\n\n…complain that it can't find a match?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThere are a number of possibilities:\n\n- First, make sure you have at least one autotagger extension/plugin enabled.\n  See :ref:`autotagger_extensions` for a list of valid plugins.\n- Check that the album is in `the MusicBrainz database\n  <https://musicbrainz.org/>`__. You can search on their site to make sure it's\n  cataloged there. (If not, anyone can edit MusicBrainz---so consider adding the\n  data yourself.)\n- If the album in question is a multi-disc release, see the relevant FAQ answer\n  above.\n- The music files' metadata might be insufficient. Try using the \"enter search\"\n  or \"enter ID\" options to help the matching process find the right MusicBrainz\n  entry.\n- If you have a lot of files that are missing metadata, consider using\n  :doc:`acoustic fingerprinting </plugins/chroma>` or :doc:`filename-based\n  guesses </plugins/fromfilename>` for that music.\n\nIf none of these situations apply and you're still having trouble tagging\nsomething, please :ref:`file a bug report <bugs>`.\n\n.. _plugins:\n\n…appear to be missing some plugins?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nPlease make sure you're using the latest version of beets---you might be using a\nversion earlier than the one that introduced the plugin. In many cases, the\nplugin may be introduced in beets \"trunk\" (the latest source version) and might\nnot be released yet. Take a look at :doc:`the changelog </changelog>` to see\nwhich version added the plugin. (You can type ``beet version`` to check which\nversion of beets you have installed.)\n\nIf you want to live on the bleeding edge and use the latest source version of\nbeets, you can check out the source (see :ref:`the relevant question <src>`).\n\nTo see the beets documentation for your version (and avoid confusion with new\nfeatures in trunk), select your version from the menu in the sidebar.\n\n.. _kill:\n\n…ignore control-C during an import?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nTyping a ^C (control-C) control sequence will not halt beets' multithreaded\nimporter while it is waiting at a prompt for user input. Instead, hit \"return\"\n(dismissing the prompt) after typing ^C. Alternatively, just type a \"b\" for\n\"aBort\" at most prompts. Typing ^C *will* work if the importer interface is\nbetween prompts.\n\nAlso note that beets may take some time to quit after ^C is typed; it tries to\nclean up after itself briefly even when canceled.\n\n(For developers: this is because the UI thread is blocking on ``input`` and\ncannot be interrupted by the main thread, which is trying to close all pipeline\nstages in the exception handler by setting a flag. There is no simple way to\nremedy this.)\n\n.. _id3v24:\n\n…not change my ID3 tags?\n~~~~~~~~~~~~~~~~~~~~~~~~\n\nBeets writes ID3v2.4_ tags by default. Some software, including Windows (i.e.,\nWindows Explorer and Windows Media Player) and `id3lib/id3v2\n<https://sourceforge.net/projects/id3v2/>`__, don't support v2.4 tags. When\nusing 2.4-unaware software, it might look like the tags are unmodified or\nmissing completely.\n\nTo enable ID3v2.3 tags, enable the :ref:`id3v23` config option.\n\n.. _id3v2.4: https://id3.org/id3v2.4.0-structure\n\n.. _invalid:\n\n…complain that a file is \"unreadable\"?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nBeets will log a message like \"unreadable file: /path/to/music.mp3\" when it\nencounters files that *look* like music files (according to their extension) but\nseem to be broken. Most of the time, this is because the file is corrupted. To\ncheck whether the file is intact, try opening it in another media player (e.g.,\n`VLC <https://www.videolan.org/vlc/index.html>`__) to see whether it can read\nthe file. You can also use specialized programs for checking file\nintegrity---for example, type ``metaflac --list music.flac`` to check FLAC\nfiles.\n\nIf beets still complains about a file that seems to be valid, `open a new\nticket`_ and we'll look into it. There's always a possibility that there's a bug\n\"upstream\" in the `Mutagen <https://github.com/quodlibet/mutagen>`__ library\nused by beets, in which case we'll forward the bug to that project's tracker.\n\n.. _importhang:\n\n…seem to \"hang\" after an import finishes?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nProbably not. Beets uses a *multithreaded importer* that overlaps many different\nactivities: it can prompt you for decisions while, in the background, it talks\nto MusicBrainz and copies files. This means that, even after you make your last\ndecision, there may be a backlog of files to be copied into place and tags to be\nwritten. (Plugin tasks, like looking up lyrics and genres, also run at this\ntime.) If beets pauses after you see all the albums go by, have patience.\n\n.. _replaceq:\n\n…put a bunch of underscores in my filenames?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nWhen naming files, beets replaces certain characters to avoid causing problems\non the filesystem. For example, leading dots can confusingly hide files on Unix\nand several non-alphanumeric characters are forbidden on Windows.\n\nThe :ref:`replace` config option controls which replacements are made. By\ndefault, beets makes filenames safe for all known platforms by replacing several\npatterns with underscores. This means that, even on Unix, filenames are made\nWindows-safe so that network filesystems (such as SMB) can be used safely.\n\nMost notably, Windows forbids trailing dots, so a folder called \"M.I.A.\" will be\nrewritten to \"M.I.A\\_\" by default. Change the ``replace`` config if you don't\nwant this behavior and don't need Windows-safe names.\n\n.. _pathq:\n\n…say \"command not found\"?\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\nYou need to put the ``beet`` program on your system's search path. If you\ninstalled using pip, the command ``pip show -f beets`` can show you where\n``beet`` was placed on your system. If you need help extending your ``$PATH``,\ntry `this Super User answer`_.\n\n.. _open a new ticket: https://github.com/beetbox/beets/issues/new?template=bug-report.md\n\n.. _pip: https://pip.pypa.io/en/stable/\n\n.. _this super user answer: https://superuser.com/questions/284342/what-are-path-and-other-environment-variables-and-how-can-i-set-or-use-them/284361#284361\n"
  },
  {
    "path": "docs/guides/advanced.rst",
    "content": "Advanced Awesomeness\n====================\n\nSo you have beets up and running and you've started :doc:`importing your music\n</guides/tagger>`. There's a lot more that beets can do now that it has\ncataloged your collection. Here's a few features to get you started.\n\nMost of these tips involve :doc:`plugins </plugins/index>` and fiddling with\nbeets' :doc:`configuration </reference/config>`. So use your favorite text\neditor to create a config file before you continue.\n\nFetch album art, genres, and lyrics\n-----------------------------------\n\nBeets can help you fill in more than just the basic taxonomy metadata that comes\nfrom MusicBrainz. Plugins can provide :doc:`album art </plugins/fetchart>`,\n:doc:`lyrics </plugins/lyrics>`, and :doc:`genres </plugins/lastgenre>` from\ndatabases around the Web.\n\nIf you want beets to get any of this data automatically during the import\nprocess, just enable any of the three relevant plugins (see\n:doc:`/plugins/index`). For example, put this line in your :doc:`config file\n</reference/config>` to enable all three:\n\n::\n\n    plugins: fetchart lyrics lastgenre\n\nEach plugin also has a command you can run to fetch data manually. For example,\nif you want to get lyrics for all the Beatles tracks in your collection, just\ntype ``beet lyrics beatles`` after enabling the plugin.\n\nRead more about using each of these plugins:\n\n- :doc:`/plugins/fetchart` (and its accompanying :doc:`/plugins/embedart`)\n- :doc:`/plugins/lyrics`\n- :doc:`/plugins/lastgenre`\n\nCustomize your file and folder names\n------------------------------------\n\nBeets uses an extremely flexible template system to name the folders and files\nthat organize your music in your filesystem. Take a look at\n:ref:`path-format-config` for the basics: use fields like ``$year`` and\n``$title`` to build up a naming scheme. But if you need more flexibility, there\nare two features you need to know about:\n\n- :ref:`Template functions <template-functions>` are simple expressions you can\n  use in your path formats to add logic to your names. For example, you can get\n  an artist's first initial using ``%upper{%left{$albumartist,1}}``.\n- If you need more flexibility, the :doc:`/plugins/inline` lets you write\n  snippets of Python code that generate parts of your filenames. The equivalent\n  code for getting an artist initial with the *inline* plugin looks like\n  ``initial: albumartist[0].upper()``.\n\nIf you already have music in your library and want to update their names\naccording to a new scheme, just run the :ref:`move-cmd` command to rename\neverything.\n\nStream your music to another computer\n-------------------------------------\n\nSometimes it can be really convenient to store your music on one machine and\nplay it on another. For example, I like to keep my music on a server at home,\nbut play it at work (without copying my whole library locally). The\n:doc:`/plugins/web` makes streaming your music easy---it's sort of like having\nyour own personal Spotify.\n\nFirst, enable the ``web`` plugin (see :doc:`/plugins/index`). Run the server by\ntyping ``beet web`` and head to http://localhost:8337 in a browser. You can\nbrowse your collection with queries and, if your browser supports it, play music\nusing HTML5 audio.\n\nTranscode music files for media players\n---------------------------------------\n\nDo you ever find yourself transcoding high-quality rips to a lower-bitrate,\nlossy format for your phone or music player? Beets can help with that.\n\nYou'll first need to install ffmpeg_. Then, enable beets'\n:doc:`/plugins/convert`. Set a destination directory in your :doc:`config file\n</reference/config>` like so:\n\n::\n\n    convert:\n        dest: ~/converted_music\n\nThen, use the command ``beet convert QUERY`` to transcode everything matching\nthe query and drop the resulting files in that directory, named according to\nyour path formats. For example, ``beet convert long winters`` will move over\neverything by the Long Winters for listening on the go.\n\nThe plugin has many more dials you can fiddle with to get your conversions how\nyou like them. Check out :doc:`its documentation </plugins/convert>`.\n\n.. _ffmpeg: https://www.ffmpeg.org\n\nStore any data you like\n-----------------------\n\nThe beets database keeps track of a long list of :ref:`built-in fields\n<itemfields>`, but you're not limited to just that list. Say, for example, that\nyou like to categorize your music by the setting where it should be played. You\ncan invent a new ``context`` attribute to store this. Set the field using the\n:ref:`modify-cmd` command:\n\n::\n\n    beet modify context=party artist:'beastie boys'\n\nBy default, beets will show you the changes that are about to be applied and ask\nif you really want to apply them to all, some or none of the items or albums.\nYou can type y for \"yes\", n for \"no\", or s for \"select\". If you choose the\nlatter, the command will prompt you for each individual matching item or album.\n\nThen :doc:`query </reference/query>` your music just as you would with any other\nfield:\n\n::\n\n    beet ls context:mope\n\nYou can even use these fields in your filenames (see :ref:`path-format-config`).\n\nAnd, unlike :ref:`built-in fields <itemfields>`, such fields can be removed:\n\n::\n\n    beet modify context! artist:'beastie boys'\n\nRead more than you ever wanted to know about the *flexible attributes* feature\n`on the beets blog`_.\n\n.. _on the beets blog: https://beets.io/blog/flexattr.html\n\nChoose a path style manually for some music\n-------------------------------------------\n\nSometimes, you need to categorize some songs differently in your file system.\nFor example, you might want to group together all the music you don't really\nlike, but keep around to play for friends and family. This is, of course,\nimpossible to determine automatically using metadata from MusicBrainz.\n\nInstead, use a flexible attribute (see above) to store a flag on the music you\nwant to categorize, like so:\n\n::\n\n    beet modify bad=1 christmas\n\nThen, you can query on this field in your path formats to sort this music\ndifferently. Put something like this in your configuration file:\n\n::\n\n    paths:\n        bad:1: Bad/$artist/$title\n\nUsed together, flexible attributes and path format conditions let you sort your\nmusic by any criteria you can imagine.\n\nAutomatically add new music to your library\n-------------------------------------------\n\nAs a command-line tool, beets is perfect for automated operation via a cron job\nor the like. To use it this way, you might want to use these options in your\n:doc:`config file </reference/config>`:\n\n.. code-block:: yaml\n\n    import:\n        incremental: yes\n        quiet: yes\n        log: /path/to/log.txt\n\nThe :ref:`incremental` option will skip importing any directories that have been\nimported in the past. :ref:`quiet` avoids asking you any questions (since this\nwill be run automatically, no input is possible). You might also want to use the\n:ref:`quiet_fallback` options to configure what should happen when no\nnear-perfect match is found -- this option depends on your level of paranoia.\nFinally, :ref:`import_log` will make beets record its decisions so you can come\nback later and see what you need to handle manually.\n\nThe last step is to set up cron or some other automation system to run ``beet\nimport /path/to/incoming/music``.\n\nUseful reports\n--------------\n\nSince beets has a quite powerful query tool, this list contains some useful and\npowerful queries to run on your library.\n\n- See a list of all albums which have files which are 128 bit rate:\n\n  ::\n\n      beet list bitrate:128000\n\n- See a list of all albums with the tracks listed in order of bit rate:\n\n  ::\n\n      beet ls -f '$bitrate $artist - $title' bitrate+\n\n- See a list of albums and their formats:\n\n  ::\n\n      beet ls -f '$albumartist $album $format' | sort | uniq\n\n  Note that ``beet ls --album -f '... $format'`` doesn't do what you want,\n  because ``format`` is an item-level field, not an album-level one. If an\n  album's tracks exist in multiple formats, the album will appear in the list\n  once for each format.\n"
  },
  {
    "path": "docs/guides/index.rst",
    "content": "Guides\n======\n\nThis section contains a couple of walkthroughs that will help you get familiar\nwith beets. If you're new to beets, you'll want to begin with the :doc:`main`\nguide.\n\n.. toctree::\n    :maxdepth: 1\n\n    main\n    installation\n    tagger\n    advanced\n"
  },
  {
    "path": "docs/guides/installation.rst",
    "content": "Installation\n============\n\nBeets requires `Python 3.10 or later`_. You can install it using pipx_ or pip_.\n\n.. _python 3.10 or later: https://www.python.org/downloads/\n\nUsing ``pipx`` or ``pip``\n-------------------------\n\nWe recommend installing with pipx_ as it isolates beets and its dependencies\nfrom your system Python and other Python packages. This helps avoid dependency\nconflicts and keeps your system clean.\n\n.. <!-- start-quick-install -->\n\n.. tab-set::\n\n    .. tab-item:: pipx\n\n        .. code-block:: console\n\n            pipx install beets\n\n    .. tab-item:: pip\n\n        .. code-block:: console\n\n            pip install beets\n\n    .. tab-item:: pip (user install)\n\n        .. code-block:: console\n\n            pip install --user beets\n\n.. <!-- end-quick-install -->\n\nIf you don't have pipx_ installed, you can follow the instructions on the `pipx\ninstallation page`_ to get it set up.\n\n.. _pip: https://pip.pypa.io/en/stable/\n\n.. _pipx: https://pipx.pypa.io/stable\n\n.. _pipx installation page: https://pipx.pypa.io/stable/installation/\n\nManaging Plugins with ``pipx``\n------------------------------\n\nWhen using pipx_, you can install beets with built-in plugin dependencies using\nextras, inject third-party packages, and upgrade everything cleanly.\n\nInstall beets with extras for built-in plugins:\n\n.. code-block:: console\n\n    pipx install \"beets[lyrics,lastgenre]\"\n\nIf you already have beets installed, reinstall with a new set of extras:\n\n.. code-block:: console\n\n    pipx install --force \"beets[lyrics,lastgenre]\"\n\nInject additional packages into the beets environment (useful for third-party\nplugins):\n\n.. code-block:: console\n\n    pipx inject beets <package-name>\n\nTo upgrade beets and all injected packages:\n\n.. code-block:: console\n\n    pipx upgrade beets\n\nInstallation FAQ\n----------------\n\nWindows Installation\n~~~~~~~~~~~~~~~~~~~~\n\n**Q: What's the process for installing on Windows?**\n\nInstalling beets on Windows can be tricky. Following these steps might help you\nget it right:\n\n1. `Install Python`_ (check \"Add Python to PATH\" skip to 3)\n2. Ensure Python is in your ``PATH`` (add if needed):\n\n   - Settings → System → About → Advanced system settings → Environment\n     Variables\n   - Edit \"PATH\" and add: `;C:\\Python39;C:\\Python39\\Scripts`\n   - *Guide: [Adding Python to\n     PATH](https://realpython.com/add-python-to-path/)*\n\n3. Now install beets by running: ``pip install beets``\n4. You're all set! Type ``beet version`` in a new command prompt to verify the\n   installation.\n\n**Bonus: Windows Context Menu Integration**\n\nWindows users may also want to install a context menu item for importing files\ninto beets. Download the beets.reg_ file and open it in a text file to make sure\nthe paths to Python match your system. Then double-click the file add the\nnecessary keys to your registry. You can then right-click a directory and choose\n\"Import with beets\".\n\n.. _beets.reg: https://github.com/beetbox/beets/blob/master/extra/beets.reg\n\n.. _install pip: https://pip.pypa.io/en/stable/installing/\n\n.. _install python: https://www.python.org/downloads/\n\nARM Installation\n~~~~~~~~~~~~~~~~\n\n**Q: Can I run beets on a Raspberry Pi or other ARM device?**\n\nYes, but with some considerations: Beets on ARM devices is not recommended for\nLinux novices. If you are comfortable with troubleshooting tools like ``pip``,\n``make``, and binary dependencies (e.g. ``ffmpeg`` and ``ImageMagick``), you\nwill be fine. We have `notes for ARM`_ and an `older ARM reference`_. Beets is\ngenerally developed on x86-64 based devices, and most plugins target that\nplatform as well.\n\n.. _notes for arm: https://github.com/beetbox/beets/discussions/4910\n\n.. _older arm reference: https://discourse.beets.io/t/diary-of-beets-on-arm-odroid-hc4-armbian/1993\n\nPackage Manager Installation\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n**Q: Can I install beets using my operating system's built-in package manager?**\n\nWe generally don't recommend this route. OS package managers tend to ship\noutdated versions of beets, and installing third-party plugins into a\nsystem-managed environment ranges from awkward to impossible. You'll have a much\nbetter time with pipx_ or pip_ as described above.\n\nThat said, if you know what you're doing and prefer your system package manager,\nhere are the options available:\n\n- **Debian/Ubuntu** (`Debian <debian details_>`_, `Ubuntu <ubuntu details_>`_):\n  ``apt-get install beets``\n- **Arch Linux** (`extra <arch btw_>`_, `AUR dev <aur_>`_): ``pacman -S beets``\n- **Alpine Linux** (`package <alpine package_>`_): ``apk add beets``\n- **Void Linux** (`package <void package_>`_): ``xbps-install -S beets``\n- **Gentoo Linux**: ``emerge beets`` (USE flags available for optional plugin\n  deps)\n- **FreeBSD** (`port <freebsd_>`_): ``audio/beets``\n- **OpenBSD** (`port <openbsd_>`_): ``pkg_add beets``\n- **Fedora** (`package <dnf package_>`_): ``dnf install beets beets-plugins\n  beets-doc``\n- **Solus**: ``eopkg install beets``\n- **NixOS** (`package <nixos_>`_): ``nix-env -i beets``\n- **MacPorts**: ``port install beets`` or ``port install beets-full`` (includes\n  third-party plugins)\n\n.. _alpine package: https://pkgs.alpinelinux.org/package/edge/community/x86_64/beets\n\n.. _arch btw: https://archlinux.org/packages/extra/any/beets/\n\n.. _aur: https://aur.archlinux.org/packages/beets-git/\n\n.. _debian details: https://tracker.debian.org/pkg/beets\n\n.. _dnf package: https://packages.fedoraproject.org/pkgs/beets/\n\n.. _freebsd: https://www.freshports.org/audio/beets/\n\n.. _nixos: https://github.com/NixOS/nixpkgs/tree/master/pkgs/development/python-modules/beets\n\n.. _openbsd: https://openports.pl/path/audio/beets\n\n.. _ubuntu details: https://launchpad.net/ubuntu/+source/beets\n\n.. _void package: https://github.com/void-linux/void-packages/tree/master/srcpkgs/beets\n"
  },
  {
    "path": "docs/guides/main.rst",
    "content": "Getting Started\n===============\n\nWelcome to beets_! This guide will help get started with improving and\norganizing your music collection.\n\n.. _beets: https://beets.io/\n\nQuick Installation\n------------------\n\nBeets is distributed via PyPI_ and can be installed by most users with a single\ncommand:\n\n.. include:: installation.rst\n    :start-after: <!-- start-quick-install -->\n    :end-before: <!-- end-quick-install -->\n\n.. admonition:: Need more information?\n\n    Having trouble with the commands above? Looking for information how to\n    install plugins and keep Beets updated? See the :doc:`complete installation\n    guide </guides/installation>` for:\n\n    - Managing plugins with pipx\n    - OS-specific installation notes\n    - Package manager options\n\n.. _pypi: https://pypi.org/project/beets/\n\nBasic Configuration\n-------------------\n\nBefore using beets, you'll need a configuration file. This YAML file tells beets\nwhere to store your music and how to organize it.\n\nWhile beets is highly configurable, you only need a few basic settings to get\nstarted.\n\n1. **Open the config file:**\n       .. code-block:: console\n\n           beet config -e\n\n       This creates the file (if needed) and opens it in your default editor.\n       You can also find its location with ``beet config -p``.\n2. **Add required settings:**\n       In the config file, set the ``directory`` option to the path where you\n       want beets to store your music files. Set the ``library`` option to the\n       path where you want beets to store its database file.\n\n       .. code-block:: yaml\n\n           directory: ~/music\n           library: ~/data/musiclibrary.db\n3. **Choose your import style** (pick one):\n       Beets offers flexible import strategies to match your workflow. Choose\n       one of the following approaches and put one of the following in your\n       config file:\n\n       .. tab-set::\n\n           .. tab-item:: Copy Files (Default)\n\n               This is the default configuration and assumes you want to start a new organized music folder (inside ``directory`` above). During import we will *copy* cleaned-up music into that empty folder.\n\n               .. code-block:: yaml\n\n                   import:\n                       copy: yes    # Copy files to new location\n\n\n           .. tab-item:: Move Files\n\n               Start with a new empty directory, but *move* new music in instead of copying it (saving disk space).\n\n               .. code-block:: yaml\n\n                   import:\n                       move: yes    # Move files to new location\n\n           .. tab-item:: Use Existing Structure\n\n               Keep your current directory structure; importing should never move or copy files but instead just correct the tags on music. Make sure to point ``directory`` at the place where your music is currently stored.\n\n               .. code-block:: yaml\n\n                   import:\n                       copy: no     # Use files in place\n\n           .. tab-item:: Read-Only Mode\n\n               Keep everything exactly as-is; only track metadata in database. (Corrected tags will still be stored in beets' database, and you can use them to do renaming or tag changes later.)\n\n               .. code-block:: yaml\n\n                   import:\n                       copy: no     # Use files in place\n                       write: no    # Don't modify tags\n4. **Add customization via plugins (optional):**\n       Beets comes with many plugins that extend its functionality. You can\n       enable plugins by adding a ``plugins`` section to your config file.\n\n       We recommend adding at least one :ref:`Autotagger Plugin\n       <autotagger_extensions>` to help with fetching metadata during import.\n       For getting started, :doc:`MusicBrainz </plugins/musicbrainz>` is a good\n       choice.\n\n       .. code-block:: yaml\n\n           plugins:\n             - musicbrainz  # Example plugin for fetching metadata\n             - ... other plugins you want ...\n\n       You can find a list of available plugins in the :doc:`plugins index\n       </plugins/index>`.\n\n.. _yaml: https://yaml.org/\n\nTo validate that you've set up your configuration and it is valid YAML, you can\ntype ``beet version`` to see a list of enabled plugins or ``beet config`` to get\na complete listing of your current configuration.\n\n.. dropdown:: Minimal configuration\n\n    Here's a sample configuration file that includes the settings mentioned above:\n\n    .. code-block:: yaml\n\n        directory: ~/music\n        library: ~/data/musiclibrary.db\n\n        import:\n            move: yes    # Move files to new location\n            # copy: no   # Use files in place\n            # write: no  # Don't modify tags\n\n        plugins:\n          - musicbrainz  # Example plugin for fetching metadata\n          # - ... other plugins you want ...\n\n    You can copy and paste this into your config file and modify it as needed.\n\n.. admonition:: Ready for more?\n\n    For a complete reference of all configuration options, see the\n    :doc:`configuration reference </reference/config>`.\n\nImporting Your Music\n--------------------\n\nNow you're ready to import your music into beets!\n\n.. important::\n\n    Importing can modify and move your music files. **Make sure you have a\n    recent backup** before proceeding.\n\nChoose Your Import Method\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThere are two good ways to bring your *existing* library into beets database.\n\n.. tab-set::\n\n    .. tab-item:: Autotag (Recommended)\n\n        This method uses beets' autotagger to find canonical metadata for every album you import. It may take a while, especially for large libraries, and it's an interactive process. But it ensures all your songs' tags are exactly right from the get-go.\n\n        .. code-block:: console\n\n            beet import /a/chunk/of/my/library\n\n        .. warning::\n\n            The point about speed bears repeating: using the autotagger on a large library can take a\n            very long time, and it's an interactive process. So set aside a good chunk of\n            time if you're going to go that route.\n\n            We also recommend importing smaller batches of music at a time (e.g., a few albums) to make the process more manageable. For more on the interactive tagging\n            process, see :doc:`tagger`.\n\n\n    .. tab-item:: Quick Import\n\n        This method quickly brings all your files with all their current metadata into beets' database without any changes. It's really fast, but it doesn't clean up or correct any tags.\n\n        To use this method, run:\n\n        .. code-block:: console\n\n            beet import --noautotag /my/huge/mp3/library\n\n        The ``--noautotag`` / ``-A``  flag skips autotagging and uses your files' current metadata.\n\n.. admonition:: More Import Options\n\n    The ``beet import`` command has many options to customize its behavior. For\n    a full list, type ``beet help import`` or see the :ref:`import command\n    reference <import-cmd>`.\n\nAdding More Music Later\n~~~~~~~~~~~~~~~~~~~~~~~\n\nWhen you acquire new music, use the same ``beet import`` command to add it to\nyour library:\n\n.. code-block:: console\n\n    beet import ~/new_totally_not_ripped_album\n\nThis will apply the same autotagging process to your new additions. For\nalternative import behaviors, consult the options mentioned above.\n\nSeeing Your Music\n-----------------\n\nOnce you've imported music into beets, you'll want to explore and query your\nlibrary. Beets provides several commands for searching, browsing, and getting\nstatistics about your collection.\n\nBasic Searching\n~~~~~~~~~~~~~~~\n\nThe ``beet list`` command (shortened to ``beet ls``) lets you search your music\nlibrary using :doc:`query string </reference/query>` similar to web searches:\n\n.. code-block:: console\n\n    $ beet ls the magnetic fields\n    The Magnetic Fields - Distortion - Three-Way\n    The Magnetic Fields - Dist\n    The Magnetic Fields - Distortion - Old Fools\n\n.. code-block:: console\n\n    $ beet ls hissing gronlandic\n    of Montreal - Hissing Fauna, Are You the Destroyer? - Gronlandic Edit\n\n.. code-block:: console\n\n    $ beet ls bird\n    The Knife - The Knife - Bird\n    The Mae Shi - Terrorbird - Revelation Six\n\nBy default, search terms match against :ref:`common attributes <keywordquery>`\nof songs, and multiple terms are combined with AND logic (a track must match\n*all* criteria).\n\nSearching Specific Fields\n~~~~~~~~~~~~~~~~~~~~~~~~~\n\nTo narrow a search term to a particular metadata field, prefix the term with the\nfield name followed by a colon. For example, ``album:bird`` searches for \"bird\"\nonly in the \"album\" field of your songs. For more details, see\n:doc:`/reference/query/`.\n\n.. code-block:: console\n\n    $ beet ls album:bird\n    The Mae Shi - Terrorbird - Revelation Six\n\nThis searches only the ``album`` field for the term ``bird``.\n\nSearching for Albums\n~~~~~~~~~~~~~~~~~~~~\n\nThe ``beet list`` command also has an ``-a`` option, which searches for albums\ninstead of songs:\n\n.. code-block:: console\n\n    $ beet ls -a forever\n    Bon Iver - For Emma, Forever Ago\n    Freezepop - Freezepop Forever\n\nCustom Output Formatting\n~~~~~~~~~~~~~~~~~~~~~~~~\n\nThere's also an ``-f`` option (for *format*) that lets you specify what gets\ndisplayed in the results of a search:\n\n.. code-block:: console\n\n    $ beet ls -a forever -f \"[$format] $album ($year) - $artist - $title\"\n    [MP3] For Emma, Forever Ago (2009) - Bon Iver - Flume\n    [AAC] Freezepop Forever (2011) - Freezepop - Harebrained Scheme\n\nIn the format string, field references like ``$format``, ``$year``, ``$album``,\netc., are replaced with data from each result.\n\n.. dropdown:: Available fields for formatting\n\n    To see all available fields you can use in custom formats, run:\n\n    .. code-block:: console\n\n        beet fields\n\n    This will display a comprehensive list of metadata fields available for your music.\n\nLibrary Statistics\n~~~~~~~~~~~~~~~~~~\n\nBeets can also show you statistics about your music collection:\n\n.. code-block:: console\n\n    $ beet stats\n    Tracks: 13019\n    Total time: 4.9 weeks\n    Total size: 71.1 GB\n    Artists: 548\n    Albums: 1094\n\n.. admonition:: Ready for more advanced queries?\n\n    The ``beet list`` command has many additional options for sorting, limiting\n    results, and more complex queries. For a complete reference, run:\n\n    .. code-block:: console\n\n        beet help list\n\n    Or see the :ref:`list command reference <list-cmd>`.\n\nKeep Playing\n------------\n\nCongratulations! You've now mastered the basics of beets. But this is only the\nbeginning, beets has many more powerful features to explore.\n\nContinue Your Learning Journey\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n*I was there to push people beyond what's expected of them.*\n\n.. grid:: 2\n    :gutter: 3\n\n    .. grid-item-card:: :octicon:`zap` Advanced Techniques\n        :link: advanced\n        :link-type: doc\n\n        Explore sophisticated beets workflows including:\n\n        - Advanced tagging strategies\n        - Complex import scenarios\n        - Custom metadata management\n        - Workflow automation\n\n    .. grid-item-card:: :octicon:`terminal` Command Reference\n        :link: /reference/cli\n        :link-type: doc\n\n        Comprehensive guide to all beets commands:\n\n        - Complete command syntax\n        - All available options\n        - Usage examples\n        - **Important operations like deleting music**\n\n    .. grid-item-card:: :octicon:`plug` Plugin Ecosystem\n        :link: /plugins/index\n        :link-type: doc\n\n        Discover beets' true power through plugins:\n\n        - Metadata fetching from multiple sources\n        - Audio analysis and processing\n        - Streaming service integration\n        - Custom export formats\n\n    .. grid-item-card:: :octicon:`question` Illustrated Walkthrough\n        :link: https://beets.io/blog/walkthrough.html\n        :link-type: url\n\n        Visual, step-by-step guide covering:\n\n        - Real-world import examples\n        - Screenshots of interactive tagging\n        - Common workflow patterns\n        - Troubleshooting tips\n\n.. admonition:: Need Help?\n\n    Remember you can always use ``beet help`` to see all available commands, or\n    ``beet help [command]`` for detailed help on specific commands.\n\nJoin the Community\n~~~~~~~~~~~~~~~~~~\n\nWe'd love to hear about your experience with beets!\n\n.. grid:: 2\n    :gutter: 2\n\n    .. grid-item-card:: :octicon:`comment-discussion` Discussion Board\n        :link: https://github.com/beetbox/beets/discussions\n        :link-type: url\n\n        - Ask questions\n        - Share tips and tricks\n        - Discuss feature ideas\n        - Get help from other users\n\n    .. grid-item-card:: :octicon:`git-pull-request` Developer Resources\n        :link: /dev/index\n        :link-type: doc\n\n        - Contribute code\n        - Report issues\n        - Review pull requests\n        - Join development discussions\n\n.. admonition:: Found a Bug?\n\n    If you encounter any issues, please report them on our `GitHub Issues page\n    <https://github.com/beetbox/beets/issues>`_.\n"
  },
  {
    "path": "docs/guides/tagger.rst",
    "content": ".. _using-the-auto-tagger:\n\nUsing the Auto-Tagger\n=====================\n\nBeets' automatic metadata correcter is sophisticated but complicated and\ncryptic. This is a guide to help you through its myriad inputs and options.\n\nAn Apology and a Brief Interlude\n--------------------------------\n\nI would like to sincerely apologize that the autotagger in beets is so fussy. It\nasks you a *lot* of complicated questions, insecurely asking that you verify\nnearly every assumption it makes. This means importing and correcting the tags\nfor a large library can be an endless, tedious process. I'm sorry for this.\n\nMaybe it will help to think of it as a tradeoff. By carefully examining every\nalbum you own, you get to become more familiar with your library, its extent,\nits variation, and its quirks. People used to spend hours lovingly sorting and\nresorting their shelves of LPs. In the iTunes age, many of us toss our music\ninto a heap and forget about it. This is great for some people. But there's\nvalue in intimate, complete familiarity with your collection. So instead of a\nchore, try thinking of correcting tags as quality time with your music\ncollection. That's what I do.\n\nOne practical piece of advice: because beets' importer runs in multiple threads,\nit queues up work in the background while it's waiting for you to respond. So if\nyou find yourself waiting for beets for a few seconds between every question it\nasks you, try walking away from the computer for a while, making some tea, and\ncoming back. Beets will have a chance to catch up with you and will ask you\nquestions much more quickly.\n\nBack to the guide.\n\nOverview\n--------\n\nBeets' tagger is invoked using the ``beet import`` command. Point it at a\ndirectory and it imports the files into your library, tagging them as it goes\n(unless you pass ``--noautotag``, of course). There are several assumptions\nbeets currently makes about the music you import. In time, we'd like to remove\nall of these limitations.\n\n- Your music should be organized by album into directories. That is, the tagger\n  assumes that each album is in a single directory. These directories can be\n  arbitrarily deep (like ``music/2010/hiphop/seattle/freshespresso/glamour``),\n  but any directory with music files in it is interpreted as a separate album.\n\n  There are, however, a couple of exceptions to this rule:\n\n  First, directories that look like separate parts of a *multi-disc album* are\n  tagged together as a single release. If two adjacent albums have a common\n  prefix, followed by \"disc,\" \"disk,\" or \"CD\" and then a number, they are tagged\n  together.\n\n  Second, if you have jumbled directories containing more than one album, you\n  can ask beets to split them apart for you based on their metadata. Use either\n  the ``--group-albums`` command-line flag or the *G* interactive option\n  described below.\n\n- The music may have bad tags, but it's not completely untagged. This is because\n  beets by default infers tags based on existing metadata. But this is not a\n  hard and fast rule---there are a few ways to tag metadata-poor music:\n\n      - You can use the *E* or *I* options described below to search in\n        MusicBrainz for a specific album or song.\n      - The :doc:`Acoustid plugin </plugins/chroma>` extends the autotagger to\n        use acoustic fingerprinting to find information for arbitrary audio.\n        Install that plugin if you're willing to spend a little more CPU power\n        to get tags for unidentified albums. (But be aware that it does slow\n        down the process.)\n      - The :doc:`FromFilename plugin </plugins/fromfilename>` adds the ability\n        to guess tags from the filenames. Use this plugin if your tracks have\n        useful names (like \"03 Call Me Maybe.mp3\") but their tags don't reflect\n        that.\n\n- Currently, MP3, AAC, FLAC, ALAC, Ogg Vorbis, Monkey's Audio, WavPack,\n  Musepack, Windows Media, Opus, and AIFF files are supported. (Do you use some\n  other format? Please `file a feature request`_!)\n\n.. _file a feature request: https://github.com/beetbox/beets/issues/new?template=feature-request.md\n\nNow that that's out of the way, let's tag some music.\n\n.. _import-options:\n\nOptions\n-------\n\nTo import music, just say ``beet import MUSICDIR``. There are, of course, a few\ncommand-line options you should know:\n\n- ``beet import -A``: don't try to autotag anything; just import files (this\n  goes much faster than with autotagging enabled)\n- ``beet import -W``: when autotagging, don't write new tags to the files\n  themselves (just keep the new metadata in beets' database)\n- ``beet import -C``: don't copy imported files to your music directory; leave\n  them where they are\n- ``beet import -m``: move imported files to your music directory (overrides the\n  ``-c`` option)\n- ``beet import -l LOGFILE``: write a message to ``LOGFILE`` every time you skip\n  an album or choose to take its tags \"as-is\" (see below) or the album is\n  skipped as a duplicate; this lets you come back later and reexamine albums\n  that weren't tagged successfully. Run ``beet import --from-logfile=LOGFILE``\n  rerun the importer on such paths from the logfile.\n- ``beet import -q``: quiet mode. Never prompt for input and, instead,\n  conservatively skip any albums that need your opinion. The ``-ql`` combination\n  is recommended.\n- ``beet import -t``: timid mode, which is sort of the opposite of \"quiet.\" The\n  importer will ask your permission for everything it does, confirming even very\n  good matches with a prompt.\n- ``beet import -p``: automatically resume an interrupted import. The importer\n  keeps track of imports that don't finish completely (either due to a crash or\n  because you stop them halfway through) and, by default, prompts you to decide\n  whether to resume them. The ``-p`` flag automatically says \"yes\" to this\n  question. Relatedly, ``-P`` flag automatically says \"no.\"\n- ``beet import -s``: run in *singleton* mode, tagging individual tracks instead\n  of whole albums at a time. See the \"as Tracks\" choice below. This means you\n  can use ``beet import -AC`` to quickly add a bunch of files to your library\n  without doing anything to them.\n- ``beet import -g``: assume there are multiple albums contained in each\n  directory. The tracks contained a directory are grouped by album artist and\n  album name and you will be asked to import each of these groups separately.\n  See the \"Group albums\" choice below.\n\nSimilarity\n----------\n\nSo you import an album into your beets library. It goes like this:\n\n::\n\n    $ beet imp witchinghour\n    Tagging:\n        Ladytron - Witching Hour\n    (Similarity: 98.4%)\n    * Last One Standing      -> The Last One Standing\n    * Beauty                 -> Beauty*2\n    * White Light Generation -> Whitelightgenerator\n    * All the Way            -> All the Way...\n\nHere, beets gives you a preview of the album match it has found. It shows you\nwhich track titles will be changed if the match is applied. In this case, beets\nhas found a match and thinks it's a good enough match to proceed without asking\nyour permission. It has reported the *similarity* for the match it's found.\nSimilarity is a measure of how well-matched beets thinks a tagging option is.\n100% similarity means a perfect match 0% indicates a truly horrible match.\n\nIn this case, beets has proceeded automatically because it found an option with\nvery high similarity (98.4%). But, as you'll notice, if the similarity isn't\nquite so high, beets will ask you to confirm changes. This is because beets\ncan't be very confident about more dissimilar matches, and you (as a human) are\nbetter at making the call than a computer. So it occasionally asks for help.\n\nChoices\n-------\n\nWhen beets needs your input about a match, it says something like this:\n\n::\n\n    Tagging:\n        Beirut - Lon Gisland\n    (Similarity: 94.4%)\n    * Scenic World (Second Version) -> Scenic World\n    [A]pply, More candidates, Skip, Use as-is, as Tracks, Enter search, enter Id, or aBort?\n\nWhen beets asks you this question, it wants you to enter one of the capital\nletters: A, M, S, U, T, G, E, I or B. That is, you can choose one of the\nfollowing:\n\n- *A*: Apply the suggested changes shown and move on.\n- *M*: Show more options. (See the Candidates section, below.)\n- *S*: Skip this album entirely and move on to the next one.\n- *U*: Import the album without changing any tags. This is a good option for\n  albums that aren't in the MusicBrainz database, like your friend's operatic\n  faux-goth solo record that's only on two CD-Rs in the universe.\n- *T*: Import the directory as *singleton* tracks, not as an album. Choose this\n  if the tracks don't form a real release---you just have one or more loner\n  tracks that aren't a full album. This will temporarily flip the tagger into\n  *singleton* mode, which attempts to match each track individually.\n- *G*: Group tracks in this directory by *album artist* and *album* and import\n  groups as albums. If the album artist for a track is not set then the artist\n  is used to group that track. For each group importing proceeds as for\n  directories. This is helpful if a directory contains multiple albums.\n- *E*: Enter an artist and album to use as a search in the database. Use this\n  option if beets hasn't found any good options because the album is mistagged\n  or untagged.\n- *I*: Enter a metadata backend ID to use as search in the database. Use this\n  option to specify a backend entity (for example, a MusicBrainz release or\n  recording) directly, by pasting its ID or the full URL. You can also specify\n  several IDs by separating them by a space.\n- *B*: Cancel this import task altogether. No further albums will be tagged;\n  beets shuts down immediately. The next time you attempt to import the same\n  directory, though, beets will ask you if you want to resume tagging where you\n  left off.\n\nNote that the option with ``[B]rackets`` is the default---so if you want to\napply the changes, you can just hit return without entering anything.\n\nCandidates\n----------\n\nIf you choose the M option, or if beets isn't very confident about any of the\nchoices it found, it will present you with a list of choices (called\ncandidates), like so:\n\n::\n\n    Finding tags for \"Panther - Panther\".\n    Candidates:\n    1. Panther - Yourself (66.8%)\n    2. Tav Falco's Panther Burns - Return of the Blue Panther (30.4%)\n    # selection (default 1), Skip, Use as-is, or Enter search, or aBort?\n\nHere, you have many of the same options as before, but you can also enter a\nnumber to choose one of the options that beets has found. Don't worry about\nguessing---beets will show you the proposed changes and ask you to confirm them,\njust like the earlier example. As the prompt suggests, you can just hit return\nto select the first candidate.\n\n.. _guide-duplicates:\n\nDuplicates\n----------\n\nIf beets finds an album or item in your library that seems to be the same as the\none you're importing, you may see a prompt like this:\n\n::\n\n    This album is already in the library!\n    [S]kip new, Keep all, Remove old, Merge all?\n\nBeets wants to keep you safe from duplicates, which can be a real pain, so you\nhave four choices in this situation. You can skip importing the new music,\nchoosing to keep the stuff you already have in your library; you can keep both\nthe old and the new music; you can remove the existing music and choose the new\nstuff; or you can merge all the new and old tracks into a single album. If you\nchoose that \"remove\" option, any duplicates will be removed from your library\ndatabase---and, if the corresponding files are located inside of your beets\nlibrary directory, the files themselves will be deleted as well.\n\nIf you choose \"merge\", beets will try re-importing the existing and new tracks\nas one bundle together. This is particularly helpful when you have an album\nthat's missing some tracks and then want to import the remaining songs. The\nimporter will ask you the same questions as it would if you were importing all\ntracks at once.\n\nIf you choose to keep two identically-named albums, beets can avoid storing both\nin the same directory. See :ref:`aunique` for details.\n\nFingerprinting\n--------------\n\nYou may have noticed by now that beets' autotagger works pretty well for most\nfiles, but can get confused when files don't have any metadata (or have wildly\nincorrect metadata). In this case, you need *acoustic fingerprinting*, a\ntechnology that identifies songs from the audio itself. With fingerprinting,\nbeets can autotag files that have very bad or missing tags. The :doc:`\"chroma\"\nplugin </plugins/chroma>`, distributed with beets, uses the Chromaprint_\nopen-source fingerprinting technology, but it's disabled by default. That's\nbecause it's sort of tricky to install. See the :doc:`/plugins/chroma` page for\na guide to getting it set up.\n\nBefore you jump into acoustic fingerprinting with both feet, though, give beets\na try without it. You may be surprised at how well metadata-based matching\nworks.\n\n.. _chromaprint: https://acoustid.org/chromaprint\n\nAlbum Art, Lyrics, Genres and Such\n----------------------------------\n\nAside from the basic stuff, beets can optionally fetch more specialized\nmetadata. As a rule, plugins are responsible for getting information that\ndoesn't come directly from the MusicBrainz database. This includes :doc:`album\ncover art </plugins/fetchart>`, :doc:`song lyrics </plugins/lyrics>`, and\n:doc:`musical genres </plugins/lastgenre>`. Check out the :doc:`list of plugins\n</plugins/index>` to pick and choose the data you want.\n\nMissing Albums?\n---------------\n\nIf you're having trouble tagging a particular album with beets, check to make\nsure the album is present in `the MusicBrainz database`_. You can search on\ntheir site to make sure it's cataloged there. If not, anyone can edit\nMusicBrainz---so consider adding the data yourself.\n\n.. _the musicbrainz database: https://musicbrainz.org/\n\nIf you receive a \"No matching release found\" message from the Auto-Tagger for an\nalbum you know is present in MusicBrainz, check that musicbrainz is in the\nplugin list. Until version v2.4.0_ the default metadata source for the\nAuto-Tagger, the :doc:`musicbrainz plugin </plugins/musicbrainz>`, had to be\nmanually disabled. At present, if the plugin list is changed, musicbrainz needs\nto be added to the plugin list in order to continue contributing results to\nAuto-Tagger.\n\nIf you think beets is ignoring an album that's listed in MusicBrainz, please\n`file a bug report`_.\n\n.. _file a bug report: https://github.com/beetbox/beets/issues\n\n.. _v2.4.0: https://github.com/beetbox/beets/releases/tag/v2.4.0\n\nI Hope That Makes Sense\n-----------------------\n\nIf we haven't made the process clear, please post on `the discussion board`_ and\nwe'll try to improve this guide.\n\n.. _the discussion board: https://github.com/beetbox/beets/discussions/\n"
  },
  {
    "path": "docs/index.rst",
    "content": "beets: the music geek's media organizer\n=======================================\n\nWelcome to the documentation for beets_, the media library management system for\nobsessive music geeks.\n\nIf you're new to beets, begin with the :doc:`guides/main` guide. That guide\nwalks you through installing beets, setting it up how you like it, and starting\nto build your music library.\n\nThen you can get a more detailed look at beets' features in the\n:doc:`/reference/cli/` and :doc:`/reference/config` references. You might also\nbe interested in exploring the :doc:`plugins </plugins/index>`.\n\nIf you still need help, you can drop by the ``#beets`` IRC channel on\nLibera.Chat, drop by `the discussion board`_ or `file a bug`_ in the issue\ntracker. Please let us know where you think this documentation can be improved.\n\n.. _beets: https://beets.io/\n\n.. _file a bug: https://github.com/beetbox/beets/issues\n\n.. _the discussion board: https://github.com/beetbox/beets/discussions/\n\nContents\n--------\n\n.. toctree::\n    :maxdepth: 2\n\n    guides/index\n    reference/index\n    plugins/index\n    faq\n    team\n    contributing\n    code_of_conduct\n    dev/index\n\n.. toctree::\n    :maxdepth: 1\n\n    changelog\n"
  },
  {
    "path": "docs/modd.conf",
    "content": "**/*.rst {\n  prep: make html\n}\n\n_build/html/** {\n  daemon: devd -m _build/html\n}\n"
  },
  {
    "path": "docs/plugins/absubmit.rst",
    "content": "AcousticBrainz Submit Plugin\n============================\n\nThe ``absubmit`` plugin lets you submit acoustic analysis results to an\nAcousticBrainz_ server. This plugin is now deprecated since the AcousicBrainz\nproject has been shut down.\n\nAs an alternative the beets-xtractor_ plugin can be used.\n\nWarning\n-------\n\nThe AcousticBrainz project has shut down. To use this plugin you must set the\n``base_url`` configuration option to a server offering the AcousticBrainz API.\n\nInstallation\n------------\n\nThe ``absubmit`` plugin requires the streaming_extractor_music_ program to run.\nIts source can be found on GitHub_, and while it is possible to compile the\nextractor from source, AcousticBrainz would prefer if you used their binary (see\nthe AcousticBrainz FAQ_).\n\nThen, install ``beets`` with ``absubmit`` extra\n\n    pip install \"beets[absubmit]\"\n\nLastly, enable the plugin in your configuration (see :ref:`using-plugins`).\n\nSubmitting Data\n---------------\n\nTo run the analysis program and upload its results, type:\n\n::\n\n    beet absubmit [-f] [-d] [QUERY]\n\nBy default, the command will only look for AcousticBrainz data when the tracks\ndon't already have it; the ``-f`` or ``--force`` switch makes it refetch data\neven when it already exists. You can use the ``-d`` or ``--dry`` switch to check\nwhich files will be analyzed, before you start a longer period of processing.\n\nThe plugin works on music with a MusicBrainz track ID attached. The plugin will\nalso skip music that the analysis tool doesn't support.\nstreaming_extractor_music_ currently supports files with the extensions ``mp3``,\n``ogg``, ``oga``, ``flac``, ``mp4``, ``m4a``, ``m4r``, ``m4b``, ``m4p``,\n``aac``, ``wma``, ``asf``, ``mpc``, ``wv``, ``spx``, ``tta``, ``3g2``, ``aif``,\n``aiff`` and ``ape``.\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``absubmit:`` section in your configuration\nfile. The available options are:\n\n- **auto**: Analyze every file on import. Otherwise, you need to use the ``beet\n  absubmit`` command explicitly. Default: ``no``\n- **extractor**: The absolute path to the streaming_extractor_music_ binary.\n  Default: search for the program in your ``$PATH``\n- **force**: Analyze items and submit of AcousticBrainz data even for tracks\n  that already have it. Default: ``no``.\n- **pretend**: Do not analyze and submit of AcousticBrainz data but print out\n  the items which would be processed. Default: ``no``.\n- **base_url**: The base URL of the AcousticBrainz server. The plugin has no\n  function if this option is not set. Default: None\n\n.. _acousticbrainz: https://acousticbrainz.org\n\n.. _beets-xtractor: https://github.com/adamjakab/BeetsPluginXtractor\n\n.. _faq: https://acousticbrainz.org/faq\n\n.. _github: https://github.com/MTG/essentia\n\n.. _pip: https://pip.pypa.io\n\n.. _requests: https://requests.readthedocs.io/en/latest/\n\n.. _streaming_extractor_music: https://essentia.upf.edu/\n"
  },
  {
    "path": "docs/plugins/acousticbrainz.rst",
    "content": "AcousticBrainz Plugin\n=====================\n\nThe ``acousticbrainz`` plugin gets acoustic-analysis information from the\nAcousticBrainz_ project. This plugin is now deprecated since the AcousicBrainz\nproject has been shut down.\n\nAs an alternative the beets-xtractor_ plugin can be used.\n\n.. _acousticbrainz: https://acousticbrainz.org/\n\n.. _beets-xtractor: https://github.com/adamjakab/BeetsPluginXtractor\n\nEnable the ``acousticbrainz`` plugin in your configuration (see\n:ref:`using-plugins`) and run it by typing:\n\n::\n\n    $ beet acousticbrainz [-f] [QUERY]\n\nBy default, the command will only look for AcousticBrainz data when the tracks\ndoesn't already have it; the ``-f`` or ``--force`` switch makes it re-download\ndata even when it already exists. If you specify a query, only matching tracks\nwill be processed; otherwise, the command processes every track in your library.\n\nFor all tracks with a MusicBrainz recording ID, the plugin currently sets these\nfields:\n\n- ``average_loudness``\n- ``bpm``\n- ``chords_changes_rate``\n- ``chords_key``\n- ``chords_number_rate``\n- ``chords_scale``\n- ``danceable``\n- ``gender``\n- ``genre_rosamerica``\n- ``initial_key`` (This is a built-in beets field, which can also be provided by\n  :doc:`/plugins/keyfinder`.)\n- ``key_strength``\n- ``mood_acoustic``\n- ``mood_aggressive``\n- ``mood_electronic``\n- ``mood_happy``\n- ``mood_party``\n- ``mood_relaxed``\n- ``mood_sad``\n- ``moods_mirex``\n- ``rhythm``\n- ``timbre``\n- ``tonal``\n- ``voice_instrumental``\n\nWarning\n-------\n\nThe AcousticBrainz project has shut down. To use this plugin you must set the\n``base_url`` configuration option to a server offering the AcousticBrainz API.\n\nAutomatic Tagging\n-----------------\n\nTo automatically tag files using AcousticBrainz data during import, just enable\nthe ``acousticbrainz`` plugin (see :ref:`using-plugins`). When importing new\nfiles, beets will query the AcousticBrainz API using MBID and set the\nappropriate metadata.\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``acousticbrainz:`` section in your\nconfiguration file. The available options are:\n\n- **auto**: Enable AcousticBrainz during ``beet import``. Default: ``yes``.\n- **force**: Download AcousticBrainz data even for tracks that already have it.\n  Default: ``no``.\n- **tags**: Which tags from the list above to set on your files. Default: []\n  (all).\n- **base_url**: The base URL of the AcousticBrainz server. The plugin has no\n  function if this option is not set. Default: None\n"
  },
  {
    "path": "docs/plugins/advancedrewrite.rst",
    "content": "Advanced Rewrite Plugin\n=======================\n\nThe ``advancedrewrite`` plugin lets you easily substitute values in your\ntemplates and path formats, similarly to the :doc:`/plugins/rewrite`. It's\nrecommended to read the documentation of that plugin first.\n\nThe *advanced* rewrite plugin does not only support the simple rule format of\nthe ``rewrite`` plugin, but also an advanced format: there, the plugin doesn't\nconsider the value of the rewritten field, but instead checks if the given item\nmatches a :doc:`query </reference/query>`. Only then, the field is replaced with\nthe given value. It's also possible to replace multiple fields at once, and even\nsupports multi-valued fields.\n\nTo use advanced field rewriting, first enable the ``advancedrewrite`` plugin\n(see :ref:`using-plugins`). Then, make a ``advancedrewrite:`` section in your\nconfig file to contain your rewrite rules.\n\nIn contrast to the normal ``rewrite`` plugin, you need to provide a list of\nreplacement rule objects, which can have a different syntax depending on the\nrule complexity.\n\nThe simple syntax is the same as the one of the rewrite plugin and allows to\nreplace a single field:\n\n::\n\n    advancedrewrite:\n      - artist ODD EYE CIRCLE: 이달의 소녀 오드아이써클\n\nThe advanced syntax consists of a query to match against, as well as a map of\nreplacements to apply. For example, to credit all songs of ODD EYE CIRCLE before\n2023 to their original group name, you can use the following rule:\n\n::\n\n    advancedrewrite:\n      - match: \"mb_artistid:dec0f331-cb08-4c8e-9c9f-aeb1f0f6d88c year:..2022\"\n        replacements:\n          artist: 이달의 소녀 오드아이써클\n          artist_sort: LOONA / ODD EYE CIRCLE\n\nNote how the sort name is also rewritten within the same rule. You can specify\nas many fields as you'd like in the replacements map.\n\nIf you need to work with multi-valued fields, you can use the following syntax:\n\n::\n\n    advancedrewrite:\n      - match: \"artist:배유빈 feat. 김미현\"\n        replacements:\n          artists:\n            - 유빈\n            - 미미\n\nAs a convenience, the plugin applies patterns for the ``artist`` field to the\n``albumartist`` field as well. (Otherwise, you would probably want to duplicate\nevery rule for ``artist`` and ``albumartist``.)\n\nMake sure to properly quote your query strings if they contain spaces, otherwise\nthey might not do what you expect, or even cause beets to crash.\n\nTake the following example:\n\n::\n\n    advancedrewrite:\n      # BAD, DON'T DO THIS!\n      - match: album:THE ALBUM\n        replacements:\n          artist: New artist\n\nOn the first sight, this might look sane, and replace the artist of the album\n*THE ALBUM* with *New artist*. However, due to the space and missing quotes,\nthis query will evaluate to ``album:THE`` and match ``ALBUM`` on any field,\nincluding ``artist``. As ``artist`` is the field being replaced, this query will\nresult in infinite recursion and ultimately crash beets.\n\nInstead, you should use the following rule:\n\n::\n\n    advancedrewrite:\n      # Note the quotes around the query string!\n      - match: album:\"THE ALBUM\"\n        replacements:\n          artist: New artist\n\nA word of warning: This plugin theoretically only applies to templates and path\nformats; it initially does not modify files' metadata tags or the values tracked\nby beets' library database, but since it *rewrites all field lookups*, it\nmodifies the file's metadata anyway. See comments in issue :bug:`2786`.\n\nAs an alternative to this plugin the simpler but less powerful\n:doc:`/plugins/rewrite` can be used. If you don't want to modify the item's\nmetadata and only replace values in file paths, you can check out the\n:doc:`/plugins/substitute`.\n"
  },
  {
    "path": "docs/plugins/albumtypes.rst",
    "content": "AlbumTypes Plugin\n=================\n\nThe ``albumtypes`` plugin adds the ability to format and output album types,\nsuch as \"Album\", \"EP\", \"Single\", etc. For the list of available album types, see\nthe `MusicBrainz documentation`_.\n\nTo use the ``albumtypes`` plugin, enable it in your configuration (see\n:ref:`using-plugins`). The plugin defines a new field ``$atypes``, which you can\nuse in your path formats or elsewhere.\n\n.. _musicbrainz documentation: https://musicbrainz.org/doc/Release_Group/Type\n\nA bug introduced in beets 1.6.0 could have possibly imported broken data into\nthe ``albumtypes`` library field. Please follow the instructions `described here\n<https://github.com/beetbox/beets/pull/4582#issuecomment-1445023493>`_ for a\nsanity check and potential fix. :bug:`4528`\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``albumtypes:`` section in your configuration\nfile. The available options are:\n\n- **types**: An ordered list of album type to format mappings. The order of the\n  mappings determines their order in the output. If a mapping is missing or\n  blank, it will not be in the output.\n- **ignore_va**: A list of types that should not be output for Various Artists\n  albums. Useful for not adding redundant information - various artist albums\n  are often compilations.\n- **bracket**: Defines the brackets to enclose each album type in the output.\n\nThe default configuration looks like this:\n\n::\n\n    albumtypes:\n        types:\n            - ep: 'EP'\n            - single: 'Single'\n            - soundtrack: 'OST'\n            - live: 'Live'\n            - compilation: 'Anthology'\n            - remix: 'Remix'\n        ignore_va: compilation\n        bracket: '[]'\n\nExamples\n--------\n\nWith path formats configured like:\n\n::\n\n    paths:\n        default: $albumartist/[$year]$atypes $album/...\n        albumtype:soundtrack: Various Artists/$album [$year]$atypes/...\n        comp: Various Artists/$album [$year]$atypes/...\n\nThe default plugin configuration generates paths that look like this, for\nexample:\n\n::\n\n    Aphex Twin/[1993][EP][Remix] On Remixes\n    Pink Floyd/[1995][Live] p·u·l·s·e\n    Various Artists/20th Century Lullabies [1999]\n    Various Artists/Ocean's Eleven [2001][OST]\n"
  },
  {
    "path": "docs/plugins/aura.rst",
    "content": "AURA Plugin\n===========\n\nThis plugin is a server implementation of the AURA_ specification using the\nFlask_ framework. AURA is still a work in progress and doesn't yet have a stable\nversion, but this server should be kept up to date. You are advised to read the\n:ref:`aura-issues` section.\n\n.. _aura: https://auraspec.readthedocs.io/en/latest/\n\n.. _flask: https://palletsprojects.com/projects/flask/\n\nInstall\n-------\n\nTo use the ``aura`` plugin, first enable it in your configuration (see\n:ref:`using-plugins`). Then, install ``beets`` with ``aura`` extra\n\n    pip install \"beets[aura]\"\n\nUsage\n-----\n\nUse ``beet aura`` to start the AURA server. By default Flask's built-in server\nis used, which will give a warning about using it in a production environment.\nIt is safe to ignore this warning if the server will have only a few users.\n\nAlternatively, you can use ``beet aura -d`` to start the server in `development\nmode <https://flask.palletsprojects.com/en/stable/server>`__, which will reload\nthe server every time the AURA plugin file is changed.\n\nYou can specify the hostname and port number used by the server in your\n:doc:`configuration file </reference/config>`. For more detail see the\n:ref:`configuration` section below.\n\nIf you would prefer to use a different WSGI server, such as gunicorn or uWSGI,\nthen see :ref:`aura-external-server`.\n\nAURA is designed to separate the client and server functionality. This plugin\nprovides the server but not the client, so unless you like looking at JSON you\nwill need a separate client. Currently the only client is `AURA Web Client`_. In\norder to use a local browser client with ``file:///`` see :ref:`aura-cors`.\n\nBy default the API is served under http://127.0.0.1:8337/aura/. For example\ninformation about the track with an id of 3 can be obtained at\nhttp://127.0.0.1:8337/aura/tracks/3.\n\n**Note the absence of a trailing slash**: http://127.0.0.1:8337/aura/tracks/3/\nreturns a ``404 Not Found`` error.\n\n.. _aura web client: https://sr.ht/~callum/aura-web-client/\n\n.. _configuration:\n\nConfiguration\n-------------\n\nTo configure the plugin, make an ``aura:`` section in your configuration file.\nThe available options are:\n\n- **host**: The server hostname. Set this to ``0.0.0.0`` to bind to all\n  interfaces. Default: ``127.0.0.1``.\n- **port**: The server port. Default: ``8337``.\n- **cors**: A YAML list of origins to allow CORS requests from (see\n  :ref:`aura-cors`, below). Default: disabled.\n- **cors_supports_credentials**: Allow authenticated requests when using CORS.\n  Default: disabled.\n- **page_limit**: The number of items responses should be truncated to if the\n  client does not specify. Default ``500``.\n\n.. _aura-cors:\n\nCross-Origin Resource Sharing (CORS)\n------------------------------------\n\n`CORS <https://en.wikipedia.org/wiki/Cross-origin_resource_sharing>`__ allows\nbrowser clients to make requests to the AURA server. You should set the ``cors``\nconfiguration option to a YAML list of allowed origins.\n\nFor example:\n\n::\n\n    aura:\n        cors:\n            - http://www.example.com\n            - https://aura.example.org\n\nIn order to use the plugin with a local browser client accessed using\n``file:///`` you must include ``'null'`` in the list of allowed origins\n(including quote marks):\n\n::\n\n    aura:\n        cors:\n            - 'null'\n\nAlternatively you use ``'*'`` to enable access from all origins. Note that there\nare security implications if you set the origin to ``'*'``, so please research\nthis before using it. Note the use of quote marks when allowing all origins.\n\nIf the server is behind a proxy that uses credentials, you might want to set the\n``cors_supports_credentials`` configuration option to true to let in-browser\nclients log in. Note that this option has not been tested, so it may not work.\n\n.. _aura-external-server:\n\nUsing an External WSGI Server\n-----------------------------\n\nIf you would like to use a different WSGI server (not Flask's built-in one),\nthen you can! The ``beetsplug.aura`` module provides a WSGI callable called\n``create_app()`` which can be used by many WSGI servers.\n\nFor example to run the AURA server using gunicorn_ use ``gunicorn\n'beetsplug.aura:create_app()'``, or for uWSGI_ use ``uwsgi --http :8337 --module\n'beetsplug.aura:create_app()'``. Note that these commands just show how to use\nthe AURA app and you would probably use something a bit different in a\nproduction environment. Read the relevant server's documentation to figure out\nwhat you need.\n\n.. _gunicorn: https://gunicorn.org\n\n.. _uwsgi: https://uwsgi-docs.readthedocs.io/en/latest/\n\nReverse Proxy Support\n---------------------\n\nThe plugin should work behind a reverse proxy without further configuration,\nhowever this has not been tested extensively. For details of what headers must\nbe rewritten and a sample NGINX configuration see `Flask proxy setups`_.\n\n.. _flask proxy setups: https://flask.palletsprojects.com/en/stable/deploying/proxy_fix/\n\nIt is (reportedly) possible to run the application under a URL prefix (for\nexample so you could have ``/foo/aura/server`` rather than ``/aura/server``),\nbut you'll have to work it out for yourself :-)\n\nIf using NGINX, do **not** add a trailing slash (``/``) to the URL where the\napplication is running, otherwise you will get a 404. However if you are using\nApache then you **should** add a trailing slash.\n\n.. _aura-issues:\n\nIssues\n------\n\nAs of writing there are some differences between the specification and this\nimplementation:\n\n- Compound filters are not specified in AURA, but this server interprets\n  multiple ``filter`` parameters as AND. See `issue #19`_ for discussion.\n- The ``bitrate`` parameter used for content negotiation is not supported.\n  Adding support for this is doable, but the way Flask handles acceptable MIME\n  types means it's a lot easier not to bother with it. This means an error could\n  be returned even if no transcoding was required.\n\nIt is possible that some attributes required by AURA could be absent from the\nserver's response if beets does not have a saved value for them. However, this\nhas not happened so far.\n\nBeets fields (including flexible fields) that do not have an AURA equivalent are\nnot provided in any resource's attributes section, however these fields may be\nused for filtering.\n\nThe ``mimetype`` and ``framecount`` attributes for track resources are not\nsupported. The first is due to beets storing the file type (e.g. ``MP3``), so it\nis hard to filter by MIME type. The second is because there is no corresponding\nbeets field.\n\nArtists are defined by the ``artist`` field on beets Items, which means some\nalbums have no ``artists`` relationship. Albums only have related artists when\ntheir beets ``albumartist`` field is the same as the ``artist`` field on at\nleast one of it's constituent tracks.\n\nThe only art tracked by beets is a single cover image, so only albums have\nrelated images at the moment. This could be expanded to looking in the same\ndirectory for other images, and relating tracks to their album's image.\n\nThere are likely to be some performance issues, especially with larger\nlibraries. Sorting, pagination and inclusion (most notably of images) are\nprobably the main offenders. On a related note, the program attempts to import\nPillow every time it constructs an image resource object, which is not good.\n\nThe beets library is accessed using a so called private function (with a single\nleading underscore) ``beets.ui.__init__._open_library()``. This shouldn't cause\nany issues but it is probably not best practice.\n\n.. _issue #19: https://github.com/beetbox/aura/issues/19\n"
  },
  {
    "path": "docs/plugins/autobpm.rst",
    "content": "AutoBPM Plugin\n==============\n\nThe ``autobpm`` plugin uses the Librosa_ library to calculate the BPM of a track\nfrom its audio data and store it in the ``bpm`` field of your database. It does\nso automatically when importing music or through the ``beet autobpm [QUERY]``\ncommand.\n\nInstall\n-------\n\nTo use the ``autobpm`` plugin, first enable it in your configuration (see\n:ref:`using-plugins`). Then, install ``beets`` with ``autobpm`` extra\n\n.. code-block:: bash\n\n    pip install \"beets[autobpm]\"\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``autobpm:`` section in your configuration file.\nThe available options are:\n\n- **auto**: Analyze every file on import. Otherwise, you need to use the ``beet\n  autobpm`` command explicitly. Default: ``yes``\n- **overwrite**: Calculate a BPM even for files that already have a ``bpm``\n  value. Default: ``no``.\n- **beat_track_kwargs**: Any extra keyword arguments that you would like to\n  provide to librosa's beat_track_ function call, for example:\n\n.. code-block:: yaml\n\n    autobpm:\n      beat_track_kwargs:\n        start_bpm: 160\n\n.. _beat_track: https://librosa.org/doc/latest/generated/librosa.beat.beat_track.html\n\n.. _librosa: https://github.com/librosa/librosa/\n"
  },
  {
    "path": "docs/plugins/badfiles.rst",
    "content": "Bad Files Plugin\n================\n\nThe ``badfiles`` plugin adds a ``beet bad`` command to check for missing and\ncorrupt files.\n\nConfiguring\n-----------\n\nFirst, enable the ``badfiles`` plugin (see :ref:`using-plugins`). The default\nconfiguration defines the following default checkers, which you may need to\ninstall yourself:\n\n- mp3val_ for MP3 files\n- FLAC_ command-line tools for FLAC files\n\nYou can also add custom commands for a specific extension, like this:\n\n::\n\n    badfiles:\n        check_on_import: yes\n        commands:\n            ogg: myoggchecker --opt1 --opt2\n            flac: flac --test --warnings-as-errors --silent\n\nCustom commands will be run once for each file of the specified type, with the\npath to the file as the last argument. Commands must return a status code\ngreater than zero for a file to be considered corrupt.\n\nYou can run the checkers when importing files by using the ``check_on_import``\noption. When on, checkers will be run against every imported file and warnings\nand errors will be presented when selecting a tagging option.\n\n.. _flac: https://xiph.org/flac/\n\n.. _mp3val: https://sourceforge.net/projects/mp3val/\n\nUsing\n-----\n\nType ``beet bad`` with a query according to beets' usual query syntax. For\ninstance, this will run a check on all songs containing the word \"wolf\":\n\n::\n\n    beet bad wolf\n\nThis one will run checks on a specific album:\n\n::\n\n    beet bad album_id:1234\n\nHere is an example where the FLAC decoder signals a corrupt file:\n\n::\n\n    beet bad title::^$\n    /tank/Music/__/00.flac: command exited with status 1\n      00.flac: *** Got error code 2:FLAC__STREAM_DECODER_ERROR_STATUS_FRAME_CRC_MISMATCH\n      00.flac: ERROR while decoding data\n                 state = FLAC__STREAM_DECODER_READ_FRAME\n\nNote that the default ``mp3val`` checker is a bit verbose and can output a lot\nof \"stream error\" messages, even for files that play perfectly well. Generally,\nif more than one stream error happens, or if a stream error happens in the\nmiddle of a file, this is a bad sign.\n\nBy default, only errors for the bad files will be shown. In order for the\nresults for all of the checked files to be seen, including the uncorrupted ones,\nuse the ``-v`` or ``--verbose`` option.\n"
  },
  {
    "path": "docs/plugins/bareasc.rst",
    "content": "Bare-ASCII Search Plugin\n========================\n\nThe ``bareasc`` plugin provides a prefixed query that searches your library\nusing simple ASCII character matching, with accented characters folded to their\nbase ASCII character. This can be useful if you want to find a track with\naccented characters in the title or artist, particularly if you are not\nconfident you have the accents correct. It is also not unknown for the accents\nto not be correct in the database entry or wrong in the CD information.\n\nFirst, enable the plugin named ``bareasc`` (see :ref:`using-plugins`). You'll\nthen be able to use the ``#`` prefix to use bare-ASCII matching:\n\n::\n\n    $ beet ls '#dvorak'\n    István Kertész - REQUIEM - Dvořàk: Requiem, op.89 - Confutatis maledictis\n\nCommand\n-------\n\nIn addition to the query prefix, the plugin provides a utility ``bareasc``\ncommand. This command is **exactly** the same as the ``beet list`` command\nexcept that the output is passed through the bare-ASCII transformation before\nbeing printed. This allows you to easily check what the library data looks like\nin bare ASCII, which can be useful if you are trying to work out why a query is\nnot matching.\n\nUsing the same example track as above:\n\n::\n\n    $ beet bareasc 'Dvořàk'\n    Istvan Kertesz - REQUIEM - Dvorak: Requiem, op.89 - Confutatis maledictis\n\nNote: the ``bareasc`` command does *not* automatically use bare-ASCII queries.\nIf you want a bare-ASCII query you still need to specify the ``#`` prefix.\n\nNotes\n-----\n\nIf the query string is all in lower case, the comparison ignores case as well as\naccents.\n\nThe default ``bareasc`` prefix (``#``) is used as a comment character in some\nshells so may need to be protected (for example in quotes) when typed into the\ncommand line.\n\nThe bare ASCII transliteration is quite simple. It may not give the expected\noutput for all languages. For example, German u-umlaut ``ü`` is transformed into\nASCII ``u``, not into ``ue``.\n\nThe bare ASCII transformation also changes Unicode punctuation like double\nquotes, apostrophes and even some hyphens. It is often best to leave out\npunctuation in the queries. Note that the punctuation changes are often not even\nvisible with normal terminal fonts. You can always use the ``bareasc`` command\nto print the transformed entries and use a command like ``diff`` to compare with\nthe output from the ``list`` command.\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``bareasc:`` section in your configuration file.\nThe only available option is:\n\n- **prefix**: The character used to designate bare-ASCII queries. Default:\n  ``#``, which may need to be escaped in some shells.\n\nCredits\n-------\n\nThe hard work in this plugin is done in Sean Burke's `Unidecode\n<https://pypi.org/project/Unidecode/>`__ library. Thanks are due to Sean and to\nall the people who created the Python version and the beets extensible query\narchitecture.\n"
  },
  {
    "path": "docs/plugins/beatport.rst",
    "content": "Beatport Plugin\n===============\n\n.. deprecated:: 2.8 Beatport retired the API this plugin relies on. See :bug:`3862` and :bug:`4477`.\n\nThe ``beatport`` plugin adds support for querying the Beatport_ catalogue during\nthe autotagging process. This can potentially be helpful for users whose\ncollection includes a lot of diverse electronic music releases, for which both\nMusicBrainz and (to a lesser degree) Discogs_ show no matches.\n\n.. _discogs: https://discogs.com\n\nInstallation\n------------\n\nTo use the ``beatport`` plugin, first enable it in your configuration (see\n:ref:`using-plugins`). Then, install ``beets`` with ``beatport`` extra\n\n.. code-block:: bash\n\n    pip install \"beets[beatport]\"\n\nYou will also need to register for a Beatport_ account. The first time you run\nthe :ref:`import-cmd` command after enabling the plugin, it will ask you to\nauthorize with Beatport by visiting the site in a browser. On the site you will\nbe asked to enter your username and password to authorize beets to query the\nBeatport API. You will then be displayed with a single line of text that you\nshould paste as a whole into your terminal. This will store the authentication\ndata for subsequent runs and you will not be required to repeat the above steps.\n\nMatches from Beatport should now show up alongside matches from MusicBrainz and\nother sources.\n\nIf you have a Beatport ID or a URL for a release or track you want to tag, you\ncan just enter one of the two at the \"enter Id\" prompt in the importer. You can\nalso search for an id like so:\n\n::\n\n    beet import path/to/music/library --search-id id\n\nConfiguration\n-------------\n\nThis plugin can be configured like other metadata source plugins as described in\n:ref:`metadata-source-plugin-configuration`.\n\n.. _beatport: https://www.beatport.com/\n"
  },
  {
    "path": "docs/plugins/bpd.rst",
    "content": "BPD Plugin\n==========\n\nBPD is a music player using music from a beets library. It runs as a daemon and\nimplements the MPD protocol, so it's compatible with all the great MPD clients\nout there. I'm using Theremin_, gmpc_, Sonata_, and Ario_ successfully.\n\n.. _ario: https://sourceforge.net/projects/ario-player/\n\n.. _gmpc: https://gmpc.fandom.com/wiki/Gnome_Music_Player_Client\n\n.. _sonata: https://www.nongnu.org/sonata/\n\n.. _theremin: https://github.com/TheStalwart/Theremin\n\nDependencies\n------------\n\nBefore you can use BPD, you'll need the media library called GStreamer_ (along\nwith its Python bindings) on your system.\n\n- On Mac OS X, you can use Homebrew_. Run ``brew install gstreamer\n  gst-plugins-base pygobject3``.\n- On Linux, you need to install GStreamer 1.0 and the GObject bindings for\n  python. Under Ubuntu, they are called ``python-gi`` and ``gstreamer1.0``.\n\nYou will also need the various GStreamer plugin packages to make everything\nwork. See the :doc:`/plugins/chroma` documentation for more information on\ninstalling GStreamer plugins.\n\nOnce you have system dependencies installed, install ``beets`` with ``bpd``\nextra which installs Python bindings for ``GStreamer``:\n\n.. code-block:: console\n\n    pip install \"beets[bpd]\"\n\n.. _gstreamer: https://gstreamer.freedesktop.org/\n\n.. _homebrew: https://brew.sh\n\nUsage\n-----\n\nTo use the ``bpd`` plugin, first enable it in your configuration (see\n:ref:`using-plugins`). Then, you can run BPD by invoking:\n\n::\n\n    $ beet bpd\n\nFire up your favorite MPD client to start playing music. The MPD site has `a\nlong list of available clients`_. Here are my favorites:\n\n.. _a long list of available clients: https://mpd.fandom.com/wiki/Clients\n\n- Linux: gmpc_, Sonata_\n- Mac: Theremin_\n- Windows: I don't know. Get in touch if you have a recommendation.\n- iPhone/iPod touch: Rigelian_\n\n.. _rigelian: https://www.rigelian.net/\n\nOne nice thing about MPD's (and thus BPD's) client-server architecture is that\nthe client can just as easily on a different computer from the server as it can\nbe run locally. Control your music from your laptop (or phone!) while it plays\non your headless server box. Rad!\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``bpd:`` section in your configuration file. The\navailable options are:\n\n- **host**: Default: Bind to all interfaces.\n- **port**: Default: 6600\n- **password**: Default: No password.\n- **volume**: Initial volume, as a percentage. Default: 100\n- **control_port**: Port for the internal control socket. Default: 6601\n\nHere's an example:\n\n::\n\n    bpd:\n        host: 127.0.0.1\n        port: 6600\n        password: seekrit\n        volume: 100\n\nImplementation Notes\n--------------------\n\nIn the real MPD, the user can browse a music directory as it appears on disk. In\nbeets, we like to abstract away from the directory structure. Therefore, BPD\ncreates a \"virtual\" directory structure (artist/album/track) to present to\nclients. This is static for now and cannot be reconfigured like the real on-disk\ndirectory structure can. (Note that an obvious solution to this is just string\nmatching on items' destination, but this requires examining the entire library\nPython-side for every query.)\n\nBPD plays music using GStreamer's ``playbin`` player, which has a simple API but\ndoesn't support many advanced playback features.\n\nDifferences from the real MPD\n-----------------------------\n\nBPD currently supports version 0.16 of `the MPD protocol`_, but several of the\ncommands and features are \"pretend\" implementations or have slightly different\nbehaviour to their MPD equivalents. BPD aims to look enough like MPD that it can\ninteract with the ecosystem of clients, but doesn't try to be a fully-fledged\nMPD replacement in terms of its playback capabilities.\n\n.. _the mpd protocol: https://mpd.readthedocs.io/en/latest/protocol.html\n\nThese are some of the known differences between BPD and MPD:\n\n- BPD doesn't currently support versioned playlists. Many clients, however, use\n  plchanges instead of playlistinfo to get the current playlist, so plchanges\n  contains a dummy implementation that just calls playlistinfo.\n- Stored playlists aren't supported (BPD understands the commands though).\n- The ``stats`` command always send zero for ``playtime``, which is supposed to\n  indicate the amount of time the server has spent playing music. BPD doesn't\n  currently keep track of this.\n- The ``update`` command regenerates the directory tree from the beets database\n  synchronously, whereas MPD does this in the background.\n- Advanced playback features like cross-fade, ReplayGain and MixRamp are not\n  supported due to BPD's simple audio player backend.\n- Advanced query syntax is not currently supported.\n- Clients can't use the ``tagtypes`` mask to hide fields.\n- BPD's ``random`` mode is not deterministic and doesn't support priorities.\n- Mounts and streams are not supported. BPD can only play files from disk.\n- Stickers are not supported (although this is basically a flexattr in beets\n  nomenclature so this is feasible to add).\n- There is only a single password, and is enabled it grants access to all\n  features rather than having permissions-based granularity.\n- Partitions and alternative outputs are not supported; BPD can only play one\n  song at a time.\n- Client channels are not implemented.\n"
  },
  {
    "path": "docs/plugins/bpm.rst",
    "content": "BPM Plugin\n==========\n\nThis ``bpm`` plugin lets you to get the tempo (beats per minute) of a song by\ntapping out the beat on your keyboard.\n\nUsage\n-----\n\nTo use the ``bpm`` plugin, first enable it in your configuration (see\n:ref:`using-plugins`).\n\nThen, play a song you want to measure in your favorite media player and type:\n\n::\n\n    beet bpm <song>\n\nYou'll be prompted to press Enter three times to the rhythm. This typically\nallows to determine the BPM within 5% accuracy.\n\nThe plugin works best if you wrap it in a script that gets the playing song. for\ninstance, with ``mpc`` you can do something like:\n\n::\n\n    beet bpm $(mpc |head -1|tr -d \"-\")\n\nIf :ref:`import.write <config-import-write>` is ``yes``, the song's tags are\nwritten to disk.\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``bpm:`` section in your configuration file. The\navailable options are:\n\n- **max_strokes**: The maximum number of strokes to accept when tapping out the\n  BPM. Default: 3.\n- **overwrite**: Overwrite the track's existing BPM. Default: ``yes``.\n\nCredit\n------\n\nThis plugin is inspired by a similar feature present in the Banshee media\nplayer.\n"
  },
  {
    "path": "docs/plugins/bpsync.rst",
    "content": "BPSync Plugin\n=============\n\n.. deprecated:: 2.8 Depends on the deprecated :doc:`beatport` plugin. See :bug:`3862` and :bug:`4477`.\n\nThis plugin provides the ``bpsync`` command, which lets you fetch metadata from\nBeatport for albums and tracks that already have Beatport IDs. This plugin works\nsimilarly to :doc:`/plugins/mbsync`.\n\nIf you have downloaded music from Beatport, this can speed up the initial import\nif you just import \"as-is\" and then use ``bpsync`` to get up-to-date tags that\nare written to the files according to your beets configuration.\n\nUsage\n-----\n\nEnable the ``bpsync`` plugin in your configuration (see :ref:`using-plugins`)\nand then run ``beet bpsync QUERY`` to fetch updated metadata for a part of your\ncollection (or omit the query to run over your whole library).\n\nThis plugin treats albums and singletons (non-album tracks) separately. It first\nprocesses all matching singletons and then proceeds on to full albums. The same\nquery is used to search for both kinds of entities.\n\nThe command has a few command-line options:\n\n- To preview the changes that would be made without applying them, use the\n  ``-p`` (``--pretend``) flag.\n- By default, files will be moved (renamed) according to their metadata if they\n  are inside your beets library directory. To disable this, use the ``-M``\n  (``--nomove``) command-line option.\n- If you have the ``import.write`` configuration option enabled, then this\n  plugin will write new metadata to files' tags. To disable this, use the ``-W``\n  (``--nowrite``) option.\n"
  },
  {
    "path": "docs/plugins/bucket.rst",
    "content": "Bucket Plugin\n=============\n\nThe ``bucket`` plugin groups your files into buckets folders representing\n*ranges*. This kind of organization can classify your music by periods of time\n(e.g,. *1960s*, *1970s*, etc.), or divide overwhelmingly large folders into\nsmaller subfolders by grouping albums or artists alphabetically (e.g. *A-F*,\n*G-M*, *N-Z*).\n\nTo use the ``bucket`` plugin, first enable it in your configuration (see\n:ref:`using-plugins`). The plugin provides a :ref:`template function\n<template-functions>` called ``%bucket`` for use in path format expressions:\n\n::\n\n    paths:\n        default: /%bucket{$year}/%bucket{$artist}/$albumartist-$album-$year\n\nThen, define your ranges in the ``bucket:`` section of the config file:\n\n::\n\n    bucket:\n        bucket_alpha: ['A-F', 'G-M', 'N-Z']\n        bucket_year:  ['1980s', '1990s', '2000s']\n\nThe ``bucket_year`` parameter is used for all substitutions occurring on the\n``$year`` field, while ``bucket_alpha`` takes care of textual fields.\n\nThe definition of a range is somewhat loose, and multiple formats are allowed:\n\n- For alpha ranges: the range is defined by the lowest and highest (ASCII-wise)\n  alphanumeric characters in the string you provide. For example, ``ABCD``,\n  ``A-D``, ``A->D``, and ``[AD]`` are all equivalent.\n- For year ranges: digits characters are extracted and the two extreme years\n  define the range. For example, ``1975-77``, ``1975,76,77`` and ``1975-1977``\n  are equivalent. If no upper bound is given, the range is extended to current\n  year (unless a later range is defined). For example, ``1975`` encompasses all\n  years from 1975 until now.\n\nThe ``%bucket`` template function guesses whether to use alpha- or year-style\nbuckets depending on the text it receives. It can guess wrong if, for example,\nan artist or album happens to begin with four digits. Provide ``alpha`` as the\nsecond argument to the template to avoid this automatic detection: for example,\nuse ``%bucket{$artist,alpha}``.\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``bucket:`` section in your configuration file.\nThe available options are:\n\n- **bucket_alpha**: Ranges to use for all substitutions occurring on textual\n  fields. Default: none.\n- **bucket_alpha_regex**: A ``range: regex`` mapping (one per line) where\n  ``range`` is one of the ``bucket_alpha`` ranges and ``value`` is a regex that\n  overrides original range definition. Default: none.\n- **bucket_year**: Ranges to use for all substitutions occurring on the\n  ``$year`` field. Default: none.\n- **extrapolate**: Enable this if you want to group your files into multiple\n  year ranges without enumerating them all. This option will generate year\n  bucket names by reproducing characteristics of declared buckets. Default:\n  ``no``\n\nHere's an example:\n\n::\n\n    bucket:\n       bucket_year: ['2000-05']\n       extrapolate: true\n       bucket_alpha: ['A - D', 'E - L', 'M - R', 'S - Z']\n       bucket_alpha_regex:\n         'A - D': ^[0-9a-dA-D…äÄ]\n\nThis configuration creates five-year ranges for any input year. The ``A - D``\nbucket now matches also all artists starting with ä or Ä and 0 to 9 and …\n(ellipsis). The other alpha buckets work as ranges.\n"
  },
  {
    "path": "docs/plugins/chroma.rst",
    "content": "Chromaprint/Acoustid Plugin\n===========================\n\nAcoustic fingerprinting is a technique for identifying songs from the way they\n\"sound\" rather from their existing metadata. That means that beets' autotagger\ncan theoretically use fingerprinting to tag files that don't have any ID3\ninformation at all (or have completely incorrect data). This plugin uses an\nopen-source fingerprinting technology called Chromaprint_ and its associated Web\nservice, called Acoustid_.\n\n.. _acoustid: https://acoustid.org/\n\n.. _chromaprint: https://acoustid.org/chromaprint\n\nTurning on fingerprinting can increase the accuracy of the\nautotagger---especially on files with very poor metadata---but it comes at a\ncost. First, it can be trickier to set up than beets itself (you need to set up\nthe native fingerprinting library, whereas all of the beets core is written in\npure Python). Also, fingerprinting takes significantly more CPU and memory than\nordinary tagging---which means that imports will go substantially slower.\n\nIf you're willing to pay the performance cost for fingerprinting, read on!\n\nInstalling Dependencies\n-----------------------\n\nTo get fingerprinting working, you'll need to install three things:\n\n1. pyacoustid_ Python library (version 0.6 or later). You can install it by\n   installing ``beets`` with ``chroma`` extra\n\n.. code-block:: bash\n\n    pip install \"beets[chroma]\"\n\n2. the Chromaprint_ library_ or |command-line-tool|_\n3. an |audio-decoder|_\n\n.. |command-line-tool| replace:: command line tool\n\n.. |audio-decoder| replace:: audio decoder\n\n.. _command-line-tool:\n\nInstalling the Binary Command-Line Tool\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThe simplest way to get up and running, especially on Windows, is to download_\nthe appropriate Chromaprint binary package and place the ``fpcalc`` (or\n``fpcalc.exe``) on your shell search path. On Windows, this means something like\n``C:\\\\Program Files``. On OS X or Linux, put the executable somewhere like\n``/usr/local/bin``.\n\n.. _download: https://acoustid.org/chromaprint\n\n.. _library:\n\nInstalling the Library\n~~~~~~~~~~~~~~~~~~~~~~\n\nOn OS X and Linux, you can also use a library installed by your package manager,\nwhich has some advantages (automatic upgrades, etc.). The Chromaprint site has\nlinks to packages for major Linux distributions. If you use Homebrew_ on Mac OS\nX, you can install the library with ``brew install chromaprint``.\n\n.. _audio-decoder:\n\n.. _homebrew: https://brew.sh/\n\nAudio Decoder\n~~~~~~~~~~~~~\n\nYou will also need a mechanism for decoding audio files supported by the\naudioread_ library:\n\n- OS X has a number of decoders already built into Core Audio, so there's no\n  need to install anything.\n- On Linux, you can install GStreamer_ with PyGObject_, FFmpeg_, or MAD_ with\n  pymad_. How you install these will depend on your distribution. For example,\n  on Ubuntu, run ``apt-get install gstreamer1.0 python-gi``. On Arch Linux, you\n  want ``pacman -S gstreamer python2-gobject``. If you use GStreamer, be sure to\n  install its codec plugins also (``gst-plugins-good``, etc.).\n\n  Note that if you install beets in a virtualenv, you'll need it to have\n  ``--system-site-packages`` enabled for Python to see the GStreamer bindings.\n\n- On Windows, builds are provided by GStreamer_\n\n.. _audioread: https://github.com/beetbox/audioread\n\n.. _core audio: https://developer.apple.com/technologies/mac/audio-and-video.html\n\n.. _ffmpeg: https://ffmpeg.org/\n\n.. _gstreamer: https://gstreamer.freedesktop.org/\n\n.. _mad: https://www.underbit.com/products/mad/\n\n.. _pyacoustid: https://github.com/beetbox/pyacoustid\n\n.. _pygobject: https://wiki.gnome.org/Projects/PyGObject\n\n.. _pymad: https://spacepants.org/src/pymad/\n\nTo decode audio formats (MP3, FLAC, etc.) with GStreamer, you'll need the\nstandard set of Gstreamer plugins. For example, on Ubuntu, install the packages\n``gstreamer1.0-plugins-good``, ``gstreamer1.0-plugins-bad``, and\n``gstreamer1.0-plugins-ugly``.\n\nUsage\n-----\n\nOnce you have all the dependencies sorted out, enable the ``chroma`` plugin in\nyour configuration (see :ref:`using-plugins`) to benefit from fingerprinting the\nnext time you run ``beet import``. (The plugin doesn't produce any obvious\noutput by default. If you want to confirm that it's enabled, you can try running\nin verbose mode once with ``beet -v import``.)\n\nYou can also use the ``beet fingerprint`` command to generate fingerprints for\nitems already in your library. (Provide a query to fingerprint a subset of your\nlibrary.) The generated fingerprints will be stored in the library database. If\nyou have the ``import.write`` config option enabled, they will also be written\nto files' metadata.\n\n.. _submitfp:\n\nConfiguration\n-------------\n\nThere is one configuration option in the ``chroma:`` section, ``auto``, which\ncontrols whether to fingerprint files during the import process. To disable\nfingerprint-based autotagging, set it to ``no``, like so:\n\n::\n\n    chroma:\n        auto: no\n\nSubmitting Fingerprints\n-----------------------\n\nYou can help expand the Acoustid_ database by submitting fingerprints for the\nmusic in your collection. To do this, first `get an API key`_ from the Acoustid\nservice. Just use an OpenID or MusicBrainz account to log in and you'll get a\nshort token string. Then, add the key to your ``config.yaml`` as the value\n``apikey`` in a section called ``acoustid`` like so:\n\n::\n\n    acoustid:\n        apikey: AbCd1234\n\nThen, run ``beet submit``. (You can also provide a query to submit a subset of\nyour library.) The command will use stored fingerprints if they're available;\notherwise it will fingerprint each file before submitting it.\n\n.. _get an api key: https://acoustid.org/api-key\n"
  },
  {
    "path": "docs/plugins/convert.rst",
    "content": "Convert Plugin\n==============\n\nThe ``convert`` plugin lets you convert parts of your collection to a directory\nof your choice, transcoding audio and embedding album art along the way. It can\ntranscode to and from any format using a configurable command line. Optionally\nan m3u playlist file containing all the converted files can be saved to the\ndestination path.\n\nInstallation\n------------\n\nTo use the ``convert`` plugin, first enable it in your configuration (see\n:ref:`using-plugins`). By default, the plugin depends on FFmpeg_ to transcode\nthe audio, so you might want to install it.\n\n.. _ffmpeg: https://ffmpeg.org\n\nUsage\n-----\n\nTo convert a part of your collection, run ``beet convert QUERY``. The command\nwill transcode all the files matching the query to the destination directory\ngiven by the ``-d`` (``--dest``) option or the ``dest`` configuration. The path\nlayout mirrors that of your library, but it may be customized through the\n``paths`` configuration. Files that have been previously converted---and thus\nalready exist in the destination directory---will be skipped.\n\nThe plugin uses a command-line program to transcode the audio. With the ``-f``\n(``--format``) option you can choose the transcoding command and customize the\navailable commands :ref:`through the configuration <convert-format-config>`.\n\nUnless the ``-y`` (``--yes``) flag is set, the command will list all the items\nto be converted and ask for your confirmation.\n\nThe ``-a`` (or ``--album``) option causes the command to match albums instead of\ntracks.\n\nBy default, the command places converted files into the destination directory\nand leaves your library pristine. To instead back up your original files into\nthe destination directory and keep converted files in your library, use the\n``-k`` (or ``--keep-new``) option.\n\nTo test your configuration without taking any actions, use the ``--pretend``\nflag. The plugin will print out the commands it will run instead of executing\nthem.\n\nBy default, files that do not need to be transcoded will be copied to their\ndestination. Passing the ``-l`` (``--link``) flag creates symbolic links\ninstead, passing ``-H`` (``--hardlink``) creates hard links. Note that album art\nembedding is disabled for files that are linked. Refer to the ``link`` and\n``hardlink`` options below.\n\nThe ``-F`` (or ``--force``) option forces transcoding even when safety options\nsuch as ``no_convert``, ``never_convert_lossy_files``, or ``max_bitrate`` would\nnormally cause a file to be copied or skipped instead. This can be combined with\n``--format`` to explicitly transcode lossy inputs to a chosen target format.\n\nThe ``-m`` (or ``--playlist``) option enables the plugin to create an m3u8\nplaylist file in the destination folder given by the ``-d`` (``--dest``) option\nor the ``dest`` configuration. The path to the playlist file can either be\nabsolute or relative to the ``dest`` directory. The contents will always be\nrelative paths to media files, which tries to ensure compatibility when read\nfrom external drives or on computers other than the one used for the conversion.\nThere is one caveat though: A list generated on Unix/macOS can't be read on\nWindows and vice versa.\n\nDepending on the beets user's settings a generated playlist potentially could\ncontain unicode characters. This is supported, playlists are written in `M3U8\nformat`_.\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``convert:`` section in your configuration file.\nThe available options are:\n\n- **auto**: Import transcoded versions of your files automatically during\n  imports. With this option enabled, the importer will transcode all (in the\n  default configuration) non-MP3 files over the maximum bitrate before adding\n  them to your library. Default: ``no``.\n- **auto_keep**: Convert your files automatically on import to **dest** but\n  import the non transcoded version. It uses the default format you have defined\n  in your config file. Default: ``no``.\n\n  .. note::\n\n      You probably want to use only one of the ``auto`` and ``auto_keep``\n      options, not both. Enabling both will convert your files twice on import,\n      which you probably don't want.\n\n- **tmpdir**: The directory where temporary files will be stored during import.\n  Default: none (system default),\n- **copy_album_art**: Copy album art when copying or transcoding albums matched\n  using the ``-a`` option. Default: ``no``.\n- **album_art_maxwidth**: Downscale album art if it's too big. The resize\n  operation reduces image width to at most ``maxwidth`` pixels while preserving\n  the aspect ratio. The specified image size will apply to both embedded album\n  art and external image files.\n- **dest**: The directory where the files will be converted (or copied) to.\n  Default: none.\n- **embed**: Embed album art in converted items. Default: ``yes``.\n- **id3v23**: Can be used to override the global ``id3v23`` option. Default:\n  ``inherit``.\n- **write_metadata**: Can be used to disable writing metadata to converted\n  files. Default: ``true``.\n- **max_bitrate**: By default, the plugin does not transcode files that are\n  already in the destination format. This option instead also transcodes files\n  with high bitrates, even if they are already in the same format as the output.\n  Note that this does not guarantee that all converted files will have a lower\n  bitrate---that depends on the encoder and its configuration. Default: none.\n  This option will be overridden by the ``--force`` flag\n- **no_convert**: Does not transcode items matching the query string provided\n  (see :doc:`/reference/query`). For example, to not convert AAC or WMA formats,\n  you can use ``format:AAC, format:WMA`` or ``path::\\.(m4a|wma)$``. If you only\n  want to transcode WMA format, you can use a negative query, e.g.,\n  ``^path::\\.(wma)$``, to not convert any other format except WMA. This option\n  will be overridden by the ``--force`` flag\n- **never_convert_lossy_files**: Cross-conversions between lossy codecs---such\n  as mp3, ogg vorbis, etc.---makes little sense as they will decrease quality\n  even further. If set to ``yes``, lossy files are always copied. Default:\n  ``no``. When ``never_convert_lossy_files`` is enabled, lossy source files (for\n  example MP3 or Ogg Vorbis) are normally not transcoded and are instead copied\n  or linked as-is. To explicitly transcode lossy files in spite of this, use the\n  ``--force`` option with the ``convert`` command (optionally together with\n  ``--format`` to choose a target format)\n- **paths**: The directory structure and naming scheme for the converted files.\n  Uses the same format as the top-level ``paths`` section (see\n  :ref:`path-format-config`). Default: Reuse your top-level path format\n  settings.\n- **quiet**: Prevent the plugin from announcing every file it processes.\n  Default: ``false``.\n- **threads**: The number of threads to use for parallel encoding. By default,\n  the plugin will detect the number of processors available and use them all.\n- **link**: By default, files that do not need to be transcoded will be copied\n  to their destination. This option creates symbolic links instead. Note that\n  options such as ``embed`` that modify the output files after the transcoding\n  step will cause the original files to be modified as well if ``link`` is\n  enabled. For this reason, album-art embedding is disabled for files that are\n  linked. Default: ``false``.\n- **hardlink**: This options works similar to ``link``, but it creates hard\n  links instead of symlinks. This option overrides ``link``. Only works when\n  converting to a directory on the same filesystem as the library. Default:\n  ``false``.\n- **delete_originals**: Transcoded files will be copied or moved to their\n  destination, depending on the import configuration. By default, the original\n  files are not modified by the plugin. This option deletes the original files\n  after the transcoding step has completed. Default: ``false``.\n- **playlist**: The name of a playlist file that should be written on each run\n  of the plugin. A relative file path (e.g ``playlists/mylist.m3u8``) is allowed\n  as well. The final destination of the playlist file will always be relative to\n  the destination path (``dest``, ``--dest``, ``-d``). This configuration is\n  overridden by the ``-m`` (``--playlist``) command line option. Default: none.\n\nYou can also configure the format to use for transcoding (see the next section):\n\n- **format**: The name of the format to transcode to when none is specified on\n  the command line. Default: ``mp3``.\n- **formats**: A set of formats and associated command lines for transcoding\n  each.\n\n.. _convert-format-config:\n\nConfiguring the transcoding command\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nYou can customize the transcoding command through the ``formats`` map and select\na command with the ``--format`` command-line option or the ``format``\nconfiguration.\n\n::\n\n    convert:\n        format: speex\n        formats:\n            speex:\n                command: ffmpeg -i $source -y -acodec speex $dest\n                extension: spx\n            wav: ffmpeg -i $source -y -acodec pcm_s16le $dest\n\nIn this example ``beet convert`` will use the *speex* command by default. To\nconvert the audio to ``wav``, run ``beet convert -f wav``. This will also use\nthe format key (``wav``) as the file extension.\n\nEach entry in the ``formats`` map consists of a key (the name of the format) as\nwell as the command and optionally the file extension. ``extension`` is the\nfilename extension to be used for newly transcoded files. If only the command is\ngiven as a string or the extension is not provided, the file extension defaults\nto the format's name. ``command`` is the command to use to transcode audio. The\ntokens ``$source`` and ``$dest`` in the command are replaced with the paths to\nthe existing and new file.\n\nThe plugin in comes with default commands for the most common audio formats:\n``mp3``, ``alac``, ``flac``, ``aac``, ``opus``, ``ogg``, ``wma``. For details\nhave a look at the output of ``beet config -d``.\n\nFor a one-command-fits-all solution use the ``convert.command`` and\n``convert.extension`` options. If these are set, the formats are ignored and the\ngiven command is used for all conversions.\n\n::\n\n    convert:\n        command: ffmpeg -i $source -y -vn -aq 2 $dest\n        extension: mp3\n\nGapless MP3 encoding\n~~~~~~~~~~~~~~~~~~~~\n\nWhile FFmpeg cannot produce \"gapless_\" MP3s by itself, you can create them by\nusing LAME_ directly. Use a shell script like this to pipe the output of FFmpeg\ninto the LAME tool:\n\n::\n\n    #!/bin/sh\n    ffmpeg -i \"$1\" -f wav - | lame -V 2 --noreplaygain - \"$2\"\n\nThen configure the ``convert`` plugin to use the script:\n\n::\n\n    convert:\n        command: /path/to/script.sh $source $dest\n        extension: mp3\n\nThis strategy configures FFmpeg to produce a WAV file with an accurate length\nheader for LAME to use. Using ``--noreplaygain`` disables gain analysis; you can\nuse the :doc:`/plugins/replaygain` to do this analysis. See the LAME\ndocumentation_ and the `HydrogenAudio wiki`_ for other LAME configuration\noptions and a thorough discussion of MP3 encoding.\n\n.. _documentation: https://sourceforge.net/projects/lame/\n\n.. _gapless: https://wiki.hydrogenaudio.org/index.php?title=Gapless_playback\n\n.. _hydrogenaudio wiki: https://wiki.hydrogenaudio.org/index.php?title=LAME\n\n.. _lame: https://sourceforge.net/projects/lame/\n\n.. _m3u8 format: https://en.wikipedia.org/wiki/M3U#M3U8\n"
  },
  {
    "path": "docs/plugins/deezer.rst",
    "content": "Deezer Plugin\n=============\n\nThe ``deezer`` plugin provides metadata matches for the importer using the\nDeezer_ Album_ and Track_ APIs.\n\n.. _album: https://developers.deezer.com/api/album\n\n.. _deezer: https://www.deezer.com/en/\n\n.. _track: https://developers.deezer.com/api/track\n\nBasic Usage\n-----------\n\nFirst, enable the ``deezer`` plugin (see :ref:`using-plugins`).\n\nYou can enter the URL for an album or song on Deezer at the ``enter Id`` prompt\nduring import:\n\n::\n\n    Enter search, enter Id, aBort, eDit, edit Candidates, plaY? i\n    Enter release ID: https://www.deezer.com/en/album/572261\n\nConfiguration\n-------------\n\nThis plugin can be configured like other metadata source plugins as described in\n:ref:`metadata-source-plugin-configuration`.\n\nDefault\n~~~~~~~\n\n.. code-block:: yaml\n\n    deezer:\n        search_query_ascii: no\n        data_source_mismatch_penalty: 0.5\n        search_limit: 5\n\n.. conf:: search_query_ascii\n    :default: no\n\n    If enabled, the search query will be converted to ASCII before being sent to\n    Deezer. Converting searches to ASCII can enhance search results in some cases,\n    but in general, it is not recommended. For instance, ``artist:deadmau5\n    album:4×4`` will be converted to ``artist:deadmau5 album:4x4`` (notice\n    ``×!=x``).\n\n.. include:: ./shared_metadata_source_config.rst\n\nCommands\n--------\n\nThe ``deezer`` plugin provides an additional command ``deezerupdate`` to update\nthe ``rank`` information from Deezer. The ``rank`` (ranges from 0 to 1M) is a\nglobal indicator of a song's popularity on Deezer that is updated daily based on\nstreams. The higher the ``rank``, the more popular the track is.\n"
  },
  {
    "path": "docs/plugins/discogs.rst",
    "content": "Discogs Plugin\n==============\n\nThe ``discogs`` plugin extends the autotagger's search capabilities to include\nmatches from the Discogs_ database.\n\nFiles can be imported as albums or as singletons. Since Discogs_ matches are\nalways based on Discogs_ releases, the album tag is written even to singletons.\nThis enhances the importers results when reimporting as (full or partial) albums\nlater on.\n\n.. _discogs: https://discogs.com\n\nInstallation\n------------\n\nTo use the ``discogs`` plugin, first enable it in your configuration (see\n:ref:`using-plugins`). Then, install ``beets`` with ``discogs`` extra\n\n.. code-block:: bash\n\n    pip install \"beets[discogs]\"\n\nYou will also need to register for a Discogs_ account, and provide\nauthentication credentials via a personal access token or an OAuth2\nauthorization.\n\nMatches from Discogs will now show up during import alongside matches from\nMusicBrainz. The search terms sent to the Discogs API are based on the artist\nand album tags of your tracks. If those are empty no query will be issued.\n\nIf you have a Discogs ID for an album you want to tag, you can also enter it at\nthe \"enter Id\" prompt in the importer.\n\nOAuth Authorization\n~~~~~~~~~~~~~~~~~~~\n\nThe first time you run the :ref:`import-cmd` command after enabling the plugin,\nit will ask you to authorize with Discogs by visiting the site in a browser.\nSubsequent runs will not require re-authorization.\n\nAuthentication via Personal Access Token\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nAs an alternative to OAuth, you can get a token from Discogs and add it to your\nconfiguration. To get a personal access token (called a \"user token\" in the\npython3-discogs-client_ documentation):\n\n1. login to Discogs_;\n2. visit the `Developer settings page\n   <https://www.discogs.com/settings/developers>`_;\n3. press the *Generate new token* button;\n4. copy the generated token;\n5. place it in your configuration in the ``discogs`` section as the\n   ``user_token`` option:\n\n   .. code-block:: yaml\n\n       discogs:\n           user_token: \"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n\nConfiguration\n-------------\n\nThis plugin can be configured like other metadata source plugins as described in\n:ref:`metadata-source-plugin-configuration`.\n\nDefault\n~~~~~~~\n\n.. code-block:: yaml\n\n    discogs:\n        apikey: REDACTED\n        apisecret: REDACTED\n        tokenfile: discogs_token.json\n        user_token:\n        index_tracks: no\n        append_style_genre: no\n        separator: ', '\n        strip_disambiguation: yes\n        featured_string: Feat.\n        extra_tags: []\n        anv:\n            artist_credit: yes\n            artist: no\n            album_artist: no\n        data_source_mismatch_penalty: 0.5\n        search_limit: 5\n\n.. conf:: index_tracks\n    :default: no\n\n    Index tracks (see the `Discogs guidelines`_) along with headers, mark divisions\n    between distinct works on the same release or within works. When enabled,\n    beets will incorporate the names of the divisions containing each track into the\n    imported track's title.\n\n    For example, importing `divisions album`_ would result in track names like:\n\n    .. code-block:: text\n\n     Messiah, Part I: No.1: Sinfony\n     Messiah, Part II: No.22: Chorus- Behold The Lamb Of God\n     Athalia, Act I, Scene I: Sinfonia\n\n    whereas with ``index_tracks`` disabled you'd get:\n\n    .. code-block:: text\n\n     No.1: Sinfony\n     No.22: Chorus- Behold The Lamb Of God\n     Sinfonia\n\n    This option is useful when importing classical music.\n\n.. conf:: append_style_genre\n    :default: no\n\n    Appends the Discogs style (if found) to the ``genres`` tag. This can be\n    useful if you want more granular genres to categorize your music. For\n    example, a release in Discogs might have a genre of \"Electronic\" and a style\n    of \"Techno\": enabling this setting would append \"Techno\" to the ``genres``\n    list.\n\n.. conf:: separator\n    :default: \", \"\n\n    How to join multiple style values from Discogs into a string.\n\n    .. versionchanged:: 2.7.0\n\n       This option now only applies to the ``style`` field as beets now only\n       handles lists of ``genres``.\n\n.. conf:: strip_disambiguation\n    :default: yes\n\n    Discogs uses strings like ``\"(4)\"`` to mark distinct artists and labels with\n    the same name. If you'd like to use the Discogs disambiguation in your tags,\n    you can disable this option.\n\n.. conf:: featured_string\n    :default: Feat.\n\n    Configure the string used for noting featured artists. Useful if you prefer ``Featuring`` or ``ft.``.\n\n.. conf:: extra_tags\n    :default: []\n\n    By default, beets will use only the artist and album to query Discogs.\n    Additional tags to be queried can be supplied with the\n    ``extra_tags`` setting.\n\n    This setting should improve the autotagger results if the metadata with the\n    given tags match the metadata returned by Discogs.\n\n    Tags supported by this setting:\n\n    * ``barcode``\n    * ``catalognum``\n    * ``country``\n    * ``label``\n    * ``media``\n    * ``year``\n\n    Example:\n\n    .. code-block:: yaml\n\n        discogs:\n            extra_tags: [barcode, catalognum, country, label, media, year]\n\n.. conf:: anv\n\n    This configuration option is dedicated to handling Artist Name\n    Variations (ANVs). Sometimes a release credits artists differently compared to\n    the majority of their work. For example, \"Basement Jaxx\" may be credited as\n    \"Tha Jaxx\" or \"The Basement Jaxx\". You can select any combination of these\n    config options to control where beets writes and stores the variation credit.\n    The default, shown below, writes variations to the artist_credit field.\n\n    .. code-block:: yaml\n\n        discogs:\n            anv:\n               artist_credit: yes\n               artist: no\n               album_artist: no\n\n.. include:: ./shared_metadata_source_config.rst\n\n.. _discogs guidelines: https://support.discogs.com/hc/en-us/articles/360005055373-Database-Guidelines-12-Tracklisting#Index_Tracks_And_Headings\n\n.. _divisions album: https://www.discogs.com/release/2026070-Handel-Sutherland-Kirkby-Kwella-Nelson-Watkinson-Bowman-Rolfe-Johnson-Elliott-Partridge-Thomas-The-A\n\nTroubleshooting\n---------------\n\nSeveral issues have been encountered with the Discogs API. If you have one,\nplease start by searching for `a similar issue on the repo\n<https://github.com/beetbox/beets/issues?utf8=%E2%9C%93&q=is%3Aissue+discogs>`_.\n\nHere are two things you can try:\n\n- Try deleting the token file (``~/.config/beets/discogs_token.json`` by\n  default) to force re-authorization.\n- Make sure that your system clock is accurate. The Discogs servers can reject\n  your request if your clock is too out of sync.\n\nMatching tracks by Discogs ID is not yet supported. The ``--group-albums``\noption in album import mode provides an alternative to singleton mode for\nautotagging tracks that are not in album-related folders.\n\n.. _python3-discogs-client: https://github.com/joalla/discogs_client\n"
  },
  {
    "path": "docs/plugins/duplicates.rst",
    "content": "Duplicates Plugin\n=================\n\nThis plugin adds a new command, ``duplicates`` or ``dup``, which finds and lists\nduplicate tracks or albums in your collection.\n\nUsage\n-----\n\nTo use the ``duplicates`` plugin, first enable it in your configuration (see\n:ref:`using-plugins`).\n\nBy default, the ``beet duplicates`` command lists the names of tracks in your\nlibrary that are duplicates. It assumes that Musicbrainz track and album ids are\nunique to each track or album. That is, it lists every track or album with an ID\nthat has been seen before in the library. You can customize the output format,\ncount the number of duplicate tracks or albums, and list all tracks that have\nduplicates or just the duplicates themselves via command-line switches\n\n::\n\n    -h, --help            show this help message and exit\n    -f FMT, --format=FMT  print with custom format\n    -a, --album           show duplicate albums instead of tracks\n    -c, --count           count duplicate tracks or albums\n    -C PROG, --checksum=PROG\n                          report duplicates based on arbitrary command\n    -d, --delete          delete items from library and disk\n    -F, --full            show all versions of duplicate tracks or albums\n    -s, --strict          report duplicates only if all attributes are set\n    -k, --key            report duplicates based on keys (can be used multiple times)\n    -M, --merge           merge duplicate items\n    -m DEST, --move=DEST  move items to dest\n    -o DEST, --copy=DEST  copy items to dest\n    -p, --path            print paths for matched items or albums\n    -t TAG, --tag=TAG     tag matched items with 'k=v' attribute\n    -r, --remove          remove items from library\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``duplicates:`` section in your configuration\nfile. The available options mirror the command-line options:\n\n- **album**: List duplicate albums instead of tracks. Default: ``no``.\n- **checksum**: Use an arbitrary command to compute a checksum of items. This\n  overrides the ``keys`` option the first time it is run; however, because it\n  caches the resulting checksum as ``flexattrs`` in the database, you can use\n  ``--key=name_of_the_checksumming_program --key=any_other_keys`` (or set the\n  ``keys`` configuration option) the second time around. Default: ``ffmpeg -i\n  {file} -f crc -``.\n- **copy**: A destination base directory into which to copy matched items.\n  Default: none (disabled).\n- **count**: Print a count of duplicate tracks or albums in the format\n  ``$albumartist - $album - $title: $count`` (for tracks) or ``$albumartist -\n  $album: $count`` (for albums). Default: ``no``.\n- **delete**: Remove matched items from the library and from the disk. Default:\n  ``no``\n- **format**: A specific format with which to print every track or album. This\n  uses the same template syntax as beets' :doc:`path formats\n  </reference/pathformat>`. The usage is inspired by, and therefore similar to,\n  the :ref:`list <list-cmd>` command. Default: :ref:`format_item`\n- **full**: List every track or album that has duplicates, not just the\n  duplicates themselves. Default: ``no``\n- **keys**: Define in which track or album fields duplicates are to be searched.\n  By default, the plugin uses the musicbrainz track and album IDs for this\n  purpose. Using the ``keys`` option (as a YAML list in the configuration file,\n  or as space-delimited strings in the command-line), you can extend this\n  behavior to consider other attributes. Default: ``[mb_trackid, mb_albumid]``\n- **merge**: Merge duplicate items by consolidating tracks and-or metadata where\n  possible.\n- **move**: A destination base directory into which it will move matched items.\n  Default: none (disabled).\n- **path**: Output the path instead of metadata when listing duplicates.\n  Default: ``no``.\n- **strict**: Do not report duplicate matches if some of the attributes are not\n  defined (ie. null or empty). Default: ``no``\n- **tag**: A ``key=value`` pair. The plugin will add a new ``key`` attribute\n  with ``value`` value as a flexattr to the database for duplicate items.\n  Default: ``no``.\n- **tiebreak**: Dictionary of lists of attributes keyed by ``items`` or\n  ``albums`` to use when choosing duplicates. By default, the tie-breaking\n  procedure favors the most complete metadata attribute set. If you would like\n  to consider the lower bitrates as duplicates, for example, set ``tiebreak:\n  items: [bitrate]``. Default: ``{}``.\n- **remove**: Remove matched items from the library, but not from the disk.\n  Default: ``no``.\n\nExamples\n--------\n\nList all duplicate tracks in your collection:\n\n::\n\n    beet duplicates\n\nList all duplicate tracks from 2008:\n\n::\n\n    beet duplicates year:2008\n\nPrint out a unicode histogram of duplicate track years using spark_:\n\n::\n\n    beet duplicates -f '$year' | spark\n    ▆▁▆█▄▇▇▄▇▇▁█▇▆▇▂▄█▁██▂█▁▁██▁█▂▇▆▂▇█▇▇█▆▆▇█▇█▇▆██▂▇\n\nPrint out a listing of all albums with duplicate tracks, and respective counts:\n\n::\n\n    beet duplicates -ac\n\nThe same as the above but include the original album, and show the path:\n\n::\n\n    beet duplicates -acf '$path'\n\nGet tracks with the same title, artist, and album:\n\n::\n\n    beet duplicates -k title -k albumartist -k album\n\nCompute Adler CRC32 or MD5 checksums, storing them as flexattrs, and report back\nduplicates based on those values:\n\n::\n\n    beet dup -C 'ffmpeg -i {file} -f crc -'\n    beet dup -C 'md5sum {file}'\n\nCopy highly danceable items to ``party`` directory:\n\n::\n\n    beet dup --copy /tmp/party\n\nMove likely duplicates to ``trash`` directory:\n\n::\n\n    beet dup --move ${HOME}/.Trash\n\nDelete items (careful!), if they're Nickelback:\n\n::\n\n    beet duplicates --delete -k albumartist -k albumartist:nickelback\n\nTag duplicate items with some flag:\n\n::\n\n    beet duplicates --tag dup=1\n\nIgnore items with undefined keys:\n\n::\n\n    beet duplicates --strict\n\nMerge and delete duplicate albums with different missing tracks:\n\n::\n\n    beet duplicates --album --merge --delete\n\n.. _spark: https://github.com/holman/spark\n"
  },
  {
    "path": "docs/plugins/edit.rst",
    "content": "Edit Plugin\n===========\n\nThe ``edit`` plugin lets you modify music metadata using your favorite text\neditor.\n\nEnable the ``edit`` plugin in your configuration (see :ref:`using-plugins`) and\nthen type:\n\n::\n\n    beet edit QUERY\n\nYour text editor (i.e., the command in your ``$VISUAL`` or ``$EDITOR``\nenvironment variable) will open with a list of tracks to edit. Make your changes\nand exit your text editor to apply them to your music.\n\nCommand-Line Options\n--------------------\n\nThe ``edit`` command has these command-line options:\n\n- ``-a`` or ``--album``: Edit albums instead of individual items.\n- ``-f FIELD`` or ``--field FIELD``: Specify an additional field to edit (in\n  addition to the defaults set in the configuration).\n- ``--all``: Edit *all* available fields.\n\nInteractive Usage\n-----------------\n\nThe ``edit`` plugin can also be invoked during an import session. If enabled, it\nadds two new options to the user prompt:\n\n::\n\n    [A]pply, More candidates, Skip, Use as-is, as Tracks, Group albums, Enter search, enter Id, aBort, eDit, edit Candidates?\n\n- ``eDit``: use this option for using the original items' metadata as the\n  starting point for your edits.\n- ``edit Candidates``: use this option for using a candidate's metadata as the\n  starting point for your edits.\n\nPlease note that currently the interactive usage of the plugin will only allow\nyou to change the item-level fields. In case you need to edit the album-level\nfields, the recommended approach is to invoke the plugin via the command line in\nalbum mode (``beet edit -a QUERY``) after the import.\n\nAlso, please be aware that the ``edit Candidates`` choice can only be used with\nthe matches found during the initial search (and currently not supporting the\ncandidates found via the ``Enter search`` or ``enter Id`` choices). You might\nfind the ``--search-id SEARCH_ID`` :ref:`import-cmd` option useful for those\ncases where you already have a specific candidate ID that you want to edit.\n\nConfiguration\n-------------\n\nTo configure the plugin, make an ``edit:`` section in your configuration file.\nThe available options are:\n\n- **itemfields**: A space-separated list of item fields to include in the editor\n  by default. Default: ``track title artist album``\n- **albumfields**: The same when editing albums (with the ``-a`` option).\n  Default: ``album albumartist``\n"
  },
  {
    "path": "docs/plugins/embedart.rst",
    "content": "EmbedArt Plugin\n===============\n\nTypically, beets stores album art in a \"file on the side\": along with each\nalbum, there is a file (named \"cover.jpg\" by default) that stores the album art.\nYou might want to embed the album art directly into each file's metadata. While\nthis will take more space than the external-file approach, it is necessary for\ndisplaying album art in some media players (iPods, for example).\n\nEmbedding Art Automatically\n---------------------------\n\nTo use the ``embedart`` plugin, first enable it in your configuration (see\n:ref:`using-plugins`). Then, install ``beets`` with ``embedart`` extra\n\n.. code-block:: bash\n\n    pip install \"beets[embedart]\"\n\nYou'll also want to enable the :doc:`/plugins/fetchart` to obtain the images to\nbe embedded. Art will be embedded after each album has its cover art set.\n\nThis behavior can be disabled with the ``auto`` config option (see below).\n\n.. _image-similarity-check:\n\nImage Similarity\n~~~~~~~~~~~~~~~~\n\nWhen importing a lot of files with the ``auto`` option, one may be reluctant to\noverwrite existing embedded art for all of them.\n\nYou can tell beets to avoid embedding images that are too different from the\nexisting ones. This works by computing the perceptual hashes (PHASH_) of the two\nimages and checking that the difference between the two does not exceed a\nthreshold. You can set the threshold with the ``compare_threshold`` option.\n\nA threshold of 0 (the default) disables similarity checking and always embeds\nnew images. Set the threshold to another number---we recommend between 10 and\n100---to adjust the sensitivity of the comparison. The smaller the threshold\nnumber, the more similar the images must be.\n\nThis feature requires ImageMagick_.\n\nConfiguration\n-------------\n\nTo configure the plugin, make an ``embedart:`` section in your configuration\nfile. The available options are:\n\n- **auto**: Enable automatic album art embedding. Default: ``yes``.\n- **compare_threshold**: How similar candidate art must be to existing art to be\n  written to the file (see :ref:`image-similarity-check`). Default: 0\n  (disabled).\n- **ifempty**: Avoid embedding album art for files that already have art\n  embedded. Default: ``no``.\n- **maxwidth**: A maximum width to downscale images before embedding them (the\n  original image file is not altered). The resize operation reduces image width\n  to at most ``maxwidth`` pixels. The height is recomputed so that the aspect\n  ratio is preserved. See also :ref:`image-resizing` for further caveats about\n  image resizing. Default: 0 (disabled).\n- **quality**: The JPEG quality level to use when compressing images (when\n  ``maxwidth`` is set). This should be either a number from 1 to 100 or 0 to use\n  the default quality. 65–75 is usually a good starting point. The default\n  behavior depends on the imaging tool used for scaling: ImageMagick tries to\n  estimate the input image quality and uses 92 if it cannot be determined, and\n  PIL defaults to 75. Default: 0 (disabled)\n- **remove_art_file**: Automatically remove the album art file for the album\n  after it has been embedded. This option is best used alongside the\n  :doc:`FetchArt </plugins/fetchart>` plugin to download art with the purpose of\n  directly embedding it into the file's metadata without an \"intermediate\" album\n  art file. Default: ``no``.\n- **clearart_on_import**: Enable automatic embedded art clearing. Default:\n  ``no``.\n\nNote: ``compare_threshold`` option requires ImageMagick_, and ``maxwidth``\nrequires either ImageMagick_ or Pillow_.\n\n.. _imagemagick: https://imagemagick.org/\n\n.. _phash: https://web.archive.org/web/*/http://www.fmwconcepts.com/misc_tests/perceptual_hash_test_results_510/index.html\n\n.. _pillow: https://github.com/python-pillow/Pillow\n\nManually Embedding and Extracting Art\n-------------------------------------\n\nThe ``embedart`` plugin provides a couple of commands for manually managing\nembedded album art:\n\n- ``beet embedart [-f IMAGE] QUERY``: embed images in every track of the albums\n  matching the query. If the ``-f`` (``--file``) option is given, then use a\n  specific image file from the filesystem; otherwise, each album embeds its own\n  currently associated album art. The command prompts for confirmation before\n  making the change unless you specify the ``-y`` (``--yes``) option.\n- ``beet embedart [-u IMAGE_URL] QUERY``: embed image specified in the URL into\n  every track of the albums matching the query. The ``-u`` (``--url``) option\n  can be used to specify the URL of the image to be used. The command prompts\n  for confirmation before making the change unless you specify the ``-y``\n  (``--yes``) option.\n- ``beet extractart [-a] [-n FILE] QUERY``: extracts the images for all albums\n  matching the query. The images are placed inside the album folder. You can\n  specify the destination file name using the ``-n`` option, but leave off the\n  extension: it will be chosen automatically. The destination filename is\n  specified using the ``art_filename`` configuration option. It defaults to\n  ``cover`` if it's not specified via ``-o`` nor the config. Using ``-a``, the\n  extracted image files are automatically associated with the corresponding\n  album.\n- ``beet extractart -o FILE QUERY``: extracts the image from an item matching\n  the query and stores it in a file. You have to specify the destination file\n  using the ``-o`` option, but leave off the extension: it will be chosen\n  automatically.\n- ``beet clearart QUERY``: removes all embedded images from all items matching\n  the query. The command prompts for confirmation before making the change\n  unless you specify the ``-y`` (``--yes``) option. The files listed for\n  confirmation are the ones matching the query independently of having an\n  embedded art. However, only the files with an embedded art are updated,\n  leaving untouched the files without.\n"
  },
  {
    "path": "docs/plugins/embyupdate.rst",
    "content": "EmbyUpdate Plugin\n=================\n\n``embyupdate`` is a plugin that lets you automatically update Emby_'s library\nwhenever you change your beets library.\n\nTo use it, first enable the your configuration (see :ref:`using-plugins`). Then,\ninstall ``beets`` with ``embyupdate`` extra\n\n.. code-block:: bash\n\n    pip install \"beets[embyupdate]\"\n\nThen, you'll want to configure the specifics of your Emby server. You can do\nthat using an ``emby`` section in your ``config.yaml``\n\n.. code-block:: yaml\n\n    emby:\n        host: localhost\n        port: 8096\n        username: user\n        apikey: apikey\n\nWith that all in place, you'll see beets send the \"update\" command to your Emby\nserver every time you change your beets library.\n\n.. _emby: https://emby.media/\n\nConfiguration\n-------------\n\nThe available options under the ``emby:`` section are:\n\n- **host**: The Emby server host. You also can include ``http://`` or\n  ``https://``. Default: ``localhost``\n- **port**: The Emby server port. Default: 8096\n- **username**: A username of an Emby user that is allowed to refresh the\n  library.\n- **userid**: A user ID of an Emby user that is allowed to refresh the library.\n  (This is only necessary for private users i.e. when the user is hidden from\n  login screens)\n- **apikey**: An Emby API key for the user.\n- **password**: The password for the user. (This is only necessary if no API key\n  is provided.)\n\nYou can choose to authenticate either with ``apikey`` or ``password``, but only\none of those two is required.\n"
  },
  {
    "path": "docs/plugins/export.rst",
    "content": "Export Plugin\n=============\n\nThe ``export`` plugin lets you get data from the items and export the content as\nJSON_, CSV_, or XML_.\n\n.. _csv: https://fileinfo.com/extension/csv\n\n.. _json: https://www.json.org\n\n.. _xml: https://fileinfo.com/extension/xml\n\nEnable the ``export`` plugin (see :ref:`using-plugins` for help). Then, type\n``beet export`` followed by a :doc:`query </reference/query>` to get the data\nfrom your library. For example, run this:\n\n::\n\n    $ beet export beatles\n\nto print a JSON file containing information about your Beatles tracks.\n\nCommand-Line Options\n--------------------\n\nThe ``export`` command has these command-line options:\n\n- ``--include-keys`` or ``-i``: Choose the properties to include in the output\n  data. The argument is a comma-separated list of simple glob patterns where\n  ``*`` matches any string. For example:\n\n  ::\n\n      $ beet export -i 'title,mb*' beatles\n\n  will include the ``title`` property and all properties starting with ``mb``.\n  You can add the ``-i`` option multiple times to the command line.\n\n- ``--library`` or ``-l``: Show data from the library database instead of the\n  files' tags.\n- ``--album`` or ``-a``: Show data from albums instead of tracks (implies\n  ``--library``).\n- ``--output`` or ``-o``: Path for an output file. If not informed, will print\n  the data in the console.\n- ``--append``: Appends the data to the file instead of writing.\n- ``--format`` or ``-f``: Specifies the format the data will be exported as. If\n  not informed, JSON will be used by default. The format options include csv,\n  json, `jsonlines <https://jsonlines.org/>`_ and xml.\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``export:`` section in your configuration file.\nFor JSON export, these options are available under the ``json`` and\n``jsonlines`` keys:\n\n- **ensure_ascii**: Escape non-ASCII characters with ``\\uXXXX`` entities.\n- **indent**: The number of spaces for indentation.\n- **separators**: A ``[item_separator, dict_separator]`` tuple.\n- **sort_keys**: Sorts the keys in JSON dictionaries.\n\nThose options match the options from the `Python json module`_. Similarly, these\noptions are available for the CSV format under the ``csv`` key:\n\n- **delimiter**: Used as the separating character between fields. The default\n  value is a comma (,).\n- **dialect**: The kind of CSV file to produce. The default is ``excel``.\n\nThese options match the options from the `Python csv module`_.\n\n.. _python csv module: https://docs.python.org/3/library/csv.html#csv-fmt-params\n\n.. _python json module: https://docs.python.org/3/library/json.html#basic-usage\n\nThe default options look like this:\n\n::\n\n    export:\n        json:\n            formatting:\n                ensure_ascii: false\n                indent: 4\n                separators: [',' , ': ']\n                sort_keys: true\n        csv:\n            formatting:\n                delimiter: ','\n                dialect: excel\n"
  },
  {
    "path": "docs/plugins/fetchart.rst",
    "content": "FetchArt Plugin\n===============\n\nThe ``fetchart`` plugin retrieves album art images from various sources on the\nWeb and stores them as image files.\n\nTo use the ``fetchart`` plugin, first enable it in your configuration (see\n:ref:`using-plugins`). Then, install ``beets`` with ``fetchart`` extra\n\n.. code-block:: bash\n\n    pip install \"beets[fetchart]\"\n\nFetching Album Art During Import\n--------------------------------\n\nWhen the plugin is enabled, it automatically tries to get album art for every\nalbum you import.\n\nBy default, beets stores album art image files alongside the music files for an\nalbum in a file called ``cover.jpg``. To customize the name of this file, use\nthe :ref:`art-filename` config option. To embed the art into the files' tags,\nuse the :doc:`/plugins/embedart`. (You'll want to have both plugins enabled.)\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``fetchart:`` section in your configuration\nfile. The available options are:\n\n- **auto**: Enable automatic album art fetching during import. Default: ``yes``.\n- **cautious**: Pick only trusted album art by ignoring filenames that do not\n  contain one of the keywords in ``cover_names``. Default: ``no``.\n- **cover_names**: Prioritize images containing words in this list. Default:\n  ``cover front art album folder``.\n- **fallback**: Path to a fallback album art file if no album art was found\n  otherwise. Default: ``None`` (disabled).\n- **minwidth**: Only images with a width bigger or equal to ``minwidth`` are\n  considered as valid album art candidates. Default: 0.\n- **maxwidth**: A maximum image width to downscale fetched images if they are\n  too big. The resize operation reduces image width to at most ``maxwidth``\n  pixels. The height is recomputed so that the aspect ratio is preserved. See\n  the section on :ref:`cover-art-archive-maxwidth` below for additional\n  information regarding the Cover Art Archive source. Default: 0 (no maximum is\n  enforced).\n- **quality**: The JPEG quality level to use when compressing images (when\n  ``maxwidth`` is set). This should be either a number from 1 to 100 or 0 to use\n  the default quality. 65–75 is usually a good starting point. The default\n  behavior depends on the imaging tool used for scaling: ImageMagick tries to\n  estimate the input image quality and uses 92 if it cannot be determined, and\n  PIL defaults to 75. Default: 0 (disabled)\n- **max_filesize**: The maximum size of a target piece of cover art in bytes.\n  When using an ImageMagick backend this sets ``-define\n  jpeg:extent=max_filesize``. Using PIL this will reduce JPG quality by up to\n  50% to attempt to reach the target filesize. Neither method is *guaranteed* to\n  reach the target size, however in most cases it should succeed. Default: 0\n  (disabled)\n- **enforce_ratio**: Only images with a width:height ratio of 1:1 are considered\n  as valid album art candidates if set to ``yes``. It is also possible to\n  specify a certain deviation to the exact ratio to still be considered valid.\n  This can be done either in pixels (``enforce_ratio: 10px``) or as a percentage\n  of the longer edge (``enforce_ratio: 0.5%``). Default: ``no``.\n- **sources**: List of sources to search for images. An asterisk ``*`` expands\n  to all available sources. Default: ``filesystem coverart itunes amazon\n  albumart``, i.e., everything but ``wikipedia``, ``google``, ``fanarttv`` and\n  ``lastfm``. Enable those sources for more matches at the cost of some speed.\n  They are searched in the given order, thus in the default config, no remote\n  (Web) art source are queried if local art is found in the filesystem. To use a\n  local image as fallback, move it to the end of the list. For even more\n  fine-grained control over the search order, see the section on\n  :ref:`album-art-sources` below.\n- **google_key**: Your Google API key (to enable the Google Custom Search\n  backend). Default: None.\n- **google_engine**: The custom search engine to use. Default: The `beets custom\n  search engine`_, which searches the entire web.\n- **fanarttv_key**: The personal API key for requesting art from fanart.tv. See\n  below.\n- **lastfm_key**: The personal API key for requesting art from Last.fm. See\n  below.\n- **store_source**: If enabled, fetchart stores the artwork's source in a\n  flexible tag named ``art_source``. See below for the rationale behind this.\n  Default: ``no``.\n- **high_resolution**: If enabled, fetchart retrieves artwork in the highest\n  resolution it can find (warning: image files can sometimes reach >20MB).\n  Default: ``no``.\n- **deinterlace**: If enabled, Pillow_ or ImageMagick_ backends are instructed\n  to store cover art as non-progressive JPEG. You might need this if you use\n  DAPs that don't support progressive images. Default: ``no``.\n- **cover_format**: If enabled, forced the cover image into the specified\n  format. Most often, this will be either ``JPEG`` or ``PNG`` (see\n  image-formats_). Also respects ``deinterlace``. Default: None (leave\n  unchanged).\n\nNote: ``maxwidth`` and ``enforce_ratio`` options require either ImageMagick_ or\nPillow_.\n\n.. note::\n\n    Previously, there was a ``remote_priority`` option to specify when to look\n    for art on the filesystem. This is still respected, but a deprecation\n    message will be shown until you replace this configuration with the new\n    ``filesystem`` value in the ``sources`` array.\n\n.. _image-formats:\n\n.. admonition:: Image formats\n\n    Other image formats are available, though the full list depends on your\n    system and what backend you are using. If you're using the ImageMagick\n    backend, you can use ``magick identify -list format`` to get a full list of\n    all supported formats, and you can use the Python function\n    PIL.features.pilinfo() to print a list of all supported formats in Pillow\n    (``python3 -c 'import PIL.features as f; f.pilinfo()'``).\n\n.. _beets custom search engine: https://cse.google.com/cse?cx=001442825323518660753:hrh5ch1gjzm\n\nHere's an example that makes plugin select only images that contain ``front`` or\n``back`` keywords in their filenames and prioritizes the iTunes source over\nothers:\n\n::\n\n    fetchart:\n        cautious: true\n        cover_names: front back\n        sources: itunes *\n\nManually Fetching Album Art\n---------------------------\n\nUse the ``fetchart`` command to download album art after albums have already\nbeen imported:\n\n::\n\n    $ beet fetchart [-f] [query]\n\nBy default, the command will only look for album art when the album doesn't\nalready have it; the ``-f`` or ``--force`` switch makes it search for art in Web\ndatabases regardless. If you specify a query, only matching albums will be\nprocessed; otherwise, the command processes every album in your library.\n\nDisplay Only Missing Album Art\n------------------------------\n\nUse the ``fetchart`` command with the ``-q`` switch in order to display only\nmissing art:\n\n::\n\n    $ beet fetchart [-q] [query]\n\nBy default the command will display all albums matching the ``query``. When the\n``-q`` or ``--quiet`` switch is given, only albums for which artwork has been\nfetched, or for which artwork could not be found will be printed.\n\n.. _image-resizing:\n\nImage Resizing\n--------------\n\nBeets can resize images using Pillow_, ImageMagick_, or a server-side resizing\nproxy. If either Pillow or ImageMagick is installed, beets will use those;\notherwise, it falls back to the resizing proxy. If the resizing proxy is used,\nno resizing is performed for album art found on the filesystem---only downloaded\nart is resized. Server-side resizing can also be slower than local resizing, so\nconsider installing one of the two backends for better performance.\n\nWhen using ImageMagick, beets looks for the ``convert`` executable in your path.\nOn some versions of Windows, the program can be shadowed by a system-provided\n``convert.exe``. On these systems, you may need to modify your ``%PATH%``\nenvironment variable so that ImageMagick comes first or use Pillow instead.\n\n.. _album-art-sources:\n\nAlbum Art Sources\n-----------------\n\nBy default, this plugin searches for art in the local filesystem as well as on\nthe Cover Art Archive, the iTunes Store, Amazon, and AlbumArt.org, in that\norder. You can reorder the sources or remove some to speed up the process using\nthe ``sources`` configuration option.\n\nWhen looking for local album art, beets checks for image files located in the\nsame folder as the music files you're importing. Beets prefers to use an image\nfile whose name contains \"cover\", \"front\", \"art\", \"album\" or \"folder\", but in\nthe absence of well-known names, it will use any image file in the same folder\nas your music files.\n\nFor some of the art sources, the backend service can match artwork by various\ncriteria. If you want finer control over the search order in such cases, you can\nuse this alternative syntax for the ``sources`` option:\n\n::\n\n    fetchart:\n        sources:\n            - filesystem\n            - coverart: release\n            - itunes\n            - coverart: releasegroup\n            - '*'\n\nwhere listing a source without matching criteria will default to trying all\navailable strategies. Entries of the forms ``coverart: release releasegroup``\nand ``coverart: *`` are also valid. Currently, only the ``coverart`` source\nsupports multiple criteria: namely, ``release`` and ``releasegroup``, which\nrefer to the respective MusicBrainz IDs.\n\nWhen you choose to apply changes during an import, beets will search for art as\ndescribed above. For \"as-is\" imports (and non-autotagged imports using the\n``-A`` flag), beets only looks for art on the local filesystem.\n\nGoogle custom search\n~~~~~~~~~~~~~~~~~~~~\n\nTo use the google image search backend you need to `register for a Google API\nkey`_. Set the ``google_key`` configuration option to your key, then add\n``google`` to the list of sources in your configuration.\n\n.. _register for a google api key: https://console.developers.google.com.\n\nOptionally, you can `define a custom search engine`_. Get your search engine's\ntoken and use it for your ``google_engine`` configuration option. The default\nengine searches the entire web for cover art.\n\n.. _define a custom search engine: https://programmablesearchengine.google.com/about/\n\nNote that the Google custom search API is limited to 100 queries per day. After\nthat, the fetchart plugin will fall back on other declared data sources.\n\nFanart.tv\n~~~~~~~~~\n\nAlthough not strictly necessary right now, you might think about `registering a\npersonal fanart.tv API key`_. Set the ``fanarttv_key`` configuration option to\nyour key, then add ``fanarttv`` to the list of sources in your configuration.\n\n.. _registering a personal fanart.tv api key: https://fanart.tv/get-an-api-key/\n\nMore detailed information can be found `on their Wiki`_. Specifically, the\npersonal key will give you earlier access to new art.\n\n.. _on their wiki: https://wiki.fanart.tv/General/personal%20api/\n\nLast.fm\n~~~~~~~\n\nTo use the Last.fm backend, you need to `register for a Last.fm API key`_. Set\nthe ``lastfm_key`` configuration option to your API key, then add ``lastfm`` to\nthe list of sources in your configuration.\n\n.. _register for a last.fm api key: https://www.last.fm/api/account/create\n\nSpotify\n~~~~~~~\n\nSpotify backend is enabled by default and will update album art if a valid\nSpotify album id is found.\n\n.. _beautifulsoup: https://www.crummy.com/software/BeautifulSoup/bs4/doc/\n\n.. _pip: https://pip.pypa.io\n\nCover Art URL\n~~~~~~~~~~~~~\n\nThe ``fetchart`` plugin can also use a flexible attribute field\n``cover_art_url`` where you can manually specify the image URL to be used as\ncover art. Any custom plugin can use this field to provide the cover art and\n``fetchart`` will use it as a source.\n\n.. _cover-art-archive-maxwidth:\n\nCover Art Archive Pre-sized Thumbnails\n--------------------------------------\n\nThe CAA provides pre-sized thumbnails of width 250, 500, and 1200 pixels. If you\nset the ``maxwidth`` option to one of these values, the corresponding image will\nbe downloaded, saving ``beets`` the need to scale down the image. It can also\nspeed up the downloading process, as some cover arts can sometimes be very\nlarge.\n\nStoring the Artwork's Source\n----------------------------\n\nStoring the current artwork's source might be used to narrow down ``fetchart``\ncommands. For example, if some albums have artwork placed manually in their\ndirectories that should not be replaced by a forced album art fetch, you could\ndo\n\n``beet fetchart -f ^art_source:filesystem``\n\nThe values written to ``art_source`` are the same names used in the ``sources``\nconfiguration value.\n\n.. _imagemagick: https://imagemagick.org/\n\n.. _pillow: https://github.com/python-pillow/Pillow\n"
  },
  {
    "path": "docs/plugins/filefilter.rst",
    "content": "FileFilter Plugin\n=================\n\nThe ``filefilter`` plugin allows you to skip files during import using regular\nexpressions.\n\nTo use the ``filefilter`` plugin, enable it in your configuration (see\n:ref:`using-plugins`).\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``filefilter:`` section in your configuration\nfile. The available options are:\n\n- **path**: A regular expression to filter files based on their path and name.\n  Default: ``.*`` (import everything)\n- **album_path** and **singleton_path**: You may specify different regular\n  expressions used for imports of albums and singletons. This way, you can\n  automatically skip singletons when importing albums if the names (and paths)\n  of the files are distinguishable via a regex. The regexes defined here take\n  precedence over the global ``path`` option.\n\nHere's an example:\n\n::\n\n    filefilter:\n        path: .*\\d\\d[^/]+$\n              # will only import files which names start with two digits\n        album_path: .*\\d\\d[^/]+$\n        singleton_path: .*/(?!\\d\\d)[^/]+$\n"
  },
  {
    "path": "docs/plugins/fish.rst",
    "content": "Fish Plugin\n===========\n\nThe ``fish`` plugin adds a ``beet fish`` command that creates a `Fish shell`_\ntab-completion file named ``beet.fish`` in ``~/.config/fish/completions``. This\nenables tab-completion of ``beet`` commands for the `Fish shell`_.\n\n.. _fish shell: https://fishshell.com/\n\nConfiguration\n-------------\n\nEnable the ``fish`` plugin (see :ref:`using-plugins`) on a system running the\n`Fish shell`_.\n\nUsage\n-----\n\nType ``beet fish`` to generate the ``beet.fish`` completions file at:\n``~/.config/fish/completions/``. If you later install or disable plugins, run\n``beet fish`` again to update the completions based on the enabled plugins.\n\nFor users not accustomed to tab completion… After you type ``beet`` followed by\na space in your shell prompt and then the ``TAB`` key, you should see a list of\nthe beets commands (and their abbreviated versions) that can be invoked in your\ncurrent environment. Similarly, typing ``beet -<TAB>`` will show you all the\noption flags available to you, which also applies to subcommands such as ``beet\nimport -<TAB>``. If you type ``beet ls`` followed by a space and then the and\nthe ``TAB`` key, you will see a list of all the album/track fields that can be\nused in beets queries. For example, typing ``beet ls ge<TAB>`` will complete to\n``genres:`` and leave you ready to type the rest of your query.\n\nOptions\n-------\n\nIn addition to beets commands, plugin commands, and option flags, the generated\ncompletions also include by default all the album/track fields. If you only want\nthe former and do not want the album/track fields included in the generated\ncompletions, use ``beet fish -f`` to only generate completions for beets/plugin\ncommands and option flags.\n\nIf you want generated completions to also contain album/track field *values* for\nthe items in your library, you can use the ``-e`` or ``--extravalues`` option.\nFor example: ``beet fish -e genre`` or ``beet fish -e genre -e albumartist`` In\nthe latter case, subsequently typing ``beet list genres: <TAB>`` will display a\nlist of all the genres in your library and ``beet list albumartist: <TAB>`` will\nshow a list of the album artists in your library. Keep in mind that all of these\nvalues will be put into the generated completions file, so use this option with\ncare when specified fields contain a large number of values. Libraries with, for\nexample, very large numbers of genres/artists may result in higher memory\nutilization, completion latency, et cetera. This option is not meant to replace\ndatabase queries altogether.\n\nBy default, the completion file will be generated at\n``~/.config/fish/completions/``. If you want to save it somewhere else, you can\nuse the ``-o`` or ``--output`` option.\n"
  },
  {
    "path": "docs/plugins/freedesktop.rst",
    "content": "Freedesktop Plugin\n==================\n\nThe ``freedesktop`` plugin created .directory files in your album folders. This\nplugin is now deprecated and replaced by the :doc:`/plugins/thumbnails` with the\n``dolphin`` option enabled.\n"
  },
  {
    "path": "docs/plugins/fromfilename.rst",
    "content": "FromFilename Plugin\n===================\n\nThe ``fromfilename`` plugin helps to tag albums that are missing tags altogether\nbut where the filenames contain useful information like the artist and title.\n\nWhen you attempt to import a track that's missing a title, this plugin will look\nat the track's filename and guess its track number, title, and artist. These\nwill be used to search in MusicBrainz and match track ordering.\n\nTo use the ``fromfilename`` plugin, enable it in your configuration (see\n:ref:`using-plugins`).\n"
  },
  {
    "path": "docs/plugins/ftintitle.rst",
    "content": "FtInTitle Plugin\n================\n\nThe ``ftintitle`` plugin automatically moves \"featured\" artists from the\n``artist`` field to the ``title`` field.\n\nAccording to `MusicBrainz style`_, featured artists are part of the artist\nfield. That means that, if you tag your music using MusicBrainz, you'll have\ntracks in your library like \"Tellin' Me Things\" by the artist \"Blakroc feat.\nRZA\". If you prefer to tag this as \"Tellin' Me Things feat. RZA\" by \"Blakroc\",\nthen this plugin is for you.\n\nTo use the ``ftintitle`` plugin, enable it in your configuration (see\n:ref:`using-plugins`).\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``ftintitle:`` section in your configuration\nfile. The available options are:\n\n- **auto**: Enable metadata rewriting during import. Default: ``yes``.\n- **drop**: Remove featured artists entirely instead of adding them to the title\n  field. Default: ``no``.\n- **format**: Defines the format for the featuring X part of the new title\n  field. In this format the ``{0}`` is used to define where the featured artists\n  are placed. Default: ``feat. {0}``\n- **keep_in_artist**: Keep the featuring X part in the artist field. This can be\n  useful if you still want to be able to search for features in the artist\n  field. Default: ``no``.\n- **preserve_album_artist**: If the artist and the album artist are the same,\n  skip the ftintitle processing. Default: ``yes``.\n- **custom_words**: List of additional words that will be treated as a marker\n  for artist features. Default: ``[]``.\n- **bracket_keywords**: Controls where the featuring text is inserted when the\n  title includes bracketed qualifiers such as ``(Remix)`` or ``[Live]``.\n  FtInTitle inserts the new text before the first bracket whose contents match\n  any of these keywords. Supply a list of words to fine-tune the behavior or set\n  the list to ``[]`` to match *any* bracket regardless of its contents. Default:\n\n  ::\n\n      [\"abridged\", \"acapella\", \"club\", \"demo\", \"edit\", \"edition\", \"extended\",\n       \"instrumental\", \"live\", \"mix\", \"radio\", \"release\", \"remaster\",\n       \"remastered\", \"remix\", \"rmx\", \"unabridged\", \"unreleased\",\n       \"version\", \"vip\"]\n\nPath Template Values\n--------------------\n\nThis plugin provides the ``album_artist_no_feat`` :ref:`template value\n<templ_plugins>` that you can use in your :ref:`path-format-config` in\n``paths.default``. Any ``custom_words`` in the configuration are taken into\naccount.\n\nRunning Manually\n----------------\n\nFrom the command line, type:\n\n::\n\n    $ beet ftintitle [QUERY]\n\nThe query is optional; if it's left off, the transformation will be applied to\nyour entire collection.\n\nUse the ``-d`` flag to remove featured artists (equivalent of the ``drop``\nconfig option).\n\n.. _musicbrainz style: https://musicbrainz.org/doc/Style\n"
  },
  {
    "path": "docs/plugins/fuzzy.rst",
    "content": "Fuzzy Search Plugin\n===================\n\nThe ``fuzzy`` plugin provides a prefixed query that searches your library using\nfuzzy pattern matching. This can be useful if you want to find a track with\ncomplicated characters in the title.\n\nFirst, enable the plugin named ``fuzzy`` (see :ref:`using-plugins`). You'll then\nbe able to use the ``~`` prefix to use fuzzy matching:\n\n::\n\n    $ beet ls '~Vareoldur'\n    Sigur Rós - Valtari - Varðeldur\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``fuzzy:`` section in your configuration file.\nThe available options are:\n\n- **threshold**: The \"sensitivity\" of the fuzzy match. A value of 1.0 will show\n  only perfect matches and a value of 0.0 will match everything. Default: 0.7.\n- **prefix**: The character used to designate fuzzy queries. Default: ``~``,\n  which may need to be escaped in some shells.\n"
  },
  {
    "path": "docs/plugins/hook.rst",
    "content": "Hook Plugin\n===========\n\nInternally, beets uses *events* to tell plugins when something happens. For\nexample, one event fires when the importer finishes processes a song, and\nanother triggers just before the ``beet`` command exits. The ``hook`` plugin\nlets you run commands in response to these events.\n\n.. _hook-configuration:\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``hook`` section in your configuration file. The\navailable options are:\n\n- **hooks**: A list of events and the commands to run (see\n  :ref:`individual-hook-configuration`). Default: Empty.\n\n.. _individual-hook-configuration:\n\nConfiguring Each Hook\n~~~~~~~~~~~~~~~~~~~~~\n\nEach element under ``hooks`` should have these keys:\n\n- **event**: The name of the event that will trigger this hook. See the\n  :ref:`plugin events <plugin_events>` documentation for a list of possible\n  values.\n- **command**: The command to run when this hook executes.\n\n.. _command-substitution:\n\nCommand Substitution\n~~~~~~~~~~~~~~~~~~~~\n\nCommands can access the parameters of events using `Python string formatting`_.\nUse ``{name}`` in your command and the plugin will substitute it with the named\nvalue. The name can also refer to a field, as in ``{album.path}``.\n\n.. _python string formatting: https://peps.python.org/pep-3101/\n\nYou can find a list of all available events and their arguments in the\n:ref:`plugin events <plugin_events>` documentation.\n\nExample Configuration\n---------------------\n\n.. code-block:: yaml\n\n    hook:\n      hooks:\n        # Output on exit:\n        #   beets just exited!\n        #   have a nice day!\n        - event: cli_exit\n          command: echo \"beets just exited!\"\n        - event: cli_exit\n          command: echo \"have a nice day!\"\n\n        # Output on item import:\n        #   importing \"<file_name_here>\"\n        # Where <file_name_here> is the item being imported\n        - event: item_imported\n          command: echo \"importing \\\"{item.path}\\\"\"\n\n        # Output on write:\n        #   writing to \"<file_name_here>\"\n        # Where <file_name_here> is the file being written to\n        - event: write\n          command: echo \"writing to {path}\"\n"
  },
  {
    "path": "docs/plugins/ihate.rst",
    "content": "IHate Plugin\n============\n\nThe ``ihate`` plugin allows you to automatically skip things you hate during\nimport or warn you about them. You specify queries (see :doc:`/reference/query`)\nand the plugin skips (or warns about) albums or items that match any query.\n\nTo use the ``ihate`` plugin, enable it in your configuration (see\n:ref:`using-plugins`).\n\nConfiguration\n-------------\n\nTo configure the plugin, make an ``ihate:`` section in your configuration file.\nThe available options are:\n\n- **skip**: Never import items and albums that match a query in this list.\n  Default: ``[]`` (empty list).\n- **warn**: Print a warning message for matches in this list of queries.\n  Default: ``[]``.\n\nHere's an example:\n\n::\n\n    ihate:\n        warn:\n            - artist:rnb\n            - genres:soul\n            # Only warn about tribute albums in rock genre.\n            - genres:rock album:tribute\n        skip:\n            - genres::russian\\srock\n            - genres:polka\n            - artist:manowar\n            - album:christmas\n\nThe plugin trusts your decision in \"as-is\" imports.\n"
  },
  {
    "path": "docs/plugins/importadded.rst",
    "content": "ImportAdded Plugin\n==================\n\nThe ``importadded`` plugin is useful when an existing collection is imported and\nthe time when albums and items were added should be preserved.\n\nTo use the ``importadded`` plugin, enable it in your configuration (see\n:ref:`using-plugins`).\n\nUsage\n-----\n\nThe :abbr:`mtime (modification time)` of files that are imported into the\nlibrary are assumed to represent the time when the items were originally added.\n\nThe ``item.added`` field is populated as follows:\n\n- For singleton items with no album, ``item.added`` is set to the item's file\n  mtime before it was imported.\n- For items that are part of an album, ``album.added`` and ``item.added`` are\n  set to the oldest mtime of the files in the album before they were imported.\n  The mtime of album directories is ignored.\n\nThis plugin can optionally be configured to also preserve mtimes at import using\nthe ``preserve_mtimes`` option.\n\nWhen ``preserve_write_mtimes`` option is set, this plugin preserves mtimes after\neach write to files using the ``item.added`` attribute.\n\nFile modification times are preserved as follows:\n\n- For all items:\n\n  - ``item.mtime`` is set to the mtime of the file from which the item is\n    imported from.\n  - The mtime of the file ``item.path`` is set to ``item.mtime``.\n\nNote that there is no ``album.mtime`` field in the database and that the mtime\nof album directories on disk aren't preserved.\n\nConfiguration\n-------------\n\nTo configure the plugin, make an ``importadded:`` section in your configuration\nfile. There are two options available:\n\n- **preserve_mtimes**: After importing files, re-set their mtimes to their\n  original value. Default: ``no``.\n- **preserve_write_mtimes**: After writing files, re-set their mtimes to their\n  original value. Default: ``no``.\n\nReimport\n--------\n\nThis plugin will skip reimported singleton items and reimported albums and all\nof their items.\n"
  },
  {
    "path": "docs/plugins/importfeeds.rst",
    "content": "ImportFeeds Plugin\n==================\n\nThis plugin helps you keep track of newly imported music in your library.\n\nTo use the ``importfeeds`` plugin, enable it in your configuration (see\n:ref:`using-plugins`).\n\nConfiguration\n-------------\n\nTo configure the plugin, make an ``importfeeds:`` section in your configuration\nfile. The available options are:\n\n- **absolute_path**: Use absolute paths instead of relative paths. Some\n  applications may need this to work properly. Default: ``no``.\n- **dir**: The output directory. Default: Your beets library directory.\n- **formats**: Select the kind of output. Use one or more of:\n\n      - **m3u**: Catalog the imports in a centralized playlist.\n      - **m3u_multi**: Create a new playlist for each import (uniquely named by\n        appending the date and track/album name).\n      - **m3u_session**: Create a new playlist for each import session. The file\n        is named as ``m3u_name`` appending the date and time the import session\n        was started.\n      - **link**: Create a symlink for each imported item. This is the\n        recommended setting to propagate beets imports to your iTunes library:\n        just drag and drop the ``dir`` folder on the iTunes dock icon.\n      - **echo**: Do not write a playlist file at all, but echo a list of new\n        file paths to the terminal.\n\n  Default: None.\n\n- **m3u_name**: Playlist name used by the ``m3u`` format and as a prefix used by\n  the ``m3u_session`` format. Default: ``imported.m3u``.\n- **relative_to**: Make the m3u paths relative to another folder than where the\n  playlist is being written. If you're using importfeeds to generate a playlist\n  for MPD, you should set this to the root of your music library. Default: None.\n\nHere's an example configuration for this plugin:\n\n::\n\n    importfeeds:\n        formats: m3u link\n        dir: ~/imports/\n        relative_to: ~/Music/\n        m3u_name: newfiles.m3u\n"
  },
  {
    "path": "docs/plugins/importsource.rst",
    "content": "ImportSource Plugin\n===================\n\nThe ``importsource`` plugin adds a ``source_path`` field to every item imported\nto the library which stores the original media files' paths. Using this plugin\nmakes most sense when the general importing workflow is using ``beet import\n--copy``. Additionally the plugin interactively suggests deletion of original\nsource files whenever items are removed from the Beets library.\n\nTo enable it, add ``importsource`` to the list of plugins in your configuration\n(see :ref:`using-plugins`).\n\nTracking Source Paths\n---------------------\n\nThe primary use case for the plugin is tracking the original location of\nimported files using the ``source_path`` field. Consider this scenario: you've\nimported all directories in your current working directory using:\n\n.. code-block:: bash\n\n    beet import --flat --copy */\n\nLater, for instance if the import didn't complete successfully, you'll need to\nrerun the import but don't want Beets to re-process the already successfully\nimported directories. You can view which files were successfully imported using:\n\n.. code-block:: bash\n\n    beet ls source_path:$PWD --format='$source_path'\n\nTo extract just the directory names, pipe the output to standard UNIX utilities:\n\n.. code-block:: bash\n\n    beet ls source_path:$PWD --format='$source_path' | awk -F / '{print $(NF-1)}' | sort -u\n\nThis might help to find out what's left to be imported.\n\nRemoval Suggestion\n------------------\n\nAnother feature of the plugin is suggesting removal of original source files\nwhen items are deleted from your library. Consider this scenario: you imported\nan album using:\n\n.. code-block:: bash\n\n    beet import --copy --flat ~/Desktop/interesting-album-to-check/\n\nAfter listening to that album and deciding it wasn't good, you want to delete it\nfrom your library as well as from your ``~/Desktop``, so you run:\n\n.. code-block:: bash\n\n    beet remove --delete source_path:$HOME/Desktop/interesting-album-to-check\n\nAfter approving the deletion, the plugin will prompt:\n\n.. code-block:: text\n\n    The item:\n    <music-library>/Interesting Album/01 Interesting Song.flac\n    is originated from:\n    <HOME>/Desktop/interesting-album-to-check/01-interesting-song.flac\n    What would you like to do?\n    Delete the item's source, Recursively delete the source's directory,\n    do Nothing,\n    do nothing and Stop suggesting to delete items from this album?\n\nConfiguration\n-------------\n\nTo configure the plugin, make an ``importsource:`` section in your configuration\nfile. There is one option available:\n\n- **suggest_removal**: By default ``importsource`` suggests to remove the\n  original directories / files from which the items were imported whenever\n  library items (and files) are removed. To disable these prompts set this\n  option to ``no``. Default: ``yes``.\n"
  },
  {
    "path": "docs/plugins/index.rst",
    "content": "Plugins\n=======\n\nPlugins extend beets' core functionality. They add new commands, fetch\nadditional data during import, provide new metadata sources, and much more. If\nbeets by itself doesn't do what you want it to, you may just need to enable a\nplugin---or, if you want to do something new, :doc:`writing a plugin\n</dev/plugins/index>` is easy if you know a little Python.\n\n.. _using-plugins:\n\nUsing Plugins\n-------------\n\nTo use one of the plugins included with beets (see the rest of this page for a\nlist), just use the ``plugins`` option in your :doc:`config.yaml\n</reference/config>` file:\n\n.. code-block:: sh\n\n    plugins: musicbrainz inline convert web\n\nThe value for ``plugins`` can be a space-separated list of plugin names or a\nYAML list like ``[foo, bar]``. You can see which plugins are currently enabled\nby typing ``beet version``.\n\nEach plugin has its own set of options that can be defined in a section bearing\nits name:\n\n.. code-block:: yaml\n\n    plugins: musicbrainz inline convert web\n\n    convert:\n        auto: true\n\nSome plugins have special dependencies that you'll need to install. The\ndocumentation page for each plugin will list them in the setup instructions. For\nsome, you can use ``pip``'s \"extras\" feature to install the dependencies:\n\n.. code-block:: sh\n\n    pip install \"beets[fetchart,lyrics,lastgenre]\"\n\n.. _metadata-source-plugin-configuration:\n\nUsing Metadata Source Plugins\n-----------------------------\n\nWe provide several :ref:`autotagger_extensions` that fetch metadata from online\ndatabases. They share the following configuration options:\n\n.. include:: ./shared_metadata_source_config.rst\n\n.. toctree::\n    :hidden:\n\n    absubmit\n    acousticbrainz\n    advancedrewrite\n    albumtypes\n    aura\n    autobpm\n    badfiles\n    bareasc\n    beatport\n    bpd\n    bpm\n    bpsync\n    bucket\n    chroma\n    convert\n    deezer\n    discogs\n    duplicates\n    edit\n    embedart\n    embyupdate\n    export\n    fetchart\n    filefilter\n    fish\n    freedesktop\n    fromfilename\n    ftintitle\n    fuzzy\n    hook\n    ihate\n    importadded\n    importsource\n    importfeeds\n    info\n    inline\n    ipfs\n    keyfinder\n    kodiupdate\n    lastgenre\n    lastimport\n    limit\n    listenbrainz\n    loadext\n    lyrics\n    mbsync\n    metasync\n    missing\n    mpdstats\n    mpdupdate\n    musicbrainz\n    mbcollection\n    mbpseudo\n    mbsubmit\n    parentwork\n    permissions\n    play\n    playlist\n    plexupdate\n    random\n    replace\n    replaygain\n    rewrite\n    scrub\n    smartplaylist\n    sonosupdate\n    spotify\n    subsonicplaylist\n    subsonicupdate\n    substitute\n    the\n    thumbnails\n    titlecase\n    types\n    unimported\n    web\n    zero\n\n.. _autotagger_extensions:\n\nAutotagger Extensions\n---------------------\n\n:doc:`chroma <chroma>`\n    Use acoustic fingerprinting to identify audio files with missing or\n    incorrect metadata.\n\n:doc:`deezer <deezer>`\n    Search for releases in the Deezer_ database.\n\n:doc:`discogs <discogs>`\n    Search for releases in the Discogs_ database.\n\n:doc:`fromfilename <fromfilename>`\n    Guess metadata for untagged tracks from their filenames.\n\n:doc:`musicbrainz <musicbrainz>`\n    Search for releases in the MusicBrainz_ database.\n\n:doc:`mbpseudo <mbpseudo>`\n    Search for releases and pseudo-releases in the MusicBrainz_ database.\n\n:doc:`spotify <spotify>`\n    Search for releases in the Spotify_ database.\n\n.. _deezer: https://www.deezer.com/en/\n\n.. _discogs: https://www.discogs.com\n\n.. _musicbrainz: https://www.musicbrainz.com\n\n.. _spotify: https://open.spotify.com/\n\nMetadata\n--------\n\n:doc:`absubmit <absubmit>`\n    Analyse audio with the streaming_extractor_music_ program and submit the\n    metadata to an AcousticBrainz server\n\n:doc:`acousticbrainz <acousticbrainz>`\n    Fetch various AcousticBrainz metadata\n\n:doc:`autobpm <autobpm>`\n    Use Librosa_ to calculate the BPM from the audio.\n\n:doc:`bpm <bpm>`\n    Measure tempo using keystrokes.\n\n:doc:`bpsync <bpsync>`\n    Fetch updated metadata from Beatport.\n\n:doc:`edit <edit>`\n    Edit metadata from a text editor.\n\n:doc:`embedart <embedart>`\n    Embed album art images into files' metadata.\n\n:doc:`fetchart <fetchart>`\n    Fetch album cover art from various sources.\n\n:doc:`ftintitle <ftintitle>`\n    Move \"featured\" artists from the artist field to the title field.\n\n:doc:`keyfinder <keyfinder>`\n    Use the KeyFinder_ program to detect the musical key from the audio.\n\n:doc:`importadded <importadded>`\n    Use file modification times for guessing the value for the ``added`` field\n    in the database.\n\n:doc:`lastgenre <lastgenre>`\n    Fetch genres based on Last.fm tags.\n\n:doc:`lastimport <lastimport>`\n    Collect play counts from Last.fm.\n\n:doc:`lyrics <lyrics>`\n    Automatically fetch song lyrics.\n\n:doc:`mbsync <mbsync>`\n    Fetch updated metadata from MusicBrainz.\n\n:doc:`metasync <metasync>`\n    Fetch metadata from local or remote sources\n\n:doc:`mpdstats <mpdstats>`\n    Connect to MPD_ and update the beets library with play statistics\n    (last_played, play_count, skip_count, rating).\n\n:doc:`parentwork <parentwork>`\n    Fetch work titles and works they are part of.\n\n:doc:`replaygain <replaygain>`\n    Calculate volume normalization for players that support it.\n\n:doc:`scrub <scrub>`\n    Clean extraneous metadata from music files.\n\n:doc:`zero <zero>`\n    Nullify fields by pattern or unconditionally.\n\n.. _keyfinder: https://www.ibrahimshaath.co.uk/keyfinder/\n\n.. _librosa: https://github.com/librosa/librosa/\n\n.. _streaming_extractor_music: https://acousticbrainz.org/download\n\nPath Formats\n------------\n\n:doc:`albumtypes <albumtypes>`\n    Format album type in path formats.\n\n:doc:`bucket <bucket>`\n    Group your files into bucket directories that cover different field values\n    ranges.\n\n:doc:`inline <inline>`\n    Use Python snippets to customize path format strings.\n\n:doc:`rewrite <rewrite>`\n    Substitute values in path formats.\n\n:doc:`advancedrewrite <advancedrewrite>`\n    Substitute field values for items matching a query.\n\n:doc:`substitute <substitute>`\n    As an alternative to :doc:`rewrite <rewrite>`, use this plugin. The main\n    difference between them is that this plugin never modifies the files\n    metadata.\n\n:doc:`the <the>`\n    Move patterns in path formats (i.e., move \"a\" and \"the\" to the end).\n\nInteroperability\n----------------\n\n:doc:`aura <aura>`\n    A server implementation of the AURA_ specification.\n\n:doc:`badfiles <badfiles>`\n    Check audio file integrity.\n\n:doc:`embyupdate <embyupdate>`\n    Automatically notifies Emby_ whenever the beets library changes.\n\n:doc:`fish <fish>`\n    Adds `Fish shell`_ tab autocompletion to ``beet`` commands.\n\n:doc:`importfeeds <importfeeds>`\n    Keep track of imported files via ``.m3u`` playlist file(s) or symlinks.\n\n:doc:`ipfs <ipfs>`\n    Import libraries from friends and get albums from them via ipfs.\n\n:doc:`kodiupdate <kodiupdate>`\n    Automatically notifies Kodi_ whenever the beets library changes.\n\n:doc:`mpdupdate <mpdupdate>`\n    Automatically notifies MPD_ whenever the beets library changes.\n\n:doc:`play <play>`\n    Play beets queries in your music player.\n\n:doc:`playlist <playlist>`\n    Use M3U playlists to query the beets library.\n\n:doc:`plexupdate <plexupdate>`\n    Automatically notifies Plex_ whenever the beets library changes.\n\n:doc:`smartplaylist <smartplaylist>`\n    Generate smart playlists based on beets queries.\n\n:doc:`sonosupdate <sonosupdate>`\n    Automatically notifies Sonos_ whenever the beets library changes.\n\n:doc:`thumbnails <thumbnails>`\n    Get thumbnails with the cover art on your album folders.\n\n:doc:`subsonicupdate <subsonicupdate>`\n    Automatically notifies Subsonic_ whenever the beets library changes.\n\n.. _aura: https://auraspec.readthedocs.io/en/latest/\n\n.. _emby: https://emby.media\n\n.. _fish shell: https://fishshell.com/\n\n.. _kodi: https://kodi.tv\n\n.. _plex: https://watch.plex.tv/\n\n.. _sonos: https://www.sonos.com/\n\n.. _subsonic: https://www.subsonic.org/pages/index.jsp\n\nMiscellaneous\n-------------\n\n:doc:`bareasc <bareasc>`\n    Search albums and tracks with bare ASCII string matching.\n\n:doc:`bpd <bpd>`\n    A music player for your beets library that emulates MPD_ and is compatible\n    with `MPD clients`_.\n\n:doc:`convert <convert>`\n    Transcode music and embed album art while exporting to a different\n    directory.\n\n:doc:`duplicates <duplicates>`\n    List duplicate tracks or albums.\n\n:doc:`export <export>`\n    Export data from queries to a format.\n\n:doc:`filefilter <filefilter>`\n    Automatically skip files during the import process based on regular\n    expressions.\n\n:doc:`fuzzy <fuzzy>`\n    Search albums and tracks with fuzzy string matching.\n\n:doc:`hook <hook>`\n    Run a command when an event is emitted by beets.\n\n:doc:`ihate <ihate>`\n    Automatically skip albums and tracks during the import process.\n\n:doc:`info <info>`\n    Print music files' tags to the console.\n\n:doc:`loadext <loadext>`\n    Load SQLite extensions.\n\n:doc:`mbcollection <mbcollection>`\n    Maintain your MusicBrainz collection list.\n\n:doc:`mbsubmit <mbsubmit>`\n    Print an album's tracks in a MusicBrainz-friendly format.\n\n:doc:`missing <missing>`\n    List missing tracks.\n\nmstream_\n    A music streaming server + webapp that can be used alongside beets.\n\n:doc:`random <random>`\n    Randomly choose albums and tracks from your library.\n\n:doc:`spotify <spotify>`\n    Create Spotify playlists from the Beets library.\n\n:doc:`types <types>`\n    Declare types for flexible attributes.\n\n:doc:`web <web>`\n    An experimental Web-based GUI for beets.\n\n.. _mpd: https://www.musicpd.org/\n\n.. _mpd clients: https://mpd.fandom.com/wiki/Clients\n\n.. _mstream: https://github.com/IrosTheBeggar/mStream\n\n.. _other-plugins:\n\nOther Plugins\n-------------\n\nIn addition to the plugins that come with beets, there are several plugins that\nare maintained by the beets community. To use an external plugin, there are two\noptions for installation:\n\n- Make sure it's in the Python path (known as ``sys.path`` to developers). This\n  just means the plugin has to be installed on your system (e.g., with a\n  ``setup.py`` script or a command like ``pip`` or ``easy_install``).\n- Set the ``pluginpath`` config variable to point to the directory containing\n  the plugin. (See :doc:`/reference/config`.)\n\nOnce the plugin is installed, enable it by placing its name on the ``plugins``\nline in your config file.\n\nHere are a few of the plugins written by the beets community:\n\nbeets-alternatives_\n    Manages external files.\n\nbeet-amazon_\n    Adds Amazon.com as a tagger data source.\n\nbeets-artistcountry_\n    Fetches the artist's country of origin from MusicBrainz.\n\nbeets-autofix_\n    Automates repetitive tasks to keep your library in order.\n\nbeets-autogenre_\n    Assigns genres to your library items using the :doc:`lastgenre <lastgenre>`\n    and beets-xtractor_ plugins as well as additional rules.\n\nbeets-audible_\n    Adds Audible as a tagger data source and provides other features for\n    managing audiobook collections.\n\nbeets-barcode_\n    Lets you scan or enter barcodes for physical media to search for their\n    metadata.\n\nbeetcamp_\n    Enables **bandcamp.com** autotagger with a fairly extensive amount of\n    metadata.\n\nbeetstream_\n    Server implementation of the `Subsonic API`_ specification, serving the\n    beets library and (:doc:`smartplaylist <smartplaylist>` plugin generated)\n    M3U playlists, allowing you to stream your music on a multitude of clients.\n\nbeets-bpmanalyser_\n    Analyses songs and calculates their tempo (BPM).\n\nbeets-check_\n    Automatically checksums your files to detect corruption.\n\n`A cmus plugin`_\n    Integrates with the cmus_ console music player.\n\nbeets-copyartifacts_\n    Helps bring non-music files along during import.\n\nbeets-describe_\n    Gives you the full picture of a single attribute of your library items.\n\ndrop2beets_\n    Automatically imports singles as soon as they are dropped in a folder (using\n    Linux's ``inotify``). You can also set a sub-folders hierarchy to set\n    flexible attributes by the way.\n\ndsedivec_\n    Has two plugins: ``edit`` and ``moveall``.\n\nbeets-filetote_\n    Helps bring non-music extra files, attachments, and artifacts during imports\n    and CLI file manipulation actions (``beet move``, etc.).\n\nbeets-fillmissing_\n    Interactively prompts you to fill in missing or incomplete metadata fields\n    for music tracks.\n\nbeets-follow_\n    Lets you check for new albums from artists you like.\n\nbeetFs_\n    Is a FUSE filesystem for browsing the music in your beets library. (Might be\n    out of date.)\n\nbeets-goingrunning_\n    Generates playlists to go with your running sessions.\n\nbeets-ibroadcast_\n    Uploads tracks to the iBroadcast_ cloud service.\n\nbeets-id3extract_\n    Maps arbitrary ID3 tags to beets custom fields.\n\nbeets-importreplace_\n    Lets you perform regex replacements on incoming metadata.\n\nbeets-jiosaavn_\n    Adds JioSaavn.com as a tagger data source.\n\nbeets-more_\n    Finds versions of indexed releases with more tracks, like deluxe and\n    anniversary editions.\n\nbeets-mosaic_\n    Generates a montage of a mosaic from cover art.\n\nbeets-mpd-utils_\n    Plugins to interface with MPD_. Comes with ``mpd_tracker`` (track play/skip\n    counts from MPD) and ``mpd_dj`` (auto-add songs to your queue.)\n\nbeets-noimport_\n    Adds and removes directories from the incremental import skip list.\n\nbeets-originquery_\n    Augments MusicBrainz queries with locally-sourced data to improve autotagger\n    results.\n\nbeets-plexsync_\n    Allows you to sync your Plex library with your beets library, create smart\n    playlists in Plex, and import online playlists (from services like Spotify)\n    into Plex.\n\nbeets-setlister_\n    Generate playlists from the setlists of a given artist.\n\nbeet-summarize_\n    Can compute lots of counts and statistics about your music library.\n\nbeets-usertag_\n    Lets you use keywords to tag and organize your music.\n\nbeets-webm3u_\n    Serves the (:doc:`smartplaylist <smartplaylist>` plugin generated) M3U\n    playlists via HTTP.\n\nbeets-webrouter_\n    Serves multiple beets webapps (e.g. :doc:`web <web>`, beets-webm3u_,\n    beetstream_, :doc:`aura <aura>`) using a single command/process/host/port,\n    each under a different path.\n\nwhatlastgenre_\n    Fetches genres from various music sites.\n\nbeets-xtractor_\n    Extracts low- and high-level musical information from your songs.\n\nbeets-ydl_\n    Downloads audio from youtube-dl sources and import into beets.\n\nbeets-ytimport_\n    Download and import your liked songs from YouTube into beets.\n\nbeets-yearfixer_\n    Attempts to fix all missing ``original_year`` and ``year`` fields.\n\nbeets-youtube_\n    Adds YouTube Music as a tagger data source.\n\n.. _a cmus plugin: https://github.com/coolkehon/beets/blob/master/beetsplug/cmus.py\n\n.. _beet-amazon: https://github.com/jmwatte/beet-amazon\n\n.. _beet-musicbrainz-collection: https://github.com/jeffayle/Beet-MusicBrainz-Collection/\n\n.. _beet-summarize: https://github.com/steven-murray/beet-summarize\n\n.. _beetcamp: https://github.com/snejus/beetcamp\n\n.. _beetfs: https://github.com/jbaiter/beetfs\n\n.. _beets-alternatives: https://github.com/geigerzaehler/beets-alternatives\n\n.. _beets-artistcountry: https://github.com/agrausem/beets-artistcountry\n\n.. _beets-audible: https://github.com/Neurrone/beets-audible\n\n.. _beets-autofix: https://github.com/adamjakab/BeetsPluginAutofix\n\n.. _beets-autogenre: https://github.com/mgoltzsche/beets-autogenre\n\n.. _beets-barcode: https://github.com/8h2a/beets-barcode\n\n.. _beets-bpmanalyser: https://github.com/adamjakab/BeetsPluginBpmAnalyser\n\n.. _beets-check: https://github.com/geigerzaehler/beets-check\n\n.. _beets-copyartifacts: https://github.com/adammillerio/beets-copyartifacts\n\n.. _beets-describe: https://github.com/adamjakab/BeetsPluginDescribe\n\n.. _beets-filetote: https://github.com/gtronset/beets-filetote\n\n.. _beets-fillmissing: https://github.com/amiv1/beets-fillmissing\n\n.. _beets-follow: https://github.com/nolsto/beets-follow\n\n.. _beets-goingrunning: https://pypi.org/project/beets-goingrunning\n\n.. _beets-ibroadcast: https://github.com/ctrueden/beets-ibroadcast\n\n.. _beets-id3extract: https://github.com/bcotton/beets-id3extract\n\n.. _beets-importreplace: https://github.com/edgars-supe/beets-importreplace\n\n.. _beets-jiosaavn: https://github.com/arsaboo/beets-jiosaavn\n\n.. _beets-more: https://forgejo.sny.sh/sun/beetsplug/src/branch/main/more\n\n.. _beets-mosaic: https://github.com/SusannaMaria/beets-mosaic\n\n.. _beets-mpd-utils: https://github.com/thekakkun/beets-mpd-utils\n\n.. _beets-noimport: https://gitlab.com/tiago.dias/beets-noimport\n\n.. _beets-originquery: https://github.com/x1ppy/beets-originquery\n\n.. _beets-plexsync: https://github.com/arsaboo/beets-plexsync\n\n.. _beets-setlister: https://github.com/tomjaspers/beets-setlister\n\n.. _beets-usertag: https://github.com/edgars-supe/beets-usertag\n\n.. _beets-webm3u: https://github.com/mgoltzsche/beets-webm3u\n\n.. _beets-webrouter: https://github.com/mgoltzsche/beets-webrouter\n\n.. _beets-xtractor: https://github.com/adamjakab/BeetsPluginXtractor\n\n.. _beets-ydl: https://github.com/vmassuchetto/beets-ydl\n\n.. _beets-yearfixer: https://github.com/adamjakab/BeetsPluginYearFixer\n\n.. _beets-youtube: https://github.com/arsaboo/beets-youtube\n\n.. _beets-ytimport: https://github.com/mgoltzsche/beets-ytimport\n\n.. _beetstream: https://github.com/BinaryBrain/Beetstream\n\n.. _cmus: https://sourceforge.net/projects/cmus/\n\n.. _drop2beets: https://github.com/martinkirch/drop2beets\n\n.. _dsedivec: https://github.com/dsedivec/beets-plugins\n\n.. _ibroadcast: https://ibroadcast.com/\n\n.. _subsonic api: https://www.subsonic.org/pages/api.jsp\n\n.. _whatlastgenre: https://github.com/YetAnotherNerd/whatlastgenre/tree/master/plugin/beets\n"
  },
  {
    "path": "docs/plugins/info.rst",
    "content": "Info Plugin\n===========\n\nThe ``info`` plugin provides a command that dumps the current tag values for any\nfile format supported by beets. It works like a supercharged version of mp3info_\nor id3v2_.\n\nEnable the ``info`` plugin in your configuration (see :ref:`using-plugins`) and\nthen type:\n\n::\n\n    $ beet info /path/to/music.flac\n\nand the plugin will enumerate all the tags in the specified file. It also\naccepts multiple filenames in a single command-line.\n\nYou can also enter a :doc:`query </reference/query>` to inspect music from your\nlibrary:\n\n::\n\n    $ beet info beatles\n\nIf you just want to see specific properties you can use the ``--include-keys``\noption to filter them. The argument is a comma-separated list of field names.\nFor example:\n\n::\n\n    $ beet info -i 'title,mb_artistid' beatles\n\nWill only show the ``title`` and ``mb_artistid`` properties. You can add the\n``-i`` option multiple times to the command line.\n\nAdditional command-line options include:\n\n- ``--library`` or ``-l``: Show data from the library database instead of the\n  files' tags.\n- ``--album`` or ``-a``: Show data from albums instead of tracks (implies\n  ``--library``).\n- ``--summarize`` or ``-s``: Merge all the information from multiple files into\n  a single list of values. If the tags differ across the files, print\n  ``[various]``.\n- ``--format`` or ``-f``: Specify a specific format with which to print every\n  item. This uses the same template syntax as beets’ :doc:`path formats\n  </reference/pathformat>`.\n- ``--keys-only`` or ``-k``: Show the name of the tags without the values.\n\n.. _id3v2: https://sourceforge.net/projects/id3v2/\n\n.. _mp3info: https://www.ibiblio.org/mp3info/\n"
  },
  {
    "path": "docs/plugins/inline.rst",
    "content": "Inline Plugin\n=============\n\nThe ``inline`` plugin lets you use Python to customize your path formats. Using\nit, you can define template fields in your beets configuration file and refer to\nthem from your template strings in the ``paths:`` section (see\n:doc:`/reference/config/`).\n\nTo use the ``inline`` plugin, enable it in your configuration (see\n:ref:`using-plugins`). Then, make a ``item_fields:`` block in your config file.\nUnder this key, every line defines a new template field; the key is the name of\nthe field (you'll use the name to refer to the field in your templates) and the\nvalue is a Python expression or function body. The Python code has all of a\ntrack's fields in scope, so you can refer to any normal attributes (such as\n``artist`` or ``title``) as Python variables.\n\nHere are a couple of examples of expressions:\n\n::\n\n    item_fields:\n        initial: albumartist[0].upper() + u'.'\n        disc_and_track: f\"{disc:02d}.{track:02d}\" if disctotal > 1 else f\"{track:02d}\"\n\nNote that YAML syntax allows newlines in values if the subsequent lines are\nindented.\n\nThese examples define ``$initial`` and ``$disc_and_track`` fields that can be\nreferenced in path templates like so:\n\n::\n\n    paths:\n        default: $initial/$artist/$album%aunique{}/$disc_and_track $title\n\nBlock Definitions\n-----------------\n\nIf you need to use statements like ``import``, you can write a Python function\nbody instead of a single expression. In this case, you'll need to ``return`` a\nresult for the value of the path field, like so:\n\n::\n\n    item_fields:\n        filename: |\n            import os\n            from beets.util import bytestring_path\n            return bytestring_path(os.path.basename(path))\n\nYou might want to use the YAML syntax for \"block literals,\" in which a leading\n``|`` character indicates a multi-line block of text.\n\nAlbum Fields\n------------\n\nThe above examples define fields for *item* templates, but you can also define\nfields for *album* templates. Use the ``album_fields`` configuration section. In\nthis context, all existing album fields are available as variables along with\n``items``, which is a list of items in the album.\n\nThis example defines a ``$bitrate`` field for albums as the average of the\ntracks' fields:\n\n::\n\n    album_fields:\n        bitrate: |\n            total = 0\n            for item in items:\n                total += item.bitrate\n            return total / len(items)\n"
  },
  {
    "path": "docs/plugins/ipfs.rst",
    "content": "IPFS Plugin\n===========\n\nThe ``ipfs`` plugin makes it easy to share your library and music with friends.\nThe plugin uses ipfs_ for storing the library and file content.\n\n.. _ipfs: https://about.ipfs.io/\n\nInstallation\n------------\n\nThis plugin requires go-ipfs_ to be running as a daemon and that the associated\n``ipfs`` command is on the user's ``$PATH``.\n\n.. _go-ipfs: https://github.com/ipfs/kubo\n\nOnce you have the client installed, enable the ``ipfs`` plugin in your\nconfiguration (see :ref:`using-plugins`).\n\nUsage\n-----\n\nThis plugin can store and retrieve music individually, or it can share entire\nlibrary databases.\n\nAdding\n~~~~~~\n\nTo add albums to ipfs, making them shareable, use the ``-a`` or ``--add`` flag.\nIf used without arguments it will add all albums in the local library. When\nadded, all items and albums will get an \"ipfs\" field in the database containing\nthe hash of that specific file/folder. Newly imported albums will be added\nautomatically to ipfs by default (see below).\n\nRetrieving\n~~~~~~~~~~\n\nYou can give the ipfs hash for some music to a friend. They can get that album\nfrom ipfs, and import it into beets, using the ``-g`` or ``--get`` flag. If the\nargument passed to the ``-g`` flag isn't an ipfs hash, it will be used as a\nquery instead, getting all albums matching the query.\n\nSharing Libraries\n~~~~~~~~~~~~~~~~~\n\nUsing the ``-p`` or ``--publish`` flag, a copy of the local library will be\npublished to ipfs. Only albums/items with ipfs records in the database will\npublished, and local paths will be stripped from the library. A hash of the\nlibrary will be returned to the user.\n\nA friend can then import this remote library by using the ``-i`` or ``--import``\nflag. To tag an imported library with a specific name by passing a name as the\nsecond argument to ``-i,`` after the hash. The content of all remote libraries\nwill be combined into an additional library as long as the content doesn't\nalready exist in the joined library.\n\nWhen remote libraries has been imported you can search them by using the ``-l``\nor ``--list`` flag. The hash of albums matching the query will be returned, and\nthis can then be used with ``-g`` to fetch and import the album to the local\nlibrary.\n\nIpfs can be mounted as a FUSE file system. This means that music in a remote\nlibrary can be streamed directly, without importing them to the local library\nfirst. If the ``/ipfs`` folder is mounted then matching queries will be sent to\nthe :doc:`/plugins/play` using the ``-m`` or ``--play`` flag.\n\nConfiguration\n-------------\n\nThe ipfs plugin will automatically add imported albums to ipfs and add those\nhashes to the database. This can be turned off by setting the ``auto`` option in\nthe ``ipfs:`` section of the config to ``no``.\n\nIf the setting ``nocopy`` is true (defaults false) then the plugin will pass the\n``--nocopy`` option when adding things to ipfs. If the filestore option of ipfs\nis enabled this will mean files are neither removed from beets nor copied\nsomewhere else.\n"
  },
  {
    "path": "docs/plugins/keyfinder.rst",
    "content": "Key Finder Plugin\n=================\n\nThe ``keyfinder`` plugin uses either the KeyFinder_ or keyfinder-cli_ program to\ndetect the musical key of a track from its audio data and store it in the\n``initial_key`` field of your database. It does so automatically when importing\nmusic or through the ``beet keyfinder [QUERY]`` command.\n\nTo use the ``keyfinder`` plugin, enable it in your configuration (see\n:ref:`using-plugins`).\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``keyfinder:`` section in your configuration\nfile. The available options are:\n\n- **auto**: Analyze every file on import. Otherwise, you need to use the ``beet\n  keyfinder`` command explicitly. Default: ``yes``\n- **bin**: The name of the program use for key analysis. You can use either\n  KeyFinder_ or keyfinder-cli_. If you installed the KeyFinder GUI on a Mac, for\n  example, you want something like\n  ``/Applications/KeyFinder.app/Contents/MacOS/KeyFinder``. If using\n  keyfinder-cli_, the binary must be named ``keyfinder-cli``. Default:\n  ``KeyFinder`` (i.e., search for the program in your ``$PATH``)..\n- **overwrite**: Calculate a key even for files that already have an\n  ``initial_key`` value. Default: ``no``.\n\n.. _keyfinder: https://www.ibrahimshaath.co.uk/keyfinder/\n\n.. _keyfinder-cli: https://github.com/EvanPurkhiser/keyfinder-cli/\n"
  },
  {
    "path": "docs/plugins/kodiupdate.rst",
    "content": "KodiUpdate Plugin\n=================\n\nThe ``kodiupdate`` plugin lets you automatically update Kodi_'s music library\nwhenever you change your beets library.\n\nTo use ``kodiupdate`` plugin, enable it in your configuration (see\n:ref:`using-plugins`). Then, you'll want to configure the specifics of your Kodi\nhost. You can do that using a ``kodi:`` section in your ``config.yaml``, which\nlooks like this:\n\n::\n\n    kodi:\n        host: localhost\n        port: 8080\n        user: kodi\n        pwd: kodi\n\nTo update multiple Kodi instances, specify them as an array:\n\n::\n\n    kodi:\n      - host: x.x.x.x\n        port: 8080\n        user: kodi\n        pwd: kodi\n      - host: y.y.y.y\n        port: 8081\n        user: kodi2\n        pwd: kodi2\n\nTo use the ``kodiupdate`` plugin, first enable it in your configuration (see\n:ref:`using-plugins`). Then, install ``beets`` with ``kodiupdate`` extra\n\n.. code-block:: bash\n\n    pip install \"beets[kodiupdate]\"\n\nYou'll also need to enable JSON-RPC in Kodi.\n\nIn Kodi's interface, navigate to System/Settings/Network/Services and choose\n\"Allow control of Kodi via HTTP.\"\n\nWith that all in place, you'll see beets send the \"update\" command to your Kodi\nhost every time you change your beets library.\n\n.. _kodi: https://kodi.tv/\n\nConfiguration\n-------------\n\nThe available options under the ``kodi:`` section are:\n\n- **host**: The Kodi host name. Default: ``localhost``\n- **port**: The Kodi host port. Default: 8080\n- **user**: The Kodi host user. Default: ``kodi``\n- **pwd**: The Kodi host password. Default: ``kodi``\n"
  },
  {
    "path": "docs/plugins/lastgenre.rst",
    "content": "LastGenre Plugin\n================\n\nThe ``lastgenre`` plugin fetches *tags* from Last.fm_ and assigns them as genres\nto your albums and items.\n\n.. _last.fm: https://www.last.fm/\n\nInstallation\n------------\n\nTo use the ``lastgenre`` plugin, first enable it in your configuration (see\n:ref:`using-plugins`). Then, install ``beets`` with ``lastgenre`` extra\n\n.. code-block:: bash\n\n    pip install \"beets[lastgenre]\"\n\nUsage\n-----\n\nThe plugin chooses genres based on a *whitelist*, meaning that only certain tags\ncan be considered genres. This way, tags like \"my favorite music\" or \"seen live\"\nwon't be considered genres. The plugin ships with a fairly extensive `internal\nwhitelist`_, but you can set your own in the config file using the ``whitelist``\nconfiguration value or forgo a whitelist altogether by setting the option to\n``no``.\n\nThe genre list file should contain one genre per line. Blank lines are ignored.\nFor the curious, the default genre list is generated by a `script that scrapes\nWikipedia`_.\n\n.. _internal whitelist: https://raw.githubusercontent.com/beetbox/beets/master/beetsplug/lastgenre/genres.txt\n\n.. _script that scrapes wikipedia: https://gist.github.com/sampsyo/1241307\n\nCanonicalization\n~~~~~~~~~~~~~~~~\n\nThe plugin can also *canonicalize* genres, meaning that more obscure genres can\nbe turned into coarser-grained ones that are present in the whitelist. This\nworks using a `tree of nested genre names`_, represented using YAML_, where the\nleaves of the tree represent the most specific genres.\n\nThe most common way to use this would be with a custom whitelist containing only\na desired subset of genres. Consider for a example this minimal whitelist:\n\n::\n\n    rock\n    heavy metal\n    pop\n\ntogether with the default genre tree. Then an item that has its genre specified\nas *viking metal* would actually be tagged as *heavy metal* because neither\n*viking metal* nor its parent *black metal* are in the whitelist. It always\ntries to use the most specific genre that's available in the whitelist.\n\nThe relevant subtree path in the default tree looks like this:\n\n::\n\n    - rock:\n        - heavy metal:\n            - black metal:\n                - viking metal\n\nConsidering that, it's not very useful to use the default whitelist (which\ncontains about any genre contained in the tree) with canonicalization because\nnothing would ever be matched to a more generic node since all the specific\nsubgenres are in the whitelist to begin with.\n\n.. _tree of nested genre names: https://raw.githubusercontent.com/beetbox/beets/master/beetsplug/lastgenre/genres-tree.yaml\n\n.. _yaml: https://yaml.org/\n\nGenre Source\n~~~~~~~~~~~~\n\nWhen looking up genres for albums or individual tracks, you can choose whether\nto use Last.fm tags on the album, the artist, or the track. For example, you\nmight want all the albums for a certain artist to carry the same genre. The\ndefault is \"album\". When set to \"track\", the plugin will fetch *both*\nalbum-level and track-level genres for your music when importing albums.\n\nMultiple Genres\n~~~~~~~~~~~~~~~\n\nBy default, the plugin chooses the most popular tag on Last.fm as a genre. If\nyou prefer to use a *list* of popular genre tags, you can increase the number of\nthe ``count`` config option.\n\nLists of up to *count* genres will be stored in the ``genres`` field as a list\nand written to your media files as separate genre tags.\n\nLast.fm_ provides a popularity factor, a.k.a. *weight*, for each tag ranging\nfrom 100 for the most popular tag down to 0 for the least popular. The plugin\nuses this weight to discard unpopular tags. The default is to ignore tags with a\nweight less then 10. You can change this by setting the ``min_weight`` config\noption.\n\nSpecific vs. Popular Genres\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nBy default, the plugin sorts genres by popularity. However, you can use the\n``prefer_specific`` option to override this behavior and instead sort genres by\nspecificity, as determined by your whitelist and canonicalization tree.\n\nFor instance, say you have both ``folk`` and ``americana`` in your whitelist and\ncanonicalization tree and ``americana`` is a leaf within ``folk``. If Last.fm\nreturns both of those tags, lastgenre is going to use the most popular, which is\noften the most generic (in this case ``folk``). By setting ``prefer_specific``\nto true, lastgenre would use ``americana`` instead.\n\nHandling pre-populated tags\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThe ``force``, ``keep_existing`` and ``whitelist`` options control how\npre-existing genres are handled.\n\nAs you would assume, setting ``force: no`` **won't touch pre-existing genre\ntags** and will only **fetch new genres for empty tags**. When ``force`` is\n``yes`` the setting of the ``whitelist`` option (as documented in Usage_)\napplies to any existing or newly fetched genres.\n\nThe following configurations are possible:\n\n**Setup 1** (default)\n\nAdd new last.fm genres when **empty**. Any present tags stay **untouched**.\n\n.. code-block:: yaml\n\n    force: no\n    keep_existing: no\n\n**Setup 2**\n\n**Overwrite all**. Only fresh last.fm genres remain.\n\n.. code-block:: yaml\n\n    force: yes\n    keep_existing: no\n\n**Setup 3**\n\n**Combine** genres in present tags with new ones (be aware of that with an\nenabled ``whitelist`` setting, of course some genres might get cleaned up -\nexisting genres take precedence over new ones though. To make sure any existing\ngenres remain, set ``whitelist: no``).\n\n.. code-block:: yaml\n\n    force: yes\n    keep_existing: yes\n\n.. attention::\n\n    If ``force`` is disabled the ``keep_existing`` option is simply ignored\n    (since ``force: no`` means ``not touching`` existing tags anyway).\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``lastgenre:`` section in your configuration\nfile. The available options are:\n\n- **auto**: Fetch genres automatically during import. Default: ``yes``.\n- **canonical**: Use a canonicalization tree. Setting this to ``yes`` will use a\n  built-in tree. You can also set it to a path, like the ``whitelist`` config\n  value, to use your own tree. Default: ``no`` (disabled).\n- **cleanup_existing**: This option only takes effect with ``force: no``,\n  Setting this to ``yes`` will result in cleanup of existing genres. That\n  includes canonicalization and whitelisting, if enabled. If no matching genre\n  can be determined, the ``fallback`` is used instead. Default: ``no``\n  (disabled).\n- **count**: Number of genres to fetch. Default: 1\n- **fallback**: A string to use as a fallback genre when no genre is found\n  ``or`` the original genre is not desired to be kept (``keep_existing: no``).\n  You can use the empty string ``''`` to reset the genre. Default: None.\n- **force**: By default, lastgenre will fetch new genres for empty tags only,\n  enable this option to always try to fetch new last.fm genres. Enable the\n  ``keep_existing`` option to combine existing and new genres. (see `Handling\n  pre-populated tags`_). Default: ``no``.\n- **keep_existing**: This option alters the ``force`` behavior. If both\n  ``force`` and ``keep_existing`` are enabled, existing genres are combined with\n  new ones. Depending on the ``whitelist`` setting, existing and new genres are\n  filtered accordingly. To ensure only fresh last.fm genres, disable this\n  option. (see `Handling pre-populated tags`_) Default: ``no``.\n- **min_weight**: Minimum popularity factor below which genres are discarded.\n  Default: 10.\n- **prefer_specific**: Sort genres by the most to least specific, rather than\n  most to least popular. Note that this option requires a ``canonical`` tree,\n  and if not configured it will automatically enable and use the built-in tree.\n  Default: ``no``.\n- **source**: Which entity to look up in Last.fm. Can be either ``artist``,\n  ``album`` or ``track``. Default: ``album``.\n- **whitelist**: The filename of a custom genre list, ``yes`` to use the\n  internal whitelist, or ``no`` to consider all genres valid. Default: ``yes``.\n- **title_case**: Convert the new tags to TitleCase before saving. Default:\n  ``yes``.\n\nRunning Manually\n----------------\n\nIn addition to running automatically on import, the plugin can also be run\nmanually from the command line. Use the command ``beet lastgenre [QUERY]`` to\nfetch genres for albums or items matching a certain query.\n\nBy default, ``beet lastgenre`` matches albums. To match individual tracks or\nsingletons, use the ``-A`` switch: ``beet lastgenre -A [QUERY]``.\n\nTo preview the changes that would be made without applying them, use the ``-p``\nor ``--pretend`` flag. This shows which genres would be set but does not write\nor store any changes.\n\nTo disable automatic genre fetching on import, set the ``auto`` config option to\nfalse.\n\nTuning Logs\n-----------\n\nTo enable tuning logs, run ``beet -vvv lastgenre ...`` or ``beet -vvv import\n...``. This enables additional messages at the ``DEBUG`` log level, showing for\nexample what data was received from last.fm at each stage of genre fetching\n(artist, album, and track levels) before any canonicalization or whitelist\nfiltering is applied. Tuning logs are useful for adjusting the plugin’s settings\nand understanding its behavior, though they can be quite verbose.\n"
  },
  {
    "path": "docs/plugins/lastimport.rst",
    "content": "LastImport Plugin\n=================\n\nThe ``lastimport`` plugin downloads play-count data from your Last.fm_ library\ninto beets' database. You can later create :doc:`smart playlists\n</plugins/smartplaylist>` by querying ``lastfm_play_count`` and do other fun\nstuff with this field.\n\n.. _last.fm: https://www.last.fm/\n\nInstallation\n------------\n\nTo use the ``lastimport`` plugin, first enable it in your configuration (see\n:ref:`using-plugins`). Then, install ``beets`` with ``lastimport`` extra\n\n.. code-block:: bash\n\n    pip install \"beets[lastimport]\"\n\nNext, add your Last.fm username to your beets configuration file:\n\n::\n\n    lastfm:\n        user: beetsfanatic\n\nImporting Play Counts\n---------------------\n\nSimply run ``beet lastimport`` and wait for the plugin to request tracks from\nLast.fm and match them to your beets library. (You will be notified of tracks in\nyour Last.fm profile that do not match any songs in your library.)\n\nThen, your matched tracks will be populated with the ``lastfm_play_count``\nfield, which you can use in any query or template. For example:\n\n::\n\n    $ beet ls -f '$title: $lastfm_play_count' lastfm_play_count:5..\n    Eple (Melody A.M.): 60\n\nTo see more information (namely, the specific play counts for matched tracks),\nuse the ``-v`` option.\n\n.. versionchanged:: 2.8.0\n\n    The ``play_count`` field was renamed to ``lastfm_play_count`` to avoid\n    confusion with ``play_count`` field populated by :doc:`mpdstats` plugin.\n\nConfiguration\n-------------\n\nAside from the required ``lastfm.user`` field, this plugin has some specific\noptions under the ``lastimport:`` section:\n\n- **per_page**: The number of tracks to request from the API at once. Default:\n  500.\n- **retry_limit**: How many times should we re-send requests to Last.fm on\n  failure? Default: 3.\n\nBy default, the plugin will use beets's own Last.fm API key. You can also\noverride it with your own API key:\n\n::\n\n    lastfm:\n        api_key: your_api_key\n"
  },
  {
    "path": "docs/plugins/limit.rst",
    "content": "Limit Query Plugin\n==================\n\n``limit`` is a plugin to limit a query to the first or last set of results. We\nalso provide a query prefix ``'<n'`` to inline the same behavior in the ``list``\ncommand. They are analogous to piping results:\n\n    $ beet [list|ls] [QUERY] | [head|tail] -n n\n\nThere are two provided interfaces:\n\n1. ``beet lslimit [--head n | --tail n] [QUERY]`` returns the head or tail of a\nquery\n\n2. ``beet [list|ls] [QUERY] '<n'`` returns the head of a query\n\nThere are two differences in behavior:\n\n1. The query prefix does not support tail.\n\n2. The query prefix could appear anywhere in the query but will only have the\nsame behavior as the ``lslimit`` command and piping to ``head`` when it appears\nlast.\n\nPerformance for the query previx is much worse due to the current\nsingleton-based implementation.\n\nSo why does the query prefix exist? Because it composes with any other\nquery-based API or plugin (see :doc:`/reference/query`). For example, you can\nuse the query prefix in ``smartplaylist`` (see :doc:`/plugins/smartplaylist`) to\nlimit the number of tracks in a smart playlist for applications like most played\nand recently added.\n\nConfiguration\n-------------\n\nEnable the ``limit`` plugin in your configuration (see :ref:`using-plugins`).\n\nExamples\n--------\n\nFirst 10 tracks\n\n.. code-block:: sh\n\n    $ beet ls | head -n 10\n    $ beet lslimit --head 10\n    $ beet ls '<10'\n\nLast 10 tracks\n\n.. code-block:: sh\n\n    $ beet ls | tail -n 10\n    $ beet lslimit --tail 10\n\n100 mostly recently released tracks\n\n.. code-block:: sh\n\n    $ beet lslimit --head 100 year- month- day-\n    $ beet ls year- month- day- '<100'\n    $ beet lslimit --tail 100 year+ month+ day+\n"
  },
  {
    "path": "docs/plugins/listenbrainz.rst",
    "content": ".. _listenbrainz:\n\nListenBrainz Plugin\n===================\n\nThe ListenBrainz plugin for beets allows you to interact with the ListenBrainz\nservice.\n\nConfiguration\n-------------\n\nTo enable the ListenBrainz plugin, add the following to your beets configuration\nfile (config.yaml_):\n\n.. code-block:: yaml\n\n    plugins:\n        - listenbrainz\n\nYou can then configure the plugin by providing your Listenbrainz token (see\nintructions here_) and username:\n\n::\n\n    listenbrainz:\n        token: TOKEN\n        username: LISTENBRAINZ_USERNAME\n\nUsage\n-----\n\nOnce the plugin is enabled, you can import the listening history using the\n``lbimport`` command in beets.\n\n.. _config.yaml: ../reference/config.rst\n\n.. _here: https://listenbrainz.readthedocs.io/en/latest/users/api/index.html#get-the-user-token\n"
  },
  {
    "path": "docs/plugins/loadext.rst",
    "content": "Load Extension Plugin\n=====================\n\nBeets uses an SQLite database to store and query library information, which has\nsupport for extensions to extend its functionality. The ``loadext`` plugin lets\nyou enable these SQLite extensions within beets.\n\nOne of the primary uses of this within beets is with the `\"ICU\" extension`_,\nwhich adds support for case insensitive querying of non-ASCII characters.\n\n.. _\"icu\" extension: https://www.sqlite.org/src/dir?ci=7461d2e120f21493&name=ext/icu\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``loadext`` section in your configuration file.\nThe section must consist of a list of paths to extensions to load, which looks\nlike this:\n\n.. code-block:: yaml\n\n    loadext:\n      - libicu\n\nIf a relative path is specified, it is resolved relative to the beets\nconfiguration directory.\n\nIf no file extension is specified, the default dynamic library extension for the\ncurrent platform will be used.\n\nBuilding the ICU extension\n--------------------------\n\nThis section is for **advanced** users only, and is not an in-depth guide on\nbuilding the extension.\n\nTo compile the ICU extension, you will need a few dependencies:\n\n    - gcc\n    - icu-devtools\n    - libicu\n    - libicu-dev\n    - libsqlite3-dev\n\nHere's roughly how to download, build and install the extension (although the\nspecifics may vary from system to system):\n\n.. code-block:: shell\n\n    $ wget https://sqlite.org/2019/sqlite-src-3280000.zip\n    $ unzip sqlite-src-3280000.zip\n    $ cd sqlite-src-3280000/ext/icu\n    $ gcc -shared -fPIC icu.c $(icu-config --ldflags) -o libicu.so\n    $ cp libicu.so ~/.config/beets\n"
  },
  {
    "path": "docs/plugins/lyrics.rst",
    "content": "Lyrics Plugin\n=============\n\nThe ``lyrics`` plugin fetches and stores song lyrics from databases on the Web.\nNamely, the current version of the plugin uses Genius.com_, Tekstowo.pl_,\nLRCLIB_ and, optionally, the Google Custom Search API.\n\n.. _genius.com: https://genius.com/\n\n.. _lrclib: https://lrclib.net/\n\n.. _tekstowo.pl: https://www.tekstowo.pl/\n\nInstall\n-------\n\nFirstly, enable ``lyrics`` plugin in your configuration (see\n:ref:`using-plugins`). Then, install ``beets`` with ``lyrics`` extra\n\n.. code-block:: bash\n\n    pip install \"beets[lyrics]\"\n\nFetch Lyrics During Import\n--------------------------\n\nWhen importing new files, beets will now fetch lyrics for files that don't\nalready have them. The lyrics will be stored in the beets database. The plugin\nalso sets a few useful flexible attributes:\n\n- ``lyrics_backend``: name of the backend that provided the lyrics\n- ``lyrics_url``: URL of the page where the lyrics were found\n- ``lyrics_language``: original language of the lyrics\n- ``lyrics_translation_language``: language of the lyrics translation (if\n  translation is enabled)\n\nIf the ``import.write`` config option is on, then the lyrics will also be\nwritten to the files' tags.\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``lyrics:`` section in your configuration file.\nDefault configuration:\n\n.. code-block:: yaml\n\n    lyrics:\n        auto: yes\n        translate:\n            api_key:\n            from_languages: []\n            to_language:\n        dist_thresh: 0.11\n        fallback: null\n        force: no\n        google_API_key: null\n        google_engine_ID: 009217259823014548361:lndtuqkycfu\n        print: no\n        sources: [lrclib, google, genius]\n        synced: no\n\nThe available options are:\n\n- **auto**: Fetch lyrics automatically during import.\n- **translate**:\n\n  - **api_key**: Api key to access your Azure Translator resource. (see\n    :ref:`lyrics-translation`)\n  - **from_languages**: By default all lyrics with a language other than\n    ``translate_to`` are translated. Use a list of language codes to restrict\n    them.\n  - **to_language**: Language code to translate lyrics to.\n\n- **dist_thresh**: The maximum distance between the artist and title combination\n  of the music file and lyrics candidate to consider them a match. Lower values\n  will make the plugin more strict, higher values will make it more lenient.\n  This does not apply to the ``lrclib`` backend as it matches durations.\n- **fallback**: By default, the file will be left unchanged when no lyrics are\n  found. Use the empty string ``''`` to reset the lyrics in such a case.\n- **force**: By default, beets won't fetch lyrics if the files already have\n  ones. To instead always fetch lyrics, set the ``force`` option to ``yes``.\n- **google_API_key**: Your Google API key (to enable the Google Custom Search\n  backend).\n- **google_engine_ID**: The custom search engine to use. Default: The `beets\n  custom search engine`_, which gathers an updated list of sources known to be\n  scrapeable.\n- **print**: Print lyrics to the console.\n- **sources**: List of sources to search for lyrics. An asterisk ``*`` expands\n  to all available sources. The ``google`` source will be automatically\n  deactivated if no ``google_API_key`` is setup. By default, ``musixmatch`` and\n  ``tekstowo`` are excluded because they block the beets User-Agent.\n- **synced**: Prefer synced lyrics over plain lyrics if a source offers them.\n  Currently ``lrclib`` is the only source that provides them. Using this option,\n  existing synced lyrics are not replaced by newly fetched plain lyrics (even\n  when ``force`` is enabled). To allow that replacement, disable ``synced``.\n\n.. _beets custom search engine: https://cse.google.com/cse?cx=009217259823014548361:lndtuqkycfu\n\nFetching Lyrics Manually\n------------------------\n\nThe ``lyrics`` command provided by this plugin fetches lyrics for items that\nmatch a query (see :doc:`/reference/query`). For example, ``beet lyrics magnetic\nfields absolutely cuckoo`` will get the lyrics for the appropriate Magnetic\nFields song, ``beet lyrics magnetic fields`` will get lyrics for all my tracks\nby that band, and ``beet lyrics`` will get lyrics for my entire library. The\nlyrics will be added to the beets database and, if ``import.write`` is on,\nembedded into files' metadata.\n\nThe ``-p, --print`` option to the ``lyrics`` command makes it print lyrics out\nto the console so you can view the fetched (or previously-stored) lyrics.\n\nThe ``-f, --force`` option forces the command to fetch lyrics, even for tracks\nthat already have lyrics.\n\nInversely, the ``-l, --local`` option restricts operations to lyrics that are\nlocally available, which show lyrics faster without using the network at all.\n\nRendering Lyrics into Other Formats\n-----------------------------------\n\nThe ``-r directory, --write-rest directory`` option renders all lyrics as\nreStructuredText_ (ReST) documents in ``directory``. That directory, in turn,\ncan be parsed by tools like Sphinx_ to generate HTML, ePUB, or PDF documents.\n\nMinimal ``conf.py`` and ``index.rst`` files are created the first time the\ncommand is run. They are not overwritten on subsequent runs, so you can safely\nmodify these files to customize the output.\n\nSphinx supports various builders_, see a few suggestions:\n\n.. admonition:: Build an HTML version\n\n    ::\n\n        sphinx-build -b html <dir> <dir>/html\n\n.. admonition:: Build an ePUB3 formatted file, usable on ebook readers\n\n    ::\n\n        sphinx-build -b epub3 <dir> <dir>/epub\n\n.. admonition:: Build a PDF file, which incidentally also builds a LaTeX file\n\n    ::\n\n        sphinx-build -b latex <dir> <dir>/latex && make -C <dir>/latex all-pdf\n\n.. _builders: https://www.sphinx-doc.org/en/master/usage/builders/index.html\n\n.. _restructuredtext: https://sourceforge.net/projects/docutils/\n\n.. _sphinx: https://www.sphinx-doc.org/en/master/\n\nActivate Google Custom Search\n-----------------------------\n\nYou need to `register for a Google API key\n<https://console.developers.google.com/>`__. Set the ``google_API_key``\nconfiguration option to your key.\n\nThen add ``google`` to the list of sources in your configuration (or use default\nlist, which includes it as long as you have an API key). If you use default\n``google_engine_ID``, we recommend limiting the sources to ``google`` as the\nother sources are already included in the Google results.\n\nOptionally, you can `define a custom search engine`_. Get your search engine's\ntoken and use it for your ``google_engine_ID`` configuration option. By default,\nbeets use a list of sources known to be scrapeable.\n\nNote that the Google custom search API is limited to 100 queries per day. After\nthat, the lyrics plugin will fall back on other declared data sources.\n\n.. _define a custom search engine: https://programmablesearchengine.google.com/about/\n\n.. _lyrics-translation:\n\nActivate On-the-Fly Translation\n-------------------------------\n\nWe use Azure to optionally translate your lyrics. To set up the integration,\nfollow these steps:\n\n1. `Create a Translator resource`_ on Azure.\n       Make sure the region of the translator resource is set to Global. You\n       will get 401 unauthorized errors if not. The region of the resource group\n       does not matter.\n2. `Obtain its API key`_.\n3. Add the API key to your configuration as ``translate.api_key``.\n4. Configure your target language using the ``translate.to_language`` option.\n\nFor example, with the following configuration\n\n.. code-block:: yaml\n\n    lyrics:\n      translate:\n        api_key: YOUR_TRANSLATOR_API_KEY\n        to_language: de\n\nYou should expect lyrics like this:\n\n::\n\n    Original verse / Ursprünglicher Vers\n    Some other verse / Ein anderer Vers\n\n.. _create a translator resource: https://learn.microsoft.com/en-us/azure/ai-services/translator/create-translator-resource\n\n.. _obtain its api key: https://learn.microsoft.com/en-us/python/api/overview/azure/ai-translation-text-readme?view=azure-python&preserve-view=true#get-an-api-key\n"
  },
  {
    "path": "docs/plugins/mbcollection.rst",
    "content": "MusicBrainz Collection Plugin\n=============================\n\nThe ``mbcollection`` plugin lets you submit your catalog to MusicBrainz to\nmaintain your `music collection`_ list there.\n\n.. _music collection: https://musicbrainz.org/doc/Collections\n\nTo begin, just enable the ``mbcollection`` plugin in your configuration (see\n:ref:`using-plugins`). Then, add your MusicBrainz username and password to your\n:doc:`configuration file </reference/config>` under a ``musicbrainz`` section:\n\n::\n\n    musicbrainz:\n        user: you\n        pass: seekrit\n\nThen, use the ``beet mbupdate`` command to send your albums to MusicBrainz. The\ncommand automatically adds all of your albums to the first collection it finds.\nIf you don't have a MusicBrainz collection yet, you may need to add one to your\nprofile first.\n\nThe command has one command-line option:\n\n- To remove albums from the collection which are no longer present in the beets\n  database, use the ``-r`` (``--remove``) flag.\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``mbcollection:`` section in your configuration\nfile. There is one option available:\n\n- **auto**: Automatically amend your MusicBrainz collection whenever you import\n  a new album. Default: ``no``.\n- **collection**: The MBID of which MusicBrainz collection to update. Default:\n  ``None``.\n- **remove**: Remove albums from collections which are no longer present in the\n  beets database. Default: ``no``.\n"
  },
  {
    "path": "docs/plugins/mbpseudo.rst",
    "content": "MusicBrainz Pseudo-Release Plugin\n=================================\n\nThe ``mbpseudo`` plugin can be used *instead of* the ``musicbrainz`` plugin to\nsearch for MusicBrainz pseudo-releases_ during the import process, which are\nadded to the normal candidates from the MusicBrainz search.\n\n.. _pseudo-releases: https://musicbrainz.org/doc/Style/Specific_types_of_releases/Pseudo-Releases\n\nThis is useful for releases whose title and track titles are written with a\nscript_ that can be translated or transliterated into a different one.\n\n.. _script: https://en.wikipedia.org/wiki/ISO_15924\n\nPseudo-releases will only be included if the initial search in MusicBrainz\nreturns releases whose script is *not* desired and whose relationships include\npseudo-releases with desired scripts.\n\nConfiguration\n-------------\n\nSince this plugin first searches for official releases from MusicBrainz, all\noptions from the ``musicbrainz`` plugin's :ref:`musicbrainz-config` are\nsupported, but they must be specified under ``mbpseudo`` in the configuration\nfile. Additionally, the configuration expects an array of scripts that are\ndesired for the pseudo-releases. For ``artist`` in particular, keep in mind that\neven pseudo-releases might specify it with the original script, so you should\nalso configure import :ref:`languages` to give artist aliases more priority.\nTherefore, the minimum configuration for this plugin looks like this:\n\n.. code-block:: yaml\n\n    plugins: mbpseudo # remove musicbrainz\n\n    import:\n        languages: en\n\n    mbpseudo:\n        scripts:\n        - Latn\n\nNote that the ``search_limit`` configuration applies to the initial search for\nofficial releases, and that the ``data_source`` in the database will be\n\"MusicBrainz\". Nevertheless, ``data_source_mismatch_penalty`` must also be\nspecified under ``mbpseudo`` if desired (see also\n:ref:`metadata-source-plugin-configuration`). An example with multiple data\nsources may look like this:\n\n.. code-block:: yaml\n\n    plugins: mbpseudo deezer\n\n    import:\n        languages: en\n\n    mbpseudo:\n        data_source_mismatch_penalty: 0\n        scripts:\n        - Latn\n\n    deezer:\n        data_source_mismatch_penalty: 0.2\n\nBy default, the data from the pseudo-release will be used to create a proposal\nthat is independent from the official release and sets all properties in its\nmetadata. It's possible to change the configuration so that some information\nfrom the pseudo-release is instead added as custom tags, keeping the metadata\nfrom the official release:\n\n.. code-block:: yaml\n\n    mbpseudo:\n        # other config not shown\n        custom_tags_only: yes\n\nThe default custom tags with this configuration are specified as mappings where\nthe keys define the tag names and the values define the pseudo-release property\nthat will be used to set the tag's value:\n\n.. code-block:: yaml\n\n    mbpseudo:\n        album_custom_tags:\n            album_transl: album\n            album_artist_transl: artist\n        track_custom_tags:\n            title_transl: title\n            artist_transl: artist\n\nNote that the information for each set of custom tags corresponds to different\nmetadata levels (album or track level), which is why ``artist`` appears twice\neven though it effectively references album artist and track artist\nrespectively.\n\nIf you want to modify any mapping under ``album_custom_tags`` or\n``track_custom_tags``, you must specify *everything* for that set of tags in\nyour configuration file because any customization replaces the whole dictionary\nof mappings for that level.\n\n.. note::\n\n    These custom tags are also added to the music files, not only to the\n    database.\n"
  },
  {
    "path": "docs/plugins/mbsubmit.rst",
    "content": "MusicBrainz Submit Plugin\n=========================\n\nThe ``mbsubmit`` plugin provides extra prompt choices when an import session\nfails to find a good enough match for a release. Additionally, it provides an\n``mbsubmit`` command that prints the tracks of the current album in a format\nthat is parseable by MusicBrainz's `track parser`_. The prompt choices are:\n\n- Print the tracks to stdout in a format suitable for MusicBrainz's `track\n  parser`_.\n- Open the program Picard_ with the unmatched folder as an input, allowing you\n  to start submitting the unmatched release to MusicBrainz with many input\n  fields already filled in, thanks to Picard reading the preexisting tags of the\n  files.\n\nFor the last option, Picard_ is assumed to be installed and available on the\nmachine including a ``picard`` executable. Picard developers list `download\noptions`_. `other GNU/Linux distributions`_ may distribute Picard via their\npackage manager as well.\n\n.. _download options: https://picard.musicbrainz.org/downloads/\n\n.. _other gnu/linux distributions: https://repology.org/project/picard-tagger/versions\n\n.. _picard: https://picard.musicbrainz.org/\n\n.. _track parser: https://wiki.musicbrainz.org/History:How_To_Parse_Track_Listings\n\nUsage\n-----\n\nEnable the ``mbsubmit`` plugin in your configuration (see :ref:`using-plugins`)\nand select one of the options mentioned above. Here the option ``Print tracks``\nchoice is demonstrated:\n\n::\n\n    No matching release found for 3 tracks.\n    For help, see: https://beets.readthedocs.org/en/latest/faq.html#nomatch\n    [U]se as-is, as Tracks, Group albums, Skip, Enter search, enter Id, aBort,\n    Print tracks, Open files with Picard? p\n    01. An Obscure Track - An Obscure Artist (3:37)\n    02. Another Obscure Track - An Obscure Artist (2:05)\n    03. The Third Track - Another Obscure Artist (3:02)\n\n    No matching release found for 3 tracks.\n    For help, see: https://beets.readthedocs.org/en/latest/faq.html#nomatch\n    [U]se as-is, as Tracks, Group albums, Skip, Enter search, enter Id, aBort,\n    Print tracks?\n\nYou can also run ``beet mbsubmit QUERY`` to print the track information for any\nalbum:\n\n::\n\n    $ beet mbsubmit album:\"An Obscure Album\"\n    01. An Obscure Track - An Obscure Artist (3:37)\n    02. Another Obscure Track - An Obscure Artist (2:05)\n    03. The Third Track - Another Obscure Artist (3:02)\n\nAs MusicBrainz currently does not support submitting albums programmatically,\nthe recommended workflow is to copy the output of the ``Print tracks`` choice\nand paste it into the parser that can be found by clicking on the \"Track Parser\"\nbutton on MusicBrainz \"Tracklist\" tab.\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``mbsubmit:`` section in your configuration\nfile. The following options are available:\n\n- **format**: The format used for printing the tracks, defined using the same\n  template syntax as beets’ :doc:`path formats </reference/pathformat>`.\n  Default: ``$track. $title - $artist ($length)``.\n- **threshold**: The minimum strength of the autotagger recommendation that will\n  cause the ``Print tracks`` choice to be displayed on the prompt. Default:\n  ``medium`` (causing the choice to be displayed for all albums that have a\n  recommendation of medium strength or lower). Valid values: ``none``, ``low``,\n  ``medium``, ``strong``.\n- **picard_path**: The path to the ``picard`` executable. Could be an absolute\n  path, and if not, ``$PATH`` is consulted. The default value is simply\n  ``picard``. Windows users will have to find and specify the absolute path to\n  their ``picard.exe``. That would probably be: ``C:\\Program Files\\MusicBrainz\n  Picard\\picard.exe``.\n\nPlease note that some values of the ``threshold`` configuration option might\nrequire other ``beets`` command line switches to be enabled in order to work as\nintended. In particular, setting a threshold of ``strong`` will only display the\nprompt if ``timid`` mode is enabled. You can find more information about how the\nrecommendation system works at :ref:`match-config`.\n"
  },
  {
    "path": "docs/plugins/mbsync.rst",
    "content": "MBSync Plugin\n=============\n\nThis plugin provides the ``mbsync`` command, which lets you synchronize metadata\nfor albums and tracks that have external data source IDs.\n\nThis is useful for syncing your library with online data or when changing\nconfiguration options that affect tag writing. If your music library already\ncontains correct tags, you can speed up the initial import by importing files\n\"as-is\" and then using ``mbsync`` to write tags according to your beets\nconfiguration.\n\nUsage\n-----\n\nEnable the ``mbsync`` plugin in your configuration (see :ref:`using-plugins`)\nand then run ``beet mbsync QUERY`` to fetch updated metadata for a part of your\ncollection (or omit the query to run over your whole library).\n\nID lookups use each item's stored ``data_source``. If a row has no\n``data_source``, ``mbsync`` falls back to ``MusicBrainz``.\n\nThis plugin treats albums and singletons (non-album tracks) separately. It first\nprocesses all matching singletons and then proceeds on to full albums. The same\nquery is used to search for both kinds of entities.\n\nThe command has a few command-line options:\n\n- To preview the changes that would be made without applying them, use the\n  ``-p`` (``--pretend``) flag.\n- By default, files will be moved (renamed) according to their metadata if they\n  are inside your beets library directory. To disable this, use the ``-M``\n  (``--nomove``) command-line option.\n- If you have the ``import.write`` configuration option enabled, then this\n  plugin will write new metadata to files' tags. To disable this, use the ``-W``\n  (``--nowrite``) option.\n- To customize the output of unrecognized items, use the ``-f`` (``--format``)\n  option. The default output is ``format_item`` or ``format_album`` for items\n  and albums, respectively.\n"
  },
  {
    "path": "docs/plugins/metasync.rst",
    "content": "MetaSync Plugin\n===============\n\nThis plugin provides the ``metasync`` command, which lets you fetch certain\nmetadata from other sources: for example, your favorite audio player.\n\nCurrently, the plugin supports synchronizing with the Amarok_ music player, and\nwith iTunes_. It can fetch the rating, score, first-played date, last-played\ndate, play count, and track uid from Amarok.\n\n.. _amarok: https://amarok.kde.org/\n\n.. _itunes: https://www.apple.com/itunes/\n\nInstallation\n------------\n\nEnable the ``metasync`` plugin in your configuration (see :ref:`using-plugins`).\n\nTo synchronize with Amarok, you'll need the dbus-python_ library. In such case,\ninstall ``beets`` with ``metasync`` extra\n\n.. code-block:: bash\n\n    pip install \"beets[metasync]\"\n\n.. _dbus-python: https://dbus.freedesktop.org/releases/dbus-python/\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``metasync:`` section in your configuration\nfile. The available options are:\n\n- **source**: A list of comma-separated sources to fetch metadata from. Set this\n  to \"amarok\" or \"itunes\" to enable synchronization with that player. Default:\n  empty\n\nThe follow subsections describe additional configure required for some players.\n\nitunes\n~~~~~~\n\nThe path to your iTunes library **xml** file has to be configured, e.g.:\n\n::\n\n    metasync:\n        source: itunes\n        itunes:\n            library: ~/Music/iTunes Library.xml\n\nPlease note the indentation.\n\nUsage\n-----\n\nRun ``beet metasync QUERY`` to fetch metadata from the configured list of\nsources.\n\nThe command has a few command-line options:\n\n- To preview the changes that would be made without applying them, use the\n  ``-p`` (``--pretend``) flag.\n- To specify temporary sources to fetch metadata from, use the ``-s``\n  (``--source``) flag with a comma-separated list of a sources.\n"
  },
  {
    "path": "docs/plugins/missing.rst",
    "content": "Missing Plugin\n==============\n\nThis plugin adds a new command, ``missing`` or ``miss``, which finds and lists\nmissing tracks for albums in your collection. Each album requires one network\ncall to album data source.\n\nUsage\n-----\n\nThe ``beet missing`` command fetches album information from the origin data\nsource and lists names of the **tracks** that are missing from your library.\nTrack-level checks use the album's stored ``data_source`` and fall back to\n``MusicBrainz`` when no source is stored.\n\nIt can also list the names of missing **albums** for each artist, although this\nis limited to albums from the MusicBrainz data source only.\n\nYou can customize the output format, show missing counts instead of track\ntitles, or display the total number of missing entities across your entire\nlibrary:\n\n::\n\n    -f FORMAT, --format=FORMAT\n                          print with custom FORMAT\n    -c, --count           count missing tracks per album\n    -t, --total           count totals across the entire library\n    -a, --album           show missing albums for artist instead of tracks for album\n    --release-type        show only missing albums of specified release type.\n                          You can provide this argument multiple times to\n                          specify multiple release types to filter to. If not\n                          provided, defaults to just the \"album\" release type.\n                          provided, it uses the configured\n                          ``missing.release_type`` (default: \"album\").\n\n…or by editing the corresponding configuration options.\n\n.. warning::\n\n    Option ``-c`` is ignored when used with ``-a``, and ``--release-type`` is\n    ignored when not used with ``-a``. Valid release types can be shown by\n    running ``beet missing -h``.\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``missing:`` section in your configuration file.\nThe available options are:\n\n- **count**: Print a count of missing tracks per album, with the global\n  ``format_album`` used for formatting. Default: ``no``.\n- **total**: Print a single count of missing tracks in all albums. Default:\n  ``no``.\n\nFormatting\n~~~~~~~~~~\n\n- This plugin uses global formatting options from the main configuration; see\n  :ref:`format_item` and :ref:`format_album`:\n- :ref:`format_item`: Used when listing missing tracks (default item format).\n- :ref:`format_album`: Used when showing counts (``-c``) or missing albums\n  (``-a``).\n\nHere's an example\n\n::\n\n    format_album: $albumartist - $album\n    format_item: $artist - $album - $title\n    missing:\n        count: no\n        total: no\n\nTemplate Fields\n---------------\n\nWith this plugin enabled, the ``$missing`` template field expands to the number\nof tracks missing from each album.\n\nExamples\n--------\n\nList all missing tracks in your collection:\n\n::\n\n    beet missing\n\nList all missing albums in your collection:\n\n::\n\n    beet missing -a\n\nList all missing tracks from 2008:\n\n::\n\n    beet missing year:2008\n\nPrint out a unicode histogram of the missing track years using spark_:\n\n::\n\n    beet missing -f '$year' | spark\n    ▆▁▆█▄▇▇▄▇▇▁█▇▆▇▂▄█▁██▂█▁▁██▁█▂▇▆▂▇█▇▇█▆▆▇█▇█▇▆██▂▇\n\nPrint out a listing of all albums with missing tracks, and respective counts:\n\n::\n\n    beet missing -c\n\nPrint out a count of the total number of missing tracks:\n\n::\n\n    beet missing -t\n\nList all missing albums of release type \"compilation\" in your collection:\n\n::\n\n    beet missing -a --release-type compilation\n\nList all missing albums of release type \"compilation\" and album in your\ncollection:\n\n::\n\n    beet missing -a --release-type compilation --release-type album\n\nCall this plugin from other beet commands:\n\n::\n\n    beet ls -a -f '$albumartist - $album: $missing'\n\n.. _spark: https://github.com/holman/spark\n"
  },
  {
    "path": "docs/plugins/mpdstats.rst",
    "content": "MPDStats Plugin\n===============\n\n``mpdstats`` is a plugin for beets that collects statistics about your listening\nhabits from MPD_. It collects the following information about tracks:\n\n- ``play_count``: The number of times you *fully* listened to this track.\n- ``skip_count``: The number of times you *skipped* this track.\n- ``last_played``: UNIX timestamp when you last played this track.\n- ``rating``: A rating based on ``play_count`` and ``skip_count``.\n\nTo gather these statistics it runs as an MPD client and watches the current\nstate of MPD. This means that ``mpdstats`` needs to be running continuously for\nit to work.\n\n.. _mpd: https://www.musicpd.org/\n\nInstalling Dependencies\n-----------------------\n\nThis plugin requires the python-mpd2 library in order to talk to the MPD server.\n\nTo use the ``mpdstats`` plugin, first enable it in your configuration (see\n:ref:`using-plugins`). Then, install ``beets`` with ``mpdstats`` extra\n\n    pip install \"beets[mpdstats]\"\n\nUsage\n-----\n\nUse the ``mpdstats`` command to fire it up:\n\n::\n\n    $ beet mpdstats\n\nConfiguration\n-------------\n\nTo configure the plugin, make an ``mpd:`` section in your configuration file.\nThe available options are:\n\n- **host**: The MPD server hostname. Default: The ``$MPD_HOST`` environment\n  variable if set, falling back to ``localhost`` otherwise.\n- **port**: The MPD server port. Default: The ``$MPD_PORT`` environment variable\n  if set, falling back to 6600 otherwise.\n- **password**: The MPD server password. Default: None.\n- **music_directory**: If your MPD library is at a different location from the\n  beets library (e.g., because one is mounted on a NFS share), specify the path\n  here.\n- **strip_path**: If your MPD library contains local path, specify the part to\n  remove here. Combining this with **music_directory** you can mangle MPD path\n  to match the beets library one. Default: The beets library directory.\n- **rating**: Enable rating updates. Default: ``yes``.\n- **rating_mix**: Tune the way rating is calculated (see below). Default: 0.75.\n- **played_ratio_threshold**: If a song was played for less than this percentage\n  of its duration it will be considered a skip. Default: 0.85\n\nA Word on Ratings\n-----------------\n\nRatings are calculated based on the *play_count*, *skip_count* and the last\n*action* (play or skip). It consists in one part of a *stable_rating* and in\nanother part on a *rolling_rating*. The *stable_rating* is calculated like this:\n\n::\n\n    stable_rating = (play_count + 1.0) / (play_count + skip_count + 2.0)\n\nSo if the *play_count* equals the *skip_count*, the *stable_rating* is always\n0.5. More *play_counts* adjust the rating up to 1.0. More *skip_counts* adjust\nit down to 0.0. One of the disadvantages of this rating system, is that it\ndoesn't really cover *recent developments*. e.g. a song that you loved last year\nand played over 50 times will keep a high rating even if you skipped it the last\n10 times. That's were the *rolling_rating* comes in.\n\nIf a song has been fully played, the *rolling_rating* is calculated like this:\n\n::\n\n    rolling_rating = old_rating + (1.0 - old_rating) / 2.0\n\nIf a song has been skipped, like this:\n\n::\n\n    rolling_rating = old_rating - old_rating / 2.0\n\nSo *rolling_rating* adapts pretty fast to *recent developments*. But it's too\nfast. Taking the example from above, your old favorite with 50 plays will get a\nnegative rating (<0.5) the first time you skip it. Also not good.\n\nTo take the best of both worlds, we mix the ratings together with the\n``rating_mix`` factor. A ``rating_mix`` of 0.0 means all *rolling* and 1.0 means\nall *stable*. We found 0.75 to be a good compromise, but fell free to play with\nthat.\n\nWarning\n-------\n\nThis has only been tested with MPD versions >= 0.16. It may not work on older\nversions. If that is the case, please report an issue_.\n\n.. _issue: https://github.com/beetbox/beets/issues\n"
  },
  {
    "path": "docs/plugins/mpdupdate.rst",
    "content": "MPDUpdate Plugin\n================\n\n``mpdupdate`` is a very simple plugin for beets that lets you automatically\nupdate MPD_'s index whenever you change your beets library.\n\n.. _mpd: https://www.musicpd.org/\n\nTo use ``mpdupdate`` plugin, enable it in your configuration (see\n:ref:`using-plugins`). Then, you'll probably want to configure the specifics of\nyour MPD server. You can do that using an ``mpd:`` section in your\n``config.yaml``, which looks like this:\n\n::\n\n    mpd:\n        host: localhost\n        port: 6600\n        password: seekrit\n\nWith that all in place, you'll see beets send the \"update\" command to your MPD\nserver every time you change your beets library.\n\nIf you want to communicate with MPD over a Unix domain socket instead over TCP,\njust give the path to the socket in the filesystem for the ``host`` setting.\n(Any ``host`` value starting with a slash or a tilde is interpreted as a domain\nsocket.)\n\nConfiguration\n-------------\n\nThe available options under the ``mpd:`` section are:\n\n- **host**: The MPD server name. Default: The ``$MPD_HOST`` environment variable\n  if set, falling back to ``localhost`` otherwise.\n- **port**: The MPD server port. Default: The ``$MPD_PORT`` environment variable\n  if set, falling back to 6600 otherwise.\n- **password**: The MPD server password. Default: None.\n"
  },
  {
    "path": "docs/plugins/musicbrainz.rst",
    "content": "MusicBrainz Plugin\n==================\n\nThe ``musicbrainz`` plugin extends the autotagger's search capabilities to\ninclude matches from the MusicBrainz_ database.\n\n.. _musicbrainz: https://musicbrainz.org/\n\nInstallation\n------------\n\nTo use the ``musicbrainz`` plugin, enable it in your configuration (see\n:ref:`using-plugins`)\n\n.. _musicbrainz-config:\n\nConfiguration\n-------------\n\nThis plugin can be configured like other metadata source plugins as described in\n:ref:`metadata-source-plugin-configuration`.\n\nDefault\n~~~~~~~\n\n.. code-block:: yaml\n\n    musicbrainz:\n        host: musicbrainz.org\n        https: no\n        ratelimit: 1\n        ratelimit_interval: 1.0\n        extra_tags: []\n        genres: no\n        genres_tag: genre\n        external_ids:\n            discogs: no\n            bandcamp: no\n            spotify: no\n            deezer: no\n            beatport: no\n            tidal: no\n        data_source_mismatch_penalty: 0.5\n        search_limit: 5\n\n.. conf:: host\n    :default: musicbrainz.org\n\n    The Web server hostname (and port, optionally) that will be contacted by beets.\n    You can use this to configure beets to use `your own MusicBrainz database\n    <https://musicbrainz.org/doc/MusicBrainz_Server/Setup>`__ instead of the\n    `main server`_.\n\n    The server must have search indices enabled (see `Building search indexes`_).\n\n    Example:\n\n    .. code-block:: yaml\n\n        musicbrainz:\n            host: localhost:5000\n\n.. conf:: https\n    :default: no\n\n    Makes the client use HTTPS instead of HTTP. This setting applies only to custom\n    servers. The official MusicBrainz server always uses HTTPS.\n\n.. conf:: ratelimit\n    :default: 1\n\n    Controls the number of Web service requests per second. This setting applies only\n    to custom servers. The official MusicBrainz server enforces a rate limit of 1\n    request per second.\n\n.. conf:: ratelimit_interval\n    :default: 1.0\n\n    The time interval (in seconds) for the rate limit. Only applies to custom servers.\n\n.. conf:: enabled\n    :default: yes\n\n    .. deprecated:: 2.4 Add ``musicbrainz`` to the ``plugins`` list instead.\n\n.. conf:: extra_tags\n    :default: []\n\n    By default, beets will use only the artist, album, and track count to query\n    MusicBrainz. Additional tags to be queried can be supplied with the\n    ``extra_tags`` setting.\n\n    This setting should improve the autotagger results if the metadata with the\n    given tags match the metadata returned by MusicBrainz.\n\n    Tags supported by this setting:\n\n    * ``alias`` (also search for release aliases matching the query)\n    * ``barcode``\n    * ``catalognum``\n    * ``country``\n    * ``label``\n    * ``media``\n    * ``tracks`` (number of tracks on the release)\n    * ``year``\n\n    Example:\n\n    .. code-block:: yaml\n\n        musicbrainz:\n            extra_tags: [alias, barcode, catalognum, country, label, media, tracks, year]\n\n.. conf:: genres\n    :default: no\n\n    Use MusicBrainz genre tags to populate (and replace if it's already set) the\n    ``genre`` tag. This will make it a list of all the genres tagged for the release\n    and the release-group on MusicBrainz, separated by \"; \" and sorted by the total\n    number of votes.\n\n.. conf:: external_ids\n\n    **Default**\n\n    .. code-block:: yaml\n\n        musicbrainz:\n            external_ids:\n                discogs: no\n                spotify: no\n                bandcamp: no\n                beatport: no\n                deezer: no\n                tidal: no\n\n    Set any of the ``external_ids`` options to ``yes`` to enable the MusicBrainz\n    importer to look for links to related metadata sources. If such a link is\n    available the release ID will be extracted from the URL provided and imported to\n    the beets library.\n\n    The library fields of the corresponding :ref:`autotagger_extensions` are used to\n    save the data as flexible attributes (``discogs_album_id``, ``bandcamp_album_id``, ``spotify_album_id``,\n    ``beatport_album_id``, ``deezer_album_id``, ``tidal_album_id``). On re-imports\n    existing data will be overwritten.\n\n.. conf:: genres_tag\n    :default: genre\n\n    Either ``genre`` or ``tag``. Specify ``genre`` to use just musicbrainz genre and\n    ``tag`` to use all user-supplied musicbrainz tags.\n\n.. include:: ./shared_metadata_source_config.rst\n\n.. _building search indexes: https://wiki.musicbrainz.org/History:Development/Search_server_setup\n\n.. _limited: https://musicbrainz.org/doc/XML_Web_Service/Rate_Limiting\n\n.. _main server: https://musicbrainz.org/\n"
  },
  {
    "path": "docs/plugins/parentwork.rst",
    "content": "ParentWork Plugin\n=================\n\nThe ``parentwork`` plugin fetches the work title, parent work title and parent\nwork composer from MusicBrainz.\n\nIn the MusicBrainz database, a recording can be associated with a work. A work\ncan itself be associated with another work, for example one being part of the\nother (what we call the *direct parent*). This plugin looks the work id from the\nlibrary and then looks up the direct parent, then the direct parent of the\ndirect parent and so on until it reaches the top. The work at the top is what we\ncall the *parent work*.\n\nThis plugin is especially designed for classical music. For classical music,\njust fetching the work title as in MusicBrainz is not satisfying, because\nMusicBrainz has separate works for, for example, all the movements of a\nsymphony. This plugin aims to solve this problem by also fetching the parent\nwork, which would be the whole symphony in this example.\n\nThe plugin can detect changes in ``mb_workid`` so it knows when to re-fetch\nother metadata, such as ``parentwork``. To do this, when it runs, it stores a\ncopy of ``mb_workid`` in the bookkeeping field ``parentwork_workid_current``. At\nany later run of ``beet parentwork`` it will check if the tags ``mb_workid`` and\n``parentwork_workid_current`` are still identical. If it is not the case, it\nmeans the work has changed and all the tags need to be fetched again.\n\nThis plugin adds seven tags:\n\n- **parentwork**: The title of the parent work.\n- **mb_parentworkid**: The MusicBrainz id of the parent work.\n- **parentwork_disambig**: The disambiguation of the parent work title.\n- **parent_composer**: The composer of the parent work.\n- **parent_composer_sort**: The sort name of the parent work composer.\n- **work_date**: The composition date of the work, or the first parent work that\n  has a composition date. Format: yyyy-mm-dd.\n- **parentwork_workid_current**: The MusicBrainz id of the work as it was when\n  the parentwork was retrieved. This tag exists only for internal bookkeeping,\n  to keep track of recordings whose works have changed.\n- **parentwork_date**: The composition date of the parent work.\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``parentwork:`` section in your configuration\nfile. The available options are:\n\n- **force**: As a default, ``parentwork`` only fetches work info for recordings\n  that do not already have a ``parentwork`` tag or where ``mb_workid`` differs\n  from ``parentwork_workid_current``. If ``force`` is enabled, it fetches it for\n  all recordings. Default: ``no``\n- **auto**: If enabled, automatically fetches works at import. It takes quite\n  some time, because beets is restricted to one MusicBrainz query per second.\n  Default: ``no``\n"
  },
  {
    "path": "docs/plugins/permissions.rst",
    "content": "Permissions Plugin\n==================\n\nThe ``permissions`` plugin allows you to set file permissions for imported music\nfiles and its directories.\n\nTo use the ``permissions`` plugin, enable it in your configuration (see\n:ref:`using-plugins`). Permissions will be adjusted automatically on import.\n\nConfiguration\n-------------\n\nTo configure the plugin, make an ``permissions:`` section in your configuration\nfile. The ``file`` config value therein uses **octal modes** to specify the\ndesired permissions. The default flags for files are octal 644 and 755 for\ndirectories.\n\nHere's an example:\n\n::\n\n    permissions:\n        file: 644\n        dir: 755\n"
  },
  {
    "path": "docs/plugins/play.rst",
    "content": "Play Plugin\n===========\n\nThe ``play`` plugin allows you to pass the results of a query to a music player\nin the form of an m3u playlist or paths on the command line.\n\nCommand Line Usage\n------------------\n\nTo use the ``play`` plugin, enable it in your configuration (see\n:ref:`using-plugins`). Then use it by invoking the ``beet play`` command with a\nquery. The command will create a temporary m3u file and open it using an\nappropriate application. You can query albums instead of tracks using the ``-a``\noption.\n\nBy default, the playlist is opened using the ``open`` command on OS X,\n``xdg-open`` on other Unixes, and ``start`` on Windows. To configure the\ncommand, you can use a ``play:`` section in your configuration file:\n\n::\n\n    play:\n        command: /Applications/VLC.app/Contents/MacOS/VLC\n\nYou can also specify additional space-separated options to command (like you\nwould on the command-line):\n\n::\n\n    play:\n        command: /usr/bin/command --option1 --option2 some_other_option\n\nWhile playing you'll be able to interact with the player if it is a command-line\noriented, and you'll get its output in real time.\n\nInteractive Usage\n-----------------\n\nThe ``play`` plugin can also be invoked during an import. If enabled, the plugin\nadds a ``plaY`` option to the prompt, so pressing ``y`` will execute the\nconfigured command and play the items currently being imported.\n\nOnce the configured command exits, you will be returned to the import decision\nprompt. If your player is configured to run in the background (in a\nclient/server setup), the music will play until you choose to stop it, and the\nimport operation continues immediately.\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``play:`` section in your configuration file.\nThe available options are:\n\n- **command**: The command used to open the playlist. Default: ``open`` on OS X,\n  ``xdg-open`` on other Unixes and ``start`` on Windows. Insert ``$args`` to use\n  the ``--args`` feature.\n- **relative_to**: If set, emit paths relative to this directory. Default: None.\n- **use_folders**: When using the ``-a`` option, the m3u will contain the paths\n  to each track on the matched albums. Enable this option to store paths to\n  folders instead. Default: ``no``.\n- **raw**: Instead of creating a temporary m3u playlist and then opening it,\n  simply call the command with the paths returned by the query as arguments.\n  Default: ``no``.\n- **warning_threshold**: Set the minimum number of files to play which will\n  trigger a warning to be emitted. If set to ``no``, warning are never issued.\n  Default: 100.\n- **bom**: Set whether or not a UTF-8 Byte Order Mark should be emitted into the\n  m3u file. If you're using foobar2000 or Winamp, this is needed. Default:\n  ``no``.\n\nOptional Arguments\n------------------\n\nThe ``--args`` (or ``-A``) flag to the ``play`` command lets you specify\nadditional arguments for your player command. Options are inserted after the\nconfigured ``command`` string and before the playlist filename.\n\nFor example, if you have the plugin configured like this:\n\n::\n\n    play:\n        command: mplayer -quiet\n\nand you occasionally want to shuffle the songs you play, you can type:\n\n::\n\n    $ beet play --args -shuffle\n\nto get beets to execute this command:\n\n::\n\n    mplayer -quiet -shuffle /path/to/playlist.m3u\n\ninstead of the default.\n\nIf you need to insert arguments somewhere other than the end of the ``command``\nstring, use ``$args`` to indicate where to insert them. For example:\n\n::\n\n    play:\n        command: mpv $args --playlist\n\nindicates that you need to insert extra arguments before specifying the\nplaylist.\n\nSome players require a different syntax. For example, with ``mpv`` the optional\n``$playlist`` variable can be used to match the syntax of the ``--playlist``\noption:\n\n::\n\n    play:\n        command: mpv $args --playlist=$playlist\n\nThe ``--yes`` (or ``-y``) flag to the ``play`` command will skip the warning\nmessage if you choose to play more items than the **warning_threshold** value\nusually allows.\n\nThe ``--randomize`` (or ``-R``) flag shuffles the order of playlist entries\nbefore passing it to the player:\n\n::\n\n    $ beet play --randomize my query\n\nNote on the Leakage of the Generated Playlists\n----------------------------------------------\n\nBecause the command that will open the generated ``.m3u`` files can be\narbitrarily configured by the user, beets won't try to delete those files. For\nthis reason, using this plugin will leave one or several playlist(s) in the\ndirectory selected to create temporary files (Most likely ``/tmp/`` on Unix-like\nsystems. See tempfile.tempdir_ in the Python docs.). Leaking those playlists\nuntil they are externally wiped could be an issue for privacy or storage\nreasons. If this is the case for you, you might want to use the ``raw`` config\noption described above.\n\n.. _tempfile.tempdir: https://docs.python.org/3/library/tempfile.html#tempfile.tempdir\n"
  },
  {
    "path": "docs/plugins/playlist.rst",
    "content": "Playlist Plugin\n===============\n\n``playlist`` is a plugin to use playlists in m3u format.\n\nTo use it, enable the ``playlist`` plugin in your configuration (see\n:ref:`using-plugins`). Then configure your playlists like this:\n\n::\n\n    playlist:\n        auto: no\n        relative_to: ~/Music\n        playlist_dir: ~/.mpd/playlists\n        forward_slash: no\n\nIt is possible to query the library based on a playlist by specifying its\nabsolute path:\n\n::\n\n    $ beet ls playlist:/path/to/someplaylist.m3u\n\nThe plugin also supports referencing playlists by name. The playlist is then\nsearched in the playlist_dir and the \".m3u\" extension is appended to the name:\n\n::\n\n    $ beet ls playlist:anotherplaylist\n\nA playlist query will use the paths found in the playlist file to match items in\nthe beets library. ``playlist:`` submits a regular beets :ref:`query <queries>`\nsimilar to a :ref:`specific fields query <fieldsquery>`. If you want the list in\nany particular order, you can use the standard beets query syntax for\n:ref:`sorting <query-sort>`:\n\n::\n\n    $ beet ls playlist:/path/to/someplaylist.m3u artist+ year+\n\nPlaylist queries do not reflect the original order in the m3u file.\n\nThe plugin can also update playlists in the playlist directory automatically\nevery time an item is moved or deleted. This can be controlled by the ``auto``\nconfiguration option.\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``playlist:`` section in your configuration\nfile. In addition to the ``playlists`` described above, the other configuration\noptions are:\n\n- **auto**: If this is set to ``yes``, then anytime an item in the library is\n  moved or removed, the plugin will update all playlists in the ``playlist_dir``\n  directory that contain that item to reflect the change. Default: ``no``\n- **playlist_dir**: Where to read playlist files from. Default: The current\n  working directory (i.e., ``'.'``).\n- **relative_to**: Interpret paths in the playlist files relative to a base\n  directory. Instead of setting it to a fixed path, it is also possible to set\n  it to ``playlist`` to use the playlist's parent directory or to ``library`` to\n  use the library directory. Default: ``library``\n- **forward_slash**: Forces forward slashes in the generated playlist files. If\n  you intend to use this plugin to generate playlists for MPD on Windows, set\n  this to yes. Default: Use system separator.\n"
  },
  {
    "path": "docs/plugins/plexupdate.rst",
    "content": "PlexUpdate Plugin\n=================\n\n``plexupdate`` is a very simple plugin for beets that lets you automatically\nupdate Plex_'s music library whenever you change your beets library.\n\nFirstly, install ``beets`` with ``plexupdate`` extra\n\n.. code-block:: console\n\n    pip install \"beets[plexupdate]\"\n\nThen, enable ``plexupdate`` plugin it in your configuration (see\n:ref:`using-plugins`). Optionally, configure the specifics of your Plex server.\nYou can do this using a ``plex:`` section in your ``config.yaml``:\n\n.. code-block:: yaml\n\n    plex:\n        host: \"localhost\"\n        port: 32400\n        token: \"TOKEN\"\n\nThe ``token`` key is optional: you'll need to use it when in a Plex Home (see\nPlex's own `documentation about tokens`_).\n\nWith that all in place, you'll see beets send the \"update\" command to your Plex\nserver every time you change your beets library.\n\n.. _documentation about tokens: https://support.plex.tv/articles/204059436-finding-an-authentication-token-x-plex-token/\n\n.. _plex: https://watch.plex.tv/\n\nConfiguration\n-------------\n\nThe available options under the ``plex:`` section are:\n\n- **host**: The Plex server name. Default: ``localhost``.\n- **port**: The Plex server port. Default: 32400.\n- **token**: The Plex Home token. Default: Empty.\n- **library_name**: The name of the Plex library to update. Default: ``Music``\n- **secure**: Use secure connections to the Plex server. Default: ``False``\n- **ignore_cert_errors**: Ignore TLS certificate errors when using secure\n  connections. Default: ``False``\n"
  },
  {
    "path": "docs/plugins/random.rst",
    "content": "Random Plugin\n=============\n\nThe ``random`` plugin provides a command that randomly selects tracks or albums\nfrom your library. This can be helpful if you need some help deciding what to\nlisten to.\n\nFirst, enable the plugin named ``random`` (see :ref:`using-plugins`). You'll\nthen be able to use the ``beet random`` command:\n\n.. code-block:: shell\n\n    beet random\n    >> Aesop Rock - None Shall Pass - The Harbor Is Yours\n\nUsage\n-----\n\nThe basic command selects and displays a single random track. Several options\nallow you to customize the selection:\n\n.. code-block:: shell\n\n    Usage: beet random [options]\n\n    Options:\n      -h, --help            show this help message and exit\n      -n NUMBER, --number=NUMBER\n                            number of objects to choose\n      -e, --equal-chance    each field has the same chance\n      -t TIME, --time=TIME  total length in minutes of objects to choose\n      --field=FIELD         field to use for equal chance sampling (default:\n                            albumartist)\n      -a, --album           match albums instead of tracks\n      -p PATH, --path=PATH  print paths for matched items or albums\n      -f FORMAT, --format=FORMAT\n                            print with custom format\n\nDetailed Options\n----------------\n\n``-n, --number=NUMBER``\n    Select multiple items at once. The default is 1.\n\n``-e, --equal-chance``\n    Give each distinct value of a field an equal chance of being selected. This\n    prevents artists with many albums/tracks from dominating the selection.\n\n    **Implementation note:** When this option is used, the plugin:\n\n    1. Groups items by the specified field\n    2. Shuffles items within each group\n    3. Randomly selects groups, then items from those groups\n    4. Continues until all groups are exhausted\n\n    Items without the specified field (``--field``) value are excluded from the\n    selection.\n\n``--field=FIELD``\n    Specify which field to use for equal chance sampling. Default is\n    ``albumartist``.\n\n``-t, --time=TIME``\n    Select items whose total duration (in minutes) is approximately equal to\n    TIME. The plugin will continue adding items until the total exceeds the\n    requested time.\n\n``-a, --album``\n    Operate on albums instead of tracks.\n\n``-p, --path``\n    Output filesystem paths instead of formatted metadata.\n\n``-f, --format=FORMAT``\n    Use a custom format string for output. See :doc:`/reference/query` for\n    format syntax.\n\nExamples\n--------\n\nSelect multiple items:\n\n.. code-block:: shell\n\n    # Select 5 random tracks\n    beet random -n 5\n\n    # Select 3 random albums\n    beet random -a -n 3\n\nControl selection fairness:\n\n.. code-block:: shell\n\n    # Ensure equal chance per artist (default field: albumartist)\n    beet random -e\n\n    # Ensure equal chance per genre\n    beet random -e --field genre\n\nSelect by total playtime:\n\n.. code-block:: shell\n\n    # Select tracks totaling 60 minutes (1 hour)\n    beet random -t 60\n\n    # Select albums totaling 120 minutes (2 hours)\n    beet random -a -t 120\n\nCustom output formats:\n\n.. code-block:: shell\n\n    # Print only artist and title\n    beet random -f '$artist - $title'\n\n    # Print file paths\n    beet random -p\n\n    # Print album paths\n    beet random -a -p\n"
  },
  {
    "path": "docs/plugins/replace.rst",
    "content": "Replace Plugin\n==============\n\nThe ``replace`` plugin provides a command that replaces the audio file of a\ntrack, while keeping the name and tags intact. It should save some time when you\nget the wrong version of a song.\n\nEnable the ``replace`` plugin in your configuration (see :ref:`using-plugins`)\nand then type:\n\n::\n\n    $ beet replace <query> <path>\n\nThe plugin will show you a list of files for you to pick from, and then ask for\nconfirmation.\n\nConsider using the ``replaygain`` command from the :doc:`/plugins/replaygain`\nplugin, if you usually use it during imports.\n"
  },
  {
    "path": "docs/plugins/replaygain.rst",
    "content": "ReplayGain Plugin\n=================\n\nThis plugin adds support for ReplayGain_, a technique for normalizing audio\nplayback levels.\n\n.. _replaygain: https://wiki.hydrogenaudio.org/index.php?title=ReplayGain\n\nInstallation\n------------\n\nThis plugin can use one of many backends to compute the ReplayGain values:\nGStreamer, mp3gain (and its cousins, aacgain and mp3rgain), Python Audio Tools\nor ffmpeg. ffmpeg and mp3gain can be easier to install. mp3gain supports fewer\naudio formats than the other backends.\n\nOnce installed, this plugin analyzes all files during the import process. This\ncan be a slow process; to instead analyze after the fact, disable automatic\nanalysis and use the ``beet replaygain`` command (see below).\n\nTo speed up analysis with some of the available backends, this plugin processes\ntracks or albums (when using the ``-a`` option) in parallel. By default, a\nsingle thread is used per logical core of your CPU.\n\nGStreamer\n~~~~~~~~~\n\nTo use GStreamer_ for ReplayGain analysis, you will of course need to install\nGStreamer and plugins for compatibility with your audio files. You will need at\nleast GStreamer 1.0 and `PyGObject 3.x`_ (a.k.a. ``python-gi``).\n\n.. _gstreamer: https://gstreamer.freedesktop.org/\n\n.. _pygobject 3.x: https://pygobject.gnome.org/\n\nThen, install ``beets`` with ``replaygain`` extra which installs ``GStreamer``\nbindings for Python\n\n.. code-block:: bash\n\n    pip install \"beets[replaygain]\"\n\nLastly, enable the ``replaygain`` plugin in your configuration (see\n:ref:`using-plugins`) and specify the GStreamer backend by adding this to your\nconfiguration file:\n\n::\n\n    replaygain:\n        backend: gstreamer\n\nThe GStreamer backend does not support parallel analysis.\n\nSupported ``command`` backends\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nIn order to use this backend, you will need to install a supported command-line\ntool:\n\n- mp3gain_ (MP3 only)\n- aacgain_ (MP3, AAC/M4A)\n- mp3rgain_ (MP3, AAC/M4A)\n\nmp3gain\n+++++++\n\n- On Linux, mp3gain_ is probably in your repositories. On Debian or Ubuntu, for\n  example, you can run ``apt-get install mp3gain``.\n- On Windows, download and install mp3gain_.\n\naacgain\n+++++++\n\n- On macOS, install via Homebrew_: ``brew install aacgain``.\n- For other platforms, download from aacgain_ or use a compatible fork if\n  available for your system.\n\nmp3rgain\n++++++++\n\nmp3rgain_ is a modern Rust rewrite of ``mp3gain`` that also supports AAC/M4A\nfiles. It addresses security vulnerability CVE-2019-18359 present in the\noriginal mp3gain and works on modern systems including Windows 11 and macOS with\nApple Silicon.\n\n- On macOS, install via Homebrew_: ``brew install mp3rgain``.\n- On Linux, install via Nix: ``nix-env -iA nixpkgs.mp3rgain`` or from your\n  distribution packaging (for example, AUR on Arch Linux).\n- On Windows, download and install mp3rgain_.\n\nConfiguration\n+++++++++++++\n\n.. code-block:: yaml\n\n    replaygain:\n        backend: command\n        command: # mp3rgain, mp3gain, or aacgain\n\nIf beets doesn't automatically find the command executable, you can configure\nthe path explicitly like so:\n\n.. code-block:: yaml\n\n    replaygain:\n        command: /Applications/MacMP3Gain.app/Contents/Resources/aacgain\n\n.. _aacgain: https://github.com/dgilman/aacgain\n\n.. _homebrew: https://brew.sh\n\n.. _mp3gain: https://sourceforge.net/projects/mp3gain/download.php\n\n.. _mp3rgain: https://github.com/M-Igashi/mp3rgain\n\nPython Audio Tools\n~~~~~~~~~~~~~~~~~~\n\nThis backend uses the `Python Audio Tools`_ package to compute ReplayGain for a\nrange of different file formats. The package is not available via PyPI; it must\nbe installed manually (only versions preceding 3.x are compatible).\n\nOn OS X, most of the dependencies can be installed with Homebrew_:\n\n::\n\n    brew install mpg123 mp3gain vorbisgain faad2 libvorbis\n\nThe Python Audio Tools backend does not support parallel analysis.\n\n.. _python audio tools: https://sourceforge.net/projects/audiotools/\n\nffmpeg\n~~~~~~\n\nThis backend uses ffmpeg to calculate EBU R128 gain values. To use it, install\nthe ffmpeg_ command-line tool and select the ``ffmpeg`` backend in your config\nfile.\n\n.. _ffmpeg: https://ffmpeg.org\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``replaygain:`` section in your configuration\nfile. The available options are:\n\n- **auto**: Enable ReplayGain analysis during import. Default: ``yes``.\n- **threads**: The number of parallel threads to run the analysis in. Overridden\n  by ``--threads`` at the command line. Default: # of logical CPU cores\n- **parallel_on_import**: Whether to enable parallel analysis during import. As\n  of now this ReplayGain data is not written to files properly, so this option\n  is disabled by default. If you wish to enable it, remember to run ``beet\n  write`` after importing to actually write to the imported files. Default:\n  ``no``\n- **backend**: The analysis backend; either ``gstreamer``, ``command``,\n  ``audiotools`` or ``ffmpeg``. Default: ``command``.\n- **overwrite**: On import, re-analyze files that already have ReplayGain tags.\n  Note that, for historical reasons, the name of this option is somewhat\n  unfortunate: It does not decide whether tags are written to the files (which\n  is controlled by the :ref:`import.write <config-import-write>` option).\n  Default: ``no``.\n- **targetlevel**: A number of decibels for the target loudness level for files\n  using ``REPLAYGAIN_`` tags. Default: ``89``.\n- **r128_targetlevel**: The target loudness level in decibels (i.e. ``<loudness\n  in LUFS> + 107``) for files using ``R128_`` tags. Default: 84 (Use ``83`` for\n  ATSC A/85, ``84`` for EBU R128 or ``89`` for ReplayGain 2.0.)\n- **r128**: A space separated list of formats that will use ``R128_`` tags with\n  integer values instead of the common ``REPLAYGAIN_`` tags with floating point\n  values. Requires the \"ffmpeg\" backend. Default: ``Opus``.\n- **per_disc**: Calculate album ReplayGain on disc level instead of album level.\n  Default: ``no``\n\nThese options only work with the \"command\" backend:\n\n- **command**: Name or path to your command backend of choice: either of\n  ``mp3gain``, ``aacgain`` or ``mp3rgain``.\n- **noclip**: Reduce the amount of ReplayGain adjustment to whatever amount\n  would keep clipping from occurring. Default: ``yes``.\n\nThis option only works with the \"ffmpeg\" backend:\n\n- **peak**: Either ``true`` (the default) or ``sample``. ``true`` is more\n  accurate but slower.\n\nManual Analysis\n---------------\n\nBy default, the plugin will analyze all items an albums as they are implemented.\nHowever, you can also manually analyze files that are already in your library.\nUse the ``beet replaygain`` command:\n\n::\n\n    $ beet replaygain [-Waf] [QUERY]\n\nThe ``-a`` flag analyzes whole albums instead of individual tracks. Provide a\nquery (see :doc:`/reference/query`) to indicate which items or albums to\nanalyze. Files that already have ReplayGain values are skipped unless ``-f`` is\nsupplied. Use ``-w`` (write tags) or ``-W`` (don't write tags) to control\nwhether ReplayGain tags are written into the music files, or stored in the beets\ndatabase only (the default is to use :ref:`the importer's configuration\n<config-import-write>`).\n\nTo execute with a different number of threads, call ``beet replaygain --threads\nN``:\n\n::\n\n    $ beet replaygain --threads N [-Waf] [QUERY]\n\nwith N any integer. To disable parallelism, use ``--threads 0``.\n\nReplayGain analysis is not fast, so you may want to disable it during import.\nUse the ``auto`` config option to control this:\n\n::\n\n    replaygain:\n        auto: no\n"
  },
  {
    "path": "docs/plugins/rewrite.rst",
    "content": "Rewrite Plugin\n==============\n\nThe ``rewrite`` plugin lets you easily substitute values in your templates and\npath formats. Specifically, it is intended to let you *canonicalize* names such\nas artists: for example, perhaps you want albums from The Jimi Hendrix\nExperience to be sorted into the same folder as solo Hendrix albums.\n\nTo use field rewriting, first enable the ``rewrite`` plugin (see\n:ref:`using-plugins`). Then, make a ``rewrite:`` section in your config file to\ncontain your rewrite rules. Each rule consists of a field name, a regular\nexpression pattern, and a replacement value. Rules are written ``fieldname\nregex: replacement``. For example, this line implements the Jimi Hendrix example\nabove:\n\n::\n\n    rewrite:\n        artist The Jimi Hendrix Experience: Jimi Hendrix\n\nThis will make ``$artist`` in your templates expand to \"Jimi Hendrix\" where it\nwould otherwise be \"The Jimi Hendrix Experience\".\n\nThe pattern is a case-insensitive regular expression. This means you can use\nordinary regular expression syntax to match multiple artists. For example, you\nmight use:\n\n::\n\n    rewrite:\n        artist .*jimi hendrix.*: Jimi Hendrix\n\nAs a convenience, the plugin applies patterns for the ``artist`` field to the\n``albumartist`` field as well. (Otherwise, you would probably want to duplicate\nevery rule for ``artist`` and ``albumartist``.)\n\nA word of warning: This plugin theoretically only applies to templates and path\nformats; it initially does not modify files' metadata tags or the values tracked\nby beets' library database, but since it *rewrites all field lookups*, it\nmodifies the file's metadata anyway. See comments in issue :bug:`2786`.\n\nAs an alternative to this plugin the :doc:`/plugins/substitute` could be used.\n"
  },
  {
    "path": "docs/plugins/scrub.rst",
    "content": "Scrub Plugin\n============\n\nThe ``scrub`` plugin lets you remove extraneous metadata from files' tags. If\nyou'd prefer never to see crufty tags that come from other tools, the plugin can\nautomatically remove all non-beets-tracked tags whenever a file's metadata is\nwritten to disk by removing the tag entirely before writing new data. The plugin\nalso provides a command that lets you manually remove files' tags.\n\nAutomatic Scrubbing\n-------------------\n\nTo automatically remove files' tags before writing new ones, enable ``scrub``\nplugin in your configuration (see :ref:`using-plugins`) and install ``beets``\nwith ``scrub`` extra\n\n.. code-block:: bash\n\n    pip install \"beets[scrub]\"\n\nWhen importing new files (with ``import.write`` turned on) or modifying files'\ntags with the ``beet modify`` command, beets will first strip all types of tags\nentirely and then write the database-tracked metadata to the file.\n\nThis behavior can be disabled with the ``auto`` config option (see below).\n\nManual Scrubbing\n----------------\n\nThe ``scrub`` command provided by this plugin removes tags from files and then\nrewrites their database-tracked metadata. To run it, just type ``beet scrub\nQUERY`` where ``QUERY`` matches the tracks to be scrubbed. Use this command with\ncaution, however, because any information in the tags that is out of sync with\nthe database will be lost.\n\nThe ``-W`` (or ``--nowrite``) option causes the command to just remove tags but\nnot restore any information. This will leave the files with no metadata\nwhatsoever.\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``scrub:`` section in your configuration file.\nThere is one option:\n\n- **auto**: Enable metadata stripping during import. Default: ``yes``.\n"
  },
  {
    "path": "docs/plugins/shared_metadata_source_config.rst",
    "content": ".. _data_source_mismatch_penalty:\n\n.. conf:: data_source_mismatch_penalty\n    :default: 0.5\n\n    Penalty applied when the data source of a\n    match candidate differs from the original source of your existing tracks. Any\n    decimal number between 0.0 and 1.0\n\n    This setting controls how much to penalize matches from different metadata\n    sources during import. The penalty is applied when beets detects that a match\n    candidate comes from a different data source than what appears to be the\n    original source of your music collection.\n\n    **Example configurations:**\n\n    .. code-block:: yaml\n\n        # Prefer MusicBrainz over Discogs when sources don't match\n        plugins: musicbrainz discogs\n\n        musicbrainz:\n            data_source_mismatch_penalty: 0.3  # Lower penalty = preferred\n        discogs:\n            data_source_mismatch_penalty: 0.8  # Higher penalty = less preferred\n\n    .. code-block:: yaml\n\n        # Do not penalise candidates from Discogs at all\n        plugins: musicbrainz discogs\n\n        musicbrainz:\n            data_source_mismatch_penalty: 0.5\n        discogs:\n            data_source_mismatch_penalty: 0.0\n\n    .. code-block:: yaml\n\n        # Disable cross-source penalties entirely\n        plugins: musicbrainz discogs\n\n        musicbrainz:\n            data_source_mismatch_penalty: 0.0\n        discogs:\n            data_source_mismatch_penalty: 0.0\n\n    .. tip::\n\n        The last configuration is equivalent to setting:\n\n        .. code-block:: yaml\n\n            match:\n                distance_weights:\n                    data_source: 0.0  # Disable data source matching\n\n.. conf:: source_weight\n    :default: 0.5\n\n    .. deprecated:: 2.5 Use `data_source_mismatch_penalty`_ instead.\n\n.. conf:: search_limit\n    :default: 5\n\n    Maximum number of search results to return.\n"
  },
  {
    "path": "docs/plugins/smartplaylist.rst",
    "content": "Smart Playlist Plugin\n=====================\n\n``smartplaylist`` is a plugin to generate smart playlists in m3u format based on\nbeets queries every time your library changes. This plugin is specifically\ncreated to work well with `MPD's`_ playlist functionality.\n\n.. _mpd's: https://www.musicpd.org/\n\nTo use it, enable the ``smartplaylist`` plugin in your configuration (see\n:ref:`using-plugins`). Then configure your smart playlists like the following\nexample:\n\n::\n\n    smartplaylist:\n        relative_to: ~/Music\n        playlist_dir: ~/.mpd/playlists\n        forward_slash: no\n        playlists:\n            - name: all.m3u\n              query: ''\n\n            - name: beatles.m3u\n              query: 'artist:Beatles'\n\nYou can generate as many playlists as you want by adding them to the\n``playlists`` section, using beets query syntax (see :doc:`/reference/query`)\nfor ``query`` and the file name to be generated for ``name``. The query will be\nsplit using shell-like syntax, so if you need to use spaces in the query, be\nsure to quote them (e.g., ``artist:\"The Beatles\"``). If you have existing files\nwith the same names, you should back them up---they will be overwritten when the\nplugin runs.\n\nFor more advanced usage, you can use template syntax (see\n:doc:`/reference/pathformat/`) in the ``name`` field. For example:\n\n::\n\n    - name: 'ReleasedIn$year.m3u'\n      query: 'year::201(0|1)'\n\nThis will query all the songs in 2010 and 2011 and generate the two playlist\nfiles ``ReleasedIn2010.m3u`` and ``ReleasedIn2011.m3u`` using those songs.\n\nYou can also gather the results of several queries by putting them in a list.\n(Items that match both queries are not included twice.) For example:\n\n::\n\n    - name: 'BeatlesUniverse.m3u'\n      query: ['artist:beatles', 'genre:\"beatles cover\"']\n\nNote that since beets query syntax is in effect, you can also use sorting\ndirectives:\n\n::\n\n    - name: 'Chronological Beatles'\n      query: 'artist:Beatles year+'\n    - name: 'Mixed Rock'\n      query: ['artist:Beatles year+', 'artist:\"Led Zeppelin\" bitrate+']\n\nThe former case behaves as expected, however please note that in the latter the\nsorts will be merged: ``year+ bitrate+`` will apply to both the Beatles and Led\nZeppelin. If that bothers you, please get in touch.\n\nFor querying albums instead of items (mainly useful with extensible fields), use\nthe ``album_query`` field. ``query`` and ``album_query`` can be used at the same\ntime. The following example gathers single items but also items belonging to\nalbums that have a ``for_travel`` extensible field set to 1:\n\n::\n\n    - name: 'MyTravelPlaylist.m3u'\n      album_query: 'for_travel:1'\n      query: 'for_travel:1'\n\nBy default, each playlist is automatically regenerated at the end of the session\nif an item or album it matches changed in the library database. To force\nregeneration, you can invoke it manually from the command line:\n\n::\n\n    $ beet splupdate\n\nThis will regenerate all smart playlists. You can also specify which ones you\nwant to regenerate:\n\n::\n\n    $ beet splupdate BeatlesUniverse.m3u MyTravelPlaylist\n\nYou can also use this plugin together with the :doc:`mpdupdate`, in order to\nautomatically notify MPD of the playlist change, by adding ``mpdupdate`` to the\n``plugins`` line in your config file *after* the ``smartplaylist`` plugin.\n\nWhile changing existing playlists in the beets configuration it can help to use\nthe ``--pretend`` option to find out if the edits work as expected. The results\nof the queries will be printed out instead of being written to the playlist\nfile.\n\n::\n\n    $ beet splupdate --pretend BeatlesUniverse.m3u\n\nThe ``pretend_paths`` configuration option sets whether the items should be\ndisplayed as per the user's ``format_item`` setting or what the file paths as\nthey would be written to the m3u file look like.\n\nIn case you want to export additional fields from the beets database into the\ngenerated playlists, you can do so by specifying them within the ``fields``\nconfiguration option and setting the ``output`` option to ``extm3u``. For\ninstance the following configuration exports the ``id`` and ``genre`` fields:\n\n::\n\n    smartplaylist:\n        playlist_dir: /data/playlists\n        relative_to: /data/playlists\n        output: extm3u\n        fields:\n            - id\n            - genres\n        playlists:\n            - name: all.m3u\n              query: ''\n\nValues of additional fields are URL-encoded. A resulting ``all.m3u`` file could\nlook as follows:\n\n::\n\n    #EXTM3U\n    #EXTINF:805 id=\"1931\" genres=\"Rock%3B%20Pop\",Led Zeppelin - Stairway to Heaven\n    ../music/singles/Led Zeppelin/Stairway to Heaven.mp3\n\nTo give a usage example, the webm3u_ and Beetstream_ plugins read the exported\n``id`` field, allowing you to serve your local m3u playlists via HTTP.\n\n.. _beetstream: https://github.com/BinaryBrain/Beetstream\n\n.. _webm3u: https://github.com/mgoltzsche/beets-webm3u\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``smartplaylist:`` section in your configuration\nfile. In addition to the ``playlists`` described above, the other configuration\noptions are:\n\n- **auto**: Regenerate the playlist after every database change. Default:\n  ``yes``.\n- **playlist_dir**: Where to put the generated playlist files. Default: The\n  current working directory (i.e., ``'.'``).\n- **dest_regen**: Regenerate the destination path as ``move`` or ``convert``\n  commands would do. This operation will happen before ``relative_to`` and\n  ``prefix``. Helpful to generate playlists compatible with the ``convert``\n  plugin when items have been imported with the ``-C -M`` options. Default:\n  ``false``.\n- **relative_to**: Generate paths in the playlist files relative to a base\n  directory. If you intend to use this plugin to generate playlists for MPD,\n  point this to your MPD music directory. Default: Use absolute paths.\n- **forward_slash**: Forces forward slashes in the generated playlist files. If\n  you intend to use this plugin to generate playlists for MPD on Windows, set\n  this to yes. Default: Use system separator.\n- **prefix**: Prepend this string to every path in the playlist file. For\n  example, you could use the URL for a server where the music is stored.\n  Default: empty string.\n- **urlencode**: URL-encode all paths. Default: ``no``.\n- **pretend_paths**: When running with ``--pretend``, show the actual file paths\n  that will be written to the m3u file. Default: ``false``.\n- **uri_format**: Template with an ``$id`` placeholder used generate a playlist\n  item URI, e.g. ``http://beets:8337/item/$id/file``. When this option is\n  specified, the local path-related options ``dest_regen``, ``prefix``,\n  ``relative_to``, ``forward_slash`` and ``urlencode`` are ignored.\n- **output**: Specify the playlist format: m3u|extm3u. Default ``m3u``.\n- **fields**: Specify the names of the additional item fields to export into the\n  playlist. This allows using e.g. the ``id`` field within other tools such as\n  the webm3u_ and Beetstream_ plugins. To use this option, you must set the\n  ``output`` option to ``extm3u``.\n\nFor many configuration options, there is a corresponding CLI option, e.g.\n``--playlist-dir``, ``--dest-regen``, ``--relative-to``, ``--prefix``,\n``--forward-slash``, ``--urlencode``, ``--uri-format``, ``--output``,\n``--pretend-paths``. CLI options take precedence over those specified within the\nconfiguration file.\n"
  },
  {
    "path": "docs/plugins/sonosupdate.rst",
    "content": "SonosUpdate Plugin\n==================\n\nThe ``sonosupdate`` plugin lets you automatically update Sonos_'s music library\nwhenever you change your beets library.\n\nTo use ``sonosupdate`` plugin, enable it in your configuration (see\n:ref:`using-plugins`).\n\nTo use the ``sonosupdate`` plugin, first enable it in your configuration (see\n:ref:`using-plugins`). Then, install ``beets`` with ``sonosupdate`` extra\n\n    pip install \"beets[sonosupdate]\"\n\nWith that all in place, you'll see beets send the \"update\" command to your Sonos\ncontroller every time you change your beets library.\n\n.. _sonos: https://www.sonos.com/\n"
  },
  {
    "path": "docs/plugins/spotify.rst",
    "content": "Spotify Plugin\n==============\n\nThe ``spotify`` plugin generates Spotify_ playlists from tracks in your library\nwith the ``beet spotify`` command using the `Spotify Search API`_.\n\nAlso, the plugin can use the Spotify Album_ and Track_ APIs to provide metadata\nmatches for the importer.\n\n.. _album: https://developer.spotify.com/documentation/web-api/reference/get-an-album\n\n.. _spotify: https://open.spotify.com/\n\n.. _spotify search api: https://developer.spotify.com/documentation/web-api/reference/search\n\n.. _track: https://developer.spotify.com/documentation/web-api/reference/get-track\n\nWhy Use This Plugin?\n--------------------\n\n- You're a Beets user and Spotify user already.\n- You have playlists or albums you'd like to make available in Spotify from\n  Beets without having to search for each artist/album/track.\n- You want to check which tracks in your library are available on Spotify.\n- You want to autotag music with metadata from the Spotify API.\n- You want to obtain track popularity and audio features (e.g., danceability)\n\nBasic Usage\n-----------\n\nFirst, enable the ``spotify`` plugin (see :ref:`using-plugins`). Then, use the\n``spotify`` command with a beets query:\n\n::\n\n    beet spotify [OPTIONS...] QUERY\n\nHere's an example:\n\n::\n\n    $ beet spotify \"In The Lonely Hour\"\n    Processing 14 tracks...\n    https://open.spotify.com/track/19w0OHr8SiZzRhjpnjctJ4\n    https://open.spotify.com/track/3PRLM4FzhplXfySa4B7bxS\n    [...]\n\nCommand-line options include:\n\n- ``-m MODE`` or ``--mode=MODE`` where ``MODE`` is either \"list\" or \"open\"\n  controls whether to print out the playlist (for copying and pasting) or open\n  it in the Spotify app. (See below.)\n- ``--show-failures`` or ``-f``: List the tracks that did not match a Spotify\n  ID.\n\nYou can enter the URL for an album or song on Spotify at the ``enter Id`` prompt\nduring import:\n\n::\n\n    Enter search, enter Id, aBort, eDit, edit Candidates, plaY? i\n    Enter release ID: https://open.spotify.com/album/2rFYTHFBLQN3AYlrymBPPA\n\nConfiguration\n-------------\n\nThis plugin can be configured like other metadata source plugins as described in\n:ref:`metadata-source-plugin-configuration`.\n\nDefault\n~~~~~~~\n\n.. code-block:: yaml\n\n    spotify:\n        mode: list\n        region_filter:\n        show_failures: no\n        tiebreak: popularity\n        regex: []\n        search_query_ascii: no\n        client_id: REDACTED\n        client_secret: REDACTED\n        tokenfile: spotify_token.json\n        data_source_mismatch_penalty: 0.5\n        search_limit: 5\n\n.. conf:: mode\n    :default: list\n\n    Controls how the playlist is output:\n\n    - ``list``: Print out the playlist as a list of links. This list can then\n      be pasted in to a new or existing Spotify playlist.\n    - ``open``: This mode actually sends a link to your default browser with\n      instructions to open Spotify with the playlist you created. Until this\n      has been tested on all platforms, it will remain optional.\n\n.. conf:: region_filter\n    :default:\n\n    A two-character country abbreviation, to limit results to that market.\n\n.. conf:: show_failures\n    :default: no\n\n    List each lookup that does not return a Spotify ID (and therefore cannot be\n    added to a playlist).\n\n.. conf:: tiebreak\n    :default: popularity\n\n    How to choose the candidate if there is more than one identical result. For\n    example, there might be multiple releases of the same album.\n\n    - ``popularity``: pick the more popular candidate\n    - ``first``: pick the first candidate\n\n.. conf:: regex\n    :default: []\n\n    An array of regex transformations to perform on the track/album/artist fields\n    before sending them to Spotify. Can be useful for changing certain\n    abbreviations, like ft. -> feat. For example:\n\n    .. code-block:: yaml\n\n        regex:\n          - field: albumartist\n            search: Something\n            replace: Replaced\n          - field: title\n            search: Something Else\n            replace: AlsoReplaced\n\n.. conf:: search_query_ascii\n    :default: no\n\n    If enabled, the search query will be converted to ASCII before being sent to\n    Spotify. Converting searches to ASCII can enhance search results in some\n    cases, but in general, it is not recommended. For instance,\n    ``artist:deadmau5 album:4×4`` will be converted to ``artist:deadmau5\n    album:4x4`` (notice ``×!=x``).\n\n.. include:: ./shared_metadata_source_config.rst\n\nObtaining Track Popularity and Audio Features from Spotify\n----------------------------------------------------------\n\nSpotify provides information on track popularity_ and audio features_ that can\nbe used for music discovery.\n\n.. _features: https://developer.spotify.com/documentation/web-api/reference/get-audio-features\n\n.. _popularity: https://developer.spotify.com/documentation/web-api/reference/get-track\n\nThe ``spotify`` plugin provides an additional command ``spotifysync`` to obtain\nthese track attributes from Spotify:\n\n- ``beet spotifysync [-f]``: obtain popularity and audio features information\n  for every track in the library. By default, ``spotifysync`` will skip tracks\n  that already have this information populated. Using the ``-f`` or ``-force``\n  option will download the data even for tracks that already have it. Please\n  note that ``spotifysync`` works on tracks that have the Spotify track\n  identifiers. So run ``spotifysync`` only after importing your music, during\n  which Spotify identifiers will be added for tracks where Spotify is chosen as\n  the tag source.\n\n  In addition to ``popularity``, the command currently sets these audio features\n  for all tracks with a Spotify track ID:\n\n  - ``acousticness``\n  - ``danceability``\n  - ``energy``\n  - ``instrumentalness``\n  - ``key``\n  - ``liveness``\n  - ``loudness``\n  - ``mode``\n  - ``speechiness``\n  - ``tempo``\n  - ``time_signature``\n  - ``valence``\n"
  },
  {
    "path": "docs/plugins/subsonicplaylist.rst",
    "content": "Subsonic Playlist Plugin\n========================\n\nThe ``subsonicplaylist`` plugin allows to import playlists from a subsonic\nserver. This is done by retrieving the track info from the subsonic server,\nsearching for them in the beets library, and adding the playlist names to the\n``subsonic_playlist`` tag of the found items. The content of the tag has the\nformat:\n\n    subsonic_playlist: \";first playlist;second playlist;\"\n\nTo get all items in a playlist use the query ``;playlist name;``.\n\nCommand Line Usage\n------------------\n\nTo use the ``subsonicplaylist`` plugin, enable it in your configuration (see\n:ref:`using-plugins`). Then use it by invoking the ``subsonicplaylist`` command.\nNext, configure the plugin to connect to your Subsonic server, like this:\n\n::\n\n    subsonicplaylist:\n        base_url: http://subsonic.example.com\n        username: someUser\n        password: somePassword\n\nAfter this you can import your playlists by invoking the ``subsonicplaylist``\ncommand.\n\nBy default only the tags of the items found for playlists will be updated. This\nmeans that, if one imported a playlist, then delete one song from it and\nimported the playlist again, the deleted song will still have the playlist set\nin its ``subsonic_playlist`` tag. To solve this problem one can use the\n``-d/--delete`` flag. This resets all ``subsonic_playlist`` tag before importing\nplaylists.\n\nHere's an example configuration with all the available options and their default\nvalues:\n\n::\n\n    subsonicplaylist:\n        base_url: \"https://your.subsonic.server\"\n        delete: no\n        playlist_ids: []\n        playlist_names: []\n        username: ''\n        password: ''\n\nThe ``base_url``, ``username``, and ``password`` options are required.\n"
  },
  {
    "path": "docs/plugins/subsonicupdate.rst",
    "content": "SubsonicUpdate Plugin\n=====================\n\n``subsonicupdate`` is a very simple plugin for beets that lets you automatically\nupdate Subsonic_'s index whenever you change your beets library.\n\n.. _subsonic: https://www.subsonic.org/pages/index.jsp\n\nTo use ``subsonicupdate`` plugin, enable it in your configuration (see\n:ref:`using-plugins`). Then, you'll probably want to configure the specifics of\nyour Subsonic server. You can do that using a ``subsonic:`` section in your\n``config.yaml``, which looks like this:\n\n::\n\n    subsonic:\n        url: https://example.com:443/subsonic\n        user: username\n        pass: password\n        auth: token\n\nWith that all in place, this plugin will send a REST API call to your Subsonic\nserver every time you change your beets library. Due to a current limitation of\nthe API, all libraries visible to that user will be scanned.\n\nIf the :doc:`/plugins/smartplaylist` is used, creating or changing any playlist\nwill trigger a Subsonic update as well.\n\nThis plugin requires Subsonic with an active Premium license (or active trial)\nor any other `Subsonic API compatible`_ server implementing the ``startScan``\nendpoint.\n\n.. _subsonic api compatible: https://www.subsonic.org/pages/api.jsp\n\nConfiguration\n-------------\n\nThe available options under the ``subsonic:`` section are:\n\n- **url**: The Subsonic server resource. Default: ``http://localhost:4040``\n- **user**: The Subsonic user. Default: ``admin``\n- **pass**: The Subsonic user password. (This may either be a clear-text\n  password or hex-encoded with the prefix ``enc:``.) Default: ``admin``\n- **auth**: The authentication method. Possible choices are ``token`` or\n  ``password``. ``token`` authentication is preferred to avoid sending cleartext\n  password.\n"
  },
  {
    "path": "docs/plugins/substitute.rst",
    "content": "Substitute Plugin\n=================\n\nThe ``substitute`` plugin lets you easily substitute values in your templates\nand path formats. Specifically, it is intended to let you *canonicalize* names\nsuch as artists: For example, perhaps you want albums from The Jimi Hendrix\nExperience to be sorted into the same folder as solo Hendrix albums.\n\nThis plugin is intended as a replacement for the ``rewrite`` plugin. While the\n``rewrite`` plugin modifies the metadata, this plugin does not.\n\nEnable the ``substitute`` plugin (see :ref:`using-plugins`), then make a\n``substitute:`` section in your config file to contain your rules. Each rule\nconsists of a case-insensitive regular expression pattern, and a replacement\nstring. For example, you might use:\n\n.. code-block:: yaml\n\n    substitute:\n      .*jimi hendrix.*: Jimi Hendrix\n\nThe replacement can be an expression utilising the matched regex, allowing us to\ncreate more general rules. Say for example, we want to sort all albums by\nmultiple artists into the directory of the first artist. We can thus capture\neverything before the first ``,``, ``&`` or ``and``, and use this capture group\nin the output, discarding the rest of the string.\n\n.. code-block:: yaml\n\n    substitute:\n      ^(.*?)(,| &| and).*: \\1\n\nThis would handle all the below cases in a single rule:\n\n    |   Bob Dylan and The Band -> Bob Dylan\n    |   Neil Young & Crazy Horse -> Neil Young\n    |   James Yorkston, Nina Persson & The Second Hand Orchestra -> James\n        Yorkston\n\nTo apply the substitution, you have to call the function ``%substitute{}`` in\nthe paths section. For example:\n\n.. code-block:: yaml\n\n    paths:\n        default: \\%substitute{$albumartist}/$year - $album\\%aunique{}/$track - $title\n"
  },
  {
    "path": "docs/plugins/the.rst",
    "content": "The Plugin\n==========\n\nThe ``the`` plugin allows you to move patterns in path formats. It's suitable,\nfor example, for moving articles from string start to the end. This is useful\nfor quick search on filesystems and generally looks good. Plugin does not change\ntags. By default plugin supports English \"the, a, an\", but custom regexp\npatterns can be added by user. How it works:\n\n::\n\n    The Something -> Something, The\n    A Band -> Band, A\n    An Orchestra -> Orchestra, An\n\nTo use the ``the`` plugin, enable it (see :doc:`/plugins/index`) and then use a\ntemplate function called ``%the`` in path format expressions:\n\n::\n\n    paths:\n        default: %the{$albumartist}/($year) $album/$track $title\n\nThe default configuration moves all English articles to the end of the string,\nbut you can override these defaults to make more complex changes.\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``the:`` section in your configuration file. The\navailable options are:\n\n- **a**: Handle \"A/An\" moves. Default: ``yes``.\n- **the**: handle \"The\" moves. Default: ``yes``.\n- **patterns**: Custom regexp patterns, space-separated. Custom patterns are\n  case-insensitive regular expressions. Patterns can be matched anywhere in the\n  string (not just the beginning), so use ``^`` if you intend to match leading\n  words. Default: ``[]``.\n- **strip**: Remove the article altogether instead of moving it to the end.\n  Default: ``no``.\n- **format**: A Python format string for the output. Use ``{0}`` to indicate the\n  part without the article and ``{1}`` for the article. Spaces are already\n  trimmed from ends of both parts. Default: ``'{0}, {1}'``.\n"
  },
  {
    "path": "docs/plugins/thumbnails.rst",
    "content": "Thumbnails Plugin\n=================\n\nThe ``thumbnails`` plugin creates thumbnails for your album folders with the\nalbum cover. This works on freedesktop.org-compliant file managers such as\nNautilus or Thunar, and is therefore POSIX-only.\n\nTo use the ``thumbnails`` plugin, enable ``thumbnails`` and\n:doc:`/plugins/fetchart` in your configuration (see :ref:`using-plugins`) and\ninstall ``beets`` with ``thumbnails`` and ``fetchart`` extras\n\n.. code-block:: bash\n\n    pip install \"beets[fetchart,thumbnails]\"\n\n``thumbnails`` need to resize the covers, and therefore requires either\nImageMagick_ or Pillow_.\n\n.. _imagemagick: https://imagemagick.org/\n\n.. _pillow: https://github.com/python-pillow/Pillow\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``thumbnails`` section in your configuration\nfile. The available options are\n\n- **auto**: Whether the thumbnail should be automatically set on import.\n  Default: ``yes``.\n- **force**: Generate the thumbnail even when there's one that seems fine (more\n  recent than the cover art). Default: ``no``.\n- **dolphin**: Generate dolphin-compatible thumbnails. Dolphin (KDE file\n  explorer) does not respect freedesktop.org's standard on thumbnails. This\n  functionality replaces the :doc:`/plugins/freedesktop` Default: ``no``\n\nUsage\n-----\n\nThe ``thumbnails`` command provided by this plugin creates a thumbnail for\nalbums that match a query (see :doc:`/reference/query`).\n"
  },
  {
    "path": "docs/plugins/titlecase.rst",
    "content": "Titlecase Plugin\n================\n\nThe ``titlecase`` plugin lets you format tags and paths in accordance with the\ntitlecase guidelines in the `New York Times Manual of Style`_ and uses the\n`python titlecase library`_.\n\nMotivation for this plugin comes from a desire to resolve differences in style\nbetween databases sources. For example, `MusicBrainz style`_ follows standard\ntitle case rules, except in the case of terms that are deemed generic, like\n\"mix\" and \"remix\". On the other hand, `Discogs guidelines`_ recommend\ncapitalizing the first letter of each word, even for small words like \"of\" and\n\"a\". This plugin aims to achieve a middle ground between disparate approaches to\ncasing, and bring more consistency to titles in your library.\n\n.. _discogs guidelines: https://support.discogs.com/hc/en-us/articles/360005006334-Database-Guidelines-1-General-Rules#Capitalization_And_Grammar\n\n.. _musicbrainz style: https://musicbrainz.org/doc/Style\n\n.. _new york times manual of style: https://search.worldcat.org/en/title/946964415\n\n.. _python titlecase library: https://pypi.org/project/titlecase/\n\nInstallation\n------------\n\nTo use the ``titlecase`` plugin, first enable it in your configuration (see\n:ref:`using-plugins`). Then, install ``beets`` with ``titlecase`` extra:\n\n.. code-block:: bash\n\n    pip install \"beets[titlecase]\"\n\nIf you'd like to just use the path format expression, call ``%titlecase`` in\nyour path formatter, and set ``auto`` to ``no`` in the configuration.\n\n::\n\n    paths:\n      default: %titlecase($albumartist)/$titlecase($albumtitle)/$track $title\n\nYou can now configure ``titlecase`` to your preference.\n\nConfiguration\n-------------\n\nThis plugin offers several configuration options to tune its function to your\npreference.\n\nDefault\n~~~~~~~\n\n.. code-block:: yaml\n\n    titlecase:\n        auto: yes\n        fields: []\n        preserve: []\n        replace: []\n        separators: []\n        force_lowercase: no\n        small_first_last: yes\n        the_artist: yes\n        all_lowercase: no\n        all_caps: no\n        after_choice: no\n\n.. conf:: auto\n    :default: yes\n\n    Whether to automatically apply titlecase to new imports.\n\n.. conf:: fields\n    :default: []\n\n     A list of fields to apply the titlecase logic to. You must specify the fields\n     you want to have modified in order for titlecase to apply changes to metadata.\n\n     A good starting point is below, which will titlecase album titles, track titles, and all artist fields.\n\n.. code-block:: yaml\n\n    titlecase:\n      fields:\n        - album\n        - title\n        - albumartist\n        - albumartist_credit\n        - albumartist_sort\n        - albumartists\n        - albumartists_credit\n        - albumartists_sort\n        - artist\n        - artist_credit\n        - artist_sort\n        - artists\n        - artists_credit\n        - artists_sort\n\n.. conf:: preserve\n    :default: []\n\n     List of words and phrases to preserve the case of. Without specifying ``DJ`` on\n     the list, titlecase will format it as ``Dj``, or specify ``The Beatles`` to make sure\n     ``With The Beatles`` is not capitalized as ``With the Beatles``.\n\n.. conf:: replace\n    :default: []\n\n     The replace function takes place before any titlecasing occurs, and is intended to\n     help normalize differences in puncuation styles. It accepts a list of tuples, with\n     the first being the target, and the second being the replacement.\n\n     An example configuration that enforces one style of quotation mark is below.\n\n.. code-block:: yaml\n\n    titlecase:\n      replace:\n        - \"’\": \"'\"\n        - \"‘\": \"'\"\n        - \"“\": '\"'\n        - \"”\": '\"'\n\n.. conf:: separators\n    :default: []\n\n     A list of characters to treat as markers of new sentences. Helpful for split titles\n     that might otherwise have a lowercase letter at the start of the second string.\n\n.. conf:: force_lowercase\n    :default: no\n\n    Force all strings to lowercase before applying titlecase, but can cause\n    problems with all caps acronyms titlecase would otherwise recognize.\n\n.. conf:: small_first_last\n    :default: yes\n\n     An option from the base titlecase library. Controls capitalizing small words at the start\n     of a sentence. With this turned off ``a`` and similar words will not be capitalized\n     under any circumstance.\n\n.. conf:: the_artist\n    :default: yes\n\n     If a field name contains ``artist``, then any lowercase ``the`` will be\n     capitalized. Useful for bands with ``The`` as part of the proper name,\n     like ``Amyl and The Sniffers``.\n\n.. conf:: all_caps\n    :default: no\n\n    If the letters a-Z in a string are all caps, do not modify the string. Useful\n    if you encounter a lot of acronyms.\n\n.. conf:: all_lowercase\n    :default: no\n\n    If the letters a-Z in a string are all lowercase, do not modify the string.\n    Useful if you encounter a lot of stylized lowercase spellings, but otherwise\n    want titlecase applied.\n\n.. conf:: after_choice\n    :default: no\n\n     By default, titlecase runs on the candidates that are received, adjusting them before\n     you make your selection and creating different weight calculations. If you'd rather\n     see the data as recieved from the database, set this to true to run after you make\n     your tag choice.\n\nDangerous Fields\n~~~~~~~~~~~~~~~~\n\n``titlecase`` only ever modifies string fields, however, this doesn't prevent\nyou from selecting a case sensitive field that another plugin or feature may\nrely on.\n\nIn particular, including any of the following in your configuration could lead\nto unintended behavior:\n\n.. code-block:: bash\n\n    acoustid_fingerprint\n    acoustid_id\n    artists_ids\n    asin\n    deezer_track_id\n    format\n    id\n    isrc\n    mb_workid\n    mb_trackid\n    mb_albumid\n    mb_artistid\n    mb_artistids\n    mb_albumartistid\n    mb_albumartistids\n    mb_releasetrackid\n    mb_releasegroupid\n    bitrate_mode\n    encoder_info\n    encoder_settings\n\nRunning Manually\n----------------\n\nFrom the command line, type:\n\n::\n\n    $ beet titlecase [QUERY]\n\nConfiguration is drawn from the config file. Without a query the operation will\nbe applied to the entire collection.\n"
  },
  {
    "path": "docs/plugins/types.rst",
    "content": "Types Plugin\n============\n\nThe ``types`` plugin lets you declare types for attributes you use in your\nlibrary. For example, you can declare that a ``rating`` field is numeric so that\nyou can query it with ranges---which isn't possible when the field is considered\na string (the default).\n\nEnable the ``types`` plugin as described in :doc:`/plugins/index` and then add a\n``types`` section to your :doc:`configuration file </reference/config>`. The\nconfiguration section should map field name to one of ``int``, ``float``,\n``bool``, or ``date``.\n\nHere's an example:\n\n::\n\n    types:\n        rating: int\n\nNow you can assign numeric ratings to tracks and albums and use :ref:`range\nqueries <numericquery>` to filter them.:\n\n::\n\n    beet modify \"My favorite track\" rating=5\n    beet ls rating:4..5\n\n    beet modify --album \"My favorite album\" rating=5\n    beet ls --album rating:4..5\n"
  },
  {
    "path": "docs/plugins/unimported.rst",
    "content": "Unimported Plugin\n=================\n\nThe ``unimported`` plugin allows one to list all files in the library folder\nwhich are not listed in the beets library database, including art files.\n\nCommand Line Usage\n------------------\n\nTo use the ``unimported`` plugin, enable it in your configuration (see\n:ref:`using-plugins`). Then use it by invoking the ``beet unimported`` command.\nThe command will list all files in the library folder which are not imported.\nYou can exclude file extensions or entire subdirectories using the configuration\nfile:\n\n::\n\n    unimported:\n        ignore_extensions: jpg png\n        ignore_subdirectories: NonMusic data temp\n\nThe default configuration lists all unimported files, ignoring no extensions.\n"
  },
  {
    "path": "docs/plugins/web.rst",
    "content": "Web Plugin\n==========\n\nThe ``web`` plugin is a very basic alternative interface to beets that\nsupplements the CLI. It can't do much right now, and the interface is a little\nclunky, but you can use it to query and browse your music and---in browsers that\nsupport HTML5 Audio---you can even play music.\n\nWhile it's not meant to replace the CLI, a graphical interface has a number of\nadvantages in certain situations. For example, when editing a tag, a natural CLI\nmakes you retype the whole thing---common GUI conventions can be used to just\nedit the part of the tag you want to change. A graphical interface could also\ndrastically increase the number of people who can use beets.\n\nInstall\n-------\n\nTo use the ``web`` plugin, first enable it in your configuration (see\n:ref:`using-plugins`). Then, install ``beets`` with ``web`` extra\n\n.. code-block:: bash\n\n    pip install \"beets[web]\"\n\nRun the Server\n--------------\n\nThen just type ``beet web`` to start the server and go to\nhttp://localhost:8337/. This is what it looks like:\n\n.. image:: beetsweb.png\n\nYou can also specify the hostname and port number used by the Web server. These\ncan be specified on the command line or in the ``[web]`` section of your\n:doc:`configuration file </reference/config>`.\n\nOn the command line, use ``beet web [HOSTNAME] [PORT]``. Or the configuration\noptions below.\n\nUsage\n-----\n\nType queries into the little search box. Double-click a track to play it with\nHTML5 Audio.\n\nConfiguration\n-------------\n\nTo configure the plugin, make a ``web:`` section in your configuration file. The\navailable options are:\n\n- **host**: The server hostname. Set this to 0.0.0.0 to bind to all interfaces.\n  Default: Bind to 127.0.0.1.\n- **port**: The server port. Default: 8337.\n- **cors**: The CORS allowed origin (see :ref:`web-cors`, below). Default: CORS\n  is disabled.\n- **cors_supports_credentials**: Support credentials when using CORS (see\n  :ref:`web-cors`, below). Default: CORS_SUPPORTS_CREDENTIALS is disabled.\n- **reverse_proxy**: If true, enable reverse proxy support (see\n  :ref:`reverse-proxy`, below). Default: false.\n- **include_paths**: If true, includes paths in item objects. Default: false.\n- **readonly**: If true, DELETE and PATCH operations are not allowed. Only GET\n  is permitted. Default: true.\n\nImplementation\n--------------\n\nThe Web backend is built using a simple REST+JSON API with the excellent Flask_\nlibrary. The frontend is a single-page application written with Backbone.js_.\nThis allows future non-Web clients to use the same backend API.\n\n.. _backbone.js: https://backbonejs.org\n\nEventually, to make the Web player really viable, we should use a Flash fallback\nfor unsupported formats/browsers. There are a number of options for this:\n\n- audio.js_\n- html5media_\n- MediaElement.js_\n\n.. _audio.js: https://kolber.github.io/audiojs/\n\n.. _html5media: https://html5media.info/\n\n.. _mediaelement.js: https://www.mediaelementjs.com/\n\n.. _web-cors:\n\nCross-Origin Resource Sharing (CORS)\n------------------------------------\n\nThe ``web`` plugin's API can be used as a backend for an in-browser client. By\ndefault, browsers will only allow access from clients running on the same server\nas the API. (You will get an arcane error about ``XMLHttpRequest`` otherwise.) A\ntechnology called CORS_ lets you relax this restriction.\n\nIf you want to use an in-browser client hosted elsewhere (or running from a\ndifferent server on your machine), set the ``cors`` configuration option to the\n\"origin\" (protocol, host, and optional port number) where the client is served.\nOr set it to ``'*'`` to enable access from all origins. Note that there are\nsecurity implications if you set the origin to ``'*'``, so please research this\nbefore using it.\n\nIf the ``web`` server is behind a proxy that uses credentials, you might want to\nset the ``cors_supports_credentials`` configuration option to true to let\nin-browser clients log in.\n\nFor example:\n\n::\n\n    web:\n        host: 0.0.0.0\n        cors: 'http://example.com'\n\n.. _cors: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing\n\n.. _reverse-proxy:\n\nReverse Proxy Support\n---------------------\n\nWhen the server is running behind a reverse proxy, you can tell the plugin to\nrespect forwarded headers. Specifically, this can help when you host the plugin\nat a base URL other than the root ``/`` or when you use the proxy to handle\nsecure connections. Enable the ``reverse_proxy`` configuration option if you do\nthis.\n\nTechnically, this option lets the proxy provide ``X-Script-Name`` and\n``X-Scheme`` HTTP headers to control the plugin's the ``SCRIPT_NAME`` and its\n``wsgi.url_scheme`` parameter.\n\nHere's a sample Nginx_ configuration that serves the web plugin under the /beets\ndirectory:\n\n::\n\n    location /beets {\n        proxy_pass http://127.0.0.1:8080;\n        proxy_set_header Host $host;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Scheme $scheme;\n        proxy_set_header X-Script-Name /beets;\n    }\n\n.. _nginx: https://www.f5.com/products/nginx\n\nJSON API\n--------\n\n``GET /item/``\n~~~~~~~~~~~~~~\n\nResponds with a list of all tracks in the beets library.\n\n::\n\n    {\n      \"items\": [\n        {\n          \"id\": 6,\n          \"title\": \"A Song\",\n          ...\n        }, {\n          \"id\": 12,\n          \"title\": \"Another Song\",\n          ...\n        }\n        ...\n      ]\n    }\n\n``GET /item/6``\n~~~~~~~~~~~~~~~\n\nLooks for an item with id *6* in the beets library and responds with its JSON\nrepresentation.\n\n::\n\n    {\n      \"id\": 6,\n      \"title\": \"A Song\",\n      ...\n    }\n\nIf there is no item with that id responds with a *404* status code.\n\n``DELETE /item/6``\n~~~~~~~~~~~~~~~~~~\n\nRemoves the item with id *6* from the beets library. If the *?delete* query\nstring is included, the matching file will be deleted from disk.\n\nOnly allowed if ``readonly`` configuration option is set to ``no``.\n\n``PATCH /item/6``\n~~~~~~~~~~~~~~~~~\n\nUpdates the item with id *6* and write the changes to the music file. The body\nshould be a JSON object containing the changes to the object.\n\nReturns the updated JSON representation.\n\n::\n\n    {\n      \"id\": 6,\n      \"title\": \"A Song\",\n      ...\n    }\n\nOnly allowed if ``readonly`` configuration option is set to ``no``.\n\n``GET /item/6,12,13``\n~~~~~~~~~~~~~~~~~~~~~\n\nResponse with a list of tracks with the ids *6*, *12* and *13*. The format of\nthe response is the same as for `GET /item/`_. It is *not guaranteed* that the\nresponse includes all the items requested. If a track is not found it is\nsilently dropped from the response.\n\nThis endpoint also supports *DELETE* and *PATCH* methods as above, to operate on\nall items of the list.\n\n``GET /item/path/...``\n~~~~~~~~~~~~~~~~~~~~~~\n\nLook for an item at the given absolute path on the server. If it corresponds to\na track, return the track in the same format as ``/item/*``.\n\nIf the server runs UNIX, you'll need to include an extra leading slash:\n``http://localhost:8337/item/path//Users/beets/Music/Foo/Bar/Baz.mp3``\n\n``GET /item/query/querystring``\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nReturns a list of tracks matching the query. The *querystring* must be a valid\nquery as described in :doc:`/reference/query`.\n\n::\n\n    {\n      \"results\": [\n        { \"id\" : 6,  \"title\": \"A Song\" },\n        { \"id\" : 12, \"title\": \"Another Song\" }\n      ]\n    }\n\nPath elements are joined as parts of a query. For example,\n``/item/query/foo/bar`` will be converted to the query ``foo,bar``. To specify\nliteral path separators in a query, use a backslash instead of a slash.\n\nThis endpoint also supports *DELETE* and *PATCH* methods as above, to operate on\nall items returned by the query.\n\n``GET /item/6/file``\n~~~~~~~~~~~~~~~~~~~~\n\nSends the media file for the track. If the item or its corresponding file do not\nexist a *404* status code is returned.\n\nAlbums\n~~~~~~\n\nFor albums, the following endpoints are provided:\n\n- ``GET /album/``\n- ``GET /album/5``\n- ``GET /album/5/art``\n- ``DELETE /album/5``\n- ``GET /album/5,7``\n- ``DELETE /album/5,7``\n- ``GET /album/query/querystring``\n- ``DELETE /album/query/querystring``\n\nThe interface and response format is similar to the item API, except replacing\nthe encapsulation key ``\"items\"`` with ``\"albums\"`` when requesting ``/album/``\nor ``/album/5,7``. In addition we can request the cover art of an album with\n``GET /album/5/art``. You can also add the '?expand' flag to get the individual\nitems of an album.\n\n``DELETE`` is only allowed if ``readonly`` configuration option is set to\n``no``.\n\n``GET /stats``\n~~~~~~~~~~~~~~\n\nResponds with the number of tracks and albums in the database.\n\n::\n\n    {\n      \"items\": 5,\n      \"albums\": 3\n    }\n\n.. _flask: https://flask.palletsprojects.com/en/stable/\n"
  },
  {
    "path": "docs/plugins/zero.rst",
    "content": "Zero Plugin\n===========\n\nThe ``zero`` plugin allows you to null fields in files' metadata tags. Fields\ncan be nulled unconditionally or conditioned on a pattern match. For example,\nthe plugin can strip useless comments like \"ripped by MyGreatRipper.\"\n\nThe plugin can work in one of two modes:\n\n- ``fields``: A blacklist, where you choose the tags you want to remove (used by\n  default).\n- ``keep_fields``: A whitelist, where you instead specify the tags you want to\n  keep.\n\nTo use the ``zero`` plugin, enable the plugin in your configuration (see\n:ref:`using-plugins`).\n\nConfiguration\n-------------\n\nMake a ``zero:`` section in your configuration file. You can specify the fields\nto nullify and the conditions for nullifying them:\n\n- Set ``auto`` to ``yes`` to null fields automatically on import. Default:\n  ``yes``.\n- Set ``fields`` to a whitespace-separated list of fields to remove. You can get\n  the list of all available fields by running ``beet fields``. In addition, the\n  ``images`` field allows you to remove any images embedded in the media file.\n- Set ``keep_fields`` to *invert* the logic of the plugin. Only these fields\n  will be kept; other fields will be removed. Remember to set only ``fields`` or\n  ``keep_fields``---not both!\n- To conditionally filter a field, use ``field: [regexp, regexp]`` to specify\n  regular expressions.\n- Set ``omit_single_disc`` to ``True`` to omit writing the ``disc`` number and\n  the ``disctotal`` number for albums with a single disc (``disctotal == 1``).\n- By default this plugin only affects files' tags; the beets database is left\n  unchanged. To update the tags in the database, set the ``update_database``\n  option to true.\n\nFor example:\n\n::\n\n    zero:\n        fields: month day genre genres comments\n        comments: [EAC, LAME, from.+collection, 'ripped by']\n        genres: [rnb, 'power metal']\n        update_database: true\n\nIf a custom pattern is not defined for a given field, the field will be nulled\nunconditionally.\n\nNote that the plugin currently does not zero fields when importing \"as-is\".\n\nManually Triggering Zero\n------------------------\n\nYou can also type ``beet zero [QUERY]`` to manually invoke the plugin on music\nin your library.\n\nPreserving Album Art\n--------------------\n\nIf you use the ``keep_fields`` option, the plugin will remove embedded album art\nfrom files' tags unless you tell it not to. To keep the album art, include the\nspecial field ``images`` in the list. For example:\n\n::\n\n    zero:\n        keep_fields: title artist album year track genre genres images\n"
  },
  {
    "path": "docs/reference/cli.rst",
    "content": "Command-Line Interface\n======================\n\n.. only:: man\n\n    SYNOPSIS\n    --------\n\n    | **beet** [*args*...] *command* [*args*...]\n    | **beet help** *command*\n\n.. only:: html\n\n    **beet** is the command-line interface to beets.\n\n    You invoke beets by specifying a *command*, like so::\n\n        beet COMMAND [ARGS...]\n\n    The rest of this document describes the available\n    commands. If you ever need a quick list of what's available, just\n    type ``beet help`` or ``beet help COMMAND`` for help with a specific\n    command.\n\n    Beets also offers shell completion. For bash, see the `completion`_\n    command; for zsh, see the accompanying `completion script`_ for the\n    ``beet`` command.\n\nCommands\n--------\n\n.. only:: html\n\n    Here are the built-in commands available in beets:\n\n    .. contents::\n        :local:\n        :depth: 1\n\n    Also be sure to see the :ref:`global flags <global-flags>`.\n\n.. _import-cmd:\n\nimport\n~~~~~~\n\n::\n\n    beet import [-CWAPRqst] [-l LOGPATH] PATH...\n    beet import [options] -L QUERY\n\nAdd music to your library, attempting to get correct tags for it from\nMusicBrainz.\n\nPoint the command at some music: directories, single files, or compressed\narchives. The music will be copied to a configurable directory structure and\nadded to a library database. The command is interactive and will try to get you\nto verify MusicBrainz tags that it thinks are suspect. See the :doc:`autotagging\nguide </guides/tagger>` for detail on how to use the interactive tag-correction\nflow.\n\nDirectories passed to the import command can contain either a single album or\nmany, in which case the leaf directories will be considered albums (the latter\ncase is true of typical Artist/Album organizations and many people's \"downloads\"\nfolders). The path can also be a single song or an archive. Beets supports\n``zip`` and ``tar`` archives out of the box. To extract ``rar`` files, install\nthe rarfile_ package and the ``unrar`` command. To extract ``7z`` files, install\nthe py7zr_ package.\n\nOptional command flags:\n\n- By default, the command copies files to your library directory and updates the\n  ID3 tags on your music. In order to move the files, instead of copying, use\n  the ``-m`` (move) option. If you'd like to leave your music files untouched,\n  try the ``-C`` (don't copy) and ``-W`` (don't write tags) options. You can\n  also disable this behavior by default in the configuration file (below).\n- Also, you can disable the autotagging behavior entirely using ``-A`` (don't\n  autotag)---then your music will be imported with its existing metadata.\n- During a long tagging import, it can be useful to keep track of albums that\n  weren't tagged successfully---either because they're not in the MusicBrainz\n  database or because something's wrong with the files. Use the ``-l`` option to\n  specify a filename to log every time you skip an album or import it \"as-is\" or\n  an album gets skipped as a duplicate. You can later review the file manually\n  or import skipped paths from the logfile automatically by using the\n  ``--from-logfile LOGFILE`` argument.\n- Relatedly, the ``-q`` (quiet) option can help with large imports by\n  autotagging without ever bothering to ask for user input. Whenever the normal\n  autotagger mode would ask for confirmation, the quiet mode performs a fallback\n  action that can be configured using the ``quiet_fallback`` configuration or\n  ``--quiet-fallback`` CLI option. By default it pessimistically skips the file.\n  Alternatively, it can be used as is, by configuring ``asis``.\n- Speaking of resuming interrupted imports, the tagger will prompt you if it\n  seems like the last import of the directory was interrupted (by you or by a\n  crash). If you want to skip this prompt, you can say \"yes\" automatically by\n  providing ``-p`` or \"no\" using ``-P``. The resuming feature can be disabled by\n  default using a configuration option (see below).\n- If you want to import only the *new* stuff from a directory, use the ``-i``\n  option to run an *incremental* import. With this flag, beets will keep track\n  of every directory it ever imports and avoid importing them again. This is\n  useful if you have an \"incoming\" directory that you periodically add things\n  to. To get this to work correctly, you'll need to use an incremental import\n  *every time* you run an import on the directory in question---including the\n  first time, when no subdirectories will be skipped. So consider enabling the\n  ``incremental`` configuration option.\n- If you don't want to record skipped files during an *incremental* import, use\n  the ``--incremental-skip-later`` flag which corresponds to the\n  ``incremental_skip_later`` configuration option. Setting the flag prevents\n  beets from persisting skip decisions during a non-interactive import so that a\n  user can make a decision regarding previously skipped files during a\n  subsequent interactive import run. To record skipped files during incremental\n  import explicitly, use the ``--noincremental-skip-later`` option.\n- When beets applies metadata to your music, it will retain the value of any\n  existing tags that weren't overwritten, and import them into the database. You\n  may prefer to only use existing metadata for finding matches, and to erase it\n  completely when new metadata is applied. You can enforce this behavior with\n  the ``--from-scratch`` option, or the ``from_scratch`` configuration option.\n- By default, beets will proceed without asking if it finds a very close\n  metadata match. To disable this and have the importer ask you every time, use\n  the ``-t`` (for *timid*) option.\n- The importer typically works in a whole-album-at-a-time mode. If you instead\n  want to import individual, non-album tracks, use the *singleton* mode by\n  supplying the ``-s`` option.\n- If you have an album that's split across several directories under a common\n  top directory, use the ``--flat`` option. This takes all the music files under\n  the directory (recursively) and treats them as a single large album instead of\n  as one album per directory. This can help with your more stubborn multi-disc\n  albums.\n- Similarly, if you have one directory that contains multiple albums, use the\n  ``--group-albums`` option to split the files based on their metadata before\n  matching them as separate albums.\n- If you want to preview which files would be imported, use the ``--pretend``\n  option. If set, beets will just print a list of files that it would otherwise\n  import.\n- If you already have a metadata backend ID that matches the items to be\n  imported, you can instruct beets to restrict the search to that ID instead of\n  searching for other candidates by using the ``--search-id SEARCH_ID`` option.\n  Multiple IDs can be specified by simply repeating the option several times.\n- You can supply ``--set field=value`` to assign ``field`` to ``value`` on\n  import. Values support the same template syntax as beets' :doc:`path formats\n  <pathformat>`.\n\n  These assignments will merge with (and possibly override) the\n  :ref:`set_fields` configuration dictionary. You can use the option multiple\n  times on the command line, like so:\n\n.. code-block:: sh\n\n    beet import --set genres=\"Alternative Rock\" --set mood=\"emotional\"\n\n.. _py7zr: https://pypi.org/project/py7zr/\n\n.. _rarfile: https://pypi.org/project/rarfile/\n\n.. only:: html\n\n    .. _reimport:\n\n    Reimporting\n    ^^^^^^^^^^^\n\n    The ``import`` command can also be used to \"reimport\" music that you've\n    already added to your library. This is useful when you change your mind\n    about some selections you made during the initial import, or if you prefer\n    to import everything \"as-is\" and then correct tags later.\n\n    Just point the ``beet import`` command at a directory of files that are\n    already catalogged in your library. Beets will automatically detect this\n    situation and avoid duplicating any items. In this situation, the \"copy\n    files\" option (``-c``/``-C`` on the command line or ``copy`` in the\n    config file) has slightly different behavior: it causes files to be *moved*,\n    rather than duplicated, if they're already in your library. (The same is\n    true, of course, if ``move`` is enabled.) That is, your directory\n    structure will be updated to reflect the new tags if copying is enabled; you\n    never end up with two copies of the file.\n\n    The ``-L`` (``--library``) flag is also useful for retagging. Instead of\n    listing paths you want to import on the command line, specify a :doc:`query\n    string <query>` that matches items from your library. In this case, the\n    ``-s`` (singleton) flag controls whether the query matches individual items\n    or full albums. If you want to retag your whole library, just supply a null\n    query, which matches everything: ``beet import -L``\n\n    Note that, if you just want to update your files' tags according to\n    changes in the MusicBrainz database, the :doc:`/plugins/mbsync` is a\n    better choice. Reimporting uses the full matching machinery to guess\n    metadata matches; ``mbsync`` just relies on MusicBrainz IDs.\n\n.. _list-cmd:\n\nlist\n~~~~\n\n::\n\n    beet list [-apf] QUERY\n\n:doc:`Queries <query>` the database for music.\n\nWant to search for \"Gronlandic Edit\" by of Montreal? Try ``beet list\ngronlandic``. Maybe you want to see everything released in 2009 with\n\"vegetables\" in the title? Try ``beet list year:2009 title:vegetables``. You can\nalso specify the sort order. (Read more in :doc:`query`.)\n\nYou can use the ``-a`` switch to search for albums instead of individual items.\nIn this case, the queries you use are restricted to album-level fields: for\nexample, you can search for ``year:1969`` but query parts for item-level fields\nlike ``title:foo`` will be ignored. Remember that ``artist`` is an item-level\nfield; ``albumartist`` is the corresponding album field.\n\nThe ``-p`` option makes beets print out filenames of matched items, which might\nbe useful for piping into other Unix commands (such as `xargs\n<https://en.wikipedia.org/wiki/Xargs>`__). Similarly, the ``-f`` option lets you\nspecify a specific format with which to print every album or track. This uses\nthe same template syntax as beets' :doc:`path formats <pathformat>`. For\nexample, the command ``beet ls -af '$album: $albumtotal' beatles`` prints out\nthe number of tracks on each Beatles album. In Unix shells, remember to enclose\nthe template argument in single quotes to avoid environment variable expansion.\n\n.. _remove-cmd:\n\nremove\n~~~~~~\n\n::\n\n    beet remove [-adf] QUERY\n\nRemove music from your library.\n\nThis command uses the same :doc:`query <query>` syntax as the ``list`` command.\nBy default, it just removes entries from the library database; it doesn't touch\nthe files on disk. To actually delete the files, use the ``-d`` flag. When the\n``-a`` flag is given, the command operates on albums instead of individual\ntracks.\n\nWhen you run the ``remove`` command, it prints a list of all affected items in\nthe library and asks for your permission before removing them. You can then\nchoose to abort (type ``n``), confirm (``y``), or interactively choose some of\nthe items (``s``). In the latter case, the command will prompt you for every\nmatching item or album and invite you to type ``y`` to remove the item/album,\n``n`` to keep it or ``q`` to exit and only remove the items/albums selected up\nto this point.\n\nThis option lets you choose precisely which tracks/albums to remove without\nspending too much time to carefully craft a query. If you do not want to be\nprompted at all, use the ``-f`` option.\n\n.. _modify-cmd:\n\nmodify\n~~~~~~\n\n::\n\n    beet modify [-IMWay] [-f FORMAT] QUERY [FIELD=VALUE...] [FIELD!...]\n\nChange the metadata for items or albums in the database.\n\nSupply a :doc:`query <query>` matching the things you want to change and a\nseries of ``field=value`` pairs. For example, ``beet modify genius of love\nartist=\"Tom Tom Club\"`` will change the artist for the track \"Genius of Love.\"\nTo remove fields (which is only possible for flexible attributes), follow a\nfield name with an exclamation point: ``field!``.\n\nValues can also be *templates*, using the same syntax as :doc:`path formats\n<pathformat>`. For example, ``beet modify artist='$artist_sort'`` will copy the\nartist sort name into the artist field for all your tracks, and ``beet modify\ntitle='$track $title'`` will add track numbers to their title metadata.\n\nTo adjust a multi-valued field, such as ``genres``, separate the values with\n|semicolon_space|. For example, ``beet modify genres=\"rock; pop\"``.\n\nThe ``-a`` option changes to querying album fields instead of track fields and\nalso enables to operate on albums in addition to the individual tracks. Without\nthis flag, the command will only change *track-level* data, even if all the\ntracks belong to the same album. If you want to change an *album-level* field,\nsuch as ``year`` or ``albumartist``, you'll want to use the ``-a`` flag to avoid\na confusing situation where the data for individual tracks conflicts with the\ndata for the whole album.\n\nModifications issued using ``-a`` by default cascade to individual tracks. To\nprevent this behavior, use ``-I``/``--noinherit``.\n\nItems will automatically be moved around when necessary if they're in your\nlibrary directory, but you can disable that with ``-M``. Tags will be written to\nthe files according to the settings you have for imports, but these can be\noverridden with ``-w`` (write tags, the default) and ``-W`` (don't write tags).\n\nWhen you run the ``modify`` command, it prints a list of all affected items in\nthe library and asks for your permission before making any changes. You can then\nchoose to abort the change (type ``n``), confirm (``y``), or interactively\nchoose some of the items (``s``). In the latter case, the command will prompt\nyou for every matching item or album and invite you to type ``y`` to apply the\nchanges, ``n`` to discard them or ``q`` to exit and apply the selected changes.\nThis option lets you choose precisely which data to change without spending too\nmuch time to carefully craft a query. To skip the prompts entirely, use the\n``-y`` option.\n\n.. _move-cmd:\n\nmove\n~~~~\n\n::\n\n    beet move [-capt] [-d DIR] QUERY\n\nMove or copy items in your library.\n\nThis command, by default, acts as a library consolidator: items matching the\nquery are renamed into your library directory structure. By specifying a\ndestination directory with ``-d`` manually, you can move items matching a query\nanywhere in your filesystem. The ``-c`` option copies files instead of moving\nthem. As with other commands, the ``-a`` option matches albums instead of items.\nThe ``-e`` flag (for \"export\") copies files without changing the database.\n\nTo perform a \"dry run\", just use the ``-p`` (for \"pretend\") flag. This will show\nyou a list of files that would be moved but won't actually change anything on\ndisk. The ``-t`` option sets the timid mode which will ask again before really\nmoving or copying the files.\n\n.. _update-cmd:\n\nupdate\n~~~~~~\n\n::\n\n    beet update [-F] FIELD [-e] EXCLUDE_FIELD [-aMp] QUERY\n\nUpdate the library (and, by default, move files) to reflect out-of-band metadata\nchanges and file deletions.\n\nThis will scan all the matched files and read their tags, populating the\ndatabase with the new values. By default, files will be renamed according to\ntheir new metadata; disable this with ``-M``. Beets will skip files if their\nmodification times have not changed, so any out-of-band metadata changes must\nalso update these for ``beet update`` to recognise that the files have been\nedited.\n\nTo perform a \"dry run\" of an update, just use the ``-p`` (for \"pretend\") flag.\nThis will show you all the proposed changes but won't actually change anything\non disk.\n\nBy default, all the changed metadata will be populated back to the database. If\nyou only want certain fields to be written, specify them with the ``-F`` flags\n(which can be used multiple times). Alternatively, specify fields to *not* write\nwith ``-e`` flags (which can be used multiple times). For the list of supported\nfields, please see ``beet fields``.\n\nWhen an updated track is part of an album, the album-level fields of *all*\ntracks from the album are also updated. (Specifically, the command copies\nalbum-level data from the first track on the album and applies it to the rest of\nthe tracks.) This means that, if album-level fields aren't identical within an\nalbum, some changes shown by the ``update`` command may be overridden by data\nfrom other tracks on the same album. This means that running the ``update``\ncommand multiple times may show the same changes being applied.\n\n.. _write-cmd:\n\nwrite\n~~~~~\n\n::\n\n    beet write [-pf] [QUERY]\n\nWrite metadata from the database into files' tags.\n\nWhen you make changes to the metadata stored in beets' library database (during\nimport or with the :ref:`modify-cmd` command, for example), you often have the\noption of storing changes only in the database, leaving your files untouched.\nThe ``write`` command lets you later change your mind and write the contents of\nthe database into the files. By default, this writes the changes only if there\nis a difference between the database and the tags in the file.\n\nYou can think of this command as the opposite of :ref:`update-cmd`.\n\nThe ``-p`` option previews metadata changes without actually applying them.\n\nThe ``-f`` option forces a write to the file, even if the file tags match the\ndatabase. This is useful for making sure that enabled plugins that run on write\n(e.g., the Scrub and Zero plugins) are run on the file.\n\n.. _stats-cmd:\n\nstats\n~~~~~\n\n::\n\n    beet stats [-e] [QUERY]\n\nShow some statistics on your entire library (if you don't provide a :doc:`query\n<query>`) or the matched items (if you do).\n\nBy default, the command calculates file sizes using their bitrate and duration.\nThe ``-e`` (``--exact``) option reads the exact sizes of each file (but is\nslower). The exact mode also outputs the exact duration in seconds.\n\n.. _fields-cmd:\n\nfields\n~~~~~~\n\n::\n\n    beet fields\n\nShow the item and album metadata fields available for use in :doc:`query` and\n:doc:`pathformat`. The listing includes any template fields provided by plugins\nand any flexible attributes you've manually assigned to your items and albums.\n\n.. _config-cmd:\n\nconfig\n~~~~~~\n\n::\n\n    beet config [-pdc]\n    beet config -e\n\nShow or edit the user configuration. This command does one of three things:\n\n- With no options, print a YAML representation of the current user\n  configuration. With the ``--default`` option, beets' default options are also\n  included in the dump.\n- The ``--path`` option instead shows the path to your configuration file. This\n  can be combined with the ``--default`` flag to show where beets keeps its\n  internal defaults.\n- By default, sensitive information like passwords is removed when dumping the\n  configuration. The ``--clear`` option includes this sensitive data.\n- With the ``--edit`` option, beets attempts to open your config file for\n  editing. It first tries the ``$EDITOR`` environment variable, followed by\n  ``$EDITOR`` and then a fallback option depending on your platform: ``open`` on\n  OS X, ``xdg-open`` on Unix, and direct invocation on Windows.\n\n.. _global-flags:\n\nGlobal Flags\n------------\n\nBeets has a few \"global\" flags that affect all commands. These must appear\nbetween the executable name (``beet``) and the command---for example, ``beet -v\nimport ...``.\n\n- ``-l LIBPATH``: specify the library database file to use.\n- ``-d DIRECTORY``: specify the library root directory.\n- ``-v``: verbose mode; prints out a deluge of debugging information. Please use\n  this flag when reporting bugs. You can use it twice, as in ``-vv``, to make\n  beets even more verbose.\n- ``-c FILE``: read a specified YAML :doc:`configuration file <config>`. This\n  configuration works as an overlay: rather than replacing your normal\n  configuration options entirely, the two are merged. Any individual options set\n  in this config file will override the corresponding settings in your base\n  configuration.\n- ``-p plugins``: specify a comma-separated list of plugins to enable. If\n  specified, the plugin list in your configuration is ignored. The long form of\n  this argument also allows specifying no plugins, effectively disabling all\n  plugins: ``--plugins=``.\n- ``-P plugins``: specify a comma-separated list of plugins to disable in a\n  specific beets run. This will overwrite ``-p`` if used with it. To disable all\n  plugins, use ``--plugins=`` instead.\n\nBeets also uses the ``BEETSDIR`` environment variable to look for configuration\nand data.\n\n.. _completion:\n\nShell Completion\n----------------\n\nBeets includes support for shell command completion. The command ``beet\ncompletion`` prints out a bash_ 3.2 script; to enable completion put a line like\nthis into your ``.bashrc`` or similar file:\n\n::\n\n    eval \"$(beet completion)\"\n\nOr, to avoid slowing down your shell startup time, you can pipe the ``beet\ncompletion`` output to a file and source that instead.\n\nYou will also need to source the bash-completion_ script, which is probably\navailable via your package manager. On OS X, you can install it via Homebrew\nwith ``brew install bash-completion``; Homebrew will give you instructions for\nsourcing the script.\n\n.. _bash: https://www.gnu.org/software/bash/\n\n.. _bash-completion: https://github.com/scop/bash-completion\n\nThe completion script suggests names of subcommands and (after typing ``-``)\noptions of the given command. If you are using a command that accepts a query,\nthe script will also complete field names.\n\n::\n\n    beet list ar[TAB]\n    # artist:  artist_credit:  artist_sort:  artpath:\n    beet list artp[TAB]\n    beet list artpath\\:\n\n(Don't worry about the slash in front of the colon: this is a escape sequence\nfor the shell and won't be seen by beets.)\n\nCompletion of plugin commands only works for those plugins that were enabled\nwhen running ``beet completion``. If you add a plugin later on you will want to\nre-generate the script.\n\nzsh\n~~~\n\nIf you use zsh, take a look at the included `completion script`_. The script\nshould be placed in a directory that is part of your ``fpath``, and ``not``\nsourced in your ``.zshrc``. Running ``echo $fpath`` will give you a list of\nvalid directories.\n\nAnother approach is to use zsh's bash completion compatibility. This snippet\ndefines some bash-specific functions to make this work without errors:\n\n::\n\n    autoload bashcompinit\n    bashcompinit\n    _get_comp_words_by_ref() { :; }\n    compopt() { :; }\n    _filedir() { :; }\n    eval \"$(beet completion)\"\n\n.. _completion script: https://github.com/beetbox/beets/blob/master/extra/_beet\n\n.. only:: man\n\n    See Also\n    --------\n\n    ``https://beets.readthedocs.org/``\n\n    :manpage:`beetsconfig(5)`\n"
  },
  {
    "path": "docs/reference/config.rst",
    "content": "Configuration\n=============\n\nBeets has an extensive configuration system that lets you customize nearly every\naspect of its operation. To configure beets, you create a file called\n``config.yaml``. The location of the file depends on your platform (type ``beet\nconfig -p`` to see the path on your system):\n\n- On Unix-like OSes, write ``~/.config/beets/config.yaml``.\n- On Windows, use ``%APPDATA%\\beets\\config.yaml``. This is usually in a\n  directory like ``C:\\Users\\You\\AppData\\Roaming``.\n- On OS X, you can use either the Unix location or ``~/Library/Application\n  Support/beets/config.yaml``.\n\nYou can launch your text editor to create or update your configuration by typing\n``beet config -e``. (See the :ref:`config-cmd` command for details.) It is also\npossible to customize the location of the configuration file and even use\nmultiple layers of configuration. See `Configuration Location`_, below.\n\nThe config file uses YAML_ syntax. You can use the full power of YAML, but most\nconfiguration options are simple key/value pairs. This means your config file\nwill look like this:\n\n::\n\n    option: value\n    another_option: foo\n    bigger_option:\n        key: value\n        foo: bar\n\nIn YAML, you will need to use spaces (not tabs!) to indent some lines. If you\nhave questions about more sophisticated syntax, take a look at the YAML_\ndocumentation.\n\n.. _yaml: https://yaml.org/\n\nThe rest of this page enumerates the dizzying litany of configuration options\navailable in beets. You might also want to see an :ref:`example\n<config-example>`.\n\n.. contents::\n    :local:\n    :depth: 2\n\nGlobal Options\n--------------\n\nThese options control beets' global operation.\n\nlibrary\n~~~~~~~\n\nPath to the beets library file. By default, beets will use a file called\n``library.db`` alongside your configuration file.\n\ndirectory\n~~~~~~~~~\n\nThe directory to which files will be copied/moved when adding them to the\nlibrary. Defaults to a folder called ``Music`` in your home directory.\n\n.. _plugins-config:\n\nplugins\n~~~~~~~\n\nA space-separated list of plugin module names to load. See :ref:`using-plugins`.\n\ninclude\n~~~~~~~\n\nA space-separated list of extra configuration files to include. Filenames are\nrelative to the directory containing ``config.yaml``.\n\npluginpath\n~~~~~~~~~~\n\nDirectories to search for plugins. Each Python file or directory in a plugin\npath represents a plugin and should define a subclass of |BeetsPlugin|. A plugin\ncan then be loaded by adding the plugin name to the ``plugins`` configuration.\nThe plugin path can either be a single string or a list of strings---so, if you\nhave multiple paths, format them as a YAML list like so:\n\n::\n\n    pluginpath:\n        - /path/one\n        - /path/two\n\n.. _ignore:\n\nignore\n~~~~~~\n\nA list of glob patterns specifying file and directory names to be ignored when\nimporting. By default, this consists of ``.*``, ``*~``, ``System Volume\nInformation``, ``lost+found`` (i.e., beets ignores Unix-style hidden files,\nbackup files, and directories that appears at the root of some Linux and Windows\nfilesystems).\n\n.. _ignore_hidden:\n\nignore_hidden\n~~~~~~~~~~~~~\n\nEither ``yes`` or ``no``; whether to ignore hidden files when importing. On\nWindows, the \"Hidden\" property of files is used to detect whether or not a file\nis hidden. On OS X, the file's \"IsHidden\" flag is used to detect whether or not\na file is hidden. On both OS X and other platforms (excluding Windows), files\n(and directories) starting with a dot are detected as hidden files.\n\n.. _replace:\n\nreplace\n~~~~~~~\n\nA set of regular expression/replacement pairs to be applied to all filenames\ncreated by beets. Typically, these replacements are used to avoid confusing\nproblems or errors with the filesystem (for example, leading dots, which hide\nfiles on Unix, and trailing whitespace, which is illegal on Windows). To\noverride these substitutions, specify a mapping from regular expression to\nreplacement strings. For example, ``[xy]: z`` will make beets replace all\ninstances of the characters ``x`` or ``y`` with the character ``z``.\n\nIf you do change this value, be certain that you include at least enough\nsubstitutions to avoid causing errors on your operating system. Here are the\ndefault substitutions used by beets, which are sufficient to avoid unexpected\nbehavior on all popular platforms:\n\n::\n\n    replace:\n        '[\\\\/]': _\n        '^\\.': _\n        '[\\x00-\\x1f]': _\n        '[<>:\"\\?\\*\\|]': _\n        '\\.$': _\n        '\\s+$': ''\n        '^\\s+': ''\n        '^-': _\n\nThese substitutions remove forward and back slashes, leading dots, and control\ncharacters—all of which is a good idea on any OS. The fourth line removes the\nWindows \"reserved characters\" (useful even on Unix for compatibility with\nWindows-influenced network filesystems like Samba). Trailing dots and trailing\nwhitespace, which can cause problems on Windows clients, are also removed.\n\nWhen replacements other than the defaults are used, it is possible that they\nwill increase the length of the path. In the scenario where this leads to a\nconflict with the maximum filename length, the default replacements will be used\nto resolve the conflict and beets will display a warning.\n\nNote that paths might contain special characters such as typographical quotes\n(``“”``). With the configuration above, those will not be replaced as they don't\nmatch the typewriter quote (``\"``). To also strip these special characters, you\ncan either add them to the replacement list or use the :ref:`asciify-paths`\nconfiguration option below.\n\n.. _path-sep-replace:\n\npath_sep_replace\n~~~~~~~~~~~~~~~~\n\nA string that replaces the path separator (for example, the forward slash ``/``\non Linux and MacOS, and the backward slash ``\\\\`` on Windows) when generating\nfilenames with beets. This option is related to :ref:`replace`, but is distinct\nfrom it for technical reasons.\n\n.. warning::\n\n    Changing this option is potentially dangerous. For example, setting it to\n    the actual path separator could create directories in unexpected locations.\n    Use caution when changing it and always try it out on a small number of\n    files before applying it to your whole library.\n\nDefault: ``_``.\n\n.. _asciify-paths:\n\nasciify_paths\n~~~~~~~~~~~~~\n\nConvert all non-ASCII characters in paths to ASCII equivalents.\n\nFor example, if your path template for singletons is ``singletons/$title`` and\nthe title of a track is \"Café\", then the track will be saved as\n``singletons/Cafe.mp3``. The changes take place before applying the\n:ref:`replace` configuration and are roughly equivalent to wrapping all your\npath templates in the ``%asciify{}`` :ref:`template function\n<template-functions>`.\n\nThis uses the `unidecode module <https://pypi.org/project/Unidecode>`__ which is\nlanguage agnostic, so some characters may be transliterated from a different\nlanguage than expected. For example, Japanese kanji will usually use their\nChinese readings.\n\nDefault: ``no``.\n\n.. _art-filename:\n\nart_filename\n~~~~~~~~~~~~\n\nWhen importing album art, the name of the file (without extension) where the\ncover art image should be placed. This is a template string, so you can use any\nof the syntax available to :doc:`/reference/pathformat`. Defaults to ``cover``\n(i.e., images will be named ``cover.jpg`` or ``cover.png`` and placed in the\nalbum's directory).\n\nthreaded\n~~~~~~~~\n\nEither ``yes`` or ``no``, indicating whether the autotagger should use multiple\nthreads. This makes things substantially faster by overlapping work: for\nexample, it can copy files for one album in parallel with looking up data in\nMusicBrainz for a different album. You may want to disable this when debugging\nproblems with the autotagger. Defaults to ``yes``.\n\n.. _format_item:\n\n.. _list_format_item:\n\nformat_item\n~~~~~~~~~~~\n\nFormat to use when listing *individual items* with the :ref:`list-cmd` command\nand other commands that need to print out items. Defaults to ``$artist - $album\n- $title``. The ``-f`` command-line option overrides this setting.\n\nIt used to be named ``list_format_item``.\n\n.. _format_album:\n\n.. _list_format_album:\n\nformat_album\n~~~~~~~~~~~~\n\nFormat to use when listing *albums* with :ref:`list-cmd` and other commands.\nDefaults to ``$albumartist - $album``. The ``-f`` command-line option overrides\nthis setting.\n\nIt used to be named ``list_format_album``.\n\n.. _sort_item:\n\nsort_item\n~~~~~~~~~\n\nDefault sort order to use when fetching items from the database. Defaults to\n``artist+ album+ disc+ track+``. Explicit sort orders override this default.\n\n.. _sort_album:\n\nsort_album\n~~~~~~~~~~\n\nDefault sort order to use when fetching albums from the database. Defaults to\n``albumartist+ album+``. Explicit sort orders override this default.\n\n.. _sort_case_insensitive:\n\nsort_case_insensitive\n~~~~~~~~~~~~~~~~~~~~~\n\nEither ``yes`` or ``no``, indicating whether the case should be ignored when\nsorting lexicographic fields. When set to ``no``, lower-case values will be\nplaced after upper-case values (e.g., *Bar Qux foo*), while ``yes`` would result\nin the more expected *Bar foo Qux*. Default: ``yes``.\n\n.. _original_date:\n\noriginal_date\n~~~~~~~~~~~~~\n\nEither ``yes`` or ``no``, indicating whether matched albums should have their\n``year``, ``month``, and ``day`` fields set to the release date of the\n*original* version of an album rather than the selected version of the release.\nThat is, if this option is turned on, then ``year`` will always equal\n``original_year`` and so on. Default: ``no``.\n\n.. _overwrite_null:\n\noverwrite_null\n~~~~~~~~~~~~~~\n\nThis confusingly-named option indicates which fields have meaningful ``null``\nvalues. If an album or track field is in the corresponding list, then an\nexisting value for this field in an item in the database can be overwritten with\n``null``. By default, however, ``null`` is interpreted as information about the\nfield being unavailable, so it would not overwrite existing values. For example:\n\n::\n\n    overwrite_null:\n        album: [\"albumid\"]\n        track: [\"title\", \"date\"]\n\n.. _artist_credit:\n\nartist_credit\n~~~~~~~~~~~~~\n\nEither ``yes`` or ``no``, indicating whether matched tracks and albums should\nuse the artist credit, rather than the artist. That is, if this option is turned\non, then ``artist`` will contain the artist as credited on the release.\n\n.. _per_disc_numbering:\n\nper_disc_numbering\n~~~~~~~~~~~~~~~~~~\n\nA boolean controlling the track numbering style on multi-disc releases. By\ndefault (``per_disc_numbering: no``), tracks are numbered per-release, so the\nfirst track on the second disc has track number N+1 where N is the number of\ntracks on the first disc. If this ``per_disc_numbering`` is enabled, then the\nfirst (non-pregap) track on each disc always has track number 1.\n\nIf you enable ``per_disc_numbering``, you will likely want to change your\n:ref:`path-format-config` also to include ``$disc`` before ``$track`` to make\nfilenames sort correctly in album directories. For example, you might want to\nuse a path format like this:\n\n::\n\n    paths:\n        default: $albumartist/$album%aunique{}/$disc-$track $title\n\nWhen this option is off (the default), even \"pregap\" hidden tracks are numbered\nfrom one, not zero, so other track numbers may appear to be bumped up by one.\nWhen it is on, the pregap track for each disc can be numbered zero.\n\n.. _config-aunique:\n\naunique\n~~~~~~~\n\nThese options are used to generate a string that is guaranteed to be unique\namong all albums in the library who share the same set of keys.\n\nThe defaults look like this:\n\n::\n\n    aunique:\n        keys: albumartist album\n        disambiguators: albumtype year label catalognum albumdisambig releasegroupdisambig\n        bracket: '[]'\n\nSee :ref:`aunique` for more details.\n\n.. _config-sunique:\n\nsunique\n~~~~~~~\n\nLike :ref:`config-aunique` above for albums, these options control the\ngeneration of a unique string to disambiguate *singletons* that share similar\nmetadata.\n\nThe defaults look like this:\n\n::\n\n    sunique:\n        keys: artist title\n        disambiguators: year trackdisambig\n        bracket: '[]'\n\nSee :ref:`sunique` for more details.\n\n.. _terminal_encoding:\n\nterminal_encoding\n~~~~~~~~~~~~~~~~~\n\nThe text encoding, as `known to Python\n<https://docs.python.org/3/library/codecs.html#standard-encodings>`__, to use\nfor messages printed to the standard output. It's also used to read messages\nfrom the standard input. By default, this is determined automatically from the\nlocale environment variables.\n\n.. _clutter:\n\nclutter\n~~~~~~~\n\nWhen beets imports all the files in a directory, it tries to remove the\ndirectory if it's empty. A directory is considered empty if it only contains\nfiles whose names match the glob patterns in ``clutter``, which should be a list\nof strings. The default list consists of \"Thumbs.DB\" and \".DS_Store\".\n\nThe importer only removes recursively searched subdirectories---the top-level\ndirectory you specify on the command line is never deleted.\n\n.. _max_filename_length:\n\nmax_filename_length\n~~~~~~~~~~~~~~~~~~~\n\nSet the maximum number of characters in a filename, after which names will be\ntruncated. By default, beets tries to ask the filesystem for the correct\nmaximum.\n\n.. _id3v23:\n\nid3v23\n~~~~~~\n\nBy default, beets writes MP3 tags using the ID3v2.4 standard, the latest version\nof ID3. Enable this option to instead use the older ID3v2.3 standard, which is\npreferred by certain older software such as Windows Media Player.\n\n.. _va_name:\n\nva_name\n~~~~~~~\n\nSets the albumartist for various-artist compilations. Defaults to ``'Various\nArtists'`` (the MusicBrainz standard). Affects other sources, such as\n:doc:`/plugins/discogs`, too.\n\n.. _ui_options:\n\nUI Options\n----------\n\nThe options that allow for customization of the visual appearance of the console\ninterface.\n\ncolor\n~~~~~\n\nEither ``yes`` or ``no``; whether to use color in console output. Turn this off\nif your terminal doesn't support ANSI colors.\n\n.. _colors:\n\ncolors\n~~~~~~\n\nThe colors that are used throughout the user interface. These are only used if\nthe ``color`` option is set to ``yes``. See the default configuration:\n\n.. code-block:: yaml\n\n    ui:\n        colors:\n            text_success: ['bold', 'green']\n            text_warning: ['bold', 'yellow']\n            text_error: ['bold', 'red']\n            text_highlight: ['bold', 'red']\n            text_highlight_minor: ['white']\n            action_default: ['bold', 'cyan']\n            action: ['bold', 'cyan']\n            # New colors after UI overhaul\n            text_faint: ['faint']\n            import_path: ['bold', 'blue']\n            import_path_items: ['bold', 'blue']\n            changed: ['yellow']\n            text_diff_added: ['bold', 'green']\n            text_diff_removed: ['bold', 'red']\n            action_description: ['white']\n\nAvailable attributes:\n\nForeground colors\n    ``black``, ``red``, ``green``, ``yellow``, ``blue``, ``magenta``, ``cyan``,\n    ``white``, ``bright_black``, ``bright_red``, ``bright_green``,\n    ``bright_yellow``, ``bright_blue``, ``bright_magenta``, ``bright_cyan``,\n    ``bright_white``\n\nBackground colors\n    ``bg_black``, ``bg_red``, ``bg_green``, ``bg_yellow``, ``bg_blue``,\n    ``bg_magenta``, ``bg_cyan``, ``bg_white``, ``bg_bright_black``,\n    ``bg_bright_red``, ``bg_bright_green``, ``bg_bright_yellow``,\n    ``bg_bright_blue``, ``bg_bright_magenta``, ``bg_bright_cyan``,\n    ``bg_bright_white``\n\nText styles\n    ``normal``, ``bold``, ``faint``, ``italic``, ``underline``, ``blink_slow``,\n    ``blink_rapid``, ``inverse``, ``conceal``, ``crossed_out``\n\nterminal_width\n~~~~~~~~~~~~~~\n\nControls line wrapping on non-Unix systems. On Unix systems, the width of the\nterminal is detected automatically. If this fails, or on non-Unix systems, the\nspecified value is used as a fallback. Defaults to ``80`` characters:\n\n.. code-block:: yaml\n\n    ui:\n        terminal_width: 80\n\nlength_diff_thresh\n~~~~~~~~~~~~~~~~~~\n\nBeets compares the length of the imported track with the length the metadata\nsource provides. If any tracks differ by at least ``length_diff_thresh``\nseconds, they will be colored with ``text_highlight``. Below this threshold,\ndifferent track lengths are colored with ``text_highlight_minor``.\n``length_diff_thresh`` does not impact which releases are selected in autotagger\nmatching or distance score calculation (see :ref:`match-config`,\n``distance_weights`` and :ref:`colors`):\n\n.. code-block:: yaml\n\n    ui:\n        length_diff_thresh: 10.0\n\nimport\n~~~~~~\n\nWhen importing, beets will read several options to configure the visuals of the\nimport dialogue. There are two layouts controlling how horizontal space and line\nwrapping is dealt with: ``column`` and ``newline``. The indentation of the\nrespective elements of the import UI can also be configured. For example setting\n``2`` for ``match_header`` will indent the very first block of a proposed match\nby two characters in the terminal:\n\n.. code-block:: yaml\n\n    ui:\n        import:\n            indentation:\n                match_header: 2\n                match_details: 2\n                match_tracklist: 5\n            layout: column\n\nImporter Options\n----------------\n\nThe options that control the :ref:`import-cmd` command are indented under the\n``import:`` key. For example, you might have a section in your configuration\nfile that looks like this:\n\n::\n\n    import:\n        write: yes\n        copy: yes\n        resume: no\n\nThese options are available in this section:\n\n.. _config-import-write:\n\nwrite\n~~~~~\n\nEither ``yes`` or ``no``, controlling whether metadata (e.g., ID3) tags are\nwritten to files when using ``beet import``. Defaults to ``yes``. The ``-w`` and\n``-W`` command-line options override this setting.\n\n.. _config-import-copy:\n\ncopy\n~~~~\n\nEither ``yes`` or ``no``, indicating whether to **copy** files into the library\ndirectory when using ``beet import``. Defaults to ``yes``. Can be overridden\nwith the ``-c`` and ``-C`` command-line options.\n\nThe option is ignored if ``move`` is enabled (i.e., beets can move or copy files\nbut it doesn't make sense to do both).\n\n.. _config-import-move:\n\nmove\n~~~~\n\nEither ``yes`` or ``no``, indicating whether to **move** files into the library\ndirectory when using ``beet import``. Defaults to ``no``.\n\nThe effect is similar to the ``copy`` option but you end up with only one copy\nof the imported file. (\"Moving\" works even across filesystems; if necessary,\nbeets will copy and then delete when a simple rename is impossible.) Moving\nfiles can be risky—it's a good idea to keep a backup in case beets doesn't do\nwhat you expect with your files.\n\nThis option *overrides* ``copy``, so enabling it will always move (and not copy)\nfiles. The ``-c`` switch to the ``beet import`` command, however, still takes\nprecedence.\n\n.. _link:\n\nlink\n~~~~\n\nEither ``yes`` or ``no``, indicating whether to use symbolic links instead of\nmoving or copying files. (It conflicts with the ``move``, ``copy`` and\n``hardlink`` options.) Defaults to ``no``.\n\nThis option only works on platforms that support symbolic links: i.e., Unixes.\nIt will fail on Windows.\n\nIt's likely that you'll also want to set ``write`` to ``no`` if you use this\noption to preserve the metadata on the linked files.\n\n.. _hardlink:\n\nhardlink\n~~~~~~~~\n\nEither ``yes`` or ``no``, indicating whether to use hard links instead of\nmoving, copying, or symlinking files. (It conflicts with the ``move``, ``copy``,\nand ``link`` options.) Defaults to ``no``.\n\nAs with symbolic links (see :ref:`link`, above), this will not work on Windows\nand you will want to set ``write`` to ``no``. Otherwise, metadata on the\noriginal file will be modified.\n\n.. _reflink:\n\nreflink\n~~~~~~~\n\nEither ``yes``, ``no``, or ``auto``, indicating whether to use copy-on-write\n`file clones`_ (a.k.a. \"reflinks\") instead of copying or moving files. The\n``auto`` option uses reflinks when possible and falls back to plain copying when\nnecessary. Defaults to ``no``.\n\nThis kind of clone is only available on certain filesystems: for example, btrfs\nand APFS. For more details on filesystem support, see the pyreflink_\ndocumentation. Note that you need to install ``pyreflink``, either through\n``python -m pip install beets[reflink]`` or ``python -m pip install reflink``.\n\nThe option is ignored if ``move`` is enabled (i.e., beets can move or copy files\nbut it doesn't make sense to do both).\n\n.. _file clones: https://en.wikipedia.org/wiki/Copy-on-write\n\n.. _pyreflink: https://reflink.readthedocs.io/en/latest/\n\nresume\n~~~~~~\n\nEither ``yes``, ``no``, or ``ask``. Controls whether interrupted imports should\nbe resumed. \"Yes\" means that imports are always resumed when possible; \"no\"\nmeans resuming is disabled entirely; \"ask\" (the default) means that the user\nshould be prompted when resuming is possible. The ``-p`` and ``-P`` flags\ncorrespond to the \"yes\" and \"no\" settings and override this option.\n\n.. _incremental:\n\nincremental\n~~~~~~~~~~~\n\nEither ``yes`` or ``no``, controlling whether imported directories are recorded\nand whether these recorded directories are skipped. This corresponds to the\n``-i`` flag to ``beet import``.\n\n.. _incremental_skip_later:\n\nincremental_skip_later\n~~~~~~~~~~~~~~~~~~~~~~\n\nEither ``yes`` or ``no``, controlling whether skipped directories are recorded\nin the incremental list. When set to ``yes``, skipped directories won't be\nrecorded, and beets will try to import them again later. When set to ``no``,\nskipped directories will be recorded, and skipped later. Defaults to ``no``.\n\n.. _from_scratch:\n\nfrom_scratch\n~~~~~~~~~~~~\n\nEither ``yes`` or ``no`` (default), controlling whether existing metadata is\ndiscarded when a match is applied. This corresponds to the ``--from-scratch``\nflag to ``beet import``.\n\n.. _quiet:\n\nquiet\n~~~~~\n\nEither ``yes`` or ``no`` (default), controlling whether to ask for a manual\ndecision from the user when the importer is unsure how to proceed. This\ncorresponds to the ``--quiet`` flag to ``beet import``.\n\n.. _quiet_fallback:\n\nquiet_fallback\n~~~~~~~~~~~~~~\n\nEither ``skip`` (default) or ``asis``, specifying what should happen in quiet\nmode (see the ``-q`` flag to ``import``, above) when there is no strong\nrecommendation.\n\n.. _none_rec_action:\n\nnone_rec_action\n~~~~~~~~~~~~~~~\n\nEither ``ask`` (default), ``asis`` or ``skip``. Specifies what should happen\nduring an interactive import session when there is no recommendation. Useful\nwhen you are only interested in processing medium and strong recommendations\ninteractively.\n\ntimid\n~~~~~\n\nEither ``yes`` or ``no``, controlling whether the importer runs in *timid* mode,\nin which it asks for confirmation on every autotagging match, even the ones that\nseem very close. Defaults to ``no``. The ``-t`` command-line flag controls the\nsame setting.\n\n.. _import_log:\n\nlog\n~~~\n\nSpecifies a filename where the importer's log should be kept. By default, no log\nis written. This can be overridden with the ``-l`` flag to ``import``.\n\n.. _default_action:\n\ndefault_action\n~~~~~~~~~~~~~~\n\nOne of ``apply``, ``skip``, ``asis``, or ``none``, indicating which option\nshould be the *default* when selecting an action for a given match. This is the\naction that will be taken when you type return without an option letter. The\ndefault is ``apply``.\n\n.. _languages:\n\nlanguages\n~~~~~~~~~\n\nA list of locale names to search for preferred aliases. For example, setting\nthis to ``en`` uses the transliterated artist name \"Pyotr Ilyich Tchaikovsky\"\ninstead of the Cyrillic script for the composer's name when tagging from\nMusicBrainz. You can use a space-separated list of language abbreviations, like\n``en jp es``, to specify a preference order. Defaults to an empty list, meaning\nthat no language is preferred.\n\nThe alias is used for artist name, track title, release group title and album\ntitle.\n\n.. _ignored_alias_types:\n\nignored_alias_types\n~~~~~~~~~~~~~~~~~~~\n\nA list of alias types to be ignored when importing new items.\n\nSee the ``MusicBrainz Documentation`` for more information on aliases.\n\n.._MusicBrainz Documentation: https://musicbrainz.org/doc/Aliases\n\n.. _detail:\n\ndetail\n~~~~~~\n\nWhether the importer UI should show detailed information about each match it\nfinds. When enabled, this mode prints out the title of every track, regardless\nof whether it matches the original metadata. (The default behavior only shows\nchanges.) Default: ``no``.\n\n.. _group_albums:\n\ngroup_albums\n~~~~~~~~~~~~\n\nBy default, the beets importer groups tracks into albums based on the\ndirectories they reside in. This option instead uses files' metadata to\npartition albums. Enable this option if you have directories that contain tracks\nfrom many albums mixed together.\n\nThe ``--group-albums`` or ``-g`` option to the :ref:`import-cmd` command is\nequivalent, and the *G* interactive option invokes the same workflow.\n\nDefault: ``no``.\n\n.. _autotag:\n\nautotag\n~~~~~~~\n\nBy default, the beets importer always attempts to autotag new music. If most of\nyour collection consists of obscure music, you may be interested in disabling\nautotagging by setting this option to ``no``. (You can re-enable it with the\n``-a`` flag to the :ref:`import-cmd` command.)\n\nDefault: ``yes``.\n\n.. _duplicate_keys:\n\nduplicate_keys\n~~~~~~~~~~~~~~\n\nThe fields used to find duplicates when importing. There are two sub-values\nhere: ``album`` and ``item``. Each one is a list of field names; if an existing\nobject (album or item) in the library matches the new object on all of these\nfields, the importer will consider it a duplicate.\n\nDefault:\n\n::\n\n    album: albumartist album\n    item: artist title\n\n.. _duplicate_action:\n\nduplicate_action\n~~~~~~~~~~~~~~~~\n\nEither ``skip``, ``keep``, ``remove``, ``merge`` or ``ask``. Controls how\nduplicates are treated in import task. \"skip\" means that new item(album or\ntrack) will be skipped; \"keep\" means keep both old and new items; \"remove\" means\nremove old item; \"merge\" means merge into one album; \"ask\" means the user should\nbe prompted for the action each time. The default is ``ask``.\n\n.. _duplicate_verbose_prompt:\n\nduplicate_verbose_prompt\n~~~~~~~~~~~~~~~~~~~~~~~~\n\nUsually when duplicates are detected during import, information about the\nexisting and the newly imported album is summarized. Enabling this option also\nlists details on individual tracks. The :ref:`format_item setting <format_item>`\nis applied, which would, considering the default, look like this:\n\n.. code-block:: console\n\n    This item is already in the library!\n    Old: 1 items, MP3, 320kbps, 5:56, 13.6 MiB\n      Artist Name - Album Name - Third Track Title\n    New: 2 items, MP3, 320kbps, 7:18, 17.1 MiB\n      Artist Name - Album Name - First Track Title\n      Artist Name - Album Name - Second Track Title\n    [S]kip new, Keep all, Remove old, Merge all?\n\nDefault: ``no``.\n\n.. _bell:\n\nbell\n~~~~\n\nRing the terminal bell to get your attention when the importer needs your input.\n\nDefault: ``no``.\n\n.. _set_fields:\n\nset_fields\n~~~~~~~~~~\n\nA dictionary indicating fields to set to values for newly imported music. Here's\nan example:\n\n.. code-block:: yaml\n\n    set_fields:\n        genres: To Listen\n        collection: Unordered\n\nOther field/value pairs supplied via the ``--set`` option on the command-line\noverride any settings here for fields with the same name.\n\nValues support the same template syntax as beets' :doc:`path formats\n<pathformat>`.\n\nFields are set on both the album and each individual track of the album. Fields\nare persisted to the media files of each track.\n\nDefault: ``{}`` (empty).\n\n.. _singleton_album_disambig:\n\nsingleton_album_disambig\n~~~~~~~~~~~~~~~~~~~~~~~~\n\nDuring singleton imports and if the metadata source provides it, album names are\nappended to the disambiguation string of matching track candidates. For example:\n``The Artist - The Title (Discogs, Index 3, Track B1, [The Album]``. This\nfeature is currently supported by the :doc:`/plugins/discogs` and the\n:doc:`/plugins/spotify`.\n\nDefault: ``yes``.\n\n.. _match-config:\n\nAutotagger Matching Options\n---------------------------\n\nYou can configure some aspects of the logic beets uses when automatically\nmatching MusicBrainz results under the ``match:`` section. To control how\n*tolerant* the autotagger is of differences, use the ``strong_rec_thresh``\noption, which reflects the distance threshold below which beets will make a\n\"strong recommendation\" that the metadata be used. Strong recommendations are\naccepted automatically (except in \"timid\" mode), so you can use this to make\nbeets ask your opinion more or less often.\n\nThe threshold is a *distance* value between 0.0 and 1.0, so you can think of it\nas the opposite of a *similarity* value. For example, if you want to\nautomatically accept any matches above 90% similarity, use:\n\n::\n\n    match:\n        strong_rec_thresh: 0.10\n\nThe default strong recommendation threshold is 0.04.\n\nThe ``medium_rec_thresh`` and ``rec_gap_thresh`` options work similarly. When a\nmatch is below the *medium* recommendation threshold or the distance between it\nand the next-best match is above the *gap* threshold, the importer will suggest\nthat match but not automatically confirm it. Otherwise, you'll see a list of\noptions to choose from.\n\n.. _distance-weights:\n\ndistance_weights\n~~~~~~~~~~~~~~~~\n\nThe ``distance_weights`` option allows you to customize how much each field\ncontributes to the overall distance score when matching albums and tracks.\nHigher weights mean that differences in that field are penalized more heavily,\nmaking them more important in the matching decision.\n\nThe defaults are:\n\n.. code-block:: yaml\n\n    match:\n        distance_weights:\n            data_source: 2.0\n            artist: 3.0\n            album: 3.0\n            media: 1.0\n            mediums: 1.0\n            year: 1.0\n            country: 0.5\n            label: 0.5\n            catalognum: 0.5\n            albumdisambig: 0.5\n            album_id: 5.0\n            tracks: 2.0\n            missing_tracks: 0.9\n            unmatched_tracks: 0.6\n            track_title: 3.0\n            track_artist: 2.0\n            track_index: 1.0\n            track_length: 2.0\n            track_id: 5.0\n            medium: 1.0\n\nFor example, if you don't care as much about matching the exact release year,\nyou can reduce its weight:\n\n.. code-block:: yaml\n\n    match:\n        distance_weights:\n            year: 0.1\n\nYou only need to specify the fields you want to override; unspecified fields\nkeep their default weights.\n\n.. _max_rec:\n\nmax_rec\n~~~~~~~\n\nAs mentioned above, autotagger matches have *recommendations* that control how\nthe UI behaves for a certain quality of match. The recommendation for a certain\nmatch is based on the overall distance calculation. But you can also control the\nrecommendation when a specific distance penalty is applied by defining *maximum*\nrecommendations for each field:\n\nTo define maxima, use keys under ``max_rec:`` in the ``match`` section. The\ndefaults are \"medium\" for missing and unmatched tracks and \"strong\" (i.e., no\nmaximum) for everything else:\n\n::\n\n    match:\n        max_rec:\n            missing_tracks: medium\n            unmatched_tracks: medium\n\nIf a recommendation is higher than the configured maximum and the indicated\npenalty is applied, the recommendation is downgraded. The setting for each field\ncan be one of ``none``, ``low``, ``medium`` or ``strong``. When the maximum\nrecommendation is ``strong``, no \"downgrading\" occurs. The available penalty\nnames here are:\n\n- data_source\n- artist\n- album\n- media\n- mediums\n- year\n- country\n- label\n- catalognum\n- albumdisambig\n- album_id\n- tracks\n- missing_tracks\n- unmatched_tracks\n- track_title\n- track_artist\n- track_index\n- track_length\n- track_id\n\n.. _preferred:\n\npreferred\n~~~~~~~~~\n\nIn addition to comparing the tagged metadata with the match metadata for\nsimilarity, you can also specify an ordered list of preferred countries and\nmedia types.\n\nA distance penalty will be applied if the country or media type from the match\nmetadata doesn't match. The specified values are preferred in descending order\n(i.e., the first item will be most preferred). Each item may be a regular\nexpression, and will be matched case insensitively. The number of media will be\nstripped when matching preferred media (e.g. \"2x\" in \"2xCD\").\n\nYou can also tell the autotagger to prefer matches that have a release year\nclosest to the original year for an album.\n\nHere's an example:\n\n::\n\n    match:\n        preferred:\n            countries: ['US', 'GB|UK']\n            media: ['CD', 'Digital Media|File']\n            original_year: yes\n\nBy default, none of these options are enabled.\n\n.. _ignored:\n\nignored\n~~~~~~~\n\nYou can completely avoid matches that have certain penalties applied by adding\nthe penalty name to the ``ignored`` setting:\n\n::\n\n    match:\n        ignored: missing_tracks unmatched_tracks\n\nThe available penalties are the same as those for the :ref:`max_rec` setting.\n\nFor example, setting ``ignored: missing_tracks`` will skip any album matches\nwhere your audio files are missing some of the tracks. The importer will not\nattempt to display these matches. It does not ignore the fact that the album is\nmissing tracks, which would allow these matches to apply more easily. To do\nthat, you'll want to adjust the penalty for missing tracks.\n\n.. _required:\n\nrequired\n~~~~~~~~\n\nYou can avoid matches that lack certain required information. Add the tags you\nwant to enforce to the ``required`` setting:\n\n::\n\n    match:\n        required: year label catalognum country\n\nNo tags are required by default.\n\n.. _ignored_media:\n\nignored_media\n~~~~~~~~~~~~~\n\nA list of media (i.e., formats) in metadata databases to ignore when matching\nmusic. You can use this to ignore all media that usually contain video instead\nof audio, for example:\n\n::\n\n    match:\n        ignored_media: ['Data CD', 'DVD', 'DVD-Video', 'Blu-ray', 'HD-DVD',\n                        'VCD', 'SVCD', 'UMD', 'VHS']\n\nNo formats are ignored by default.\n\n.. _ignore_data_tracks:\n\nignore_data_tracks\n~~~~~~~~~~~~~~~~~~\n\nBy default, audio files contained in data tracks within a release are included\nin the album's tracklist. If you want them to be included, set it ``no``.\n\nDefault: ``yes``.\n\n.. _ignore_video_tracks:\n\nignore_video_tracks\n~~~~~~~~~~~~~~~~~~~\n\nBy default, video tracks within a release will be ignored. If you want them to\nbe included (for example if you would like to track the audio-only versions of\nthe video tracks), set it to ``no``.\n\nDefault: ``yes``.\n\n.. _path-format-config:\n\nPath Format Configuration\n-------------------------\n\nYou can also configure the directory hierarchy beets uses to store music. These\nsettings appear under the ``paths:`` key. Each string is a template string that\ncan refer to metadata fields like ``$artist`` or ``$title``. The filename\nextension is added automatically. At the moment, you can specify three special\npaths: ``default`` for most releases, ``comp`` for \"various artist\" releases\nwith no dominant artist, and ``singleton`` for non-album tracks. The defaults\nlook like this:\n\n::\n\n    paths:\n        default: $albumartist/$album%aunique{}/$track $title\n        singleton: Non-Album/$artist/$title\n        comp: Compilations/$album%aunique{}/$track $title\n\nNote the use of ``$albumartist`` instead of ``$artist``; this ensures that\nalbums will be well-organized. For more about these format strings, see\n:doc:`pathformat`. The ``aunique{}`` function ensures that identically-named\nalbums are placed in different directories; see :ref:`aunique` for details.\n\nIn addition to ``default``, ``comp``, and ``singleton``, you can condition path\nqueries based on beets queries (see :doc:`/reference/query`). This means that a\nconfig file like this:\n\n::\n\n    paths:\n        albumtype:soundtrack: Soundtracks/$album/$track $title\n\nwill place soundtrack albums in a separate directory. The queries are tested in\nthe order they appear in the configuration file, meaning that if an item matches\nmultiple queries, beets will use the path format for the *first* matching query.\n\nNote that the special ``singleton`` and ``comp`` path format conditions are, in\nfact, just shorthand for the explicit queries ``singleton:true`` and\n``comp:true``. In contrast, ``default`` is special and has no query equivalent:\nthe ``default`` format is only used if no queries match.\n\nConfiguration Location\n----------------------\n\nThe beets configuration file is usually located in a standard location that\ndepends on your OS, but there are a couple of ways you can tell beets where to\nlook.\n\nEnvironment Variable\n~~~~~~~~~~~~~~~~~~~~\n\nFirst, you can set the ``BEETSDIR`` environment variable to a directory\ncontaining a ``config.yaml`` file. This replaces your configuration in the\ndefault location. This also affects where auxiliary files, like the library\ndatabase, are stored by default (that's where relative paths are resolved to).\nThis environment variable is useful if you need to manage multiple beets\nlibraries with separate configurations.\n\nCommand-Line Option\n~~~~~~~~~~~~~~~~~~~\n\nAlternatively, you can use the ``--config`` command-line option to indicate a\nYAML file containing options that will then be merged with your existing options\n(from ``BEETSDIR`` or the default locations). This is useful if you want to keep\nyour configuration mostly the same but modify a few options as a batch. For\nexample, you might have different strategies for importing files, each with a\ndifferent set of importer options.\n\nDefault Location\n~~~~~~~~~~~~~~~~\n\nIn the absence of a ``BEETSDIR`` variable, beets searches a few places for your\nconfiguration, depending on the platform:\n\n- On Unix platforms, including OS X:``~/.config/beets`` and then\n  ``$XDG_CONFIG_DIR/beets``, if the environment variable is set.\n- On OS X, we also search ``~/Library/Application Support/beets`` before the\n  Unixy locations.\n- On Windows: ``~\\AppData\\Roaming\\beets``, and then ``%APPDATA%\\beets``, if the\n  environment variable is set.\n\nBeets uses the first directory in your platform's list that contains\n``config.yaml``. If no config file exists, the last path in the list is used.\n\n.. _config-example:\n\nExample\n-------\n\nHere's an example file:\n\n::\n\n    directory: /var/mp3\n    import:\n        copy: yes\n        write: yes\n        log: beetslog.txt\n    art_filename: albumart\n    plugins: bpd\n    pluginpath: ~/beets/myplugins\n    ui:\n        color: yes\n\n    paths:\n        default: %first{$genres}/$albumartist/$album/$track $title\n        singleton: Singletons/$artist - $title\n        comp: %first{$genres}/$album/$track $title\n        albumtype:soundtrack: Soundtracks/$album/$track $title\n\n.. only:: man\n\n    See Also\n    --------\n\n    ``https://beets.readthedocs.org/``\n\n    :manpage:`beet(1)`\n"
  },
  {
    "path": "docs/reference/index.rst",
    "content": "Reference\n=========\n\nThis section contains reference materials for various parts of beets. To get\nstarted with beets as a new user, though, you may want to read the\n:doc:`/guides/main` guide first.\n\n.. toctree::\n    :maxdepth: 2\n\n    cli\n    config\n    pathformat\n    query\n"
  },
  {
    "path": "docs/reference/pathformat.rst",
    "content": "Path Formats\n============\n\nThe ``paths:`` section of the config file (see :doc:`config`) lets you specify\nthe directory and file naming scheme for your music library. Templates\nsubstitute symbols like ``$title`` (any field value prefixed by ``$``) with the\nappropriate value from the track's metadata. Beets adds the filename extension\nautomatically.\n\nFor example, consider this path format string: ``$albumartist/$album/$track\n$title``\n\nHere are some paths this format will generate:\n\n- ``Yeah Yeah Yeahs/It's Blitz!/01 Zero.mp3``\n- ``Spank Rock/YoYoYoYoYo/11 Competition.mp3``\n- ``The Magnetic Fields/Realism/01 You Must Be Out of Your Mind.mp3``\n\nBecause ``$`` is used to delineate a field reference, you can use ``$$`` to emit\na dollars sign. As with `Python template strings`_, ``${title}`` is equivalent\nto ``$title``; you can use this if you need to separate a field name from the\ntext that follows it.\n\n.. _python template strings: https://docs.python.org/3/library/string.html#template-strings-strings\n\nA Note About Artists\n--------------------\n\nNote that in path formats, you almost certainly want to use ``$albumartist`` and\nnot ``$artist``. The latter refers to the \"track artist\" when it is present,\nwhich means that albums that have tracks from different artists on them (like\n`Stop Making Sense`_, for example) will be placed into different folders!\nContinuing with the Stop Making Sense example, you'll end up with most of the\ntracks in a \"Talking Heads\" directory and one in a \"Tom Tom Club\" directory. You\nprobably don't want that! So use ``$albumartist``.\n\n.. _stop making sense: https://musicbrainz.org/release/798dcaab-0f1a-4f02-a9cb-61d5b0ddfd36\n\nAs a convenience, however, beets allows ``$albumartist`` to fall back to the\nvalue for ``$artist`` and vice-versa if one tag is present but the other is not.\n\n.. _template-functions:\n\nTemplate Functions\n------------------\n\nBeets path formats also support *function calls*, which can be used to transform\ntext and perform logical manipulations. The syntax for function calls is like\nthis: ``%func{arg,arg}``. For example, the ``upper`` function makes its argument\nupper-case, so ``%upper{beets rocks}`` will be replaced with ``BEETS ROCKS``.\nYou can, of course, nest function calls and place variable references in\nfunction arguments, so ``%upper{$artist}`` becomes the upper-case version of the\ntrack's artists.\n\nThese functions are built in to beets:\n\n- ``%lower{text}``: Convert ``text`` to lowercase.\n- ``%upper{text}``: Convert ``text`` to UPPERCASE.\n- ``%capitalize{text}``: Make the first letter of ``text`` UPPERCASE and the\n  rest lowercase.\n- ``%title{text}``: Convert ``text`` to Title Case.\n- ``%left{text,n}``: Return the first ``n`` characters of ``text``.\n- ``%right{text,n}``: Return the last ``n`` characters of ``text``.\n- ``%if{condition,text}`` or ``%if{condition,truetext,falsetext}``: If\n  ``condition`` is not empty, and not one of the values ``0`` or ``false`` (case\n  insensitive), then returns the second argument. Otherwise, returns the third\n  argument if specified (or nothing if ``falsetext`` is left off).\n- ``%asciify{text}``: Convert non-ASCII characters to their ASCII equivalents.\n  For example, \"café\" becomes \"cafe\". Uses the mapping provided by the\n  `unidecode module`_. See the :ref:`asciify-paths` configuration option.\n- ``%aunique{identifiers,disambiguators,brackets}``: Provides a unique string to\n  disambiguate similar albums in the database. See :ref:`aunique`, below.\n- ``%sunique{identifiers,disambiguators,brackets}``: Similarly, a unique string\n  to disambiguate similar singletons in the database. See :ref:`sunique`, below.\n- ``%time{date_time,format}``: Return the date and time in any format accepted\n  by strftime_. For example, to get the year some music was added to your\n  library, use ``%time{$added,%Y}``.\n- ``%first{text,count,skip,sep,join}``: Extract a subset of items from a\n  delimited string. Splits ``text`` by ``sep``, skips the first ``skip`` items,\n  then returns the next ``count`` items joined by ``join``.\n\n  This is especially useful for multi-valued fields like ``artists`` or\n  ``genres`` where you may only want the first artist or a limited number of\n  genres in a path.\n\n  Defaults:\n\n  ..\n      Comically, you need to follow |semicolon_space| with some punctuation to\n      make sure it gets rendered correctly as '; ' in the docs.\n\n  - **count**: 1,\n  - **skip**: 0,\n  - **sep**: |semicolon_space|,\n  - **join**: |semicolon_space|.\n\n  Examples:\n\n  ::\n\n      %first{$genres}             →  returns the first genre\n      %first{$genres,2}           →  returns the first two genres, joined by \"; \"\n      %first{$genres,2,1}         →  skips the first genre, returns the next two\n      %first{$genres,2,0, , -> }  →  splits by space, joins with \" -> \"\n\n- ``%ifdef{field}``, ``%ifdef{field,truetext}`` or\n  ``%ifdef{field,truetext,falsetext}``: Checks if an flexible attribute\n  ``field`` is defined. If it exists, then return ``truetext`` or ``field``\n  (default). Otherwise, returns ``falsetext``. The ``field`` should be entered\n  without ``$``. Note that this doesn't work with built-in :ref:`itemfields`, as\n  they are always defined.\n\n.. _strftime: https://docs.python.org/3/library/time.html#time.strftime\n\n.. _unidecode module: https://pypi.org/project/Unidecode\n\nPlugins can extend beets with more template functions (see\n:ref:`templ_plugins`).\n\n.. _aunique:\n\nAlbum Disambiguation\n--------------------\n\nOccasionally, bands release two albums with the same name (c.f. Crystal Castles,\nWeezer, and any situation where a single has the same name as an album or EP).\nBeets ships with special support, in the form of the ``%aunique{}`` template\nfunction, to avoid placing two identically-named albums in the same directory on\ndisk.\n\nThe ``aunique`` function detects situations where two albums have some identical\nfields and emits text from additional fields to disambiguate the albums. For\nexample, if you have both Crystal Castles albums in your library, ``%aunique{}``\nwill expand to \"[2008]\" for one album and \"[2010]\" for the other. The function\ndetects that you have two albums with the same artist and title but that they\nhave different release years.\n\nFor full flexibility, the ``%aunique`` function takes three arguments. The first\ntwo are whitespace-separated lists of album field names: a set of *identifiers*\nand a set of *disambiguators*. The third argument is a pair of characters used\nto surround the disambiguator.\n\nAny group of albums with identical values for all the identifiers will be\nconsidered \"duplicates\". Then, the function tries each disambiguator field,\nlooking for one that distinguishes each of the duplicate albums from each other.\nThe first such field is used as the result for ``%aunique``. If no field\nsuffices, an arbitrary number is used to distinguish the two albums.\n\nThe default identifiers are ``albumartist album`` and the default disambiguators\nare ``albumtype year label catalognum albumdisambig releasegroupdisambig``. So\nyou can get reasonable disambiguation behavior if you just use ``%aunique{}``\nwith no parameters in your path forms (as in the default path formats), but you\ncan customize the disambiguation if, for example, you include the year by\ndefault in path formats.\n\nThe default characters used as brackets are ``[]``. To change this, provide a\nthird argument to the ``%aunique`` function consisting of two characters: the\nleft and right brackets. Or, to turn off bracketing entirely, leave argument\nblank.\n\nOne caveat: When you import an album that is named identically to one already in\nyour library, the *first* album—the one already in your library— will not\nconsider itself a duplicate at import time. This means that ``%aunique{}`` will\nexpand to nothing for this album and no disambiguation string will be used at\nits import time. Only the second album will receive a disambiguation string. If\nyou want to add the disambiguation string to both albums, just run ``beet move``\n(possibly restricted by a query) to update the paths for the albums.\n\n.. _sunique:\n\nSingleton Disambiguation\n------------------------\n\nIt is also possible to have singleton tracks with the same name and the same\nartist. Beets provides the ``%sunique{}`` template to avoid giving these tracks\nthe same file path.\n\nIt has the same arguments as the :ref:`%aunique <aunique>` template, but the\ndefault values are different. The default identifiers are ``artist title`` and\nthe default disambiguators are ``year trackdisambig``.\n\nSyntax Details\n--------------\n\nThe characters ``$``, ``%``, ``{``, ``}``, and ``,`` are \"special\" in the path\ntemplate syntax. This means that, for example, if you want a ``%`` character to\nappear in your paths, you'll need to be careful that you don't accidentally\nwrite a function call. To escape any of these characters (except ``{``, and\n``,`` outside a function argument), prefix it with a ``$``. For example, ``$$``\nbecomes ``$``; ``$%`` becomes ``%``, etc. The only exceptions are:\n\n- ``${``, which is ambiguous with the variable reference syntax (like\n  ``${title}``). To insert a ``{`` alone, it's always sufficient to just type\n  ``{``. You do, however need to use ``$`` to escape a closing brace ``$}``.\n- commas are used as argument separators in function calls. Inside of a\n  function's argument, use ``$,`` to get a literal ``,`` character. Outside of\n  any function argument, escaping is not necessary: ``,`` by itself will produce\n  ``,`` in the output.\n\nIf a value or function is undefined, the syntax is simply left unreplaced. For\nexample, if you write ``$foo`` in a path template, this will yield ``$foo`` in\nthe resulting paths because \"foo\" is not a valid field name. The same is true of\nsyntax errors like unclosed ``{}`` pairs; if you ever see template syntax\nconstructs leaking into your paths, check your template for errors.\n\nIf an error occurs in the Python code that implements a function, the function\ncall will be expanded to a string that describes the exception so you can debug\nyour template. For example, the second parameter to ``%left`` must be an\ninteger; if you write ``%left{foo,bar}``, this will be expanded to something\nlike ``<ValueError: invalid literal for int()>``.\n\n.. _itemfields:\n\nAvailable Values\n----------------\n\nHere's a list of the different values available to path formats. The current\nlist can be found definitively by running the command ``beet fields``. Note that\nplugins can add new (or replace existing) template values (see\n:ref:`templ_plugins`).\n\nOrdinary metadata:\n\n- title\n- artist\n- artist_sort: The \"sort name\" of the track artist (e.g., \"Beatles, The\" or\n  \"White, Jack\").\n- artist_credit: The track-specific `artist credit`_ name, which may be a\n  variation of the artist's \"canonical\" name.\n- album\n- albumartist: The artist for the entire album, which may be different from the\n  artists for the individual tracks.\n- albumartist_sort\n- albumartist_credit\n- genre\n- composer\n- grouping\n- year, month, day: The release date of the specific release.\n- original_year, original_month, original_day: The release date of the original\n  version of the album.\n- track\n- tracktotal\n- disc\n- disctotal\n- lyrics\n- comments\n- bpm\n- comp: Compilation flag.\n- albumtype: The MusicBrainz album type; the MusicBrainz wiki has a `list of\n  type names`_.\n- label\n- asin\n- catalognum\n- script\n- language\n- country\n- albumstatus\n- media\n- albumdisambig\n- disctitle\n- encoder\n\n.. _artist credit: https://wiki.musicbrainz.org/Artist_Credit\n\n.. _list of type names: https://musicbrainz.org/doc/Release_Group/Type\n\nAudio information:\n\n- length (in seconds)\n- bitrate (in kilobits per second, with units: e.g., \"192kbps\")\n- bitrate_mode (e.g., \"CBR\", \"VBR\" or \"ABR\", only available for the MP3 format)\n- encoder_info (e.g., \"LAME 3.97.0\", only available for some formats)\n- encoder_settings (e.g., \"-V2\", only available for the MP3 format)\n- format (e.g., \"MP3\" or \"FLAC\")\n- channels\n- bitdepth (only available for some formats)\n- samplerate (in kilohertz, with units: e.g., \"48kHz\")\n\nMusicBrainz and fingerprint information:\n\n- mb_trackid\n- mb_releasetrackid\n- mb_albumid\n- mb_artistid\n- mb_albumartistid\n- mb_releasegroupid\n- acoustid_fingerprint\n- acoustid_id\n\nLibrary metadata:\n\n- mtime: The modification time of the audio file.\n- added: The date and time that the music was added to your library.\n- path: The item's filename.\n\n.. _templ_plugins:\n\nTemplate functions and values provided by plugins\n-------------------------------------------------\n\nBeets plugins can provide additional fields and functions to templates. See the\n:doc:`/plugins/index` page for a full list of plugins. Some plugin-provided\nconstructs include:\n\n- ``$missing`` by :doc:`/plugins/missing`: The number of missing tracks per\n  album.\n- ``$album_artist_no_feat`` by :doc:`/plugins/ftintitle`: The album artist\n  without any featured artists\n- ``%bucket{text}`` by :doc:`/plugins/bucket`: Substitute a string by the range\n  it belongs to.\n- ``%the{text}`` by :doc:`/plugins/the`: Moves English articles to ends of\n  strings.\n\nThe :doc:`/plugins/inline` lets you define template fields in your beets\nconfiguration file using Python snippets. And for more advanced processing, you\ncan go all-in and write a dedicated plugin to register your own fields and\nfunctions (see :ref:`basic-plugin-setup`).\n"
  },
  {
    "path": "docs/reference/query.rst",
    "content": ".. _queries:\n\nQueries\n=======\n\nMany of beets' :doc:`commands <cli>` are built around **query strings:**\nsearches that select tracks and albums from your library. This page explains the\nquery string syntax, which is meant to vaguely resemble the syntax used by Web\nsearch engines.\n\n.. _keywordquery:\n\nKeyword\n-------\n\nThis command:\n\n::\n\n    $ beet list love\n\nwill show all tracks matching the query string ``love``. By default any\nunadorned word like this matches in a track's title, artist, album name, album\nartist, genre and comments. See below on how to search other fields.\n\nFor example, this is what I might see when I run the command above:\n\n::\n\n    Against Me! - Reinventing Axl Rose - I Still Love You Julie\n    Air - Love 2 - Do the Joy\n    Bag Raiders - Turbo Love - Shooting Stars\n    Bat for Lashes - Two Suns - Good Love\n    ...\n\n.. _combiningqueries:\n\nCombining Keywords\n------------------\n\nMultiple keywords are implicitly joined with a Boolean \"and.\" That is, if a\nquery has two keywords, it only matches tracks that contain *both* keywords. For\nexample, this command:\n\n::\n\n    $ beet ls magnetic tomorrow\n\nmatches songs from the album \"The House of Tomorrow\" by The Magnetic Fields in\nmy library. It *doesn't* match other songs by the Magnetic Fields, nor does it\nmatch \"Tomorrowland\" by Walter Meego---those songs only have *one* of the two\nkeywords I specified.\n\nKeywords can also be joined with a Boolean \"or\" using a comma. For example, the\ncommand:\n\n::\n\n    $ beet ls magnetic tomorrow , beatles yesterday\n\nwill match both \"The House of Tomorrow\" by the Magnetic Fields, as well as\n\"Yesterday\" by The Beatles. Note that the comma has to be followed by a space\n(e.g., ``foo,bar`` will be treated as a single keyword, *not* as an OR-query).\n\n.. _fieldsquery:\n\nSpecific Fields\n---------------\n\nSometimes, a broad keyword match isn't enough. Beets supports a syntax that lets\nyou query a specific field---only the artist, only the track title, and so on.\nJust say ``field:value``, where ``field`` is the name of the thing you're trying\nto match (such as ``artist``, ``album``, or ``title``) and ``value`` is the\nkeyword you're searching for.\n\nFor example, while this query:\n\n::\n\n    $ beet list dream\n\nmatches a lot of songs in my library, this more-specific query:\n\n::\n\n    $ beet list artist:dream\n\nonly matches songs by the artist The-Dream. One query I especially appreciate is\none that matches albums by year:\n\n::\n\n    $ beet list -a year:2012\n\nRecall that ``-a`` makes the ``list`` command show albums instead of individual\ntracks, so this command shows me all the releases I have from this year.\n\nFor multi-valued tags (such as ``artists`` or ``albumartists``), a regular\nexpression search must be used to search for a single value within the\nmulti-valued tag.\n\nNote that you can filter albums by querying tracks fields and vice versa:\n\n::\n\n    $ beet list -a title:love\n\nand vice versa:\n\n::\n\n    $ beet list art_path::love\n\nPhrases\n-------\n\nYou can query for strings with spaces in them by quoting or escaping them using\nyour shell's argument syntax. For example, this command:\n\n::\n\n    $ beet list the rebel\n\nshows several tracks in my library, but these (equivalent) commands:\n\n::\n\n    $ beet list \"the rebel\"\n    $ beet list the\\ rebel\n\nonly match the track \"The Rebel\" by Buck 65. Note that the quotes and\nbackslashes are not part of beets' syntax; I'm just using the escaping\nfunctionality of my shell (bash or zsh, for instance) to pass ``the rebel`` as a\nsingle argument instead of two.\n\n.. _exact-match:\n\nExact Matches\n-------------\n\nWhile ordinary queries perform *substring* matches, beets can also match whole\nstrings by adding either ``=`` (case-sensitive) or ``=~`` (ignore case) after\nthe field name's colon and before the expression:\n\n::\n\n    $ beet list artist:air\n    $ beet list artist:=~air\n    $ beet list artist:=AIR\n\nThe first query is a simple substring one that returns tracks by Air, AIR, and\nAir Supply. The second query returns tracks by Air and AIR, since both are a\ncase-insensitive match for the entire expression, but does not return anything\nby Air Supply. The third query, which requires a case-sensitive exact match,\nreturns tracks by AIR only.\n\nExact matches may be performed on phrases as well:\n\n::\n\n    $ beet list artist:=~\"dave matthews\"\n    $ beet list artist:=\"Dave Matthews\"\n\nBoth of these queries return tracks by Dave Matthews, but not by Dave Matthews\nBand.\n\nTo search for exact matches across *all* fields, just prefix the expression with\na single ``=`` or ``=~``:\n\n::\n\n    $ beet list =~crash\n    $ beet list =\"American Football\"\n\n.. _regex:\n\nRegular Expressions\n-------------------\n\nIn addition to simple substring and exact matches, beets also supports regular\nexpression matching for more advanced queries. To run a regex query, use an\nadditional ``:`` between the field name and the expression:\n\n::\n\n    $ beet list \"artist::Ann(a|ie)\"\n\nThat query finds songs by Anna Calvi and Annie but not Annuals. Similarly, this\nquery prints the path to any file in my library that's missing a track title:\n\n::\n\n    $ beet list -p title::^$\n\nTo search *all* fields using a regular expression, just prefix the expression\nwith a single ``:``, like so:\n\n::\n\n    $ beet list \":Ho[pm]eless\"\n\nRegular expressions are case-sensitive and build on `Python's built-in\nimplementation <https://docs.python.org/3/library/re.html>`__. See Python's\ndocumentation for specifics on regex syntax.\n\nMost command-line shells will try to interpret common characters in regular\nexpressions, such as ``()[]|``. To type those characters, you'll need to escape\nthem (e.g., with backslashes or quotation marks, depending on your shell).\n\n.. _numericquery:\n\nNumeric Range Queries\n---------------------\n\nFor numeric fields, such as year, bitrate, and track, you can query using one-or\ntwo-sided intervals. That is, you can find music that falls within a *range* of\nvalues. To use ranges, write a query that has two dots (``..``) at the\nbeginning, middle, or end of a string of numbers. Dots in the beginning let you\nspecify a maximum (e.g., ``..7``); dots at the end mean a minimum (``4..``);\ndots in the middle mean a range (``4..7``).\n\nFor example, this command finds all your albums that were released in the '90s:\n\n::\n\n    $ beet list -a year:1990..1999\n\nand this command finds MP3 files with bitrates of 128k or lower:\n\n::\n\n    $ beet list format:MP3 bitrate:..128000\n\nThe ``length`` field also lets you use a \"M:SS\" format. For example, this query\nfinds tracks that are less than four and a half minutes in length:\n\n::\n\n    $ beet list length:..4:30\n\n.. _datequery:\n\nDate and Date Range Queries\n---------------------------\n\nDate-valued fields, such as *added* and *mtime*, have a special query syntax\nthat lets you specify years, months, and days as well as ranges between dates.\n\nDates are written separated by hyphens, like ``year-month-day``, but the month\nand day are optional. If you leave out the day, for example, you will get\nmatches for the whole month.\n\nDate *intervals*, like the numeric intervals described above, are separated by\ntwo dots (``..``). You can specify a start, an end, or both.\n\nHere is an example that finds all the albums added in 2008:\n\n::\n\n    $ beet ls -a 'added:2008'\n\nFind all items added in the years 2008, 2009 and 2010:\n\n::\n\n    $ beet ls 'added:2008..2010'\n\nFind all items added before the year 2010:\n\n::\n\n    $ beet ls 'added:..2009'\n\nFind all items added on or after 2008-12-01 but before 2009-10-12:\n\n::\n\n    $ beet ls 'added:2008-12..2009-10-11'\n\nFind all items with a file modification time between 2008-12-01 and 2008-12-03:\n\n::\n\n    $ beet ls 'mtime:2008-12-01..2008-12-02'\n\nYou can also add an optional time value to date queries, specifying hours,\nminutes, and seconds.\n\nTimes are separated from dates by a space, an uppercase 'T' or a lowercase 't',\nfor example: ``2008-12-01T23:59:59``. If you specify a time, then the date must\ncontain a year, month, and day. The minutes and seconds are optional.\n\nHere is an example that finds all items added on 2008-12-01 at or after 22:00\nbut before 23:00:\n\n::\n\n    $ beet ls 'added:2008-12-01T22'\n\nTo find all items added on or after 2008-12-01 at 22:45:\n\n::\n\n    $ beet ls 'added:2008-12-01T22:45..'\n\nTo find all items added on 2008-12-01, at or after 22:45:20 but before 22:45:41:\n\n::\n\n    $ beet ls 'added:2008-12-01T22:45:20..2008-12-01T22:45:40'\n\nHere are example of the three ways to separate dates from times. All of these\nqueries do the same thing:\n\n::\n\n    $ beet ls 'added:2008-12-01T22:45:20'\n    $ beet ls 'added:2008-12-01t22:45:20'\n    $ beet ls 'added:2008-12-01 22:45:20'\n\nYou can also use *relative* dates. For example, ``-3w`` means three weeks ago,\nand ``+4d`` means four days in the future. A relative date has three parts:\n\n- Either ``+`` or ``-``, to indicate the past or the future. The sign is\n  optional; if you leave this off, it defaults to the future.\n- A number.\n- A letter indicating the unit: ``d``, ``w``, ``m`` or ``y``, meaning days,\n  weeks, months or years. (A \"month\" is always 30 days and a \"year\" is always\n  365 days.)\n\nHere's an example that finds all the albums added since last week:\n\n::\n\n    $ beet ls -a 'added:-1w..'\n\nAnd here's an example that lists items added in a two-week period starting four\nweeks ago:\n\n::\n\n    $ beet ls 'added:-6w..-4w'\n\n.. _not_query:\n\nQuery Term Negation\n-------------------\n\nQuery terms can also be negated, acting like a Boolean \"not,\" by prefixing them\nwith ``-`` or ``^``. This has the effect of returning all the items that do\n**not** match the query term. For example, this command:\n\n::\n\n    $ beet list ^love\n\nmatches all the songs in the library that do not have \"love\" in any of their\nfields.\n\nNegation can be combined with the rest of the query mechanisms, so you can\nnegate specific fields, regular expressions, etc. For example, this command:\n\n::\n\n    $ beet list -a artist:dylan ^year:1980..1989 \"^album::the(y)?\"\n\nmatches all the albums with an artist containing \"dylan\", but excluding those\nreleased in the eighties and those that have \"the\" or \"they\" on the title.\n\nThe syntax supports both ``^`` and ``-`` as synonyms because the latter\nindicates flags on the command line. To use a minus sign in a command-line\nquery, use a double dash ``--`` to separate the options from the query:\n\n::\n\n    $ beet list -a -- artist:dylan -year:1980..1990 \"-album::the(y)?\"\n\n.. _pathquery:\n\nPath Queries\n------------\n\nSometimes it's useful to find all the items in your library that are\n(recursively) inside a certain directory. Use the ``path:`` field to do this:\n\n::\n\n    $ beet list path:/my/music/directory\n\nIn fact, beets automatically recognizes any query term containing a path\nseparator (``/`` on POSIX systems) as a path query if that path exists, so this\ncommand is equivalent as long as ``/my/music/directory`` exist:\n\n::\n\n    $ beet list /my/music/directory\n\nNote that this only matches items that are *already in your library*, so a path\nquery won't necessarily find *all* the audio files in a directory---just the\nones you've already added to your beets library.\n\nPath queries are case sensitive if the queried path is on a case-sensitive\nfilesystem.\n\n.. _query-sort:\n\nSort Order\n----------\n\nQueries can specify a sort order. Use the name of the ``field`` you want to sort\non, followed by a ``+`` or ``-`` sign to indicate ascending or descending sort.\nFor example, this command:\n\n::\n\n    $ beet list -a year+\n\nwill list all albums in chronological order. You can also specify several sort\norders, which will be used in the same order as they appear in your query:\n\n::\n\n    $ beet list -a genre+ year+\n\nThis command will sort all albums by genre and, in each genre, in chronological\norder.\n\nThe ``artist`` and ``albumartist`` keys are special: they attempt to use their\ncorresponding ``artist_sort`` and ``albumartist_sort`` fields for sorting\ntransparently (but fall back to the ordinary fields when those are empty).\n\nLexicographic sorts are case insensitive by default, resulting in the following\nsort order: ``Bar foo Qux``. This behavior can be changed with the\n:ref:`sort_case_insensitive` configuration option. Case sensitive sort will\nresult in lower-case values being placed after upper-case values, e.g., ``Bar\nQux foo``.\n\nNote that when sorting by fields that are not present on all items (such as\nflexible fields, or those defined by plugins) in *ascending* order, the items\nthat lack that particular field will be listed at the *beginning* of the list.\n\nYou can set the default sorting behavior with the :ref:`sort_item` and\n:ref:`sort_album` configuration options.\n"
  },
  {
    "path": "docs/team.rst",
    "content": "Team\n====\n\nThis is an introduction of beets' core-team members, collaborators and frequent\ncontributors. Refer to this list to find out who to ask about your collaboration\nidea, discuss a usage-question, request a review of your open PR. Beets is a\nhuge project and not everyone involved, knows everything. We hope this helps to\npoint you in the right direction in the first place and should give you an idea\nof what you can expect from these *knowledge owners*.\n\n@arsaboo\n--------\n\n- The master of the Spotify plugin\n- Testing out new contributions\n- beets as a music discovery tool\n\n@bal-e\n------\n\n- Documentation\n- The Fish plugin\n- Type annotations\n\n@govynnus\n---------\n\n- The AURA plugin\n- The AURA specification\n- The web plugin\n- The plugin ecosystem\n- The library database API and its documentation\n\n@jackwilsdon\n------------\n\n- Broad knowledge around beets' configuration and plugins\n- Assists in discussion forums frequently\n- Knows internals of beets and puts new contributors into the right direction\n\n@joj0\n-----\n\n- The Discogs plugin\n- Good documentation throughout the project\n- The smartplaylist plugin\n- The lastgenre plugin\n- Support for M3U and other playlist formats\n- beets as a DJ companion tool (BPM, audio features, key)\n\n@RollingStar\n------------\n\n- Data visualization\n- ListenBrainz / Last.fm\n- Smart playlists\n- Library reports\n- MusicBrainz fields and searching\n- Project organization and roadmap\n\n@sampsyo\n--------\n\n- The founder\n- Knows almost everything ;-)\n\n@serene-arc\n-----------\n\n- Good documentation throughout the project\n- Experienced Python developer\n- Experienced in test-driven-development\n- Code quality\n- Typing\n\n@snejus\n-------\n\n- Grug-minded approach: simple, obvious solutions over clever complexity\n- MusicBrainz/autotagger internals and source-plugin behavior\n- Query/path handling and SQL-backed lookup behavior\n- Typing and tooling modernization (mypy, Ruff, annotations)\n- Test architecture, CI reliability, and coverage improvements\n- Release and packaging workflows (Poetry/pyproject, dependencies, changelog)\n- Cross-plugin refactors (especially metadata and lyrics-related internals)\n\n@wisp3rwind\n-----------\n\n- Mr. Tidy - Keeping the code in shape\n- Focus on improving core things rather than implementing new features\n\n@ybnd\n-----\n\n- The replaygain plugin\n- Improving the general parallelism of plugins\n- Experienced with web scrapers\n- Experienced with Flask and JavaScript integration\n- The web plugin\n"
  },
  {
    "path": "extra/_beet",
    "content": "#compdef beet\n\n# zsh completion for beets music library manager and MusicBrainz tagger: https://beets.io/\n\n# Default values for BEETS_LIBRARY & BEETS_CONFIG needed for the cache checking function.\n# They will be updated under the assumption that the config file is in the same directory as the library.\nlocal BEETS_LIBRARY=~/.config/beets/library.db\nlocal BEETS_CONFIG=~/.config/beets/config.yaml\n# Use separate caches for file locations, command completions, and query completions.\n# This allows the use of different rules for when to update each one.\nzstyle \":completion:${curcontext%:*}:*\" cache-policy _beet_check_cache\nzstyle \":completion:${curcontext%:*}:*\" use-cache true\n_beet_check_cache () {\n    local cachefile=\"$(basename ${1})\"\n    if [[ ! -a \"${1}\" ]] || [[ \"${1}\" -ot =beet ]]; then\n\t# always update the cache if it doesn't exist, or if the beet executable changes\n\treturn 0\n    fi\n    case cachefile; in\n\t(beetslibrary)\n\t    if [[ ! -a \"${~BEETS_LIBRARY}\" ]] || [[ \"${1}\" -ot \"${~BEETS_CONFIG}\" ]]; then\n\t\treturn 0\n\t    fi\n\t    ;;\n\t(beetscmds)\n\t    _retrieve_cache beetslibrary\n\t    if [[ \"${1}\" -ot \"${~BEETS_CONFIG}\" ]]; then\n\t\treturn 0\n\t    fi\n\t    ;;\n    esac\n    return 1\n}\n\n# useful: argument to _regex_arguments for matching any word\nlocal matchany=/$'[^\\0]##\\0'/\n# arguments to _regex_arguments for completing files and directories\nlocal -a files dirs\nfiles=(\"$matchany\" ':file:file:_files')\ndirs=(\"$matchany\" ':dir:directory:_dirs')\n\n# Retrieve or update caches\nif ! _retrieve_cache beetslibrary || _cache_invalid beetslibrary; then\n    local BEETS_LIBRARY=\"${$(beet config|grep library|cut -f 2 -d ' '):-${BEETS_LIBRARY}}\"\n    local BEETS_CONFIG=\"${$(beet config -p):-${BEETS_CONFIG}}\"\n    _store_cache beetslibrary BEETS_LIBRARY BEETS_CONFIG\nfi\n\nif ! _retrieve_cache beetscmds || _cache_invalid beetscmds; then\n    local -a subcommands fields beets_regex_words_subcmds beets_regex_words_help query modify\n    local subcmd cmddesc matchquery matchmodify field fieldargs queryelem modifyelem\n    # Useful function for joining grouped lines of output into single lines (taken from _completion_helpers)\n    _join_lines() {\n\tawk -v SEP=\"$1\" -v ARG2=\"$2\" -v START=\"$3\" -v END2=\"$4\" 'BEGIN {if(START==\"\"){f=1}{f=0};\n         if(ARG2 ~ \"^[0-9]+\"){LINE1 = \"^[[:space:]]{0,\"ARG2\"}[^[:space:]]\"}else{LINE1 = ARG2}}\n         ($0 ~ END2 && f>0 && END2!=\"\") {exit}\n         ($0 ~ START && f<1) {f=1; if(length(START)!=0){next}}\n         ($0 ~ LINE1 && f>0) {if(f<2){f=2; printf(\"%s\",$0)}else{printf(\"\\n%s\",$0)}; next}\n         (f>1) {gsub(/^[[:space:]]+|[[:space:]]+$/,\"\",$0); printf(\"%s%s\",SEP, $0); next}\n         END {print \"\"}'\n    }\n    # Variables used for completing subcommands and queries\n    subcommands=(${${(f)\"$(beet help | _join_lines ' ' 3 'Commands:')\"}[@]})\n    fields=($(beet fields | grep -G '^  ' | sort -u | colrm 1 2))\n    for field in \"${fields[@]}\"\n    do\n\tfieldargs=\"$fieldargs '$field:::{_beet_field_values $field}'\"\n    done\n    queryelem=\"_values -S : 'query field (add an extra : to match by regexp)' '::' $fieldargs\"\n    modifyelem=\"_values -S = 'modify field (replace = with ! to remove field)' $(echo \"'${^fields[@]}:: '\")\"\n    # regexps for matching query and modify terms on the command line\n    matchquery=/\"(${(j/|/)fields[@]})\"$':[^\\0]##\\0'/\n    matchmodify=/\"(${(j/|/)fields[@]})\"$'(=[^\\0]##|!)\\0'/\n    # create completion function for queries\n    _regex_arguments _beet_query \"$matchany\" \\# \\( \"$matchquery\" \":query:query string:$queryelem\" \\) \\#\n    local \"beets_query\"=\"$(which _beet_query)\"\n    # arguments for _regex_arguments for completing lists of queries and modifications\n    beets_query_args=( \\( \"$matchquery\" \":query:query string:{_beet_query}\" \\) \\# )\n    beets_modify_args=( \\( \"$matchmodify\" \":modify:modify string:$modifyelem\" \\) \\# )\n    # now build arguments for _beet and _beet_help completion functions\n    beets_regex_words_subcmds=('(')\n    for i in ${subcommands[@]}; do\n\tsubcmd=\"${i[(w)1]}\"\n\t# remove first word and parenthesised alias, replace : with -, [ with (, ] with ), and remove single quotes\n\tcmddesc=\"${${${${${i[(w)2,-1]##\\(*\\) #}//:/-}//\\[/(}//\\]/)}//\\'/}\"\n\t# update arguments needed for creating _beet\n\tbeets_regex_words_subcmds+=(/\"${subcmd}\"$'\\0'/ \":subcmds:subcommands:((${subcmd}:${cmddesc// /\\ }))\")\n\tbeets_regex_words_subcmds+=(\\( \"${matchany}\" \":option:option:{_beet_subcmd ${subcmd}}\" \\) \\# \\|)\n\t# update arguments needed for creating _beet_help\n\tbeets_regex_words_help+=(\"${subcmd}:${cmddesc}\")\n    done\n    beets_regex_words_subcmds[-1]=')'\n    _store_cache beetscmds beets_regex_words_subcmds beets_regex_words_help beets_query_args beets_modify_args beets_query\nelse\n    # Evaluate the variable containing the query completer function\n    eval \"${beets_query}\"\nfi\n\n# Function for getting unique values for field from database (you may need to change the path to the database).\n_beet_field_values() {\n    local -a output fieldvals\n    local sqlcmd=\"select distinct $1 from items;\"\n    _retrieve_cache beetslibrary\n    case ${1}\n    in\n        lyrics)\n            fieldvals=\n            ;;\n        *)\n\t    if [[ \"$(sqlite3 ${~BEETS_LIBRARY} ${sqlcmd} 2>&1)\" =~ \"no such column\" ]]; then\n\t\tsqlcmd=\"select distinct value from item_attributes where key=='$1' and value!='';\"\n\t    fi\n\t    output=\"$(sqlite3 -list -noheader ${~BEETS_LIBRARY} ${sqlcmd} 2>/dev/null)\"\n            fieldvals=(\"${(f)output[@]}\")\n            ;;\n    esac\n    compadd -P \\\" -S \\\" -M 'm:{[:lower:][:upper:]}={[:upper:][:lower:]}' -Q -a fieldvals\n}\n\n# This function takes a beet subcommand as its first argument, and then uses _regex_words to set ${reply[@]}\n# to an array containing arguments for the _regex_arguments function.\n_beet_subcmd_options() {\n    local shortopt optarg optdesc\n    local matchany=/$'[^\\0]##\\0'/\n    local -a regex_words\n    regex_words=()\n    for i in ${${(f)\"$(beet help $1 | awk '/^ +-/{if(x)print x;x=$0;next}/^ *$/{if(x) exit}{if(x) x=x$0}END{print x}')\"}[@]}\n    do\n        opt=\"${i[(w)1]/,/}\"\n        optarg=\"${${${i## #[-a-zA-Z]# }##[- ]##*}%%[, ]*}\"\n        optdesc=\"${${${${${i[(w)2,-1]/[A-Z, ]#--[-a-z]##[=A-Z]# #/}//:/-}//\\[/(}//\\]/)}//\\'/}\"\n        case $optarg; in\n            (\"\")\n                if [[ \"$1\" == \"import\" && \"$opt\" == \"-L\" ]]; then\n                    regex_words+=(\"$opt:$optdesc:\\${beets_query_args[@]}\")\n                else\n                    regex_words+=(\"$opt:$optdesc\")\n                fi\n                ;;\n            (LOG)\n\t\tlocal -a files\n\t\tfiles=(\"$matchany\" ':file:file:_files')\n\t\tregex_words+=(\"$opt:$optdesc:\\$files\")\n                ;;\n            (CONFIG)\n                local -a configfile\n                configfile=(\"$matchany\" ':file:config file:{_files -g *.yaml}')\n                regex_words+=(\"$opt:$optdesc:\\$configfile\")\n                ;;\n            (LIB|LIBRARY)\n                local -a libfile\n                libfile=(\"$matchany\" ':file:database file:{_files -g *.db}')\n                regex_words+=(\"$opt:$optdesc:\\$libfile\")\n                ;;\n            (DIR|DIRECTORY|DEST)\n\t\tlocal -a dirs\n\t\tdirs=(\"$matchany\" ':dir:directory:_dirs')\n                regex_words+=(\"$opt:$optdesc:\\$dirs\")\n                ;;\n            (SOURCE)\n                if [[ \"${1}\" -eq lastgenre ]]; then\n                    local -a lastgenresource\n                    lastgenresource=(/$'(artist|album|track)\\0'/ ':source:genre source:(artist album track)')\n                    regex_words+=(\"$opt:$optdesc:\\$lastgenresource\")\n                else\n                    regex_words+=(\"$opt:$optdesc:\\$matchany\")\n                fi\n                ;;\n            (*)\n                regex_words+=(\"$opt:$optdesc:\\$matchany\")\n                ;;\n        esac\n    done\n    _regex_words options \"$1 options\" \"${regex_words[@]}\"\n}\n\n## Function for completing subcommands. It calls another completion function which is first created if it doesn't already exist.\n_beet_subcmd() {\n    local -a options\n    local subcmd=\"${1}\"\n    if [[ ! $(type _beet_${subcmd} | grep function) =~ function ]]; then\n\tif ! _retrieve_cache \"beets${subcmd}\" || _cache_invalid \"beets${subcmd}\"; then\n\t    local matchany=/$'[^\\0]##\\0'/\n\t    local -a files\n\t    files=(\"$matchany\" ':file:file:_files')\n\t    # get arguments for completing subcommand options\n\t    _beet_subcmd_options \"$subcmd\"\n\t    options=(\"${reply[@]}\" \\#)\n\t    _retrieve_cache beetscmds\n\t    case ${subcmd}; in\n\t\t(import)\n\t\t    _regex_arguments _beet_import \"${matchany}\" /\"${subcmd}\"$'\\0'/ \"${options[@]}\" \"${files[@]}\" \\#\n\t\t    ;;\n\t\t(modify)\n\t\t    _regex_arguments _beet_modify \"${matchany}\" /\"${subcmd}\"$'\\0'/ \"${options[@]}\" \\\n\t\t\t\t     \"${beets_query_args[@]}\" \"${beets_modify_args[@]}\"\n\t\t    ;;\n\t\t(fields|migrate|version|config)\n\t\t    _regex_arguments _beet_${subcmd} \"${matchany}\" /\"${subcmd}\"$'\\0'/ \"${options[@]}\"\n\t\t    ;;\n\t\t(help)\n\t\t    _regex_words subcmds \"subcommands\" \"${beets_regex_words_help[@]}\"\n\t\t    _regex_arguments _beet_help \"${matchany}\" /$'help\\0'/ \"${options[@]}\" \"${reply[@]}\"\n\t\t    ;;\n\t\t(*) # Other commands have options followed by a query\n\t\t    _regex_arguments _beet_${subcmd} \"${matchany}\" /\"${subcmd}\"$'\\0'/ \"${options[@]}\" \"${beets_query_args[@]}\"\n\t\t    ;;\n\t    esac\n\t    # Store completion function in a cache file\n\t    local \"beets_${subcmd}\"=\"$(which _beet_${subcmd})\"\n\t    _store_cache \"beets${subcmd}\" \"beets_${subcmd}\"\n\telse\n\t    # Evaluate the function which is stored in $beets_${subcmd}\n\t    local var=\"beets_${subcmd}\"\n\t    eval \"${(P)var}\"\n\tfi\n    fi\n    _beet_${subcmd}\n}\n\n# Global options\nlocal -a globalopts\n_regex_words options \"global options\" '-c:path to configuration file:$files' '-v:print debugging information' \\\n\t     '-l:library database file to use:$files' '-h:show this help message and exit' '-d:destination music directory:$dirs'\nglobalopts=(\"${reply[@]}\")\n\n# Create main completion function\n_regex_arguments _beet \"$matchany\" \\( \"${globalopts[@]}\" \\# \\) \"${beets_regex_words_subcmds[@]}\"\n\n# Set tag-order so that options are completed separately from arguments\nzstyle \":completion:${curcontext}:\" tag-order '! options'\n\n# Execute the completion function\n_beet \"$@\"\n\n# Local Variables:\n# mode:shell-script\n# End:\n"
  },
  {
    "path": "extra/ascii_logo.txt",
    "content": "[][][][]\n[][]  []\n[][][][]\n[][]  []\n[][][][]\n\n[][][][]  [][][][]\n[][]  []  [][]  []\n[][][][]  [][][][]\n[][]      [][]    \n[][][][]  [][][][]\n\n[][][][]  [][][][]\n[][][][]  [][]\n  [][]      [][]\n  [][]        [][]\n  [][]    [][][][]\n"
  },
  {
    "path": "extra/release.py",
    "content": "#!/usr/bin/env python3\n\n\"\"\"A utility script for automating the beets release process.\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nimport subprocess\nfrom collections.abc import Callable\nfrom contextlib import redirect_stdout\nfrom datetime import datetime, timezone\nfrom functools import partial\nfrom io import StringIO\nfrom pathlib import Path\nfrom typing import NamedTuple, TypeAlias\n\nimport click\nimport tomli\nfrom packaging.version import Version, parse\nfrom sphinx.ext import intersphinx\n\nfrom docs.conf import rst_epilog\n\nBASE = Path(__file__).parent.parent.absolute()\nPYPROJECT = BASE / \"pyproject.toml\"\nCHANGELOG = BASE / \"docs\" / \"changelog.rst\"\nDOCS = \"https://beets.readthedocs.io/en/stable\"\n\nVERSION_HEADER = r\"\\d+\\.\\d+\\.\\d+ \\([^)]+\\)\"\nRST_LATEST_CHANGES = re.compile(\n    rf\"{VERSION_HEADER}\\n--+\\s+(.+?)\\n\\n+{VERSION_HEADER}\", re.DOTALL\n)\n\nReplacement: TypeAlias = \"tuple[str, str | Callable[[re.Match[str]], str]]\"\n\n\nclass Ref(NamedTuple):\n    \"\"\"A reference to documentation with ID, path, and optional title.\"\"\"\n\n    id: str\n    path: str | None\n    title: str | None\n\n    @classmethod\n    def from_line(cls, line: str) -> Ref:\n        \"\"\"Create Ref from a Sphinx objects.inv line.\n\n        Each line has the following structure:\n        <id>    [optional title : ] <relative-url-path>\n\n        See the output of\n            python -m sphinx.ext.intersphinx docs/_build/html/objects.inv\n        \"\"\"\n        if len(line_parts := line.split(\" \", 1)) == 1:\n            return cls(line, None, None)\n\n        id, path_with_name = line_parts\n        parts = [p.strip() for p in path_with_name.split(\":\", 1)]\n\n        if len(parts) == 1:\n            path, name = parts[0], None\n        else:\n            name, path = parts\n\n        return cls(id, path, name)\n\n    @property\n    def url(self) -> str:\n        \"\"\"Full documentation URL.\"\"\"\n        return f\"{DOCS}/{self.path}\"\n\n    @property\n    def name(self) -> str:\n        \"\"\"Display name (title if available, otherwise ID).\"\"\"\n        return self.title or self.id\n\n\ndef get_refs() -> dict[str, Ref]:\n    \"\"\"Parse Sphinx objects.inv and return dict of documentation references.\"\"\"\n    objects_filepath = Path(\"docs/_build/html/objects.inv\")\n    if not objects_filepath.exists():\n        raise ValueError(\"Documentation does not exist. Run 'poe docs' first.\")\n\n    captured_output = StringIO()\n\n    with redirect_stdout(captured_output):\n        intersphinx.inspect_main([str(objects_filepath)])\n\n    lines = captured_output.getvalue().replace(\"\\t\", \"    \").splitlines()\n    return {\n        r.id: r\n        for ln in lines\n        if ln.startswith(\"    \") and (r := Ref.from_line(ln.strip()))\n    }\n\n\ndef create_rst_replacements() -> list[Replacement]:\n    \"\"\"Generate list of pattern replacements for RST changelog.\"\"\"\n    refs = get_refs()\n\n    def make_ref_link(ref_id: str, name: str | None = None) -> str:\n        if ref_id.endswith(\"-cmd\"):\n            name = f\"{ref_id.removesuffix('-cmd')} command\"\n        ref = refs[ref_id]\n        return rf\"`{name or ref.name} <{ref.url}>`_\"\n\n    commands = \"|\".join(r.split(\"-\")[0] for r in refs if r.endswith(\"-cmd\"))\n    plugins = \"|\".join(\n        r.split(\"/\")[-1] for r in refs if r.startswith(\"plugins/\")\n    )\n    explicit_replacements = dict(\n        line.removeprefix(\".. \").split(\" replace:: \")\n        for line in filter(None, rst_epilog.splitlines())\n    )\n    return [\n        # Replace explicitly defined substitutions from rst_epilog\n        #    |BeetsPlugin| -> :class:`beets.plugins.BeetsPlugin`\n        (\n            r\"\\|\\w[^ ]*\\|\",\n            lambda m: explicit_replacements.get(m[0], m[0]),\n        ),\n        # Replace Sphinx directives by documentation URLs, e.g.,\n        #   :ref:`/plugins/autobpm` -> [AutoBPM Plugin](DOCS/plugins/autobpm.html)  # noqa: E501\n        #   :ref:`list-cmd` -> [list command](DOCS/reference/cli.html#list-cmd)\n        (\n            r\":(?:ref|doc|class|conf):`+~?(?:([^`<]+)<)?/?([\\w.:/_-]+)>?`+\",\n            lambda m: make_ref_link(m[2], m[1]),\n        ),\n        # Convert command references to documentation URLs\n        #   `beet move` or `move` command -> [move command](DOCS/reference/cli.html#move-cmd)  # noqa: E501\n        (\n            rf\"`+beet ({commands})`+|`+({commands})`+(?= command)\",\n            lambda m: make_ref_link(f\"{m[1] or m[2]}-cmd\"),\n        ),\n        # Convert plugin references to documentation URLs\n        #   `fetchart` plugin -> [fetchart](DOCS/plugins/fetchart.html)\n        (rf\"`+({plugins})`+\", lambda m: make_ref_link(f\"plugins/{m[1]}\")),\n        # Convert bug references to GitHub issue links\n        (r\":bug:`(\\d+)`\", r\":bug: (#\\1)\"),\n        # Convert user references to GitHub @mentions\n        (r\":user:`(\\w+)`\", r\"\\@\\1\"),\n    ]\n\n\norder_bullet_points = partial(\n    re.compile(r\"(\\n- .*?(?=\\n(?! *(-|\\d\\.) )|$))\", flags=re.DOTALL).sub,\n    lambda m: \"\\n- \".join(sorted(m.group().split(\"\\n- \"), key=str.lower)),\n)\n\n\ndef update_docs_config(text: str, new: Version) -> str:\n    new_major_minor = f\"{new.major}.{new.minor}\"\n    text = re.sub(r\"(?<=version = )[^\\n]+\", f'\"{new_major_minor}\"', text)\n    return re.sub(r\"(?<=release = )[^\\n]+\", f'\"{new}\"', text)\n\n\ndef update_changelog(text: str, new: Version) -> str:\n    new_header = f\"{new} ({datetime.now(timezone.utc).date():%B %d, %Y})\"\n    return re.sub(\n        # do not match if the new version is already present\n        r\"\\nUnreleased\\n--+\\n\",\n        rf\"\"\"\nUnreleased\n----------\n\n..\n    New features\n    ~~~~~~~~~~~~\n\n..\n    Bug fixes\n    ~~~~~~~~~\n\n..\n    For plugin developers\n    ~~~~~~~~~~~~~~~~~~~~~\n\n..\n    Other changes\n    ~~~~~~~~~~~~~\n\n{new_header}\n{\"-\" * len(new_header)}\n\"\"\",\n        text,\n    )\n\n\nUpdateVersionCallable = Callable[[str, Version], str]\nFILENAME_AND_UPDATE_TEXT: list[tuple[Path, UpdateVersionCallable]] = [\n    (\n        PYPROJECT,\n        lambda text, new: re.sub(r\"(?<=\\nversion = )[^\\n]+\", f'\"{new}\"', text),\n    ),\n    (\n        BASE / \"beets\" / \"__init__.py\",\n        lambda text, new: re.sub(\n            r\"(?<=__version__ = )[^\\n]+\", f'\"{new}\"', text\n        ),\n    ),\n    (CHANGELOG, update_changelog),\n    (BASE / \"docs\" / \"conf.py\", update_docs_config),\n]\n\n\ndef validate_new_version(\n    ctx: click.Context, param: click.Argument, value: Version\n) -> Version:\n    \"\"\"Validate the version is newer than the current one.\"\"\"\n    with PYPROJECT.open(\"rb\") as f:\n        current = parse(tomli.load(f)[\"tool\"][\"poetry\"][\"version\"])\n\n    if not value > current:\n        msg = f\"version must be newer than {current}\"\n        raise click.BadParameter(msg)\n\n    return value\n\n\ndef bump_version(new: Version) -> None:\n    \"\"\"Update the version number in specified files.\"\"\"\n    for path, perform_update in FILENAME_AND_UPDATE_TEXT:\n        with path.open(\"r+\") as f:\n            contents = f.read()\n            f.seek(0)\n            f.write(perform_update(contents, new))\n            f.truncate()\n\n\ndef rst2md(text: str) -> str:\n    \"\"\"Use Pandoc to convert text from ReST to Markdown.\"\"\"\n    return (\n        subprocess.check_output(\n            [\"pandoc\", \"--from=rst\", \"--to=gfm+hard_line_breaks\"],\n            input=text.encode(),\n        )\n        .decode()\n        .strip()\n    )\n\n\ndef get_changelog_contents() -> str | None:\n    if m := RST_LATEST_CHANGES.search(CHANGELOG.read_text()):\n        return m.group(1)\n\n    return None\n\n\ndef changelog_as_markdown(rst: str) -> str:\n    \"\"\"Get the latest changelog entry as hacked up Markdown.\"\"\"\n    for pattern, repl in create_rst_replacements():\n        rst = re.sub(pattern, repl, rst, flags=re.M | re.DOTALL)\n\n    md = rst2md(rst)\n\n    # order bullet points in each of the lists alphabetically to\n    # improve readability\n    return order_bullet_points(md)\n\n\n@click.group()\ndef cli():\n    pass\n\n\n@cli.command()\n@click.argument(\"version\", type=Version, callback=validate_new_version)\ndef bump(version: Version) -> None:\n    \"\"\"Bump the version in project files.\"\"\"\n    bump_version(version)\n\n\n@cli.command()\ndef changelog():\n    \"\"\"Get the most recent version's changelog as Markdown.\"\"\"\n    if changelog := get_changelog_contents():\n        try:\n            print(changelog_as_markdown(changelog))\n        except ValueError as e:\n            raise click.exceptions.UsageError(str(e))\n\n\nif __name__ == \"__main__\":\n    cli()\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[tool.poetry]\nname = \"beets\"\nversion = \"2.7.1\"\ndescription = \"music tagger and library organizer\"\nauthors = [\"Adrian Sampson <adrian@radbox.org>\"]\nmaintainers = [\"Serene-Arc\"]\nlicense = \"MIT\"\nreadme = \"README.rst\"\nhomepage = \"https://beets.io/\"\nrepository = \"https://github.com/beetbox/beets\"\ndocumentation = \"https://beets.readthedocs.io/en/stable/\"\nclassifiers = [\n    \"Topic :: Multimedia :: Sound/Audio\",\n    \"Topic :: Multimedia :: Sound/Audio :: Players :: MP3\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Environment :: Console\",\n    \"Environment :: Web Environment\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: Implementation :: CPython\",\n]\npackages = [\n    { include = \"beets\" },\n    { include = \"beetsplug\" },\n]\ninclude = [ # extra files to include in the sdist\n    { path = \"docs\", format = \"sdist\" },\n    { path = \"extra\", format = \"sdist\" },\n    { path = \"man/**/*\", format = \"sdist\" },\n    { path = \"test/*.py\", format = \"sdist\" },\n    { path = \"test/rsrc/**/*\", format = \"sdist\" },\n]\nexclude = [\"docs/_build\", \"docs/modd.conf\", \"docs/**/*.css\"]\n\n[tool.poetry.urls]\nChangelog = \"https://github.com/beetbox/beets/blob/master/docs/changelog.rst\"\n\"Bug Tracker\" = \"https://github.com/beetbox/beets/issues\"\n\n[tool.poetry.dependencies]\npython = \">=3.10,<4\"\n\ncolorama = { version = \"*\", markers = \"sys_platform == 'win32'\" }\nconfuse = \">=2.2.0\"\njellyfish = \"*\"\nlap = \">=0.5.12\"\nmediafile = \">=0.12.0\"\nnumpy = [\n    { python = \"<3.13\", version = \">=2.0.2\" },\n    { python = \">=3.13\", version = \">=2.3.4\" },\n]\npackaging = \">=24.0\"\nplatformdirs = \">=3.5.0\"\npyyaml = \"*\"\nrequests = \">=2.32.5\"\nrequests-ratelimiter = \">=0.7.0\"\ntyping_extensions = \"*\"\nunidecode = \">=1.3.6\"\n\nbeautifulsoup4 = { version = \"*\", optional = true }\ndbus-python = { version = \"*\", optional = true }\nflask = { version = \"*\", optional = true }\nflask-cors = { version = \"*\", optional = true }\nlangdetect = { version = \"*\", optional = true }\nlibrosa = { version = \">=0.11\", optional = true }\nscipy = [ # for librosa\n    { python = \"<3.13\", version = \">=1.13.1\", optional = true },\n    { python = \">=3.13\", version = \">=1.16.1\", optional = true },\n]\nnumba = [ # for librosa\n    { python = \"<3.13\", version = \">=0.60\", optional = true },\n    { python = \">=3.13\", version = \">=0.62.1\", optional = true },\n]\nmutagen = { version = \">=1.33\", optional = true }\nPillow = { version = \"*\", optional = true }\npy7zr = { version = \"*\", optional = true }\npyacoustid = { version = \"*\", optional = true }\nPyGObject = { version = \"*\", optional = true }\npylast = { version = \"*\", optional = true }\npython-mpd2 = { version = \">=0.4.2\", optional = true }\npython3-discogs-client = { version = \">=2.3.15\", optional = true }\npyxdg = { version = \"*\", optional = true }\nrarfile = { version = \"*\", optional = true }\nreflink = { version = \"*\", optional = true }\nresampy = { version = \">=0.4.3\", optional = true }\nrequests-oauthlib = { version = \">=0.6.1\", optional = true }\nsoco = { version = \"*\", optional = true }\n\ndocutils = { version = \">=0.20.1\", optional = true }\npydata-sphinx-theme = { version = \"*\", optional = true }\nsphinx = { version = \"*\", optional = true }\nsphinx-design = { version = \">=0.6.1\", optional = true }\nsphinx-copybutton = { version = \">=0.5.2\", optional = true }\nsphinx-toolbox = { version = \">=4.1.0\", optional = true }\ntitlecase = { version = \"^2.4.1\", optional = true }\n\n[tool.poetry.group.test.dependencies]\nbeautifulsoup4 = \"*\"\ncodecov = \">=2.1.13\"\nflask = \"*\"\nlangdetect = \"*\"\npylast = \"*\"\npytest = \"*\"\npytest-cov = \"*\"\npytest-flask = \"*\"\npython-mpd2 = \"*\"\npython3-discogs-client = \">=2.3.15\"\npy7zr = \"*\"\npyxdg = \"*\"\nrarfile = \"*\"\nrequests-mock = \">=1.12.1\"\nrequests_oauthlib = \"*\"\nresponses = \">=0.3.0\"\ntitlecase = \"^2.4.1\"\n\n[tool.poetry.group.lint.dependencies]\ndocstrfmt = \">=2.0.2\"\nruff = \">=0.13.0\"\nsphinx-lint = \">=1.0.0\"\n\n[tool.poetry.group.typing.dependencies]\nmypy = \"*\"\ntypes-beautifulsoup4 = \"*\"\ntypes-docutils = \">=0.22.2.20251006\"\ntypes-Flask-Cors = \"*\"\ntypes-Pillow = \"*\"\ntypes-PyYAML = \"*\"\ntypes-requests = \"*\"\ntypes-urllib3 = \"*\"\n\n[tool.poetry.group.release.dependencies]\nclick = \">=8.1.7\"\ntomli = \">=2.0.1\"\n\n[tool.poetry.extras]\n# inline comments note required external / non-python dependencies\nabsubmit = [\"requests\"] # extractor binary from https://acousticbrainz.org/download\naura = [\"flask\", \"flask-cors\", \"Pillow\"]\nautobpm = [\"librosa\", \"resampy\"]\n# badfiles # mp3val and flac\nbeatport = [\"requests-oauthlib\"]\nbpd = [\"PyGObject\"] # gobject-introspection, gstreamer1.0-plugins-base, python3-gst-1.0\nchroma = [\"pyacoustid\"] # chromaprint or fpcalc\n# convert # ffmpeg\ndocs = [\n    \"docutils\",\n    \"pydata-sphinx-theme\",\n    \"sphinx\",\n    \"sphinx-lint\",\n    \"sphinx-design\",\n    \"sphinx-copybutton\",\n    \"sphinx-toolbox\",\n]\ndiscogs = [\"python3-discogs-client\"]\nembedart = [\"Pillow\"] # ImageMagick\nembyupdate = [\"requests\"]\nfetchart = [\"beautifulsoup4\", \"langdetect\", \"Pillow\", \"requests\"]\nimport = [\"py7zr\", \"rarfile\"]\n# ipfs # go-ipfs\n# keyfinder # KeyFinder\nkodiupdate = [\"requests\"]\nlastgenre = [\"pylast\"]\nlastimport = [\"pylast\"]\nlyrics = [\"beautifulsoup4\", \"langdetect\", \"requests\"]\nmetasync = [\"dbus-python\"]\nmpdstats = [\"python-mpd2\"]\nplexupdate = [\"requests\"]\nreflink = [\"reflink\"]\nreplaygain = [\n    \"PyGObject\",\n] # python-gi and GStreamer 1.0+ or mp3gain/aacgain or Python Audio Tools or ffmpeg\nscrub = [\"mutagen\"]\nsonosupdate = [\"soco\"]\ntitlecase = [\"titlecase\"]\nthumbnails = [\"Pillow\", \"pyxdg\"]\nweb = [\"flask\", \"flask-cors\"]\n\n[tool.poetry.scripts]\nbeet = \"beets.ui:main\"\n\n[build-system]\nrequires = [\"poetry-core>=1.0.0\"]\nbuild-backend = \"poetry.core.masonry.api\"\n\n[tool.pipx-install]\npoethepoet = \">=0.26\"\npoetry = \">=1.8,<2\"\n\n[tool.poe.tasks.build]\nhelp = \"Build the package\"\nshell = \"\"\"\nmake -C docs man\nrm -rf man\nmv docs/_build/man .\npoetry build\n\"\"\"\n\n[tool.poe.tasks.bump]\nhelp = \"Bump project version and update relevant files\"\ncmd = \"python ./extra/release.py bump $version\"\nargs = { version = { help = \"The new version to set\", positional = true, required = true } }\n\n[tool.poe.tasks.changelog]\nhelp = \"Print the latest version's changelog in Markdown\"\ncmd = \"python ./extra/release.py changelog\"\n\n[tool.poe.tasks.check-docs-links]\nhelp = \"Check the documentation for broken URLs\"\ncmd = \"make -C docs linkcheck\"\n\n[tool.poe.tasks.check-format]\nhelp = \"Check the code for style issues\"\ncmd = \"ruff format --check --diff\"\n\n[tool.poe.tasks.check-types]\nhelp = \"Check the code for typing issues. Accepts mypy options.\"\ncmd = \"mypy\"\n\n[tool.poe.tasks.docs]\nhelp = \"Build documentation\"\nargs = [{ name = \"COMMANDS\", positional = true, multiple = true, default = \"html\" }]\ncmd = \"make -C docs $COMMANDS\"\n\n[tool.poe.tasks.format]\nhelp = \"Format the codebase\"\ncmd = \"ruff format --config=pyproject.toml\"\n\n[tool.poe.tasks.format-docs]\nhelp = \"Format the documentation\"\ncmd = \"docstrfmt --preserve-adornments docs *.rst\"\n\n[tool.poe.tasks.lint]\nhelp = \"Check the code for linting issues. Accepts ruff options.\"\ncmd = \"ruff check --config=pyproject.toml\"\n\n[tool.poe.tasks.lint-docs]\nhelp = \"Lint the documentation\"\ninterpreter = \"bash\"\nshell = \"\"\"\nset -o pipefail\nfiles=$(git ls-files '*.rst')\n\ngrep -Eno ' `[^`][^`]+`[^_]' $files |\n    sed 's/ .*/ Use double backticks for inline literal (double-backticks-required)/' && failed=1\nsphinx-lint --enable all --disable default-role $files || failed=1\n\nexit ${failed:-0}\n\"\"\"\n\n[tool.poe.tasks.update-dependencies]\nhelp = \"Update dependencies to their latest versions.\"\ncmd = \"poetry update -vv\"\n\n[tool.poe.tasks.test]\nhelp = \"Run tests with pytest\"\ncmd = \"pytest $OPTS\"\nenv.OPTS.default = \"-p no:cov\"\n\n[tool.poe.tasks.test-with-coverage]\nhelp = \"Run tests and record coverage\"\nref = \"test\"\n# record coverage in beets and beetsplug packages\n# save xml for coverage upload to coveralls\n# save html report for local dev use\n# measure coverage across logical branches\n# show which tests cover specific lines in the code (see the HTML report)\nenv.OPTS = \"\"\"\n--cov=beets\n--cov=beetsplug\n--cov-report=xml:.reports/coverage.xml\n--cov-report=html:.reports/html\n--cov-branch\n--cov-context=test\n\"\"\"\n\n[tool.poe.tasks.check-temp-files]\nhelp = \"Run each test module one by one and check for leftover temp files\"\nshell = \"\"\"\nsetopt nullglob\nfor file in test/**/*.py; do\n  print Temp files created by $file && poe test $file &>/dev/null\n  tempfiles=(/tmp/**/tmp* /tmp/beets/**/*)\n  if (( $#tempfiles )); then\n    print -l $'\\t'$^tempfiles\n    rm -r --interactive=never $tempfiles &>/dev/null\n  fi\ndone\n\"\"\"\ninterpreter = \"zsh\"\n\n[tool.docstrfmt]\nline-length = 80\nextend-exclude = [\n    \"docs/_templates/**/*\",\n    \"docs/api/**/*\",\n    \"README_kr.rst\",\n]\n\n[tool.ruff]\ntarget-version = \"py310\"\nline-length = 80\n\n[tool.ruff.lint]\nfuture-annotations = true\nselect = [\n    # \"ARG\", # flake8-unused-arguments\n    # \"C4\", # flake8-comprehensions\n    \"E\", # pycodestyle\n    \"F\", # pyflakes\n    # \"B\", # flake8-bugbear\n    \"G\", # flake8-logging-format\n    \"I\", # isort\n    \"ISC\", # flake8-implicit-str-concat\n    \"N\", # pep8-naming\n    \"PT\", # flake8-pytest-style\n    \"RUF\", # ruff\n    \"UP\", # pyupgrade\n    \"TC\", # flake8-type-checking\n    \"W\", # pycodestyle\n]\nignore = [\n    \"TC006\", # no need to quote 'cast's since we use 'from __future__ import annotations'\n]\n\n[tool.ruff.lint.per-file-ignores]\n\"beets/**\" = [\"PT\"]\n\"test/plugins/test_ftintitle.py\" = [\"E501\"]\n\"test/test_util.py\" = [\"E501\"]\n\"test/util/test_diff.py\" = [\"E501\"]\n\"test/util/test_id_extractors.py\" = [\"E501\"]\n\"test/**\" = [\"RUF001\"] # we use Unicode characters in tests\n\n[tool.ruff.lint.isort]\nsplit-on-trailing-comma = false\n\n[tool.ruff.lint.pycodestyle]\nmax-line-length = 88\n\n[tool.ruff.lint.flake8-pytest-style]\nfixture-parentheses = false\nmark-parentheses = false\nparametrize-names-type = \"csv\"\n\n[tool.ruff.lint.flake8-unused-arguments]\nignore-variadic-names = true\n\n[tool.ruff.lint.pep8-naming]\nclassmethod-decorators = [\"cached_classproperty\"]\nextend-ignore-names = [\"assert*\", \"cached_classproperty\"]\n"
  },
  {
    "path": "setup.cfg",
    "content": "[tool:pytest]\n# do not litter the working directory\ncache_dir = /tmp/pytest_cache\n# slightly more verbose output\nconsole_output_style = count\n# pretty-print test names in the Codecov U\njunit_family = legacy\naddopts =\n    # show all skipped/failed/xfailed tests in the summary except passed\n    -ra\n    --strict-config\n    --junitxml=.reports/pytest.xml\n\n[coverage:run]\ndata_file = .reports/coverage/data\nbranch = true\nrelative_files = true\nomit =\n    beets/test/*\n    beetsplug/_typing.py\n\n[coverage:report]\nprecision = 2\nskip_empty = true\nshow_missing = true\nexclude_also =\n    @atexit.register\n    if TYPE_CHECKING\n    if typing.TYPE_CHECKING\n    raise AssertionError\n    raise NotImplementedError\n\n[coverage:html]\nshow_contexts = true\n\n[mypy]\nallow_any_generics = false\n# FIXME: Would be better to actually type the libraries (if under our control),\n# or write our own stubs. For now, silence errors\nignore_missing_imports = true\nnamespace_packages = true\nexplicit_package_bases = true\n\n# Temporary, until we decide on a mypy\n# config for all files.\n[[mypy-beets.plugins]]\ndisallow_untyped_decorators = true\ncheck_untyped_defs = true\n\n[[mypy-beets.metadata_plugins]]\ndisallow_untyped_decorators = true\ncheck_untyped_defs = true\n"
  },
  {
    "path": "test/__init__.py",
    "content": "# Make python -m testall.py work.\n"
  },
  {
    "path": "test/autotag/__init__.py",
    "content": ""
  },
  {
    "path": "test/autotag/test_autotag.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for autotagging functionality.\"\"\"\n\nimport operator\n\nimport pytest\n\nfrom beets import autotag\nfrom beets.autotag import AlbumInfo, TrackInfo, correct_list_fields\nfrom beets.library import Item\nfrom beets.test.helper import BeetsTestCase\n\n\nclass ApplyTest(BeetsTestCase):\n    def _apply(self, per_disc_numbering=False, artist_credit=False):\n        info = self.info\n        item_info_pairs = list(zip(self.items, info.tracks))\n        self.config[\"per_disc_numbering\"] = per_disc_numbering\n        self.config[\"artist_credit\"] = artist_credit\n        autotag.apply_metadata(self.info, item_info_pairs)\n\n    def setUp(self):\n        super().setUp()\n\n        self.items = [Item(), Item()]\n        self.info = AlbumInfo(\n            tracks=[\n                TrackInfo(\n                    title=\"title\",\n                    track_id=\"dfa939ec-118c-4d0f-84a0-60f3d1e6522c\",\n                    medium=1,\n                    medium_index=1,\n                    medium_total=1,\n                    index=1,\n                    artist=\"trackArtist\",\n                    artist_credit=\"trackArtistCredit\",\n                    artists_credit=[\"trackArtistCredit\"],\n                    artist_sort=\"trackArtistSort\",\n                    artists_sort=[\"trackArtistSort\"],\n                ),\n                TrackInfo(\n                    title=\"title2\",\n                    track_id=\"40130ed1-a27c-42fd-a328-1ebefb6caef4\",\n                    medium=2,\n                    medium_index=1,\n                    index=2,\n                    medium_total=1,\n                ),\n            ],\n            artist=\"albumArtist\",\n            artists=[\"albumArtist\", \"albumArtist2\"],\n            album=\"album\",\n            album_id=\"7edb51cb-77d6-4416-a23c-3a8c2994a2c7\",\n            artist_id=\"a6623d39-2d8e-4f70-8242-0a9553b91e50\",\n            artists_ids=None,\n            artist_credit=\"albumArtistCredit\",\n            artists_credit=[\"albumArtistCredit1\", \"albumArtistCredit2\"],\n            artist_sort=None,\n            artists_sort=[\"albumArtistSort\", \"albumArtistSort2\"],\n            albumtype=\"album\",\n            va=True,\n            mediums=2,\n            data_source=\"MusicBrainz\",\n            year=2013,\n            month=12,\n            day=18,\n            genres=[\"Rock\", \"Pop\"],\n        )\n\n        common_expected = {\n            \"album\": \"album\",\n            \"albumartist_credit\": \"albumArtistCredit\",\n            \"albumartist_sort\": \"\",\n            \"albumartist\": \"albumArtist\",\n            \"albumartists\": [\"albumArtist\", \"albumArtist2\"],\n            \"albumartists_credit\": [\n                \"albumArtistCredit1\",\n                \"albumArtistCredit2\",\n            ],\n            \"albumartists_sort\": [\"albumArtistSort\", \"albumArtistSort2\"],\n            \"albumtype\": \"album\",\n            \"albumtypes\": [\"album\"],\n            \"comp\": True,\n            \"disctotal\": 2,\n            \"mb_albumartistid\": \"a6623d39-2d8e-4f70-8242-0a9553b91e50\",\n            \"mb_albumartistids\": [\"a6623d39-2d8e-4f70-8242-0a9553b91e50\"],\n            \"mb_albumid\": \"7edb51cb-77d6-4416-a23c-3a8c2994a2c7\",\n            \"mb_artistid\": \"a6623d39-2d8e-4f70-8242-0a9553b91e50\",\n            \"mb_artistids\": [\"a6623d39-2d8e-4f70-8242-0a9553b91e50\"],\n            \"tracktotal\": 2,\n            \"year\": 2013,\n            \"month\": 12,\n            \"day\": 18,\n            \"genres\": [\"Rock\", \"Pop\"],\n        }\n\n        self.expected_tracks = [\n            {\n                **common_expected,\n                \"artist\": \"trackArtist\",\n                \"artists\": [\"albumArtist\", \"albumArtist2\"],\n                \"artist_credit\": \"trackArtistCredit\",\n                \"artist_sort\": \"trackArtistSort\",\n                \"artists_credit\": [\"trackArtistCredit\"],\n                \"artists_sort\": [\"trackArtistSort\"],\n                \"disc\": 1,\n                \"mb_trackid\": \"dfa939ec-118c-4d0f-84a0-60f3d1e6522c\",\n                \"title\": \"title\",\n                \"track\": 1,\n            },\n            {\n                **common_expected,\n                \"artist\": \"albumArtist\",\n                \"artists\": [\"albumArtist\", \"albumArtist2\"],\n                \"artist_credit\": \"albumArtistCredit\",\n                \"artist_sort\": \"\",\n                \"artists_credit\": [\n                    \"albumArtistCredit1\",\n                    \"albumArtistCredit2\",\n                ],\n                \"artists_sort\": [\"albumArtistSort\", \"albumArtistSort2\"],\n                \"disc\": 2,\n                \"mb_trackid\": \"40130ed1-a27c-42fd-a328-1ebefb6caef4\",\n                \"title\": \"title2\",\n                \"track\": 2,\n            },\n        ]\n\n    def test_autotag_items(self):\n        self._apply()\n\n        keys = self.expected_tracks[0].keys()\n        get_values = operator.itemgetter(*keys)\n\n        applied_data = [\n            dict(zip(keys, get_values(dict(i)))) for i in self.items\n        ]\n\n        assert applied_data == self.expected_tracks\n\n    def test_per_disc_numbering(self):\n        self._apply(per_disc_numbering=True)\n\n        assert self.items[0].track == 1\n        assert self.items[1].track == 1\n        assert self.items[0].tracktotal == 1\n        assert self.items[1].tracktotal == 1\n\n    def test_artist_credit_prefers_artist_over_albumartist_credit(self):\n        self.info.tracks[0].update(artist=\"oldArtist\", artist_credit=None)\n\n        self._apply(artist_credit=True)\n\n        assert self.items[0].artist == \"oldArtist\"\n\n    def test_artist_credit_falls_back_to_albumartist(self):\n        self.info.artist_credit = None\n\n        self._apply(artist_credit=True)\n\n        assert self.items[1].artist == \"albumArtist\"\n\n    def test_date_only_zeroes_month_and_day(self):\n        self.items = [Item(year=1, month=2, day=3)]\n        self.info.update(year=2013, month=None, day=None)\n\n        self._apply()\n\n        assert self.items[0].year == 2013\n        assert self.items[0].month == 0\n        assert self.items[0].day == 0\n\n    def test_missing_date_applies_nothing(self):\n        self.items = [Item(year=1, month=2, day=3)]\n        self.info.update(year=None, month=None, day=None)\n\n        self._apply()\n\n        assert self.items[0].year == 1\n        assert self.items[0].month == 2\n        assert self.items[0].day == 3\n\n\n@pytest.mark.parametrize(\n    \"single_field,list_field\",\n    [\n        (\"mb_artistid\", \"mb_artistids\"),\n        (\"mb_albumartistid\", \"mb_albumartistids\"),\n        (\"albumtype\", \"albumtypes\"),\n    ],\n)\n@pytest.mark.parametrize(\n    \"single_value,list_value\",\n    [\n        (None, []),\n        (None, [\"1\"]),\n        (None, [\"1\", \"2\"]),\n        (\"1\", []),\n        (\"1\", [\"1\"]),\n        (\"1\", [\"1\", \"2\"]),\n        (\"1\", [\"2\", \"1\"]),\n    ],\n)\ndef test_correct_list_fields(\n    single_field, list_field, single_value, list_value\n):\n    \"\"\"Ensure that the first value in a list field matches the single field.\"\"\"\n    data = {single_field: single_value, list_field: list_value}\n    item = Item(**data)\n\n    correct_list_fields(item)\n\n    single_val, list_val = item[single_field], item[list_field]\n    assert (not single_val and not list_val) or single_val == list_val[0]\n"
  },
  {
    "path": "test/autotag/test_distance.py",
    "content": "import re\n\nimport pytest\n\nfrom beets.autotag import AlbumInfo, TrackInfo\nfrom beets.autotag.distance import (\n    Distance,\n    distance,\n    string_dist,\n    track_distance,\n)\nfrom beets.library import Item\nfrom beets.metadata_plugins import MetadataSourcePlugin, get_penalty\nfrom beets.plugins import BeetsPlugin\n\n_p = pytest.param\n\n\nclass TestDistance:\n    @pytest.fixture(autouse=True, scope=\"class\")\n    def setup_config(self, config):\n        config[\"match\"][\"distance_weights\"][\"data_source\"] = 2.0\n        config[\"match\"][\"distance_weights\"][\"album\"] = 4.0\n        config[\"match\"][\"distance_weights\"][\"medium\"] = 2.0\n\n    @pytest.fixture\n    def dist(self):\n        return Distance()\n\n    def test_add(self, dist):\n        dist.add(\"add\", 1.0)\n\n        assert dist._penalties == {\"add\": [1.0]}\n\n    @pytest.mark.parametrize(\n        \"key, args_with_expected\",\n        [\n            (\n                \"equality\",\n                [\n                    ((\"ghi\", [\"abc\", \"def\", \"ghi\"]), [0.0]),\n                    ((\"xyz\", [\"abc\", \"def\", \"ghi\"]), [0.0, 1.0]),\n                    ((\"abc\", re.compile(r\"ABC\", re.I)), [0.0, 1.0, 0.0]),\n                ],\n            ),\n            (\"expr\", [((True,), [1.0]), ((False,), [1.0, 0.0])]),\n            (\n                \"number\",\n                [\n                    ((1, 1), [0.0]),\n                    ((1, 2), [0.0, 1.0]),\n                    ((2, 1), [0.0, 1.0, 1.0]),\n                    ((-1, 2), [0.0, 1.0, 1.0, 1.0, 1.0, 1.0]),\n                ],\n            ),\n            (\n                \"priority\",\n                [\n                    ((\"abc\", \"abc\"), [0.0]),\n                    ((\"def\", [\"abc\", \"def\"]), [0.0, 0.5]),\n                    ((\"gh\", [\"ab\", \"cd\", \"ef\", re.compile(\"GH\", re.I)]), [0.0, 0.5, 0.75]),  # noqa: E501\n                    ((\"xyz\", [\"abc\", \"def\"]), [0.0, 0.5, 0.75, 1.0]),\n                ],\n            ),\n            (\n                \"ratio\",\n                [\n                    ((25, 100), [0.25]),\n                    ((10, 5), [0.25, 1.0]),\n                    ((-5, 5), [0.25, 1.0, 0.0]),\n                    ((5, 0), [0.25, 1.0, 0.0, 0.0]),\n                ],\n            ),\n            (\n                \"string\",\n                [\n                    ((\"abc\", \"bcd\"), [2 / 3]),\n                    ((\"abc\", None), [2 / 3, 1]),\n                    ((None, None), [2 / 3, 1, 0]),\n                ],\n            ),\n        ],\n    )  # fmt: skip\n    def test_add_methods(self, dist, key, args_with_expected):\n        method = getattr(dist, f\"add_{key}\")\n        for arg_set, expected in args_with_expected:\n            method(key, *arg_set)\n            assert dist._penalties[key] == expected\n\n    def test_distance(self, dist):\n        dist.add(\"album\", 0.5)\n        dist.add(\"media\", 0.25)\n        dist.add(\"media\", 0.75)\n\n        assert dist.distance == 0.5\n        assert dist.max_distance == 6.0\n        assert dist.raw_distance == 3.0\n\n        assert dist[\"album\"] == 1 / 3\n        assert dist[\"media\"] == 1 / 6\n\n    def test_operators(self, dist):\n        dist.add(\"data_source\", 0.0)\n        dist.add(\"album\", 0.5)\n        dist.add(\"medium\", 0.25)\n        dist.add(\"medium\", 0.75)\n        assert len(dist) == 2\n        assert list(dist) == [(\"album\", 0.2), (\"medium\", 0.2)]\n        assert dist == 0.4\n        assert dist < 1.0\n        assert dist > 0.0\n        assert dist - 0.4 == 0.0\n        assert 0.4 - dist == 0.0\n        assert float(dist) == 0.4\n\n    def test_penalties_sort(self, dist):\n        dist.add(\"album\", 0.1875)\n        dist.add(\"medium\", 0.75)\n        assert dist.items() == [(\"medium\", 0.25), (\"album\", 0.125)]\n\n        # Sort by key if distance is equal.\n        dist = Distance()\n        dist.add(\"album\", 0.375)\n        dist.add(\"medium\", 0.75)\n        assert dist.items() == [(\"album\", 0.25), (\"medium\", 0.25)]\n\n    def test_update(self, dist):\n        dist1 = dist\n        dist1.add(\"album\", 0.5)\n        dist1.add(\"media\", 1.0)\n\n        dist2 = Distance()\n        dist2.add(\"album\", 0.75)\n        dist2.add(\"album\", 0.25)\n        dist2.add(\"media\", 0.05)\n\n        dist1.update(dist2)\n\n        assert dist1._penalties == {\n            \"album\": [0.5, 0.75, 0.25],\n            \"media\": [1.0, 0.05],\n        }\n\n\nclass TestTrackDistance:\n    @pytest.fixture(scope=\"class\")\n    def info(self):\n        return TrackInfo(title=\"title\", artist=\"artist\")\n\n    @pytest.mark.parametrize(\n        \"title, artist, expected_penalty\",\n        [\n            _p(\"title\", \"artist\", False, id=\"identical\"),\n            _p(\"title\", \"Various Artists\", False, id=\"tolerate-va\"),\n            _p(\"title\", \"different artist\", True, id=\"different-artist\"),\n            _p(\"different title\", \"artist\", True, id=\"different-title\"),\n        ],\n    )\n    def test_track_distance(self, info, title, artist, expected_penalty):\n        item = Item(artist=artist, title=title)\n\n        dist = track_distance(item, info, incl_artist=True)\n        assert bool(dist) == expected_penalty, dist._penalties\n\n\nclass TestAlbumDistance:\n    @pytest.fixture(scope=\"class\")\n    def items(self):\n        return [\n            Item(\n                title=title,\n                track=track,\n                artist=\"artist\",\n                album=\"album\",\n                length=1,\n            )\n            for title, track in [(\"one\", 1), (\"two\", 2), (\"three\", 3)]\n        ]\n\n    @pytest.fixture\n    def get_dist(self, items):\n        def inner(info: AlbumInfo):\n            return distance(items, info, list(zip(items, info.tracks)))\n\n        return inner\n\n    @pytest.fixture\n    def info(self, items):\n        return AlbumInfo(\n            artist=\"artist\",\n            album=\"album\",\n            tracks=[\n                TrackInfo(\n                    title=i.title,\n                    artist=i.artist,\n                    index=i.track,\n                    length=i.length,\n                )\n                for i in items\n            ],\n            va=False,\n        )\n\n    def test_identical_albums(self, get_dist, info):\n        assert get_dist(info) == 0\n\n    def test_incomplete_album(self, get_dist, info):\n        info.tracks.pop(2)\n\n        assert 0 < float(get_dist(info)) < 0.2\n\n    def test_overly_complete_album(self, get_dist, info):\n        info.tracks.append(\n            Item(index=4, title=\"four\", artist=\"artist\", length=1)\n        )\n\n        assert 0 < float(get_dist(info)) < 0.2\n\n    @pytest.mark.parametrize(\"va\", [True, False])\n    def test_albumartist(self, get_dist, info, va):\n        info.artist = \"another artist\"\n        info.va = va\n\n        assert bool(get_dist(info)) is not va\n\n    def test_comp_no_track_artists(self, get_dist, info):\n        # Some VA releases don't have track artists (incomplete metadata).\n        info.artist = \"another artist\"\n        info.va = True\n        for track in info.tracks:\n            track.artist = None\n\n        assert get_dist(info) == 0\n\n    def test_comp_track_artists_do_not_match(self, get_dist, info):\n        info.va = True\n        info.tracks[0].artist = \"another artist\"\n\n        assert get_dist(info) != 0\n\n    def test_tracks_out_of_order(self, get_dist, info):\n        tracks = info.tracks\n        tracks[1].title, tracks[2].title = tracks[2].title, tracks[1].title\n\n        assert 0 < float(get_dist(info)) < 0.2\n\n    def test_two_medium_release(self, get_dist, info):\n        info.tracks[0].medium_index = 1\n        info.tracks[1].medium_index = 2\n        info.tracks[2].medium_index = 1\n\n        assert get_dist(info) == 0\n\n\nclass TestStringDistance:\n    @pytest.mark.parametrize(\n        \"string1, string2\",\n        [\n            (\"Some String\", \"Some String\"),\n            (\"Some String\", \"Some.String!\"),\n            (\"Some String\", \"sOME sTring\"),\n            (\"My Song (EP)\", \"My Song\"),\n            (\"The Song Title\", \"Song Title, The\"),\n            (\"A Song Title\", \"Song Title, A\"),\n            (\"An Album Title\", \"Album Title, An\"),\n            (\"\", \"\"),\n            (\"Untitled\", \"[Untitled]\"),\n            (\"And\", \"&\"),\n            (\"\\xe9\\xe1\\xf1\", \"ean\"),\n        ],\n    )\n    def test_matching_distance(self, string1, string2):\n        assert string_dist(string1, string2) == 0.0\n\n    def test_different_distance(self):\n        assert string_dist(\"Some String\", \"Totally Different\") != 0.0\n\n    @pytest.mark.parametrize(\n        \"string1, string2, reference\",\n        [\n            (\"XXX Band Name\", \"The Band Name\", \"Band Name\"),\n            (\"One .Two.\", \"One (Two)\", \"One\"),\n            (\"One .Two.\", \"One [Two]\", \"One\"),\n            (\"My Song blah Someone\", \"My Song feat Someone\", \"My Song\"),\n        ],\n    )\n    def test_relative_weights(self, string1, string2, reference):\n        assert string_dist(string2, reference) < string_dist(string1, reference)\n\n    def test_solo_pattern(self):\n        # Just make sure these don't crash.\n        string_dist(\"The \", \"\")\n        string_dist(\"(EP)\", \"(EP)\")\n        string_dist(\", An\", \"\")\n\n\nclass TestDataSourceDistance:\n    MATCH = 0.0\n    MISMATCH = 0.125\n\n    @pytest.fixture(autouse=True)\n    def setup(self, monkeypatch, penalty, weight, multiple_data_sources):\n        monkeypatch.setitem(Distance._weights, \"data_source\", weight)\n        get_penalty.cache_clear()\n\n        class TestMetadataSourcePlugin(MetadataSourcePlugin):\n            def album_for_id(self, *args, **kwargs): ...\n            def track_for_id(self, *args, **kwargs): ...\n            def candidates(self, *args, **kwargs): ...\n            def item_candidates(self, *args, **kwargs): ...\n\n        # We use BeetsPlugin here to check if our compatibility layer\n        # for pre 2.4.0 MetadataPlugins is working as expected\n        # TODO: Replace BeetsPlugin with TestMetadataSourcePlugin in v3.0.0\n        with pytest.deprecated_call():\n\n            class OriginalPlugin(BeetsPlugin):\n                data_source = \"Original\"\n\n        class OtherPlugin(TestMetadataSourcePlugin):\n            @property\n            def data_source_mismatch_penalty(self):\n                return penalty\n\n        monkeypatch.setattr(\n            \"beets.metadata_plugins.find_metadata_source_plugins\",\n            lambda: (\n                [OriginalPlugin(), OtherPlugin()]\n                if multiple_data_sources\n                else [OtherPlugin()]\n            ),\n        )\n\n    @pytest.mark.parametrize(\n        \"item,info,penalty,weight,multiple_data_sources,expected_distance\",\n        [\n            _p(\"Original\", \"Original\", 0.5, 1.0, True, MATCH, id=\"match\"),\n            _p(\"Original\", \"Other\", 0.5, 1.0, True, MISMATCH, id=\"mismatch\"),\n            _p(\"Other\", \"Original\", 0.5, 1.0, True, MISMATCH, id=\"mismatch\"),\n            _p(\"Original\", \"unknown\", 0.5, 1.0, True, MISMATCH, id=\"mismatch-unknown\"),\n            _p(\"Original\", None, 0.5, 1.0, True, MISMATCH, id=\"mismatch-no-info\"),\n            _p(None, \"Other\", 0.5, 1.0, True, MISMATCH, id=\"mismatch-no-original-multiple-sources\"),  # noqa: E501\n            _p(None, \"Other\", 0.5, 1.0, False, MATCH, id=\"match-no-original-but-single-source\"),  # noqa: E501\n            _p(\"unknown\", \"unknown\", 0.5, 1.0, True, MATCH, id=\"match-unknown\"),\n            _p(\"Original\", \"Other\", 1.0, 1.0, True, 0.25, id=\"mismatch-max-penalty\"),\n            _p(\"Original\", \"Other\", 0.5, 5.0, True, 0.3125, id=\"mismatch-high-weight\"),\n            _p(\"Original\", \"Other\", 0.0, 1.0, True, MATCH, id=\"match-no-penalty\"),\n            _p(\"Original\", \"Other\", 0.5, 0.0, True, MATCH, id=\"match-no-weight\"),\n        ],\n    )  # fmt: skip\n    def test_distance(self, item, info, expected_distance):\n        item = Item(data_source=item)\n        info = TrackInfo(data_source=info, title=\"\")\n\n        dist = track_distance(item, info)\n\n        assert dist.distance == expected_distance\n"
  },
  {
    "path": "test/autotag/test_hooks.py",
    "content": "import pytest\n\nfrom beets.autotag.hooks import Info\n\n\n@pytest.mark.parametrize(\n    \"genre, expected_genres\",\n    [\n        (\"Rock\", (\"Rock\",)),\n        (\"Rock; Alternative\", (\"Rock\", \"Alternative\")),\n    ],\n)\ndef test_genre_deprecation(genre, expected_genres):\n    with pytest.warns(\n        DeprecationWarning, match=\"The 'genre' parameter is deprecated\"\n    ):\n        assert tuple(Info(genre=genre).genres) == expected_genres\n"
  },
  {
    "path": "test/autotag/test_match.py",
    "content": "from typing import ClassVar\n\nimport pytest\n\nfrom beets import metadata_plugins\nfrom beets.autotag import AlbumInfo, TrackInfo, match\nfrom beets.library import Item\n\n\nclass TestAssignment:\n    A = \"one\"\n    B = \"two\"\n    C = \"three\"\n\n    @pytest.fixture(autouse=True)\n    def config(self, config):\n        config[\"match\"][\"track_length_grace\"] = 10\n        config[\"match\"][\"track_length_max\"] = 30\n\n    @pytest.mark.parametrize(\n        # 'expected' is a tuple of expected (mapping, extra_items, extra_tracks)\n        \"item_titles, track_titles, expected\",\n        [\n            # items ordering gets corrected\n            ([A, C, B], [A, B, C], ({A: A, B: B, C: C}, [], [])),\n            # unmatched tracks are returned as 'extra_tracks'\n            # the first track is unmatched\n            ([B, C], [A, B, C], ({B: B, C: C}, [], [A])),\n            # the middle track is unmatched\n            ([A, C], [A, B, C], ({A: A, C: C}, [], [B])),\n            # the last track is unmatched\n            ([A, B], [A, B, C], ({A: A, B: B}, [], [C])),\n            # unmatched items are returned as 'extra_items'\n            ([A, C, B], [A, C], ({A: A, C: C}, [B], [])),\n        ],\n    )\n    def test_assign_tracks(self, item_titles, track_titles, expected):\n        expected_mapping, expected_extra_items, expected_extra_tracks = expected\n\n        items = [Item(title=title) for title in item_titles]\n        tracks = [TrackInfo(title=title) for title in track_titles]\n\n        item_info_pairs, extra_items, extra_tracks = match.assign_items(\n            items, tracks\n        )\n\n        assert (\n            {i.title: t.title for i, t in item_info_pairs},\n            [i.title for i in extra_items],\n            [t.title for t in extra_tracks],\n        ) == (expected_mapping, expected_extra_items, expected_extra_tracks)\n\n    def test_order_works_when_track_names_are_entirely_wrong(self):\n        # A real-world test case contributed by a user.\n        def item(i, length):\n            return Item(\n                artist=\"ben harper\",\n                album=\"burn to shine\",\n                title=f\"ben harper - Burn to Shine {i}\",\n                track=i,\n                length=length,\n            )\n\n        items = []\n        items.append(item(1, 241.37243007106997))\n        items.append(item(2, 342.27781704375036))\n        items.append(item(3, 245.95070222338137))\n        items.append(item(4, 472.87662515485437))\n        items.append(item(5, 279.1759535763187))\n        items.append(item(6, 270.33333768012))\n        items.append(item(7, 247.83435613222923))\n        items.append(item(8, 216.54504531525072))\n        items.append(item(9, 225.72775379800484))\n        items.append(item(10, 317.7643606963552))\n        items.append(item(11, 243.57001238834192))\n        items.append(item(12, 186.45916150485752))\n\n        def info(index, title, length):\n            return TrackInfo(title=title, length=length, index=index)\n\n        trackinfo = []\n        trackinfo.append(info(1, \"Alone\", 238.893))\n        trackinfo.append(info(2, \"The Woman in You\", 341.44))\n        trackinfo.append(info(3, \"Less\", 245.59999999999999))\n        trackinfo.append(info(4, \"Two Hands of a Prayer\", 470.49299999999999))\n        trackinfo.append(info(5, \"Please Bleed\", 277.86599999999999))\n        trackinfo.append(info(6, \"Suzie Blue\", 269.30599999999998))\n        trackinfo.append(info(7, \"Steal My Kisses\", 245.36000000000001))\n        trackinfo.append(info(8, \"Burn to Shine\", 214.90600000000001))\n        trackinfo.append(info(9, \"Show Me a Little Shame\", 224.0929999999999))\n        trackinfo.append(info(10, \"Forgiven\", 317.19999999999999))\n        trackinfo.append(info(11, \"Beloved One\", 243.733))\n        trackinfo.append(info(12, \"In the Lord's Arms\", 186.13300000000001))\n\n        expected = list(zip(items, trackinfo)), [], []\n\n        assert match.assign_items(items, trackinfo) == expected\n\n\nclass TestTagMultipleDataSources:\n    @pytest.fixture\n    def shared_track_id(self):\n        return \"track-12345\"\n\n    @pytest.fixture\n    def shared_album_id(self):\n        return \"album-12345\"\n\n    @pytest.fixture(autouse=True)\n    def _setup_plugins(self, monkeypatch, shared_album_id, shared_track_id):\n        class StubPlugin:\n            data_source: ClassVar[str]\n            data_source_mismatch_penalty = 0\n\n            @property\n            def track(self):\n                return TrackInfo(\n                    artist=\"Artist\",\n                    title=\"Title\",\n                    track_id=shared_track_id,\n                    data_source=self.data_source,\n                )\n\n            @property\n            def album(self):\n                return AlbumInfo(\n                    [self.track],\n                    artist=\"Albumartist\",\n                    album=\"Album\",\n                    album_id=shared_album_id,\n                    data_source=self.data_source,\n                )\n\n            def albums_for_ids(self, *_):\n                yield self.album\n\n            def tracks_for_ids(self, *_):\n                yield self.track\n\n            def candidates(self, *_, **__):\n                yield self.album\n\n            def item_candidates(self, *_, **__):\n                yield self.track\n\n        class DeezerPlugin(StubPlugin):\n            data_source = \"Deezer\"\n\n        class DiscogsPlugin(StubPlugin):\n            data_source = \"Discogs\"\n\n        monkeypatch.setattr(\n            metadata_plugins,\n            \"find_metadata_source_plugins\",\n            lambda: [DeezerPlugin(), DiscogsPlugin()],\n        )\n\n    def check_proposal(self, proposal):\n        sources = [\n            candidate.info.data_source for candidate in proposal.candidates\n        ]\n        assert len(sources) == 2\n        assert set(sources) == {\"Discogs\", \"Deezer\"}\n\n    def test_search_album_ids(self, shared_album_id):\n        _, _, proposal = match.tag_album([Item()], search_ids=[shared_album_id])\n\n        self.check_proposal(proposal)\n\n    def test_search_album_current_id(self, shared_album_id):\n        _, _, proposal = match.tag_album([Item(mb_albumid=shared_album_id)])\n\n        self.check_proposal(proposal)\n\n    def test_search_track_ids(self, shared_track_id):\n        proposal = match.tag_item(Item(), search_ids=[shared_track_id])\n\n        self.check_proposal(proposal)\n\n    def test_search_track_current_id(self, shared_track_id):\n        proposal = match.tag_item(Item(mb_trackid=shared_track_id))\n\n        self.check_proposal(proposal)\n"
  },
  {
    "path": "test/conftest.py",
    "content": "import importlib.util\nimport inspect\nimport os\nfrom functools import cache\n\nimport pytest\n\nfrom beets.autotag.distance import Distance\nfrom beets.dbcore.query import Query\nfrom beets.test._common import DummyIO\nfrom beets.test.helper import ConfigMixin\nfrom beets.util import cached_classproperty\n\n\n@cache\ndef _is_importable(modname: str) -> bool:\n    return bool(importlib.util.find_spec(modname))\n\n\ndef skip_marked_items(items: list[pytest.Item], marker_name: str, reason: str):\n    for item in (i for i in items if i.get_closest_marker(marker_name)):\n        test_name = item.nodeid.split(\"::\", 1)[-1]\n        item.add_marker(pytest.mark.skip(f\"{reason}: {test_name}\"))\n\n\ndef pytest_collection_modifyitems(\n    config: pytest.Config, items: list[pytest.Item]\n):\n    if not os.environ.get(\"INTEGRATION_TEST\") == \"true\":\n        skip_marked_items(\n            items, \"integration_test\", \"INTEGRATION_TEST=1 required\"\n        )\n\n    if not os.environ.get(\"LYRICS_UPDATED\") == \"true\":\n        skip_marked_items(\n            items, \"on_lyrics_update\", \"No change in lyrics source code\"\n        )\n\n    for item in items:\n        if marker := item.get_closest_marker(\"requires_import\"):\n            force_ci = marker.kwargs.get(\"force_ci\", True)\n            if (\n                force_ci\n                and os.environ.get(\"GITHUB_ACTIONS\") == \"true\"\n                # only apply this to our repository, to allow other projects to\n                # run tests without installing all dependencies\n                and os.environ.get(\"GITHUB_REPOSITORY\", \"\") == \"beetbox/beets\"\n            ):\n                continue\n\n            modname = marker.args[0]\n            if not _is_importable(modname):\n                test_name = item.nodeid.split(\"::\", 1)[-1]\n                item.add_marker(\n                    pytest.mark.skip(\n                        f\"{modname!r} is not installed: {test_name}\"\n                    )\n                )\n\n\ndef pytest_configure(config: pytest.Config) -> None:\n    config.addinivalue_line(\n        \"markers\",\n        \"integration_test: mark a test as an integration test\",\n    )\n    config.addinivalue_line(\n        \"markers\",\n        \"on_lyrics_update: run test only when lyrics source code changes\",\n    )\n    config.addinivalue_line(\n        \"markers\",\n        (\n            \"requires_import(module, force_ci=True): run test only if module\"\n            \" is importable (use force_ci=False to allow CI to skip the test too)\"\n        ),\n    )\n\n\ndef pytest_make_parametrize_id(config, val, argname):\n    \"\"\"Generate readable test identifiers for pytest parametrized tests.\n\n    Provides custom string representations for:\n    - Query classes/instances: use class name\n    - Lambda functions: show abbreviated source\n    - Other values: use standard repr()\n    \"\"\"\n    if inspect.isclass(val) and issubclass(val, Query):\n        return val.__name__\n\n    if inspect.isfunction(val) and val.__name__ == \"<lambda>\":\n        return inspect.getsource(val).split(\"lambda\")[-1][:30]\n\n    return repr(val)\n\n\ndef pytest_assertrepr_compare(op, left, right):\n    if isinstance(left, Distance) or isinstance(right, Distance):\n        return [f\"Comparing Distance: {float(left)} {op} {float(right)}\"]\n\n\n@pytest.fixture(autouse=True)\ndef clear_cached_classproperty():\n    cached_classproperty.cache.clear()\n\n\n@pytest.fixture(scope=\"module\")\ndef config():\n    \"\"\"Provide a fresh beets configuration for a module, when requested.\"\"\"\n    return ConfigMixin().config\n\n\n@pytest.fixture\ndef io(\n    request: pytest.FixtureRequest,\n    monkeypatch: pytest.MonkeyPatch,\n    capteesys: pytest.CaptureFixture[str],\n) -> DummyIO:\n    \"\"\"Fixture for tests that need controllable stdin and captured stdout.\n\n    This fixture builds a per-test ``DummyIO`` helper and exposes it to the\n    test. When used on a test class, it attaches the helper as ``self.io``\n    attribute to make it available to all test methods, including\n    ``unittest.TestCase``-based ones.\n    \"\"\"\n    io = DummyIO(monkeypatch, capteesys)\n\n    if request.instance:\n        request.instance.io = io\n\n    return io\n\n\n@pytest.fixture\ndef is_importable():\n    \"\"\"Fixture that provides a function to check if a module can be imported.\"\"\"\n\n    return _is_importable\n"
  },
  {
    "path": "test/library/__init__.py",
    "content": ""
  },
  {
    "path": "test/library/test_migrations.py",
    "content": "import textwrap\n\nimport pytest\n\nfrom beets.dbcore import types\nfrom beets.library import migrations\nfrom beets.library.models import Album, Item\nfrom beets.test.helper import TestHelper\n\n\nclass TestMultiGenreFieldMigration:\n    @pytest.fixture\n    def helper(self, monkeypatch):\n        # do not apply migrations upon library initialization\n        monkeypatch.setattr(\"beets.library.library.Library._migrations\", ())\n        # add genre field to both models to make sure this column is created\n        monkeypatch.setattr(\n            \"beets.library.models.Item._fields\",\n            {**Item._fields, \"genre\": types.STRING},\n        )\n        monkeypatch.setattr(\n            \"beets.library.models.Album._fields\",\n            {**Album._fields, \"genre\": types.STRING},\n        )\n        monkeypatch.setattr(\n            \"beets.library.models.Album.item_keys\",\n            {*Album.item_keys, \"genre\"},\n        )\n        helper = TestHelper()\n        helper.setup_beets()\n\n        # and now configure the migrations to be tested\n        monkeypatch.setattr(\n            \"beets.library.library.Library._migrations\",\n            ((migrations.MultiGenreFieldMigration, (Item, Album)),),\n        )\n        yield helper\n\n        helper.teardown_beets()\n\n    def test_migrate(self, helper: TestHelper):\n        helper.config[\"lastgenre\"][\"separator\"] = \" - \"\n\n        expected_item_genres = []\n        for genre, initial_genres, expected_genres in [\n            # already existing value is not overwritten\n            (\"Item Rock\", (\"Ignored\",), (\"Ignored\",)),\n            (\"\", (), ()),\n            (\"Rock\", (), (\"Rock\",)),\n            # multiple genres are split on one of default separators\n            (\"Item Rock; Alternative\", (), (\"Item Rock\", \"Alternative\")),\n            # multiple genres are split the first (lastgenre) separator ONLY\n            (\"Item - Rock, Alternative\", (), (\"Item\", \"Rock, Alternative\")),\n        ]:\n            helper.add_item(genre=genre, genres=initial_genres)\n            expected_item_genres.append(expected_genres)\n\n        unmigrated_album = helper.add_album(\n            genre=\"Album Rock / Alternative\", genres=[]\n        )\n        expected_item_genres.append((\"Album Rock\", \"Alternative\"))\n\n        helper.lib._migrate()\n\n        actual_item_genres = [tuple(i.genres) for i in helper.lib.items()]\n        assert actual_item_genres == expected_item_genres\n\n        unmigrated_album.load()\n        assert unmigrated_album.genres == [\"Album Rock\", \"Alternative\"]\n\n        # remove cached initial db tables data\n        del helper.lib.db_tables\n        assert helper.lib.migration_exists(\"multi_genre_field\", \"items\")\n        assert helper.lib.migration_exists(\"multi_genre_field\", \"albums\")\n\n\nclass TestLyricsMetadataInFlexFieldsMigration:\n    @pytest.fixture\n    def helper(self, monkeypatch):\n        # do not apply migrations upon library initialization\n        monkeypatch.setattr(\"beets.library.library.Library._migrations\", ())\n\n        helper = TestHelper()\n        helper.setup_beets()\n\n        # and now configure the migrations to be tested\n        monkeypatch.setattr(\n            \"beets.library.library.Library._migrations\",\n            ((migrations.LyricsMetadataInFlexFieldsMigration, (Item,)),),\n        )\n        yield helper\n\n        helper.teardown_beets()\n\n    def test_migrate(self, helper: TestHelper, is_importable):\n        lyrics_item = helper.add_item(\n            lyrics=textwrap.dedent(\"\"\"\n            [00:00.00] Some synced lyrics / Quelques paroles synchronisées\n            [00:00.50]\n            [00:01.00] Some more synced lyrics / Quelques paroles plus synchronisées\n\n            Source: https://lrclib.net/api/1/\"\"\")\n        )\n        instrumental_lyrics_item = helper.add_item(lyrics=\"[Instrumental]\")\n\n        helper.lib._migrate()\n\n        lyrics_item.load()\n\n        assert lyrics_item.lyrics == textwrap.dedent(\n            \"\"\"\n            [00:00.00] Some synced lyrics / Quelques paroles synchronisées\n            [00:00.50]\n            [00:01.00] Some more synced lyrics / Quelques paroles plus synchronisées\"\"\"\n        )\n        assert lyrics_item.lyrics_backend == \"lrclib\"\n        assert lyrics_item.lyrics_url == \"https://lrclib.net/api/1/\"\n\n        if is_importable(\"langdetect\"):\n            assert lyrics_item.lyrics_language == \"EN\"\n            assert lyrics_item.lyrics_translation_language == \"FR\"\n        else:\n            with pytest.raises(AttributeError):\n                instrumental_lyrics_item.lyrics_language\n            with pytest.raises(AttributeError):\n                instrumental_lyrics_item.lyrics_translation_language\n\n        with pytest.raises(AttributeError):\n            instrumental_lyrics_item.lyrics_backend\n        with pytest.raises(AttributeError):\n            instrumental_lyrics_item.lyrics_url\n        with pytest.raises(AttributeError):\n            instrumental_lyrics_item.lyrics_language\n        with pytest.raises(AttributeError):\n            instrumental_lyrics_item.lyrics_translation_language\n\n        # remove cached initial db tables data\n        del helper.lib.db_tables\n        assert helper.lib.migration_exists(\n            \"lyrics_metadata_in_flex_fields\", \"items\"\n        )\n"
  },
  {
    "path": "test/plugins/__init__.py",
    "content": ""
  },
  {
    "path": "test/plugins/conftest.py",
    "content": "from __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nimport pytest\nimport requests\n\nif TYPE_CHECKING:\n    from requests_mock import Mocker\n\n\n@pytest.fixture\ndef requests_mock(requests_mock, monkeypatch) -> Mocker:\n    \"\"\"Use plain session wherever MB requests are mocked.\n\n    This avoids rate limiting requests to speed up tests.\n    \"\"\"\n    monkeypatch.setattr(\n        \"beetsplug._utils.musicbrainz.MusicBrainzAPI.create_session\",\n        lambda _: requests.Session(),\n    )\n    return requests_mock\n"
  },
  {
    "path": "test/plugins/lyrics_pages.py",
    "content": "from __future__ import annotations\n\nimport os\nimport textwrap\nfrom typing import NamedTuple\nfrom urllib.parse import urlparse\n\nimport pytest\n\n\ndef xfail_on_ci(msg: str) -> pytest.MarkDecorator:\n    return pytest.mark.xfail(\n        bool(os.environ.get(\"GITHUB_ACTIONS\")),\n        reason=msg,\n        raises=AssertionError,\n    )\n\n\nclass LyricsPage(NamedTuple):\n    \"\"\"Lyrics page representation for integrated tests.\"\"\"\n\n    url: str\n    lyrics: str\n    artist: str = \"The Beatles\"\n    track_title: str = \"Lady Madonna\"\n    language: str = \"EN\"\n    url_title: str | None = None  # only relevant to the Google backend\n    marks: list[str] = []  # markers for pytest.param  # noqa: RUF012\n\n    def __str__(self) -> str:\n        \"\"\"Return name of this test case.\"\"\"\n        return f\"{self.backend}-{self.source}\"\n\n    @classmethod\n    def make(cls, url, lyrics, *args, **kwargs):\n        return cls(url, textwrap.dedent(lyrics).strip(), *args, **kwargs)\n\n    @property\n    def root_url(self) -> str:\n        return urlparse(self.url).netloc\n\n    @property\n    def source(self) -> str:\n        return self.root_url.replace(\"www.\", \"\").split(\".\")[0]\n\n    @property\n    def backend(self) -> str:\n        if (source := self.source) in {\"genius\", \"tekstowo\", \"lrclib\"}:\n            return source\n        return \"google\"\n\n\nlyrics_pages = [\n    LyricsPage.make(\n        \"http://www.absolutelyrics.com/lyrics/view/the_beatles/lady_madonna\",\n        \"\"\"\n        The Beatles - Lady Madonna\n\n        Lady Madonna, children at your feet.\n        Wonder how you manage to make ends meet.\n        Who finds the money? When you pay the rent?\n        Did you think that money was heaven sent?\n        Friday night arrives without a suitcase.\n        Sunday morning creep in like a nun.\n        Monday's child has learned to tie his bootlace.\n        See how they run.\n        Lady Madonna, baby at your breast.\n        Wonder how you manage to feed the rest.\n        See how they run.\n        Lady Madonna, lying on the bed,\n        Listen to the music playing in your head.\n        Tuesday afternoon is never ending.\n        Wednesday morning papers didn't come.\n        Thursday night you stockings needed mending.\n        See how they run.\n        Lady Madonna, children at your feet.\n        Wonder how you manage to make ends meet.\n        \"\"\",\n        url_title=\"Lady Madonna Lyrics :: The Beatles - Absolute Lyrics\",\n    ),\n    LyricsPage.make(\n        \"https://www.azlyrics.com/lyrics/beatles/ladymadonna.html\",\n        \"\"\"\n        Lady Madonna, children at your feet\n        Wonder how you manage to make ends meet\n        Who finds the money when you pay the rent\n        Did you think that money was Heaven sent?\n        Friday night arrives without a suitcase\n        Sunday morning creeping like a nun\n        Monday's child has learned to tie his bootlace\n        See how they run\n\n        Lady Madonna, baby at your breast\n        Wonders how you manage to feed the rest?\n\n        See how they run\n\n        Lady Madonna lying on the bed\n        Listen to the music playing in your head\n\n        Tuesday afternoon is never ending\n        Wednesday morning papers didn't come\n        Thursday night your stockings needed mending\n        See how they run\n\n        Lady Madonna, children at your feet\n        Wonder how you manage to make ends meet\n        \"\"\",\n        url_title=\"The Beatles - Lady Madonna Lyrics | AZLyrics.com\",\n        marks=[xfail_on_ci(\"AZLyrics is blocked by Cloudflare\")],\n    ),\n    LyricsPage.make(\n        \"https://www.dainuzodziai.lt/m/mergaites-nori-mylet-atlanta/\",\n        \"\"\"\n        Jos nesuspėja skriet paskui vėją\n        Bangos į krantą grąžina jas vėl\n        Jos karštą saulę paliesti norėjo\n        Ant kranto palikę visas negandas\n\n        Bet jos nori mylėt\n        Jos nenori liūdėt\n        Leisk mergaitėms mylėt\n        Kaip jos moka mylėt\n        Koks vakaras šiltas ir nieko nestinga\n        Veidus apšviečia žaisminga šviesa\n        Jos buvo laimingos prie jūros kur liko\n        Tik vėjas išmokęs visas jų dainas\n        \"\"\",\n        artist=\"Atlanta\",\n        track_title=\"Mergaitės Nori Mylėt\",\n        language=\"LT\",\n        url_title=\"Mergaitės nori mylėt – Atlanta | Dainų Žodžiai\",\n        marks=[xfail_on_ci(\"Expired SSL certificate\")],\n    ),\n    LyricsPage.make(\n        \"https://genius.com/The-beatles-lady-madonna-lyrics\",\n        \"\"\"\n        [Verse 1: Paul McCartney]\n        Lady Madonna, children at your feet\n        Wonder how you manage to make ends meet\n        Who finds the money when you pay the rent?\n        Did you think that money was heaven sent?\n\n        [Bridge: Paul McCartney, Paul McCartney, John Lennon & George Harrison]\n        Friday night arrives without a suitcase\n        Sunday morning creeping like a nun\n        Monday's child has learned to tie his bootlace\n        See how they run\n\n        [Verse 2: Paul McCartney]\n        Lady Madonna, baby at your breast\n        Wonders how you manage to feed the rest\n        [Tenor Saxophone Solo: Ronnie Scott]\n\n        [Bridge: John Lennon & George Harrison, Paul McCartney, John Lennon & George Harrison]\n        Pa-pa-pa-pa, pa-pa-pa-pa-pa\n        Pa-pa-pa-pa-pa, pa-pa-pa, pa-pa, pa-pa\n        Pa-pa-pa-pa, pa-pa-pa-pa-pa\n        See how they run\n\n        [Verse 3: Paul McCartney]\n        Lady Madonna, lying on the bed\n        Listen to the music playing in your head\n\n        [Bridge: Paul McCartney, John Lennon & George Harrison, Paul McCartney, John Lennon & George Harrison]\n        Tuesday afternoon is never ending (Pa-pa-pa-pa, pa-pa-pa-pa-pa)\n        Wednesday morning, papers didn't come (Pa-pa-pa-pa-pa, pa-pa-pa, pa-pa, pa-pa)\n        Thursday night, your stockings needed mending (Pa-pa-pa-pa, pa-pa-pa-pa-pa)\n        See how they run\n\n        [Verse 4: Paul McCartney]\n        Lady Madonna, children at your feet\n        Wonder how you manage to make ends meet\n        \"\"\",  # noqa: E501\n        marks=[xfail_on_ci(\"Genius returns 403 FORBIDDEN in CI\")],\n    ),\n    LyricsPage.make(\n        \"https://www.lacoccinelle.net/259956-the-beatles-lady-madonna.html\",\n        \"\"\"\n        Lady Madonna\n        Mademoiselle Madonna\n\n        Lady Madonna, children at your feet.\n        Mademoiselle Madonna, les enfants à vos pieds\n        Wonder how you manage to make ends meet.\n        Je me demande comment vous vous débrouillez pour joindre les deux bouts\n        Who finds the money, when you pay the rent ?\n        Qui trouve l'argent pour payer le loyer ?\n        Did you think that money was heaven sent ?\n        Pensiez-vous que ça allait être envoyé du ciel ?\n\n        Friday night arrives without a suitcase.\n        Le vendredi soir arrive sans bagages\n        Sunday morning creeping like a nun.\n        Le dimanche matin elle se traine comme une nonne\n        Monday's child has learned to tie his bootlace.\n        Lundi l'enfant a appris à lacer ses chaussures\n        See how they run.\n        Regardez comme ils courent\n\n        Lady Madonna, baby at your breast.\n        Mademoiselle Madonna, le bébé a votre sein\n        Wonder how you manage to feed the rest.\n        Je me demande comment vous faites pour nourrir le reste\n\n        Lady Madonna, lying on the bed,\n        Mademoiselle Madonna, couchée sur votre lit\n        Listen to the music playing in your head.\n        Vous écoutez la musique qui joue dans votre tête\n\n        Tuesday afternoon is never ending.\n        Le mardi après-midi n'en finit pas\n        Wednesday morning papers didn't come.\n        Le mercredi matin les journaux ne sont pas arrivés\n        Thursday night you stockings needed mending.\n        Jeudi soir, vos bas avaient besoin d'être réparés\n        See how they run.\n        Regardez comme ils filent\n\n        Lady Madonna, children at your feet.\n        Mademoiselle Madonna, les enfants à vos pieds\n        Wonder how you manage to make ends meet.\n        Je me demande comment vous vous débrouillez pour joindre les deux bouts\n        \"\"\",\n        url_title=\"Paroles et traduction The Beatles : Lady Madonna - paroles de chanson\",  # noqa: E501\n        language=\"FR\",\n    ),\n    LyricsPage.make(\n        # note that this URL needs to be followed with a slash, otherwise it\n        # redirects to the same URL with a slash\n        \"https://www.letras.mus.br/the-beatles/275/\",\n        \"\"\"\n        Lady Madonna\n        Children at your feet\n        Wonder how you manage\n        To make ends meet\n\n        Who finds the money\n        When you pay the rent?\n        Did you think that money\n        Was Heaven sent?\n\n        Friday night arrives without a suitcase\n        Sunday morning creeping like a nun\n        Monday's child has learned\n        To tie his bootlace\n        See how they run\n\n        Lady Madonna\n        Baby at your breast\n        Wonders how you manage\n        To feed the rest\n        See how they run\n\n        Lady Madonna\n        Lying on the bed\n        Listen to the music\n        Playing in your head\n\n        Tuesday afternoon is neverending\n        Wednesday morning papers didn't come\n        Thursday night your stockings\n        Needed mending\n        See how they run\n\n        Lady Madonna\n        Children at your feet\n        Wonder how you manage\n        To make ends meet\n        \"\"\",\n        url_title=\"Lady Madonna - The Beatles - LETRAS.MUS.BR\",\n    ),\n    LyricsPage.make(\n        \"https://lrclib.net/api/get/19648857\",\n        \"\"\"\n        [00:08.35] Lady Madonna, children at your feet\n        [00:12.85] Wonder how you manage to make ends meet\n        [00:17.56] Who finds the money when you pay the rent\n        [00:21.78] Did you think that money was heaven sent\n        [00:26.22] Friday night arrives without a suitcase\n        [00:30.02] Sunday morning creeping like a nun\n        [00:34.53] Monday's child has learned to tie his bootlace\n        [00:39.18] See how they run\n        [00:43.33] Lady Madonna, baby at your breast\n        [00:48.50] Wonders how you manage to feed the rest\n        [00:52.54]\n        [01:01.32] Ba-ba, ba-ba, ba-ba, ba-ba-ba\n        [01:05.03] Ba-ba, ba-ba, ba-ba, ba, ba-ba, ba-ba\n        [01:09.58] Ba-ba, ba-ba, ba-ba, ba-ba-ba\n        [01:14.27] See how they run\n        [01:19.05] Lady Madonna, lying on the bed\n        [01:22.99] Listen to the music playing in your head\n        [01:27.92]\n        [01:36.33] Tuesday afternoon is never ending\n        [01:40.47] Wednesday morning papers didn't come\n        [01:44.76] Thursday night your stockings needed mending\n        [01:49.35] See how they run\n        [01:53.73] Lady Madonna, children at your feet\n        [01:58.65] Wonder how you manage to make ends meet\n        [02:06.04]\n        \"\"\",\n    ),\n    LyricsPage.make(\n        \"https://www.lyricsmania.com/lady_madonna_lyrics_the_beatles.html\",\n        \"\"\"\n        Lady Madonna, children at your feet.\n        Wonder how you manage to make ends meet.\n        Who finds the money? When you pay the rent?\n        Did you think that money was heaven sent?\n\n        Friday night arrives without a suitcase.\n        Sunday morning creep in like a nun.\n        Monday's child has learned to tie his bootlace.\n        See how they run.\n\n        Lady Madonna, baby at your breast.\n        Wonder how you manage to feed the rest.\n\n        See how they run.\n        Lady Madonna, lying on the bed,\n        Listen to the music playing in your head.\n\n        Tuesday afternoon is never ending.\n        Wednesday morning papers didn't come.\n        Thursday night you stockings needed mending.\n        See how they run.\n\n        Lady Madonna, children at your feet.\n        Wonder how you manage to make ends meet.\n        \"\"\",\n        url_title=\"The Beatles - Lady Madonna Lyrics\",\n    ),\n    LyricsPage.make(\n        \"https://www.lyricsmode.com/lyrics/b/beatles/mother_natures_son.html\",\n        \"\"\"\n        Born a poor young country boy, Mother Nature's son\n        All day long I'm sitting singing songs for everyone\n\n        Sit beside a mountain stream, see her waters rise\n        Listen to the pretty sound of music as she flies\n\n        Doo doo doo doo doo doo doo doo doo doo doo\n        Doo doo doo doo doo doo doo doo doo\n        Doo doo doo\n\n        Find me in my field of grass, Mother Nature's son\n        Swaying daises sing a lazy song beneath the sun\n\n        Doo doo doo doo doo doo doo doo doo doo doo\n        Doo doo doo doo doo doo doo doo doo\n        Doo doo doo doo doo doo\n        Yeah yeah yeah\n\n        Mm mm mm mm mm mm mm\n        Mm mm mm, ooh ooh ooh\n        Mm mm mm mm mm mm mm\n        Mm mm mm mm, wah wah wah\n\n        Wah, Mother Nature's son\n        \"\"\",\n        artist=\"The Beatles\",\n        track_title=\"Mother Nature's Son\",\n        url_title=(\n            \"Mother Nature's Son lyrics by The Beatles - original song full\"\n            \" text. Official Mother Nature's Son lyrics, 2025 version\"\n            \" | LyricsMode.com\"\n        ),\n    ),\n    LyricsPage.make(\n        \"https://www.lyricsontop.com/amy-winehouse-songs/jazz-n-blues-lyrics.html\",\n        \"\"\"\n        It's all gone within two days,\n        Follow my father\n        His extravagant ways\n        So, if I got it out I'll spend it all.\n        Heading In parkway, til I hit the wall.\n        I cross my fingers at the cash machine,\n        As I check my balance I kiss the screen,\n        I love it when it says I got the main's\n        To got o Miss Sixty and pick up my jeans.\n        Money ever last long\n        Had to fight what's wrong,\n        Blow it all on bags and shoes,\n        Jazz n' blues.\n        Money ever last long,\n        Had to fight what's wrong,\n        Blow it all on bags and shoes,\n        Jazz n' blues.\n\n        Standing to the â€¦ bar today,\n        Waiting impatient to throw my cash away,\n        For that Russian JD and coke\n        Had the drinks all night, and now I am bold\n        But that's cool, cause I can buy more from you.\n        And I didn't forgot about that 50 Compton,\n        Tell you what? My fancy's coming through\n        I'll take you at shopping, can you wait til next June?\n        Yeah, Money ever last long\n        Had to fight what's wrong,\n        Blow it all on bags and shoes,\n        Jazz n' blues.\n        Money ever last long,\n        Had to fight what's wrong,\n        Blow it all on bags and shoes,\n        Jazz n' blues.\n\n        (Instrumental Break)\n\n        Money ever last long\n        Had to fight what's wrong,\n        Blow it all on bags and shoes,\n        Jazz n' blues.\n        Money ever last long,\n        Had to fight what's wrong,\n        Blow it all on bags and shoes,\n        Jazz n' blues.\n        Money ever last long,\n        Had to fight what's wrong,\n        Blow it all on bags and shoes,\n        Jazz n' blues.\n        \"\"\",\n        artist=\"Amy Winehouse\",\n        track_title=\"Jazz N' Blues\",\n        url_title=\"Amy Winehouse - Jazz N' Blues lyrics complete\",\n    ),\n    LyricsPage.make(\n        \"https://www.musica.com/letras.asp?letra=59862\",\n        \"\"\"\n        Lady Madonna, baby at your breast\n        Wonders how you manage to feed the rest\n\n        See how they run\n\n        Lady Madonna lying on the bed\n        Listen to the music playing in your head\n\n        Tuesday afternoon is never ending\n        Wednesday morning papers didn't come\n        Thursday night your stockings needed mending\n        See how they run\n\n        Lady Madonna, children at your feet\n        Wonder how you manage to make ends meet\n        \"\"\",\n        url_title=\"Lady Madonna - Letra - The Beatles - Musica.com\",\n    ),\n    LyricsPage.make(\n        \"https://www.paroles.net/the-beatles/paroles-lady-madonna\",\n        \"\"\"\n        Lady Madonna, children at your feet.\n        Wonder how you manage to make ends meet.\n        Who finds the money? When you pay the rent?\n        Did you think that money was heaven sent?\n\n        Friday night arrives without a suitcase.\n        Sunday morning creep in like a nun.\n        Monday's child has learned to tie his bootlace.\n        See how they run.\n\n        Lady Madonna, baby at your breast.\n        Wonders how you manage to feed the rest.\n\n        See how they run.\n        Lady Madonna, lying on the bed,\n        Listen to the music playing in your head.\n\n        Tuesday afternoon is never ending.\n        Wednesday morning papers didn't come.\n        Thursday night your stockings needed mending.\n        See how they run.\n\n        Lady Madonna, children at your feet.\n        Wonder how you manage to make ends meet.\n        \"\"\",\n        url_title=\"Paroles Lady Madonna par The Beatles - Lyrics - Paroles.net\",\n    ),\n    LyricsPage.make(\n        \"https://www.songlyrics.com/the-beatles/lady-madonna-lyrics\",\n        \"\"\"\n        Lady Madonna, children at your feet\n        Wonder how you manage to make ends meet\n        Who finds the money? When you pay the rent?\n        Did you think that money was Heaven sent?\n        Friday night arrives without a suitcase\n        Sunday morning creep in like a nun\n        Monday's child has learned to tie his bootlace\n        See how they run\n\n        Lady Madonna, baby at your breast\n        Wonder how you manage to feed the rest\n\n        See how they run\n\n        Lady Madonna, lying on the bed\n        Listen to the music playing in your head\n\n        Tuesday afternoon is never ending\n        Wednesday morning papers didn't come\n        Thursday night you stockings needed mending\n        See how they run\n\n        Lady Madonna, children at your feet\n        Wonder how you manage to make ends meet\n        \"\"\",\n        url_title=\"THE BEATLES - LADY MADONNA LYRICS\",\n        marks=[xfail_on_ci(\"Songlyrics is blocked by Cloudflare\")],\n    ),\n    LyricsPage.make(\n        \"https://sweetslyrics.com/the-beatles/lady-madonna-lyrics\",\n        \"\"\"\n        Lady Madonna, children at your feet.\n        Wonder how you manage to make ends meet.\n        Who finds the money when you pay the rent?\n        Did you think that money was heaven sent?\n\n        Friday night arrives without a suitcase.\n        Sunday morning creeping like a nun.\n        Monday's child has learned to tie his bootlace.\n        See how they run...\n\n        Lady Madonna, baby at your breast.\n        Wonders how you manage to feed the rest.\n\n        (Sax solo)\n\n        See how they run...\n\n        Lady Madonna, lying on the bed.\n        Listen to the music playing in your head.\n\n        Tuesday afternoon is never ending.\n        Wednesday morning papers didn't come.\n        Thursday night your stockings needed mending.\n        See how they run...\n\n        Lady Madonna, children at your feet.\n        Wonder how you manage to make ends meet.\n        \"\"\",\n        url_title=\"The Beatles - Lady Madonna\",\n        marks=[xfail_on_ci(\"Sweetslyrics also fails with 403 FORBIDDEN in CI\")],\n    ),\n    LyricsPage.make(\n        \"https://www.tekstowo.pl/piosenka,the_beatles,lady_madonna.html\",\n        \"\"\"\n        Lady Madonna,\n        Children at your feet\n        Wonder how you manage to make ends meet.\n\n        Who find the money\n        When you pay the rent?\n        Did you think that money was Heaven sent?\n\n        Friday night arrives without a suitcase\n        Sunday morning creeping like a nun\n        Monday's child has learned to tie his bootlace\n\n        See how they run\n\n        Lady Madonna\n        Baby at your breast\n        Wonders how you manage to feed the rest\n\n        See how they run\n\n        Lady Madonna\n        Lying on the bed\n        Listen to the music playing in your head\n\n        Tuesday afternoon is neverending\n        Wednesday morning papers didn't come\n        Thursday night your stockings needed mending\n\n        See how they run\n\n        Lady Madonna,\n        Children at your feet\n        Wonder how you manage to make ends meet\n        \"\"\",\n        marks=[pytest.mark.xfail(reason=\"Tekstowo seems to be broken again\")],\n    ),\n]\n"
  },
  {
    "path": "test/plugins/test_acousticbrainz.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Nathan Dwek.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for the 'acousticbrainz' plugin.\"\"\"\n\nimport json\nimport os.path\nimport unittest\n\nfrom beets.test._common import RSRC\nfrom beetsplug.acousticbrainz import ABSCHEME, AcousticPlugin\n\n\nclass MapDataToSchemeTest(unittest.TestCase):\n    def test_basic(self):\n        ab = AcousticPlugin()\n        data = {\"key 1\": \"value 1\", \"key 2\": \"value 2\"}\n        scheme = {\"key 1\": \"attribute 1\", \"key 2\": \"attribute 2\"}\n        mapping = set(ab._map_data_to_scheme(data, scheme))\n        assert mapping == {\n            (\"attribute 1\", \"value 1\"),\n            (\"attribute 2\", \"value 2\"),\n        }\n\n    def test_recurse(self):\n        ab = AcousticPlugin()\n        data = {\n            \"key\": \"value\",\n            \"group\": {\n                \"subkey\": \"subvalue\",\n                \"subgroup\": {\"subsubkey\": \"subsubvalue\"},\n            },\n        }\n        scheme = {\n            \"key\": \"attribute 1\",\n            \"group\": {\n                \"subkey\": \"attribute 2\",\n                \"subgroup\": {\"subsubkey\": \"attribute 3\"},\n            },\n        }\n        mapping = set(ab._map_data_to_scheme(data, scheme))\n        assert mapping == {\n            (\"attribute 1\", \"value\"),\n            (\"attribute 2\", \"subvalue\"),\n            (\"attribute 3\", \"subsubvalue\"),\n        }\n\n    def test_composite(self):\n        ab = AcousticPlugin()\n        data = {\"key 1\": \"part 1\", \"key 2\": \"part 2\"}\n        scheme = {\"key 1\": (\"attribute\", 0), \"key 2\": (\"attribute\", 1)}\n        mapping = set(ab._map_data_to_scheme(data, scheme))\n        assert mapping == {(\"attribute\", \"part 1 part 2\")}\n\n    def test_realistic(self):\n        ab = AcousticPlugin()\n        data_path = os.path.join(RSRC, b\"acousticbrainz/data.json\")\n        with open(data_path) as res:\n            data = json.load(res)\n        mapping = set(ab._map_data_to_scheme(data, ABSCHEME))\n        expected = {\n            (\"chords_key\", \"A\"),\n            (\"average_loudness\", 0.815025985241),\n            (\"mood_acoustic\", 0.415711194277),\n            (\"chords_changes_rate\", 0.0445116683841),\n            (\"tonal\", 0.874250173569),\n            (\"mood_sad\", 0.299694597721),\n            (\"bpm\", 162.532119751),\n            (\"gender\", \"female\"),\n            (\"initial_key\", \"A minor\"),\n            (\"chords_number_rate\", 0.00194468453992),\n            (\"mood_relaxed\", 0.123632438481),\n            (\"chords_scale\", \"minor\"),\n            (\"voice_instrumental\", \"instrumental\"),\n            (\"key_strength\", 0.636936545372),\n            (\"genre_rosamerica\", \"roc\"),\n            (\"mood_party\", 0.234383180737),\n            (\"mood_aggressive\", 0.0779221653938),\n            (\"danceable\", 0.143928021193),\n            (\"rhythm\", \"VienneseWaltz\"),\n            (\"mood_electronic\", 0.339881360531),\n            (\"mood_happy\", 0.0894767045975),\n            (\"moods_mirex\", \"Cluster3\"),\n            (\"timbre\", \"bright\"),\n        }\n        assert mapping == expected\n"
  },
  {
    "path": "test/plugins/test_advancedrewrite.py",
    "content": "# This file is part of beets.\n# Copyright 2023, Max Rumpf.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Test the advancedrewrite plugin for various configurations.\"\"\"\n\nimport pytest\n\nfrom beets.test.helper import PluginTestCase\nfrom beets.ui import UserError\n\nPLUGIN_NAME = \"advancedrewrite\"\n\n\nclass AdvancedRewritePluginTest(PluginTestCase):\n    plugin = \"advancedrewrite\"\n    preload_plugin = False\n\n    def test_simple_rewrite_example(self):\n        with self.configure_plugin(\n            [{\"artist ODD EYE CIRCLE\": \"이달의 소녀 오드아이써클\"}]\n        ):\n            item = self.add_item(\n                artist=\"ODD EYE CIRCLE\",\n                albumartist=\"ODD EYE CIRCLE\",\n            )\n\n            assert item.artist == \"이달의 소녀 오드아이써클\"\n\n    def test_advanced_rewrite_example(self):\n        with self.configure_plugin(\n            [\n                {\n                    \"match\": \"mb_artistid:dec0f331-cb08-4c8e-9c9f-aeb1f0f6d88c year:..2022\",  # noqa: E501\n                    \"replacements\": {\n                        \"artist\": \"이달의 소녀 오드아이써클\",\n                        \"artist_sort\": \"LOONA / ODD EYE CIRCLE\",\n                    },\n                },\n            ]\n        ):\n            item_a = self.add_item(\n                artist=\"ODD EYE CIRCLE\",\n                artist_sort=\"ODD EYE CIRCLE\",\n                mb_artistid=\"dec0f331-cb08-4c8e-9c9f-aeb1f0f6d88c\",\n                year=2017,\n            )\n            item_b = self.add_item(\n                artist=\"ODD EYE CIRCLE\",\n                artist_sort=\"ODD EYE CIRCLE\",\n                mb_artistid=\"dec0f331-cb08-4c8e-9c9f-aeb1f0f6d88c\",\n                year=2023,\n            )\n\n            # Assert that all replacements were applied to item_a\n            assert \"이달의 소녀 오드아이써클\" == item_a.artist\n            assert \"LOONA / ODD EYE CIRCLE\" == item_a.artist_sort\n            assert \"LOONA / ODD EYE CIRCLE\" == item_a.albumartist_sort\n\n            # Assert that no replacements were applied to item_b\n            assert \"ODD EYE CIRCLE\" == item_b.artist\n\n    def test_advanced_rewrite_example_with_multi_valued_field(self):\n        with self.configure_plugin(\n            [\n                {\n                    \"match\": \"artist:배유빈 feat. 김미현\",\n                    \"replacements\": {\"artists\": [\"유빈\", \"미미\"]},\n                },\n            ]\n        ):\n            item = self.add_item(\n                artist=\"배유빈 feat. 김미현\",\n                artists=[\"배유빈\", \"김미현\"],\n            )\n\n            assert item.artists == [\"유빈\", \"미미\"]\n\n    def test_fail_when_replacements_empty(self):\n        with (\n            pytest.raises(\n                UserError,\n                match=\"Advanced rewrites must have at least one replacement\",\n            ),\n            self.configure_plugin([{\"match\": \"artist:A\", \"replacements\": {}}]),\n        ):\n            pass\n\n    def test_fail_when_rewriting_single_valued_field_with_list(self):\n        with (\n            pytest.raises(\n                UserError,\n                match=\"Field artist is not a multi-valued field but a list was given: C, D\",  # noqa: E501\n            ),\n            self.configure_plugin(\n                [\n                    {\n                        \"match\": \"artist:'A & B'\",\n                        \"replacements\": {\"artist\": [\"C\", \"D\"]},\n                    },\n                ]\n            ),\n        ):\n            pass\n\n    def test_combined_rewrite_example(self):\n        with self.configure_plugin(\n            [\n                {\"artist A\": \"B\"},\n                {\"match\": \"album:'C'\", \"replacements\": {\"artist\": \"D\"}},\n            ]\n        ):\n            item = self.add_item(artist=\"A\", albumartist=\"A\")\n            assert item.artist == \"B\"\n\n            item = self.add_item(artist=\"C\", albumartist=\"C\", album=\"C\")\n            assert item.artist == \"D\"\n"
  },
  {
    "path": "test/plugins/test_albumtypes.py",
    "content": "# This file is part of beets.\n# Copyright 2021, Edgars Supe.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for the 'albumtypes' plugin.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING\n\nfrom beets.test.helper import PluginTestCase\nfrom beetsplug.albumtypes import AlbumTypesPlugin\nfrom beetsplug.musicbrainz import VARIOUS_ARTISTS_ID\n\nif TYPE_CHECKING:\n    from collections.abc import Sequence\n\n\nclass AlbumTypesPluginTest(PluginTestCase):\n    \"\"\"Tests for albumtypes plugin.\"\"\"\n\n    plugin = \"albumtypes\"\n\n    def test_renames_types(self):\n        \"\"\"Tests if the plugin correctly renames the specified types.\"\"\"\n        self._set_config(\n            types=[(\"ep\", \"EP\"), (\"remix\", \"Remix\")], ignore_va=[], bracket=\"()\"\n        )\n        album = self._create_album(album_types=[\"ep\", \"remix\"])\n        subject = AlbumTypesPlugin()\n        result = subject._atypes(album)\n        assert \"(EP)(Remix)\" == result\n        return\n\n    def test_returns_only_specified_types(self):\n        \"\"\"Tests if the plugin returns only non-blank types given in config.\"\"\"\n        self._set_config(\n            types=[(\"ep\", \"EP\"), (\"soundtrack\", \"\")], ignore_va=[], bracket=\"()\"\n        )\n        album = self._create_album(album_types=[\"ep\", \"remix\", \"soundtrack\"])\n        subject = AlbumTypesPlugin()\n        result = subject._atypes(album)\n        assert \"(EP)\" == result\n\n    def test_respects_type_order(self):\n        \"\"\"Tests if the types are returned in the same order as config.\"\"\"\n        self._set_config(\n            types=[(\"remix\", \"Remix\"), (\"ep\", \"EP\")], ignore_va=[], bracket=\"()\"\n        )\n        album = self._create_album(album_types=[\"ep\", \"remix\"])\n        subject = AlbumTypesPlugin()\n        result = subject._atypes(album)\n        assert \"(Remix)(EP)\" == result\n        return\n\n    def test_ignores_va(self):\n        \"\"\"Tests if the specified type is ignored for VA albums.\"\"\"\n        self._set_config(\n            types=[(\"ep\", \"EP\"), (\"soundtrack\", \"OST\")],\n            ignore_va=[\"ep\"],\n            bracket=\"()\",\n        )\n        album = self._create_album(\n            album_types=[\"ep\", \"soundtrack\"], artist_id=VARIOUS_ARTISTS_ID\n        )\n        subject = AlbumTypesPlugin()\n        result = subject._atypes(album)\n        assert \"(OST)\" == result\n\n    def test_respects_defaults(self):\n        \"\"\"Tests if the plugin uses the default values if config not given.\"\"\"\n        album = self._create_album(\n            album_types=[\n                \"ep\",\n                \"single\",\n                \"soundtrack\",\n                \"live\",\n                \"compilation\",\n                \"remix\",\n            ],\n            artist_id=VARIOUS_ARTISTS_ID,\n        )\n        subject = AlbumTypesPlugin()\n        result = subject._atypes(album)\n        assert \"[EP][Single][OST][Live][Remix]\" == result\n\n    def _set_config(\n        self,\n        types: Sequence[tuple[str, str]],\n        ignore_va: Sequence[str],\n        bracket: str,\n    ):\n        self.config[\"albumtypes\"][\"types\"] = types\n        self.config[\"albumtypes\"][\"ignore_va\"] = ignore_va\n        self.config[\"albumtypes\"][\"bracket\"] = bracket\n\n    def _create_album(self, album_types: Sequence[str], artist_id: str = \"0\"):\n        return self.add_album(\n            albumtypes=album_types, mb_albumartistid=artist_id\n        )\n"
  },
  {
    "path": "test/plugins/test_art.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for the album art fetchers.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport shutil\nimport unittest\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING\nfrom unittest.mock import patch\n\nimport confuse\nimport pytest\nimport responses\n\nfrom beets import config, importer, logging, util\nfrom beets.autotag import AlbumInfo, AlbumMatch\nfrom beets.test import _common\nfrom beets.test.helper import (\n    BeetsTestCase,\n    CleanupModulesMixin,\n    FetchImageHelper,\n    capture_log,\n)\nfrom beets.util import syspath\nfrom beets.util.artresizer import ArtResizer\nfrom beetsplug import fetchart\n\nlogger = logging.getLogger(\"beets.test_art\")\n\nif TYPE_CHECKING:\n    from collections.abc import Iterator, Sequence\n\n    from beets.library import Album\n\n\nclass Settings:\n    \"\"\"Used to pass settings to the ArtSources when the plugin isn't fully\n    instantiated.\n    \"\"\"\n\n    def __init__(self, **kwargs):\n        for k, v in kwargs.items():\n            setattr(self, k, v)\n\n\nclass DummyRemoteArtSource(fetchart.RemoteArtSource):\n    NAME = \"Dummy Art Source\"\n    ID = \"dummy\"\n\n    def get(\n        self,\n        album: Album,\n        plugin: fetchart.FetchArtPlugin,\n        paths: None | Sequence[bytes],\n    ) -> Iterator[fetchart.Candidate]:\n        return iter(())\n\n\nclass UseThePlugin(CleanupModulesMixin, BeetsTestCase):\n    modules = (fetchart.__name__, ArtResizer.__module__)\n\n    def setUp(self):\n        super().setUp()\n        self.plugin = fetchart.FetchArtPlugin()\n\n\nclass FetchImageTestCase(FetchImageHelper, UseThePlugin):\n    pass\n\n\nclass CAAHelper:\n    \"\"\"Helper mixin for mocking requests to the Cover Art Archive.\"\"\"\n\n    MBID_RELASE = \"rid\"\n    MBID_GROUP = \"rgid\"\n\n    RELEASE_URL = f\"coverartarchive.org/release/{MBID_RELASE}\"\n    GROUP_URL = f\"coverartarchive.org/release-group/{MBID_GROUP}\"\n\n    RELEASE_URL = f\"https://{RELEASE_URL}\"\n    GROUP_URL = f\"https://{GROUP_URL}\"\n\n    RESPONSE_RELEASE = \"\"\"{\n    \"images\": [\n      {\n        \"approved\": false,\n        \"back\": false,\n        \"comment\": \"GIF\",\n        \"edit\": 12345,\n        \"front\": true,\n        \"id\": 12345,\n        \"image\": \"http://coverartarchive.org/release/rid/12345.gif\",\n        \"thumbnails\": {\n          \"1200\": \"http://coverartarchive.org/release/rid/12345-1200.jpg\",\n          \"250\": \"http://coverartarchive.org/release/rid/12345-250.jpg\",\n          \"500\": \"http://coverartarchive.org/release/rid/12345-500.jpg\",\n          \"large\": \"http://coverartarchive.org/release/rid/12345-500.jpg\",\n          \"small\": \"http://coverartarchive.org/release/rid/12345-250.jpg\"\n        },\n        \"types\": [\n          \"Front\"\n        ]\n      },\n      {\n        \"approved\": false,\n        \"back\": false,\n        \"comment\": \"\",\n        \"edit\": 12345,\n        \"front\": false,\n        \"id\": 12345,\n        \"image\": \"http://coverartarchive.org/release/rid/12345.jpg\",\n        \"thumbnails\": {\n          \"1200\": \"http://coverartarchive.org/release/rid/12345-1200.jpg\",\n          \"250\": \"http://coverartarchive.org/release/rid/12345-250.jpg\",\n          \"500\": \"http://coverartarchive.org/release/rid/12345-500.jpg\",\n          \"large\": \"http://coverartarchive.org/release/rid/12345-500.jpg\",\n          \"small\": \"http://coverartarchive.org/release/rid/12345-250.jpg\"\n        },\n        \"types\": [\n          \"Front\"\n        ]\n      }\n    ],\n    \"release\": \"https://musicbrainz.org/release/releaseid\"\n}\"\"\"\n    RESPONSE_RELEASE_WITHOUT_THUMBNAILS = \"\"\"{\n    \"images\": [\n      {\n        \"approved\": false,\n        \"back\": false,\n        \"comment\": \"GIF\",\n        \"edit\": 12345,\n        \"front\": true,\n        \"id\": 12345,\n        \"image\": \"http://coverartarchive.org/release/rid/12345.gif\",\n        \"types\": [\n          \"Front\"\n        ]\n      },\n      {\n        \"approved\": false,\n        \"back\": false,\n        \"comment\": \"\",\n        \"edit\": 12345,\n        \"front\": false,\n        \"id\": 12345,\n        \"image\": \"http://coverartarchive.org/release/rid/12345.jpg\",\n        \"thumbnails\": {\n            \"large\": \"http://coverartarchive.org/release/rgid/12345-500.jpg\",\n            \"small\": \"http://coverartarchive.org/release/rgid/12345-250.jpg\"\n        },\n        \"types\": [\n          \"Front\"\n        ]\n      }\n    ],\n    \"release\": \"https://musicbrainz.org/release/releaseid\"\n}\"\"\"\n    RESPONSE_GROUP = \"\"\"{\n        \"images\": [\n          {\n            \"approved\": false,\n            \"back\": false,\n            \"comment\": \"\",\n            \"edit\": 12345,\n            \"front\": true,\n            \"id\": 12345,\n            \"image\": \"http://coverartarchive.org/release/releaseid/12345.jpg\",\n            \"thumbnails\": {\n              \"1200\": \"http://coverartarchive.org/release/rgid/12345-1200.jpg\",\n              \"250\": \"http://coverartarchive.org/release/rgid/12345-250.jpg\",\n              \"500\": \"http://coverartarchive.org/release/rgid/12345-500.jpg\",\n              \"large\": \"http://coverartarchive.org/release/rgid/12345-500.jpg\",\n              \"small\": \"http://coverartarchive.org/release/rgid/12345-250.jpg\"\n            },\n            \"types\": [\n              \"Front\"\n            ]\n          }\n        ],\n        \"release\": \"https://musicbrainz.org/release/release-id\"\n    }\"\"\"\n    RESPONSE_GROUP_WITHOUT_THUMBNAILS = \"\"\"{\n        \"images\": [\n          {\n            \"approved\": false,\n            \"back\": false,\n            \"comment\": \"\",\n            \"edit\": 12345,\n            \"front\": true,\n            \"id\": 12345,\n            \"image\": \"http://coverartarchive.org/release/releaseid/12345.jpg\",\n            \"types\": [\n              \"Front\"\n            ]\n          }\n        ],\n        \"release\": \"https://musicbrainz.org/release/release-id\"\n    }\"\"\"\n\n    def mock_caa_response(self, url, json):\n        responses.add(\n            responses.GET, url, body=json, content_type=\"application/json\"\n        )\n\n\nclass FetchImageTest(FetchImageTestCase):\n    URL = \"http://example.com/test.jpg\"\n\n    def setUp(self):\n        super().setUp()\n        self.dpath = os.path.join(self.temp_dir, b\"arttest\")\n        self.source = DummyRemoteArtSource(logger, self.plugin.config)\n        self.settings = Settings(maxwidth=0)\n        self.candidate = fetchart.Candidate(\n            logger, self.source.ID, url=self.URL\n        )\n\n    def test_invalid_type_returns_none(self):\n        self.mock_response(self.URL, \"image/watercolour\")\n        self.source.fetch_image(self.candidate, self.settings)\n        assert self.candidate.path is None\n\n    def test_jpeg_type_returns_path(self):\n        self.mock_response(self.URL, \"image/jpeg\")\n        self.source.fetch_image(self.candidate, self.settings)\n        assert self.candidate.path is not None\n\n    def test_extension_set_by_content_type(self):\n        self.mock_response(self.URL, \"image/png\")\n        self.source.fetch_image(self.candidate, self.settings)\n        assert os.path.splitext(self.candidate.path)[1] == b\".png\"\n        assert Path(os.fsdecode(self.candidate.path)).exists()\n\n    def test_does_not_rely_on_server_content_type(self):\n        self.mock_response(self.URL, \"image/jpeg\", \"image/png\")\n        self.source.fetch_image(self.candidate, self.settings)\n        assert os.path.splitext(self.candidate.path)[1] == b\".png\"\n        assert Path(os.fsdecode(self.candidate.path)).exists()\n\n\nclass FSArtTest(UseThePlugin):\n    def setUp(self):\n        super().setUp()\n        self.dpath = os.path.join(self.temp_dir, b\"arttest\")\n        os.mkdir(syspath(self.dpath))\n\n        self.source = fetchart.FileSystem(logger, self.plugin.config)\n        self.settings = Settings(\n            cautious=False, cover_names=(\"art\",), fallback=None\n        )\n\n    def test_finds_jpg_in_directory(self):\n        _common.touch(os.path.join(self.dpath, b\"a.jpg\"))\n        candidate = next(self.source.get(None, self.settings, [self.dpath]))\n        assert candidate.path == os.path.join(self.dpath, b\"a.jpg\")\n\n    def test_appropriately_named_file_takes_precedence(self):\n        _common.touch(os.path.join(self.dpath, b\"a.jpg\"))\n        _common.touch(os.path.join(self.dpath, b\"art.jpg\"))\n        candidate = next(self.source.get(None, self.settings, [self.dpath]))\n        assert candidate.path == os.path.join(self.dpath, b\"art.jpg\")\n\n    def test_non_image_file_not_identified(self):\n        _common.touch(os.path.join(self.dpath, b\"a.txt\"))\n        with pytest.raises(StopIteration):\n            next(self.source.get(None, self.settings, [self.dpath]))\n\n    def test_cautious_skips_fallback(self):\n        _common.touch(os.path.join(self.dpath, b\"a.jpg\"))\n        self.settings.cautious = True\n        with pytest.raises(StopIteration):\n            next(self.source.get(None, self.settings, [self.dpath]))\n\n    def test_configured_fallback_is_used(self):\n        fallback = os.path.join(self.temp_dir, b\"a.jpg\")\n        _common.touch(fallback)\n        self.settings.fallback = fallback\n        candidate = next(self.source.get(None, self.settings, [self.dpath]))\n        assert candidate.path == fallback\n\n    def test_empty_dir(self):\n        with pytest.raises(StopIteration):\n            next(self.source.get(None, self.settings, [self.dpath]))\n\n    def test_precedence_amongst_correct_files(self):\n        images = [b\"front-cover.jpg\", b\"front.jpg\", b\"back.jpg\"]\n        paths = [os.path.join(self.dpath, i) for i in images]\n        for p in paths:\n            _common.touch(p)\n        self.settings.cover_names = [\"cover\", \"front\", \"back\"]\n        candidates = [\n            candidate.path\n            for candidate in self.source.get(None, self.settings, [self.dpath])\n        ]\n        assert candidates == paths\n\n    @patch(\"os.path.samefile\")\n    def test_is_candidate_fallback_os_error(self, mock_samefile):\n        mock_samefile.side_effect = OSError(\"os error\")\n        fallback = os.path.join(self.temp_dir, b\"a.jpg\")\n        self.plugin.fallback = fallback\n        candidate = fetchart.Candidate(logger, self.source.ID, fallback)\n        result = self.plugin._is_candidate_fallback(candidate)\n        mock_samefile.assert_called_once()\n        assert not result\n\n\nclass CombinedTest(FetchImageTestCase, CAAHelper):\n    ASIN = \"xxxx\"\n    MBID = \"releaseid\"\n    AMAZON_URL = f\"https://images.amazon.com/images/P/{ASIN}.01.LZZZZZZZ.jpg\"\n    AAO_URL = f\"https://www.albumart.org/index_detail.php?asin={ASIN}\"\n\n    def setUp(self):\n        super().setUp()\n        self.dpath = os.path.join(self.temp_dir, b\"arttest\")\n        os.mkdir(syspath(self.dpath))\n\n    def test_main_interface_returns_amazon_art(self):\n        self.mock_response(self.AMAZON_URL)\n        album = _common.Bag(asin=self.ASIN)\n        candidate = self.plugin.art_for_album(album, None)\n        assert candidate is not None\n\n    def test_main_interface_returns_none_for_missing_asin_and_path(self):\n        album = _common.Bag()\n        candidate = self.plugin.art_for_album(album, None)\n        assert candidate is None\n\n    def test_main_interface_gives_precedence_to_fs_art(self):\n        _common.touch(os.path.join(self.dpath, b\"art.jpg\"))\n        self.mock_response(self.AMAZON_URL)\n        album = _common.Bag(asin=self.ASIN)\n        candidate = self.plugin.art_for_album(album, [self.dpath])\n        assert candidate is not None\n        assert candidate.path == os.path.join(self.dpath, b\"art.jpg\")\n\n    def test_main_interface_falls_back_to_amazon(self):\n        self.mock_response(self.AMAZON_URL)\n        album = _common.Bag(asin=self.ASIN)\n        candidate = self.plugin.art_for_album(album, [self.dpath])\n        assert candidate is not None\n        assert not candidate.path.startswith(self.dpath)\n\n    def test_main_interface_tries_amazon_before_aao(self):\n        self.mock_response(self.AMAZON_URL)\n        album = _common.Bag(asin=self.ASIN)\n        self.plugin.art_for_album(album, [self.dpath])\n        assert len(responses.calls) == 1\n        assert responses.calls[0].request.url == self.AMAZON_URL\n\n    def test_main_interface_falls_back_to_aao(self):\n        self.mock_response(self.AMAZON_URL, content_type=\"text/html\")\n        album = _common.Bag(asin=self.ASIN)\n        self.plugin.art_for_album(album, [self.dpath])\n        assert responses.calls[-1].request.url == self.AAO_URL\n\n    def test_main_interface_uses_caa_when_mbid_available(self):\n        self.mock_caa_response(self.RELEASE_URL, self.RESPONSE_RELEASE)\n        self.mock_caa_response(self.GROUP_URL, self.RESPONSE_GROUP)\n        self.mock_response(\n            \"http://coverartarchive.org/release/rid/12345.gif\",\n            content_type=\"image/gif\",\n        )\n        self.mock_response(\n            \"http://coverartarchive.org/release/rid/12345.jpg\",\n            content_type=\"image/jpeg\",\n        )\n        album = _common.Bag(\n            mb_albumid=self.MBID_RELASE,\n            mb_releasegroupid=self.MBID_GROUP,\n            asin=self.ASIN,\n        )\n        candidate = self.plugin.art_for_album(album, None)\n        assert candidate is not None\n        assert len(responses.calls) == 3\n        assert responses.calls[0].request.url == self.RELEASE_URL\n\n    def test_local_only_does_not_access_network(self):\n        album = _common.Bag(mb_albumid=self.MBID, asin=self.ASIN)\n        self.plugin.art_for_album(album, None, local_only=True)\n        assert len(responses.calls) == 0\n\n    def test_local_only_gets_fs_image(self):\n        _common.touch(os.path.join(self.dpath, b\"art.jpg\"))\n        album = _common.Bag(mb_albumid=self.MBID, asin=self.ASIN)\n        candidate = self.plugin.art_for_album(\n            album, [self.dpath], local_only=True\n        )\n        assert candidate is not None\n        assert candidate.path == os.path.join(self.dpath, b\"art.jpg\")\n        assert len(responses.calls) == 0\n\n\nclass AAOTest(UseThePlugin):\n    ASIN = \"xxxx\"\n    AAO_URL = f\"https://www.albumart.org/index_detail.php?asin={ASIN}\"\n\n    def setUp(self):\n        super().setUp()\n        self.source = fetchart.AlbumArtOrg(logger, self.plugin.config)\n        self.settings = Settings()\n\n    @responses.activate\n    def run(self, *args, **kwargs):\n        super().run(*args, **kwargs)\n\n    def mock_response(self, url, body):\n        responses.add(responses.GET, url, body=body, content_type=\"text/html\")\n\n    def test_aao_scraper_finds_image(self):\n        body = \"\"\"\n        <br />\n        <a href=\\\"TARGET_URL\\\" title=\\\"View larger image\\\"\n           class=\\\"thickbox\\\" style=\\\"color: #7E9DA2; text-decoration:none;\\\">\n        <img src=\\\"http://www.albumart.org/images/zoom-icon.jpg\\\"\n       alt=\\\"View larger image\\\" width=\\\"17\\\" height=\\\"15\\\"  border=\\\"0\\\"/></a>\n        \"\"\"\n        self.mock_response(self.AAO_URL, body)\n        album = _common.Bag(asin=self.ASIN)\n        candidate = next(self.source.get(album, self.settings, []))\n        assert candidate.url == \"TARGET_URL\"\n\n    def test_aao_scraper_returns_no_result_when_no_image_present(self):\n        self.mock_response(self.AAO_URL, \"blah blah\")\n        album = _common.Bag(asin=self.ASIN)\n        with pytest.raises(StopIteration):\n            next(self.source.get(album, self.settings, []))\n\n\nclass ITunesStoreTest(UseThePlugin):\n    def setUp(self):\n        super().setUp()\n        self.source = fetchart.ITunesStore(logger, self.plugin.config)\n        self.settings = Settings()\n        self.album = _common.Bag(albumartist=\"some artist\", album=\"some album\")\n\n    @responses.activate\n    def run(self, *args, **kwargs):\n        super().run(*args, **kwargs)\n\n    def mock_response(self, url, json):\n        responses.add(\n            responses.GET, url, body=json, content_type=\"application/json\"\n        )\n\n    def test_itunesstore_finds_image(self):\n        json = \"\"\"{\n                    \"results\":\n                        [\n                            {\n                                \"artistName\": \"some artist\",\n                                \"collectionName\": \"some album\",\n                                \"artworkUrl100\": \"url_to_the_image\"\n                            }\n                        ]\n                  }\"\"\"\n        self.mock_response(fetchart.ITunesStore.API_URL, json)\n        candidate = next(self.source.get(self.album, self.settings, []))\n        assert candidate.url == \"url_to_the_image\"\n        assert candidate.match == fetchart.MetadataMatch.EXACT\n\n    def test_itunesstore_no_result(self):\n        json = '{\"results\": []}'\n        self.mock_response(fetchart.ITunesStore.API_URL, json)\n        expected = \"got no results\"\n\n        with capture_log(\"beets.test_art\") as logs:\n            with pytest.raises(StopIteration):\n                next(self.source.get(self.album, self.settings, []))\n        assert expected in logs[1]\n\n    def test_itunesstore_requestexception(self):\n        responses.add(\n            responses.GET,\n            fetchart.ITunesStore.API_URL,\n            json={\"error\": \"not found\"},\n            status=404,\n        )\n        expected = \"iTunes search failed: 404 Client Error\"\n\n        with capture_log(\"beets.test_art\") as logs:\n            with pytest.raises(StopIteration):\n                next(self.source.get(self.album, self.settings, []))\n        assert expected in logs[1]\n\n    def test_itunesstore_fallback_match(self):\n        json = \"\"\"{\n                    \"results\":\n                        [\n                            {\n                                \"collectionName\": \"some album\",\n                                \"artworkUrl100\": \"url_to_the_image\"\n                            }\n                        ]\n                  }\"\"\"\n        self.mock_response(fetchart.ITunesStore.API_URL, json)\n        candidate = next(self.source.get(self.album, self.settings, []))\n        assert candidate.url == \"url_to_the_image\"\n        assert candidate.match == fetchart.MetadataMatch.FALLBACK\n\n    def test_itunesstore_returns_result_without_artwork(self):\n        json = \"\"\"{\n                    \"results\":\n                        [\n                            {\n                                \"artistName\": \"some artist\",\n                                \"collectionName\": \"some album\"\n                            }\n                        ]\n                  }\"\"\"\n        self.mock_response(fetchart.ITunesStore.API_URL, json)\n        expected = \"Malformed itunes candidate\"\n\n        with capture_log(\"beets.test_art\") as logs:\n            with pytest.raises(StopIteration):\n                next(self.source.get(self.album, self.settings, []))\n        assert expected in logs[1]\n\n    def test_itunesstore_returns_no_result_when_error_received(self):\n        json = '{\"error\": {\"errors\": [{\"reason\": \"some reason\"}]}}'\n        self.mock_response(fetchart.ITunesStore.API_URL, json)\n        expected = \"not found in json. Fields are\"\n\n        with capture_log(\"beets.test_art\") as logs:\n            with pytest.raises(StopIteration):\n                next(self.source.get(self.album, self.settings, []))\n        assert expected in logs[1]\n\n    def test_itunesstore_returns_no_result_with_malformed_response(self):\n        json = \"\"\"bla blup\"\"\"\n        self.mock_response(fetchart.ITunesStore.API_URL, json)\n        expected = \"Could not decode json response:\"\n\n        with capture_log(\"beets.test_art\") as logs:\n            with pytest.raises(StopIteration):\n                next(self.source.get(self.album, self.settings, []))\n        assert expected in logs[1]\n\n\nclass GoogleImageTest(UseThePlugin):\n    def setUp(self):\n        super().setUp()\n        self.source = fetchart.GoogleImages(logger, self.plugin.config)\n        self.settings = Settings()\n\n    @responses.activate\n    def run(self, *args, **kwargs):\n        super().run(*args, **kwargs)\n\n    def mock_response(self, url, json):\n        responses.add(\n            responses.GET, url, body=json, content_type=\"application/json\"\n        )\n\n    def test_google_art_finds_image(self):\n        album = _common.Bag(albumartist=\"some artist\", album=\"some album\")\n        json = '{\"items\": [{\"link\": \"url_to_the_image\"}]}'\n        self.mock_response(fetchart.GoogleImages.URL, json)\n        candidate = next(self.source.get(album, self.settings, []))\n        assert candidate.url == \"url_to_the_image\"\n\n    def test_google_art_returns_no_result_when_error_received(self):\n        album = _common.Bag(albumartist=\"some artist\", album=\"some album\")\n        json = '{\"error\": {\"errors\": [{\"reason\": \"some reason\"}]}}'\n        self.mock_response(fetchart.GoogleImages.URL, json)\n        with pytest.raises(StopIteration):\n            next(self.source.get(album, self.settings, []))\n\n    def test_google_art_returns_no_result_with_malformed_response(self):\n        album = _common.Bag(albumartist=\"some artist\", album=\"some album\")\n        json = \"\"\"bla blup\"\"\"\n        self.mock_response(fetchart.GoogleImages.URL, json)\n        with pytest.raises(StopIteration):\n            next(self.source.get(album, self.settings, []))\n\n\nclass CoverArtArchiveTest(UseThePlugin, CAAHelper):\n    def setUp(self):\n        super().setUp()\n        self.source = fetchart.CoverArtArchive(logger, self.plugin.config)\n        self.settings = Settings(maxwidth=0)\n\n    @responses.activate\n    def run(self, *args, **kwargs):\n        super().run(*args, **kwargs)\n\n    def test_caa_finds_image(self):\n        album = _common.Bag(\n            mb_albumid=self.MBID_RELASE, mb_releasegroupid=self.MBID_GROUP\n        )\n        self.mock_caa_response(self.RELEASE_URL, self.RESPONSE_RELEASE)\n        self.mock_caa_response(self.GROUP_URL, self.RESPONSE_GROUP)\n        candidates = list(self.source.get(album, self.settings, []))\n        assert len(candidates) == 3\n        assert len(responses.calls) == 2\n        assert responses.calls[0].request.url == self.RELEASE_URL\n\n    def test_fetchart_uses_caa_pre_sized_maxwidth_thumbs(self):\n        # CAA provides pre-sized thumbnails of width 250px, 500px, and 1200px\n        # We only test with one of them here\n        maxwidth = 1200\n        self.settings = Settings(maxwidth=maxwidth)\n\n        album = _common.Bag(\n            mb_albumid=self.MBID_RELASE, mb_releasegroupid=self.MBID_GROUP\n        )\n        self.mock_caa_response(self.RELEASE_URL, self.RESPONSE_RELEASE)\n        self.mock_caa_response(self.GROUP_URL, self.RESPONSE_GROUP)\n        candidates = list(self.source.get(album, self.settings, []))\n        assert len(candidates) == 3\n        for candidate in candidates:\n            assert f\"-{maxwidth}.jpg\" in candidate.url\n\n    def test_caa_finds_image_if_maxwidth_is_set_and_thumbnails_is_empty(self):\n        # CAA provides pre-sized thumbnails of width 250px, 500px, and 1200px\n        # We only test with one of them here\n        maxwidth = 1200\n        self.settings = Settings(maxwidth=maxwidth)\n\n        album = _common.Bag(\n            mb_albumid=self.MBID_RELASE, mb_releasegroupid=self.MBID_GROUP\n        )\n        self.mock_caa_response(\n            self.RELEASE_URL, self.RESPONSE_RELEASE_WITHOUT_THUMBNAILS\n        )\n        self.mock_caa_response(\n            self.GROUP_URL,\n            self.RESPONSE_GROUP_WITHOUT_THUMBNAILS,\n        )\n        candidates = list(self.source.get(album, self.settings, []))\n        assert len(candidates) == 3\n        for candidate in candidates:\n            assert f\"-{maxwidth}.jpg\" not in candidate.url\n\n\nclass FanartTVTest(UseThePlugin):\n    RESPONSE_MULTIPLE = \"\"\"{\n        \"name\": \"artistname\",\n        \"mbid_id\": \"artistid\",\n        \"albums\": {\n            \"thereleasegroupid\": {\n                \"albumcover\": [\n                    {\n                        \"id\": \"24\",\n                        \"url\": \"http://example.com/1.jpg\",\n                        \"likes\": \"0\"\n                    },\n                    {\n                        \"id\": \"42\",\n                        \"url\": \"http://example.com/2.jpg\",\n                        \"likes\": \"0\"\n                    },\n                    {\n                        \"id\": \"23\",\n                        \"url\": \"http://example.com/3.jpg\",\n                        \"likes\": \"0\"\n                    }\n                ],\n                \"cdart\": [\n                    {\n                        \"id\": \"123\",\n                        \"url\": \"http://example.com/4.jpg\",\n                        \"likes\": \"0\",\n                        \"disc\": \"1\",\n                        \"size\": \"1000\"\n                    }\n                ]\n            }\n        }\n    }\"\"\"\n    RESPONSE_NO_ART = \"\"\"{\n        \"name\": \"artistname\",\n        \"mbid_id\": \"artistid\",\n        \"albums\": {\n            \"thereleasegroupid\": {\n               \"cdart\": [\n                    {\n                        \"id\": \"123\",\n                        \"url\": \"http://example.com/4.jpg\",\n                        \"likes\": \"0\",\n                        \"disc\": \"1\",\n                        \"size\": \"1000\"\n                    }\n                ]\n            }\n        }\n    }\"\"\"\n    RESPONSE_ERROR = \"\"\"{\n        \"status\": \"error\",\n        \"error message\": \"the error message\"\n    }\"\"\"\n    RESPONSE_MALFORMED = \"bla blup\"\n\n    def setUp(self):\n        super().setUp()\n        self.source = fetchart.FanartTV(logger, self.plugin.config)\n        self.settings = Settings()\n\n    @responses.activate\n    def run(self, *args, **kwargs):\n        super().run(*args, **kwargs)\n\n    def mock_response(self, url, json):\n        responses.add(\n            responses.GET, url, body=json, content_type=\"application/json\"\n        )\n\n    def test_fanarttv_finds_image(self):\n        album = _common.Bag(mb_releasegroupid=\"thereleasegroupid\")\n        self.mock_response(\n            f\"{fetchart.FanartTV.API_ALBUMS}thereleasegroupid\",\n            self.RESPONSE_MULTIPLE,\n        )\n        candidate = next(self.source.get(album, self.settings, []))\n        assert candidate.url == \"http://example.com/1.jpg\"\n\n    def test_fanarttv_returns_no_result_when_error_received(self):\n        album = _common.Bag(mb_releasegroupid=\"thereleasegroupid\")\n        self.mock_response(\n            f\"{fetchart.FanartTV.API_ALBUMS}thereleasegroupid\",\n            self.RESPONSE_ERROR,\n        )\n        with pytest.raises(StopIteration):\n            next(self.source.get(album, self.settings, []))\n\n    def test_fanarttv_returns_no_result_with_malformed_response(self):\n        album = _common.Bag(mb_releasegroupid=\"thereleasegroupid\")\n        self.mock_response(\n            f\"{fetchart.FanartTV.API_ALBUMS}thereleasegroupid\",\n            self.RESPONSE_MALFORMED,\n        )\n        with pytest.raises(StopIteration):\n            next(self.source.get(album, self.settings, []))\n\n    def test_fanarttv_only_other_images(self):\n        # The source used to fail when there were images present, but no cover\n        album = _common.Bag(mb_releasegroupid=\"thereleasegroupid\")\n        self.mock_response(\n            f\"{fetchart.FanartTV.API_ALBUMS}thereleasegroupid\",\n            self.RESPONSE_NO_ART,\n        )\n        with pytest.raises(StopIteration):\n            next(self.source.get(album, self.settings, []))\n\n\n@_common.slow_test()\nclass ArtImporterTest(UseThePlugin):\n    def setUp(self):\n        super().setUp()\n\n        # Mock the album art fetcher to always return our test file.\n        self.art_file = self.temp_dir_path / \"tmpcover.jpg\"\n        self.art_file.touch()\n        self.old_afa = self.plugin.art_for_album\n        self.afa_response = fetchart.Candidate(\n            logger,\n            source_name=\"test\",\n            path=self.art_file,\n        )\n\n        def art_for_album(i, p, local_only=False):\n            return self.afa_response\n\n        self.plugin.art_for_album = art_for_album\n\n        # Test library.\n        os.mkdir(syspath(os.path.join(self.libdir, b\"album\")))\n        itempath = os.path.join(self.libdir, b\"album\", b\"test.mp3\")\n        shutil.copyfile(\n            syspath(os.path.join(_common.RSRC, b\"full.mp3\")),\n            syspath(itempath),\n        )\n        self.i = _common.item()\n        self.i.path = itempath\n        self.album = self.lib.add_album([self.i])\n        self.lib._connection().commit()\n\n        # The import configuration.\n        self.session = _common.import_session(self.lib)\n\n        # Import task for the coroutine.\n        self.task = importer.ImportTask(None, None, [self.i])\n        self.task.is_album = True\n        self.task.album = self.album\n        info = AlbumInfo(\n            album=\"some album\",\n            album_id=\"albumid\",\n            artist=\"some artist\",\n            artist_id=\"artistid\",\n            tracks=[],\n        )\n        self.task.set_choice(AlbumMatch(0, info, {}, set(), set()))\n\n    def tearDown(self):\n        super().tearDown()\n        self.plugin.art_for_album = self.old_afa\n\n    def _fetch_art(self, should_exist):\n        \"\"\"Execute the fetch_art coroutine for the task and return the\n        album's resulting artpath. ``should_exist`` specifies whether to\n        assert that art path was set (to the correct value) or or that\n        the path was not set.\n        \"\"\"\n        # Execute the two relevant parts of the importer.\n        self.plugin.fetch_art(self.session, self.task)\n        self.plugin.assign_art(self.session, self.task)\n\n        artpath = self.lib.albums()[0].art_filepath\n        if should_exist:\n            assert artpath == self.i.filepath.parent / \"cover.jpg\"\n            assert artpath.exists()\n        else:\n            assert artpath is None\n        return artpath\n\n    def test_fetch_art(self):\n        assert not self.lib.albums()[0].artpath\n        self._fetch_art(True)\n\n    def test_art_not_found(self):\n        self.afa_response = None\n        self._fetch_art(False)\n\n    def test_no_art_for_singleton(self):\n        self.task.is_album = False\n        self._fetch_art(False)\n\n    def test_leave_original_file_in_place(self):\n        self._fetch_art(True)\n        assert self.art_file.exists()\n\n    def test_delete_original_file(self):\n        prev_move = config[\"import\"][\"move\"].get()\n        try:\n            config[\"import\"][\"move\"] = True\n            self._fetch_art(True)\n            assert not self.art_file.exists()\n        finally:\n            config[\"import\"][\"move\"] = prev_move\n\n    def test_do_not_delete_original_if_already_in_place(self):\n        artdest = os.path.join(os.path.dirname(self.i.path), b\"cover.jpg\")\n        shutil.copyfile(self.art_file, syspath(artdest))\n        self.afa_response = fetchart.Candidate(\n            logger,\n            source_name=\"test\",\n            path=artdest,\n        )\n        self._fetch_art(True)\n\n    def test_fetch_art_if_imported_file_deleted(self):\n        # See #1126. Test the following scenario:\n        #   - Album art imported, `album.artpath` set.\n        #   - Imported album art file subsequently deleted (by user or other\n        #     program).\n        # `fetchart` should import album art again instead of printing the\n        # message \"<album> has album art\".\n        self._fetch_art(True)\n        util.remove(self.album.artpath)\n        self.plugin.batch_fetch_art(\n            self.lib, self.lib.albums(), force=False, quiet=False\n        )\n        assert self.album.art_filepath.exists()\n\n\nclass AlbumArtOperationTestCase(UseThePlugin):\n    \"\"\"Base test case for album art operations.\n\n    Provides common setup for testing album art processing operations by setting\n    up a mock filesystem source that returns a predefined test image.\n    \"\"\"\n\n    IMAGE_PATH = os.path.join(_common.RSRC, b\"abbey-similar.jpg\")\n    IMAGE_FILESIZE = os.stat(util.syspath(IMAGE_PATH)).st_size\n    IMAGE_WIDTH = 500\n    IMAGE_HEIGHT = 490\n    IMAGE_WIDTH_HEIGHT_DIFF = IMAGE_WIDTH - IMAGE_HEIGHT\n\n    @classmethod\n    def setUpClass(cls):\n        super().setUpClass()\n\n        def fs_source_get(_self, album, settings, paths):\n            if paths:\n                yield fetchart.Candidate(\n                    logger, source_name=_self.ID, path=cls.IMAGE_PATH\n                )\n\n        patch(\"beetsplug.fetchart.FileSystem.get\", fs_source_get).start()\n        cls.addClassCleanup(patch.stopall)\n\n    def get_album_art(self):\n        return self.plugin.art_for_album(_common.Bag(), [\"\"], True)\n\n\nclass AlbumArtOperationConfigurationTest(AlbumArtOperationTestCase):\n    \"\"\"Check that scale & filesize configuration is respected.\n\n    Depending on `minwidth`, `enforce_ratio`, `margin_px`, and `margin_percent`\n    configuration the plugin should or should not return an art candidate.\n    \"\"\"\n\n    def test_minwidth(self):\n        self.plugin.minwidth = self.IMAGE_WIDTH / 2\n        assert self.get_album_art()\n\n        self.plugin.minwidth = self.IMAGE_WIDTH * 2\n        assert not self.get_album_art()\n\n    def test_enforce_ratio(self):\n        self.plugin.enforce_ratio = True\n        assert not self.get_album_art()\n\n        self.plugin.enforce_ratio = False\n        assert self.get_album_art()\n\n    def test_enforce_ratio_with_px_margin(self):\n        self.plugin.enforce_ratio = True\n\n        self.plugin.margin_px = self.IMAGE_WIDTH_HEIGHT_DIFF * 0.5\n        assert not self.get_album_art()\n\n        self.plugin.margin_px = self.IMAGE_WIDTH_HEIGHT_DIFF * 1.5\n        assert self.get_album_art()\n\n    def test_enforce_ratio_with_percent_margin(self):\n        self.plugin.enforce_ratio = True\n        diff_by_width = self.IMAGE_WIDTH_HEIGHT_DIFF / self.IMAGE_WIDTH\n\n        self.plugin.margin_percent = diff_by_width * 0.5\n        assert not self.get_album_art()\n\n        self.plugin.margin_percent = diff_by_width * 1.5\n        assert self.get_album_art()\n\n\nclass AlbumArtPerformOperationTest(AlbumArtOperationTestCase):\n    \"\"\"Test that the art is resized and deinterlaced if necessary.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.resizer_mock = patch.object(\n            ArtResizer.shared, \"resize\", return_value=self.IMAGE_PATH\n        ).start()\n        self.deinterlacer_mock = patch.object(\n            ArtResizer.shared, \"deinterlace\", return_value=self.IMAGE_PATH\n        ).start()\n\n    def test_resize(self):\n        self.plugin.maxwidth = self.IMAGE_WIDTH / 2\n        assert self.get_album_art()\n        assert self.resizer_mock.called\n\n    def test_file_resized(self):\n        self.plugin.max_filesize = self.IMAGE_FILESIZE // 2\n        assert self.get_album_art()\n        assert self.resizer_mock.called\n\n    def test_file_not_resized(self):\n        self.plugin.max_filesize = self.IMAGE_FILESIZE\n        assert self.get_album_art()\n        assert not self.resizer_mock.called\n\n    def test_file_resized_but_not_scaled(self):\n        self.plugin.maxwidth = self.IMAGE_WIDTH * 2\n        self.plugin.max_filesize = self.IMAGE_FILESIZE // 2\n        assert self.get_album_art()\n        assert self.resizer_mock.called\n\n    def test_file_resized_and_scaled(self):\n        self.plugin.maxwidth = self.IMAGE_WIDTH / 2\n        self.plugin.max_filesize = self.IMAGE_FILESIZE // 2\n        assert self.get_album_art()\n        assert self.resizer_mock.called\n\n    def test_deinterlaced(self):\n        self.plugin.deinterlace = True\n        assert self.get_album_art()\n        assert self.deinterlacer_mock.called\n\n    def test_not_deinterlaced(self):\n        self.plugin.deinterlace = False\n        assert self.get_album_art()\n        assert not self.deinterlacer_mock.called\n\n    def test_deinterlaced_and_resized(self):\n        self.plugin.maxwidth = self.IMAGE_WIDTH / 2\n        self.plugin.deinterlace = True\n        assert self.get_album_art()\n        assert self.deinterlacer_mock.called\n        assert self.resizer_mock.called\n\n\nclass DeprecatedConfigTest(unittest.TestCase):\n    \"\"\"While refactoring the plugin, the remote_priority option was deprecated,\n    and a new codepath should translate its effect. Check that it actually does\n    so.\n    \"\"\"\n\n    # If we subclassed UseThePlugin, the configuration change would either be\n    # overwritten by BeetsTestCase or be set after constructing the\n    # plugin object\n    def setUp(self):\n        super().setUp()\n        config[\"fetchart\"][\"remote_priority\"] = True\n        self.plugin = fetchart.FetchArtPlugin()\n\n    def test_moves_filesystem_to_end(self):\n        assert isinstance(self.plugin.sources[-1], fetchart.FileSystem)\n\n\nclass EnforceRatioConfigTest(unittest.TestCase):\n    \"\"\"Throw some data at the regexes.\"\"\"\n\n    def _load_with_config(self, values, should_raise):\n        if should_raise:\n            for v in values:\n                config[\"fetchart\"][\"enforce_ratio\"] = v\n                with pytest.raises(confuse.ConfigValueError):\n                    fetchart.FetchArtPlugin()\n        else:\n            for v in values:\n                config[\"fetchart\"][\"enforce_ratio\"] = v\n                fetchart.FetchArtPlugin()\n\n    def test_px(self):\n        self._load_with_config(\"0px 4px 12px 123px\".split(), False)\n        self._load_with_config(\"00px stuff5px\".split(), True)\n\n    def test_percent(self):\n        self._load_with_config(\"0% 0.00% 5.1% 5% 100%\".split(), False)\n        self._load_with_config(\"00% 1.234% foo5% 100.1%\".split(), True)\n"
  },
  {
    "path": "test/plugins/test_aura.py",
    "content": "from __future__ import annotations\n\nimport os\nfrom http import HTTPStatus\nfrom pathlib import Path\nfrom typing import TYPE_CHECKING, Any\n\nimport pytest\n\nfrom beets.test.helper import TestHelper\n\nif TYPE_CHECKING:\n    from flask.testing import Client\n\n\n@pytest.fixture(scope=\"session\", autouse=True)\ndef helper():\n    helper = TestHelper()\n    helper.setup_beets()\n    yield helper\n    helper.teardown_beets()\n\n\n@pytest.fixture(scope=\"session\")\ndef app(helper):\n    from beetsplug.aura import create_app\n\n    app = create_app()\n    app.config[\"lib\"] = helper.lib\n    return app\n\n\n@pytest.fixture(scope=\"session\")\ndef item(helper):\n    return helper.add_item_fixture(\n        album=\"Album\",\n        title=\"Title\",\n        artist=\"Artist\",\n        albumartist=\"Album Artist\",\n    )\n\n\n@pytest.fixture(scope=\"session\")\ndef album(helper, item):\n    return helper.lib.add_album([item])\n\n\n@pytest.fixture(scope=\"session\", autouse=True)\ndef _other_album_and_item(helper):\n    \"\"\"Add another item and album to prove that filtering works.\"\"\"\n    item = helper.add_item_fixture(\n        album=\"Other Album\",\n        title=\"Other Title\",\n        artist=\"Other Artist\",\n        albumartist=\"Other Album Artist\",\n    )\n    helper.lib.add_album([item])\n\n\nclass TestAuraResponse:\n    @pytest.fixture\n    def get_response_data(self, client: Client, item):\n        \"\"\"Return a callback accepting `endpoint` and `params` parameters.\"\"\"\n\n        def get(endpoint: str, params: dict[str, str]) -> dict[str, Any] | None:\n            \"\"\"Add additional `params` and GET the given endpoint.\n\n            `include` parameter is added to every call to check that the\n            functionality that fetches related entities works.\n\n            Before returning the response data, ensure that the request is\n            successful.\n            \"\"\"\n            response = client.get(\n                endpoint,\n                query_string={\"include\": \"tracks,artists,albums\", **params},\n            )\n\n            assert response.status_code == HTTPStatus.OK\n\n            return response.json\n\n        return get\n\n    @pytest.fixture(scope=\"class\")\n    def track_document(self, item, album):\n        return {\n            \"type\": \"track\",\n            \"id\": str(item.id),\n            \"attributes\": {\n                \"album\": item.album,\n                \"albumartist\": item.albumartist,\n                \"artist\": item.artist,\n                \"size\": Path(os.fsdecode(item.path)).stat().st_size,\n                \"title\": item.title,\n                \"track\": 1,\n            },\n            \"relationships\": {\n                \"albums\": {\"data\": [{\"id\": str(album.id), \"type\": \"album\"}]},\n                \"artists\": {\"data\": [{\"id\": item.artist, \"type\": \"artist\"}]},\n            },\n        }\n\n    @pytest.fixture(scope=\"class\")\n    def artist_document(self, item):\n        return {\n            \"type\": \"artist\",\n            \"id\": item.artist,\n            \"attributes\": {\"name\": item.artist},\n            \"relationships\": {\n                \"tracks\": {\"data\": [{\"id\": str(item.id), \"type\": \"track\"}]}\n            },\n        }\n\n    @pytest.fixture(scope=\"class\")\n    def album_document(self, album):\n        return {\n            \"type\": \"album\",\n            \"id\": str(album.id),\n            \"attributes\": {\"artist\": album.albumartist, \"title\": album.album},\n            \"relationships\": {\n                \"tracks\": {\"data\": [{\"id\": str(album.id), \"type\": \"track\"}]}\n            },\n        }\n\n    def test_tracks(\n        self,\n        get_response_data,\n        item,\n        album_document,\n        artist_document,\n        track_document,\n    ):\n        data = get_response_data(\"/aura/tracks\", {\"filter[title]\": item.title})\n\n        assert data == {\n            \"data\": [track_document],\n            \"included\": [artist_document, album_document],\n        }\n\n    def test_artists(\n        self, get_response_data, item, artist_document, track_document\n    ):\n        data = get_response_data(\n            \"/aura/artists\", {\"filter[artist]\": item.artist}\n        )\n\n        assert data == {\"data\": [artist_document], \"included\": [track_document]}\n\n    def test_albums(\n        self, get_response_data, album, album_document, track_document\n    ):\n        data = get_response_data(\"/aura/albums\", {\"filter[album]\": album.album})\n\n        assert data == {\"data\": [album_document], \"included\": [track_document]}\n"
  },
  {
    "path": "test/plugins/test_autobpm.py",
    "content": "import pytest\n\nfrom beets.test.helper import ImportHelper, PluginMixin\n\npytestmark = pytest.mark.requires_import(\"librosa\")\n\n\nclass TestAutoBPMPlugin(PluginMixin, ImportHelper):\n    plugin = \"autobpm\"\n\n    @pytest.fixture(scope=\"class\", name=\"lib\")\n    def fixture_lib(self):\n        self.setup_beets()\n\n        yield self.lib\n\n        self.teardown_beets()\n\n    @pytest.fixture(scope=\"class\")\n    def item(self):\n        return self.add_item_fixture()\n\n    @pytest.fixture(scope=\"class\")\n    def importer(self, lib):\n        self.import_media = []\n        self.prepare_album_for_import(1)\n        track = self.import_media[0]\n        track.bpm = None\n        track.save()\n        return self.setup_importer(autotag=False)\n\n    def test_command(self, lib, item):\n        self.run_command(\"autobpm\", lib=lib)\n\n        item.load()\n        assert item.bpm == 117\n\n    def test_import(self, lib, importer):\n        importer.run()\n\n        assert lib.items().get().bpm == 117\n"
  },
  {
    "path": "test/plugins/test_bareasc.py",
    "content": "# This file is part of beets.\n# Copyright 2021, Graham R. Cobb.\n\n\"\"\"Tests for the 'bareasc' plugin.\"\"\"\n\nfrom beets import logging\nfrom beets.test.helper import IOMixin, PluginTestCase\n\n\nclass BareascPluginTest(IOMixin, PluginTestCase):\n    \"\"\"Test bare ASCII query matching.\"\"\"\n\n    plugin = \"bareasc\"\n\n    def setUp(self):\n        \"\"\"Set up test environment for bare ASCII query matching.\"\"\"\n        super().setUp()\n        self.log = logging.getLogger(\"beets.web\")\n        self.config[\"bareasc\"][\"prefix\"] = \"#\"\n\n        # Add library elements. Note that self.lib.add overrides any \"id=<n>\"\n        # and assigns the next free id number.\n        self.add_item(title=\"with accents\", album_id=2, artist=\"Antonín Dvořák\")\n        self.add_item(title=\"without accents\", artist=\"Antonín Dvorak\")\n        self.add_item(title=\"with umlaut\", album_id=2, artist=\"Brüggen\")\n        self.add_item(title=\"without umlaut or e\", artist=\"Bruggen\")\n        self.add_item(title=\"without umlaut with e\", artist=\"Brueggen\")\n\n    def test_bareasc_search(self):\n        test_cases = [\n            (\n                \"dvorak\",\n                [\"without accents\"],\n            ),  # Normal search, no accents, not using bare-ASCII match.\n            (\n                \"dvořák\",\n                [\"with accents\"],\n            ),  # Normal search, with accents, not using bare-ASCII match.\n            (\n                \"#dvorak\",\n                [\"without accents\", \"with accents\"],\n            ),  # Bare-ASCII search, no accents.\n            (\n                \"#dvořák\",\n                [\"without accents\", \"with accents\"],\n            ),  # Bare-ASCII search, with accents.\n            (\n                \"#dvořäk\",\n                [\"without accents\", \"with accents\"],\n            ),  # Bare-ASCII search, with incorrect accent.\n            (\n                \"#Bruggen\",\n                [\"without umlaut or e\", \"with umlaut\"],\n            ),  # Bare-ASCII search, with no umlaut.\n            (\n                \"#Brüggen\",\n                [\"without umlaut or e\", \"with umlaut\"],\n            ),  # Bare-ASCII search, with umlaut.\n        ]\n\n        for query, expected_titles in test_cases:\n            with self.subTest(query=query, expected_titles=expected_titles):\n                items = self.lib.items(query)\n                assert [item.title for item in items] == expected_titles\n\n    def test_bareasc_list_output(self):\n        \"\"\"Bare-ASCII version of list command - check output.\"\"\"\n        self.run_command(\"bareasc\", \"with accents\")\n\n        assert \"Antonin Dvorak\" in self.io.getoutput()\n\n    def test_bareasc_format_output(self):\n        \"\"\"Bare-ASCII version of list -f command - check output.\"\"\"\n        self.run_command(\"bareasc\", \"with accents\", \"-f\", \"$artist:: $title\")\n\n        assert \"Antonin Dvorak:: with accents\\n\" == self.io.getoutput()\n"
  },
  {
    "path": "test/plugins/test_beatport.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for the 'beatport' plugin.\"\"\"\n\nimport unittest\nfrom datetime import timedelta\n\nfrom beets.test import _common\nfrom beets.test.helper import BeetsTestCase\nfrom beetsplug import beatport\n\n\nclass BeatportTest(BeetsTestCase):\n    def _make_release_response(self):\n        \"\"\"Returns a dict that mimics a response from the beatport API.\n\n        The results were retrieved from:\n        https://oauth-api.beatport.com/catalog/3/releases?id=1742984\n        The list of elements on the returned dict is incomplete, including just\n        those required for the tests on this class.\n        \"\"\"\n        results = {\n            \"id\": 1742984,\n            \"type\": \"release\",\n            \"name\": \"Charade\",\n            \"slug\": \"charade\",\n            \"releaseDate\": \"2016-04-11\",\n            \"publishDate\": \"2016-04-11\",\n            \"audioFormat\": \"\",\n            \"category\": \"Release\",\n            \"currentStatus\": \"General Content\",\n            \"catalogNumber\": \"GR089\",\n            \"description\": \"\",\n            \"label\": {\n                \"id\": 24539,\n                \"name\": \"Gravitas Recordings\",\n                \"type\": \"label\",\n                \"slug\": \"gravitas-recordings\",\n            },\n            \"artists\": [\n                {\n                    \"id\": 326158,\n                    \"name\": \"Supersillyus\",\n                    \"slug\": \"supersillyus\",\n                    \"type\": \"artist\",\n                }\n            ],\n            \"genres\": [\n                {\"id\": 9, \"name\": \"Breaks\", \"slug\": \"breaks\", \"type\": \"genre\"}\n            ],\n        }\n        return results\n\n    def _make_tracks_response(self):\n        \"\"\"Return a list that mimics a response from the beatport API.\n\n        The results were retrieved from:\n        https://oauth-api.beatport.com/catalog/3/tracks?releaseId=1742984\n        The list of elements on the returned list is incomplete, including just\n        those required for the tests on this class.\n        \"\"\"\n        results = [\n            {\n                \"id\": 7817567,\n                \"type\": \"track\",\n                \"sku\": \"track-7817567\",\n                \"name\": \"Mirage a Trois\",\n                \"trackNumber\": 1,\n                \"mixName\": \"Original Mix\",\n                \"title\": \"Mirage a Trois (Original Mix)\",\n                \"slug\": \"mirage-a-trois-original-mix\",\n                \"releaseDate\": \"2016-04-11\",\n                \"publishDate\": \"2016-04-11\",\n                \"currentStatus\": \"General Content\",\n                \"length\": \"7:05\",\n                \"lengthMs\": 425421,\n                \"bpm\": 90,\n                \"key\": {\n                    \"standard\": {\n                        \"letter\": \"G\",\n                        \"sharp\": False,\n                        \"flat\": False,\n                        \"chord\": \"minor\",\n                    },\n                    \"shortName\": \"Gmin\",\n                },\n                \"artists\": [\n                    {\n                        \"id\": 326158,\n                        \"name\": \"Supersillyus\",\n                        \"slug\": \"supersillyus\",\n                        \"type\": \"artist\",\n                    }\n                ],\n                \"genres\": [\n                    {\n                        \"id\": 9,\n                        \"name\": \"Breaks\",\n                        \"slug\": \"breaks\",\n                        \"type\": \"genre\",\n                    }\n                ],\n                \"subGenres\": [\n                    {\n                        \"id\": 209,\n                        \"name\": \"Glitch Hop\",\n                        \"slug\": \"glitch-hop\",\n                        \"type\": \"subgenre\",\n                    }\n                ],\n                \"release\": {\n                    \"id\": 1742984,\n                    \"name\": \"Charade\",\n                    \"type\": \"release\",\n                    \"slug\": \"charade\",\n                },\n                \"label\": {\n                    \"id\": 24539,\n                    \"name\": \"Gravitas Recordings\",\n                    \"type\": \"label\",\n                    \"slug\": \"gravitas-recordings\",\n                    \"status\": True,\n                },\n            },\n            {\n                \"id\": 7817568,\n                \"type\": \"track\",\n                \"sku\": \"track-7817568\",\n                \"name\": \"Aeon Bahamut\",\n                \"trackNumber\": 2,\n                \"mixName\": \"Original Mix\",\n                \"title\": \"Aeon Bahamut (Original Mix)\",\n                \"slug\": \"aeon-bahamut-original-mix\",\n                \"releaseDate\": \"2016-04-11\",\n                \"publishDate\": \"2016-04-11\",\n                \"currentStatus\": \"General Content\",\n                \"length\": \"7:38\",\n                \"lengthMs\": 458000,\n                \"bpm\": 100,\n                \"key\": {\n                    \"standard\": {\n                        \"letter\": \"G\",\n                        \"sharp\": False,\n                        \"flat\": False,\n                        \"chord\": \"major\",\n                    },\n                    \"shortName\": \"Gmaj\",\n                },\n                \"artists\": [\n                    {\n                        \"id\": 326158,\n                        \"name\": \"Supersillyus\",\n                        \"slug\": \"supersillyus\",\n                        \"type\": \"artist\",\n                    }\n                ],\n                \"genres\": [\n                    {\n                        \"id\": 9,\n                        \"name\": \"Breaks\",\n                        \"slug\": \"breaks\",\n                        \"type\": \"genre\",\n                    }\n                ],\n                \"subGenres\": [\n                    {\n                        \"id\": 209,\n                        \"name\": \"Glitch Hop\",\n                        \"slug\": \"glitch-hop\",\n                        \"type\": \"subgenre\",\n                    }\n                ],\n                \"release\": {\n                    \"id\": 1742984,\n                    \"name\": \"Charade\",\n                    \"type\": \"release\",\n                    \"slug\": \"charade\",\n                },\n                \"label\": {\n                    \"id\": 24539,\n                    \"name\": \"Gravitas Recordings\",\n                    \"type\": \"label\",\n                    \"slug\": \"gravitas-recordings\",\n                    \"status\": True,\n                },\n            },\n            {\n                \"id\": 7817569,\n                \"type\": \"track\",\n                \"sku\": \"track-7817569\",\n                \"name\": \"Trancendental Medication\",\n                \"trackNumber\": 3,\n                \"mixName\": \"Original Mix\",\n                \"title\": \"Trancendental Medication (Original Mix)\",\n                \"slug\": \"trancendental-medication-original-mix\",\n                \"releaseDate\": \"2016-04-11\",\n                \"publishDate\": \"2016-04-11\",\n                \"currentStatus\": \"General Content\",\n                \"length\": \"1:08\",\n                \"lengthMs\": 68571,\n                \"bpm\": 141,\n                \"key\": {\n                    \"standard\": {\n                        \"letter\": \"F\",\n                        \"sharp\": False,\n                        \"flat\": False,\n                        \"chord\": \"major\",\n                    },\n                    \"shortName\": \"Fmaj\",\n                },\n                \"artists\": [\n                    {\n                        \"id\": 326158,\n                        \"name\": \"Supersillyus\",\n                        \"slug\": \"supersillyus\",\n                        \"type\": \"artist\",\n                    }\n                ],\n                \"genres\": [\n                    {\n                        \"id\": 9,\n                        \"name\": \"Breaks\",\n                        \"slug\": \"breaks\",\n                        \"type\": \"genre\",\n                    }\n                ],\n                \"subGenres\": [\n                    {\n                        \"id\": 209,\n                        \"name\": \"Glitch Hop\",\n                        \"slug\": \"glitch-hop\",\n                        \"type\": \"subgenre\",\n                    }\n                ],\n                \"release\": {\n                    \"id\": 1742984,\n                    \"name\": \"Charade\",\n                    \"type\": \"release\",\n                    \"slug\": \"charade\",\n                },\n                \"label\": {\n                    \"id\": 24539,\n                    \"name\": \"Gravitas Recordings\",\n                    \"type\": \"label\",\n                    \"slug\": \"gravitas-recordings\",\n                    \"status\": True,\n                },\n            },\n            {\n                \"id\": 7817570,\n                \"type\": \"track\",\n                \"sku\": \"track-7817570\",\n                \"name\": \"A List of Instructions for When I'm Human\",\n                \"trackNumber\": 4,\n                \"mixName\": \"Original Mix\",\n                \"title\": \"A List of Instructions for When I'm Human (Original Mix)\",\n                \"slug\": \"a-list-of-instructions-for-when-im-human-original-mix\",\n                \"releaseDate\": \"2016-04-11\",\n                \"publishDate\": \"2016-04-11\",\n                \"currentStatus\": \"General Content\",\n                \"length\": \"6:57\",\n                \"lengthMs\": 417913,\n                \"bpm\": 88,\n                \"key\": {\n                    \"standard\": {\n                        \"letter\": \"A\",\n                        \"sharp\": False,\n                        \"flat\": False,\n                        \"chord\": \"minor\",\n                    },\n                    \"shortName\": \"Amin\",\n                },\n                \"artists\": [\n                    {\n                        \"id\": 326158,\n                        \"name\": \"Supersillyus\",\n                        \"slug\": \"supersillyus\",\n                        \"type\": \"artist\",\n                    }\n                ],\n                \"genres\": [\n                    {\n                        \"id\": 9,\n                        \"name\": \"Breaks\",\n                        \"slug\": \"breaks\",\n                        \"type\": \"genre\",\n                    }\n                ],\n                \"subGenres\": [\n                    {\n                        \"id\": 209,\n                        \"name\": \"Glitch Hop\",\n                        \"slug\": \"glitch-hop\",\n                        \"type\": \"subgenre\",\n                    }\n                ],\n                \"release\": {\n                    \"id\": 1742984,\n                    \"name\": \"Charade\",\n                    \"type\": \"release\",\n                    \"slug\": \"charade\",\n                },\n                \"label\": {\n                    \"id\": 24539,\n                    \"name\": \"Gravitas Recordings\",\n                    \"type\": \"label\",\n                    \"slug\": \"gravitas-recordings\",\n                    \"status\": True,\n                },\n            },\n            {\n                \"id\": 7817571,\n                \"type\": \"track\",\n                \"sku\": \"track-7817571\",\n                \"name\": \"The Great Shenanigan\",\n                \"trackNumber\": 5,\n                \"mixName\": \"Original Mix\",\n                \"title\": \"The Great Shenanigan (Original Mix)\",\n                \"slug\": \"the-great-shenanigan-original-mix\",\n                \"releaseDate\": \"2016-04-11\",\n                \"publishDate\": \"2016-04-11\",\n                \"currentStatus\": \"General Content\",\n                \"length\": \"9:49\",\n                \"lengthMs\": 589875,\n                \"bpm\": 123,\n                \"key\": {\n                    \"standard\": {\n                        \"letter\": \"E\",\n                        \"sharp\": False,\n                        \"flat\": True,\n                        \"chord\": \"major\",\n                    },\n                    \"shortName\": \"E&#9837;maj\",\n                },\n                \"artists\": [\n                    {\n                        \"id\": 326158,\n                        \"name\": \"Supersillyus\",\n                        \"slug\": \"supersillyus\",\n                        \"type\": \"artist\",\n                    }\n                ],\n                \"genres\": [\n                    {\n                        \"id\": 9,\n                        \"name\": \"Breaks\",\n                        \"slug\": \"breaks\",\n                        \"type\": \"genre\",\n                    }\n                ],\n                \"subGenres\": [\n                    {\n                        \"id\": 209,\n                        \"name\": \"Glitch Hop\",\n                        \"slug\": \"glitch-hop\",\n                        \"type\": \"subgenre\",\n                    }\n                ],\n                \"release\": {\n                    \"id\": 1742984,\n                    \"name\": \"Charade\",\n                    \"type\": \"release\",\n                    \"slug\": \"charade\",\n                },\n                \"label\": {\n                    \"id\": 24539,\n                    \"name\": \"Gravitas Recordings\",\n                    \"type\": \"label\",\n                    \"slug\": \"gravitas-recordings\",\n                    \"status\": True,\n                },\n            },\n            {\n                \"id\": 7817572,\n                \"type\": \"track\",\n                \"sku\": \"track-7817572\",\n                \"name\": \"Charade\",\n                \"trackNumber\": 6,\n                \"mixName\": \"Original Mix\",\n                \"title\": \"Charade (Original Mix)\",\n                \"slug\": \"charade-original-mix\",\n                \"releaseDate\": \"2016-04-11\",\n                \"publishDate\": \"2016-04-11\",\n                \"currentStatus\": \"General Content\",\n                \"length\": \"7:05\",\n                \"lengthMs\": 425423,\n                \"bpm\": 123,\n                \"key\": {\n                    \"standard\": {\n                        \"letter\": \"A\",\n                        \"sharp\": False,\n                        \"flat\": False,\n                        \"chord\": \"major\",\n                    },\n                    \"shortName\": \"Amaj\",\n                },\n                \"artists\": [\n                    {\n                        \"id\": 326158,\n                        \"name\": \"Supersillyus\",\n                        \"slug\": \"supersillyus\",\n                        \"type\": \"artist\",\n                    }\n                ],\n                \"genres\": [\n                    {\n                        \"id\": 9,\n                        \"name\": \"Breaks\",\n                        \"slug\": \"breaks\",\n                        \"type\": \"genre\",\n                    }\n                ],\n                \"subGenres\": [\n                    {\n                        \"id\": 209,\n                        \"name\": \"Glitch Hop\",\n                        \"slug\": \"glitch-hop\",\n                        \"type\": \"subgenre\",\n                    }\n                ],\n                \"release\": {\n                    \"id\": 1742984,\n                    \"name\": \"Charade\",\n                    \"type\": \"release\",\n                    \"slug\": \"charade\",\n                },\n                \"label\": {\n                    \"id\": 24539,\n                    \"name\": \"Gravitas Recordings\",\n                    \"type\": \"label\",\n                    \"slug\": \"gravitas-recordings\",\n                    \"status\": True,\n                },\n            },\n        ]\n        return results\n\n    def setUp(self):\n        super().setUp()\n\n        # Set up 'album'.\n        response_release = self._make_release_response()\n        self.album = beatport.BeatportRelease(response_release)\n\n        # Set up 'tracks'.\n        response_tracks = self._make_tracks_response()\n        self.tracks = [beatport.BeatportTrack(t) for t in response_tracks]\n\n        # Set up 'test_album'.\n        self.test_album = self.mk_test_album()\n\n        # Set up 'test_tracks'\n        self.test_tracks = self.test_album.items()\n\n    def mk_test_album(self):\n        items = [_common.item() for _ in range(6)]\n        for item in items:\n            item.album = \"Charade\"\n            item.catalognum = \"GR089\"\n            item.label = \"Gravitas Recordings\"\n            item.artist = \"Supersillyus\"\n            item.year = 2016\n            item.comp = False\n            item.label_name = \"Gravitas Recordings\"\n            item.genres = [\"Glitch Hop\", \"Breaks\"]\n            item.year = 2016\n            item.month = 4\n            item.day = 11\n            item.mix_name = \"Original Mix\"\n\n        items[0].title = \"Mirage a Trois\"\n        items[1].title = \"Aeon Bahamut\"\n        items[2].title = \"Trancendental Medication\"\n        items[3].title = \"A List of Instructions for When I'm Human\"\n        items[4].title = \"The Great Shenanigan\"\n        items[5].title = \"Charade\"\n\n        items[0].length = timedelta(minutes=7, seconds=5).total_seconds()\n        items[1].length = timedelta(minutes=7, seconds=38).total_seconds()\n        items[2].length = timedelta(minutes=1, seconds=8).total_seconds()\n        items[3].length = timedelta(minutes=6, seconds=57).total_seconds()\n        items[4].length = timedelta(minutes=9, seconds=49).total_seconds()\n        items[5].length = timedelta(minutes=7, seconds=5).total_seconds()\n\n        items[0].url = \"mirage-a-trois-original-mix\"\n        items[1].url = \"aeon-bahamut-original-mix\"\n        items[2].url = \"trancendental-medication-original-mix\"\n        items[3].url = \"a-list-of-instructions-for-when-im-human-original-mix\"\n        items[4].url = \"the-great-shenanigan-original-mix\"\n        items[5].url = \"charade-original-mix\"\n\n        counter = 0\n        for item in items:\n            counter += 1\n            item.track_number = counter\n\n        items[0].bpm = 90\n        items[1].bpm = 100\n        items[2].bpm = 141\n        items[3].bpm = 88\n        items[4].bpm = 123\n        items[5].bpm = 123\n\n        items[0].initial_key = \"Gmin\"\n        items[1].initial_key = \"Gmaj\"\n        items[2].initial_key = \"Fmaj\"\n        items[3].initial_key = \"Amin\"\n        items[4].initial_key = \"E&#9837;maj\"\n        items[5].initial_key = \"Amaj\"\n\n        for item in items:\n            self.lib.add(item)\n\n        album = self.lib.add_album(items)\n        album.store()\n\n        return album\n\n    # Test BeatportRelease.\n    def test_album_name_applied(self):\n        assert self.album.name == self.test_album[\"album\"]\n\n    def test_catalog_number_applied(self):\n        assert self.album.catalog_number == self.test_album[\"catalognum\"]\n\n    def test_label_applied(self):\n        assert self.album.label_name == self.test_album[\"label\"]\n\n    def test_category_applied(self):\n        assert self.album.category == \"Release\"\n\n    def test_album_url_applied(self):\n        assert self.album.url == \"https://beatport.com/release/charade/1742984\"\n\n    # Test BeatportTrack.\n    def test_title_applied(self):\n        for track, test_track in zip(self.tracks, self.test_tracks):\n            assert track.name == test_track.title\n\n    def test_mix_name_applied(self):\n        for track, test_track in zip(self.tracks, self.test_tracks):\n            assert track.mix_name == test_track.mix_name\n\n    def test_length_applied(self):\n        for track, test_track in zip(self.tracks, self.test_tracks):\n            assert int(track.length.total_seconds()) == int(test_track.length)\n\n    def test_track_url_applied(self):\n        # Specify beatport ids here because an 'item.id' is beets-internal.\n        ids = [\n            7817567,\n            7817568,\n            7817569,\n            7817570,\n            7817571,\n            7817572,\n        ]\n        # Concatenate with 'id' to pass strict equality test.\n        for track, test_track, id in zip(self.tracks, self.test_tracks, ids):\n            assert (\n                track.url == f\"https://beatport.com/track/{test_track.url}/{id}\"\n            )\n\n    def test_bpm_applied(self):\n        for track, test_track in zip(self.tracks, self.test_tracks):\n            assert track.bpm == test_track.bpm\n\n    def test_initial_key_applied(self):\n        for track, test_track in zip(self.tracks, self.test_tracks):\n            assert track.initial_key == test_track.initial_key\n\n    def test_genre_applied(self):\n        for track, test_track in zip(self.tracks, self.test_tracks):\n            assert track.genres == test_track.genres\n\n\nclass BeatportResponseEmptyTest(unittest.TestCase):\n    def _make_tracks_response(self):\n        results = [\n            {\n                \"id\": 7817567,\n                \"name\": \"Mirage a Trois\",\n                \"genres\": [\n                    {\n                        \"id\": 9,\n                        \"name\": \"Breaks\",\n                        \"slug\": \"breaks\",\n                        \"type\": \"genre\",\n                    }\n                ],\n                \"subGenres\": [\n                    {\n                        \"id\": 209,\n                        \"name\": \"Glitch Hop\",\n                        \"slug\": \"glitch-hop\",\n                        \"type\": \"subgenre\",\n                    }\n                ],\n            }\n        ]\n        return results\n\n    def setUp(self):\n        super().setUp()\n\n        # Set up 'tracks'.\n        self.response_tracks = self._make_tracks_response()\n        self.tracks = [beatport.BeatportTrack(t) for t in self.response_tracks]\n\n        # Make alias to be congruent with class `BeatportTest`.\n        self.test_tracks = self.response_tracks\n\n    def test_response_tracks_empty(self):\n        response_tracks = []\n        tracks = [beatport.BeatportTrack(t) for t in response_tracks]\n        assert tracks == []\n\n    def test_sub_genre_empty_fallback(self):\n        \"\"\"No 'sub_genre' is provided. Test if fallback to 'genre' works.\"\"\"\n        self.response_tracks[0][\"subGenres\"] = []\n        tracks = [beatport.BeatportTrack(t) for t in self.response_tracks]\n\n        self.test_tracks[0][\"subGenres\"] = []\n\n        assert tracks[0].genres == [self.test_tracks[0][\"genres\"][0][\"name\"]]\n\n    def test_genre_empty(self):\n        \"\"\"No 'genre' is provided. Test if 'sub_genre' is applied.\"\"\"\n        self.response_tracks[0][\"genres\"] = []\n        tracks = [beatport.BeatportTrack(t) for t in self.response_tracks]\n\n        self.test_tracks[0][\"genres\"] = []\n\n        assert tracks[0].genres == [self.test_tracks[0][\"subGenres\"][0][\"name\"]]\n"
  },
  {
    "path": "test/plugins/test_bpd.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for BPD's implementation of the MPD protocol.\"\"\"\n\nimport multiprocessing as mp\nimport os\nimport socket\nimport tempfile\nimport threading\nimport time\nimport unittest\nfrom contextlib import contextmanager\nfrom typing import ClassVar\nfrom unittest.mock import MagicMock, patch\n\nimport confuse\nimport pytest\nimport yaml\n\nfrom beets.test.helper import PluginTestCase\nfrom beets.util import bluelet\n\nbpd = pytest.importorskip(\"beetsplug.bpd\", exc_type=ImportError)\n\n\nclass CommandParseTest(unittest.TestCase):\n    def test_no_args(self):\n        s = r\"command\"\n        c = bpd.Command(s)\n        assert c.name == \"command\"\n        assert c.args == []\n\n    def test_one_unquoted_arg(self):\n        s = r\"command hello\"\n        c = bpd.Command(s)\n        assert c.name == \"command\"\n        assert c.args == [\"hello\"]\n\n    def test_two_unquoted_args(self):\n        s = r\"command hello there\"\n        c = bpd.Command(s)\n        assert c.name == \"command\"\n        assert c.args == [\"hello\", \"there\"]\n\n    def test_one_quoted_arg(self):\n        s = r'command \"hello there\"'\n        c = bpd.Command(s)\n        assert c.name == \"command\"\n        assert c.args == [\"hello there\"]\n\n    def test_heterogenous_args(self):\n        s = r'command \"hello there\" sir'\n        c = bpd.Command(s)\n        assert c.name == \"command\"\n        assert c.args == [\"hello there\", \"sir\"]\n\n    def test_quote_in_arg(self):\n        s = r'command \"hello \\\" there\"'\n        c = bpd.Command(s)\n        assert c.args == ['hello \" there']\n\n    def test_backslash_in_arg(self):\n        s = r'command \"hello \\\\ there\"'\n        c = bpd.Command(s)\n        assert c.args == [\"hello \\\\ there\"]\n\n\nclass MPCResponse:\n    def __init__(self, raw_response):\n        body = b\"\\n\".join(raw_response.split(b\"\\n\")[:-2]).decode(\"utf-8\")\n        self.data = self._parse_body(body)\n        status = raw_response.split(b\"\\n\")[-2].decode(\"utf-8\")\n        self.ok, self.err_data = self._parse_status(status)\n\n    def _parse_status(self, status):\n        \"\"\"Parses the first response line, which contains the status.\"\"\"\n        if status.startswith(\"OK\") or status.startswith(\"list_OK\"):\n            return True, None\n        elif status.startswith(\"ACK\"):\n            code, rest = status[5:].split(\"@\", 1)\n            pos, rest = rest.split(\"]\", 1)\n            cmd, rest = rest[2:].split(\"}\")\n            return False, (int(code), int(pos), cmd, rest[1:])\n        else:\n            raise RuntimeError(f\"Unexpected status: {status!r}\")\n\n    def _parse_body(self, body):\n        \"\"\"Messages are generally in the format \"header: content\".\n        Convert them into a dict, storing the values for repeated headers as\n        lists of strings, and non-repeated ones as string.\n        \"\"\"\n        data = {}\n        repeated_headers = set()\n        for line in body.split(\"\\n\"):\n            if not line:\n                continue\n            if \":\" not in line:\n                raise RuntimeError(f\"Unexpected line: {line!r}\")\n            header, content = line.split(\":\", 1)\n            content = content.lstrip()\n            if header in repeated_headers:\n                data[header].append(content)\n            elif header in data:\n                data[header] = [data[header], content]\n                repeated_headers.add(header)\n            else:\n                data[header] = content\n        return data\n\n\nclass MPCClient:\n    def __init__(self, sock, do_hello=True):\n        self.sock = sock\n        self.buf = b\"\"\n        if do_hello:\n            hello = self.get_response()\n            if not hello.ok:\n                raise RuntimeError(\"Bad hello\")\n\n    def get_response(self, force_multi=None):\n        \"\"\"Wait for a full server response and wrap it in a helper class.\n        If the request was a batch request then this will return a list of\n        `MPCResponse`s, one for each processed subcommand.\n        \"\"\"\n\n        response = b\"\"\n        responses = []\n        while True:\n            line = self.readline()\n            response += line\n            if line.startswith(b\"OK\") or line.startswith(b\"ACK\"):\n                if force_multi or any(responses):\n                    if line.startswith(b\"ACK\"):\n                        responses.append(MPCResponse(response))\n                        n_remaining = force_multi - len(responses)\n                        responses.extend([None] * n_remaining)\n                    return responses\n                else:\n                    return MPCResponse(response)\n            if line.startswith(b\"list_OK\"):\n                responses.append(MPCResponse(response))\n                response = b\"\"\n            elif not line:\n                raise RuntimeError(f\"Unexpected response: {line!r}\")\n\n    def serialise_command(self, command, *args):\n        cmd = [command.encode(\"utf-8\")]\n        for arg in [a.encode(\"utf-8\") for a in args]:\n            if b\" \" in arg:\n                cmd.append(b'\"' + arg + b'\"')\n            else:\n                cmd.append(arg)\n        return b\" \".join(cmd) + b\"\\n\"\n\n    def send_command(self, command, *args):\n        request = self.serialise_command(command, *args)\n        self.sock.sendall(request)\n        return self.get_response()\n\n    def send_commands(self, *commands):\n        \"\"\"Use MPD command batching to send multiple commands at once.\n        Each item of commands is a tuple containing a command followed by\n        any arguments.\n        \"\"\"\n\n        requests = []\n        for command_and_args in commands:\n            command = command_and_args[0]\n            args = command_and_args[1:]\n            requests.append(self.serialise_command(command, *args))\n        requests.insert(0, b\"command_list_ok_begin\\n\")\n        requests.append(b\"command_list_end\\n\")\n        request = b\"\".join(requests)\n        self.sock.sendall(request)\n        return self.get_response(force_multi=len(commands))\n\n    def readline(self, terminator=b\"\\n\", bufsize=1024):\n        \"\"\"Reads a line of data from the socket.\"\"\"\n\n        while True:\n            if terminator in self.buf:\n                line, self.buf = self.buf.split(terminator, 1)\n                line += terminator\n                return line\n            self.sock.settimeout(1)\n            data = self.sock.recv(bufsize)\n            if data:\n                self.buf += data\n            else:\n                line = self.buf\n                self.buf = b\"\"\n                return line\n\n\ndef implements(commands, fail=False):\n    def _test(self):\n        with self.run_bpd() as client:\n            response = client.send_command(\"commands\")\n        self._assert_ok(response)\n        implemented = response.data[\"command\"]\n        assert commands.intersection(implemented) == commands\n\n    return unittest.expectedFailure(_test) if fail else _test\n\n\nbluelet_listener = bluelet.Listener\n\n\n@patch(\"beets.util.bluelet.Listener\")\ndef start_server(args, assigned_port, listener_patch):\n    \"\"\"Start the bpd server, writing the port to `assigned_port`.\"\"\"\n\n    def listener_wrap(host, port):\n        \"\"\"Wrap `bluelet.Listener`, writing the port to `assigend_port`.\"\"\"\n        # `bluelet.Listener` has previously been saved to\n        # `bluelet_listener` as this function will replace it at its\n        # original location.\n        listener = bluelet_listener(host, port)\n        # read port assigned by OS\n        assigned_port.put_nowait(listener.sock.getsockname()[1])\n        return listener\n\n    listener_patch.side_effect = listener_wrap\n\n    import beets.ui\n\n    beets.ui.main(args)\n\n\nclass BPDTestHelper(PluginTestCase):\n    db_on_disk = True\n    plugin = \"bpd\"\n\n    def setUp(self):\n        super().setUp()\n        self.item1 = self.add_item(\n            title=\"Track One Title\",\n            track=1,\n            album=\"Album Title\",\n            artist=\"Artist Name\",\n        )\n        self.item2 = self.add_item(\n            title=\"Track Two Title\",\n            track=2,\n            album=\"Album Title\",\n            artist=\"Artist Name\",\n        )\n        self.lib.add_album([self.item1, self.item2])\n\n    @contextmanager\n    def run_bpd(\n        self,\n        host=\"localhost\",\n        password=None,\n        do_hello=True,\n        second_client=False,\n    ):\n        \"\"\"Runs BPD in another process, configured with the same library\n        database as we created in the setUp method. Exposes a client that is\n        connected to the server, and kills the server at the end.\n        \"\"\"\n        # Create a config file:\n        config = {\n            \"pluginpath\": [str(self.temp_dir_path)],\n            \"plugins\": \"bpd\",\n            # use port 0 to let the OS choose a free port\n            \"bpd\": {\"host\": host, \"port\": 0, \"control_port\": 0},\n        }\n        if password:\n            config[\"bpd\"][\"password\"] = password\n        config_file = tempfile.NamedTemporaryFile(\n            mode=\"wb\",\n            dir=str(self.temp_dir_path),\n            suffix=\".yaml\",\n            delete=False,\n        )\n        config_file.write(\n            yaml.dump(config, Dumper=confuse.Dumper, encoding=\"utf-8\")\n        )\n        config_file.close()\n\n        # Fork and launch BPD in the new process:\n        assigned_port = mp.Queue(2)  # 2 slots, `control_port` and `port`\n        server = mp.Process(\n            target=start_server,\n            args=(\n                [\n                    \"--library\",\n                    self.config[\"library\"].as_filename(),\n                    \"--directory\",\n                    os.fsdecode(self.libdir),\n                    \"--config\",\n                    os.fsdecode(config_file.name),\n                    \"bpd\",\n                ],\n                assigned_port,\n            ),\n        )\n        server.start()\n\n        try:\n            assigned_port.get(timeout=1)  # skip control_port\n            port = assigned_port.get(timeout=0.5)  # read port\n\n            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n            try:\n                sock.connect((host, port))\n\n                if second_client:\n                    sock2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n                    try:\n                        sock2.connect((host, port))\n                        yield (\n                            MPCClient(sock, do_hello),\n                            MPCClient(sock2, do_hello),\n                        )\n                    finally:\n                        sock2.close()\n\n                else:\n                    yield MPCClient(sock, do_hello)\n            finally:\n                sock.close()\n        finally:\n            server.terminate()\n            server.join(timeout=0.2)\n\n    def _assert_ok(self, *responses):\n        for response in responses:\n            assert response is not None\n            assert response.ok, f\"Response failed: {response.err_data}\"\n\n    def _assert_failed(self, response, code, pos=None):\n        \"\"\"Check that a command failed with a specific error code. If this\n        is a list of responses, first check all preceding commands were OK.\n        \"\"\"\n        if pos is not None:\n            previous_commands = response[0:pos]\n            self._assert_ok(*previous_commands)\n            response = response[pos]\n        assert not response.ok\n        if pos is not None:\n            assert pos == response.err_data[1]\n        if code is not None:\n            assert code == response.err_data[0]\n\n    def _bpd_add(self, client, *items, **kwargs):\n        \"\"\"Add the given item to the BPD playlist or queue.\"\"\"\n        paths = [\n            \"/\".join(\n                [\n                    item.artist,\n                    item.album,\n                    os.fsdecode(os.path.basename(item.path)),\n                ]\n            )\n            for item in items\n        ]\n        playlist = kwargs.get(\"playlist\")\n        if playlist:\n            commands = [(\"playlistadd\", playlist, path) for path in paths]\n        else:\n            commands = [(\"add\", path) for path in paths]\n        responses = client.send_commands(*commands)\n        self._assert_ok(*responses)\n\n\nclass BPDTest(BPDTestHelper):\n    def test_server_hello(self):\n        with self.run_bpd(do_hello=False) as client:\n            assert client.readline() == b\"OK MPD 0.16.0\\n\"\n\n    def test_unknown_cmd(self):\n        with self.run_bpd() as client:\n            response = client.send_command(\"notacommand\")\n        self._assert_failed(response, bpd.ERROR_UNKNOWN)\n\n    def test_unexpected_argument(self):\n        with self.run_bpd() as client:\n            response = client.send_command(\"ping\", \"extra argument\")\n        self._assert_failed(response, bpd.ERROR_ARG)\n\n    def test_missing_argument(self):\n        with self.run_bpd() as client:\n            response = client.send_command(\"add\")\n        self._assert_failed(response, bpd.ERROR_ARG)\n\n    def test_system_error(self):\n        with self.run_bpd() as client:\n            response = client.send_command(\"crash\")\n        self._assert_failed(response, bpd.ERROR_SYSTEM)\n\n    def test_empty_request(self):\n        with self.run_bpd() as client:\n            response = client.send_command(\"\")\n        self._assert_failed(response, bpd.ERROR_UNKNOWN)\n\n\nclass BPDQueryTest(BPDTestHelper):\n    test_implements_query = implements(\n        {\n            \"clearerror\",\n        }\n    )\n\n    def test_cmd_currentsong(self):\n        with self.run_bpd() as client:\n            self._bpd_add(client, self.item1)\n            responses = client.send_commands(\n                (\"play\",), (\"currentsong\",), (\"stop\",), (\"currentsong\",)\n            )\n        self._assert_ok(*responses)\n        assert \"1\" == responses[1].data[\"Id\"]\n        assert \"Id\" not in responses[3].data\n\n    def test_cmd_currentsong_tagtypes(self):\n        with self.run_bpd() as client:\n            self._bpd_add(client, self.item1)\n            responses = client.send_commands((\"play\",), (\"currentsong\",))\n        self._assert_ok(*responses)\n        assert BPDConnectionTest.TAGTYPES.union(BPDQueueTest.METADATA) == set(\n            responses[1].data.keys()\n        )\n\n    def test_cmd_status(self):\n        with self.run_bpd() as client:\n            self._bpd_add(client, self.item1, self.item2)\n            responses = client.send_commands(\n                (\"status\",), (\"play\",), (\"status\",)\n            )\n        self._assert_ok(*responses)\n        fields_not_playing = {\n            \"repeat\",\n            \"random\",\n            \"single\",\n            \"consume\",\n            \"playlist\",\n            \"playlistlength\",\n            \"mixrampdb\",\n            \"state\",\n            \"volume\",\n        }\n        assert fields_not_playing == set(responses[0].data.keys())\n        fields_playing = fields_not_playing | {\n            \"song\",\n            \"songid\",\n            \"time\",\n            \"elapsed\",\n            \"bitrate\",\n            \"duration\",\n            \"audio\",\n            \"nextsong\",\n            \"nextsongid\",\n        }\n        assert fields_playing == set(responses[2].data.keys())\n\n    def test_cmd_stats(self):\n        with self.run_bpd() as client:\n            response = client.send_command(\"stats\")\n        self._assert_ok(response)\n        details = {\n            \"artists\",\n            \"albums\",\n            \"songs\",\n            \"uptime\",\n            \"db_playtime\",\n            \"db_update\",\n            \"playtime\",\n        }\n        assert details == set(response.data.keys())\n\n    def test_cmd_idle(self):\n        def _toggle(c):\n            for _ in range(3):\n                rs = c.send_commands((\"play\",), (\"pause\",))\n                # time.sleep(0.05)  # uncomment if test is flaky\n                if any(not r.ok for r in rs):\n                    raise RuntimeError(\"Toggler failed\")\n\n        with self.run_bpd(second_client=True) as (client, client2):\n            self._bpd_add(client, self.item1, self.item2)\n            toggler = threading.Thread(target=_toggle, args=(client2,))\n            toggler.start()\n            # Idling will hang until the toggler thread changes the play state.\n            # Since the client sockets have a 1s timeout set at worst this will\n            # raise a socket.timeout and fail the test if the toggler thread\n            # manages to finish before the idle command is sent here.\n            response = client.send_command(\"idle\", \"player\")\n            toggler.join()\n        self._assert_ok(response)\n\n    def test_cmd_idle_with_pending(self):\n        with self.run_bpd(second_client=True) as (client, client2):\n            response1 = client.send_command(\"random\", \"1\")\n            response2 = client2.send_command(\"idle\")\n        self._assert_ok(response1, response2)\n        assert \"options\" == response2.data[\"changed\"]\n\n    def test_cmd_noidle(self):\n        with self.run_bpd() as client:\n            # Manually send a command without reading a response.\n            request = client.serialise_command(\"idle\")\n            client.sock.sendall(request)\n            time.sleep(0.01)\n            response = client.send_command(\"noidle\")\n        self._assert_ok(response)\n\n    def test_cmd_noidle_when_not_idle(self):\n        with self.run_bpd() as client:\n            # Manually send a command without reading a response.\n            request = client.serialise_command(\"noidle\")\n            client.sock.sendall(request)\n            response = client.send_command(\"notacommand\")\n        self._assert_failed(response, bpd.ERROR_UNKNOWN)\n\n\nclass BPDPlaybackTest(BPDTestHelper):\n    test_implements_playback = implements(\n        {\n            \"random\",\n        }\n    )\n\n    def test_cmd_consume(self):\n        with self.run_bpd() as client:\n            self._bpd_add(client, self.item1, self.item2)\n            responses = client.send_commands(\n                (\"consume\", \"0\"),\n                (\"playlistinfo\",),\n                (\"next\",),\n                (\"playlistinfo\",),\n                (\"consume\", \"1\"),\n                (\"playlistinfo\",),\n                (\"play\", \"0\"),\n                (\"next\",),\n                (\"playlistinfo\",),\n                (\"status\",),\n            )\n        self._assert_ok(*responses)\n        assert responses[1].data[\"Id\"] == responses[3].data[\"Id\"]\n        assert [\"1\", \"2\"] == responses[5].data[\"Id\"]\n        assert \"2\" == responses[8].data[\"Id\"]\n        assert \"1\" == responses[9].data[\"consume\"]\n        assert \"play\" == responses[9].data[\"state\"]\n\n    def test_cmd_consume_in_reverse(self):\n        with self.run_bpd() as client:\n            self._bpd_add(client, self.item1, self.item2)\n            responses = client.send_commands(\n                (\"consume\", \"1\"),\n                (\"play\", \"1\"),\n                (\"playlistinfo\",),\n                (\"previous\",),\n                (\"playlistinfo\",),\n                (\"status\",),\n            )\n        self._assert_ok(*responses)\n        assert [\"1\", \"2\"] == responses[2].data[\"Id\"]\n        assert \"1\" == responses[4].data[\"Id\"]\n        assert \"play\" == responses[5].data[\"state\"]\n\n    def test_cmd_single(self):\n        with self.run_bpd() as client:\n            self._bpd_add(client, self.item1, self.item2)\n            responses = client.send_commands(\n                (\"status\",),\n                (\"single\", \"1\"),\n                (\"play\",),\n                (\"status\",),\n                (\"next\",),\n                (\"status\",),\n            )\n        self._assert_ok(*responses)\n        assert \"0\" == responses[0].data[\"single\"]\n        assert \"1\" == responses[3].data[\"single\"]\n        assert \"play\" == responses[3].data[\"state\"]\n        assert \"stop\" == responses[5].data[\"state\"]\n\n    def test_cmd_repeat(self):\n        with self.run_bpd() as client:\n            self._bpd_add(client, self.item1, self.item2)\n            responses = client.send_commands(\n                (\"repeat\", \"1\"),\n                (\"play\",),\n                (\"currentsong\",),\n                (\"next\",),\n                (\"currentsong\",),\n                (\"next\",),\n                (\"currentsong\",),\n            )\n        self._assert_ok(*responses)\n        assert \"1\" == responses[2].data[\"Id\"]\n        assert \"2\" == responses[4].data[\"Id\"]\n        assert \"1\" == responses[6].data[\"Id\"]\n\n    def test_cmd_repeat_with_single(self):\n        with self.run_bpd() as client:\n            self._bpd_add(client, self.item1, self.item2)\n            responses = client.send_commands(\n                (\"repeat\", \"1\"),\n                (\"single\", \"1\"),\n                (\"play\",),\n                (\"currentsong\",),\n                (\"next\",),\n                (\"status\",),\n                (\"currentsong\",),\n            )\n        self._assert_ok(*responses)\n        assert \"1\" == responses[3].data[\"Id\"]\n        assert \"play\" == responses[5].data[\"state\"]\n        assert \"1\" == responses[6].data[\"Id\"]\n\n    def test_cmd_repeat_in_reverse(self):\n        with self.run_bpd() as client:\n            self._bpd_add(client, self.item1, self.item2)\n            responses = client.send_commands(\n                (\"repeat\", \"1\"),\n                (\"play\",),\n                (\"currentsong\",),\n                (\"previous\",),\n                (\"currentsong\",),\n            )\n        self._assert_ok(*responses)\n        assert \"1\" == responses[2].data[\"Id\"]\n        assert \"2\" == responses[4].data[\"Id\"]\n\n    def test_cmd_repeat_with_single_in_reverse(self):\n        with self.run_bpd() as client:\n            self._bpd_add(client, self.item1, self.item2)\n            responses = client.send_commands(\n                (\"repeat\", \"1\"),\n                (\"single\", \"1\"),\n                (\"play\",),\n                (\"currentsong\",),\n                (\"previous\",),\n                (\"status\",),\n                (\"currentsong\",),\n            )\n        self._assert_ok(*responses)\n        assert \"1\" == responses[3].data[\"Id\"]\n        assert \"play\" == responses[5].data[\"state\"]\n        assert \"1\" == responses[6].data[\"Id\"]\n\n    def test_cmd_crossfade(self):\n        with self.run_bpd() as client:\n            responses = client.send_commands(\n                (\"status\",),\n                (\"crossfade\", \"123\"),\n                (\"status\",),\n                (\"crossfade\", \"-2\"),\n            )\n            response = client.send_command(\"crossfade\", \"0.5\")\n        self._assert_failed(responses, bpd.ERROR_ARG, pos=3)\n        self._assert_failed(response, bpd.ERROR_ARG)\n        assert \"xfade\" not in responses[0].data\n        assert 123 == pytest.approx(int(responses[2].data[\"xfade\"]))\n\n    def test_cmd_mixrampdb(self):\n        with self.run_bpd() as client:\n            responses = client.send_commands((\"mixrampdb\", \"-17\"), (\"status\",))\n        self._assert_ok(*responses)\n        assert -17 == pytest.approx(float(responses[1].data[\"mixrampdb\"]))\n\n    def test_cmd_mixrampdelay(self):\n        with self.run_bpd() as client:\n            responses = client.send_commands(\n                (\"mixrampdelay\", \"2\"),\n                (\"status\",),\n                (\"mixrampdelay\", \"nan\"),\n                (\"status\",),\n                (\"mixrampdelay\", \"-2\"),\n            )\n        self._assert_failed(responses, bpd.ERROR_ARG, pos=4)\n        assert 2 == pytest.approx(float(responses[1].data[\"mixrampdelay\"]))\n        assert \"mixrampdelay\" not in responses[3].data\n\n    def test_cmd_setvol(self):\n        with self.run_bpd() as client:\n            responses = client.send_commands(\n                (\"setvol\", \"67\"),\n                (\"status\",),\n                (\"setvol\", \"32\"),\n                (\"status\",),\n                (\"setvol\", \"101\"),\n            )\n        self._assert_failed(responses, bpd.ERROR_ARG, pos=4)\n        assert \"67\" == responses[1].data[\"volume\"]\n        assert \"32\" == responses[3].data[\"volume\"]\n\n    def test_cmd_volume(self):\n        with self.run_bpd() as client:\n            responses = client.send_commands(\n                (\"setvol\", \"10\"), (\"volume\", \"5\"), (\"volume\", \"-2\"), (\"status\",)\n            )\n        self._assert_ok(*responses)\n        assert \"13\" == responses[3].data[\"volume\"]\n\n    def test_cmd_replay_gain(self):\n        with self.run_bpd() as client:\n            responses = client.send_commands(\n                (\"replay_gain_mode\", \"track\"),\n                (\"replay_gain_status\",),\n                (\"replay_gain_mode\", \"notanoption\"),\n            )\n        self._assert_failed(responses, bpd.ERROR_ARG, pos=2)\n        assert \"track\" == responses[1].data[\"replay_gain_mode\"]\n\n\nclass BPDControlTest(BPDTestHelper):\n    test_implements_control = implements(\n        {\n            \"seek\",\n            \"seekid\",\n            \"seekcur\",\n        },\n        fail=True,\n    )\n\n    def test_cmd_play(self):\n        with self.run_bpd() as client:\n            self._bpd_add(client, self.item1, self.item2)\n            responses = client.send_commands(\n                (\"status\",),\n                (\"play\",),\n                (\"status\",),\n                (\"play\", \"1\"),\n                (\"currentsong\",),\n            )\n        self._assert_ok(*responses)\n        assert \"stop\" == responses[0].data[\"state\"]\n        assert \"play\" == responses[2].data[\"state\"]\n        assert \"2\" == responses[4].data[\"Id\"]\n\n    def test_cmd_playid(self):\n        with self.run_bpd() as client:\n            self._bpd_add(client, self.item1, self.item2)\n            responses = client.send_commands(\n                (\"playid\", \"2\"), (\"currentsong\",), (\"clear\",)\n            )\n            self._bpd_add(client, self.item2, self.item1)\n            responses.extend(\n                client.send_commands((\"playid\", \"2\"), (\"currentsong\",))\n            )\n        self._assert_ok(*responses)\n        assert \"2\" == responses[1].data[\"Id\"]\n        assert \"2\" == responses[4].data[\"Id\"]\n\n    def test_cmd_pause(self):\n        with self.run_bpd() as client:\n            self._bpd_add(client, self.item1)\n            responses = client.send_commands(\n                (\"play\",), (\"pause\",), (\"status\",), (\"currentsong\",)\n            )\n        self._assert_ok(*responses)\n        assert \"pause\" == responses[2].data[\"state\"]\n        assert \"1\" == responses[3].data[\"Id\"]\n\n    def test_cmd_stop(self):\n        with self.run_bpd() as client:\n            self._bpd_add(client, self.item1)\n            responses = client.send_commands(\n                (\"play\",), (\"stop\",), (\"status\",), (\"currentsong\",)\n            )\n        self._assert_ok(*responses)\n        assert \"stop\" == responses[2].data[\"state\"]\n        assert \"Id\" not in responses[3].data\n\n    def test_cmd_next(self):\n        with self.run_bpd() as client:\n            self._bpd_add(client, self.item1, self.item2)\n            responses = client.send_commands(\n                (\"play\",),\n                (\"currentsong\",),\n                (\"next\",),\n                (\"currentsong\",),\n                (\"next\",),\n                (\"status\",),\n            )\n        self._assert_ok(*responses)\n        assert \"1\" == responses[1].data[\"Id\"]\n        assert \"2\" == responses[3].data[\"Id\"]\n        assert \"stop\" == responses[5].data[\"state\"]\n\n    def test_cmd_previous(self):\n        with self.run_bpd() as client:\n            self._bpd_add(client, self.item1, self.item2)\n            responses = client.send_commands(\n                (\"play\", \"1\"),\n                (\"currentsong\",),\n                (\"previous\",),\n                (\"currentsong\",),\n                (\"previous\",),\n                (\"status\",),\n                (\"currentsong\",),\n            )\n        self._assert_ok(*responses)\n        assert \"2\" == responses[1].data[\"Id\"]\n        assert \"1\" == responses[3].data[\"Id\"]\n        assert \"play\" == responses[5].data[\"state\"]\n        assert \"1\" == responses[6].data[\"Id\"]\n\n\nclass BPDQueueTest(BPDTestHelper):\n    test_implements_queue = implements(\n        {\n            \"addid\",\n            \"clear\",\n            \"delete\",\n            \"deleteid\",\n            \"move\",\n            \"moveid\",\n            \"playlist\",\n            \"playlistfind\",\n            \"playlistsearch\",\n            \"plchanges\",\n            \"plchangesposid\",\n            \"prio\",\n            \"prioid\",\n            \"rangeid\",\n            \"shuffle\",\n            \"swap\",\n            \"swapid\",\n            \"addtagid\",\n            \"cleartagid\",\n        },\n        fail=True,\n    )\n\n    METADATA: ClassVar[set[str]] = {\"Pos\", \"Time\", \"Id\", \"file\", \"duration\"}\n\n    def test_cmd_add(self):\n        with self.run_bpd() as client:\n            self._bpd_add(client, self.item1)\n\n    def test_cmd_playlistinfo(self):\n        with self.run_bpd() as client:\n            self._bpd_add(client, self.item1, self.item2)\n            responses = client.send_commands(\n                (\"playlistinfo\",),\n                (\"playlistinfo\", \"0\"),\n                (\"playlistinfo\", \"0:2\"),\n                (\"playlistinfo\", \"200\"),\n            )\n        self._assert_failed(responses, bpd.ERROR_ARG, pos=3)\n        assert \"1\" == responses[1].data[\"Id\"]\n        assert [\"1\", \"2\"] == responses[2].data[\"Id\"]\n\n    def test_cmd_playlistinfo_tagtypes(self):\n        with self.run_bpd() as client:\n            self._bpd_add(client, self.item1)\n            response = client.send_command(\"playlistinfo\", \"0\")\n        self._assert_ok(response)\n        assert BPDConnectionTest.TAGTYPES.union(BPDQueueTest.METADATA) == set(\n            response.data.keys()\n        )\n\n    def test_cmd_playlistid(self):\n        with self.run_bpd() as client:\n            self._bpd_add(client, self.item1, self.item2)\n            responses = client.send_commands(\n                (\"playlistid\", \"2\"), (\"playlistid\",)\n            )\n        self._assert_ok(*responses)\n        assert \"Track Two Title\" == responses[0].data[\"Title\"]\n        assert [\"1\", \"2\"] == responses[1].data[\"Track\"]\n\n\nclass BPDPlaylistsTest(BPDTestHelper):\n    test_implements_playlists = implements({\"playlistadd\"})\n\n    def test_cmd_listplaylist(self):\n        with self.run_bpd() as client:\n            response = client.send_command(\"listplaylist\", \"anything\")\n        self._assert_failed(response, bpd.ERROR_NO_EXIST)\n\n    def test_cmd_listplaylistinfo(self):\n        with self.run_bpd() as client:\n            response = client.send_command(\"listplaylistinfo\", \"anything\")\n        self._assert_failed(response, bpd.ERROR_NO_EXIST)\n\n    def test_cmd_listplaylists(self):\n        with self.run_bpd() as client:\n            response = client.send_command(\"listplaylists\")\n        self._assert_failed(response, bpd.ERROR_UNKNOWN)\n\n    def test_cmd_load(self):\n        with self.run_bpd() as client:\n            response = client.send_command(\"load\", \"anything\")\n        self._assert_failed(response, bpd.ERROR_NO_EXIST)\n\n    @unittest.expectedFailure\n    def test_cmd_playlistadd(self):\n        with self.run_bpd() as client:\n            self._bpd_add(client, self.item1, playlist=\"anything\")\n\n    def test_cmd_playlistclear(self):\n        with self.run_bpd() as client:\n            response = client.send_command(\"playlistclear\", \"anything\")\n        self._assert_failed(response, bpd.ERROR_UNKNOWN)\n\n    def test_cmd_playlistdelete(self):\n        with self.run_bpd() as client:\n            response = client.send_command(\"playlistdelete\", \"anything\", \"0\")\n        self._assert_failed(response, bpd.ERROR_UNKNOWN)\n\n    def test_cmd_playlistmove(self):\n        with self.run_bpd() as client:\n            response = client.send_command(\"playlistmove\", \"anything\", \"0\", \"1\")\n        self._assert_failed(response, bpd.ERROR_UNKNOWN)\n\n    def test_cmd_rename(self):\n        with self.run_bpd() as client:\n            response = client.send_command(\"rename\", \"anything\", \"newname\")\n        self._assert_failed(response, bpd.ERROR_UNKNOWN)\n\n    def test_cmd_rm(self):\n        with self.run_bpd() as client:\n            response = client.send_command(\"rm\", \"anything\")\n        self._assert_failed(response, bpd.ERROR_UNKNOWN)\n\n    def test_cmd_save(self):\n        with self.run_bpd() as client:\n            self._bpd_add(client, self.item1)\n            response = client.send_command(\"save\", \"newplaylist\")\n        self._assert_failed(response, bpd.ERROR_UNKNOWN)\n\n\nclass BPDDatabaseTest(BPDTestHelper):\n    test_implements_database = implements(\n        {\n            \"albumart\",\n            \"find\",\n            \"findadd\",\n            \"listall\",\n            \"listallinfo\",\n            \"listfiles\",\n            \"readcomments\",\n            \"searchadd\",\n            \"searchaddpl\",\n            \"update\",\n            \"rescan\",\n        },\n        fail=True,\n    )\n\n    def test_cmd_search(self):\n        with self.run_bpd() as client:\n            response = client.send_command(\"search\", \"track\", \"1\")\n        self._assert_ok(response)\n        assert self.item1.title == response.data[\"Title\"]\n\n    def test_cmd_list(self):\n        with self.run_bpd() as client:\n            responses = client.send_commands(\n                (\"list\", \"album\"),\n                (\"list\", \"track\"),\n                (\"list\", \"album\", \"artist\", \"Artist Name\", \"track\"),\n            )\n        self._assert_failed(responses, bpd.ERROR_ARG, pos=2)\n        assert \"Album Title\" == responses[0].data[\"Album\"]\n        assert [\"1\", \"2\"] == responses[1].data[\"Track\"]\n\n    def test_cmd_list_three_arg_form(self):\n        with self.run_bpd() as client:\n            responses = client.send_commands(\n                (\"list\", \"album\", \"artist\", \"Artist Name\"),\n                (\"list\", \"album\", \"Artist Name\"),\n                (\"list\", \"track\", \"Artist Name\"),\n            )\n        self._assert_failed(responses, bpd.ERROR_ARG, pos=2)\n        assert responses[0].data == responses[1].data\n\n    def test_cmd_lsinfo(self):\n        with self.run_bpd() as client:\n            response1 = client.send_command(\"lsinfo\")\n            self._assert_ok(response1)\n            response2 = client.send_command(\n                \"lsinfo\", response1.data[\"directory\"]\n            )\n            self._assert_ok(response2)\n            response3 = client.send_command(\n                \"lsinfo\", response2.data[\"directory\"]\n            )\n            self._assert_ok(response3)\n        assert self.item1.title in response3.data[\"Title\"]\n\n    def test_cmd_count(self):\n        with self.run_bpd() as client:\n            response = client.send_command(\"count\", \"track\", \"1\")\n        self._assert_ok(response)\n        assert \"1\" == response.data[\"songs\"]\n        assert \"0\" == response.data[\"playtime\"]\n\n\nclass BPDMountsTest(BPDTestHelper):\n    test_implements_mounts = implements(\n        {\n            \"mount\",\n            \"unmount\",\n            \"listmounts\",\n            \"listneighbors\",\n        },\n        fail=True,\n    )\n\n\nclass BPDStickerTest(BPDTestHelper):\n    test_implements_stickers = implements(\n        {\n            \"sticker\",\n        },\n        fail=True,\n    )\n\n\nclass BPDConnectionTest(BPDTestHelper):\n    test_implements_connection = implements(\n        {\n            \"close\",\n            \"kill\",\n        }\n    )\n\n    ALL_MPD_TAGTYPES: ClassVar[set[str]] = {\n        \"Artist\",\n        \"ArtistSort\",\n        \"Album\",\n        \"AlbumSort\",\n        \"AlbumArtist\",\n        \"AlbumArtistSort\",\n        \"Title\",\n        \"Track\",\n        \"Name\",\n        \"Genre\",\n        \"Date\",\n        \"Composer\",\n        \"Performer\",\n        \"Comment\",\n        \"Disc\",\n        \"Label\",\n        \"OriginalDate\",\n        \"MUSICBRAINZ_ARTISTID\",\n        \"MUSICBRAINZ_ALBUMID\",\n        \"MUSICBRAINZ_ALBUMARTISTID\",\n        \"MUSICBRAINZ_TRACKID\",\n        \"MUSICBRAINZ_RELEASETRACKID\",\n        \"MUSICBRAINZ_WORKID\",\n    }\n    UNSUPPORTED_TAGTYPES: ClassVar[set[str]] = {\n        \"MUSICBRAINZ_WORKID\",  # not tracked by beets\n        \"Performer\",  # not tracked by beets\n        \"AlbumSort\",  # not tracked by beets\n        \"Name\",  # junk field for internet radio\n    }\n    TAGTYPES = ALL_MPD_TAGTYPES.difference(UNSUPPORTED_TAGTYPES)\n\n    def test_cmd_password(self):\n        with self.run_bpd(password=\"abc123\") as client:\n            response = client.send_command(\"status\")\n            self._assert_failed(response, bpd.ERROR_PERMISSION)\n\n            response = client.send_command(\"password\", \"wrong\")\n            self._assert_failed(response, bpd.ERROR_PASSWORD)\n\n            responses = client.send_commands(\n                (\"password\", \"abc123\"), (\"status\",)\n            )\n        self._assert_ok(*responses)\n\n    def test_cmd_ping(self):\n        with self.run_bpd() as client:\n            response = client.send_command(\"ping\")\n        self._assert_ok(response)\n\n    def test_cmd_tagtypes(self):\n        with self.run_bpd() as client:\n            response = client.send_command(\"tagtypes\")\n        self._assert_ok(response)\n        assert self.TAGTYPES == set(response.data[\"tagtype\"])\n\n    @unittest.expectedFailure\n    def test_tagtypes_mask(self):\n        with self.run_bpd() as client:\n            response = client.send_command(\"tagtypes\", \"clear\")\n        self._assert_ok(response)\n\n\nclass BPDPartitionTest(BPDTestHelper):\n    test_implements_partitions = implements(\n        {\n            \"partition\",\n            \"listpartitions\",\n            \"newpartition\",\n        },\n        fail=True,\n    )\n\n\nclass BPDDeviceTest(BPDTestHelper):\n    test_implements_devices = implements(\n        {\n            \"disableoutput\",\n            \"enableoutput\",\n            \"toggleoutput\",\n            \"outputs\",\n        },\n        fail=True,\n    )\n\n\nclass BPDReflectionTest(BPDTestHelper):\n    test_implements_reflection = implements(\n        {\n            \"config\",\n            \"commands\",\n            \"notcommands\",\n            \"urlhandlers\",\n        },\n        fail=True,\n    )\n\n    @patch(\n        \"beetsplug.bpd.gstplayer.GstPlayer.get_decoders\",\n        MagicMock(return_value={\"default\": ({\"audio/mpeg\"}, {\"mp3\"})}),\n    )\n    def test_cmd_decoders(self):\n        with self.run_bpd() as client:\n            response = client.send_command(\"decoders\")\n        self._assert_ok(response)\n        assert \"default\" == response.data[\"plugin\"]\n        assert \"mp3\" == response.data[\"suffix\"]\n        assert \"audio/mpeg\" == response.data[\"mime_type\"]\n\n\nclass BPDPeersTest(BPDTestHelper):\n    test_implements_peers = implements(\n        {\n            \"subscribe\",\n            \"unsubscribe\",\n            \"channels\",\n            \"readmessages\",\n            \"sendmessage\",\n        },\n        fail=True,\n    )\n"
  },
  {
    "path": "test/plugins/test_bucket.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Fabrice Laporte.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for the 'bucket' plugin.\"\"\"\n\nfrom datetime import datetime\n\nimport pytest\n\nfrom beets import config, ui\nfrom beets.test.helper import BeetsTestCase\nfrom beetsplug import bucket\n\n\nclass BucketPluginTest(BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n        self.plugin = bucket.BucketPlugin()\n\n    def _setup_config(\n        self,\n        bucket_year=[],\n        bucket_alpha=[],\n        bucket_alpha_regex={},\n        extrapolate=False,\n    ):\n        config[\"bucket\"][\"bucket_year\"] = bucket_year\n        config[\"bucket\"][\"bucket_alpha\"] = bucket_alpha\n        config[\"bucket\"][\"bucket_alpha_regex\"] = bucket_alpha_regex\n        config[\"bucket\"][\"extrapolate\"] = extrapolate\n        self.plugin.setup()\n\n    def test_year_single_year(self):\n        \"\"\"If a single year is given, range starts from this year and stops at\n        the year preceding the one of next bucket.\"\"\"\n        self._setup_config(bucket_year=[\"1950s\", \"1970s\"])\n        assert self.plugin._tmpl_bucket(\"1959\") == \"1950s\"\n        assert self.plugin._tmpl_bucket(\"1969\") == \"1950s\"\n\n    def test_year_single_year_last_folder(self):\n        \"\"\"If a single year is given for the last bucket, extend it to current\n        year.\"\"\"\n        self._setup_config(bucket_year=[\"1950\", \"1970\"])\n        assert self.plugin._tmpl_bucket(\"2014\") == \"1970\"\n        next_year = datetime.now().year + 1\n        assert self.plugin._tmpl_bucket(str(next_year)) == str(next_year)\n\n    def test_year_two_years(self):\n        \"\"\"Buckets can be named with the 'from-to' syntax.\"\"\"\n        self._setup_config(bucket_year=[\"1950-59\", \"1960-1969\"])\n        assert self.plugin._tmpl_bucket(\"1959\") == \"1950-59\"\n        assert self.plugin._tmpl_bucket(\"1969\") == \"1960-1969\"\n\n    def test_year_multiple_years(self):\n        \"\"\"Buckets can be named by listing all the years\"\"\"\n        self._setup_config(bucket_year=[\"1950,51,52,53\"])\n        assert self.plugin._tmpl_bucket(\"1953\") == \"1950,51,52,53\"\n        assert self.plugin._tmpl_bucket(\"1974\") == \"1974\"\n\n    def test_year_out_of_range(self):\n        \"\"\"If no range match, return the year\"\"\"\n        self._setup_config(bucket_year=[\"1950-59\", \"1960-69\"])\n        assert self.plugin._tmpl_bucket(\"1974\") == \"1974\"\n        self._setup_config(bucket_year=[])\n        assert self.plugin._tmpl_bucket(\"1974\") == \"1974\"\n\n    def test_year_out_of_range_extrapolate(self):\n        \"\"\"If no defined range match, extrapolate all ranges using the most\n        common syntax amongst existing buckets and return the matching one.\"\"\"\n        self._setup_config(bucket_year=[\"1950-59\", \"1960-69\"], extrapolate=True)\n        assert self.plugin._tmpl_bucket(\"1914\") == \"1910-19\"\n        # pick single year format\n        self._setup_config(\n            bucket_year=[\"1962-81\", \"2002\", \"2012\"], extrapolate=True\n        )\n        assert self.plugin._tmpl_bucket(\"1983\") == \"1982\"\n        # pick from-end format\n        self._setup_config(\n            bucket_year=[\"1962-81\", \"2002\", \"2012-14\"], extrapolate=True\n        )\n        assert self.plugin._tmpl_bucket(\"1983\") == \"1982-01\"\n        # extrapolate add ranges, but never modifies existing ones\n        self._setup_config(\n            bucket_year=[\"1932\", \"1942\", \"1952\", \"1962-81\", \"2002\"],\n            extrapolate=True,\n        )\n        assert self.plugin._tmpl_bucket(\"1975\") == \"1962-81\"\n\n    def test_alpha_all_chars(self):\n        \"\"\"Alphabet buckets can be named by listing all their chars\"\"\"\n        self._setup_config(bucket_alpha=[\"ABCD\", \"FGH\", \"IJKL\"])\n        assert self.plugin._tmpl_bucket(\"garry\") == \"FGH\"\n\n    def test_alpha_first_last_chars(self):\n        \"\"\"Alphabet buckets can be named by listing the 'from-to' syntax\"\"\"\n        self._setup_config(bucket_alpha=[\"0->9\", \"A->D\", \"F-H\", \"I->Z\"])\n        assert self.plugin._tmpl_bucket(\"garry\") == \"F-H\"\n        assert self.plugin._tmpl_bucket(\"2pac\") == \"0->9\"\n\n    def test_alpha_out_of_range(self):\n        \"\"\"If no range match, return the initial\"\"\"\n        self._setup_config(bucket_alpha=[\"ABCD\", \"FGH\", \"IJKL\"])\n        assert self.plugin._tmpl_bucket(\"errol\") == \"E\"\n        self._setup_config(bucket_alpha=[])\n        assert self.plugin._tmpl_bucket(\"errol\") == \"E\"\n\n    def test_alpha_regex(self):\n        \"\"\"Check regex is used\"\"\"\n        self._setup_config(\n            bucket_alpha=[\"foo\", \"bar\"],\n            bucket_alpha_regex={\"foo\": \"^[a-d]\", \"bar\": \"^[e-z]\"},\n        )\n        assert self.plugin._tmpl_bucket(\"alpha\") == \"foo\"\n        assert self.plugin._tmpl_bucket(\"delta\") == \"foo\"\n        assert self.plugin._tmpl_bucket(\"zeta\") == \"bar\"\n        assert self.plugin._tmpl_bucket(\"Alpha\") == \"A\"\n\n    def test_alpha_regex_mix(self):\n        \"\"\"Check mixing regex and non-regex is possible\"\"\"\n        self._setup_config(\n            bucket_alpha=[\"A - D\", \"E - L\"],\n            bucket_alpha_regex={\"A - D\": \"^[0-9a-dA-D…äÄ]\"},\n        )\n        assert self.plugin._tmpl_bucket(\"alpha\") == \"A - D\"\n        assert self.plugin._tmpl_bucket(\"Ärzte\") == \"A - D\"\n        assert self.plugin._tmpl_bucket(\"112\") == \"A - D\"\n        assert self.plugin._tmpl_bucket(\"…and Oceans\") == \"A - D\"\n        assert self.plugin._tmpl_bucket(\"Eagles\") == \"E - L\"\n\n    def test_bad_alpha_range_def(self):\n        \"\"\"If bad alpha range definition, a UserError is raised.\"\"\"\n        with pytest.raises(ui.UserError):\n            self._setup_config(bucket_alpha=[\"$%\"])\n\n    def test_bad_year_range_def_no4digits(self):\n        \"\"\"If bad year range definition, a UserError is raised.\n        Range origin must be expressed on 4 digits.\n        \"\"\"\n        with pytest.raises(ui.UserError):\n            self._setup_config(bucket_year=[\"62-64\"])\n\n    def test_bad_year_range_def_nodigits(self):\n        \"\"\"If bad year range definition, a UserError is raised.\n        At least the range origin must be declared.\n        \"\"\"\n        with pytest.raises(ui.UserError):\n            self._setup_config(bucket_year=[\"nodigits\"])\n\n    def check_span_from_str(self, sstr, dfrom, dto):\n        d = bucket.span_from_str(sstr)\n        assert dfrom == d[\"from\"]\n        assert dto == d[\"to\"]\n\n    def test_span_from_str(self):\n        self.check_span_from_str(\"1980 2000\", 1980, 2000)\n        self.check_span_from_str(\"1980 00\", 1980, 2000)\n        self.check_span_from_str(\"1930 00\", 1930, 2000)\n        self.check_span_from_str(\"1930 50\", 1930, 1950)\n"
  },
  {
    "path": "test/plugins/test_convert.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Thomas Scholtes.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\nfrom __future__ import annotations\n\nimport fnmatch\nimport os.path\nimport re\nimport sys\nimport unittest\nfrom typing import TYPE_CHECKING\n\nimport pytest\nfrom mediafile import MediaFile\n\nfrom beets import util\nfrom beets.library import Item\nfrom beets.test import _common\nfrom beets.test.helper import (\n    AsIsImporterMixin,\n    ImportHelper,\n    IOMixin,\n    PluginTestCase,\n    capture_log,\n)\nfrom beetsplug import convert\n\nif TYPE_CHECKING:\n    from pathlib import Path\n\n\ndef shell_quote(text):\n    import shlex\n\n    return shlex.quote(text)\n\n\nclass ConvertMixin:\n    def tagged_copy_cmd(self, tag):\n        \"\"\"Return a conversion command that copies files and appends\n        `tag` to the copy.\n        \"\"\"\n        if re.search(\"[^a-zA-Z0-9]\", tag):\n            raise ValueError(\n                f\"tag '{tag}' must only contain letters and digits\"\n            )\n\n        # A Python script that copies the file and appends a tag.\n        stub = os.path.join(_common.RSRC, b\"convert_stub.py\").decode(\"utf-8\")\n        return f\"{shell_quote(sys.executable)} {shell_quote(stub)} $source $dest {tag}\"\n\n    def file_endswith(self, path: Path, tag: str):\n        \"\"\"Check the path is a file and if its content ends with `tag`.\"\"\"\n        assert path.exists()\n        assert path.is_file()\n        return path.read_bytes().endswith(tag.encode(\"utf-8\"))\n\n\nclass ConvertTestCase(IOMixin, ConvertMixin, PluginTestCase):\n    db_on_disk = True\n    plugin = \"convert\"\n\n\n@_common.slow_test()\nclass ImportConvertTest(AsIsImporterMixin, ImportHelper, ConvertTestCase):\n    def setUp(self):\n        super().setUp()\n        self.config[\"convert\"] = {\n            \"dest\": os.path.join(self.temp_dir, b\"convert\"),\n            \"command\": self.tagged_copy_cmd(\"convert\"),\n            # Enforce running convert\n            \"max_bitrate\": 1,\n            \"auto\": True,\n            \"quiet\": False,\n        }\n\n    def test_import_converted(self):\n        self.run_asis_importer()\n        item = self.lib.items().get()\n        assert self.file_endswith(item.filepath, \"convert\")\n\n    # FIXME: fails on windows\n    @unittest.skipIf(sys.platform == \"win32\", \"win32\")\n    def test_import_original_on_convert_error(self):\n        # `false` exits with non-zero code\n        self.config[\"convert\"][\"command\"] = \"false\"\n        self.run_asis_importer()\n\n        item = self.lib.items().get()\n        assert item is not None\n        assert item.filepath.is_file()\n\n    def test_delete_originals(self):\n        self.config[\"convert\"][\"delete_originals\"] = True\n        self.run_asis_importer()\n        for path in self.importer.paths:\n            for root, dirnames, filenames in os.walk(path):\n                assert len(fnmatch.filter(filenames, \"*.mp3\")) == 0, (\n                    f\"Non-empty import directory {util.displayable_path(path)}\"\n                )\n\n    def get_count_of_import_files(self):\n        import_file_count = 0\n\n        for path in self.importer.paths:\n            for root, _, filenames in os.walk(path):\n                import_file_count += len(filenames)\n\n        return import_file_count\n\n\nclass ConvertCommand:\n    \"\"\"A mixin providing a utility method to run the `convert`command\n    in tests.\n    \"\"\"\n\n    def run_convert_path(self, item, *args):\n        \"\"\"Run the `convert` command on a given path.\"\"\"\n        return self.run_command(\"convert\", *args, f\"path:{item.filepath}\")\n\n    def run_convert(self, *args):\n        \"\"\"Run the `convert` command on `self.item`.\"\"\"\n        return self.run_convert_path(self.item, *args)\n\n\n@_common.slow_test()\nclass ConvertCliTest(ConvertTestCase, ConvertCommand):\n    def setUp(self):\n        super().setUp()\n        self.album = self.add_album_fixture(ext=\"ogg\")\n        self.item = self.album.items()[0]\n\n        self.convert_dest = self.temp_dir_path / \"convert_dest\"\n        self.converted_mp3 = self.convert_dest / \"converted.mp3\"\n        self.config[\"convert\"] = {\n            \"dest\": str(self.convert_dest),\n            \"paths\": {\"default\": \"converted\"},\n            \"format\": \"mp3\",\n            \"formats\": {\n                \"mp3\": self.tagged_copy_cmd(\"mp3\"),\n                \"ogg\": self.tagged_copy_cmd(\"ogg\"),\n                \"opus\": {\n                    \"command\": self.tagged_copy_cmd(\"opus\"),\n                    \"extension\": \"ops\",\n                },\n            },\n        }\n\n    def test_convert(self):\n        self.io.addinput(\"y\")\n        self.run_convert()\n        assert self.file_endswith(self.converted_mp3, \"mp3\")\n\n    def test_convert_with_auto_confirmation(self):\n        self.run_convert(\"--yes\")\n        assert self.file_endswith(self.converted_mp3, \"mp3\")\n\n    def test_reject_confirmation(self):\n        self.io.addinput(\"n\")\n        self.run_convert()\n        assert not self.converted_mp3.exists()\n\n    def test_convert_keep_new(self):\n        assert os.path.splitext(self.item.path)[1] == b\".ogg\"\n\n        self.io.addinput(\"y\")\n        self.run_convert(\"--keep-new\")\n\n        self.item.load()\n        assert os.path.splitext(self.item.path)[1] == b\".mp3\"\n\n    def test_format_option(self):\n        self.io.addinput(\"y\")\n        self.run_convert(\"--format\", \"opus\")\n        assert self.file_endswith(self.convert_dest / \"converted.ops\", \"opus\")\n\n    def test_embed_album_art(self):\n        self.config[\"convert\"][\"embed\"] = True\n        image_path = os.path.join(_common.RSRC, b\"image-2x3.jpg\")\n        self.album.artpath = image_path\n        self.album.store()\n        with open(os.path.join(image_path), \"rb\") as f:\n            image_data = f.read()\n\n        self.io.addinput(\"y\")\n        self.run_convert()\n        mediafile = MediaFile(self.converted_mp3)\n        assert mediafile.images[0].data == image_data\n\n    def test_skip_existing(self):\n        converted = self.converted_mp3\n        self.touch(converted, content=\"XXX\")\n        self.run_convert(\"--yes\")\n        with open(converted) as f:\n            assert f.read() == \"XXX\"\n\n    def test_pretend(self):\n        self.run_convert(\"--pretend\")\n        assert not self.converted_mp3.exists()\n\n    def test_empty_query(self):\n        with capture_log(\"beets.convert\") as logs:\n            self.run_convert(\"An impossible query\")\n        assert logs[0] == \"convert: Empty query result.\"\n\n    def test_no_transcode_when_maxbr_set_high_and_different_formats(self):\n        self.config[\"convert\"][\"max_bitrate\"] = 5000\n        self.io.addinput(\"y\")\n        self.run_convert()\n        assert self.file_endswith(self.converted_mp3, \"mp3\")\n\n    def test_transcode_when_maxbr_set_low_and_different_formats(self):\n        self.config[\"convert\"][\"max_bitrate\"] = 5\n        self.io.addinput(\"y\")\n        self.run_convert()\n        assert self.file_endswith(self.converted_mp3, \"mp3\")\n\n    def test_transcode_when_maxbr_set_to_none_and_different_formats(self):\n        self.io.addinput(\"y\")\n        self.run_convert()\n        assert self.file_endswith(self.converted_mp3, \"mp3\")\n\n    def test_no_transcode_when_maxbr_set_high_and_same_formats(self):\n        self.config[\"convert\"][\"max_bitrate\"] = 5000\n        self.config[\"convert\"][\"format\"] = \"ogg\"\n        self.io.addinput(\"y\")\n        self.run_convert()\n        assert not self.file_endswith(\n            self.convert_dest / \"converted.ogg\", \"ogg\"\n        )\n\n    def test_force_overrides_max_bitrate_and_same_formats(self):\n        self.config[\"convert\"][\"max_bitrate\"] = 5000\n        self.config[\"convert\"][\"format\"] = \"ogg\"\n\n        self.io.addinput(\"y\")\n        self.run_convert(\"--force\")\n\n        converted = self.convert_dest / \"converted.ogg\"\n        assert self.file_endswith(converted, \"ogg\")\n\n    def test_transcode_when_maxbr_set_low_and_same_formats(self):\n        self.config[\"convert\"][\"max_bitrate\"] = 5\n        self.config[\"convert\"][\"format\"] = \"ogg\"\n        self.io.addinput(\"y\")\n        self.run_convert()\n        assert self.file_endswith(self.convert_dest / \"converted.ogg\", \"ogg\")\n\n    def test_transcode_when_maxbr_set_to_none_and_same_formats(self):\n        self.config[\"convert\"][\"format\"] = \"ogg\"\n        self.io.addinput(\"y\")\n        self.run_convert()\n        assert not self.file_endswith(\n            self.convert_dest / \"converted.ogg\", \"ogg\"\n        )\n\n    def test_playlist(self):\n        self.io.addinput(\"y\")\n        self.run_convert(\"--playlist\", \"playlist.m3u8\")\n        assert (self.convert_dest / \"playlist.m3u8\").exists()\n\n    def test_playlist_pretend(self):\n        self.run_convert(\"--playlist\", \"playlist.m3u8\", \"--pretend\")\n        assert not (self.convert_dest / \"playlist.m3u8\").exists()\n\n    def test_force_overrides_no_convert(self):\n        self.config[\"convert\"][\"formats\"][\"opus\"] = {\n            \"command\": self.tagged_copy_cmd(\"opus\"),\n            \"extension\": \"ops\",\n        }\n        self.config[\"convert\"][\"no_convert\"] = \"format:ogg\"\n\n        [item] = self.add_item_fixtures(ext=\"ogg\")\n\n        self.io.addinput(\"y\")\n        self.run_convert_path(item, \"--format\", \"opus\", \"--force\")\n\n        converted = self.convert_dest / \"converted.ops\"\n        assert self.file_endswith(converted, \"opus\")\n\n    def assert_playlist_entry(self, expected_entry, *args):\n        self.io.addinput(\"y\")\n        self.run_convert(*args, \"--playlist\", \"playlist.m3u8\")\n        lines = (self.convert_dest / \"playlist.m3u8\").read_text().splitlines()\n        assert lines[0] == \"#EXTM3U\"\n        assert lines[1] == expected_entry\n\n    def test_playlist_entry_uses_config_format(self):\n        self.assert_playlist_entry(\"converted.mp3\")\n\n    def test_playlist_entry_uses_cli_format(self):\n        self.assert_playlist_entry(\"converted.ops\", \"--format\", \"opus\")\n\n    def test_playlist_entry_keeps_original_extension_when_not_transcoded(self):\n        self.config[\"convert\"][\"no_convert\"] = \"format:ogg\"\n        self.assert_playlist_entry(\"converted.ogg\")\n\n    def test_playlist_entry_keep_new_points_to_destination_file(self):\n        self.assert_playlist_entry(\"converted.ogg\", \"--keep-new\")\n\n\n@_common.slow_test()\nclass NeverConvertLossyFilesTest(ConvertTestCase, ConvertCommand):\n    \"\"\"Test the effect of the `never_convert_lossy_files` option.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        self.convert_dest = self.temp_dir_path / \"convert_dest\"\n        self.config[\"convert\"] = {\n            \"dest\": str(self.convert_dest),\n            \"paths\": {\"default\": \"converted\"},\n            \"never_convert_lossy_files\": True,\n            \"format\": \"mp3\",\n            \"formats\": {\n                \"mp3\": self.tagged_copy_cmd(\"mp3\"),\n            },\n        }\n\n    def test_transcode_from_lossless(self):\n        [item] = self.add_item_fixtures(ext=\"flac\")\n        self.io.addinput(\"y\")\n        self.run_convert_path(item)\n        converted = self.convert_dest / \"converted.mp3\"\n        assert self.file_endswith(converted, \"mp3\")\n\n    def test_transcode_from_lossy(self):\n        self.config[\"convert\"][\"never_convert_lossy_files\"] = False\n        [item] = self.add_item_fixtures(ext=\"ogg\")\n        self.io.addinput(\"y\")\n        self.run_convert_path(item)\n        converted = self.convert_dest / \"converted.mp3\"\n        assert self.file_endswith(converted, \"mp3\")\n\n    def test_transcode_from_lossy_prevented(self):\n        [item] = self.add_item_fixtures(ext=\"ogg\")\n        self.io.addinput(\"y\")\n        self.run_convert_path(item)\n        converted = self.convert_dest / \"converted.ogg\"\n        assert not self.file_endswith(converted, \"mp3\")\n\n    def test_force_overrides_never_convert_lossy_files(self):\n        self.config[\"convert\"][\"formats\"][\"opus\"] = {\n            \"command\": self.tagged_copy_cmd(\"opus\"),\n            \"extension\": \"ops\",\n        }\n        [item] = self.add_item_fixtures(ext=\"ogg\")\n\n        self.io.addinput(\"y\")\n        self.run_convert_path(item, \"--format\", \"opus\", \"--force\")\n\n        converted = self.convert_dest / \"converted.ops\"\n        assert self.file_endswith(converted, \"opus\")\n\n\nclass TestNoConvert:\n    \"\"\"Test the effect of the `no_convert` option.\"\"\"\n\n    @pytest.mark.parametrize(\n        \"config_value, should_skip\",\n        [\n            (\"\", False),\n            (\"bitrate:320\", False),\n            (\"bitrate:320 format:ogg\", False),\n            (\"bitrate:320 , format:ogg\", True),\n        ],\n    )\n    def test_no_convert_skip(self, config_value, should_skip):\n        item = Item(format=\"ogg\", bitrate=256)\n        convert.config[\"convert\"][\"no_convert\"] = config_value\n        assert convert.in_no_convert(item) == should_skip\n"
  },
  {
    "path": "test/plugins/test_discogs.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for discogs plugin.\"\"\"\n\nfrom unittest.mock import Mock, patch\n\nimport pytest\n\nfrom beets import config\nfrom beets.library import Item\nfrom beets.test._common import Bag\nfrom beets.test.helper import BeetsTestCase, capture_log\nfrom beetsplug.discogs import ArtistState, DiscogsPlugin\n\n\ndef _artist(name: str, **kwargs):\n    return {\n        \"id\": 1,\n        \"name\": name,\n        \"join\": \"\",\n        \"role\": \"\",\n        \"anv\": \"\",\n        \"tracks\": \"\",\n        \"resource_url\": \"\",\n    } | kwargs\n\n\n@patch(\"beetsplug.discogs.DiscogsPlugin.setup\", Mock())\nclass DGAlbumInfoTest(BeetsTestCase):\n    def _make_release(self, tracks=None):\n        \"\"\"Returns a Bag that mimics a discogs_client.Release. The list\n        of elements on the returned Bag is incomplete, including just\n        those required for the tests on this class.\"\"\"\n        data = {\n            \"id\": \"ALBUM ID\",\n            \"uri\": \"https://www.discogs.com/release/release/13633721\",\n            \"title\": \"ALBUM TITLE\",\n            \"year\": \"3001\",\n            \"artists\": [_artist(\"ARTIST NAME\", id=\"ARTIST ID\", join=\",\")],\n            \"formats\": [\n                {\n                    \"descriptions\": [\"FORMAT DESC 1\", \"FORMAT DESC 2\"],\n                    \"name\": \"FORMAT\",\n                    \"qty\": 1,\n                }\n            ],\n            \"styles\": [\"STYLE1\", \"STYLE2\"],\n            \"genres\": [\"GENRE1\", \"GENRE2\"],\n            \"labels\": [\n                {\n                    \"name\": \"LABEL NAME\",\n                    \"catno\": \"CATALOG NUMBER\",\n                }\n            ],\n            \"tracklist\": [],\n        }\n\n        if tracks:\n            for recording in tracks:\n                data[\"tracklist\"].append(recording)\n\n        return Bag(\n            data=data,\n            # Make some fields available as properties, as they are\n            # accessed by DiscogsPlugin methods.\n            title=data[\"title\"],\n            artists=[Bag(data=d) for d in data[\"artists\"]],\n        )\n\n    def _make_track(self, title, position=\"\", duration=\"\", type_=None):\n        track = {\"title\": title, \"position\": position, \"duration\": duration}\n        if type_ is not None:\n            # Test samples on discogs_client do not have a 'type_' field, but\n            # the API seems to return it. Values: 'track' for regular tracks,\n            # 'heading' for descriptive texts (ie. not real tracks - 12.13.2).\n            track[\"type_\"] = type_\n\n        return track\n\n    def _make_release_from_positions(self, positions):\n        \"\"\"Return a Bag that mimics a discogs_client.Release with a\n        tracklist where tracks have the specified `positions`.\"\"\"\n        tracks = [\n            self._make_track(f\"TITLE{i}\", position)\n            for (i, position) in enumerate(positions, start=1)\n        ]\n        return self._make_release(tracks)\n\n    def test_parse_media_for_tracks(self):\n        tracks = [\n            self._make_track(\"TITLE ONE\", \"1\", \"01:01\"),\n            self._make_track(\"TITLE TWO\", \"2\", \"02:02\"),\n        ]\n        release = self._make_release(tracks=tracks)\n\n        d = DiscogsPlugin().get_album_info(release)\n        t = d.tracks\n        assert d.media == \"FORMAT\"\n        assert t[0].media == d.media\n        assert t[1].media == d.media\n\n    def test_parse_medium_numbers_single_medium(self):\n        release = self._make_release_from_positions([\"1\", \"2\"])\n        d = DiscogsPlugin().get_album_info(release)\n        t = d.tracks\n\n        assert d.mediums == 1\n        assert t[0].medium == 1\n        assert t[0].medium_total == 2\n        assert t[1].medium == 1\n        assert t[0].medium_total == 2\n\n    def test_parse_medium_numbers_two_mediums(self):\n        release = self._make_release_from_positions([\"1-1\", \"2-1\"])\n        d = DiscogsPlugin().get_album_info(release)\n        t = d.tracks\n\n        assert d.mediums == 2\n        assert t[0].medium == 1\n        assert t[0].medium_total == 1\n        assert t[1].medium == 2\n        assert t[1].medium_total == 1\n\n    def test_parse_medium_numbers_two_mediums_two_sided(self):\n        release = self._make_release_from_positions([\"A1\", \"B1\", \"C1\"])\n        d = DiscogsPlugin().get_album_info(release)\n        t = d.tracks\n\n        assert d.mediums == 2\n        assert t[0].medium == 1\n        assert t[0].medium_total == 2\n        assert t[0].medium_index == 1\n        assert t[1].medium == 1\n        assert t[1].medium_total == 2\n        assert t[1].medium_index == 2\n        assert t[2].medium == 2\n        assert t[2].medium_total == 1\n        assert t[2].medium_index == 1\n\n    def test_parse_track_indices(self):\n        release = self._make_release_from_positions([\"1\", \"2\"])\n        d = DiscogsPlugin().get_album_info(release)\n        t = d.tracks\n\n        assert t[0].medium_index == 1\n        assert t[0].index == 1\n        assert t[0].medium_total == 2\n        assert t[1].medium_index == 2\n        assert t[1].index == 2\n        assert t[1].medium_total == 2\n\n    def test_parse_track_indices_several_media(self):\n        release = self._make_release_from_positions(\n            [\"1-1\", \"1-2\", \"2-1\", \"3-1\"]\n        )\n        d = DiscogsPlugin().get_album_info(release)\n        t = d.tracks\n\n        assert d.mediums == 3\n        assert t[0].medium_index == 1\n        assert t[0].index == 1\n        assert t[0].medium_total == 2\n        assert t[1].medium_index == 2\n        assert t[1].index == 2\n        assert t[1].medium_total == 2\n        assert t[2].medium_index == 1\n        assert t[2].index == 3\n        assert t[2].medium_total == 1\n        assert t[3].medium_index == 1\n        assert t[3].index == 4\n        assert t[3].medium_total == 1\n\n    def test_parse_tracklist_without_sides(self):\n        \"\"\"Test standard Discogs position 12.2.9#1: \"without sides\".\"\"\"\n        release = self._make_release_from_positions([\"1\", \"2\", \"3\"])\n        d = DiscogsPlugin().get_album_info(release)\n\n        assert d.mediums == 1\n        assert len(d.tracks) == 3\n\n    def test_parse_tracklist_with_sides(self):\n        \"\"\"Test standard Discogs position 12.2.9#2: \"with sides\".\"\"\"\n        release = self._make_release_from_positions([\"A1\", \"A2\", \"B1\", \"B2\"])\n        d = DiscogsPlugin().get_album_info(release)\n\n        assert d.mediums == 1  # 2 sides = 1 LP\n        assert len(d.tracks) == 4\n\n    def test_parse_tracklist_multiple_lp(self):\n        \"\"\"Test standard Discogs position 12.2.9#3: \"multiple LP\".\"\"\"\n        release = self._make_release_from_positions([\"A1\", \"A2\", \"B1\", \"C1\"])\n        d = DiscogsPlugin().get_album_info(release)\n\n        assert d.mediums == 2  # 3 sides = 1 LP + 1 LP\n        assert len(d.tracks) == 4\n\n    def test_parse_tracklist_multiple_cd(self):\n        \"\"\"Test standard Discogs position 12.2.9#4: \"multiple CDs\".\"\"\"\n        release = self._make_release_from_positions(\n            [\"1-1\", \"1-2\", \"2-1\", \"3-1\"]\n        )\n        d = DiscogsPlugin().get_album_info(release)\n\n        assert d.mediums == 3\n        assert len(d.tracks) == 4\n\n    def test_parse_tracklist_non_standard(self):\n        \"\"\"Test non standard Discogs position.\"\"\"\n        release = self._make_release_from_positions([\"I\", \"II\", \"III\", \"IV\"])\n        d = DiscogsPlugin().get_album_info(release)\n\n        assert d.mediums == 1\n        assert len(d.tracks) == 4\n\n    def test_parse_tracklist_subtracks_dot(self):\n        \"\"\"Test standard Discogs position 12.2.9#5: \"sub tracks, dots\".\"\"\"\n        release = self._make_release_from_positions([\"1\", \"2.1\", \"2.2\", \"3\"])\n        d = DiscogsPlugin().get_album_info(release)\n\n        assert d.mediums == 1\n        assert len(d.tracks) == 3\n\n        release = self._make_release_from_positions(\n            [\"A1\", \"A2.1\", \"A2.2\", \"A3\"]\n        )\n        d = DiscogsPlugin().get_album_info(release)\n\n        assert d.mediums == 1\n        assert len(d.tracks) == 3\n\n    def test_parse_tracklist_subtracks_letter(self):\n        \"\"\"Test standard Discogs position 12.2.9#5: \"sub tracks, letter\".\"\"\"\n        release = self._make_release_from_positions([\"A1\", \"A2a\", \"A2b\", \"A3\"])\n        d = DiscogsPlugin().get_album_info(release)\n\n        assert d.mediums == 1\n        assert len(d.tracks) == 3\n\n        release = self._make_release_from_positions(\n            [\"A1\", \"A2.a\", \"A2.b\", \"A3\"]\n        )\n        d = DiscogsPlugin().get_album_info(release)\n\n        assert d.mediums == 1\n        assert len(d.tracks) == 3\n\n    def test_parse_tracklist_subtracks_extra_material(self):\n        \"\"\"Test standard Discogs position 12.2.9#6: \"extra material\".\"\"\"\n        release = self._make_release_from_positions([\"1\", \"2\", \"Video 1\"])\n        d = DiscogsPlugin().get_album_info(release)\n\n        assert d.mediums == 2\n        assert len(d.tracks) == 3\n\n    def test_parse_tracklist_subtracks_indices(self):\n        \"\"\"Test parsing of subtracks that include index tracks.\"\"\"\n        release = self._make_release_from_positions([\"\", \"\", \"1.1\", \"1.2\"])\n        # Track 1: Index track with medium title\n        release.data[\"tracklist\"][0][\"title\"] = \"MEDIUM TITLE\"\n        # Track 2: Index track with track group title\n        release.data[\"tracklist\"][1][\"title\"] = \"TRACK GROUP TITLE\"\n\n        d = DiscogsPlugin().get_album_info(release)\n        assert d.mediums == 1\n        assert d.tracks[0].disctitle == \"MEDIUM TITLE\"\n        assert len(d.tracks) == 1\n        assert d.tracks[0].title == \"TRACK GROUP TITLE\"\n\n    def test_parse_tracklist_subtracks_nested_logical(self):\n        \"\"\"Test parsing of subtracks defined inside a index track that are\n        logical subtracks (ie. should be grouped together into a single track).\n        \"\"\"\n        release = self._make_release_from_positions([\"1\", \"\", \"3\"])\n        # Track 2: Index track with track group title, and sub_tracks\n        release.data[\"tracklist\"][1][\"title\"] = \"TRACK GROUP TITLE\"\n        release.data[\"tracklist\"][1][\"sub_tracks\"] = [\n            self._make_track(\"TITLE ONE\", \"2.1\", \"01:01\"),\n            self._make_track(\"TITLE TWO\", \"2.2\", \"02:02\"),\n        ]\n\n        d = DiscogsPlugin().get_album_info(release)\n        assert d.mediums == 1\n        assert len(d.tracks) == 3\n        assert d.tracks[1].title == \"TRACK GROUP TITLE\"\n\n    def test_parse_tracklist_subtracks_nested_physical(self):\n        \"\"\"Test parsing of subtracks defined inside a index track that are\n        physical subtracks (ie. should not be grouped together).\n        \"\"\"\n        release = self._make_release_from_positions([\"1\", \"\", \"4\"])\n        # Track 2: Index track with track group title, and sub_tracks\n        release.data[\"tracklist\"][1][\"title\"] = \"TRACK GROUP TITLE\"\n        release.data[\"tracklist\"][1][\"sub_tracks\"] = [\n            self._make_track(\"TITLE ONE\", \"2\", \"01:01\"),\n            self._make_track(\"TITLE TWO\", \"3\", \"02:02\"),\n        ]\n\n        d = DiscogsPlugin().get_album_info(release)\n        assert d.mediums == 1\n        assert len(d.tracks) == 4\n        assert d.tracks[1].title == \"TITLE ONE\"\n        assert d.tracks[2].title == \"TITLE TWO\"\n\n    def test_parse_tracklist_disctitles(self):\n        \"\"\"Test parsing of index tracks that act as disc titles.\"\"\"\n        release = self._make_release_from_positions(\n            [\"\", \"1-1\", \"1-2\", \"\", \"2-1\"]\n        )\n        # Track 1: Index track with medium title (Cd1)\n        release.data[\"tracklist\"][0][\"title\"] = \"MEDIUM TITLE CD1\"\n        # Track 4: Index track with medium title (Cd2)\n        release.data[\"tracklist\"][3][\"title\"] = \"MEDIUM TITLE CD2\"\n\n        d = DiscogsPlugin().get_album_info(release)\n        assert d.mediums == 2\n        assert d.tracks[0].disctitle == \"MEDIUM TITLE CD1\"\n        assert d.tracks[1].disctitle == \"MEDIUM TITLE CD1\"\n        assert d.tracks[2].disctitle == \"MEDIUM TITLE CD2\"\n        assert len(d.tracks) == 3\n\n    def test_parse_minimal_release(self):\n        \"\"\"Test parsing of a release with the minimal amount of information.\"\"\"\n        data = {\n            \"id\": 123,\n            \"uri\": \"https://www.discogs.com/release/123456-something\",\n            \"tracklist\": [self._make_track(\"A\", \"1\", \"01:01\")],\n            \"artists\": [_artist(\"ARTIST NAME\", id=321)],\n            \"title\": \"TITLE\",\n        }\n        release = Bag(\n            data=data,\n            title=data[\"title\"],\n            artists=[Bag(data=d) for d in data[\"artists\"]],\n        )\n        d = DiscogsPlugin().get_album_info(release)\n        assert d.artist == \"ARTIST NAME\"\n        assert d.album == \"TITLE\"\n        assert len(d.tracks) == 1\n\n    def test_parse_release_without_required_fields(self):\n        \"\"\"Test parsing of a release that does not have the required fields.\"\"\"\n        release = Bag(data={}, refresh=lambda *args: None)\n        with capture_log() as logs:\n            d = DiscogsPlugin().get_album_info(release)\n\n        assert d is None\n        assert \"Release does not contain the required fields\" in logs[0]\n\n    def test_default_genre_style_settings(self):\n        \"\"\"Test genre default settings, genres to genre, styles to style\"\"\"\n        release = self._make_release_from_positions([\"1\", \"2\"])\n\n        d = DiscogsPlugin().get_album_info(release)\n        assert d.genres == [\"GENRE1\", \"GENRE2\"]\n        assert d.style == \"STYLE1, STYLE2\"\n\n    def test_append_style_to_genre(self):\n        \"\"\"Test appending style to genre if config enabled\"\"\"\n        config[\"discogs\"][\"append_style_genre\"] = True\n        release = self._make_release_from_positions([\"1\", \"2\"])\n\n        d = DiscogsPlugin().get_album_info(release)\n        assert d.genres == [\"GENRE1\", \"GENRE2\", \"STYLE1\", \"STYLE2\"]\n        assert d.style == \"STYLE1, STYLE2\"\n\n    def test_append_style_to_genre_no_style(self):\n        \"\"\"Test nothing appended to genre if style is empty\"\"\"\n        config[\"discogs\"][\"append_style_genre\"] = True\n        release = self._make_release_from_positions([\"1\", \"2\"])\n        release.data[\"styles\"] = []\n\n        d = DiscogsPlugin().get_album_info(release)\n        assert d.genres == [\"GENRE1\", \"GENRE2\"]\n        assert d.style is None\n\n    def test_strip_disambiguation(self):\n        \"\"\"Test removing disambiguation from all disambiguated fields.\"\"\"\n        data = {\n            \"id\": 123,\n            \"uri\": \"https://www.discogs.com/release/123456-something\",\n            \"tracklist\": [\n                {\n                    \"title\": \"track\",\n                    \"position\": \"A\",\n                    \"type_\": \"track\",\n                    \"duration\": \"5:44\",\n                    \"artists\": [_artist(\"TEST ARTIST (5)\", id=11146)],\n                }\n            ],\n            \"artists\": [\n                _artist(\"ARTIST NAME (2)\", id=321, join=\"&\"),\n                _artist(\"OTHER ARTIST (5)\", id=321),\n            ],\n            \"title\": \"title\",\n            \"labels\": [\n                {\n                    \"name\": \"LABEL NAME (5)\",\n                    \"catno\": \"catalog number\",\n                }\n            ],\n        }\n        release = Bag(\n            data=data,\n            title=data[\"title\"],\n            artists=[Bag(data=d) for d in data[\"artists\"]],\n        )\n        d = DiscogsPlugin().get_album_info(release)\n        assert d.artist == \"ARTIST NAME & OTHER ARTIST\"\n        assert d.artists == [\"ARTIST NAME\", \"OTHER ARTIST\"]\n        assert d.artists_ids == [\"321\", \"321\"]\n        assert d.tracks[0].artist == \"TEST ARTIST\"\n        assert d.tracks[0].artists == [\"TEST ARTIST\"]\n        assert d.tracks[0].artist_id == \"11146\"\n        assert d.tracks[0].artists_ids == [\"11146\"]\n        assert d.label == \"LABEL NAME\"\n\n    def test_strip_disambiguation_false(self):\n        \"\"\"Test disabling disambiguation removal from all disambiguated fields.\"\"\"\n        config[\"discogs\"][\"strip_disambiguation\"] = False\n        data = {\n            \"id\": 123,\n            \"uri\": \"https://www.discogs.com/release/123456-something\",\n            \"tracklist\": [\n                {\n                    \"title\": \"track\",\n                    \"position\": \"A\",\n                    \"type_\": \"track\",\n                    \"duration\": \"5:44\",\n                    \"artists\": [_artist(\"TEST ARTIST (5)\", id=11146)],\n                }\n            ],\n            \"artists\": [\n                _artist(\"ARTIST NAME (2)\", id=321, join=\"&\"),\n                _artist(\"OTHER ARTIST (5)\", id=321),\n            ],\n            \"title\": \"title\",\n            \"labels\": [\n                {\n                    \"name\": \"LABEL NAME (5)\",\n                    \"catno\": \"catalog number\",\n                }\n            ],\n        }\n        release = Bag(\n            data=data,\n            title=data[\"title\"],\n            artists=[Bag(data=d) for d in data[\"artists\"]],\n        )\n        d = DiscogsPlugin().get_album_info(release)\n        assert d.artist == \"ARTIST NAME (2) & OTHER ARTIST (5)\"\n        assert d.artists == [\"ARTIST NAME (2)\", \"OTHER ARTIST (5)\"]\n        assert d.tracks[0].artist == \"TEST ARTIST (5)\"\n        assert d.tracks[0].artists == [\"TEST ARTIST (5)\"]\n        assert d.label == \"LABEL NAME (5)\"\n        config[\"discogs\"][\"strip_disambiguation\"] = True\n\n\n@patch(\"beetsplug.discogs.DiscogsPlugin.setup\", Mock())\nclass DGSearchQueryTest(BeetsTestCase):\n    def test_default_search_filters_without_extra_tags(self):\n        \"\"\"Discogs search uses only the type filter when no extra_tags are set.\"\"\"\n        plugin = DiscogsPlugin()\n        items = [Item()]\n\n        query, filters = plugin.get_search_query_with_filters(\n            \"album\", items, \"Artist\", \"Album\", False\n        )\n\n        assert \"Album\" in query\n        assert filters == {\"type\": \"release\"}\n\n    def test_extra_tags_populate_discogs_filters(self):\n        \"\"\"Configured extra_tags should populate Discogs search filters.\"\"\"\n        plugin = DiscogsPlugin()\n        plugin.config[\"extra_tags\"] = [\"label\", \"catalognum\"]\n\n        items = [\n            Item(catalognum=\"ABC 123\", label=\"abc\"),\n            Item(catalognum=\"ABC 123\", label=\"abc\"),\n            Item(catalognum=\"ABC 123\", label=\"def\"),\n        ]\n\n        _query, filters = plugin.get_search_query_with_filters(\n            \"album\", items, \"Artist\", \"Album\", False\n        )\n\n        assert filters[\"type\"] == \"release\"\n        assert filters[\"label\"] == \"abc\"\n        # Catalog number should have whitespace removed.\n        assert filters[\"catno\"] == \"ABC123\"\n        config[\"discogs\"][\"extra_tags\"] = []\n\n\n@pytest.mark.parametrize(\n    \"track_artist_anv,track_artist,track_artists\",\n    [\n        (False, \"ARTIST Feat. PERFORMER\", [\"ARTIST\", \"PERFORMER\"]),\n        (True, \"ART Feat. PERF\", [\"ART\", \"PERF\"]),\n    ],\n)\n@pytest.mark.parametrize(\n    \"album_artist_anv,album_artist,album_artists\",\n    [\n        (False, \"DRUMMER, ARTIST & SOLOIST\", [\"DRUMMER\", \"ARTIST\", \"SOLOIST\"]),\n        (True, \"DRUM, ARTY & SOLO\", [\"DRUM\", \"ARTY\", \"SOLO\"]),\n    ],\n)\n@pytest.mark.parametrize(\n    (\n        \"artist_credit_anv,track_artist_credit,\"\n        \"track_artists_credit,album_artist_credit,album_artists_credit\"\n    ),\n    [\n        (\n            False,\n            \"ARTIST Feat. PERFORMER\",\n            [\"ARTIST\", \"PERFORMER\"],\n            \"DRUMMER, ARTIST & SOLOIST\",\n            [\"DRUMMER\", \"ARTIST\", \"SOLOIST\"],\n        ),\n        (\n            True,\n            \"ART Feat. PERF\",\n            [\"ART\", \"PERF\"],\n            \"DRUM, ARTY & SOLO\",\n            [\"DRUM\", \"ARTY\", \"SOLO\"],\n        ),\n    ],\n)\n@patch(\"beetsplug.discogs.DiscogsPlugin.setup\", Mock())\ndef test_anv(\n    track_artist_anv,\n    track_artist,\n    track_artists,\n    album_artist_anv,\n    album_artist,\n    album_artists,\n    artist_credit_anv,\n    track_artist_credit,\n    track_artists_credit,\n    album_artist_credit,\n    album_artists_credit,\n):\n    \"\"\"Test using artist name variations.\"\"\"\n    data = {\n        \"id\": 123,\n        \"uri\": \"https://www.discogs.com/release/123456-something\",\n        \"tracklist\": [\n            {\n                \"title\": \"track\",\n                \"position\": \"A\",\n                \"type_\": \"track\",\n                \"duration\": \"5:44\",\n                \"artists\": [_artist(\"ARTIST\", id=11146, anv=\"ART\")],\n                \"extraartists\": [\n                    _artist(\n                        \"PERFORMER\",\n                        id=787,\n                        role=\"Featuring\",\n                        anv=\"PERF\",\n                    )\n                ],\n            }\n        ],\n        \"artists\": [\n            _artist(\"DRUMMER\", id=445, anv=\"DRUM\", join=\", \"),\n            _artist(\"ARTIST (4)\", id=321, anv=\"ARTY\", join=\"&\"),\n            _artist(\"SOLOIST\", id=445, anv=\"SOLO\"),\n        ],\n        \"title\": \"title\",\n    }\n    release = Bag(\n        data=data,\n        title=data[\"title\"],\n        artists=[Bag(data=d) for d in data[\"artists\"]],\n    )\n    config[\"discogs\"][\"anv\"][\"album_artist\"] = album_artist_anv\n    config[\"discogs\"][\"anv\"][\"artist\"] = track_artist_anv\n    config[\"discogs\"][\"anv\"][\"artist_credit\"] = artist_credit_anv\n    r = DiscogsPlugin().get_album_info(release)\n    assert r.artist == album_artist\n    assert r.artists == album_artists\n    assert r.artist_credit == album_artist_credit\n    assert r.artists_credit == album_artists_credit\n    assert r.tracks[0].artist == track_artist\n    assert r.tracks[0].artists == track_artists\n    assert r.tracks[0].artist_credit == track_artist_credit\n    assert r.tracks[0].artists_credit == track_artists_credit\n\n\n@pytest.mark.parametrize(\"artist_anv\", [True, False])\n@pytest.mark.parametrize(\"albumartist_anv\", [True, False])\n@pytest.mark.parametrize(\"artistcredit_anv\", [True, False])\n@patch(\"beetsplug.discogs.DiscogsPlugin.setup\", Mock())\ndef test_anv_no_variation(artist_anv, albumartist_anv, artistcredit_anv):\n    \"\"\"Test behavior when there is no ANV but the anv field is set\"\"\"\n    data = {\n        \"id\": 123,\n        \"uri\": \"https://www.discogs.com/release/123456-something\",\n        \"tracklist\": [\n            {\n                \"title\": \"track\",\n                \"position\": \"A\",\n                \"type_\": \"track\",\n                \"duration\": \"5:44\",\n                \"artists\": [_artist(\"PERFORMER\", id=1)],\n            }\n        ],\n        \"artists\": [_artist(\"ARTIST\", id=2)],\n        \"title\": \"title\",\n    }\n    release = Bag(\n        data=data,\n        title=data[\"title\"],\n        artists=[Bag(data=d) for d in data[\"artists\"]],\n    )\n    config[\"discogs\"][\"anv\"][\"album_artist\"] = albumartist_anv\n    config[\"discogs\"][\"anv\"][\"artist\"] = artist_anv\n    config[\"discogs\"][\"anv\"][\"artist_credit\"] = artistcredit_anv\n    r = DiscogsPlugin().get_album_info(release)\n    assert r.artist == \"ARTIST\"\n    assert r.artists == [\"ARTIST\"]\n    assert r.artist_credit == \"ARTIST\"\n    assert r.artists_credit == [\"ARTIST\"]\n    assert r.tracks[0].artist == \"PERFORMER\"\n    assert r.tracks[0].artists == [\"PERFORMER\"]\n    assert r.tracks[0].artist_credit == \"PERFORMER\"\n    assert r.tracks[0].artists_credit == [\"PERFORMER\"]\n\n\n@patch(\"beetsplug.discogs.DiscogsPlugin.setup\", Mock())\ndef test_anv_album_artist():\n    \"\"\"Test using artist name variations when the album artist\n    is the same as the track artist, but only the track artist\n    should use the artist name variation.\"\"\"\n    data = {\n        \"id\": 123,\n        \"uri\": \"https://www.discogs.com/release/123456-something\",\n        \"tracklist\": [\n            {\n                \"title\": \"track\",\n                \"position\": \"A\",\n                \"type_\": \"track\",\n                \"duration\": \"5:44\",\n            }\n        ],\n        \"artists\": [_artist(\"ARTIST (4)\", id=321, anv=\"VARIATION\")],\n        \"title\": \"title\",\n    }\n    release = Bag(\n        data=data,\n        title=data[\"title\"],\n        artists=[Bag(data=d) for d in data[\"artists\"]],\n    )\n    config[\"discogs\"][\"anv\"][\"album_artist\"] = False\n    config[\"discogs\"][\"anv\"][\"artist\"] = True\n    config[\"discogs\"][\"anv\"][\"artist_credit\"] = False\n    r = DiscogsPlugin().get_album_info(release)\n    assert r.artist == \"ARTIST\"\n    assert r.artists == [\"ARTIST\"]\n    assert r.artist_credit == \"ARTIST\"\n    assert r.artist_id == \"321\"\n    assert r.artists_credit == [\"ARTIST\"]\n    assert r.tracks[0].artist == \"VARIATION\"\n    assert r.tracks[0].artists == [\"VARIATION\"]\n    assert r.tracks[0].artist_credit == \"ARTIST\"\n    assert r.tracks[0].artists_credit == [\"ARTIST\"]\n\n\n@pytest.mark.parametrize(\n    \"track, expected_artist, expected_artists\",\n    [\n        (\n            {\n                \"type_\": \"track\",\n                \"title\": \"track\",\n                \"position\": \"1\",\n                \"duration\": \"5:00\",\n                \"artists\": [\n                    _artist(\"NEW ARTIST\", id=11146, join=\"&\"),\n                    _artist(\"VOCALIST\", id=344, join=\"feat.\"),\n                ],\n                \"extraartists\": [\n                    _artist(\"SOLOIST\", id=3, role=\"Featuring\"),\n                    _artist(\n                        \"PERFORMER (1)\", id=5, role=\"Other Role, Featuring\"\n                    ),\n                    _artist(\"RANDOM\", id=8, role=\"Written-By\"),\n                    _artist(\"MUSICIAN\", id=10, role=\"Featuring [Uncredited]\"),\n                ],\n            },\n            \"NEW ARTIST & VOCALIST feat. SOLOIST, PERFORMER, MUSICIAN\",\n            [\"NEW ARTIST\", \"VOCALIST\", \"SOLOIST\", \"PERFORMER\", \"MUSICIAN\"],\n        ),\n    ],\n)\n@patch(\"beetsplug.discogs.DiscogsPlugin.setup\", Mock())\ndef test_parse_featured_artists(track, expected_artist, expected_artists):\n    \"\"\"Tests the plugins ability to parse a featured artist.\n    Ignores artists that are not listed as featured.\"\"\"\n    plugin = DiscogsPlugin()\n    artistinfo = ArtistState.from_config(plugin.config, [_artist(\"ARTIST\")])\n    t, _, _ = plugin.get_track_info(track, 1, 1, artistinfo)\n    assert t.artist == expected_artist\n    assert t.artists == expected_artists\n\n\n@pytest.mark.parametrize(\n    \"formats, expected_media, expected_albumtype\",\n    [\n        (None, None, None),\n        (\n            [\n                {\n                    \"descriptions\": ['7\"', \"Single\", \"45 RPM\"],\n                    \"name\": \"Vinyl\",\n                    \"qty\": 1,\n                }\n            ],\n            \"Vinyl\",\n            '7\", Single, 45 RPM',\n        ),\n    ],\n)\ndef test_get_media_and_albumtype(formats, expected_media, expected_albumtype):\n    result = DiscogsPlugin.get_media_and_albumtype(formats)\n\n    assert result == (expected_media, expected_albumtype)\n\n\n@pytest.mark.parametrize(\n    \"given_artists,expected_info,config_va_name\",\n    [\n        (\n            [_artist(\"Various\")],\n            {\n                \"artist\": \"VARIOUS ARTISTS\",\n                \"artist_id\": \"1\",\n                \"artists\": [\"VARIOUS ARTISTS\"],\n                \"artists_ids\": [\"1\"],\n                \"artist_credit\": \"VARIOUS ARTISTS\",\n                \"artists_credit\": [\"VARIOUS ARTISTS\"],\n            },\n            \"VARIOUS ARTISTS\",\n        )\n    ],\n)\n@patch(\"beetsplug.discogs.DiscogsPlugin.setup\", Mock())\ndef test_va_buildartistinfo(given_artists, expected_info, config_va_name):\n    config[\"va_name\"] = config_va_name\n    assert (\n        ArtistState.from_config(DiscogsPlugin().config, given_artists).info\n        == expected_info\n    )\n\n\n@pytest.mark.parametrize(\n    \"position, medium, index, subindex\",\n    [\n        (\"1\", None, \"1\", None),\n        (\"A12\", \"A\", \"12\", None),\n        (\"12-34\", \"12-\", \"34\", None),\n        (\"CD1-1\", \"CD1-\", \"1\", None),\n        (\"1.12\", None, \"1\", \"12\"),\n        (\"12.a\", None, \"12\", \"A\"),\n        (\"12.34\", None, \"12\", \"34\"),\n        (\"1ab\", None, \"1\", \"AB\"),\n        # Non-standard\n        (\"IV\", \"IV\", None, None),\n    ],\n)\ndef test_get_track_index(position, medium, index, subindex):\n    assert DiscogsPlugin.get_track_index(position) == (medium, index, subindex)\n"
  },
  {
    "path": "test/plugins/test_edit.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson and Diego Moreda.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\nimport codecs\nfrom typing import ClassVar\nfrom unittest.mock import patch\n\nfrom beets.dbcore.query import TrueQuery\nfrom beets.importer import Action\nfrom beets.library import Item\nfrom beets.test import _common\nfrom beets.test.helper import (\n    AutotagImportTestCase,\n    AutotagStub,\n    BeetsTestCase,\n    IOMixin,\n    PluginMixin,\n    TerminalImportMixin,\n)\n\n\nclass ModifyFileMocker:\n    \"\"\"Helper for modifying a file, replacing or editing its contents. Used for\n    mocking the calls to the external editor during testing.\n    \"\"\"\n\n    def __init__(self, contents=None, replacements=None):\n        \"\"\"`self.contents` and `self.replacements` are initialized here, in\n        order to keep the rest of the functions of this class with the same\n        signature as `EditPlugin.get_editor()`, making mocking easier.\n            - `contents`: string with the contents of the file to be used for\n            `overwrite_contents()`\n            - `replacement`: dict with the in-place replacements to be used for\n            `replace_contents()`, in the form {'previous string': 'new string'}\n\n        TODO: check if it can be solved more elegantly with a decorator\n        \"\"\"\n        self.contents = contents\n        self.replacements = replacements\n        self.action = self.overwrite_contents\n        if replacements:\n            self.action = self.replace_contents\n\n    # The two methods below mock the `edit` utility function in the plugin.\n\n    def overwrite_contents(self, filename, log):\n        \"\"\"Modify `filename`, replacing its contents with `self.contents`. If\n        `self.contents` is empty, the file remains unchanged.\n        \"\"\"\n        if self.contents:\n            with codecs.open(filename, \"w\", encoding=\"utf-8\") as f:\n                f.write(self.contents)\n\n    def replace_contents(self, filename, log):\n        \"\"\"Modify `filename`, reading its contents and replacing the strings\n        specified in `self.replacements`.\n        \"\"\"\n        with codecs.open(filename, \"r\", encoding=\"utf-8\") as f:\n            contents = f.read()\n        for old, new_ in self.replacements.items():\n            contents = contents.replace(old, new_)\n        with codecs.open(filename, \"w\", encoding=\"utf-8\") as f:\n            f.write(contents)\n\n\nclass EditMixin(PluginMixin):\n    \"\"\"Helper containing some common functionality used for the Edit tests.\"\"\"\n\n    plugin = \"edit\"\n\n    def assertItemFieldsModified(\n        self, library_items, items, fields=[], allowed=[\"path\"]\n    ):\n        \"\"\"Assert that items in the library (`lib_items`) have different values\n        on the specified `fields` (and *only* on those fields), compared to\n        `items`.\n\n        An empty `fields` list results in asserting that no modifications have\n        been performed. `allowed` is a list of field changes that are ignored\n        (they may or may not have changed; the assertion doesn't care).\n        \"\"\"\n        for lib_item, item in zip(library_items, items):\n            diff_fields = [\n                field\n                for field in lib_item._fields\n                if lib_item[field] != item[field]\n            ]\n            assert set(diff_fields).difference(allowed) == set(fields)\n\n    def run_mocked_interpreter(self, modify_file_args={}, stdin=[]):\n        \"\"\"Run the edit command during an import session, with mocked stdin and\n        yaml writing.\n        \"\"\"\n        m = ModifyFileMocker(**modify_file_args)\n        with patch(\"beetsplug.edit.edit\", side_effect=m.action):\n            for char in stdin:\n                self.importer.add_choice(char)\n            self.importer.run()\n\n    def run_mocked_command(self, modify_file_args={}, stdin=[], args=[]):\n        \"\"\"Run the edit command, with mocked stdin and yaml writing, and\n        passing `args` to `run_command`.\"\"\"\n        m = ModifyFileMocker(**modify_file_args)\n        with patch(\"beetsplug.edit.edit\", side_effect=m.action):\n            for char in stdin:\n                self.io.addinput(char)\n            self.run_command(\"edit\", *args)\n\n\n@_common.slow_test()\n@patch(\"beets.library.Item.write\")\nclass EditCommandTest(IOMixin, EditMixin, BeetsTestCase):\n    \"\"\"Black box tests for `beetsplug.edit`. Command line interaction is\n    simulated using mocked stdin, and yaml editing via an external editor is\n    simulated using `ModifyFileMocker`.\n    \"\"\"\n\n    ALBUM_COUNT = 1\n    TRACK_COUNT = 10\n\n    def setUp(self):\n        super().setUp()\n        # Add an album, storing the original fields for comparison.\n        self.album = self.add_album_fixture(track_count=self.TRACK_COUNT)\n        self.album_orig = {f: self.album[f] for f in self.album._fields}\n        self.items_orig = [\n            {f: item[f] for f in item._fields} for item in self.album.items()\n        ]\n\n    def test_title_edit_discard(self, mock_write):\n        \"\"\"Edit title for all items in the library, then discard changes.\"\"\"\n        # Edit track titles.\n        self.run_mocked_command(\n            {\"replacements\": {\"t\\u00eftle\": \"modified t\\u00eftle\"}},\n            # Cancel.\n            [\"c\"],\n        )\n\n        assert mock_write.call_count == 0\n        self.assertItemFieldsModified(self.album.items(), self.items_orig, [])\n\n    def test_title_edit_apply(self, mock_write):\n        \"\"\"Edit title for all items in the library, then apply changes.\"\"\"\n        # Edit track titles.\n        self.run_mocked_command(\n            {\"replacements\": {\"t\\u00eftle\": \"modified t\\u00eftle\"}},\n            # Apply changes.\n            [\"a\"],\n        )\n\n        assert mock_write.call_count == self.TRACK_COUNT\n        self.assertItemFieldsModified(\n            self.album.items(), self.items_orig, [\"title\", \"mtime\"]\n        )\n\n    def test_single_title_edit_apply(self, mock_write):\n        \"\"\"Edit title for one item in the library, then apply changes.\"\"\"\n        # Edit one track title.\n        self.run_mocked_command(\n            {\"replacements\": {\"t\\u00eftle 9\": \"modified t\\u00eftle 9\"}},\n            # Apply changes.\n            [\"a\"],\n        )\n\n        assert mock_write.call_count == 1\n        # No changes except on last item.\n        self.assertItemFieldsModified(\n            list(self.album.items())[:-1], self.items_orig[:-1], []\n        )\n        assert list(self.album.items())[-1].title == \"modified t\\u00eftle 9\"\n\n    def test_title_edit_keep_editing_then_apply(self, mock_write):\n        \"\"\"Edit titles, keep editing once, then apply changes.\"\"\"\n        self.run_mocked_command(\n            {\"replacements\": {\"t\\u00eftle\": \"modified t\\u00eftle\"}},\n            # keep Editing, then Apply\n            [\"e\", \"a\"],\n        )\n\n        assert mock_write.call_count == self.TRACK_COUNT\n        self.assertItemFieldsModified(\n            self.album.items(),\n            self.items_orig,\n            [\"title\", \"mtime\"],\n        )\n\n    def test_title_edit_keep_editing_then_cancel(self, mock_write):\n        \"\"\"Edit titles, keep editing once, then cancel.\"\"\"\n        self.run_mocked_command(\n            {\"replacements\": {\"t\\u00eftle\": \"modified t\\u00eftle\"}},\n            # keep Editing, then Cancel\n            [\"e\", \"c\"],\n        )\n\n        assert mock_write.call_count == 0\n        self.assertItemFieldsModified(\n            self.album.items(),\n            self.items_orig,\n            [],\n        )\n\n    def test_noedit(self, mock_write):\n        \"\"\"Do not edit anything.\"\"\"\n        # Do not edit anything.\n        self.run_mocked_command(\n            {\"contents\": None},\n            # No stdin.\n            [],\n        )\n\n        assert mock_write.call_count == 0\n        self.assertItemFieldsModified(self.album.items(), self.items_orig, [])\n\n    def test_album_edit_apply(self, mock_write):\n        \"\"\"Edit the album field for all items in the library, apply changes.\n        By design, the album should not be updated.\"\"\n        \"\"\"\n        # Edit album.\n        self.run_mocked_command(\n            {\"replacements\": {\"\\u00e4lbum\": \"modified \\u00e4lbum\"}},\n            # Apply changes.\n            [\"a\"],\n        )\n\n        assert mock_write.call_count == self.TRACK_COUNT\n        self.assertItemFieldsModified(\n            self.album.items(), self.items_orig, [\"album\", \"mtime\"]\n        )\n        # Ensure album is *not* modified.\n        self.album.load()\n        assert self.album.album == \"\\u00e4lbum\"\n\n    def test_single_edit_add_field(self, mock_write):\n        \"\"\"Edit the yaml file appending an extra field to the first item, then\n        apply changes.\"\"\"\n        # Append \"foo: bar\" to item with id == 2. (\"id: 1\" would match both\n        # \"id: 1\" and \"id: 10\")\n        self.run_mocked_command(\n            {\"replacements\": {\"id: 2\": \"id: 2\\nfoo: bar\"}},\n            # Apply changes.\n            [\"a\"],\n        )\n\n        assert self.lib.items(\"id:2\")[0].foo == \"bar\"\n        # Even though a flexible attribute was written (which is not directly\n        # written to the tags), write should still be called since templates\n        # might use it.\n        assert mock_write.call_count == 1\n\n    def test_a_album_edit_apply(self, mock_write):\n        \"\"\"Album query (-a), edit album field, apply changes.\"\"\"\n        self.run_mocked_command(\n            {\"replacements\": {\"\\u00e4lbum\": \"modified \\u00e4lbum\"}},\n            # Apply changes.\n            [\"a\"],\n            args=[\"-a\"],\n        )\n\n        self.album.load()\n        assert mock_write.call_count == self.TRACK_COUNT\n        assert self.album.album == \"modified \\u00e4lbum\"\n        self.assertItemFieldsModified(\n            self.album.items(), self.items_orig, [\"album\", \"mtime\"]\n        )\n\n    def test_a_albumartist_edit_apply(self, mock_write):\n        \"\"\"Album query (-a), edit albumartist field, apply changes.\"\"\"\n        self.run_mocked_command(\n            {\"replacements\": {\"album artist\": \"modified album artist\"}},\n            # Apply changes.\n            [\"a\"],\n            args=[\"-a\"],\n        )\n\n        self.album.load()\n        assert mock_write.call_count == self.TRACK_COUNT\n        assert self.album.albumartist == \"the modified album artist\"\n        self.assertItemFieldsModified(\n            self.album.items(), self.items_orig, [\"albumartist\", \"mtime\"]\n        )\n\n    def test_malformed_yaml(self, mock_write):\n        \"\"\"Edit the yaml file incorrectly (resulting in a malformed yaml\n        document).\"\"\"\n        # Edit the yaml file to an invalid file.\n        self.run_mocked_command(\n            {\"contents\": \"!MALFORMED\"},\n            # Edit again to fix? No.\n            [\"n\"],\n        )\n\n        assert mock_write.call_count == 0\n\n    def test_invalid_yaml(self, mock_write):\n        \"\"\"Edit the yaml file incorrectly (resulting in a well-formed but\n        invalid yaml document).\"\"\"\n        # Edit the yaml file to an invalid but parseable file.\n        self.run_mocked_command(\n            {\"contents\": \"wellformed: yes, but invalid\"},\n            # No stdin.\n            [],\n        )\n\n        assert mock_write.call_count == 0\n\n\n@_common.slow_test()\nclass EditDuringImporterTestCase(\n    EditMixin, TerminalImportMixin, AutotagImportTestCase\n):\n    \"\"\"TODO\"\"\"\n\n    matching = AutotagStub.GOOD\n\n    IGNORED: ClassVar[list[str]] = [\"added\", \"album_id\", \"id\", \"mtime\", \"path\"]\n\n    def setUp(self):\n        super().setUp()\n        # Create some mediafiles, and store them for comparison.\n        self.prepare_album_for_import(1)\n        self.items_orig = [Item.from_path(f.path) for f in self.import_media]\n\n\n@_common.slow_test()\nclass EditDuringImporterNonSingletonTest(EditDuringImporterTestCase):\n    def setUp(self):\n        super().setUp()\n        self.importer = self.setup_importer()\n\n    def test_edit_apply_asis(self):\n        \"\"\"Edit the album field for all items in the library, apply changes,\n        using the original item tags.\n        \"\"\"\n        # Edit track titles.\n        self.run_mocked_interpreter(\n            {\"replacements\": {\"Tag Track\": \"Edited Track\"}},\n            # eDit, Apply changes.\n            [\"d\", \"a\"],\n        )\n\n        # Check that only the 'title' field is modified.\n        self.assertItemFieldsModified(\n            self.lib.items(),\n            self.items_orig,\n            [\"title\"],\n            [\n                *self.IGNORED,\n                \"albumartist\",\n                \"mb_albumartistid\",\n                \"mb_albumartistids\",\n            ],\n        )\n        assert all(\"Edited Track\" in i.title for i in self.lib.items())\n\n        # Ensure album is *not* fetched from a candidate.\n        assert self.lib.albums()[0].mb_albumid == \"\"\n\n    def test_edit_discard_asis(self):\n        \"\"\"Edit the album field for all items in the library, discard changes,\n        using the original item tags.\n        \"\"\"\n        # Edit track titles.\n        self.run_mocked_interpreter(\n            {\"replacements\": {\"Tag Track\": \"Edited Track\"}},\n            # eDit, Cancel, Use as-is.\n            [\"d\", \"c\", \"u\"],\n        )\n\n        # Check that nothing is modified, the album is imported ASIS.\n        self.assertItemFieldsModified(\n            self.lib.items(),\n            self.items_orig,\n            [],\n            [*self.IGNORED, \"albumartist\", \"mb_albumartistid\"],\n        )\n        assert all(\"Tag Track\" in i.title for i in self.lib.items())\n\n        # Ensure album is *not* fetched from a candidate.\n        assert self.lib.albums()[0].mb_albumid == \"\"\n\n    def test_edit_apply_candidate(self):\n        \"\"\"Edit the album field for all items in the library, apply changes,\n        using a candidate.\n        \"\"\"\n        # Edit track titles.\n        self.run_mocked_interpreter(\n            {\"replacements\": {\"Applied Track\": \"Edited Track\"}},\n            # edit Candidates, 1, Apply changes.\n            [\"c\", \"1\", \"a\"],\n        )\n\n        # Check that 'title' field is modified, and other fields come from\n        # the candidate.\n        assert all(\"Edited Track \" in i.title for i in self.lib.items())\n        assert all(\"match \" in i.mb_trackid for i in self.lib.items())\n\n        # Ensure album is fetched from a candidate.\n        assert \"albumid\" in self.lib.albums()[0].mb_albumid\n\n    def test_edit_retag_apply(self):\n        \"\"\"Import the album using a candidate, then retag and edit and apply\n        changes.\n        \"\"\"\n        self.run_mocked_interpreter(\n            {},\n            # 1, Apply changes.\n            [\"1\", Action.APPLY],\n        )\n\n        # Retag and edit track titles.  On retag, the importer will reset items\n        # ids but not the db connections.\n        self.importer.paths = []\n        self.importer.query = TrueQuery()\n        self.run_mocked_interpreter(\n            {\"replacements\": {\"Applied Track\": \"Edited Track\"}},\n            # eDit, Apply changes.\n            [\"d\", \"a\"],\n        )\n\n        # Check that 'title' field is modified, and other fields come from\n        # the candidate.\n        assert all(\"Edited Track \" in i.title for i in self.lib.items())\n        assert all(\"match \" in i.mb_trackid for i in self.lib.items())\n\n        # Ensure album is fetched from a candidate.\n        assert \"albumid\" in self.lib.albums()[0].mb_albumid\n\n    def test_edit_discard_candidate(self):\n        \"\"\"Edit the album field for all items in the library, discard changes,\n        using a candidate.\n        \"\"\"\n        # Edit track titles.\n        self.run_mocked_interpreter(\n            {\"replacements\": {\"Applied Track\": \"Edited Track\"}},\n            # edit Candidates, 1, Apply changes.\n            [\"c\", \"1\", \"a\"],\n        )\n\n        # Check that 'title' field is modified, and other fields come from\n        # the candidate.\n        assert all(\"Edited Track \" in i.title for i in self.lib.items())\n        assert all(\"match \" in i.mb_trackid for i in self.lib.items())\n\n        # Ensure album is fetched from a candidate.\n        assert \"albumid\" in self.lib.albums()[0].mb_albumid\n\n    def test_edit_apply_candidate_singleton(self):\n        \"\"\"Edit the album field for all items in the library, apply changes,\n        using a candidate and singleton mode.\n        \"\"\"\n        # Edit track titles.\n        self.run_mocked_interpreter(\n            {\"replacements\": {\"Applied Track\": \"Edited Track\"}},\n            # edit Candidates, 1, Apply changes, aBort.\n            [\"c\", \"1\", \"a\", \"b\"],\n        )\n\n        # Check that 'title' field is modified, and other fields come from\n        # the candidate.\n        assert all(\"Edited Track \" in i.title for i in self.lib.items())\n        assert all(\"match \" in i.mb_trackid for i in self.lib.items())\n\n\n@_common.slow_test()\nclass EditDuringImporterSingletonTest(EditDuringImporterTestCase):\n    def setUp(self):\n        super().setUp()\n        self.importer = self.setup_singleton_importer()\n\n    def test_edit_apply_asis_singleton(self):\n        \"\"\"Edit the album field for all items in the library, apply changes,\n        using the original item tags and singleton mode.\n        \"\"\"\n        # Edit track titles.\n        self.run_mocked_interpreter(\n            {\"replacements\": {\"Tag Track\": \"Edited Track\"}},\n            # eDit, Apply changes, aBort.\n            [\"d\", \"a\", \"b\"],\n        )\n\n        # Check that only the 'title' field is modified.\n        self.assertItemFieldsModified(\n            self.lib.items(),\n            self.items_orig,\n            [\"title\"],\n            [*self.IGNORED, \"albumartist\", \"mb_albumartistid\"],\n        )\n        assert all(\"Edited Track\" in i.title for i in self.lib.items())\n"
  },
  {
    "path": "test/plugins/test_embedart.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Thomas Scholtes.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nimport os\nimport os.path\nimport shutil\nimport tempfile\nimport unittest\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\nfrom mediafile import MediaFile\n\nfrom beets import config, logging, ui\nfrom beets.test import _common\nfrom beets.test.helper import (\n    BeetsTestCase,\n    FetchImageHelper,\n    ImportHelper,\n    IOMixin,\n    PluginMixin,\n)\nfrom beets.util import bytestring_path, displayable_path, syspath\nfrom beets.util.artresizer import ArtResizer\nfrom beetsplug._utils import art\nfrom test.test_art_resize import DummyIMBackend\n\n\ndef require_artresizer_compare(test):\n    def wrapper(*args, **kwargs):\n        if not ArtResizer.shared.can_compare:\n            raise unittest.SkipTest(\"compare not available\")\n\n        # PHASH computation in ImageMagick changed at some point in an\n        # undocumented way. Check at a low level that comparisons of our\n        # fixtures give the expected results. Only then, plugin logic tests\n        # below are meaningful.\n        # cf. https://github.com/ImageMagick/ImageMagick/discussions/5191\n        # It would be better to investigate what exactly change in IM and\n        # handle that in ArtResizer.IMBackend.{can_compare,compare}.\n        # Skipping the tests as below is a quick fix to CI, but users may\n        # still see unexpected behaviour.\n        abbey_artpath = os.path.join(_common.RSRC, b\"abbey.jpg\")\n        abbey_similarpath = os.path.join(_common.RSRC, b\"abbey-similar.jpg\")\n        abbey_differentpath = os.path.join(_common.RSRC, b\"abbey-different.jpg\")\n        compare_threshold = 20\n\n        similar_compares_ok = ArtResizer.shared.compare(\n            abbey_artpath,\n            abbey_similarpath,\n            compare_threshold,\n        )\n        different_compares_ok = ArtResizer.shared.compare(\n            abbey_artpath,\n            abbey_differentpath,\n            compare_threshold,\n        )\n        if not similar_compares_ok or different_compares_ok:\n            raise unittest.SkipTest(\"IM version with broken compare\")\n\n        return test(*args, **kwargs)\n\n    wrapper.__name__ = test.__name__\n    return wrapper\n\n\nclass EmbedartCliTest(\n    ImportHelper, IOMixin, PluginMixin, FetchImageHelper, BeetsTestCase\n):\n    plugin = \"embedart\"\n    small_artpath = os.path.join(_common.RSRC, b\"image-2x3.jpg\")\n    abbey_artpath = os.path.join(_common.RSRC, b\"abbey.jpg\")\n    abbey_similarpath = os.path.join(_common.RSRC, b\"abbey-similar.jpg\")\n    abbey_differentpath = os.path.join(_common.RSRC, b\"abbey-different.jpg\")\n\n    def _setup_data(self, artpath=None):\n        if not artpath:\n            artpath = self.small_artpath\n        with open(syspath(artpath), \"rb\") as f:\n            self.image_data = f.read()\n\n    def test_embed_art_from_file_with_yes_input(self):\n        self._setup_data()\n        album = self.add_album_fixture()\n        item = album.items()[0]\n        self.io.addinput(\"y\")\n        self.run_command(\"embedart\", \"-f\", self.small_artpath)\n        mediafile = MediaFile(syspath(item.path))\n        assert mediafile.images[0].data == self.image_data\n\n    def test_embed_art_from_file_with_no_input(self):\n        self._setup_data()\n        album = self.add_album_fixture()\n        item = album.items()[0]\n        self.io.addinput(\"n\")\n        self.run_command(\"embedart\", \"-f\", self.small_artpath)\n        mediafile = MediaFile(syspath(item.path))\n        # make sure that images array is empty (nothing embedded)\n        assert not mediafile.images\n\n    def test_embed_art_from_file(self):\n        self._setup_data()\n        album = self.add_album_fixture()\n        item = album.items()[0]\n        self.run_command(\"embedart\", \"-y\", \"-f\", self.small_artpath)\n        mediafile = MediaFile(syspath(item.path))\n        assert mediafile.images[0].data == self.image_data\n\n    def test_embed_art_from_album(self):\n        self._setup_data()\n        album = self.add_album_fixture()\n        item = album.items()[0]\n        album.artpath = self.small_artpath\n        album.store()\n        self.run_command(\"embedart\", \"-y\")\n        mediafile = MediaFile(syspath(item.path))\n        assert mediafile.images[0].data == self.image_data\n\n    def test_embed_art_remove_art_file(self):\n        self._setup_data()\n        album = self.add_album_fixture()\n\n        logging.getLogger(\"beets.embedart\").setLevel(logging.DEBUG)\n\n        handle, tmp_path = tempfile.mkstemp()\n        tmp_path = bytestring_path(tmp_path)\n        os.write(handle, self.image_data)\n        os.close(handle)\n\n        album.artpath = tmp_path\n        album.store()\n\n        config[\"embedart\"][\"remove_art_file\"] = True\n        self.run_command(\"embedart\", \"-y\")\n\n        if os.path.isfile(syspath(tmp_path)):\n            os.remove(syspath(tmp_path))\n            self.fail(\n                f\"Artwork file {displayable_path(tmp_path)} was not deleted\"\n            )\n\n    def test_art_file_missing(self):\n        self.add_album_fixture()\n        logging.getLogger(\"beets.embedart\").setLevel(logging.DEBUG)\n        with pytest.raises(ui.UserError):\n            self.run_command(\"embedart\", \"-y\", \"-f\", \"/doesnotexist\")\n\n    def test_embed_non_image_file(self):\n        album = self.add_album_fixture()\n        logging.getLogger(\"beets.embedart\").setLevel(logging.DEBUG)\n\n        handle, tmp_path = tempfile.mkstemp()\n        tmp_path = bytestring_path(tmp_path)\n        os.write(handle, b\"I am not an image.\")\n        os.close(handle)\n\n        try:\n            self.run_command(\"embedart\", \"-y\", \"-f\", tmp_path)\n        finally:\n            os.remove(syspath(tmp_path))\n\n        mediafile = MediaFile(syspath(album.items()[0].path))\n        assert not mediafile.images  # No image added.\n\n    @require_artresizer_compare\n    def test_reject_different_art(self):\n        self._setup_data(self.abbey_artpath)\n        album = self.add_album_fixture()\n        item = album.items()[0]\n        self.run_command(\"embedart\", \"-y\", \"-f\", self.abbey_artpath)\n        config[\"embedart\"][\"compare_threshold\"] = 20\n        self.run_command(\"embedart\", \"-y\", \"-f\", self.abbey_differentpath)\n        mediafile = MediaFile(syspath(item.path))\n\n        assert mediafile.images[0].data == self.image_data, (\n            f\"Image written is not {displayable_path(self.abbey_artpath)}\"\n        )\n\n    @require_artresizer_compare\n    def test_accept_similar_art(self):\n        self._setup_data(self.abbey_similarpath)\n        album = self.add_album_fixture()\n        item = album.items()[0]\n        self.run_command(\"embedart\", \"-y\", \"-f\", self.abbey_artpath)\n        config[\"embedart\"][\"compare_threshold\"] = 20\n        self.run_command(\"embedart\", \"-y\", \"-f\", self.abbey_similarpath)\n        mediafile = MediaFile(syspath(item.path))\n\n        assert mediafile.images[0].data == self.image_data, (\n            f\"Image written is not {displayable_path(self.abbey_similarpath)}\"\n        )\n\n    def test_non_ascii_album_path(self):\n        resource_path = os.path.join(_common.RSRC, b\"image.mp3\")\n        album = self.add_album_fixture()\n        trackpath = album.items()[0].path\n        shutil.copy(syspath(resource_path), syspath(trackpath))\n\n        self.run_command(\"extractart\", \"-n\", \"extracted\")\n\n        assert (album.filepath / \"extracted.png\").exists()\n\n    def test_extracted_extension(self):\n        resource_path = os.path.join(_common.RSRC, b\"image-jpeg.mp3\")\n        album = self.add_album_fixture()\n        trackpath = album.items()[0].path\n        shutil.copy(syspath(resource_path), syspath(trackpath))\n\n        self.run_command(\"extractart\", \"-n\", \"extracted\")\n\n        assert (album.filepath / \"extracted.jpg\").exists()\n\n    def test_clear_art_with_yes_input(self):\n        self._setup_data()\n        album = self.add_album_fixture()\n        item = album.items()[0]\n        self.io.addinput(\"y\")\n        self.run_command(\"embedart\", \"-f\", self.small_artpath)\n        embedded_time = os.path.getmtime(syspath(item.path))\n\n        self.io.addinput(\"y\")\n        self.run_command(\"clearart\")\n        mediafile = MediaFile(syspath(item.path))\n        assert not mediafile.images\n        clear_time = os.path.getmtime(syspath(item.path))\n        assert clear_time > embedded_time\n\n        # A run on a file without an image should not be modified\n        self.io.addinput(\"y\")\n        self.run_command(\"clearart\")\n        no_clear_time = os.path.getmtime(syspath(item.path))\n        assert no_clear_time == clear_time\n\n    def test_clear_art_with_no_input(self):\n        self._setup_data()\n        album = self.add_album_fixture()\n        item = album.items()[0]\n        self.io.addinput(\"y\")\n        self.run_command(\"embedart\", \"-f\", self.small_artpath)\n        self.io.addinput(\"n\")\n        self.run_command(\"clearart\")\n        mediafile = MediaFile(syspath(item.path))\n        assert mediafile.images[0].data == self.image_data\n\n    def test_embed_art_from_url_with_yes_input(self):\n        self._setup_data()\n        album = self.add_album_fixture()\n        item = album.items()[0]\n        self.mock_response(\"http://example.com/test.jpg\", \"image/jpeg\")\n        self.io.addinput(\"y\")\n        self.run_command(\"embedart\", \"-u\", \"http://example.com/test.jpg\")\n        mediafile = MediaFile(syspath(item.path))\n        assert mediafile.images[0].data == self.IMAGEHEADER.get(\n            \"image/jpeg\"\n        ).ljust(32, b\"\\x00\")\n\n    def test_embed_art_from_url_png(self):\n        self._setup_data()\n        album = self.add_album_fixture()\n        item = album.items()[0]\n        self.mock_response(\"http://example.com/test.png\", \"image/png\")\n        self.run_command(\"embedart\", \"-y\", \"-u\", \"http://example.com/test.png\")\n        mediafile = MediaFile(syspath(item.path))\n        assert mediafile.images[0].data == self.IMAGEHEADER.get(\n            \"image/png\"\n        ).ljust(32, b\"\\x00\")\n\n    def test_embed_art_from_url_not_image(self):\n        self._setup_data()\n        album = self.add_album_fixture()\n        item = album.items()[0]\n        self.mock_response(\"http://example.com/test.html\", \"text/html\")\n        self.run_command(\"embedart\", \"-y\", \"-u\", \"http://example.com/test.html\")\n        mediafile = MediaFile(syspath(item.path))\n        assert not mediafile.images\n\n    def test_clearart_on_import_disabled(self):\n        file_path = self.create_mediafile_fixture(\n            images=[\"jpg\"], target_dir=self.import_path\n        )\n        self.import_media.append(file_path)\n        with self.configure_plugin({\"clearart_on_import\": False}):\n            importer = self.setup_importer(autotag=False, write=True)\n            importer.run()\n\n        item = self.lib.items()[0]\n        assert MediaFile(os.path.join(item.path)).images\n\n    def test_clearart_on_import_enabled(self):\n        file_path = self.create_mediafile_fixture(\n            images=[\"jpg\"], target_dir=self.import_path\n        )\n        self.import_media.append(file_path)\n        # Force re-init the plugin to register the listener\n        self.unload_plugins()\n        with self.configure_plugin({\"clearart_on_import\": True}):\n            importer = self.setup_importer(autotag=False, write=True)\n            importer.run()\n\n        item = self.lib.items()[0]\n        assert not MediaFile(os.path.join(item.path)).images\n\n\nclass DummyArtResizer(ArtResizer):\n    \"\"\"An `ArtResizer` which pretends that ImageMagick is available, and has\n    a sufficiently recent version to support image comparison.\n    \"\"\"\n\n    def __init__(self):\n        self.local_method = DummyIMBackend()\n\n\n@patch(\"beets.util.artresizer.subprocess\")\n@patch(\"beetsplug._utils.art.extract\")\nclass ArtSimilarityTest(unittest.TestCase):\n    def setUp(self):\n        self.item = _common.item()\n        self.log = logging.getLogger(\"beets.embedart\")\n        self.artresizer = DummyArtResizer()\n\n    def _similarity(self, threshold):\n        return art.check_art_similarity(\n            self.log,\n            self.item,\n            b\"path\",\n            threshold,\n            artresizer=self.artresizer,\n        )\n\n    def _popen(self, status=0, stdout=\"\", stderr=\"\"):\n        \"\"\"Create a mock `Popen` object.\"\"\"\n        popen = MagicMock(returncode=status)\n        popen.communicate.return_value = stdout, stderr\n        return popen\n\n    def _mock_popens(\n        self,\n        mock_extract,\n        mock_subprocess,\n        compare_status=0,\n        compare_stdout=b\"\",\n        compare_stderr=b\"\",\n        convert_status=0,\n    ):\n        mock_extract.return_value = b\"extracted_path\"\n        mock_subprocess.Popen.side_effect = [\n            # The `convert` call.\n            self._popen(convert_status),\n            # The `compare` call.\n            self._popen(compare_status, compare_stdout, compare_stderr),\n        ]\n\n    def test_compare_success_similar(self, mock_extract, mock_subprocess):\n        self._mock_popens(mock_extract, mock_subprocess, 0, b\"10\", b\"err\")\n        assert self._similarity(20)\n\n    def test_compare_success_different(self, mock_extract, mock_subprocess):\n        self._mock_popens(mock_extract, mock_subprocess, 0, b\"10\", b\"err\")\n        assert not self._similarity(5)\n\n    def test_compare_status1_similar(self, mock_extract, mock_subprocess):\n        self._mock_popens(mock_extract, mock_subprocess, 1, b\"out\", b\"10\")\n        assert self._similarity(20)\n\n    def test_compare_status1_different(self, mock_extract, mock_subprocess):\n        self._mock_popens(mock_extract, mock_subprocess, 1, b\"out\", b\"10\")\n        assert not self._similarity(5)\n\n    def test_compare_failed(self, mock_extract, mock_subprocess):\n        self._mock_popens(mock_extract, mock_subprocess, 2, b\"out\", b\"10\")\n        assert self._similarity(20) is None\n\n    def test_compare_parsing_error(self, mock_extract, mock_subprocess):\n        self._mock_popens(mock_extract, mock_subprocess, 0, b\"foo\", b\"bar\")\n        assert self._similarity(20) is None\n\n    def test_compare_parsing_error_and_failure(\n        self, mock_extract, mock_subprocess\n    ):\n        self._mock_popens(mock_extract, mock_subprocess, 1, b\"foo\", b\"bar\")\n        assert self._similarity(20) is None\n\n    def test_convert_failure(self, mock_extract, mock_subprocess):\n        self._mock_popens(mock_extract, mock_subprocess, convert_status=1)\n        assert self._similarity(20) is None\n"
  },
  {
    "path": "test/plugins/test_embyupdate.py",
    "content": "import responses\n\nfrom beets.test.helper import PluginTestCase\nfrom beetsplug import embyupdate\n\n\nclass EmbyUpdateTest(PluginTestCase):\n    plugin = \"embyupdate\"\n\n    def setUp(self):\n        super().setUp()\n\n        self.config[\"emby\"] = {\n            \"host\": \"localhost\",\n            \"port\": 8096,\n            \"username\": \"username\",\n            \"password\": \"password\",\n        }\n\n    def test_api_url_only_name(self):\n        assert (\n            embyupdate.api_url(\n                self.config[\"emby\"][\"host\"].get(),\n                self.config[\"emby\"][\"port\"].get(),\n                \"/Library/Refresh\",\n            )\n            == \"http://localhost:8096/Library/Refresh?format=json\"\n        )\n\n    def test_api_url_http(self):\n        assert (\n            embyupdate.api_url(\n                \"http://localhost\",\n                self.config[\"emby\"][\"port\"].get(),\n                \"/Library/Refresh\",\n            )\n            == \"http://localhost:8096/Library/Refresh?format=json\"\n        )\n\n    def test_api_url_https(self):\n        assert (\n            embyupdate.api_url(\n                \"https://localhost\",\n                self.config[\"emby\"][\"port\"].get(),\n                \"/Library/Refresh\",\n            )\n            == \"https://localhost:8096/Library/Refresh?format=json\"\n        )\n\n    def test_password_data(self):\n        assert embyupdate.password_data(\n            self.config[\"emby\"][\"username\"].get(),\n            self.config[\"emby\"][\"password\"].get(),\n        ) == {\n            \"username\": \"username\",\n            \"password\": \"5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8\",\n            \"passwordMd5\": \"5f4dcc3b5aa765d61d8327deb882cf99\",\n        }\n\n    def test_create_header_no_token(self):\n        assert embyupdate.create_headers(\n            \"e8837bc1-ad67-520e-8cd2-f629e3155721\"\n        ) == {\n            \"x-emby-authorization\": (\n                \"MediaBrowser \"\n                'UserId=\"e8837bc1-ad67-520e-8cd2-f629e3155721\", '\n                'Client=\"other\", '\n                'Device=\"beets\", '\n                'DeviceId=\"beets\", '\n                'Version=\"0.0.0\"'\n            )\n        }\n\n    def test_create_header_with_token(self):\n        assert embyupdate.create_headers(\n            \"e8837bc1-ad67-520e-8cd2-f629e3155721\", token=\"abc123\"\n        ) == {\n            \"x-emby-authorization\": (\n                \"MediaBrowser \"\n                'UserId=\"e8837bc1-ad67-520e-8cd2-f629e3155721\", '\n                'Client=\"other\", '\n                'Device=\"beets\", '\n                'DeviceId=\"beets\", '\n                'Version=\"0.0.0\"'\n            ),\n            \"x-mediabrowser-token\": \"abc123\",\n        }\n\n    @responses.activate\n    def test_get_token(self):\n        body = (\n            '{\"User\":{\"Name\":\"username\", '\n            '\"ServerId\":\"1efa5077976bfa92bc71652404f646ec\",'\n            '\"Id\":\"2ec276a2642e54a19b612b9418a8bd3b\",\"HasPassword\":true,'\n            '\"HasConfiguredPassword\":true,'\n            '\"HasConfiguredEasyPassword\":false,'\n            '\"LastLoginDate\":\"2015-11-09T08:35:03.6357440Z\",'\n            '\"LastActivityDate\":\"2015-11-09T08:35:03.6665060Z\",'\n            '\"Configuration\":{\"AudioLanguagePreference\":\"\",'\n            '\"PlayDefaultAudioTrack\":true,\"SubtitleLanguagePreference\":\"\",'\n            '\"DisplayMissingEpisodes\":false,'\n            '\"DisplayUnairedEpisodes\":false,'\n            '\"GroupMoviesIntoBoxSets\":false,'\n            '\"DisplayChannelsWithinViews\":[],'\n            '\"ExcludeFoldersFromGrouping\":[],\"GroupedFolders\":[],'\n            '\"SubtitleMode\":\"Default\",\"DisplayCollectionsView\":true,'\n            '\"DisplayFoldersView\":false,\"EnableLocalPassword\":false,'\n            '\"OrderedViews\":[],\"IncludeTrailersInSuggestions\":true,'\n            '\"EnableCinemaMode\":true,\"LatestItemsExcludes\":[],'\n            '\"PlainFolderViews\":[],\"HidePlayedInLatest\":true,'\n            '\"DisplayChannelsInline\":false},'\n            '\"Policy\":{\"IsAdministrator\":true,\"IsHidden\":false,'\n            '\"IsDisabled\":false,\"BlockedTags\":[],'\n            '\"EnableUserPreferenceAccess\":true,\"AccessSchedules\":[],'\n            '\"BlockUnratedItems\":[],'\n            '\"EnableRemoteControlOfOtherUsers\":false,'\n            '\"EnableSharedDeviceControl\":true,'\n            '\"EnableLiveTvManagement\":true,\"EnableLiveTvAccess\":true,'\n            '\"EnableMediaPlayback\":true,'\n            '\"EnableAudioPlaybackTranscoding\":true,'\n            '\"EnableVideoPlaybackTranscoding\":true,'\n            '\"EnableContentDeletion\":false,'\n            '\"EnableContentDownloading\":true,\"EnableSync\":true,'\n            '\"EnableSyncTranscoding\":true,\"EnabledDevices\":[],'\n            '\"EnableAllDevices\":true,\"EnabledChannels\":[],'\n            '\"EnableAllChannels\":true,\"EnabledFolders\":[],'\n            '\"EnableAllFolders\":true,\"InvalidLoginAttemptCount\":0,'\n            '\"EnablePublicSharing\":true}},'\n            '\"SessionInfo\":{\"SupportedCommands\":[],'\n            '\"QueueableMediaTypes\":[],\"PlayableMediaTypes\":[],'\n            '\"Id\":\"89f3b33f8b3a56af22088733ad1d76b3\",'\n            '\"UserId\":\"2ec276a2642e54a19b612b9418a8bd3b\",'\n            '\"UserName\":\"username\",\"AdditionalUsers\":[],'\n            '\"ApplicationVersion\":\"Unknown version\",'\n            '\"Client\":\"Unknown app\",'\n            '\"LastActivityDate\":\"2015-11-09T08:35:03.6665060Z\",'\n            '\"DeviceName\":\"Unknown device\",\"DeviceId\":\"Unknown device id\",'\n            '\"SupportsRemoteControl\":false,\"PlayState\":{\"CanSeek\":false,'\n            '\"IsPaused\":false,\"IsMuted\":false,\"RepeatMode\":\"RepeatNone\"}},'\n            '\"AccessToken\":\"4b19180cf02748f7b95c7e8e76562fc8\",'\n            '\"ServerId\":\"1efa5077976bfa92bc71652404f646ec\"}'\n        )\n\n        responses.add(\n            responses.POST,\n            (\"http://localhost:8096/Users/AuthenticateByName\"),\n            body=body,\n            status=200,\n            content_type=\"application/json\",\n        )\n\n        headers = {\n            \"x-emby-authorization\": (\n                \"MediaBrowser \"\n                'UserId=\"e8837bc1-ad67-520e-8cd2-f629e3155721\", '\n                'Client=\"other\", '\n                'Device=\"beets\", '\n                'DeviceId=\"beets\", '\n                'Version=\"0.0.0\"'\n            )\n        }\n\n        auth_data = {\n            \"username\": \"username\",\n            \"password\": \"5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8\",\n            \"passwordMd5\": \"5f4dcc3b5aa765d61d8327deb882cf99\",\n        }\n\n        assert (\n            embyupdate.get_token(\"http://localhost\", 8096, headers, auth_data)\n            == \"4b19180cf02748f7b95c7e8e76562fc8\"\n        )\n\n    @responses.activate\n    def test_get_user(self):\n        body = (\n            '[{\"Name\":\"username\",'\n            '\"ServerId\":\"1efa5077976bfa92bc71652404f646ec\",'\n            '\"Id\":\"2ec276a2642e54a19b612b9418a8bd3b\",\"HasPassword\":true,'\n            '\"HasConfiguredPassword\":true,'\n            '\"HasConfiguredEasyPassword\":false,'\n            '\"LastLoginDate\":\"2015-11-09T08:35:03.6357440Z\",'\n            '\"LastActivityDate\":\"2015-11-09T08:42:39.3693220Z\",'\n            '\"Configuration\":{\"AudioLanguagePreference\":\"\",'\n            '\"PlayDefaultAudioTrack\":true,\"SubtitleLanguagePreference\":\"\",'\n            '\"DisplayMissingEpisodes\":false,'\n            '\"DisplayUnairedEpisodes\":false,'\n            '\"GroupMoviesIntoBoxSets\":false,'\n            '\"DisplayChannelsWithinViews\":[],'\n            '\"ExcludeFoldersFromGrouping\":[],\"GroupedFolders\":[],'\n            '\"SubtitleMode\":\"Default\",\"DisplayCollectionsView\":true,'\n            '\"DisplayFoldersView\":false,\"EnableLocalPassword\":false,'\n            '\"OrderedViews\":[],\"IncludeTrailersInSuggestions\":true,'\n            '\"EnableCinemaMode\":true,\"LatestItemsExcludes\":[],'\n            '\"PlainFolderViews\":[],\"HidePlayedInLatest\":true,'\n            '\"DisplayChannelsInline\":false},'\n            '\"Policy\":{\"IsAdministrator\":true,\"IsHidden\":false,'\n            '\"IsDisabled\":false,\"BlockedTags\":[],'\n            '\"EnableUserPreferenceAccess\":true,\"AccessSchedules\":[],'\n            '\"BlockUnratedItems\":[],'\n            '\"EnableRemoteControlOfOtherUsers\":false,'\n            '\"EnableSharedDeviceControl\":true,'\n            '\"EnableLiveTvManagement\":true,\"EnableLiveTvAccess\":true,'\n            '\"EnableMediaPlayback\":true,'\n            '\"EnableAudioPlaybackTranscoding\":true,'\n            '\"EnableVideoPlaybackTranscoding\":true,'\n            '\"EnableContentDeletion\":false,'\n            '\"EnableContentDownloading\":true,'\n            '\"EnableSync\":true,\"EnableSyncTranscoding\":true,'\n            '\"EnabledDevices\":[],\"EnableAllDevices\":true,'\n            '\"EnabledChannels\":[],\"EnableAllChannels\":true,'\n            '\"EnabledFolders\":[],\"EnableAllFolders\":true,'\n            '\"InvalidLoginAttemptCount\":0,\"EnablePublicSharing\":true}}]'\n        )\n\n        responses.add(\n            responses.GET,\n            \"http://localhost:8096/Users/Public\",\n            body=body,\n            status=200,\n            content_type=\"application/json\",\n        )\n\n        response = embyupdate.get_user(\"http://localhost\", 8096, \"username\")\n\n        assert response[0][\"Id\"] == \"2ec276a2642e54a19b612b9418a8bd3b\"\n\n        assert response[0][\"Name\"] == \"username\"\n"
  },
  {
    "path": "test/plugins/test_export.py",
    "content": "# This file is part of beets.\n# Copyright 2019, Carl Suster\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Test the beets.export utilities associated with the export plugin.\"\"\"\n\nimport json\nimport re  # used to test csv format\nfrom xml.etree import ElementTree\nfrom xml.etree.ElementTree import Element\n\nfrom beets.test.helper import IOMixin, PluginTestCase\n\n\nclass ExportPluginTest(IOMixin, PluginTestCase):\n    plugin = \"export\"\n\n    def setUp(self):\n        super().setUp()\n        self.test_values = {\"title\": \"xtitle\", \"album\": \"xalbum\"}\n\n    def execute_command(self, format_type, artist):\n        query = \",\".join(self.test_values.keys())\n        out = self.run_with_output(\n            \"export\", \"-f\", format_type, \"-i\", query, artist\n        )\n        return out\n\n    def create_item(self):\n        (item,) = self.add_item_fixtures()\n        item.artist = \"xartist\"\n        item.title = self.test_values[\"title\"]\n        item.album = self.test_values[\"album\"]\n        item.write()\n        item.store()\n        return item\n\n    def test_json_output(self):\n        item1 = self.create_item()\n        out = self.execute_command(format_type=\"json\", artist=item1.artist)\n        json_data = json.loads(out)[0]\n        for key, val in self.test_values.items():\n            assert key in json_data\n            assert val == json_data[key]\n\n    def test_jsonlines_output(self):\n        item1 = self.create_item()\n        out = self.execute_command(format_type=\"jsonlines\", artist=item1.artist)\n        json_data = json.loads(out)\n        for key, val in self.test_values.items():\n            assert key in json_data\n            assert val == json_data[key]\n\n    def test_csv_output(self):\n        item1 = self.create_item()\n        out = self.execute_command(format_type=\"csv\", artist=item1.artist)\n        csv_list = re.split(\"\\r\", re.sub(\"\\n\", \"\", out))\n        head = re.split(\",\", csv_list[0])\n        vals = re.split(\",|\\r\", csv_list[1])\n        for index, column in enumerate(head):\n            assert self.test_values.get(column, None) is not None\n            assert vals[index] == self.test_values[column]\n\n    def test_xml_output(self):\n        item1 = self.create_item()\n        out = self.execute_command(format_type=\"xml\", artist=item1.artist)\n        library = ElementTree.fromstring(out)\n        assert isinstance(library, Element)\n        for track in library[0]:\n            for details in track:\n                tag = details.tag\n                txt = details.text\n                assert tag in self.test_values, tag\n                assert self.test_values[tag] == txt, txt\n"
  },
  {
    "path": "test/plugins/test_fetchart.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Thomas Scholtes.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nimport ctypes\nimport os\nimport sys\n\nfrom beets import util\nfrom beets.test.helper import IOMixin, PluginTestCase\n\n\nclass FetchartCliTest(IOMixin, PluginTestCase):\n    plugin = \"fetchart\"\n\n    def setUp(self):\n        super().setUp()\n        self.config[\"fetchart\"][\"cover_names\"] = \"c\\xc3\\xb6ver.jpg\"\n        self.config[\"art_filename\"] = \"mycover\"\n        self.album = self.add_album()\n        self.cover_path = os.path.join(self.album.path, b\"mycover.jpg\")\n\n    def check_cover_is_stored(self):\n        assert self.album[\"artpath\"] == self.cover_path\n        with open(util.syspath(self.cover_path)) as f:\n            assert f.read() == \"IMAGE\"\n\n    def hide_file_windows(self):\n        hidden_mask = 2\n        success = ctypes.windll.kernel32.SetFileAttributesW(\n            self.cover_path, hidden_mask\n        )\n        if not success:\n            self.skipTest(\"unable to set file attributes\")\n\n    def test_set_art_from_folder(self):\n        self.touch(b\"c\\xc3\\xb6ver.jpg\", dir=self.album.path, content=\"IMAGE\")\n\n        self.run_command(\"fetchart\")\n\n        self.album.load()\n        self.check_cover_is_stored()\n\n    def test_filesystem_does_not_pick_up_folder(self):\n        os.makedirs(os.path.join(self.album.path, b\"mycover.jpg\"))\n        self.run_command(\"fetchart\")\n        self.album.load()\n        assert self.album[\"artpath\"] is None\n\n    def test_filesystem_does_not_pick_up_ignored_file(self):\n        self.touch(b\"co_ver.jpg\", dir=self.album.path, content=\"IMAGE\")\n        self.config[\"ignore\"] = [\"*_*\"]\n        self.run_command(\"fetchart\")\n        self.album.load()\n        assert self.album[\"artpath\"] is None\n\n    def test_filesystem_picks_up_non_ignored_file(self):\n        self.touch(b\"cover.jpg\", dir=self.album.path, content=\"IMAGE\")\n        self.config[\"ignore\"] = [\"*_*\"]\n        self.run_command(\"fetchart\")\n        self.album.load()\n        self.check_cover_is_stored()\n\n    def test_filesystem_does_not_pick_up_hidden_file(self):\n        self.touch(b\".cover.jpg\", dir=self.album.path, content=\"IMAGE\")\n        if sys.platform == \"win32\":\n            self.hide_file_windows()\n        self.config[\"ignore\"] = []  # By default, ignore includes '.*'.\n        self.config[\"ignore_hidden\"] = True\n        self.run_command(\"fetchart\")\n        self.album.load()\n        assert self.album[\"artpath\"] is None\n\n    def test_filesystem_picks_up_non_hidden_file(self):\n        self.touch(b\"cover.jpg\", dir=self.album.path, content=\"IMAGE\")\n        self.config[\"ignore_hidden\"] = True\n        self.run_command(\"fetchart\")\n        self.album.load()\n        self.check_cover_is_stored()\n\n    def test_filesystem_picks_up_hidden_file(self):\n        self.touch(b\".cover.jpg\", dir=self.album.path, content=\"IMAGE\")\n        if sys.platform == \"win32\":\n            self.hide_file_windows()\n        self.config[\"ignore\"] = []  # By default, ignore includes '.*'.\n        self.config[\"ignore_hidden\"] = False\n        self.run_command(\"fetchart\")\n        self.album.load()\n        self.check_cover_is_stored()\n\n    def test_colorization(self):\n        self.config[\"ui\"][\"color\"] = True\n        out = self.run_with_output(\"fetchart\")\n        assert \" - the älbum: \\x1b[1;31mno art found\\x1b[39;49;00m\\n\" == out\n"
  },
  {
    "path": "test/plugins/test_filefilter.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Malte Ried.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for the `filefilter` plugin.\"\"\"\n\nfrom beets.test.helper import ImportTestCase, PluginMixin\nfrom beets.util import bytestring_path\n\n\nclass FileFilterPluginMixin(PluginMixin, ImportTestCase):\n    plugin = \"filefilter\"\n    preload_plugin = False\n\n    def setUp(self):\n        super().setUp()\n        self.prepare_tracks_for_import()\n\n    def prepare_tracks_for_import(self):\n        self.album_track, self.other_album_track, self.single_track = (\n            bytestring_path(self.prepare_album_for_import(1, album_path=p)[0])\n            for p in [\n                self.import_path / \"album\",\n                self.import_path / \"other_album\",\n                self.import_path,\n            ]\n        )\n        self.all_tracks = {\n            self.album_track,\n            self.other_album_track,\n            self.single_track,\n        }\n\n    def _run(self, config, expected_album_count, expected_paths):\n        with self.configure_plugin(config):\n            self.importer.run()\n\n        assert len(self.lib.albums()) == expected_album_count\n        assert {i.path for i in self.lib.items()} == expected_paths\n\n\nclass FileFilterPluginNonSingletonTest(FileFilterPluginMixin):\n    def setUp(self):\n        super().setUp()\n        self.importer = self.setup_importer(autotag=False, copy=False)\n\n    def test_import_default(self):\n        \"\"\"The default configuration should import everything.\"\"\"\n        self._run({}, 3, self.all_tracks)\n\n    def test_import_nothing(self):\n        self._run({\"path\": \"not_there\"}, 0, set())\n\n    def test_global_config(self):\n        self._run(\n            {\"path\": \".*album.*\"},\n            2,\n            {self.album_track, self.other_album_track},\n        )\n\n    def test_album_config(self):\n        self._run(\n            {\"album_path\": \".*other_album.*\"},\n            1,\n            {self.other_album_track},\n        )\n\n    def test_singleton_config(self):\n        \"\"\"Check that singleton configuration is ignored for album import.\"\"\"\n        self._run({\"singleton_path\": \".*other_album.*\"}, 3, self.all_tracks)\n\n\nclass FileFilterPluginSingletonTest(FileFilterPluginMixin):\n    def setUp(self):\n        super().setUp()\n        self.importer = self.setup_singleton_importer(autotag=False, copy=False)\n\n    def test_global_config(self):\n        self._run(\n            {\"path\": \".*album.*\"}, 0, {self.album_track, self.other_album_track}\n        )\n\n    def test_album_config(self):\n        \"\"\"Check that album configuration is ignored for singleton import.\"\"\"\n        self._run({\"album_path\": \".*other_album.*\"}, 0, self.all_tracks)\n\n    def test_singleton_config(self):\n        self._run(\n            {\"singleton_path\": \".*other_album.*\"}, 0, {self.other_album_track}\n        )\n"
  },
  {
    "path": "test/plugins/test_fromfilename.py",
    "content": "# This file is part of beets.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for the fromfilename plugin.\"\"\"\n\nimport pytest\n\nfrom beetsplug import fromfilename\n\n\nclass Session:\n    pass\n\n\nclass Item:\n    def __init__(self, path):\n        self.path = path\n        self.track = 0\n        self.artist = \"\"\n        self.title = \"\"\n\n\nclass Task:\n    def __init__(self, items):\n        self.items = items\n        self.is_album = True\n\n\n@pytest.mark.parametrize(\n    \"song1, song2\",\n    [\n        (\n            (\n                \"/tmp/01 - The Artist - Song One.m4a\",\n                1,\n                \"The Artist\",\n                \"Song One\",\n            ),\n            (\n                \"/tmp/02. - The Artist - Song Two.m4a\",\n                2,\n                \"The Artist\",\n                \"Song Two\",\n            ),\n        ),\n        (\n            (\"/tmp/01-The_Artist-Song_One.m4a\", 1, \"The_Artist\", \"Song_One\"),\n            (\"/tmp/02.-The_Artist-Song_Two.m4a\", 2, \"The_Artist\", \"Song_Two\"),\n        ),\n        (\n            (\"/tmp/01 - Song_One.m4a\", 1, \"\", \"Song_One\"),\n            (\"/tmp/02. - Song_Two.m4a\", 2, \"\", \"Song_Two\"),\n        ),\n        (\n            (\"/tmp/Song One by The Artist.m4a\", 0, \"The Artist\", \"Song One\"),\n            (\"/tmp/Song Two by The Artist.m4a\", 0, \"The Artist\", \"Song Two\"),\n        ),\n        ((\"/tmp/01.m4a\", 1, \"\", \"01\"), (\"/tmp/02.m4a\", 2, \"\", \"02\")),\n        (\n            (\"/tmp/Song One.m4a\", 0, \"\", \"Song One\"),\n            (\"/tmp/Song Two.m4a\", 0, \"\", \"Song Two\"),\n        ),\n    ],\n)\ndef test_fromfilename(song1, song2):\n    \"\"\"\n    Each \"song\" is a tuple of path, expected track number, expected artist,\n    expected title.\n\n    We use two songs for each test for two reasons:\n    - The plugin needs more than one item to look for uniform strings in paths\n      in order to guess if the string describes an artist or a title.\n    - Sometimes we allow for an optional \".\" after the track number in paths.\n    \"\"\"\n\n    session = Session()\n    item1 = Item(song1[0])\n    item2 = Item(song2[0])\n    task = Task([item1, item2])\n\n    f = fromfilename.FromFilenamePlugin()\n    f.filename_task(task, session)\n\n    assert task.items[0].track == song1[1]\n    assert task.items[0].artist == song1[2]\n    assert task.items[0].title == song1[3]\n    assert task.items[1].track == song2[1]\n    assert task.items[1].artist == song2[2]\n    assert task.items[1].title == song2[3]\n"
  },
  {
    "path": "test/plugins/test_ftintitle.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Fabrice Laporte.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for the 'ftintitle' plugin.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING, TypeAlias\n\nimport pytest\n\nfrom beets.library.models import Album\nfrom beets.test.helper import PluginTestCase\nfrom beetsplug import ftintitle\n\nif TYPE_CHECKING:\n    from collections.abc import Generator\n\n    from beets.library.models import Item\n\nConfigValue: TypeAlias = str | bool | list[str]\n\n\nclass FtInTitlePluginFunctional(PluginTestCase):\n    plugin = \"ftintitle\"\n\n\n@pytest.fixture\ndef env() -> Generator[FtInTitlePluginFunctional, None, None]:\n    case = FtInTitlePluginFunctional(methodName=\"runTest\")\n    case.setUp()\n    try:\n        yield case\n    finally:\n        case.tearDown()\n\n\ndef set_config(\n    env: FtInTitlePluginFunctional,\n    cfg: dict[str, ConfigValue] | None,\n) -> None:\n    cfg = {} if cfg is None else cfg\n    defaults = {\n        \"drop\": False,\n        \"auto\": True,\n        \"keep_in_artist\": False,\n        \"custom_words\": [],\n    }\n    env.config[\"ftintitle\"].set(defaults)\n    env.config[\"ftintitle\"].set(cfg)\n\n\ndef add_item(\n    env: FtInTitlePluginFunctional,\n    path: str,\n    artist: str,\n    title: str,\n    albumartist: str | None,\n) -> Item:\n    return env.add_item(\n        path=path,\n        artist=artist,\n        artist_sort=artist,\n        title=title,\n        albumartist=albumartist,\n    )\n\n\n@pytest.mark.parametrize(\n    \"cfg, cmd_args, given, expected\",\n    [\n        pytest.param(\n            None,\n            (\"ftintitle\",),\n            (\"Alice\", \"Song 1\", \"Alice\"),\n            (\"Alice\", \"Song 1\"),\n            id=\"no-featured-artist\",\n        ),\n        pytest.param(\n            {\"format\": \"feat {0}\"},\n            (\"ftintitle\",),\n            (\"Alice ft. Bob\", \"Song 1\", None),\n            (\"Alice\", \"Song 1 feat Bob\"),\n            id=\"no-albumartist-custom-format\",\n        ),\n        pytest.param(\n            None,\n            (\"ftintitle\",),\n            (\"Alice\", \"Song 1\", None),\n            (\"Alice\", \"Song 1\"),\n            id=\"no-albumartist-no-feature\",\n        ),\n        pytest.param(\n            {\"format\": \"featuring {0}\"},\n            (\"ftintitle\",),\n            (\"Alice ft Bob\", \"Song 1\", \"George\"),\n            (\"Alice\", \"Song 1 featuring Bob\"),\n            id=\"guest-artist-custom-format\",\n        ),\n        pytest.param(\n            None,\n            (\"ftintitle\",),\n            (\"Alice\", \"Song 1\", \"George\"),\n            (\"Alice\", \"Song 1\"),\n            id=\"guest-artist-no-feature\",\n        ),\n        # ---- drop (-d) variants ----\n        pytest.param(\n            None,\n            (\"ftintitle\", \"-d\"),\n            (\"Alice ft Bob\", \"Song 1\", \"Alice\"),\n            (\"Alice\", \"Song 1\"),\n            id=\"drop-self-ft\",\n        ),\n        pytest.param(\n            None,\n            (\"ftintitle\", \"-d\"),\n            (\"Alice\", \"Song 1\", \"Alice\"),\n            (\"Alice\", \"Song 1\"),\n            id=\"drop-self-no-ft\",\n        ),\n        pytest.param(\n            None,\n            (\"ftintitle\", \"-d\"),\n            (\"Alice ft Bob\", \"Song 1\", \"George\"),\n            (\"Alice\", \"Song 1\"),\n            id=\"drop-guest-ft\",\n        ),\n        pytest.param(\n            None,\n            (\"ftintitle\", \"-d\"),\n            (\"Alice\", \"Song 1\", \"George\"),\n            (\"Alice\", \"Song 1\"),\n            id=\"drop-guest-no-ft\",\n        ),\n        # ---- custom format variants ----\n        pytest.param(\n            {\"format\": \"feat. {}\"},\n            (\"ftintitle\",),\n            (\"Alice ft Bob\", \"Song 1\", \"Alice\"),\n            (\"Alice\", \"Song 1 feat. Bob\"),\n            id=\"custom-format-feat-dot\",\n        ),\n        pytest.param(\n            {\"format\": \"featuring {}\"},\n            (\"ftintitle\",),\n            (\"Alice feat. Bob\", \"Song 1\", \"Alice\"),\n            (\"Alice\", \"Song 1 featuring Bob\"),\n            id=\"custom-format-featuring\",\n        ),\n        pytest.param(\n            {\"format\": \"with {}\"},\n            (\"ftintitle\",),\n            (\"Alice feat Bob\", \"Song 1\", \"Alice\"),\n            (\"Alice\", \"Song 1 with Bob\"),\n            id=\"custom-format-with\",\n        ),\n        # ---- keep_in_artist variants ----\n        pytest.param(\n            {\"format\": \"feat. {}\", \"keep_in_artist\": True},\n            (\"ftintitle\",),\n            (\"Alice ft Bob\", \"Song 1\", \"Alice\"),\n            (\"Alice ft Bob\", \"Song 1 feat. Bob\"),\n            id=\"keep-in-artist-add-to-title\",\n        ),\n        pytest.param(\n            {\"format\": \"feat. {}\", \"keep_in_artist\": True},\n            (\"ftintitle\", \"-d\"),\n            (\"Alice ft Bob\", \"Song 1\", \"Alice\"),\n            (\"Alice ft Bob\", \"Song 1\"),\n            id=\"keep-in-artist-drop-from-title\",\n        ),\n        # ---- custom_words variants ----\n        pytest.param(\n            {\"format\": \"featuring {}\", \"custom_words\": [\"med\"]},\n            (\"ftintitle\",),\n            (\"Alice med Bob\", \"Song 1\", \"Alice\"),\n            (\"Alice\", \"Song 1 featuring Bob\"),\n            id=\"custom-feat-words\",\n        ),\n        pytest.param(\n            {\n                \"format\": \"featuring {}\",\n                \"keep_in_artist\": True,\n                \"custom_words\": [\"med\"],\n            },\n            (\"ftintitle\",),\n            (\"Alice med Bob\", \"Song 1\", \"Alice\"),\n            (\"Alice med Bob\", \"Song 1 featuring Bob\"),\n            id=\"custom-feat-words-keep-in-artists\",\n        ),\n        pytest.param(\n            {\n                \"format\": \"featuring {}\",\n                \"keep_in_artist\": True,\n                \"custom_words\": [\"med\"],\n            },\n            (\n                \"ftintitle\",\n                \"-d\",\n            ),\n            (\"Alice med Bob\", \"Song 1\", \"Alice\"),\n            (\"Alice med Bob\", \"Song 1\"),\n            id=\"custom-feat-words-keep-in-artists-drop-from-title\",\n        ),\n        # ---- preserve_album_artist variants ----\n        pytest.param(\n            {\n                \"format\": \"feat. {}\",\n                \"preserve_album_artist\": True,\n            },\n            (\"ftintitle\",),\n            (\"Alice feat. Bob\", \"Song 1\", \"Alice\"),\n            (\"Alice\", \"Song 1 feat. Bob\"),\n            id=\"skip-if-artist-and-album-artists-is-the-same-different-match\",\n        ),\n        pytest.param(\n            {\n                \"format\": \"feat. {}\",\n                \"preserve_album_artist\": False,\n            },\n            (\"ftintitle\",),\n            (\"Alice feat. Bob\", \"Song 1\", \"Alice\"),\n            (\"Alice\", \"Song 1 feat. Bob\"),\n            id=\"skip-if-artist-and-album-artists-is-the-same-different-match-b\",\n        ),\n        pytest.param(\n            {\n                \"format\": \"feat. {}\",\n                \"preserve_album_artist\": True,\n            },\n            (\"ftintitle\",),\n            (\"Alice feat. Bob\", \"Song 1\", \"Alice feat. Bob\"),\n            (\"Alice feat. Bob\", \"Song 1\"),\n            id=\"skip-if-artist-and-album-artists-is-the-same-matching-match\",\n        ),\n        pytest.param(\n            {\n                \"format\": \"feat. {}\",\n                \"preserve_album_artist\": False,\n            },\n            (\"ftintitle\",),\n            (\"Alice feat. Bob\", \"Song 1\", \"Alice feat. Bob\"),\n            (\"Alice\", \"Song 1 feat. Bob\"),\n            id=\"skip-if-artist-and-album-artists-is-the-same-matching-match-b\",\n        ),\n        # ---- titles with brackets/parentheses ----\n        pytest.param(\n            {\"format\": \"ft. {}\", \"bracket_keywords\": [\"mix\"]},\n            (\"ftintitle\",),\n            (\"Alice ft. Bob\", \"Song 1 (Club Mix)\", \"Alice\"),\n            (\"Alice\", \"Song 1 ft. Bob (Club Mix)\"),\n            id=\"ft-inserted-before-matching-bracket-keyword\",\n        ),\n        pytest.param(\n            {\"format\": \"ft. {}\", \"bracket_keywords\": [\"nomatch\"]},\n            (\"ftintitle\",),\n            (\"Alice ft. Bob\", \"Song 1 (Club Remix)\", \"Alice\"),\n            (\"Alice\", \"Song 1 (Club Remix) ft. Bob\"),\n            id=\"ft-inserted-at-end-no-bracket-keyword-match\",\n        ),\n    ],\n)\ndef test_ftintitle_functional(\n    env: FtInTitlePluginFunctional,\n    cfg: dict[str, str | bool | list[str]] | None,\n    cmd_args: tuple[str, ...],\n    given: tuple[str, str, str | None],\n    expected: tuple[str, str],\n) -> None:\n    set_config(env, cfg)\n    ftintitle.FtInTitlePlugin()\n\n    artist, title, albumartist = given\n    item = add_item(env, \"/\", artist, title, albumartist)\n\n    env.run_command(*cmd_args)\n    item.load()\n\n    expected_artist, expected_title = expected\n    assert item[\"artist\"] == expected_artist\n    assert item[\"title\"] == expected_title\n\n\n@pytest.mark.parametrize(\n    \"artist,albumartist,expected\",\n    [\n        (\"Alice ft. Bob\", \"Alice\", \"Bob\"),\n        (\"Alice feat Bob\", \"Alice\", \"Bob\"),\n        (\"Alice featuring Bob\", \"Alice\", \"Bob\"),\n        (\"Alice & Bob\", \"Alice\", \"Bob\"),\n        (\"Alice and Bob\", \"Alice\", \"Bob\"),\n        (\"Alice With Bob\", \"Alice\", \"Bob\"),\n        (\"Alice defeat Bob\", \"Alice\", None),\n        (\"Alice & Bob\", \"Bob\", \"Alice\"),\n        (\"Alice ft. Bob\", \"Bob\", \"Alice\"),\n        (\"Alice ft. Carol\", \"Bob\", \"Carol\"),\n    ],\n)\ndef test_find_feat_part(\n    artist: str,\n    albumartist: str,\n    expected: str | None,\n) -> None:\n    assert ftintitle.find_feat_part(artist, albumartist) == expected\n\n\n@pytest.mark.parametrize(\n    \"given,expected\",\n    [\n        (\"Alice ft. Bob\", (\"Alice\", \"Bob\")),\n        (\"Alice feat Bob\", (\"Alice\", \"Bob\")),\n        (\"Alice feat. Bob\", (\"Alice\", \"Bob\")),\n        (\"Alice featuring Bob\", (\"Alice\", \"Bob\")),\n        (\"Alice & Bob\", (\"Alice\", \"Bob\")),\n        (\"Alice, Bob & Charlie\", (\"Alice\", \"Bob & Charlie\")),\n        (\n            \"Alice, Bob & Charlie feat. Xavier\",\n            (\"Alice, Bob & Charlie\", \"Xavier\"),\n        ),\n        (\"Alice and Bob\", (\"Alice\", \"Bob\")),\n        (\"Alice With Bob\", (\"Alice\", \"Bob\")),\n        (\"Alice defeat Bob\", (\"Alice defeat Bob\", None)),\n        (\"Alice & Bob feat Charlie\", (\"Alice & Bob\", \"Charlie\")),\n        (\"Alice & Bob ft. Charlie\", (\"Alice & Bob\", \"Charlie\")),\n        (\"Alice & Bob featuring Charlie\", (\"Alice & Bob\", \"Charlie\")),\n        (\"Alice and Bob feat Charlie\", (\"Alice and Bob\", \"Charlie\")),\n    ],\n)\ndef test_split_on_feat(\n    given: str,\n    expected: tuple[str, str | None],\n) -> None:\n    assert ftintitle.split_on_feat(given) == expected\n\n\n@pytest.mark.parametrize(\n    \"given,keywords,expected\",\n    [\n        ## default keywords\n        # different braces and keywords\n        (\"Song (Remix)\", None, \"Song ft. Bob (Remix)\"),\n        (\"Song [Version]\", None, \"Song ft. Bob [Version]\"),\n        (\"Song {Extended Mix}\", None, \"Song ft. Bob {Extended Mix}\"),\n        (\"Song <Instrumental>\", None, \"Song ft. Bob <Instrumental>\"),\n        # two keyword clauses\n        (\"Song (Remix) (Live)\", None, \"Song ft. Bob (Remix) (Live)\"),\n        # brace insensitivity\n        (\"Song (Live) [Remix]\", None, \"Song ft. Bob (Live) [Remix]\"),\n        (\"Song [Edit] (Remastered)\", None, \"Song ft. Bob [Edit] (Remastered)\"),\n        # negative cases\n        (\"Song\", None, \"Song ft. Bob\"),  # no clause\n        (\"Song (Arbitrary)\", None, \"Song (Arbitrary) ft. Bob\"),  # no keyword\n        (\"Song (\", None, \"Song ( ft. Bob\"),  # no matching brace or keyword\n        (\"Song (Live\", None, \"Song (Live ft. Bob\"),  # no matching brace with keyword\n        # one keyword clause, one non-keyword clause\n        (\"Song (Live) (Arbitrary)\", None, \"Song ft. Bob (Live) (Arbitrary)\"),\n        (\"Song (Arbitrary) (Remix)\", None, \"Song (Arbitrary) ft. Bob (Remix)\"),\n        # nested brackets - same type\n        (\"Song (Remix (Extended))\", None, \"Song ft. Bob (Remix (Extended))\"),\n        (\"Song [Arbitrary [Description]]\", None, \"Song [Arbitrary [Description]] ft. Bob\"),\n        # nested brackets - different types\n        (\"Song (Remix [Extended])\", None, \"Song ft. Bob (Remix [Extended])\"),\n        # nested - returns outer start position despite inner keyword\n        (\"Song [Arbitrary {Extended}]\", None, \"Song ft. Bob [Arbitrary {Extended}]\"),\n        (\"Song {Live <Arbitrary>}\", None, \"Song ft. Bob {Live <Arbitrary>}\"),\n        (\"Song <Remaster (Arbitrary)>\", None, \"Song ft. Bob <Remaster (Arbitrary)>\"),\n        (\"Song <Extended> [Live]\", None, \"Song ft. Bob <Extended> [Live]\"),\n        (\"Song (Version) <Live>\", None, \"Song ft. Bob (Version) <Live>\"),\n        (\"Song (Arbitrary [Description])\", None, \"Song (Arbitrary [Description]) ft. Bob\"),\n        (\"Song [Description (Arbitrary)]\", None, \"Song [Description (Arbitrary)] ft. Bob\"),\n        ## custom keywords\n        (\"Song (Live)\", [\"live\"], \"Song ft. Bob (Live)\"),\n        (\"Song (Concert)\", [\"concert\"], \"Song ft. Bob (Concert)\"),\n        (\"Song (Remix)\", [\"custom\"], \"Song (Remix) ft. Bob\"),\n        (\"Song (Custom)\", [\"custom\"], \"Song ft. Bob (Custom)\"),\n        (\"Song\", [], \"Song ft. Bob\"),\n        (\"Song (\", [], \"Song ( ft. Bob\"),\n        # Multi-word keyword tests\n        (\"Song (Club Mix)\", [\"club mix\"], \"Song ft. Bob (Club Mix)\"),  # Positive: matches multi-word\n        (\"Song (Club Remix)\", [\"club mix\"], \"Song (Club Remix) ft. Bob\"),  # Negative: no match\n    ],\n)  # fmt: skip\ndef test_insert_ft_into_title(\n    given: str,\n    keywords: list[str] | None,\n    expected: str,\n) -> None:\n    assert (\n        ftintitle.FtInTitlePlugin.insert_ft_into_title(\n            given, \"ft. Bob\", keywords\n        )\n        == expected\n    )\n\n\n@pytest.mark.parametrize(\n    \"given,expected\",\n    [\n        (\"Alice ft. Bob\", True),\n        (\"Alice feat. Bob\", True),\n        (\"Alice feat Bob\", True),\n        (\"Alice featuring Bob\", True),\n        (\"Alice (ft. Bob)\", True),\n        (\"Alice (feat. Bob)\", True),\n        (\"Alice [ft. Bob]\", True),\n        (\"Alice [feat. Bob]\", True),\n        (\"Alice defeat Bob\", False),\n        (\"Aliceft.Bob\", False),\n        (\"Alice (defeat Bob)\", False),\n        (\"Live and Let Go\", False),\n        (\"Come With Me\", False),\n    ],\n)\ndef test_contains_feat(given: str, expected: bool) -> None:\n    assert ftintitle.contains_feat(given) is expected\n\n\n@pytest.mark.parametrize(\n    \"given,custom_words,expected\",\n    [\n        (\"Alice ft. Bob\", [], True),\n        (\"Alice feat. Bob\", [], True),\n        (\"Alice feat Bob\", [], True),\n        (\"Alice featuring Bob\", [], True),\n        (\"Alice (ft. Bob)\", [], True),\n        (\"Alice (feat. Bob)\", [], True),\n        (\"Alice [ft. Bob]\", [], True),\n        (\"Alice [feat. Bob]\", [], True),\n        (\"Alice defeat Bob\", [], False),\n        (\"Aliceft.Bob\", [], False),\n        (\"Alice (defeat Bob)\", [], False),\n        (\"Live and Let Go\", [], False),\n        (\"Come With Me\", [], False),\n        (\"Alice x Bob\", [\"x\"], True),\n        (\"Alice x Bob\", [\"X\"], True),\n        (\"Alice och Xavier\", [\"x\"], False),\n        (\"Alice ft. Xavier\", [\"x\"], True),\n        (\"Alice med Carol\", [\"med\"], True),\n        (\"Alice med Carol\", [], False),\n    ],\n)\ndef test_custom_words(\n    given: str, custom_words: list[str] | None, expected: bool\n) -> None:\n    if custom_words is None:\n        custom_words = []\n    assert ftintitle.contains_feat(given, custom_words) is expected\n\n\ndef test_album_template_value(config):\n    config[\"ftintitle\"][\"custom_words\"] = []\n\n    album = Album()\n    album[\"albumartist\"] = \"Foo ft. Bar\"\n    assert ftintitle._album_artist_no_feat(album) == \"Foo\"\n\n    album[\"albumartist\"] = \"Foobar\"\n    assert ftintitle._album_artist_no_feat(album) == \"Foobar\"\n"
  },
  {
    "path": "test/plugins/test_fuzzy.py",
    "content": "# This file is part of beets.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for the fuzzy query plugin.\"\"\"\n\nimport pytest\n\nfrom beets.test.helper import PluginMixin, TestHelper\n\n\n@pytest.fixture\ndef helper(request):\n    helper = TestHelper()\n    helper.setup_beets()\n\n    request.instance.lib = helper.lib\n    request.instance.add_item = helper.add_item\n\n    yield\n\n    helper.teardown_beets()\n\n\n@pytest.mark.usefixtures(\"helper\")\nclass TestFuzzyPlugin(PluginMixin):\n    plugin = \"fuzzy\"\n\n    @pytest.mark.parametrize(\n        \"query,expected_titles\",\n        [\n            pytest.param(\"~foo\", [\"seafood\"], id=\"all-fields-substring\"),\n            pytest.param(\"title:~foo\", [\"seafood\"], id=\"field-substring\"),\n            pytest.param(\"~seafood\", [\"seafood\"], id=\"all-fields-equal-length\"),\n            pytest.param(\"~zzz\", [], id=\"all-fields-no-match\"),\n        ],\n    )\n    def test_fuzzy_queries(self, query, expected_titles):\n        self.add_item(title=\"seafood\", artist=\"alpha\")\n        self.add_item(title=\"bread\", artist=\"beta\")\n\n        with self.configure_plugin({}):\n            items = self.lib.items(query)\n\n        assert [item.title for item in items] == expected_titles\n"
  },
  {
    "path": "test/plugins/test_hook.py",
    "content": "# This file is part of beets.\n# Copyright 2015, Thomas Scholtes.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nfrom __future__ import annotations\n\nimport os\nimport sys\nimport unittest\nfrom contextlib import contextmanager\nfrom typing import TYPE_CHECKING, ClassVar\n\nfrom beets import plugins\nfrom beets.test.helper import PluginTestCase, capture_log\n\nif TYPE_CHECKING:\n    from collections.abc import Callable, Iterator\n\n\nclass HookTestCase(PluginTestCase):\n    plugin = \"hook\"\n    preload_plugin = False\n\n    def _get_hook(self, event: str, command: str) -> dict[str, str]:\n        return {\"event\": event, \"command\": command}\n\n\nclass HookLogsTest(HookTestCase):\n    HOOK: plugins.EventType = \"write\"\n\n    @contextmanager\n    def _configure_logs(self, command: str) -> Iterator[list[str]]:\n        config = {\"hooks\": [self._get_hook(self.HOOK, command)]}\n\n        with self.configure_plugin(config), capture_log(\"beets.hook\") as logs:\n            plugins.send(self.HOOK)\n            yield logs\n\n    def test_hook_empty_command(self):\n        with self._configure_logs(\"\") as logs:\n            assert 'hook: invalid command \"\"' in logs\n\n    # FIXME: fails on windows\n    @unittest.skipIf(sys.platform == \"win32\", \"win32\")\n    def test_hook_non_zero_exit(self):\n        with self._configure_logs('sh -c \"exit 1\"') as logs:\n            assert f\"hook: hook for {self.HOOK} exited with status 1\" in logs\n\n    def test_hook_non_existent_command(self):\n        with self._configure_logs(\"non-existent-command\") as logs:\n            logs = \"\\n\".join(logs)\n\n        assert f\"hook: hook for {self.HOOK} failed: \" in logs\n        # The error message is different for each OS. Unfortunately the text is\n        # different in each case, where the only shared text is the string\n        # 'file' and substring 'Err'\n        assert \"Err\" in logs\n        assert \"file\" in logs\n\n\nclass HookCommandTest(HookTestCase):\n    EVENTS: ClassVar[list[plugins.EventType]] = [\"write\", \"after_write\"]\n\n    def setUp(self):\n        super().setUp()\n        self.paths = [str(self.temp_dir_path / e) for e in self.EVENTS]\n\n    def _test_command(\n        self,\n        make_test_path: Callable[[str, str], str],\n        send_path_kwarg: bool = False,\n    ) -> None:\n        \"\"\"Check that each of the configured hooks is executed.\n\n        Configure hooks for each event:\n        1. Use the given 'make_test_path' callable to create a test path from the event\n           and the original path.\n        2. Configure a hook with a command to touch this path.\n\n        For each of the original paths:\n        1. Send a test event\n        2. Assert that a file has been created under the original path, which proves\n           that the configured hook command has been executed.\n        \"\"\"\n        events_with_paths = list(zip(self.EVENTS, self.paths))\n        hooks = [\n            self._get_hook(e, f\"touch {make_test_path(e, p)}\")\n            for e, p in events_with_paths\n        ]\n\n        with self.configure_plugin({\"hooks\": hooks}):\n            for event, path in events_with_paths:\n                if send_path_kwarg:\n                    plugins.send(event, path=path)\n                else:\n                    plugins.send(event)\n                assert os.path.isfile(path)\n\n    @unittest.skipIf(sys.platform == \"win32\", \"win32\")\n    def test_hook_no_arguments(self):\n        self._test_command(lambda _, p: p)\n\n    @unittest.skipIf(sys.platform == \"win32\", \"win32\")\n    def test_hook_event_substitution(self):\n        self._test_command(lambda e, p: p.replace(e, \"{event}\"))\n\n    @unittest.skipIf(sys.platform == \"win32\", \"win32\")\n    def test_hook_argument_substitution(self):\n        self._test_command(lambda *_: \"{path}\", send_path_kwarg=True)\n\n    @unittest.skipIf(sys.platform == \"win32\", \"win32\")\n    def test_hook_bytes_interpolation(self):\n        self.paths = [p.encode() for p in self.paths]\n        self._test_command(lambda *_: \"{path}\", send_path_kwarg=True)\n"
  },
  {
    "path": "test/plugins/test_ihate.py",
    "content": "\"\"\"Tests for the 'ihate' plugin\"\"\"\n\nimport unittest\n\nfrom beets import importer\nfrom beets.library import Item\nfrom beetsplug.ihate import IHatePlugin\n\n\nclass IHatePluginTest(unittest.TestCase):\n    def test_hate(self):\n        match_pattern = {}\n        test_item = Item(\n            genres=[\"TestGenre\"], album=\"TestAlbum\", artist=\"TestArtist\"\n        )\n        task = importer.SingletonImportTask(None, test_item)\n\n        # Empty query should let it pass.\n        assert not IHatePlugin.do_i_hate_this(task, match_pattern)\n\n        # 1 query match.\n        match_pattern = [\"artist:bad_artist\", \"artist:TestArtist\"]\n        assert IHatePlugin.do_i_hate_this(task, match_pattern)\n\n        # 2 query matches, either should trigger.\n        match_pattern = [\"album:test\", \"artist:testartist\"]\n        assert IHatePlugin.do_i_hate_this(task, match_pattern)\n\n        # Query is blocked by AND clause.\n        match_pattern = [\"album:notthis genres:testgenre\"]\n        assert not IHatePlugin.do_i_hate_this(task, match_pattern)\n\n        # Both queries are blocked by AND clause with unmatched condition.\n        match_pattern = [\n            \"album:notthis genres:testgenre\",\n            \"artist:testartist album:notthis\",\n        ]\n        assert not IHatePlugin.do_i_hate_this(task, match_pattern)\n\n        # Only one query should fire.\n        match_pattern = [\n            \"album:testalbum genres:testgenre\",\n            \"artist:testartist album:notthis\",\n        ]\n        assert IHatePlugin.do_i_hate_this(task, match_pattern)\n"
  },
  {
    "path": "test/plugins/test_importadded.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Stig Inge Lea Bjornsen.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\n\"\"\"Tests for the `importadded` plugin.\"\"\"\n\nimport os\n\nimport pytest\n\nfrom beets import importer\nfrom beets.test.helper import AutotagImportTestCase, PluginMixin\nfrom beets.util import displayable_path, syspath\nfrom beetsplug.importadded import ImportAddedPlugin\n\n_listeners = ImportAddedPlugin.listeners\n\n\ndef preserve_plugin_listeners():\n    \"\"\"Preserve the initial plugin listeners as they would otherwise be\n    deleted after the first setup / tear down cycle.\n    \"\"\"\n    if not ImportAddedPlugin.listeners:\n        ImportAddedPlugin.listeners = _listeners\n\n\ndef modify_mtimes(paths, offset=-60000):\n    for i, path in enumerate(paths, start=1):\n        mstat = os.stat(path)\n        os.utime(syspath(path), (mstat.st_atime, mstat.st_mtime + offset * i))\n\n\nclass ImportAddedTest(PluginMixin, AutotagImportTestCase):\n    # The minimum mtime of the files to be imported\n    plugin = \"importadded\"\n    min_mtime = None\n\n    def setUp(self):\n        preserve_plugin_listeners()\n        super().setUp()\n        self.prepare_album_for_import(2)\n        # Different mtimes on the files to be imported in order to test the\n        # plugin\n        modify_mtimes(mfile.path for mfile in self.import_media)\n        self.min_mtime = min(\n            os.path.getmtime(mfile.path) for mfile in self.import_media\n        )\n        self.importer = self.setup_importer()\n        self.importer.add_choice(importer.Action.APPLY)\n\n    def find_media_file(self, item):\n        \"\"\"Find the pre-import MediaFile for an Item\"\"\"\n        for m in self.import_media:\n            if m.title.replace(\"Tag\", \"Applied\") == item.title:\n                return m\n        raise AssertionError(\n            f\"No MediaFile found for Item {displayable_path(item.path)}\"\n        )\n\n    def test_import_album_with_added_dates(self):\n        self.importer.run()\n\n        album = self.lib.albums().get()\n        assert album.added == self.min_mtime\n        for item in album.items():\n            assert item.added == self.min_mtime\n\n    def test_import_album_inplace_with_added_dates(self):\n        self.config[\"import\"][\"copy\"] = False\n\n        self.importer.run()\n\n        album = self.lib.albums().get()\n        assert album.added == self.min_mtime\n        for item in album.items():\n            assert item.added == self.min_mtime\n\n    def test_import_album_with_preserved_mtimes(self):\n        self.config[\"importadded\"][\"preserve_mtimes\"] = True\n        self.importer.run()\n        album = self.lib.albums().get()\n        assert album.added == self.min_mtime\n        for item in album.items():\n            assert item.added == pytest.approx(self.min_mtime, rel=1e-4)\n            mediafile_mtime = os.path.getmtime(self.find_media_file(item).path)\n            assert item.mtime == pytest.approx(mediafile_mtime, rel=1e-4)\n            assert os.path.getmtime(item.path) == pytest.approx(\n                mediafile_mtime, rel=1e-4\n            )\n\n    def test_reimported_album_skipped(self):\n        # Import and record the original added dates\n        self.importer.run()\n        album = self.lib.albums().get()\n        album_added_before = album.added\n        items_added_before = {item.path: item.added for item in album.items()}\n        # Newer Item path mtimes as if Beets had modified them\n        modify_mtimes(items_added_before.keys(), offset=10000)\n        # Reimport\n        self.setup_importer(import_dir=self.libdir)\n        self.importer.run()\n        # Verify the reimported items\n        album = self.lib.albums().get()\n        assert album.added == pytest.approx(album_added_before, rel=1e-4)\n        items_added_after = {item.path: item.added for item in album.items()}\n        for item_path, added_after in items_added_after.items():\n            assert items_added_before[item_path] == pytest.approx(\n                added_after, rel=1e-4\n            ), f\"reimport modified Item.added for {displayable_path(item_path)}\"\n\n    def test_import_singletons_with_added_dates(self):\n        self.config[\"import\"][\"singletons\"] = True\n        self.importer.run()\n        for item in self.lib.items():\n            mfile = self.find_media_file(item)\n            assert item.added == pytest.approx(\n                os.path.getmtime(mfile.path), rel=1e-4\n            )\n\n    def test_import_singletons_with_preserved_mtimes(self):\n        self.config[\"import\"][\"singletons\"] = True\n        self.config[\"importadded\"][\"preserve_mtimes\"] = True\n        self.importer.run()\n        for item in self.lib.items():\n            mediafile_mtime = os.path.getmtime(self.find_media_file(item).path)\n            assert item.added == pytest.approx(mediafile_mtime, rel=1e-4)\n            assert item.mtime == pytest.approx(mediafile_mtime, rel=1e-4)\n            assert os.path.getmtime(item.path) == pytest.approx(\n                mediafile_mtime, rel=1e-4\n            )\n\n    def test_reimported_singletons_skipped(self):\n        self.config[\"import\"][\"singletons\"] = True\n        # Import and record the original added dates\n        self.importer.run()\n        items_added_before = {\n            item.path: item.added for item in self.lib.items()\n        }\n        # Newer Item path mtimes as if Beets had modified them\n        modify_mtimes(items_added_before.keys(), offset=10000)\n        # Reimport\n        self.setup_importer(import_dir=self.libdir, singletons=True)\n        self.importer.run()\n        # Verify the reimported items\n        items_added_after = {item.path: item.added for item in self.lib.items()}\n        for item_path, added_after in items_added_after.items():\n            assert items_added_before[item_path] == pytest.approx(\n                added_after, rel=1e-4\n            ), f\"reimport modified Item.added for {displayable_path(item_path)}\"\n"
  },
  {
    "path": "test/plugins/test_importfeeds.py",
    "content": "import datetime\nimport os\n\nfrom beets.library import Album, Item\nfrom beets.test.helper import PluginTestCase\nfrom beetsplug.importfeeds import ImportFeedsPlugin\n\n\nclass ImportFeedsTest(PluginTestCase):\n    plugin = \"importfeeds\"\n\n    def setUp(self):\n        super().setUp()\n        self.importfeeds = ImportFeedsPlugin()\n        self.feeds_dir = self.temp_dir_path / \"importfeeds\"\n        self.config[\"importfeeds\"][\"dir\"] = str(self.feeds_dir)\n\n    def test_multi_format_album_playlist(self):\n        self.config[\"importfeeds\"][\"formats\"] = \"m3u_multi\"\n        album = Album(album=\"album/name\", id=1)\n        item_path = os.path.join(\"path\", \"to\", \"item\")\n        item = Item(title=\"song\", album_id=1, path=item_path)\n        self.lib.add(album)\n        self.lib.add(item)\n\n        self.importfeeds.album_imported(self.lib, album)\n        playlist_path = self.feeds_dir / next(self.feeds_dir.iterdir())\n        assert str(playlist_path).endswith(\"album_name.m3u\")\n        with open(playlist_path) as playlist:\n            assert item_path in playlist.read()\n\n    def test_playlist_in_subdir(self):\n        self.config[\"importfeeds\"][\"formats\"] = \"m3u\"\n        self.config[\"importfeeds\"][\"m3u_name\"] = os.path.join(\n            \"subdir\", \"imported.m3u\"\n        )\n        album = Album(album=\"album/name\", id=1)\n        item_path = os.path.join(\"path\", \"to\", \"item\")\n        item = Item(title=\"song\", album_id=1, path=item_path)\n        self.lib.add(album)\n        self.lib.add(item)\n\n        self.importfeeds.album_imported(self.lib, album)\n        playlist = self.feeds_dir / self.config[\"importfeeds\"][\"m3u_name\"].get()\n        playlist_subdir = os.path.dirname(playlist)\n        assert os.path.isdir(playlist_subdir)\n        assert os.path.isfile(playlist)\n\n    def test_playlist_per_session(self):\n        self.config[\"importfeeds\"][\"formats\"] = \"m3u_session\"\n        self.config[\"importfeeds\"][\"m3u_name\"] = \"imports.m3u\"\n        album = Album(album=\"album/name\", id=1)\n        item_path = os.path.join(\"path\", \"to\", \"item\")\n        item = Item(title=\"song\", album_id=1, path=item_path)\n        self.lib.add(album)\n        self.lib.add(item)\n\n        self.importfeeds.import_begin(self)\n        self.importfeeds.album_imported(self.lib, album)\n        date = datetime.datetime.now().strftime(\"%Y%m%d_%Hh%M\")\n        playlist = self.feeds_dir / f\"imports_{date}.m3u\"\n        assert os.path.isfile(playlist)\n        with open(playlist) as playlist_contents:\n            assert item_path in playlist_contents.read()\n"
  },
  {
    "path": "test/plugins/test_importsource.py",
    "content": "# This file is part of beets.\n# Copyright 2025, Stig Inge Lea Bjornsen.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\n\"\"\"Tests for the `importsource` plugin.\"\"\"\n\nimport os\nimport time\n\nfrom beets import importer, plugins\nfrom beets.test.helper import AutotagImportTestCase, IOMixin, PluginMixin\nfrom beets.util import syspath\nfrom beetsplug.importsource import ImportSourcePlugin\n\n_listeners = ImportSourcePlugin.listeners\n\n\ndef preserve_plugin_listeners():\n    \"\"\"Preserve the initial plugin listeners as they would otherwise be\n    deleted after the first setup / tear down cycle.\n    \"\"\"\n    if not ImportSourcePlugin.listeners:\n        ImportSourcePlugin.listeners = _listeners\n\n\nclass ImportSourceTest(IOMixin, PluginMixin, AutotagImportTestCase):\n    plugin = \"importsource\"\n    preload_plugin = False\n\n    def setUp(self):\n        preserve_plugin_listeners()\n        super().setUp()\n        self.config[self.plugin][\"suggest_removal\"] = True\n        self.load_plugins()\n        self.prepare_album_for_import(2)\n        self.importer = self.setup_importer()\n        self.importer.add_choice(importer.Action.APPLY)\n        self.importer.run()\n        self.all_items = self.lib.albums().get().items()\n        self.item_to_remove = self.all_items[0]\n\n    def interact(self, stdin: list[str]):\n        for char in stdin:\n            self.io.addinput(char)\n        self.run_command(\"remove\", f\"path:{syspath(self.item_to_remove.path)}\")\n\n    def test_do_nothing(self):\n        self.interact([\"N\"])\n\n        assert os.path.exists(self.item_to_remove.source_path)\n\n    def test_remove_single(self):\n        self.interact([\"y\", \"D\"])\n\n        assert not os.path.exists(self.item_to_remove.source_path)\n\n    def test_remove_all_from_single(self):\n        self.interact([\"y\", \"R\", \"y\"])\n\n        for item in self.all_items:\n            assert not os.path.exists(item.source_path)\n\n    def test_stop_suggesting(self):\n        self.interact([\"y\", \"S\"])\n\n        for item in self.all_items:\n            assert os.path.exists(item.source_path)\n\n    def test_source_path_attribute_written(self):\n        \"\"\"Test that source_path attribute is correctly written to imported items.\n\n        The items should already have source_path from the setUp import\n        \"\"\"\n        for item in self.all_items:\n            assert \"source_path\" in item\n            assert item.source_path  # Should not be empty\n\n    def test_source_files_not_modified_during_import(self):\n        \"\"\"Test that source files timestamps are not changed during import.\"\"\"\n        # Prepare fresh files and record timestamps\n        test_album_path = self.import_path / \"test_album\"\n        import_paths = self.prepare_album_for_import(\n            2, album_path=test_album_path\n        )\n        original_mtimes = {\n            path: os.stat(path).st_mtime for path in import_paths\n        }\n\n        # Small delay to detect timestamp changes\n        time.sleep(0.1)\n\n        # Run a fresh import\n        importer_session = self.setup_importer()\n        importer_session.add_choice(importer.Action.APPLY)\n        importer_session.run()\n\n        # Verify timestamps haven't changed\n        for path, original_mtime in original_mtimes.items():\n            current_mtime = os.stat(path).st_mtime\n            assert current_mtime == original_mtime, (\n                f\"Source file timestamp changed: {path}\"\n            )\n\n    def test_prevent_suggest_removal_on_reimport(self):\n        \"\"\"Test that removal suggestions are prevented during reimport.\"\"\"\n        album = self.lib.albums().get()\n        mb_albumid = album.mb_albumid\n\n        # Reimport from library\n        reimporter = self.setup_importer(import_dir=self.libdir)\n        reimporter.add_choice(importer.Action.APPLY)\n        reimporter.run()\n\n        plugin = plugins._instances[0]\n        assert mb_albumid in plugin.stop_suggestions_for_albums\n\n        # Calling suggest_removal should exit early without prompting\n        item = self.lib.items().get()\n        plugin.suggest_removal(item)\n        assert os.path.exists(item.source_path)\n\n    def test_prevent_suggest_removal_handles_skipped_task(self):\n        \"\"\"Test that skipped tasks don't crash prevent_suggest_removal.\"\"\"\n\n        class MockTask:\n            skip = True\n\n            def imported_items(self):\n                return \"whatever\"\n\n        plugin = plugins._instances[0]\n        mock_task = MockTask()\n        plugin.prevent_suggest_removal(None, mock_task)\n"
  },
  {
    "path": "test/plugins/test_info.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Thomas Scholtes.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nfrom mediafile import MediaFile\n\nfrom beets.test.helper import IOMixin, PluginTestCase\nfrom beets.util import displayable_path\n\n\nclass InfoTest(IOMixin, PluginTestCase):\n    plugin = \"info\"\n\n    def test_path(self):\n        path = self.create_mediafile_fixture()\n\n        mediafile = MediaFile(path)\n        mediafile.albumartist = \"AAA\"\n        mediafile.disctitle = \"DDD\"\n        mediafile.genres = [\"a\", \"b\", \"c\"]\n        mediafile.composer = None\n        mediafile.save()\n\n        out = self.run_with_output(\"info\", path)\n        assert displayable_path(path) in out\n        assert \"albumartist: AAA\" in out\n        assert \"disctitle: DDD\" in out\n        assert \"genres: a; b; c\" in out\n        assert \"composer:\" not in out\n\n    def test_item_query(self):\n        item1, item2 = self.add_item_fixtures(count=2)\n        item1.album = \"xxxx\"\n        item1.write()\n        item1.album = \"yyyy\"\n        item1.store()\n\n        out = self.run_with_output(\"info\", \"album:yyyy\")\n        assert displayable_path(item1.path) in out\n        assert \"album: xxxx\" in out\n\n        assert displayable_path(item2.path) not in out\n\n    def test_item_library_query(self):\n        (item,) = self.add_item_fixtures()\n        item.album = \"xxxx\"\n        item.store()\n\n        out = self.run_with_output(\"info\", \"--library\", \"album:xxxx\")\n        assert displayable_path(item.path) in out\n        assert \"album: xxxx\" in out\n\n    def test_collect_item_and_path(self):\n        path = self.create_mediafile_fixture()\n        mediafile = MediaFile(path)\n        (item,) = self.add_item_fixtures()\n\n        item.album = mediafile.album = \"AAA\"\n        item.tracktotal = mediafile.tracktotal = 5\n        item.title = \"TTT\"\n        mediafile.title = \"SSS\"\n\n        item.write()\n        item.store()\n        mediafile.save()\n\n        out = self.run_with_output(\"info\", \"--summarize\", \"album:AAA\", path)\n        assert \"album: AAA\" in out\n        assert \"tracktotal: 5\" in out\n        assert \"title: [various]\" in out\n\n    def test_collect_item_and_path_with_multi_values(self):\n        path = self.create_mediafile_fixture()\n        mediafile = MediaFile(path)\n        (item,) = self.add_item_fixtures()\n\n        item.album = mediafile.album = \"AAA\"\n        item.tracktotal = mediafile.tracktotal = 5\n        item.title = \"TTT\"\n        mediafile.title = \"SSS\"\n\n        item.albumartists = [\"Artist A\", \"Artist B\"]\n        mediafile.albumartists = [\"Artist C\", \"Artist D\"]\n\n        item.artists = [\"Artist A\", \"Artist Z\"]\n        mediafile.artists = [\"Artist A\", \"Artist Z\"]\n\n        item.write()\n        item.store()\n        mediafile.save()\n\n        out = self.run_with_output(\"info\", \"--summarize\", \"album:AAA\", path)\n        assert \"album: AAA\" in out\n        assert \"tracktotal: 5\" in out\n        assert \"title: [various]\" in out\n        assert \"albumartists: [various]\" in out\n        assert \"artists: Artist A; Artist Z\" in out\n\n    def test_custom_format(self):\n        self.add_item_fixtures()\n        out = self.run_with_output(\n            \"info\",\n            \"--library\",\n            \"--format\",\n            \"$track. $title - $artist ($length)\",\n        )\n        assert \"02. tïtle 0 - the artist (0:01)\\n\" == out\n"
  },
  {
    "path": "test/plugins/test_inline.py",
    "content": "# This file is part of beets.\n# Copyright 2025, Gabe Push.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\nfrom beets import config, plugins\nfrom beets.test.helper import PluginTestCase\nfrom beetsplug.inline import InlinePlugin\n\n\nclass TestInlineRecursion(PluginTestCase):\n    def test_no_recursion_when_inline_shadows_fixed_field(self):\n        config[\"plugins\"] = [\"inline\"]\n\n        config[\"item_fields\"] = {\n            \"track_no\": (\n                \"f'{disc:02d}-{track:02d}' if disctotal > 1 else f'{track:02d}'\"\n            )\n        }\n\n        plugins._instances.clear()\n        plugins.load_plugins()\n\n        item = self.add_item_fixture(\n            artist=\"Artist\",\n            album=\"Album\",\n            title=\"Title\",\n            track=1,\n            disc=1,\n            disctotal=1,\n        )\n\n        out = item.evaluate_template(\"$track_no\")\n\n        assert out == \"01\"\n\n    def test_inline_function_body_item_field(self):\n        plugin = InlinePlugin()\n        func = plugin.compile_inline(\n            \"return track + 1\", album=False, field_name=\"next_track\"\n        )\n\n        item = self.add_item_fixture(track=3)\n        assert func(item) == 4\n\n    def test_inline_album_expression_uses_items(self):\n        plugin = InlinePlugin()\n        func = plugin.compile_inline(\n            \"len(items)\", album=True, field_name=\"item_count\"\n        )\n\n        album = self.add_album_fixture()\n        assert func(album) == len(list(album.items()))\n"
  },
  {
    "path": "test/plugins/test_ipfs.py",
    "content": "# This file is part of beets.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nimport os\nfrom unittest.mock import Mock, patch\n\nfrom beets.test import _common\nfrom beets.test.helper import PluginTestCase\nfrom beets.util import bytestring_path\nfrom beetsplug.ipfs import IPFSPlugin\n\n\n@patch(\"beets.util.command_output\", Mock())\nclass IPFSPluginTest(PluginTestCase):\n    plugin = \"ipfs\"\n\n    def test_stored_hashes(self):\n        test_album = self.mk_test_album()\n        ipfs = IPFSPlugin()\n        added_albums = ipfs.ipfs_added_albums(self.lib, self.lib.path)\n        added_album = added_albums.get_album(1)\n        assert added_album.ipfs == test_album.ipfs\n        found = False\n        want_item = test_album.items()[2]\n        for check_item in added_album.items():\n            try:\n                if check_item.get(\"ipfs\", with_album=False):\n                    ipfs_item = os.fsdecode(os.path.basename(want_item.path))\n                    want_path = f\"/ipfs/{test_album.ipfs}/{ipfs_item}\"\n                    want_path = bytestring_path(want_path)\n                    assert check_item.path == want_path\n                    assert (\n                        check_item.get(\"ipfs\", with_album=False)\n                        == want_item.ipfs\n                    )\n                    assert check_item.title == want_item.title\n                    found = True\n            except AttributeError:\n                pass\n        assert found\n\n    def mk_test_album(self):\n        items = [_common.item() for _ in range(3)]\n        items[0].title = \"foo bar\"\n        items[0].artist = \"1one\"\n        items[0].album = \"baz\"\n        items[0].year = 2001\n        items[0].comp = True\n        items[1].title = \"baz qux\"\n        items[1].artist = \"2two\"\n        items[1].album = \"baz\"\n        items[1].year = 2002\n        items[1].comp = True\n        items[2].title = \"beets 4 eva\"\n        items[2].artist = \"3three\"\n        items[2].album = \"foo\"\n        items[2].year = 2003\n        items[2].comp = False\n        items[2].ipfs = \"QmfM9ic5LJj7V6ecozFx1MkSoaaiq3PXfhJoFvyqzpLXSk\"\n\n        for item in items:\n            self.lib.add(item)\n\n        album = self.lib.add_album(items)\n        album.ipfs = \"QmfM9ic5LJj7V6ecozFx1MkSoaaiq3PXfhJoFvyqzpLXSf\"\n        album.store(inherit=False)\n\n        return album\n"
  },
  {
    "path": "test/plugins/test_keyfinder.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Thomas Scholtes.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nfrom unittest.mock import patch\n\nfrom beets import util\nfrom beets.library import Item\nfrom beets.test.helper import AsIsImporterMixin, ImportTestCase, PluginMixin\n\n\n@patch(\"beets.util.command_output\")\nclass KeyFinderTest(AsIsImporterMixin, PluginMixin, ImportTestCase):\n    plugin = \"keyfinder\"\n\n    def test_add_key(self, command_output):\n        item = Item(path=\"/file\")\n        item.add(self.lib)\n\n        command_output.return_value = util.CommandOutput(b\"dbm\", b\"\")\n        self.run_command(\"keyfinder\")\n\n        item.load()\n        assert item[\"initial_key\"] == \"C#m\"\n        command_output.assert_called_with(\n            [\"KeyFinder\", \"-f\", util.syspath(item.path)]\n        )\n\n    def test_add_key_on_import(self, command_output):\n        command_output.return_value = util.CommandOutput(b\"dbm\", b\"\")\n        self.run_asis_importer()\n\n        item = self.lib.items().get()\n        assert item[\"initial_key\"] == \"C#m\"\n\n    def test_force_overwrite(self, command_output):\n        self.config[\"keyfinder\"][\"overwrite\"] = True\n\n        item = Item(path=\"/file\", initial_key=\"F\")\n        item.add(self.lib)\n\n        command_output.return_value = util.CommandOutput(b\"C#m\", b\"\")\n        self.run_command(\"keyfinder\")\n\n        item.load()\n        assert item[\"initial_key\"] == \"C#m\"\n\n    def test_do_not_overwrite(self, command_output):\n        item = Item(path=\"/file\", initial_key=\"F\")\n        item.add(self.lib)\n\n        command_output.return_value = util.CommandOutput(b\"dbm\", b\"\")\n        self.run_command(\"keyfinder\")\n\n        item.load()\n        assert item[\"initial_key\"] == \"F\"\n\n    def test_no_key(self, command_output):\n        item = Item(path=\"/file\")\n        item.add(self.lib)\n\n        command_output.return_value = util.CommandOutput(b\"\", b\"\")\n        self.run_command(\"keyfinder\")\n\n        item.load()\n        assert item[\"initial_key\"] is None\n"
  },
  {
    "path": "test/plugins/test_lastgenre.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Fabrice Laporte.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for the 'lastgenre' plugin.\"\"\"\n\nfrom unittest.mock import Mock, patch\n\nimport pytest\n\nfrom beets.test import _common\nfrom beets.test.helper import IOMixin, PluginTestCase\nfrom beetsplug import lastgenre\n\n\nclass LastGenrePluginTest(IOMixin, PluginTestCase):\n    plugin = \"lastgenre\"\n\n    def setUp(self):\n        super().setUp()\n        self.plugin = lastgenre.LastGenrePlugin()\n\n    def _setup_config(\n        self, whitelist=False, canonical=False, count=1, prefer_specific=False\n    ):\n        self.config[\"lastgenre\"][\"canonical\"] = canonical\n        self.config[\"lastgenre\"][\"count\"] = count\n        self.config[\"lastgenre\"][\"prefer_specific\"] = prefer_specific\n        if isinstance(whitelist, (bool, (str,))):\n            # Filename, default, or disabled.\n            self.config[\"lastgenre\"][\"whitelist\"] = whitelist\n        self.plugin.setup()\n        if not isinstance(whitelist, (bool, (str,))):\n            # Explicit list of genres.\n            self.plugin.whitelist = whitelist\n\n    def test_default(self):\n        \"\"\"Fetch genres with whitelist and c14n deactivated\"\"\"\n        self._setup_config()\n        assert self.plugin._resolve_genres([\"delta blues\"]) == [\"delta blues\"]\n\n    def test_c14n_only(self):\n        \"\"\"Default c14n tree funnels up to most common genre except for *wrong*\n        genres that stay unchanged.\n        \"\"\"\n        self._setup_config(canonical=True, count=99)\n        assert self.plugin._resolve_genres([\"delta blues\"]) == [\"blues\"]\n        assert self.plugin._resolve_genres([\"iota blues\"]) == [\"iota blues\"]\n\n    def test_whitelist_only(self):\n        \"\"\"Default whitelist rejects *wrong* (non existing) genres.\"\"\"\n        self._setup_config(whitelist=True)\n        assert self.plugin._resolve_genres([\"iota blues\"]) == []\n\n    def test_whitelist_c14n(self):\n        \"\"\"Default whitelist and c14n both activated result in all parents\n        genres being selected (from specific to common).\n        \"\"\"\n        self._setup_config(canonical=True, whitelist=True, count=99)\n        assert self.plugin._resolve_genres([\"delta blues\"]) == [\n            \"delta blues\",\n            \"blues\",\n        ]\n\n    def test_whitelist_custom(self):\n        \"\"\"Keep only genres that are in the whitelist.\"\"\"\n        self._setup_config(whitelist={\"blues\", \"rock\", \"jazz\"}, count=2)\n        assert self.plugin._resolve_genres([\"pop\", \"blues\"]) == [\"blues\"]\n\n        self._setup_config(canonical=\"\", whitelist={\"rock\"})\n        assert self.plugin._resolve_genres([\"delta blues\"]) == []\n\n    def test_format_genres(self):\n        \"\"\"Format genres list.\"\"\"\n        self._setup_config(count=2)\n        assert self.plugin._format_genres([\"jazz\", \"pop\", \"rock\", \"blues\"]) == [\n            \"Jazz\",\n            \"Pop\",\n            \"Rock\",\n            \"Blues\",\n        ]\n\n    def test_count_c14n(self):\n        \"\"\"Keep the n first genres, after having applied c14n when necessary\"\"\"\n        self._setup_config(\n            whitelist={\"blues\", \"rock\", \"jazz\"}, canonical=True, count=2\n        )\n        # thanks to c14n, 'blues' superseeds 'country blues' and takes the\n        # second slot\n        assert self.plugin._resolve_genres(\n            [\"jazz\", \"pop\", \"country blues\", \"rock\"]\n        ) == [\"jazz\", \"blues\"]\n\n    def test_c14n_whitelist(self):\n        \"\"\"Genres first pass through c14n and are then filtered\"\"\"\n        self._setup_config(canonical=True, whitelist={\"rock\"})\n        assert self.plugin._resolve_genres([\"delta blues\"]) == []\n\n    def test_empty_string_enables_canonical(self):\n        \"\"\"For backwards compatibility, setting the `canonical` option\n        to the empty string enables it using the default tree.\n        \"\"\"\n        self._setup_config(canonical=\"\", count=99)\n        assert self.plugin._resolve_genres([\"delta blues\"]) == [\"blues\"]\n\n    def test_empty_string_enables_whitelist(self):\n        \"\"\"Again for backwards compatibility, setting the `whitelist`\n        option to the empty string enables the default set of genres.\n        \"\"\"\n        self._setup_config(whitelist=\"\")\n        assert self.plugin._resolve_genres([\"iota blues\"]) == []\n\n    def test_prefer_specific_loads_tree(self):\n        \"\"\"When prefer_specific is enabled but canonical is not the\n        tree still has to be loaded.\n        \"\"\"\n        self._setup_config(prefer_specific=True, canonical=False)\n        assert self.plugin.c14n_branches != []\n\n    def test_prefer_specific_without_canonical(self):\n        \"\"\"Prefer_specific works without canonical.\"\"\"\n        self._setup_config(prefer_specific=True, canonical=False, count=4)\n        assert self.plugin._resolve_genres([\"math rock\", \"post-rock\"]) == [\n            \"post-rock\",\n            \"math rock\",\n        ]\n\n    @patch(\"beets.ui.should_write\", Mock(return_value=True))\n    @patch(\n        \"beetsplug.lastgenre.LastGenrePlugin._get_genre\",\n        Mock(return_value=(\"Mock Genre\", \"mock stage\")),\n    )\n    def test_pretend_option_skips_library_updates(self):\n        item = self.create_item(\n            album=\"Pretend Album\",\n            albumartist=\"Pretend Artist\",\n            artist=\"Pretend Artist\",\n            title=\"Pretend Track\",\n            genres=[\"Original Genre\"],\n        )\n        album = self.lib.add_album([item])\n\n        def unexpected_store(*_, **__):\n            raise AssertionError(\"Unexpected store call\")\n\n        # Verify that try_write was never called (file operations skipped)\n        with patch(\"beetsplug.lastgenre.Item.store\", unexpected_store):\n            output = self.run_with_output(\"lastgenre\", \"--pretend\")\n\n        assert \"genres:\" in output\n        album.load()\n        assert album.genres == [\"Original Genre\"]\n        assert album.items()[0].genres == [\"Original Genre\"]\n\n    def test_no_duplicate(self):\n        \"\"\"Remove duplicated genres.\"\"\"\n        self._setup_config(count=99)\n        assert self.plugin._resolve_genres([\"blues\", \"blues\"]) == [\"blues\"]\n\n    def test_tags_for(self):\n        class MockPylastElem:\n            def __init__(self, name):\n                self.name = name\n\n            def get_name(self):\n                return self.name\n\n        class MockPylastObj:\n            def get_top_tags(self):\n                tag1 = Mock()\n                tag1.weight = 90\n                tag1.item = MockPylastElem(\"Pop\")\n                tag2 = Mock()\n                tag2.weight = 40\n                tag2.item = MockPylastElem(\"Rap\")\n                return [tag1, tag2]\n\n        plugin = lastgenre.LastGenrePlugin()\n        res = plugin.client._tags_for(MockPylastObj())\n        assert res == [\"pop\", \"rap\"]\n        res = plugin.client._tags_for(MockPylastObj(), min_weight=50)\n        assert res == [\"pop\"]\n\n    def test_sort_by_depth(self):\n        self._setup_config(canonical=True)\n        # Normal case.\n        tags = (\"electronic\", \"ambient\", \"post-rock\", \"downtempo\")\n        res = lastgenre.sort_by_depth(tags, self.plugin.c14n_branches)\n        assert res == [\"post-rock\", \"downtempo\", \"ambient\", \"electronic\"]\n        # Non-canonical tag ('chillout') present.\n        tags = (\"electronic\", \"ambient\", \"chillout\")\n        res = lastgenre.sort_by_depth(tags, self.plugin.c14n_branches)\n        assert res == [\"ambient\", \"electronic\"]\n\n\n@pytest.fixture\ndef config(config):\n    \"\"\"Provide a fresh beets configuration for every test/parameterize call\n\n    This is necessary to prevent the following parameterized test to bleed\n    config test state in between test cases.\n    \"\"\"\n    return config\n\n\n@pytest.mark.parametrize(\n    \"config_values, item_genre, mock_genres, expected_result\",\n    [\n        # force and keep whitelisted\n        (\n            {\n                \"force\": True,\n                \"keep_existing\": True,\n                \"source\": \"album\",  # means album or artist genre\n                \"whitelist\": True,\n                \"canonical\": False,\n                \"prefer_specific\": False,\n                \"count\": 10,\n            },\n            [\"Blues\"],\n            {\n                \"album\": [\"Jazz\"],\n            },\n            ([\"Blues\", \"Jazz\"], \"keep + album, whitelist\"),\n        ),\n        # force and keep whitelisted, unknown original\n        (\n            {\n                \"force\": True,\n                \"keep_existing\": True,\n                \"source\": \"album\",\n                \"whitelist\": True,\n                \"canonical\": False,\n                \"prefer_specific\": False,\n                \"count\": 10,\n            },\n            [\"original unknown\", \"Blues\"],\n            {\n                \"album\": [\"Jazz\"],\n            },\n            ([\"Blues\", \"Jazz\"], \"keep + album, whitelist\"),\n        ),\n        # force and keep whitelisted on empty tag\n        (\n            {\n                \"force\": True,\n                \"keep_existing\": True,\n                \"source\": \"album\",\n                \"whitelist\": True,\n                \"canonical\": False,\n                \"prefer_specific\": False,\n            },\n            [],\n            {\n                \"album\": [\"Jazz\"],\n            },\n            ([\"Jazz\"], \"album, whitelist\"),\n        ),\n        # force and keep, artist configured\n        (\n            {\n                \"force\": True,\n                \"keep_existing\": True,\n                \"source\": \"artist\",  # means artist genre, original or fallback\n                \"whitelist\": True,\n                \"canonical\": False,\n                \"prefer_specific\": False,\n                \"count\": 10,\n            },\n            [\"original unknown\", \"Blues\"],\n            {\n                \"album\": [\"Jazz\"],\n                \"artist\": [\"Pop\"],\n            },\n            ([\"Blues\", \"Pop\"], \"keep + artist, whitelist\"),\n        ),\n        # don't force, disabled whitelist\n        (\n            {\n                \"force\": False,\n                \"keep_existing\": False,\n                \"source\": \"album\",\n                \"whitelist\": False,\n                \"canonical\": False,\n                \"prefer_specific\": False,\n            },\n            [\"any genre\"],\n            {\n                \"album\": [\"Jazz\"],\n            },\n            ([\"any genre\"], \"keep any, no-force\"),\n        ),\n        # don't force and empty is regular last.fm fetch; no whitelist too\n        (\n            {\n                \"force\": False,\n                \"keep_existing\": False,\n                \"source\": \"album\",\n                \"whitelist\": False,\n                \"canonical\": False,\n                \"prefer_specific\": False,\n            },\n            [],\n            {\n                \"album\": [\"Jazzin\"],\n            },\n            ([\"Jazzin\"], \"album, any\"),\n        ),\n        # Canonicalize original genre when force is **off** and\n        # whitelist, canonical and cleanup_existing are on.\n        # \"Cosmic Disco\" is not in the default whitelist, thus gets resolved \"up\" in the\n        # tree to \"Disco\" and \"Electronic\".\n        (\n            {\n                \"force\": False,\n                \"keep_existing\": False,\n                \"source\": \"artist\",\n                \"whitelist\": True,\n                \"canonical\": True,\n                \"cleanup_existing\": True,\n                \"prefer_specific\": False,\n                \"count\": 10,\n            },\n            [\"Cosmic Disco\"],\n            {\n                \"artist\": [],\n            },\n            (\n                [\"Disco\", \"Electronic\"],\n                \"keep + cleanup, whitelist\",\n            ),\n        ),\n        # fallback to next stages until found\n        (\n            {\n                \"force\": True,\n                \"keep_existing\": True,\n                \"source\": \"track\",  # means track,album,artist,...\n                \"whitelist\": False,\n                \"canonical\": False,\n                \"prefer_specific\": False,\n                \"count\": 10,\n            },\n            [\"unknown genre\"],\n            {\n                \"track\": None,\n                \"album\": None,\n                \"artist\": [\"Jazz\"],\n            },\n            ([\"Unknown Genre\", \"Jazz\"], \"keep + artist, any\"),\n        ),\n        # Keep the original genre when force and keep_existing are on, and\n        # whitelist is disabled\n        (\n            {\n                \"force\": True,\n                \"keep_existing\": True,\n                \"source\": \"track\",\n                \"whitelist\": False,\n                \"fallback\": \"fallback genre\",\n                \"canonical\": False,\n                \"prefer_specific\": False,\n            },\n            [\"any existing\"],\n            {\n                \"track\": None,\n                \"album\": None,\n                \"artist\": None,\n            },\n            ([\"any existing\"], \"original fallback\"),\n        ),\n        # Keep the original genre when force and keep_existing are on, and\n        # whitelist is enabled, and genre is valid.\n        (\n            {\n                \"force\": True,\n                \"keep_existing\": True,\n                \"source\": \"track\",\n                \"whitelist\": True,\n                \"fallback\": \"fallback genre\",\n                \"canonical\": False,\n                \"prefer_specific\": False,\n            },\n            [\"Jazz\"],\n            {\n                \"track\": None,\n                \"album\": None,\n                \"artist\": None,\n            },\n            ([\"Jazz\"], \"original fallback\"),\n        ),\n        # Return the configured fallback when force is on but\n        # keep_existing is not.\n        (\n            {\n                \"force\": True,\n                \"keep_existing\": False,\n                \"source\": \"track\",\n                \"whitelist\": True,\n                \"fallback\": \"fallback genre\",\n                \"canonical\": False,\n                \"prefer_specific\": False,\n            },\n            [\"Jazz\"],\n            {\n                \"track\": None,\n                \"album\": None,\n                \"artist\": None,\n            },\n            ([\"fallback genre\"], \"fallback\"),\n        ),\n        # fallback to fallback if no original\n        (\n            {\n                \"force\": True,\n                \"keep_existing\": True,\n                \"source\": \"track\",\n                \"whitelist\": True,\n                \"fallback\": \"fallback genre\",\n                \"canonical\": False,\n                \"prefer_specific\": False,\n            },\n            [],\n            {\n                \"track\": None,\n                \"album\": None,\n                \"artist\": None,\n            },\n            ([\"fallback genre\"], \"fallback\"),\n        ),\n        # limit a lot of results\n        (\n            {\n                \"force\": True,\n                \"keep_existing\": True,\n                \"source\": \"album\",\n                \"whitelist\": True,\n                \"count\": 5,\n                \"canonical\": False,\n                \"prefer_specific\": False,\n            },\n            [\"original unknown\", \"Blues\", \"Rock\", \"Folk\", \"Metal\"],\n            {\n                \"album\": [\"Jazz\", \"Bebop\", \"Hardbop\"],\n            },\n            (\n                [\"Blues\", \"Rock\", \"Metal\", \"Jazz\", \"Bebop\"],\n                \"keep + album, whitelist\",\n            ),\n        ),\n        # fallback to next stage (artist) if no allowed original present\n        # and no album genre were fetched.\n        (\n            {\n                \"force\": True,\n                \"keep_existing\": True,\n                \"source\": \"album\",\n                \"whitelist\": True,\n                \"fallback\": \"fallback genre\",\n                \"canonical\": False,\n                \"prefer_specific\": False,\n            },\n            [\"not whitelisted original\"],\n            {\n                \"track\": None,\n                \"album\": None,\n                \"artist\": [\"Jazz\"],\n            },\n            ([\"Jazz\"], \"keep + artist, whitelist\"),\n        ),\n        # canonicalization transforms non-whitelisted genres to canonical forms\n        #\n        # \"Acid Techno\" is not in the default whitelist, thus gets resolved \"up\" in the\n        # tree to \"Techno\" and \"Electronic\".\n        (\n            {\n                \"force\": True,\n                \"keep_existing\": False,\n                \"source\": \"album\",\n                \"whitelist\": True,\n                \"canonical\": True,\n                \"prefer_specific\": False,\n                \"count\": 10,\n            },\n            [],\n            {\n                \"album\": [\"acid techno\"],\n            },\n            ([\"Techno\", \"Electronic\"], \"album, whitelist\"),\n        ),\n        # canonicalization transforms whitelisted genres to canonical forms and\n        # includes originals\n        #\n        # \"Detroit Techno\" is in the default whitelist, thus it stays and and also gets\n        # resolved \"up\" in the tree to \"Techno\" and \"Electronic\". The same happens for\n        # newly fetched genre \"Acid House\".\n        (\n            {\n                \"force\": True,\n                \"keep_existing\": True,\n                \"source\": \"album\",\n                \"whitelist\": True,\n                \"canonical\": True,\n                \"prefer_specific\": False,\n                \"count\": 10,\n                \"extended_debug\": True,\n            },\n            [\"detroit techno\"],\n            {\n                \"album\": [\"acid house\"],\n            },\n            (\n                [\n                    \"Detroit Techno\",\n                    \"Techno\",\n                    \"Electronic\",\n                    \"Acid House\",\n                    \"House\",\n                ],\n                \"keep + album, whitelist\",\n            ),\n        ),\n        # canonicalization transforms non-whitelisted original genres to canonical\n        # forms and deduplication works.\n        #\n        # \"Cosmic Disco\" is not in the default whitelist, thus gets resolved \"up\" in the\n        # tree to \"Disco\" and \"Electronic\". New genre \"Detroit Techno\" resolves to\n        # \"Techno\". Both resolve to \"Electronic\" which gets deduplicated.\n        (\n            {\n                \"force\": True,\n                \"keep_existing\": True,\n                \"source\": \"album\",\n                \"whitelist\": True,\n                \"canonical\": True,\n                \"prefer_specific\": False,\n                \"count\": 10,\n            },\n            [\"Cosmic Disco\"],\n            {\n                \"album\": [\"Detroit Techno\"],\n            },\n            (\n                [\"Disco\", \"Electronic\", \"Detroit Techno\", \"Techno\"],\n                \"keep + album, whitelist\",\n            ),\n        ),\n        # canonicalization transforms non-whitelisted original genres to canonical\n        # forms and deduplication works, **even** when no new genres are found online.\n        #\n        # \"Cosmic Disco\" is not in the default whitelist, thus gets resolved \"up\" in the\n        # tree to \"Disco\" and \"Electronic\".\n        (\n            {\n                \"force\": True,\n                \"keep_existing\": True,\n                \"source\": \"album\",\n                \"whitelist\": True,\n                \"canonical\": True,\n                \"prefer_specific\": False,\n                \"count\": 10,\n            },\n            [\"Cosmic Disco\"],\n            {\n                \"album\": [],\n                \"artist\": [],\n            },\n            (\n                [\"Disco\", \"Electronic\"],\n                \"keep + original fallback, whitelist\",\n            ),\n        ),\n    ],\n)\ndef test_get_genre(\n    config, config_values, item_genre, mock_genres, expected_result\n):\n    \"\"\"Test _get_genre with various configurations.\"\"\"\n\n    def mock_fetch_track_genre(self, trackartist, tracktitle):\n        return mock_genres[\"track\"]\n\n    def mock_fetch_album_genre(self, albumartist, albumtitle):\n        return mock_genres[\"album\"]\n\n    def mock_fetch_artist_genre(self, artist):\n        return mock_genres[\"artist\"]\n\n    # Mock the last.fm fetchers. When whitelist enabled, we can assume only\n    # whitelisted genres get returned, the plugin's _resolve_genre method\n    # ensures it.\n    lastgenre.client.LastFmClient.fetch_track_genre = mock_fetch_track_genre\n    lastgenre.client.LastFmClient.fetch_album_genre = mock_fetch_album_genre\n    lastgenre.client.LastFmClient.fetch_artist_genre = mock_fetch_artist_genre\n\n    # Initialize plugin instance and item\n    plugin = lastgenre.LastGenrePlugin()\n    # Configure\n    plugin.config.set(config_values)\n    plugin.setup()  # Loads default whitelist and canonicalization tree\n\n    item = _common.item()\n    item.genres = item_genre\n\n    # Run\n    assert plugin._get_genre(item) == expected_result\n"
  },
  {
    "path": "test/plugins/test_limit.py",
    "content": "# This file is part of beets.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for the 'limit' plugin.\"\"\"\n\nfrom beets.test.helper import IOMixin, PluginTestCase\n\n\nclass LimitPluginTest(IOMixin, PluginTestCase):\n    \"\"\"Unit tests for LimitPlugin\n\n    Note: query prefix tests do not work correctly with `run_with_output`.\n    \"\"\"\n\n    plugin = \"limit\"\n\n    def setUp(self):\n        super().setUp()\n\n        # we'll create an even number of tracks in the library\n        self.num_test_items = 10\n        assert self.num_test_items % 2 == 0\n        for item_no, item in enumerate(\n            self.add_item_fixtures(count=self.num_test_items)\n        ):\n            item.track = item_no + 1\n            item.store()\n\n        # our limit tests will use half of this number\n        self.num_limit = self.num_test_items // 2\n        self.num_limit_prefix = \"\".join([\"'\", \"<\", str(self.num_limit), \"'\"])\n\n        # a subset of tests has only `num_limit` results, identified by a\n        # range filter on the track number\n        self.track_head_range = f\"track:..{self.num_limit}\"\n        self.track_tail_range = f\"track:{self.num_limit + 1}{'..'}\"\n\n    def test_no_limit(self):\n        \"\"\"Returns all when there is no limit or filter.\"\"\"\n        result = self.run_with_output(\"lslimit\")\n        assert result.count(\"\\n\") == self.num_test_items\n\n    def test_lslimit_head(self):\n        \"\"\"Returns the expected number with `lslimit --head`.\"\"\"\n        result = self.run_with_output(\"lslimit\", \"--head\", str(self.num_limit))\n        assert result.count(\"\\n\") == self.num_limit\n\n    def test_lslimit_tail(self):\n        \"\"\"Returns the expected number with `lslimit --tail`.\"\"\"\n        result = self.run_with_output(\"lslimit\", \"--tail\", str(self.num_limit))\n        assert result.count(\"\\n\") == self.num_limit\n\n    def test_lslimit_head_invariant(self):\n        \"\"\"Returns the expected number with `lslimit --head` and a filter.\"\"\"\n        result = self.run_with_output(\n            \"lslimit\", \"--head\", str(self.num_limit), self.track_tail_range\n        )\n        assert result.count(\"\\n\") == self.num_limit\n\n    def test_lslimit_tail_invariant(self):\n        \"\"\"Returns the expected number with `lslimit --tail` and a filter.\"\"\"\n        result = self.run_with_output(\n            \"lslimit\", \"--tail\", str(self.num_limit), self.track_head_range\n        )\n        assert result.count(\"\\n\") == self.num_limit\n\n    def test_prefix(self):\n        \"\"\"Returns the expected number with the query prefix.\"\"\"\n        result = self.lib.items(self.num_limit_prefix)\n        assert len(result) == self.num_limit\n\n    def test_prefix_when_correctly_ordered(self):\n        \"\"\"Returns the expected number with the query prefix and filter when\n        the prefix portion (correctly) appears last.\"\"\"\n        correct_order = f\"{self.track_tail_range} {self.num_limit_prefix}\"\n        result = self.lib.items(correct_order)\n        assert len(result) == self.num_limit\n\n    def test_prefix_when_incorrectly_ordred(self):\n        \"\"\"Returns no results with the query prefix and filter when the prefix\n        portion (incorrectly) appears first.\"\"\"\n        incorrect_order = f\"{self.num_limit_prefix} {self.track_tail_range}\"\n        result = self.lib.items(incorrect_order)\n        assert len(result) == 0\n"
  },
  {
    "path": "test/plugins/test_listenbrainz.py",
    "content": "import pytest\n\nfrom beets.test.helper import ConfigMixin\nfrom beetsplug.listenbrainz import ListenBrainzPlugin\n\n\nclass TestListenBrainzPlugin(ConfigMixin):\n    @pytest.fixture(scope=\"class\")\n    def plugin(self) -> ListenBrainzPlugin:\n        self.config[\"listenbrainz\"][\"token\"] = \"test_token\"\n        self.config[\"listenbrainz\"][\"username\"] = \"test_user\"\n        return ListenBrainzPlugin()\n\n    @pytest.mark.parametrize(\n        \"search_response, expected_id\",\n        [([{\"id\": \"id1\"}], \"id1\"), ([], None)],\n        ids=[\"found\", \"not_found\"],\n    )\n    def test_get_mb_recording_id(\n        self, plugin, requests_mock, search_response, expected_id\n    ):\n        requests_mock.get(\n            \"/ws/2/recording\", json={\"recordings\": search_response}\n        )\n        track = {\"track_metadata\": {\"track_name\": \"S\", \"release_name\": \"A\"}}\n\n        assert plugin.get_mb_recording_id(track) == expected_id\n\n    def test_get_track_info(self, plugin, requests_mock):\n        requests_mock.get(\n            \"/ws/2/recording/id1?inc=releases%2Bartist-credits\",\n            json={\n                \"title\": \"T\",\n                \"artist-credit\": [],\n                \"releases\": [{\"title\": \"Al\", \"date\": \"2023-01\"}],\n            },\n        )\n\n        assert plugin.get_track_info([{\"identifier\": \"id1\"}]) == [\n            {\n                \"identifier\": \"id1\",\n                \"title\": \"T\",\n                \"artist\": None,\n                \"album\": \"Al\",\n                \"year\": \"2023\",\n            }\n        ]\n"
  },
  {
    "path": "test/plugins/test_lyrics.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Fabrice Laporte.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for the 'lyrics' plugin.\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nimport textwrap\nfrom functools import partial\nfrom http import HTTPStatus\nfrom typing import TYPE_CHECKING\n\nimport pytest\nimport requests\n\nfrom beets.library import Item\nfrom beets.test.helper import PluginMixin, TestHelper\nfrom beets.util.lyrics import Lyrics\nfrom beetsplug import lyrics\n\nfrom .lyrics_pages import lyrics_pages\n\nif TYPE_CHECKING:\n    from pathlib import Path\n\n    from .lyrics_pages import LyricsPage\n\nPHRASE_BY_TITLE = {\n    \"Lady Madonna\": \"friday night arrives without a suitcase\",\n    \"Jazz'n'blues\": \"as i check my balance i kiss the screen\",\n    \"Beets song\": \"via plugins, beets becomes a panacea\",\n}\n\n\n@pytest.fixture(scope=\"module\")\ndef helper():\n    helper = TestHelper()\n    helper.setup_beets()\n    yield helper\n    helper.teardown_beets()\n\n\nclass TestLyricsUtils:\n    @pytest.mark.parametrize(\n        \"artist, title\",\n        [\n            (\"Various Artists\", \"Title\"),\n            (\"Artist\", \"\"),\n            (\"\", \"Title\"),\n            (\" \", \"\"),\n            (\"\", \" \"),\n            (\"\", \"\"),\n        ],\n    )\n    def test_search_empty(self, artist, title):\n        actual_pairs = lyrics.search_pairs(Item(artist=artist, title=title))\n\n        assert not list(actual_pairs)\n\n    @pytest.mark.parametrize(\n        \"artist, artist_sort, expected_extra_artists\",\n        [\n            (\"Alice ft. Bob\", \"\", [\"Alice\"]),\n            (\"Alice feat Bob\", \"\", [\"Alice\"]),\n            (\"Alice feat. Bob\", \"\", [\"Alice\"]),\n            (\"Alice feats Bob\", \"\", []),\n            (\"Alice featuring Bob\", \"\", [\"Alice\"]),\n            (\"Alice & Bob\", \"\", [\"Alice\"]),\n            (\"Alice and Bob\", \"\", [\"Alice\"]),\n            (\"Alice\", \"\", []),\n            (\"Alice\", \"Alice\", []),\n            (\"Alice\", \"alice\", []),\n            (\"Alice\", \"alice \", []),\n            (\"Alice\", \"Alice A\", [\"Alice A\"]),\n            (\"CHVRCHΞS\", \"CHVRCHES\", [\"CHVRCHES\"]),\n            (\"横山克\", \"Masaru Yokoyama\", [\"Masaru Yokoyama\"]),\n        ],\n    )\n    def test_search_pairs_artists(\n        self, artist, artist_sort, expected_extra_artists\n    ):\n        item = Item(artist=artist, artist_sort=artist_sort, title=\"song\")\n\n        actual_artists = [a for a, _ in lyrics.search_pairs(item)]\n\n        # Make sure that the original artist name is still the first entry\n        assert actual_artists == [artist, *expected_extra_artists]\n\n    @pytest.mark.parametrize(\n        \"title, expected_extra_titles\",\n        [\n            (\"1/2\", []),\n            (\"1 / 2\", [\"1\", \"2\"]),\n            (\"Song (live)\", [\"Song\"]),\n            (\"Song (live) (new)\", [\"Song\"]),\n            (\"Song (live (new))\", [\"Song\"]),\n            (\"Song ft. B\", [\"Song\"]),\n            (\"Song featuring B\", [\"Song\"]),\n            (\"Song and B\", []),\n            (\"Song: B\", [\"Song\"]),\n        ],\n    )\n    def test_search_pairs_titles(self, title, expected_extra_titles):\n        item = Item(title=title, artist=\"A\")\n\n        actual_titles = {\n            t: None for _, tit in lyrics.search_pairs(item) for t in tit\n        }\n\n        assert list(actual_titles) == [title, *expected_extra_titles]\n\n    @pytest.mark.parametrize(\n        \"text, expected\",\n        [\n            (\"test\", \"test\"),\n            (\"Mørdag\", \"mordag\"),\n            (\"l'été c'est fait pour jouer\", \"l-ete-c-est-fait-pour-jouer\"),\n            (\"\\xe7afe au lait (boisson)\", \"cafe-au-lait-boisson\"),\n            (\"Multiple  spaces -- and symbols! -- merged\", \"multiple-spaces-and-symbols-merged\"),  # noqa: E501\n            (\"\\u200bno-width-space\", \"no-width-space\"),\n            (\"El\\u002dp\", \"el-p\"),\n            (\"\\u200bblackbear\", \"blackbear\"),\n            (\"\\u200d\", \"\"),\n            (\"\\u2010\", \"\"),\n        ],\n    )  # fmt: skip\n    def test_slug(self, text, expected):\n        assert lyrics.slug(text) == expected\n\n\nclass TestHtml:\n    def test_scrape_strip_cruft(self):\n        initial = \"\"\"<!--lyrics below-->\n                  &nbsp;one\n                  <br class='myclass'>\n                  two  !\n                  <br><br \\\\>\n                  <blink>four</blink>\"\"\"\n        expected = \"<!--lyrics below-->\\none\\ntwo !\\n\\n<blink>four</blink>\"\n\n        assert lyrics.Html.normalize_space(initial) == expected\n\n    def test_scrape_merge_paragraphs(self):\n        text = \"one</p>   <p class='myclass'>two</p><p>three\"\n        expected = \"one\\ntwo\\n\\nthree\"\n\n        assert lyrics.Html.merge_paragraphs(text) == expected\n\n\nclass TestSearchBackend:\n    @pytest.fixture\n    def backend(self, dist_thresh):\n        plugin = lyrics.LyricsPlugin()\n        plugin.config.set({\"dist_thresh\": dist_thresh})\n        return lyrics.SearchBackend(plugin.config, plugin._log)\n\n    @pytest.mark.parametrize(\n        \"dist_thresh, target_artist, artist, should_match\",\n        [\n            (0.11, \"Target Artist\", \"Target Artist\", True),\n            (0.11, \"Target Artist\", \"Target Artis\", True),\n            (0.11, \"Target Artist\", \"Target Arti\", False),\n            (0.11, \"Psychonaut\", \"Psychonaut (BEL)\", True),\n            (0.11, \"beets song\", \"beats song\", True),\n            (0.10, \"beets song\", \"beats song\", False),\n            (\n                0.11,\n                \"Lucid Dreams (Forget Me)\",\n                \"Lucid Dreams (Remix) ft. Lil Uzi Vert\",\n                False,\n            ),\n            (\n                0.12,\n                \"Lucid Dreams (Forget Me)\",\n                \"Lucid Dreams (Remix) ft. Lil Uzi Vert\",\n                True,\n            ),\n        ],\n    )\n    def test_check_match(self, backend, target_artist, artist, should_match):\n        result = lyrics.SearchResult(artist, \"\", \"\")\n\n        assert backend.check_match(target_artist, \"\", result) == should_match\n\n\n@pytest.fixture(scope=\"module\")\ndef lyrics_root_dir(pytestconfig: pytest.Config):\n    return pytestconfig.rootpath / \"test\" / \"rsrc\" / \"lyrics\"\n\n\nclass LyricsPluginMixin(PluginMixin):\n    plugin = \"lyrics\"\n\n    @pytest.fixture\n    def plugin_config(self):\n        \"\"\"Return lyrics configuration to test.\"\"\"\n        return {}\n\n    @pytest.fixture\n    def lyrics_plugin(self, backend_name, plugin_config):\n        \"\"\"Set configuration and returns the plugin's instance.\"\"\"\n        plugin_config[\"sources\"] = [backend_name]\n        self.config[self.plugin].set(plugin_config)\n\n        return lyrics.LyricsPlugin()\n\n\nclass TestLyricsPlugin(LyricsPluginMixin):\n    @pytest.fixture\n    def backend_name(self):\n        \"\"\"Return lyrics configuration to test.\"\"\"\n        return \"lrclib\"\n\n    @pytest.mark.parametrize(\n        \"request_kwargs, expected_log_match\",\n        [\n            (\n                {\"status_code\": HTTPStatus.BAD_GATEWAY},\n                r\"LRCLib: Request error: 502\",\n            ),\n            ({\"text\": \"invalid\"}, r\"LRCLib: Could not decode.*JSON\"),\n        ],\n    )\n    def test_error_handling(\n        self,\n        requests_mock,\n        lyrics_plugin,\n        caplog,\n        request_kwargs,\n        expected_log_match,\n    ):\n        \"\"\"Errors are logged with the backend name.\"\"\"\n        requests_mock.get(lyrics.LRCLib.SEARCH_URL, **request_kwargs)\n\n        assert lyrics_plugin.get_lyrics(\"\", \"\", \"\", 0.0) is None\n        assert caplog.messages\n        last_log = caplog.messages[-1]\n        assert last_log\n        assert re.search(expected_log_match, last_log, re.I)\n\n    @pytest.mark.parametrize(\n        \"plugin_config, old_lyrics, found, expected\",\n        [\n            pytest.param(\n                {},\n                \"old\",\n                \"new\",\n                \"old\",\n                id=\"no_force_keeps_old\",\n            ),\n            pytest.param(\n                {\"force\": True},\n                \"old\",\n                \"new\",\n                \"new\",\n                id=\"force_overwrites_with_new\",\n            ),\n            pytest.param(\n                {\"force\": True, \"local\": True},\n                \"old\",\n                \"new\",\n                \"old\",\n                id=\"force_local_keeps_old\",\n            ),\n            pytest.param(\n                {\"force\": True, \"fallback\": None},\n                \"old\",\n                None,\n                \"old\",\n                id=\"force_fallback_none_keeps_old\",\n            ),\n            pytest.param(\n                {\"force\": True, \"fallback\": \"\"},\n                \"old\",\n                None,\n                \"\",\n                id=\"force_fallback_empty_uses_empty\",\n            ),\n            pytest.param(\n                {\"force\": True, \"fallback\": \"default\"},\n                \"old\",\n                None,\n                \"default\",\n                id=\"force_fallback_default_uses_default\",\n            ),\n            pytest.param(\n                {\"force\": True, \"synced\": True},\n                \"[00:00.00] old synced\",\n                \"new plain\",\n                \"[00:00.00] old synced\",\n                id=\"keep-existing-synced-lyrics\",\n            ),\n            pytest.param(\n                {\"force\": True, \"synced\": True},\n                \"[00:00.00] old synced\",\n                \"[00:00.00] new synced\",\n                \"[00:00.00] new synced\",\n                id=\"replace-with-new-synced-lyrics\",\n            ),\n            pytest.param(\n                {\"force\": True, \"synced\": False},\n                \"[00:00.00] old synced\",\n                \"new plain\",\n                \"new plain\",\n                id=\"replace-with-unsynced-lyrics-when-disabled\",\n            ),\n        ],\n    )\n    def test_overwrite_config(\n        self,\n        monkeypatch,\n        helper,\n        lyrics_plugin,\n        old_lyrics,\n        found,\n        expected,\n    ):\n        monkeypatch.setattr(\n            lyrics_plugin,\n            \"find_lyrics\",\n            lambda _: Lyrics(found) if found is not None else None,\n        )\n        item = helper.create_item(id=1, lyrics=old_lyrics)\n\n        lyrics_plugin.add_item_lyrics(item, False)\n\n        assert item.lyrics == expected\n\n    def test_set_additional_lyrics_info(\n        self, monkeypatch, helper, lyrics_plugin, is_importable\n    ):\n        lyrics = Lyrics(\n            \"sing in the rain every hour of the day\",\n            \"lrclib\",\n            url=\"https://lrclib.net/api/1\",\n        )\n        monkeypatch.setattr(lyrics_plugin, \"find_lyrics\", lambda _: lyrics)\n        item = helper.add_item(\n            id=1, lyrics=\"\", lyrics_translation_language=\"EN\"\n        )\n\n        lyrics_plugin.add_item_lyrics(item, False)\n\n        item = helper.lib.get_item(item.id)\n\n        assert item.lyrics_url == lyrics.url\n        assert item.lyrics_backend == lyrics.backend\n        if is_importable(\"langdetect\"):\n            assert item.lyrics_language == \"EN\"\n        else:\n            with pytest.raises(AttributeError):\n                item.lyrics_language\n        # make sure translation language is cleared\n        with pytest.raises(AttributeError):\n            item.lyrics_translation_language\n\n\nclass LyricsBackendTest(LyricsPluginMixin):\n    @pytest.fixture\n    def backend(self, lyrics_plugin):\n        \"\"\"Return a lyrics backend instance.\"\"\"\n        return lyrics_plugin.backends[0]\n\n    @pytest.fixture\n    def lyrics_html(self, lyrics_root_dir, file_name):\n        return (lyrics_root_dir / f\"{file_name}.txt\").read_text(\n            encoding=\"utf-8\"\n        )\n\n\n@pytest.mark.on_lyrics_update\nclass TestLyricsSources(LyricsBackendTest):\n    @pytest.fixture(scope=\"class\")\n    def plugin_config(self):\n        return {\"google_API_key\": \"test\", \"synced\": True}\n\n    @pytest.fixture(\n        params=[pytest.param(lp, marks=lp.marks) for lp in lyrics_pages],\n        ids=str,\n    )\n    def lyrics_page(self, request):\n        return request.param\n\n    @pytest.fixture\n    def backend_name(self, lyrics_page):\n        return lyrics_page.backend\n\n    @pytest.fixture(autouse=True)\n    def _patch_google_search(self, requests_mock, lyrics_page):\n        \"\"\"Mock the Google Search API to return the lyrics page under test.\"\"\"\n        requests_mock.real_http = True\n\n        data = {\n            \"items\": [\n                {\n                    \"title\": lyrics_page.url_title,\n                    \"link\": lyrics_page.url,\n                    \"displayLink\": lyrics_page.root_url,\n                }\n            ]\n        }\n        requests_mock.get(lyrics.Google.SEARCH_URL, json=data)\n\n    def test_backend_source(\n        self, monkeypatch, lyrics_plugin, lyrics_page: LyricsPage\n    ):\n        \"\"\"Test parsed lyrics from each of the configured lyrics pages.\"\"\"\n        monkeypatch.setattr(\n            \"beetsplug.lyrics.LyricsRequestHandler.create_session\",\n            lambda _: requests.Session(),\n        )\n\n        assert lyrics_plugin.find_lyrics(\n            Item(\n                artist=lyrics_page.artist,\n                title=lyrics_page.track_title,\n                album=\"\",\n                length=186.0,\n            )\n        ) == Lyrics(\n            lyrics_page.lyrics,\n            lyrics_page.backend,\n            url=lyrics_page.url,\n            language=lyrics_page.language,\n        )\n\n\nclass TestGoogleLyrics(LyricsBackendTest):\n    \"\"\"Test scraping heuristics on a fake html page.\"\"\"\n\n    @pytest.fixture(scope=\"class\")\n    def backend_name(self):\n        return \"google\"\n\n    @pytest.fixture\n    def plugin_config(self):\n        return {\"google_API_key\": \"test\"}\n\n    @pytest.fixture(scope=\"class\")\n    def file_name(self):\n        return \"examplecom/beetssong\"\n\n    @pytest.fixture\n    def search_item(self, url_title, url):\n        return {\"title\": url_title, \"link\": url}\n\n    @pytest.mark.parametrize(\"plugin_config\", [{}])\n    def test_disabled_without_api_key(self, lyrics_plugin):\n        assert not lyrics_plugin.backends\n\n    def test_mocked_source_ok(self, backend, lyrics_html):\n        \"\"\"Test that lyrics of the mocked page are correctly scraped\"\"\"\n        result = backend.scrape(lyrics_html).lower()\n\n        assert result\n        assert PHRASE_BY_TITLE[\"Beets song\"] in result\n\n    @pytest.mark.parametrize(\n        \"url_title, expected_artist, expected_title\",\n        [\n            (\"Artist - beets song Lyrics\", \"Artist\", \"beets song\"),\n            (\"www.azlyrics.com | Beats song by Artist\", \"Artist\", \"Beats song\"),\n            (\"lyric.com | seets bong lyrics by Artist\", \"Artist\", \"seets bong\"),\n            (\"foo\", \"\", \"foo\"),\n            (\"Artist - Beets Song lyrics | AZLyrics\", \"Artist\", \"Beets Song\"),\n            (\"Letra de Artist - Beets Song\", \"Artist\", \"Beets Song\"),\n            (\"Letra de Artist - Beets ...\", \"Artist\", \"Beets\"),\n            (\"Artist Beets Song\", \"Artist\", \"Beets Song\"),\n            (\"BeetsSong - Artist\", \"Artist\", \"BeetsSong\"),\n            (\"Artist - BeetsSong\", \"Artist\", \"BeetsSong\"),\n            (\"Beets Song\", \"\", \"Beets Song\"),\n            (\"Beets Song Artist\", \"Artist\", \"Beets Song\"),\n            (\n                \"BeetsSong (feat. Other & Another) - Artist\",\n                \"Artist\",\n                \"BeetsSong (feat. Other & Another)\",\n            ),\n            (\n                (\n                    \"Beets song lyrics by Artist - original song full text. \"\n                    \"Official Beets song lyrics, 2024 version | LyricsMode.com\"\n                ),\n                \"Artist\",\n                \"Beets song\",\n            ),\n        ],\n    )\n    @pytest.mark.parametrize(\"url\", [\"http://doesntmatter.com\"])\n    def test_make_search_result(\n        self, backend, search_item, expected_artist, expected_title\n    ):\n        result = backend.make_search_result(\"Artist\", \"Beets song\", search_item)\n\n        assert result.artist == expected_artist\n        assert result.title == expected_title\n\n\nclass TestGeniusLyrics(LyricsBackendTest):\n    @pytest.fixture(scope=\"class\")\n    def backend_name(self):\n        return \"genius\"\n\n    @pytest.mark.parametrize(\n        \"file_name, expected_line_count\",\n        [\n            (\"geniuscom/2pacalleyezonmelyrics\", 131),\n            (\"geniuscom/Ttngchinchillalyrics\", 29),\n            (\"geniuscom/sample\", 0),  # see https://github.com/beetbox/beets/issues/3535\n        ],\n    )  # fmt: skip\n    def test_scrape(self, backend, lyrics_html, expected_line_count):\n        result = backend.scrape(lyrics_html) or \"\"\n\n        assert len(result.splitlines()) == expected_line_count\n\n\nclass TestTekstowoLyrics(LyricsBackendTest):\n    @pytest.fixture(scope=\"class\")\n    def backend_name(self):\n        return \"tekstowo\"\n\n    @pytest.mark.parametrize(\n        \"file_name, expecting_lyrics\",\n        [\n            (\"tekstowopl/piosenka24kgoldncityofangels1\", True),\n            (\n                \"tekstowopl/piosenkabeethovenbeethovenpianosonata17tempestthe3rdmovement\",\n                False,\n            ),\n        ],\n    )\n    def test_scrape(self, backend, lyrics_html, expecting_lyrics):\n        assert bool(backend.scrape(lyrics_html)) == expecting_lyrics\n\n\nLYRICS_DURATION = 950\n\n\ndef lyrics_match(**overrides):\n    return {\n        \"id\": 1,\n        \"instrumental\": False,\n        \"duration\": LYRICS_DURATION,\n        \"syncedLyrics\": \"[00:00.00] synced\",\n        \"plainLyrics\": \"plain\",\n        **overrides,\n    }\n\n\nclass TestLRCLibLyrics(LyricsBackendTest):\n    ITEM_DURATION = 999\n    SYNCED = \"[00:00.00] synced\"\n\n    @pytest.fixture(scope=\"class\")\n    def backend_name(self):\n        return \"lrclib\"\n\n    @pytest.fixture\n    def fetch_lyrics(self, backend, requests_mock, response_data):\n        requests_mock.get(backend.GET_URL, status_code=HTTPStatus.NOT_FOUND)\n        requests_mock.get(backend.SEARCH_URL, json=response_data)\n\n        return partial(backend.fetch, \"la\", \"la\", \"la\", self.ITEM_DURATION)\n\n    @pytest.mark.parametrize(\"response_data\", [[lyrics_match()]])\n    @pytest.mark.parametrize(\n        \"plugin_config, expected_lyrics\",\n        [\n            pytest.param({\"synced\": True}, SYNCED, id=\"pick-synced\"),\n            pytest.param({\"synced\": False}, \"plain\", id=\"pick-plain\"),\n        ],\n    )\n    def test_synced_config_option(\n        self, backend_name, fetch_lyrics, expected_lyrics\n    ):\n        lyrics = fetch_lyrics()\n\n        assert lyrics\n        assert lyrics.text == expected_lyrics\n        assert lyrics.backend == backend_name\n\n    @pytest.mark.parametrize(\n        \"response_data, expected_lyrics\",\n        [\n            pytest.param([], None, id=\"handle non-matching lyrics\"),\n            pytest.param(\n                [lyrics_match()],\n                SYNCED,\n                id=\"synced when available\",\n            ),\n            pytest.param(\n                [lyrics_match(duration=1)],\n                None,\n                id=\"none: duration too short\",\n            ),\n            pytest.param(\n                [lyrics_match(instrumental=True)],\n                \"[Instrumental]\",\n                id=\"instrumental track\",\n            ),\n            pytest.param(\n                [lyrics_match(syncedLyrics=None)],\n                \"plain\",\n                id=\"plain by default\",\n            ),\n            pytest.param(\n                [\n                    lyrics_match(\n                        duration=ITEM_DURATION,\n                        syncedLyrics=None,\n                        plainLyrics=\"plain with closer duration\",\n                    ),\n                    lyrics_match(syncedLyrics=SYNCED, plainLyrics=\"plain 2\"),\n                ],\n                SYNCED,\n                id=\"prefer synced lyrics even if plain duration is closer\",\n            ),\n            pytest.param(\n                [\n                    lyrics_match(\n                        duration=ITEM_DURATION,\n                        syncedLyrics=None,\n                        plainLyrics=\"valid plain\",\n                    ),\n                    lyrics_match(\n                        duration=1,\n                        syncedLyrics=\"synced with invalid duration\",\n                    ),\n                ],\n                \"valid plain\",\n                id=\"ignore synced with invalid duration\",\n            ),\n            pytest.param(\n                [\n                    lyrics_match(\n                        duration=59, syncedLyrics=\"[01:00.00] invalid synced\"\n                    )\n                ],\n                None,\n                id=\"ignore synced with a timestamp longer than duration\",\n            ),\n            pytest.param(\n                [lyrics_match(syncedLyrics=None), lyrics_match()],\n                SYNCED,\n                id=\"prefer match with synced lyrics\",\n            ),\n        ],\n    )\n    @pytest.mark.parametrize(\"plugin_config\", [{\"synced\": True}])\n    def test_fetch_lyrics(self, fetch_lyrics, expected_lyrics):\n        lyrics = fetch_lyrics()\n        if expected_lyrics is None:\n            assert not lyrics\n        else:\n            assert lyrics\n            assert lyrics.text == expected_lyrics\n\n\n@pytest.mark.requires_import(\"langdetect\")\nclass TestTranslation:\n    @pytest.fixture(autouse=True)\n    def _patch_bing(self, requests_mock):\n        def callback(request, _):\n            if b\"Refrain\" in request.body:\n                translations = (\n                    \"\"\n                    \" | [Refrain : Doja Cat]\"\n                    \" | Difficile pour moi de te laisser partir (Te laisser partir, te laisser partir)\"  # noqa: E501\n                    \" | Mon corps ne me laissait pas le cacher (Cachez-le)\"\n                    \" | [Chorus]\"\n                    \" | Quoi qu’il arrive, je ne plierais pas (Ne plierait pas, ne plierais pas)\"  # noqa: E501\n                    \" | Chevauchant à travers le tonnerre, la foudre\"\n                )\n            elif b\"00:00.00\" in request.body:\n                translations = (\n                    \"\"\n                    \" | [00:00.00] Quelques paroles synchronisées\"\n                    \" | [00:01.00] Quelques paroles plus synchronisées\"\n                )\n            else:\n                translations = (\n                    \"\"\n                    \" | Quelques paroles synchronisées\"\n                    \" | Quelques paroles plus synchronisées\"\n                )\n\n            return [\n                {\n                    \"detectedLanguage\": {\"language\": \"en\", \"score\": 1.0},\n                    \"translations\": [{\"text\": translations, \"to\": \"fr\"}],\n                }\n            ]\n\n        requests_mock.post(lyrics.Translator.TRANSLATE_URL, json=callback)\n\n    @pytest.mark.parametrize(\n        \"new_lyrics, old_lyrics, expected\",\n        [\n            pytest.param(\n                \"\"\"\n                [Refrain: Doja Cat]\n                Hard for me to let you go (Let you go, let you go)\n                My body wouldn't let me hide it (Hide it)\n                [Chorus]\n                No matter what, I wouldn't fold (Wouldn't fold, wouldn't fold)\n                Ridin' through the thunder, lightnin'\"\"\",\n                Lyrics(\"\"),\n                \"\"\"\n                [Refrain: Doja Cat] / [Refrain : Doja Cat]\n                Hard for me to let you go (Let you go, let you go) / Difficile pour moi de te laisser partir (Te laisser partir, te laisser partir)\n                My body wouldn't let me hide it (Hide it) / Mon corps ne me laissait pas le cacher (Cachez-le)\n                [Chorus]\n                No matter what, I wouldn't fold (Wouldn't fold, wouldn't fold) / Quoi qu’il arrive, je ne plierais pas (Ne plierait pas, ne plierais pas)\n                Ridin' through the thunder, lightnin' / Chevauchant à travers le tonnerre, la foudre\"\"\",  # noqa: E501\n                id=\"plain\",\n            ),\n            pytest.param(\n                \"\"\"\n                [00:00.00] Some synced lyrics\n                [00:00.50]\n                [00:01.00] Some more synced lyrics\n                \"\"\",\n                Lyrics(\"\"),\n                \"\"\"\n                [00:00.00] Some synced lyrics / Quelques paroles synchronisées\n                [00:00.50]\n                [00:01.00] Some more synced lyrics / Quelques paroles plus synchronisées\"\"\",  # noqa: E501\n                id=\"synced\",\n            ),\n            pytest.param(\n                \"Quelques paroles\",\n                Lyrics(\"\"),\n                \"Quelques paroles\",\n                id=\"already in the target language\",\n            ),\n            pytest.param(\n                \"Some lyrics\",\n                Lyrics(\n                    \"Some lyrics / Some translation\",\n                    language=\"EN\",\n                    translation_language=\"FR\",\n                ),\n                \"Some lyrics / Some translation\",\n                id=\"already translated\",\n            ),\n        ],\n    )\n    def test_translate(self, new_lyrics, old_lyrics, expected):\n        plugin = lyrics.LyricsPlugin()\n        bing = lyrics.Translator(plugin._log, \"123\", \"FR\", [\"EN\"])\n\n        assert bing.translate(\n            Lyrics(textwrap.dedent(new_lyrics)), old_lyrics\n        ).full_text == textwrap.dedent(expected)\n\n\nclass TestRestFiles:\n    @pytest.fixture\n    def rest_dir(self, tmp_path):\n        return tmp_path\n\n    @pytest.fixture\n    def rest_files(self, rest_dir):\n        return lyrics.RestFiles(rest_dir)\n\n    def test_write(self, rest_dir: Path, rest_files):\n        items = [\n            Item(albumartist=aa, album=a, title=t, lyrics=lyr)\n            for aa, a, t, lyr in [\n                (\"Artist One\", \"Album One\", \"Song One\", \"Lyrics One\"),\n                (\"Artist One\", \"Album One\", \"Song Two\", \"Lyrics Two\"),\n                (\"Artist Two\", \"Album Two\", \"Song Three\", \"Lyrics Three\"),\n            ]\n        ]\n\n        rest_files.write(items)\n\n        assert (rest_dir / \"index.rst\").exists()\n        assert (rest_dir / \"conf.py\").exists()\n\n        artist_one_file = rest_dir / \"artists\" / \"artist-one.rst\"\n        artist_two_file = rest_dir / \"artists\" / \"artist-two.rst\"\n        assert artist_one_file.exists()\n        assert artist_two_file.exists()\n\n        c = artist_one_file.read_text()\n        assert (\n            c.index(\"Artist One\")\n            < c.index(\"Album One\")\n            < c.index(\"Song One\")\n            < c.index(\"Lyrics One\")\n            < c.index(\"Song Two\")\n            < c.index(\"Lyrics Two\")\n        )\n\n        c = artist_two_file.read_text()\n        assert (\n            c.index(\"Artist Two\")\n            < c.index(\"Album Two\")\n            < c.index(\"Song Three\")\n            < c.index(\"Lyrics Three\")\n        )\n"
  },
  {
    "path": "test/plugins/test_mbcollection.py",
    "content": "import re\nimport uuid\nfrom contextlib import nullcontext as does_not_raise\n\nimport pytest\n\nfrom beets.library import Album\nfrom beets.test.helper import PluginMixin, TestHelper\nfrom beets.ui import UserError\nfrom beetsplug import mbcollection\n\n\nclass TestMbCollectionPlugin(PluginMixin, TestHelper):\n    \"\"\"Tests for the MusicBrainzCollectionPlugin class methods.\"\"\"\n\n    plugin = \"mbcollection\"\n\n    COLLECTION_ID = str(uuid.uuid4())\n\n    @pytest.fixture(autouse=True)\n    def setup_config(self):\n        self.config[\"musicbrainz\"][\"user\"] = \"testuser\"\n        self.config[\"musicbrainz\"][\"pass\"] = \"testpass\"\n        self.config[\"mbcollection\"][\"collection\"] = self.COLLECTION_ID\n\n    @pytest.fixture(autouse=True)\n    def helper(self):\n        self.setup_beets()\n\n        yield self\n\n        self.teardown_beets()\n\n    @pytest.mark.parametrize(\n        \"user_collections,expectation\",\n        [\n            (\n                [],\n                pytest.raises(\n                    UserError, match=r\"no collections exist for user\"\n                ),\n            ),\n            (\n                [{\"id\": \"c1\", \"entity-type\": \"event\"}],\n                pytest.raises(UserError, match=r\"No release collection found.\"),\n            ),\n            (\n                [{\"id\": \"c1\", \"entity-type\": \"release\"}],\n                pytest.raises(UserError, match=r\"invalid collection ID\"),\n            ),\n            (\n                [{\"id\": COLLECTION_ID, \"entity-type\": \"release\"}],\n                does_not_raise(),\n            ),\n        ],\n        ids=[\"no collections\", \"no release collections\", \"invalid ID\", \"valid\"],\n    )\n    def test_get_collection_validation(\n        self, requests_mock, user_collections, expectation\n    ):\n        requests_mock.get(\n            \"/ws/2/collection\", json={\"collections\": user_collections}\n        )\n\n        with expectation:\n            mbcollection.MusicBrainzCollectionPlugin().collection\n\n    def test_mbupdate(self, helper, requests_mock, monkeypatch):\n        \"\"\"Verify mbupdate sync of a MusicBrainz collection with the library.\n\n        This test ensures that the command:\n        - fetches collection releases using paginated requests,\n        - submits releases that exist locally but are missing from the remote\n          collection\n        - and removes releases from the remote collection that are not in the\n          local library. Small chunk sizes are forced to exercise pagination and\n          batching logic.\n        \"\"\"\n        for mb_albumid in [\n            # already present in remote collection\n            \"in_collection1\",\n            \"in_collection2\",\n            # two new albums not in remote collection\n            \"00000000-0000-0000-0000-000000000001\",\n            \"00000000-0000-0000-0000-000000000002\",\n        ]:\n            helper.lib.add(Album(mb_albumid=mb_albumid))\n\n        # The relevant collection\n        requests_mock.get(\n            \"/ws/2/collection\",\n            json={\n                \"collections\": [\n                    {\n                        \"id\": self.COLLECTION_ID,\n                        \"entity-type\": \"release\",\n                        \"release-count\": 3,\n                    }\n                ]\n            },\n        )\n\n        collection_releases = f\"/ws/2/collection/{self.COLLECTION_ID}/releases\"\n        # Force small fetch chunk to require multiple paged requests.\n        monkeypatch.setattr(\n            \"beetsplug.mbcollection.MBCollection.FETCH_CHUNK_SIZE\", 2\n        )\n        # 3 releases are fetched in two pages.\n        requests_mock.get(\n            re.compile(rf\".*{collection_releases}\\b.*&offset=0.*\"),\n            json={\n                \"releases\": [{\"id\": \"in_collection1\"}, {\"id\": \"not_in_library\"}]\n            },\n        )\n        requests_mock.get(\n            re.compile(rf\".*{collection_releases}\\b.*&offset=2.*\"),\n            json={\"releases\": [{\"id\": \"in_collection2\"}]},\n        )\n\n        # Force small submission chunk\n        monkeypatch.setattr(\n            \"beetsplug.mbcollection.MBCollection.SUBMISSION_CHUNK_SIZE\", 1\n        )\n        # so that releases are added using two requests\n        requests_mock.put(\n            re.compile(\n                rf\".*{collection_releases}/00000000-0000-0000-0000-000000000001\"\n            )\n        )\n        requests_mock.put(\n            re.compile(\n                rf\".*{collection_releases}/00000000-0000-0000-0000-000000000002\"\n            )\n        )\n        # and finally, one release is removed\n        requests_mock.delete(\n            re.compile(rf\".*{collection_releases}/not_in_library\")\n        )\n\n        helper.run_command(\"mbupdate\", \"--remove\")\n\n        assert requests_mock.call_count == 6\n"
  },
  {
    "path": "test/plugins/test_mbpseudo.py",
    "content": "from __future__ import annotations\n\nimport json\nfrom copy import deepcopy\nfrom typing import TYPE_CHECKING\n\nimport pytest\n\nfrom beets.autotag import AlbumMatch\nfrom beets.autotag.distance import Distance\nfrom beets.autotag.hooks import AlbumInfo, TrackInfo\nfrom beets.library import Item\nfrom beets.test.helper import PluginMixin\nfrom beetsplug.mbpseudo import (\n    _STATUS_PSEUDO,\n    MusicBrainzPseudoReleasePlugin,\n    PseudoAlbumInfo,\n)\n\nif TYPE_CHECKING:\n    import pathlib\n\n    from beetsplug._typing import JSONDict\n\n\n@pytest.fixture(scope=\"module\")\ndef rsrc_dir(pytestconfig: pytest.Config):\n    return pytestconfig.rootpath / \"test\" / \"rsrc\" / \"mbpseudo\"\n\n\n@pytest.fixture\ndef official_release(rsrc_dir: pathlib.Path) -> JSONDict:\n    info_json = (rsrc_dir / \"official_release.json\").read_text(encoding=\"utf-8\")\n    return json.loads(info_json)\n\n\n@pytest.fixture\ndef pseudo_release(rsrc_dir: pathlib.Path) -> JSONDict:\n    info_json = (rsrc_dir / \"pseudo_release.json\").read_text(encoding=\"utf-8\")\n    return json.loads(info_json)\n\n\n@pytest.fixture\ndef official_release_info() -> AlbumInfo:\n    return AlbumInfo(\n        tracks=[TrackInfo(title=\"百花繚乱\")],\n        album_id=\"official\",\n        album=\"百花繚乱\",\n    )\n\n\n@pytest.fixture\ndef pseudo_release_info() -> AlbumInfo:\n    return AlbumInfo(\n        tracks=[TrackInfo(title=\"In Bloom\")],\n        album_id=\"pseudo\",\n        album=\"In Bloom\",\n    )\n\n\n@pytest.mark.usefixtures(\"config\")\nclass TestPseudoAlbumInfo:\n    def test_album_id_always_from_pseudo(\n        self, official_release_info: AlbumInfo, pseudo_release_info: AlbumInfo\n    ):\n        info = PseudoAlbumInfo(pseudo_release_info, official_release_info)\n        info.use_official_as_ref()\n        assert info.album_id == \"pseudo\"\n\n    def test_get_attr_from_pseudo(\n        self, official_release_info: AlbumInfo, pseudo_release_info: AlbumInfo\n    ):\n        info = PseudoAlbumInfo(pseudo_release_info, official_release_info)\n        assert info.album == \"In Bloom\"\n\n    def test_get_attr_from_official(\n        self, official_release_info: AlbumInfo, pseudo_release_info: AlbumInfo\n    ):\n        info = PseudoAlbumInfo(pseudo_release_info, official_release_info)\n        info.use_official_as_ref()\n        assert info.album == info.get_official_release().album\n\n    def test_determine_best_ref(\n        self, official_release_info: AlbumInfo, pseudo_release_info: AlbumInfo\n    ):\n        info = PseudoAlbumInfo(\n            pseudo_release_info, official_release_info, data_source=\"test\"\n        )\n        item = Item(title=\"百花繚乱\")\n\n        assert info.determine_best_ref([item]) == \"official\"\n\n        info.use_pseudo_as_ref()\n        assert info.data_source == \"test\"\n\n\nclass TestMBPseudoMixin(PluginMixin):\n    plugin = \"mbpseudo\"\n\n    @pytest.fixture(autouse=True)\n    def patch_get_release(self, monkeypatch, pseudo_release: JSONDict):\n        monkeypatch.setattr(\n            \"beetsplug._utils.musicbrainz.MusicBrainzAPI.get_release\",\n            lambda _, album_id: deepcopy(\n                {pseudo_release[\"id\"]: pseudo_release}[album_id]\n            ),\n        )\n\n    @pytest.fixture(scope=\"class\")\n    def plugin_config(self):\n        return {\"scripts\": [\"Latn\", \"Dummy\"]}\n\n    @pytest.fixture\n    def mbpseudo_plugin(self, plugin_config) -> MusicBrainzPseudoReleasePlugin:\n        self.config[self.plugin].set(plugin_config)\n        return MusicBrainzPseudoReleasePlugin()\n\n\nclass TestMBPseudoPlugin(TestMBPseudoMixin):\n    def test_scripts_init(\n        self, mbpseudo_plugin: MusicBrainzPseudoReleasePlugin\n    ):\n        assert mbpseudo_plugin._scripts == [\"Latn\", \"Dummy\"]\n\n    @pytest.mark.parametrize(\n        \"album_id\",\n        [\n            \"a5ce1d11-2e32-45a4-b37f-c1589d46b103\",\n            \"-5ce1d11-2e32-45a4-b37f-c1589d46b103\",\n        ],\n    )\n    def test_extract_id_uses_music_brainz_pattern(\n        self,\n        mbpseudo_plugin: MusicBrainzPseudoReleasePlugin,\n        album_id: str,\n    ):\n        if album_id.startswith(\"-\"):\n            assert mbpseudo_plugin._extract_id(album_id) is None\n        else:\n            assert mbpseudo_plugin._extract_id(album_id) == album_id\n\n    def test_album_info_for_pseudo_release(\n        self,\n        mbpseudo_plugin: MusicBrainzPseudoReleasePlugin,\n        pseudo_release: JSONDict,\n    ):\n        album_info = mbpseudo_plugin.album_info(pseudo_release)\n        assert not isinstance(album_info, PseudoAlbumInfo)\n        assert album_info.data_source == \"MusicBrainzPseudoRelease\"\n        assert album_info.albumstatus == _STATUS_PSEUDO\n\n    @pytest.mark.parametrize(\n        \"json_key\",\n        [\n            \"type\",\n            \"direction\",\n            \"release\",\n        ],\n    )\n    def test_interception_skip_when_rel_values_dont_match(\n        self,\n        mbpseudo_plugin: MusicBrainzPseudoReleasePlugin,\n        official_release: JSONDict,\n        json_key: str,\n    ):\n        del official_release[\"release-relations\"][0][json_key]\n\n        album_info = mbpseudo_plugin.album_info(official_release)\n        assert not isinstance(album_info, PseudoAlbumInfo)\n        assert album_info.data_source == \"MusicBrainzPseudoRelease\"\n\n    def test_interception_skip_when_script_doesnt_match(\n        self,\n        mbpseudo_plugin: MusicBrainzPseudoReleasePlugin,\n        official_release: JSONDict,\n    ):\n        official_release[\"release-relations\"][0][\"release\"][\n            \"text-representation\"\n        ][\"script\"] = \"Null\"\n\n        album_info = mbpseudo_plugin.album_info(official_release)\n        assert not isinstance(album_info, PseudoAlbumInfo)\n        assert album_info.data_source == \"MusicBrainzPseudoRelease\"\n\n    def test_interception(\n        self,\n        mbpseudo_plugin: MusicBrainzPseudoReleasePlugin,\n        official_release: JSONDict,\n    ):\n        album_info = mbpseudo_plugin.album_info(official_release)\n        assert isinstance(album_info, PseudoAlbumInfo)\n        assert album_info.data_source == \"MusicBrainzPseudoRelease\"\n\n    def test_final_adjustment_skip(\n        self,\n        mbpseudo_plugin: MusicBrainzPseudoReleasePlugin,\n    ):\n        match = AlbumMatch(\n            distance=Distance(),\n            info=AlbumInfo(tracks=[], data_source=\"mb\"),\n            mapping={},\n            extra_items=[],\n            extra_tracks=[],\n        )\n\n        mbpseudo_plugin._adjust_final_album_match(match)\n        assert match.info.data_source == \"mb\"\n\n    def test_final_adjustment(\n        self,\n        mbpseudo_plugin: MusicBrainzPseudoReleasePlugin,\n        official_release_info: AlbumInfo,\n        pseudo_release_info: AlbumInfo,\n    ):\n        pseudo_album_info = PseudoAlbumInfo(\n            pseudo_release=pseudo_release_info,\n            official_release=official_release_info,\n            data_source=mbpseudo_plugin.data_source,\n        )\n        pseudo_album_info.use_official_as_ref()\n\n        item = Item()\n        item[\"title\"] = \"百花繚乱\"\n\n        match = AlbumMatch(\n            distance=Distance(),\n            info=pseudo_album_info,\n            mapping={item: pseudo_album_info.tracks[0]},\n            extra_items=[],\n            extra_tracks=[],\n        )\n\n        mbpseudo_plugin._adjust_final_album_match(match)\n\n        assert match.info.data_source == \"MusicBrainz\"\n        assert match.info.album_id == \"pseudo\"\n        assert match.info.album == \"In Bloom\"\n\n\nclass TestMBPseudoPluginCustomTagsOnly(TestMBPseudoMixin):\n    @pytest.fixture(scope=\"class\")\n    def plugin_config(self):\n        return {\"scripts\": [\"Latn\", \"Dummy\"], \"custom_tags_only\": True}\n\n    def test_custom_tags(\n        self,\n        config,\n        mbpseudo_plugin: MusicBrainzPseudoReleasePlugin,\n        official_release: JSONDict,\n    ):\n        config[\"import\"][\"languages\"] = [\"en\", \"jp\"]\n        album_info = mbpseudo_plugin.album_info(official_release)\n        assert not isinstance(album_info, PseudoAlbumInfo)\n        assert album_info.data_source == \"MusicBrainzPseudoRelease\"\n        assert album_info[\"album_transl\"] == \"In Bloom\"\n        assert album_info[\"album_artist_transl\"] == \"Lilas Ikuta\"\n        assert album_info.tracks[0][\"title_transl\"] == \"In Bloom\"\n        assert album_info.tracks[0][\"artist_transl\"] == \"Lilas Ikuta\"\n\n    def test_custom_tags_with_import_languages(\n        self,\n        config,\n        mbpseudo_plugin: MusicBrainzPseudoReleasePlugin,\n        official_release: JSONDict,\n    ):\n        config[\"import\"][\"languages\"] = []\n        album_info = mbpseudo_plugin.album_info(official_release)\n        assert not isinstance(album_info, PseudoAlbumInfo)\n        assert album_info.data_source == \"MusicBrainzPseudoRelease\"\n        assert album_info[\"album_transl\"] == \"In Bloom\"\n        assert album_info[\"album_artist_transl\"] == \"Lilas Ikuta\"\n        assert album_info.tracks[0][\"title_transl\"] == \"In Bloom\"\n        assert album_info.tracks[0][\"artist_transl\"] == \"Lilas Ikuta\"\n"
  },
  {
    "path": "test/plugins/test_mbsubmit.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson and Diego Moreda.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nfrom beets.test.helper import (\n    AutotagImportTestCase,\n    PluginMixin,\n    TerminalImportMixin,\n)\n\n\nclass MBSubmitPluginTest(\n    PluginMixin, TerminalImportMixin, AutotagImportTestCase\n):\n    plugin = \"mbsubmit\"\n\n    def setUp(self):\n        super().setUp()\n        self.prepare_album_for_import(2)\n        self.setup_importer()\n\n    def test_print_tracks_output(self):\n        \"\"\"Test the output of the \"print tracks\" choice.\"\"\"\n        self.io.addinput(\"p\")\n        self.io.addinput(\"s\")\n        # Print tracks; Skip\n        self.importer.run()\n\n        # Manually build the string for comparing the output.\n        tracklist = (\n            \"Open files with Picard? \"\n            \"01. Tag Track 1 - Tag Artist (0:01)\\n\"\n            \"02. Tag Track 2 - Tag Artist (0:01)\"\n        )\n        assert tracklist in self.io.getoutput()\n\n    def test_print_tracks_output_as_tracks(self):\n        \"\"\"Test the output of the \"print tracks\" choice, as singletons.\"\"\"\n        self.io.addinput(\"t\")\n        self.io.addinput(\"s\")\n        self.io.addinput(\"p\")\n        self.io.addinput(\"s\")\n        # as Tracks; Skip; Print tracks; Skip\n        self.importer.run()\n\n        # Manually build the string for comparing the output.\n        tracklist = (\n            \"Open files with Picard? 02. Tag Track 2 - Tag Artist (0:01)\"\n        )\n        assert tracklist in self.io.getoutput()\n"
  },
  {
    "path": "test/plugins/test_mbsync.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Thomas Scholtes.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\nfrom unittest.mock import Mock, patch\n\nfrom beets.autotag.hooks import AlbumInfo, TrackInfo\nfrom beets.library import Item\nfrom beets.test.helper import PluginTestCase, capture_log\n\n\nclass MbsyncCliTest(PluginTestCase):\n    plugin = \"mbsync\"\n\n    @patch(\n        \"beets.metadata_plugins.album_for_id\",\n        Mock(\n            side_effect=lambda *_: AlbumInfo(\n                album_id=\"album id\",\n                album=\"new album\",\n                tracks=[TrackInfo(track_id=\"track id\", title=\"new title\")],\n            )\n        ),\n    )\n    @patch(\n        \"beets.metadata_plugins.track_for_id\",\n        Mock(\n            side_effect=lambda *_: TrackInfo(\n                track_id=\"singleton id\", title=\"new title\"\n            )\n        ),\n    )\n    def test_update_library(self):\n        album_item = Item(\n            album=\"old album\",\n            mb_albumid=\"album id\",\n            mb_trackid=\"track id\",\n            data_source=\"data_source\",\n        )\n        self.lib.add_album([album_item])\n\n        singleton = Item(\n            title=\"old title\",\n            mb_trackid=\"singleton id\",\n            data_source=\"data_source\",\n        )\n        self.lib.add(singleton)\n\n        self.run_command(\"mbsync\")\n\n        singleton.load()\n        assert singleton.title == \"new title\"\n\n        album_item.load()\n        assert album_item.title == \"new title\"\n        assert album_item.mb_trackid == \"track id\"\n        assert album_item.get_album().album == \"new album\"\n\n    def test_custom_format(self):\n        for item in [\n            Item(artist=\"albumartist\", album=\"no id\"),\n            Item(\n                artist=\"albumartist\",\n                album=\"invalid id\",\n                mb_albumid=\"a1b2c3d4\",\n            ),\n        ]:\n            self.lib.add_album([item])\n\n        for item in [\n            Item(artist=\"artist\", title=\"no id\"),\n            Item(artist=\"artist\", title=\"invalid id\", mb_trackid=\"a1b2c3d4\"),\n        ]:\n            self.lib.add(item)\n\n        with capture_log(\"beets.mbsync\") as logs:\n            self.run_command(\"mbsync\", \"-f\", \"'%if{$album,$album,$title}'\")\n\n        assert \"mbsync: Skipping album with no mb_albumid: 'no id'\" in logs\n        assert \"mbsync: Skipping singleton with no mb_trackid: 'no id'\" in logs\n"
  },
  {
    "path": "test/plugins/test_missing.py",
    "content": "\"\"\"Tests for the `missing` plugin.\"\"\"\n\nimport re\nimport uuid\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom beets.autotag.hooks import AlbumInfo, TrackInfo\nfrom beets.library import Album, Item\nfrom beets.test.helper import IOMixin, PluginMixin, TestHelper\n\n\n@pytest.fixture\ndef helper(request):\n    helper = TestHelper()\n    helper.setup_beets()\n\n    request.instance.lib = helper.lib\n\n    yield\n\n    helper.teardown_beets()\n\n\n@pytest.mark.usefixtures(\"helper\")\nclass TestMissingAlbums(IOMixin, PluginMixin):\n    \"\"\"Tests for missing albums functionality.\"\"\"\n\n    plugin = \"missing\"\n\n    @pytest.mark.parametrize(\n        \"release_from_mb,expected_output\",\n        [\n            pytest.param(\n                {\"id\": \"other\", \"title\": \"Other Album\"},\n                \"Artist - Other Album\\n\",\n                id=\"missing\",\n            ),\n            pytest.param(\n                {\"id\": \"release_group_in_lib\", \"title\": \"Album\"},\n                \"\",\n                id=\"not missing\",\n            ),\n        ],\n    )\n    def test_missing_artist_albums(\n        self, requests_mock, release_from_mb, expected_output\n    ):\n        artist_mbid = str(uuid.uuid4())\n        self.lib.add(\n            Album(\n                album=\"Album\",\n                albumartist=\"Artist\",\n                mb_albumartistid=artist_mbid,\n                mb_albumid=\"album\",\n                mb_releasegroupid=\"release_group_in_lib\",\n            )\n        )\n        requests_mock.get(\n            re.compile(\n                rf\"/ws/2/release-group\\?artist={artist_mbid}&.*type=album\"\n            ),\n            json={\"release-groups\": [release_from_mb]},\n        )\n\n        with self.configure_plugin({}):\n            assert self.run_with_output(\"missing\", \"--album\") == expected_output\n\n    def test_release_types_filters_results(self, requests_mock):\n        \"\"\"Test --release-types filters to only show specified type.\"\"\"\n        artist_mbid = str(uuid.uuid4())\n        self.lib.add(\n            Album(\n                album=\"album\",\n                albumartist=\"artist\",\n                mb_albumartistid=artist_mbid,\n                mb_albumid=\"album\",\n                mb_releasegroupid=\"album_id\",\n            )\n        )\n        requests_mock.get(\n            re.compile(r\"/ws/2/release-group.*type=compilation\"),\n            json={\n                \"release-groups\": [\n                    {\"id\": \"compilation_id\", \"title\": \"compilation\"}\n                ]\n            },\n        )\n\n        with self.configure_plugin({}):\n            output = self.run_with_output(\n                \"missing\", \"-a\", \"--release-types\", \"compilation\"\n            )\n\n        assert \"artist - compilation\" in output\n\n    def test_release_types_comma_separated(self, requests_mock):\n        \"\"\"Test --release-types with comma-separated values.\"\"\"\n        artist_mbid = str(uuid.uuid4())\n        self.lib.add(\n            Album(\n                album=\"album\",\n                albumartist=\"artist\",\n                mb_albumartistid=artist_mbid,\n                mb_albumid=\"album\",\n                mb_releasegroupid=\"album_id\",\n            )\n        )\n        requests_mock.get(\n            re.compile(r\"/ws/2/release-group.*type=compilation%7Calbum\"),\n            json={\n                \"release-groups\": [\n                    {\"id\": \"album2_id\", \"title\": \"title 2\"},\n                    {\"id\": \"compilation_id\", \"title\": \"compilation\"},\n                ]\n            },\n        )\n\n        with self.configure_plugin({}):\n            output = self.run_with_output(\n                \"missing\",\n                \"-a\",\n                \"--release-types\",\n                \"compilation,album\",\n            )\n\n        assert \"artist - compilation\" in output\n        assert \"artist - title 2\" in output\n\n    def test_empty_release_types_config_sends_empty_type(self, requests_mock):\n        \"\"\"Test that release_types: [] in config sends type=\"\" to the API.\"\"\"\n        artist_mbid = str(uuid.uuid4())\n        self.lib.add(\n            Album(\n                album=\"album\",\n                albumartist=\"artist\",\n                mb_albumartistid=artist_mbid,\n                mb_albumid=\"album\",\n                mb_releasegroupid=\"album_id\",\n            )\n        )\n        adapter = requests_mock.get(\n            re.compile(r\"/ws/2/release-group\"),\n            json={\"release-groups\": []},\n        )\n\n        with self.configure_plugin({\"release_types\": []}):\n            self.run_with_output(\"missing\", \"-a\")\n\n        assert adapter.last_request.qs[\"type\"] == [\"\"]\n\n    def test_missing_albums_total(self, requests_mock):\n        \"\"\"Test -t flag with --album shows total count of missing albums.\"\"\"\n        artist_mbid = str(uuid.uuid4())\n        self.lib.add(\n            Album(\n                album=\"album\",\n                albumartist=\"artist\",\n                mb_albumartistid=artist_mbid,\n                mb_albumid=\"album\",\n                mb_releasegroupid=\"album_id\",\n            )\n        )\n        requests_mock.get(\n            re.compile(\n                rf\"/ws/2/release-group\\?artist={artist_mbid}&.*type=album\"\n            ),\n            json={\n                \"release-groups\": [\n                    {\"id\": \"album_id\", \"title\": \"album\"},\n                    {\"id\": \"other_id\", \"title\": \"other\"},\n                ]\n            },\n        )\n\n        with self.configure_plugin({}):\n            output = self.run_with_output(\"missing\", \"-a\", \"-t\")\n\n        assert output == \"1\\n\"\n\n\n@pytest.mark.usefixtures(\"helper\")\nclass TestMissingTracks(IOMixin, PluginMixin):\n    \"\"\"Tests for missing tracks functionality.\"\"\"\n\n    plugin = \"missing\"\n\n    @pytest.mark.parametrize(\n        \"total,count,expected\",\n        [\n            (True, False, \"1\\n\"),\n            (False, True, \"artist - album: 1\"),\n        ],\n    )\n    @patch(\"beets.metadata_plugins.album_for_id\")\n    def test_missing_tracks(self, album_for_id, total, count, expected):\n        \"\"\"Test getting missing tracks works with expected output.\"\"\"\n        artist_mbid = str(uuid.uuid4())\n        album_items = [\n            Item(\n                album=\"album\",\n                mb_albumid=\"81ae60d4-5b75-38df-903a-db2cfa51c2c6\",\n                mb_releasegroupid=\"album_id\",\n                mb_trackid=\"track_1\",\n                mb_albumartistid=artist_mbid,\n                albumartist=\"artist\",\n                tracktotal=3,\n            ),\n            Item(\n                album=\"album\",\n                mb_albumid=\"81ae60d4-5b75-38df-903a-db2cfa51c2c6\",\n                mb_releasegroupid=\"album_id\",\n                mb_albumartistid=artist_mbid,\n                albumartist=\"artist\",\n                tracktotal=3,\n            ),\n            Item(\n                album=\"album\",\n                mb_albumid=\"81ae60d4-5b75-38df-903a-db2cfa51c2c6\",\n                mb_releasegroupid=\"album_id\",\n                mb_trackid=\"track_3\",\n                mb_albumartistid=artist_mbid,\n                albumartist=\"artist\",\n                tracktotal=3,\n            ),\n        ]\n        self.lib.add_album(album_items[:2])\n\n        album_for_id.return_value = AlbumInfo(\n            album_id=\"album_id\",\n            album=\"album\",\n            tracks=[\n                TrackInfo(track_id=item.mb_trackid) for item in album_items\n            ],\n        )\n\n        command = [\"missing\"]\n        if total:\n            command.append(\"-t\")\n        if count:\n            command.append(\"-c\")\n\n        with self.configure_plugin({}):\n            assert expected in self.run_with_output(*command)\n"
  },
  {
    "path": "test/plugins/test_mpdstats.py",
    "content": "# This file is part of beets.\n# Copyright 2016\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nfrom typing import Any, ClassVar\nfrom unittest.mock import ANY, Mock, call, patch\n\nfrom beets import util\nfrom beets.library import Item\nfrom beets.test.helper import PluginTestCase\nfrom beetsplug.mpdstats import MPDStats\n\n\nclass MPDStatsTest(PluginTestCase):\n    plugin = \"mpdstats\"\n\n    def test_update_rating(self):\n        item = Item(title=\"title\", path=\"\", id=1)\n        item.add(self.lib)\n\n        log = Mock()\n        mpdstats = MPDStats(self.lib, log)\n\n        assert not mpdstats.update_rating(item, True)\n        assert not mpdstats.update_rating(None, True)\n\n    def test_get_item(self):\n        item_path = util.normpath(\"/foo/bar.flac\")\n        item = Item(title=\"title\", path=item_path, id=1)\n        item.add(self.lib)\n\n        log = Mock()\n        mpdstats = MPDStats(self.lib, log)\n\n        assert str(mpdstats.get_item(item_path)) == str(item)\n        assert mpdstats.get_item(\"/some/non-existing/path\") is None\n        assert \"item not found:\" in log.info.call_args[0][0]\n\n    STATUSES: ClassVar[list[dict[str, Any]]] = [\n        {\"state\": \"some-unknown-one\"},\n        {\"state\": \"pause\"},\n        {\"state\": \"play\", \"songid\": 1, \"time\": \"0:1\"},\n        {\"state\": \"stop\"},\n    ]\n    EVENTS = [[\"player\"]] * (len(STATUSES) - 1) + [KeyboardInterrupt]\n    item_path = util.normpath(\"/foo/bar.flac\")\n    songid = 1\n\n    @patch(\n        \"beetsplug.mpdstats.MPDClientWrapper\",\n        return_value=Mock(\n            **{\n                \"events.side_effect\": EVENTS,\n                \"status.side_effect\": STATUSES,\n                \"currentsong.return_value\": (item_path, songid),\n            }\n        ),\n    )\n    def test_run_mpdstats(self, mpd_mock):\n        item = Item(title=\"title\", path=self.item_path, id=1)\n        item.add(self.lib)\n\n        log = Mock()\n        try:\n            MPDStats(self.lib, log).run()\n        except KeyboardInterrupt:\n            pass\n\n        log.debug.assert_has_calls([call('unhandled status \"{}\"', ANY)])\n        log.info.assert_has_calls(\n            [call(\"pause\"), call(\"playing {}\", ANY), call(\"stop\")]\n        )\n"
  },
  {
    "path": "test/plugins/test_musicbrainz.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for MusicBrainz API wrapper.\"\"\"\n\nimport unittest\nimport uuid\nfrom typing import Any, ClassVar\nfrom unittest import mock\n\nimport pytest\nimport requests\n\nfrom beets import config\nfrom beets.library import Item\nfrom beets.test.helper import BeetsTestCase, PluginMixin\nfrom beetsplug import musicbrainz\n\n\ndef make_alias(suffix: str, locale: str, primary: bool = False):\n    alias: dict[str, Any] = {\n        \"name\": f\"ALIAS{suffix}\",\n        \"locale\": locale,\n        \"sort-name\": f\"ALIASSORT{suffix}\",\n    }\n    if primary:\n        alias[\"primary\"] = True\n    return alias\n\n\nclass MusicBrainzTestCase(BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n        self.mb = musicbrainz.MusicBrainzPlugin()\n        self.config[\"match\"][\"preferred\"][\"countries\"] = [\"US\"]\n\n\nclass MBAlbumInfoTest(MusicBrainzTestCase):\n    def _make_release(\n        self,\n        date_str=\"2009\",\n        tracks=None,\n        track_length=None,\n        track_artist=False,\n        multi_artist_credit=False,\n        data_tracks=None,\n        medium_format=\"FORMAT\",\n    ):\n        release = {\n            \"title\": \"ALBUM TITLE\",\n            \"id\": \"ALBUM ID\",\n            \"asin\": \"ALBUM ASIN\",\n            \"disambiguation\": \"R_DISAMBIGUATION\",\n            \"release-group\": {\n                \"primary-type\": \"Album\",\n                \"first-release-date\": date_str,\n                \"id\": \"RELEASE GROUP ID\",\n                \"disambiguation\": \"RG_DISAMBIGUATION\",\n                \"title\": \"RELEASE GROUP TITLE\",\n            },\n            \"artist-credit\": [\n                {\n                    \"artist\": {\n                        \"name\": \"ARTIST NAME\",\n                        \"id\": \"ARTIST ID\",\n                        \"sort-name\": \"ARTIST SORT NAME\",\n                    },\n                    \"name\": \"ARTIST CREDIT\",\n                }\n            ],\n            \"date\": \"3001\",\n            \"media\": [],\n            \"genres\": [{\"count\": 1, \"name\": \"GENRE\"}],\n            \"tags\": [{\"count\": 1, \"name\": \"TAG\"}],\n            \"label-info\": [\n                {\n                    \"catalog-number\": \"CATALOG NUMBER\",\n                    \"label\": {\"name\": \"LABEL NAME\"},\n                }\n            ],\n            \"text-representation\": {\n                \"script\": \"SCRIPT\",\n                \"language\": \"LANGUAGE\",\n            },\n            \"country\": \"COUNTRY\",\n            \"status\": \"STATUS\",\n            \"barcode\": \"BARCODE\",\n            \"release-events\": [{\"area\": None, \"date\": \"2021-03-26\"}],\n        }\n\n        if multi_artist_credit:\n            release[\"artist-credit\"][0][\"joinphrase\"] = \" & \"\n            release[\"artist-credit\"].append(\n                {\n                    \"artist\": {\n                        \"name\": \"ARTIST 2 NAME\",\n                        \"id\": \"ARTIST 2 ID\",\n                        \"sort-name\": \"ARTIST 2 SORT NAME\",\n                    },\n                    \"name\": \"ARTIST MULTI CREDIT\",\n                }\n            )\n\n        i = 0\n        track_list = []\n        if tracks:\n            for recording in tracks:\n                i += 1\n                track = {\n                    \"id\": f\"RELEASE TRACK ID {i}\",\n                    \"recording\": recording,\n                    \"position\": i,\n                    \"number\": \"A1\",\n                }\n                if track_length:\n                    # Track lengths are distinct from recording lengths.\n                    track[\"length\"] = track_length\n                if track_artist:\n                    # Similarly, track artists can differ from recording\n                    # artists.\n                    track[\"artist-credit\"] = [\n                        {\n                            \"artist\": {\n                                \"name\": \"TRACK ARTIST NAME\",\n                                \"id\": \"TRACK ARTIST ID\",\n                                \"sort-name\": \"TRACK ARTIST SORT NAME\",\n                            },\n                            \"name\": \"TRACK ARTIST CREDIT\",\n                        }\n                    ]\n\n                    if multi_artist_credit:\n                        track[\"artist-credit\"][0][\"joinphrase\"] = \" & \"\n                        track[\"artist-credit\"].append(\n                            {\n                                \"artist\": {\n                                    \"name\": \"TRACK ARTIST 2 NAME\",\n                                    \"id\": \"TRACK ARTIST 2 ID\",\n                                    \"sort-name\": \"TRACK ARTIST 2 SORT NAME\",\n                                },\n                                \"name\": \"TRACK ARTIST 2 CREDIT\",\n                            }\n                        )\n\n                track_list.append(track)\n        data_track_list = []\n        if data_tracks:\n            for recording in data_tracks:\n                i += 1\n                data_track = {\n                    \"id\": f\"RELEASE TRACK ID {i}\",\n                    \"recording\": recording,\n                    \"position\": i,\n                    \"number\": \"A1\",\n                }\n                data_track_list.append(data_track)\n        release[\"media\"].append(\n            {\n                \"position\": \"1\",\n                \"tracks\": track_list,\n                \"data-tracks\": data_track_list,\n                \"format\": medium_format,\n                \"title\": \"MEDIUM TITLE\",\n            }\n        )\n        return release\n\n    def _make_track(\n        self,\n        title,\n        tr_id,\n        duration,\n        artist=False,\n        video=False,\n        disambiguation=None,\n        remixer=False,\n        multi_artist_credit=False,\n        aliases=None,\n    ):\n        track = {\n            \"title\": title,\n            \"id\": tr_id,\n        }\n        if duration is not None:\n            track[\"length\"] = duration\n        if artist:\n            track[\"artist-credit\"] = [\n                {\n                    \"artist\": {\n                        \"name\": \"RECORDING ARTIST NAME\",\n                        \"id\": \"RECORDING ARTIST ID\",\n                        \"sort-name\": \"RECORDING ARTIST SORT NAME\",\n                    },\n                    \"name\": \"RECORDING ARTIST CREDIT\",\n                }\n            ]\n            if multi_artist_credit:\n                track[\"artist-credit\"][0][\"joinphrase\"] = \" & \"\n                track[\"artist-credit\"].append(\n                    {\n                        \"artist\": {\n                            \"name\": \"RECORDING ARTIST 2 NAME\",\n                            \"id\": \"RECORDING ARTIST 2 ID\",\n                            \"sort-name\": \"RECORDING ARTIST 2 SORT NAME\",\n                        },\n                        \"name\": \"RECORDING ARTIST 2 CREDIT\",\n                    }\n                )\n        if remixer:\n            track[\"artist-relations\"] = [\n                {\n                    \"type\": \"remixer\",\n                    \"type-id\": \"RELATION TYPE ID\",\n                    \"direction\": \"RECORDING RELATION DIRECTION\",\n                    \"artist\": {\n                        \"id\": \"RECORDING REMIXER ARTIST ID\",\n                        \"type\": \"RECORDING REMIXER ARTIST TYPE\",\n                        \"name\": \"RECORDING REMIXER ARTIST NAME\",\n                        \"sort-name\": \"RECORDING REMIXER ARTIST SORT NAME\",\n                    },\n                }\n            ]\n        if video:\n            track[\"video\"] = True\n        if disambiguation:\n            track[\"disambiguation\"] = disambiguation\n        if aliases is not None:\n            track[\"aliases\"] = aliases\n        return track\n\n    def test_parse_release_title(self):\n        release = self._make_release(None)\n        release[\"aliases\"] = [\n            make_alias(suffix=\"en\", locale=\"en\", primary=True),\n        ]\n\n        # test no alias\n        config[\"import\"][\"languages\"] = []\n        d = self.mb.album_info(release)\n        assert d.album == \"ALBUM TITLE\"\n\n        # test en primary\n        config[\"import\"][\"languages\"] = [\"en\"]\n        d = self.mb.album_info(release)\n        assert d.album == \"ALIASen\"\n\n    def test_parse_release_with_year(self):\n        release = self._make_release(\"1984\")\n        d = self.mb.album_info(release)\n        assert d.album == \"ALBUM TITLE\"\n        assert d.album_id == \"ALBUM ID\"\n        assert d.artist == \"ARTIST NAME\"\n        assert d.artist_id == \"ARTIST ID\"\n        assert d.original_year == 1984\n        assert d.year == 3001\n        assert d.artist_credit == \"ARTIST CREDIT\"\n\n    def test_parse_release_type(self):\n        release = self._make_release(\"1984\")\n        d = self.mb.album_info(release)\n        assert d.albumtype == \"album\"\n\n    def test_parse_release_full_date(self):\n        release = self._make_release(\"1987-03-31\")\n        d = self.mb.album_info(release)\n        assert d.original_year == 1987\n        assert d.original_month == 3\n        assert d.original_day == 31\n\n    def test_parse_tracks(self):\n        tracks = [\n            self._make_track(\n                \"TITLE ONE\",\n                \"ID ONE\",\n                100.0 * 1000.0,\n                aliases=[make_alias(suffix=\"ONEen\", locale=\"en\", primary=True)],\n            ),\n            self._make_track(\n                \"TITLE TWO\",\n                \"ID TWO\",\n                200.0 * 1000.0,\n                aliases=[make_alias(suffix=\"TWOen\", locale=\"en\", primary=True)],\n            ),\n        ]\n        release = self._make_release(tracks=tracks)\n\n        # Preference over recording data\n        release[\"media\"][0][\"tracks\"][1][\"title\"] = \"TRACK TITLE TWO\"\n\n        # test no alias\n        config[\"import\"][\"languages\"] = []\n        d = self.mb.album_info(release)\n        t = d.tracks\n        assert len(t) == 2\n        assert t[0].title == \"TITLE ONE\"\n        assert t[0].track_id == \"ID ONE\"\n        assert t[0].length == 100.0\n        assert t[1].title == \"TRACK TITLE TWO\"\n        assert t[1].track_id == \"ID TWO\"\n        assert t[1].length == 200.0\n\n        # test en primary\n        config[\"import\"][\"languages\"] = [\"en\"]\n        d = self.mb.album_info(release)\n        t = d.tracks\n        assert len(t) == 2\n        assert t[0].title == \"ALIASONEen\"\n        assert t[0].track_id == \"ID ONE\"\n        assert t[0].length == 100.0\n        assert t[1].title == \"ALIASTWOen\"\n        assert t[1].track_id == \"ID TWO\"\n        assert t[1].length == 200.0\n\n    def test_parse_track_indices(self):\n        tracks = [\n            self._make_track(\"TITLE ONE\", \"ID ONE\", 100.0 * 1000.0),\n            self._make_track(\"TITLE TWO\", \"ID TWO\", 200.0 * 1000.0),\n        ]\n        release = self._make_release(tracks=tracks)\n\n        d = self.mb.album_info(release)\n        t = d.tracks\n        assert t[0].medium_index == 1\n        assert t[0].index == 1\n        assert t[1].medium_index == 2\n        assert t[1].index == 2\n\n    def test_parse_medium_numbers_single_medium(self):\n        tracks = [\n            self._make_track(\"TITLE ONE\", \"ID ONE\", 100.0 * 1000.0),\n            self._make_track(\"TITLE TWO\", \"ID TWO\", 200.0 * 1000.0),\n        ]\n        release = self._make_release(tracks=tracks)\n\n        d = self.mb.album_info(release)\n        assert d.mediums == 1\n        t = d.tracks\n        assert t[0].medium == 1\n        assert t[1].medium == 1\n\n    def test_parse_medium_numbers_two_mediums(self):\n        tracks = [\n            self._make_track(\"TITLE ONE\", \"ID ONE\", 100.0 * 1000.0),\n            self._make_track(\"TITLE TWO\", \"ID TWO\", 200.0 * 1000.0),\n        ]\n        release = self._make_release(tracks=[tracks[0]])\n        second_track_list = [\n            {\n                \"id\": \"RELEASE TRACK ID 2\",\n                \"recording\": tracks[1],\n                \"position\": \"1\",\n                \"number\": \"A1\",\n            }\n        ]\n        release[\"media\"].append(\n            {\n                \"position\": \"2\",\n                \"tracks\": second_track_list,\n            }\n        )\n\n        d = self.mb.album_info(release)\n        assert d.mediums == 2\n        t = d.tracks\n        assert t[0].medium == 1\n        assert t[0].medium_index == 1\n        assert t[0].index == 1\n        assert t[1].medium == 2\n        assert t[1].medium_index == 1\n        assert t[1].index == 2\n\n    def test_parse_release_year_month_only(self):\n        release = self._make_release(\"1987-03\")\n        d = self.mb.album_info(release)\n        assert d.original_year == 1987\n        assert d.original_month == 3\n\n    def test_no_durations(self):\n        tracks = [self._make_track(\"TITLE\", \"ID\", None)]\n        release = self._make_release(tracks=tracks)\n        d = self.mb.album_info(release)\n        assert d.tracks[0].length is None\n\n    def test_track_length_overrides_recording_length(self):\n        tracks = [self._make_track(\"TITLE\", \"ID\", 1.0 * 1000.0)]\n        release = self._make_release(tracks=tracks, track_length=2.0 * 1000.0)\n        d = self.mb.album_info(release)\n        assert d.tracks[0].length == 2.0\n\n    def test_no_release_date(self):\n        release = self._make_release(None)\n        d = self.mb.album_info(release)\n        assert not d.original_year\n        assert not d.original_month\n        assert not d.original_day\n\n    def test_various_artists_defaults_false(self):\n        release = self._make_release(None)\n        d = self.mb.album_info(release)\n        assert not d.va\n\n    def test_detect_various_artists(self):\n        release = self._make_release(None)\n        release[\"artist-credit\"][0][\"artist\"][\"id\"] = (\n            musicbrainz.VARIOUS_ARTISTS_ID\n        )\n        d = self.mb.album_info(release)\n        assert d.va\n\n    def test_parse_artist_sort_name(self):\n        release = self._make_release(None)\n        d = self.mb.album_info(release)\n        assert d.artist_sort == \"ARTIST SORT NAME\"\n\n    def test_parse_releasegroupid(self):\n        release = self._make_release(None)\n        d = self.mb.album_info(release)\n        assert d.releasegroup_id == \"RELEASE GROUP ID\"\n\n    def test_parse_release_group_title(self):\n        release = self._make_release(None)\n        release[\"release-group\"][\"aliases\"] = [\n            make_alias(suffix=\"en\", locale=\"en\", primary=True),\n        ]\n\n        # test no alias\n        config[\"import\"][\"languages\"] = []\n        d = self.mb.album_info(release)\n        assert d.release_group_title == \"RELEASE GROUP TITLE\"\n\n        # test en primary\n        config[\"import\"][\"languages\"] = [\"en\"]\n        d = self.mb.album_info(release)\n        assert d.release_group_title == \"ALIASen\"\n\n    def test_parse_asin(self):\n        release = self._make_release(None)\n        d = self.mb.album_info(release)\n        assert d.asin == \"ALBUM ASIN\"\n\n    def test_parse_catalognum(self):\n        release = self._make_release(None)\n        d = self.mb.album_info(release)\n        assert d.catalognum == \"CATALOG NUMBER\"\n\n    def test_parse_textrepr(self):\n        release = self._make_release(None)\n        d = self.mb.album_info(release)\n        assert d.script == \"SCRIPT\"\n        assert d.language == \"LANGUAGE\"\n\n    def test_parse_country(self):\n        release = self._make_release(None)\n        d = self.mb.album_info(release)\n        assert d.country == \"COUNTRY\"\n\n    def test_parse_status(self):\n        release = self._make_release(None)\n        d = self.mb.album_info(release)\n        assert d.albumstatus == \"STATUS\"\n\n    def test_parse_barcode(self):\n        release = self._make_release(None)\n        d = self.mb.album_info(release)\n        assert d.barcode == \"BARCODE\"\n\n    def test_parse_media(self):\n        tracks = [\n            self._make_track(\"TITLE ONE\", \"ID ONE\", 100.0 * 1000.0),\n            self._make_track(\"TITLE TWO\", \"ID TWO\", 200.0 * 1000.0),\n        ]\n        release = self._make_release(None, tracks=tracks)\n        d = self.mb.album_info(release)\n        assert d.media == \"FORMAT\"\n\n    def test_parse_disambig(self):\n        release = self._make_release(None)\n        d = self.mb.album_info(release)\n        assert d.albumdisambig == \"R_DISAMBIGUATION\"\n        assert d.releasegroupdisambig == \"RG_DISAMBIGUATION\"\n\n    def test_parse_disctitle(self):\n        tracks = [\n            self._make_track(\"TITLE ONE\", \"ID ONE\", 100.0 * 1000.0),\n            self._make_track(\"TITLE TWO\", \"ID TWO\", 200.0 * 1000.0),\n        ]\n        release = self._make_release(None, tracks=tracks)\n        d = self.mb.album_info(release)\n        t = d.tracks\n        assert t[0].disctitle == \"MEDIUM TITLE\"\n        assert t[1].disctitle == \"MEDIUM TITLE\"\n\n    def test_missing_language(self):\n        release = self._make_release(None)\n        del release[\"text-representation\"][\"language\"]\n        d = self.mb.album_info(release)\n        assert d.language is None\n\n    def test_parse_recording_artist(self):\n        tracks = [self._make_track(\"a\", \"b\", 1, True)]\n        release = self._make_release(None, tracks=tracks)\n        track = self.mb.album_info(release).tracks[0]\n        assert track.artist == \"RECORDING ARTIST NAME\"\n        assert track.artist_id == \"RECORDING ARTIST ID\"\n        assert track.artist_sort == \"RECORDING ARTIST SORT NAME\"\n        assert track.artist_credit == \"RECORDING ARTIST CREDIT\"\n\n    def test_parse_recording_artist_multi(self):\n        tracks = [self._make_track(\"a\", \"b\", 1, True, multi_artist_credit=True)]\n        release = self._make_release(None, tracks=tracks)\n        track = self.mb.album_info(release).tracks[0]\n        assert track.artist == \"RECORDING ARTIST NAME & RECORDING ARTIST 2 NAME\"\n        assert track.artist_id == \"RECORDING ARTIST ID\"\n        assert (\n            track.artist_sort\n            == \"RECORDING ARTIST SORT NAME & RECORDING ARTIST 2 SORT NAME\"\n        )\n        assert (\n            track.artist_credit\n            == \"RECORDING ARTIST CREDIT & RECORDING ARTIST 2 CREDIT\"\n        )\n\n        assert track.artists == [\n            \"RECORDING ARTIST NAME\",\n            \"RECORDING ARTIST 2 NAME\",\n        ]\n        assert track.artists_ids == [\n            \"RECORDING ARTIST ID\",\n            \"RECORDING ARTIST 2 ID\",\n        ]\n        assert track.artists_sort == [\n            \"RECORDING ARTIST SORT NAME\",\n            \"RECORDING ARTIST 2 SORT NAME\",\n        ]\n        assert track.artists_credit == [\n            \"RECORDING ARTIST CREDIT\",\n            \"RECORDING ARTIST 2 CREDIT\",\n        ]\n\n    def test_track_artist_overrides_recording_artist(self):\n        tracks = [self._make_track(\"a\", \"b\", 1, True)]\n        release = self._make_release(None, tracks=tracks, track_artist=True)\n        track = self.mb.album_info(release).tracks[0]\n        assert track.artist == \"TRACK ARTIST NAME\"\n        assert track.artist_id == \"TRACK ARTIST ID\"\n        assert track.artist_sort == \"TRACK ARTIST SORT NAME\"\n        assert track.artist_credit == \"TRACK ARTIST CREDIT\"\n\n    def test_track_artist_overrides_recording_artist_multi(self):\n        tracks = [self._make_track(\"a\", \"b\", 1, True, multi_artist_credit=True)]\n        release = self._make_release(\n            None, tracks=tracks, track_artist=True, multi_artist_credit=True\n        )\n        track = self.mb.album_info(release).tracks[0]\n        assert track.artist == \"TRACK ARTIST NAME & TRACK ARTIST 2 NAME\"\n        assert track.artist_id == \"TRACK ARTIST ID\"\n        assert (\n            track.artist_sort\n            == \"TRACK ARTIST SORT NAME & TRACK ARTIST 2 SORT NAME\"\n        )\n        assert (\n            track.artist_credit == \"TRACK ARTIST CREDIT & TRACK ARTIST 2 CREDIT\"\n        )\n\n        assert track.artists == [\"TRACK ARTIST NAME\", \"TRACK ARTIST 2 NAME\"]\n        assert track.artists_ids == [\"TRACK ARTIST ID\", \"TRACK ARTIST 2 ID\"]\n        assert track.artists_sort == [\n            \"TRACK ARTIST SORT NAME\",\n            \"TRACK ARTIST 2 SORT NAME\",\n        ]\n        assert track.artists_credit == [\n            \"TRACK ARTIST CREDIT\",\n            \"TRACK ARTIST 2 CREDIT\",\n        ]\n\n    def test_parse_recording_remixer(self):\n        tracks = [self._make_track(\"a\", \"b\", 1, remixer=True)]\n        release = self._make_release(None, tracks=tracks)\n        track = self.mb.album_info(release).tracks[0]\n        assert track.remixer == \"RECORDING REMIXER ARTIST NAME\"\n\n    def test_data_source(self):\n        release = self._make_release()\n        d = self.mb.album_info(release)\n        assert d.data_source == \"MusicBrainz\"\n\n    def test_genres(self):\n        config[\"musicbrainz\"][\"genres\"] = True\n        config[\"musicbrainz\"][\"genres_tag\"] = \"genre\"\n        release = self._make_release()\n        d = self.mb.album_info(release)\n        assert d.genres == [\"GENRE\"]\n\n    def test_tags(self):\n        config[\"musicbrainz\"][\"genres\"] = True\n        config[\"musicbrainz\"][\"genres_tag\"] = \"tag\"\n        release = self._make_release()\n        d = self.mb.album_info(release)\n        assert d.genres == [\"TAG\"]\n\n    def test_no_genres(self):\n        config[\"musicbrainz\"][\"genres\"] = False\n        release = self._make_release()\n        d = self.mb.album_info(release)\n        assert d.genres is None\n\n    def test_ignored_media(self):\n        config[\"match\"][\"ignored_media\"] = [\"IGNORED1\", \"IGNORED2\"]\n        tracks = [\n            self._make_track(\"TITLE ONE\", \"ID ONE\", 100.0 * 1000.0),\n            self._make_track(\"TITLE TWO\", \"ID TWO\", 200.0 * 1000.0),\n        ]\n        release = self._make_release(tracks=tracks, medium_format=\"IGNORED1\")\n        d = self.mb.album_info(release)\n        assert len(d.tracks) == 0\n\n    def test_no_ignored_media(self):\n        config[\"match\"][\"ignored_media\"] = [\"IGNORED1\", \"IGNORED2\"]\n        tracks = [\n            self._make_track(\"TITLE ONE\", \"ID ONE\", 100.0 * 1000.0),\n            self._make_track(\"TITLE TWO\", \"ID TWO\", 200.0 * 1000.0),\n        ]\n        release = self._make_release(tracks=tracks, medium_format=\"NON-IGNORED\")\n        d = self.mb.album_info(release)\n        assert len(d.tracks) == 2\n\n    def test_skip_data_track(self):\n        tracks = [\n            self._make_track(\"TITLE ONE\", \"ID ONE\", 100.0 * 1000.0),\n            self._make_track(\"[data track]\", \"ID DATA TRACK\", 100.0 * 1000.0),\n            self._make_track(\"TITLE TWO\", \"ID TWO\", 200.0 * 1000.0),\n        ]\n        release = self._make_release(tracks=tracks)\n        d = self.mb.album_info(release)\n        assert len(d.tracks) == 2\n        assert d.tracks[0].title == \"TITLE ONE\"\n        assert d.tracks[1].title == \"TITLE TWO\"\n\n    def test_skip_audio_data_tracks_by_default(self):\n        tracks = [\n            self._make_track(\"TITLE ONE\", \"ID ONE\", 100.0 * 1000.0),\n            self._make_track(\"TITLE TWO\", \"ID TWO\", 200.0 * 1000.0),\n        ]\n        data_tracks = [\n            self._make_track(\n                \"TITLE AUDIO DATA\", \"ID DATA TRACK\", 100.0 * 1000.0\n            )\n        ]\n        release = self._make_release(tracks=tracks, data_tracks=data_tracks)\n        d = self.mb.album_info(release)\n        assert len(d.tracks) == 2\n        assert d.tracks[0].title == \"TITLE ONE\"\n        assert d.tracks[1].title == \"TITLE TWO\"\n\n    def test_no_skip_audio_data_tracks_if_configured(self):\n        config[\"match\"][\"ignore_data_tracks\"] = False\n        tracks = [\n            self._make_track(\"TITLE ONE\", \"ID ONE\", 100.0 * 1000.0),\n            self._make_track(\"TITLE TWO\", \"ID TWO\", 200.0 * 1000.0),\n        ]\n        data_tracks = [\n            self._make_track(\n                \"TITLE AUDIO DATA\", \"ID DATA TRACK\", 100.0 * 1000.0\n            )\n        ]\n        release = self._make_release(tracks=tracks, data_tracks=data_tracks)\n        d = self.mb.album_info(release)\n        assert len(d.tracks) == 3\n        assert d.tracks[0].title == \"TITLE ONE\"\n        assert d.tracks[1].title == \"TITLE TWO\"\n        assert d.tracks[2].title == \"TITLE AUDIO DATA\"\n\n    def test_skip_video_tracks_by_default(self):\n        tracks = [\n            self._make_track(\"TITLE ONE\", \"ID ONE\", 100.0 * 1000.0),\n            self._make_track(\n                \"TITLE VIDEO\", \"ID VIDEO\", 100.0 * 1000.0, False, True\n            ),\n            self._make_track(\"TITLE TWO\", \"ID TWO\", 200.0 * 1000.0),\n        ]\n        release = self._make_release(tracks=tracks)\n        d = self.mb.album_info(release)\n        assert len(d.tracks) == 2\n        assert d.tracks[0].title == \"TITLE ONE\"\n        assert d.tracks[1].title == \"TITLE TWO\"\n\n    def test_skip_video_data_tracks_by_default(self):\n        tracks = [\n            self._make_track(\"TITLE ONE\", \"ID ONE\", 100.0 * 1000.0),\n            self._make_track(\"TITLE TWO\", \"ID TWO\", 200.0 * 1000.0),\n        ]\n        data_tracks = [\n            self._make_track(\n                \"TITLE VIDEO\", \"ID VIDEO\", 100.0 * 1000.0, False, True\n            )\n        ]\n        release = self._make_release(tracks=tracks, data_tracks=data_tracks)\n        d = self.mb.album_info(release)\n        assert len(d.tracks) == 2\n        assert d.tracks[0].title == \"TITLE ONE\"\n        assert d.tracks[1].title == \"TITLE TWO\"\n\n    def test_no_skip_video_tracks_if_configured(self):\n        config[\"match\"][\"ignore_data_tracks\"] = False\n        config[\"match\"][\"ignore_video_tracks\"] = False\n        tracks = [\n            self._make_track(\"TITLE ONE\", \"ID ONE\", 100.0 * 1000.0),\n            self._make_track(\n                \"TITLE VIDEO\", \"ID VIDEO\", 100.0 * 1000.0, False, True\n            ),\n            self._make_track(\"TITLE TWO\", \"ID TWO\", 200.0 * 1000.0),\n        ]\n        release = self._make_release(tracks=tracks)\n        d = self.mb.album_info(release)\n        assert len(d.tracks) == 3\n        assert d.tracks[0].title == \"TITLE ONE\"\n        assert d.tracks[1].title == \"TITLE VIDEO\"\n        assert d.tracks[2].title == \"TITLE TWO\"\n\n    def test_no_skip_video_data_tracks_if_configured(self):\n        config[\"match\"][\"ignore_data_tracks\"] = False\n        config[\"match\"][\"ignore_video_tracks\"] = False\n        tracks = [\n            self._make_track(\"TITLE ONE\", \"ID ONE\", 100.0 * 1000.0),\n            self._make_track(\"TITLE TWO\", \"ID TWO\", 200.0 * 1000.0),\n        ]\n        data_tracks = [\n            self._make_track(\n                \"TITLE VIDEO\", \"ID VIDEO\", 100.0 * 1000.0, False, True\n            )\n        ]\n        release = self._make_release(tracks=tracks, data_tracks=data_tracks)\n        d = self.mb.album_info(release)\n        assert len(d.tracks) == 3\n        assert d.tracks[0].title == \"TITLE ONE\"\n        assert d.tracks[1].title == \"TITLE TWO\"\n        assert d.tracks[2].title == \"TITLE VIDEO\"\n\n    def test_track_disambiguation(self):\n        tracks = [\n            self._make_track(\"TITLE ONE\", \"ID ONE\", 100.0 * 1000.0),\n            self._make_track(\n                \"TITLE TWO\",\n                \"ID TWO\",\n                200.0 * 1000.0,\n                disambiguation=\"SECOND TRACK\",\n            ),\n        ]\n        release = self._make_release(tracks=tracks)\n\n        d = self.mb.album_info(release)\n        t = d.tracks\n        assert len(t) == 2\n        assert t[0].trackdisambig is None\n        assert t[1].trackdisambig == \"SECOND TRACK\"\n\n    def test_missing_tracks(self):\n        tracks = [\n            self._make_track(\"TITLE ONE\", \"ID ONE\", 100.0 * 1000.0),\n            self._make_track(\n                \"TITLE TWO\",\n                \"ID TWO\",\n                200.0 * 1000.0,\n                disambiguation=\"SECOND TRACK\",\n            ),\n        ]\n        release = self._make_release(tracks=tracks)\n        release[\"media\"].append(release[\"media\"][0])\n        del release[\"media\"][0][\"tracks\"]\n        del release[\"media\"][0][\"data-tracks\"]\n        d = self.mb.album_info(release)\n        assert d.mediums == 2\n\n\nclass ArtistFlatteningTest(unittest.TestCase):\n    def _credit_dict(self, suffix=\"\"):\n        return {\n            \"artist\": {\n                \"name\": f\"NAME{suffix}\",\n                \"sort-name\": f\"SORT{suffix}\",\n            },\n            \"name\": f\"CREDIT{suffix}\",\n        }\n\n    def test_single_artist(self):\n        credit = [self._credit_dict()]\n        a, s, c = musicbrainz._flatten_artist_credit(credit)\n        assert a == \"NAME\"\n        assert s == \"SORT\"\n        assert c == \"CREDIT\"\n\n        a, s, c = musicbrainz._multi_artist_credit(\n            credit, include_join_phrase=False\n        )\n        assert a == [\"NAME\"]\n        assert s == [\"SORT\"]\n        assert c == [\"CREDIT\"]\n\n    def test_two_artists(self):\n        credit = [\n            {**self._credit_dict(\"a\"), \"joinphrase\": \" AND \"},\n            self._credit_dict(\"b\"),\n        ]\n        a, s, c = musicbrainz._flatten_artist_credit(credit)\n        assert a == \"NAMEa AND NAMEb\"\n        assert s == \"SORTa AND SORTb\"\n        assert c == \"CREDITa AND CREDITb\"\n\n        a, s, c = musicbrainz._multi_artist_credit(\n            credit, include_join_phrase=False\n        )\n        assert a == [\"NAMEa\", \"NAMEb\"]\n        assert s == [\"SORTa\", \"SORTb\"]\n        assert c == [\"CREDITa\", \"CREDITb\"]\n\n    def test_alias(self):\n        credit_dict = self._credit_dict()\n        credit_dict[\"artist\"][\"aliases\"] = [\n            make_alias(suffix=\"en\", locale=\"en\", primary=True),\n            make_alias(suffix=\"en_GB\", locale=\"en_GB\", primary=True),\n            make_alias(suffix=\"fr\", locale=\"fr\"),\n            make_alias(suffix=\"fr_P\", locale=\"fr\", primary=True),\n            make_alias(suffix=\"pt_BR\", locale=\"pt_BR\"),\n        ]\n        # test no alias\n        config[\"import\"][\"languages\"] = []\n        flat = musicbrainz._flatten_artist_credit([credit_dict])\n        assert flat == (\"NAME\", \"SORT\", \"CREDIT\")\n\n        # test en primary\n        config[\"import\"][\"languages\"] = [\"en\"]\n        flat = musicbrainz._flatten_artist_credit([credit_dict])\n        assert flat == (\"ALIASen\", \"ALIASSORTen\", \"CREDIT\")\n\n        # test en_GB en primary\n        config[\"import\"][\"languages\"] = [\"en_GB\", \"en\"]\n        flat = musicbrainz._flatten_artist_credit([credit_dict])\n        assert flat == (\"ALIASen_GB\", \"ALIASSORTen_GB\", \"CREDIT\")\n\n        # test en en_GB primary\n        config[\"import\"][\"languages\"] = [\"en\", \"en_GB\"]\n        flat = musicbrainz._flatten_artist_credit([credit_dict])\n        assert flat == (\"ALIASen\", \"ALIASSORTen\", \"CREDIT\")\n\n        # test fr primary\n        config[\"import\"][\"languages\"] = [\"fr\"]\n        flat = musicbrainz._flatten_artist_credit([credit_dict])\n        assert flat == (\"ALIASfr_P\", \"ALIASSORTfr_P\", \"CREDIT\")\n\n        # test for not matching non-primary\n        config[\"import\"][\"languages\"] = [\"pt_BR\", \"fr\"]\n        flat = musicbrainz._flatten_artist_credit([credit_dict])\n        assert flat == (\"ALIASfr_P\", \"ALIASSORTfr_P\", \"CREDIT\")\n\n\nclass MBLibraryTest(MusicBrainzTestCase):\n    def test_follow_pseudo_releases(self):\n        side_effect = [\n            {\n                \"title\": \"pseudo\",\n                \"id\": \"d2a6f856-b553-40a0-ac54-a321e8e2da02\",\n                \"status\": \"Pseudo-Release\",\n                \"media\": [\n                    {\n                        \"tracks\": [\n                            {\n                                \"id\": \"baz\",\n                                \"recording\": {\n                                    \"title\": \"translated title\",\n                                    \"id\": \"bar\",\n                                    \"length\": 42,\n                                },\n                                \"position\": 9,\n                                \"number\": \"A1\",\n                            }\n                        ],\n                        \"position\": 5,\n                    }\n                ],\n                \"artist-credit\": [\n                    {\n                        \"artist\": {\n                            \"name\": \"some-artist\",\n                            \"id\": \"some-id\",\n                        },\n                    }\n                ],\n                \"release-group\": {\n                    \"id\": \"another-id\",\n                },\n                \"release-relations\": [\n                    {\n                        \"type\": \"transl-tracklisting\",\n                        \"direction\": \"backward\",\n                        \"release\": {\n                            \"id\": \"d2a6f856-b553-40a0-ac54-a321e8e2da01\"\n                        },\n                    }\n                ],\n            },\n            {\n                \"title\": \"actual\",\n                \"id\": \"d2a6f856-b553-40a0-ac54-a321e8e2da01\",\n                \"status\": \"Official\",\n                \"media\": [\n                    {\n                        \"tracks\": [\n                            {\n                                \"id\": \"baz\",\n                                \"recording\": {\n                                    \"title\": \"original title\",\n                                    \"id\": \"bar\",\n                                    \"length\": 42,\n                                },\n                                \"position\": 9,\n                                \"number\": \"A1\",\n                            }\n                        ],\n                        \"position\": 5,\n                    }\n                ],\n                \"artist-credit\": [\n                    {\n                        \"artist\": {\n                            \"name\": \"some-artist\",\n                            \"id\": \"some-id\",\n                        },\n                    }\n                ],\n                \"release-group\": {\n                    \"id\": \"another-id\",\n                },\n                \"country\": \"COUNTRY\",\n            },\n        ]\n\n        with mock.patch(\n            \"beetsplug._utils.musicbrainz.MusicBrainzAPI.get_release\"\n        ) as gp:\n            gp.side_effect = side_effect\n            album = self.mb.album_for_id(\"d2a6f856-b553-40a0-ac54-a321e8e2da02\")\n            assert album.country == \"COUNTRY\"\n\n    def test_pseudo_releases_with_empty_links(self):\n        side_effect = [\n            {\n                \"title\": \"pseudo\",\n                \"id\": \"d2a6f856-b553-40a0-ac54-a321e8e2da02\",\n                \"status\": \"Pseudo-Release\",\n                \"media\": [\n                    {\n                        \"tracks\": [\n                            {\n                                \"id\": \"baz\",\n                                \"recording\": {\n                                    \"title\": \"translated title\",\n                                    \"id\": \"bar\",\n                                    \"length\": 42,\n                                },\n                                \"position\": 9,\n                                \"number\": \"A1\",\n                            }\n                        ],\n                        \"position\": 5,\n                    }\n                ],\n                \"artist-credit\": [\n                    {\n                        \"artist\": {\n                            \"name\": \"some-artist\",\n                            \"id\": \"some-id\",\n                        },\n                    }\n                ],\n                \"release-group\": {\n                    \"id\": \"another-id\",\n                },\n            }\n        ]\n\n        with mock.patch(\n            \"beetsplug._utils.musicbrainz.MusicBrainzAPI.get_release\"\n        ) as gp:\n            gp.side_effect = side_effect\n            album = self.mb.album_for_id(\"d2a6f856-b553-40a0-ac54-a321e8e2da02\")\n            assert album.country is None\n\n    def test_pseudo_releases_without_links(self):\n        side_effect = [\n            {\n                \"title\": \"pseudo\",\n                \"id\": \"d2a6f856-b553-40a0-ac54-a321e8e2da02\",\n                \"status\": \"Pseudo-Release\",\n                \"media\": [\n                    {\n                        \"tracks\": [\n                            {\n                                \"id\": \"baz\",\n                                \"recording\": {\n                                    \"title\": \"translated title\",\n                                    \"id\": \"bar\",\n                                    \"length\": 42,\n                                },\n                                \"position\": 9,\n                                \"number\": \"A1\",\n                            }\n                        ],\n                        \"position\": 5,\n                    }\n                ],\n                \"artist-credit\": [\n                    {\n                        \"artist\": {\n                            \"name\": \"some-artist\",\n                            \"id\": \"some-id\",\n                        },\n                    }\n                ],\n                \"release-group\": {\n                    \"id\": \"another-id\",\n                },\n            }\n        ]\n\n        with mock.patch(\n            \"beetsplug._utils.musicbrainz.MusicBrainzAPI.get_release\"\n        ) as gp:\n            gp.side_effect = side_effect\n            album = self.mb.album_for_id(\"d2a6f856-b553-40a0-ac54-a321e8e2da02\")\n            assert album.country is None\n\n    def test_pseudo_releases_with_unsupported_links(self):\n        side_effect = [\n            {\n                \"title\": \"pseudo\",\n                \"id\": \"d2a6f856-b553-40a0-ac54-a321e8e2da02\",\n                \"status\": \"Pseudo-Release\",\n                \"media\": [\n                    {\n                        \"tracks\": [\n                            {\n                                \"id\": \"baz\",\n                                \"recording\": {\n                                    \"title\": \"translated title\",\n                                    \"id\": \"bar\",\n                                    \"length\": 42,\n                                },\n                                \"position\": 9,\n                                \"number\": \"A1\",\n                            }\n                        ],\n                        \"position\": 5,\n                    }\n                ],\n                \"artist-credit\": [\n                    {\n                        \"artist\": {\n                            \"name\": \"some-artist\",\n                            \"id\": \"some-id\",\n                        },\n                    }\n                ],\n                \"release-group\": {\n                    \"id\": \"another-id\",\n                },\n                \"release-relations\": [\n                    {\n                        \"type\": \"remaster\",\n                        \"direction\": \"backward\",\n                        \"release\": {\n                            \"id\": \"d2a6f856-b553-40a0-ac54-a321e8e2da01\"\n                        },\n                    }\n                ],\n            }\n        ]\n\n        with mock.patch(\n            \"beetsplug._utils.musicbrainz.MusicBrainzAPI.get_release\"\n        ) as gp:\n            gp.side_effect = side_effect\n            album = self.mb.album_for_id(\"d2a6f856-b553-40a0-ac54-a321e8e2da02\")\n            assert album.country is None\n\n\nclass TestMusicBrainzPlugin(PluginMixin):\n    plugin = \"musicbrainz\"\n\n    mbid = \"d2a6f856-b553-40a0-ac54-a321e8e2da99\"\n    RECORDING: ClassVar[dict[str, int | str]] = {\n        \"title\": \"foo\",\n        \"id\": mbid,\n        \"length\": 42,\n    }\n\n    @pytest.fixture\n    def plugin_config(self):\n        return {}\n\n    @pytest.fixture\n    def mb(self, plugin_config):\n        self.config[self.plugin].set(plugin_config)\n\n        return musicbrainz.MusicBrainzPlugin()\n\n    @pytest.mark.parametrize(\n        \"plugin_config,va_likely,expected_additional_criteria\",\n        [\n            ({}, False, {\"artist\": \"Artist \"}),\n            ({}, True, {\"arid\": \"89ad4ac3-39f7-470e-963a-56509c546377\"}),\n            (\n                {\"extra_tags\": [\"label\", \"catalognum\"]},\n                False,\n                {\"artist\": \"Artist \", \"label\": \"abc\", \"catno\": \"ABC123\"},\n            ),\n        ],\n    )\n    def test_get_album_criteria(\n        self, mb, va_likely, expected_additional_criteria\n    ):\n        items = [\n            Item(catalognum=\"ABC 123\", label=\"abc\"),\n            Item(catalognum=\"ABC 123\", label=\"abc\"),\n            Item(catalognum=\"ABC 123\", label=\"def\"),\n        ]\n\n        assert mb.get_album_criteria(items, \"Artist \", \" Album\", va_likely) == {\n            \"release\": \" Album\",\n            **expected_additional_criteria,\n        }\n\n    def test_item_candidates(self, monkeypatch, mb):\n        monkeypatch.setattr(\n            \"beetsplug._utils.musicbrainz.MusicBrainzAPI.get_json\",\n            lambda *_, **__: {\"recordings\": [self.RECORDING]},\n        )\n        monkeypatch.setattr(\n            \"beetsplug._utils.musicbrainz.MusicBrainzAPI.get_recording\",\n            lambda *_, **__: self.RECORDING,\n        )\n\n        candidates = list(mb.item_candidates(Item(), \"hello\", \"there\"))\n\n        assert len(candidates) == 1\n        assert candidates[0].track_id == self.RECORDING[\"id\"]\n\n    def test_candidates(self, monkeypatch, mb):\n        monkeypatch.setattr(\n            \"beetsplug._utils.musicbrainz.MusicBrainzAPI.get_json\",\n            lambda *_, **__: {\"releases\": [{\"id\": self.mbid}]},\n        )\n        monkeypatch.setattr(\n            \"beetsplug._utils.musicbrainz.MusicBrainzAPI.get_release\",\n            lambda *_, **__: {\n                \"title\": \"hi\",\n                \"id\": self.mbid,\n                \"status\": \"status\",\n                \"media\": [\n                    {\n                        \"tracks\": [\n                            {\n                                \"id\": \"baz\",\n                                \"recording\": self.RECORDING,\n                                \"position\": 9,\n                                \"number\": \"A1\",\n                            }\n                        ],\n                        \"position\": 5,\n                    }\n                ],\n                \"artist-credit\": [\n                    {\"artist\": {\"name\": \"some-artist\", \"id\": \"some-id\"}}\n                ],\n                \"release-group\": {\"id\": \"another-id\"},\n            },\n        )\n        candidates = list(mb.candidates([], \"hello\", \"there\", False))\n\n        assert len(candidates) == 1\n        assert candidates[0].tracks[0].track_id == self.RECORDING[\"id\"]\n        assert candidates[0].album == \"hi\"\n\n    def test_import_handles_404_gracefully(self, mb, requests_mock):\n        id_ = uuid.uuid4()\n        response = requests.Response()\n        response.status_code = 404\n        requests_mock.get(\n            f\"/ws/2/release/{id_}\",\n            exc=requests.exceptions.HTTPError(response=response),\n        )\n        res = mb.album_for_id(str(id_))\n        assert res is None\n\n    def test_import_propagates_non_404_errors(self, mb):\n        class DummyResponse:\n            status_code = 500\n\n        error = requests.exceptions.HTTPError(response=DummyResponse())\n\n        def raise_error(*args, **kwargs):\n            raise error\n\n        # Simulate mb.mb_api.get_release raising a non-404 HTTP error\n        mb.mb_api.get_release = raise_error\n\n        with pytest.raises(requests.exceptions.HTTPError) as excinfo:\n            mb.album_for_id(str(uuid.uuid4()))\n\n        # Ensure the exact error is propagated, not swallowed\n        assert excinfo.value is error\n"
  },
  {
    "path": "test/plugins/test_parentwork.py",
    "content": "# This file is part of beets.\n# Copyright 2017, Dorian Soergel\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for the 'parentwork' plugin.\"\"\"\n\nimport pytest\n\nfrom beets.library import Item\nfrom beets.test.helper import PluginTestCase\n\n\n@pytest.mark.integration_test\nclass ParentWorkIntegrationTest(PluginTestCase):\n    plugin = \"parentwork\"\n\n    # test how it works with real musicbrainz data\n    def test_normal_case_real(self):\n        item = Item(\n            path=\"/file\",\n            mb_workid=\"e27bda6e-531e-36d3-9cd7-b8ebc18e8c53\",\n            parentwork_workid_current=\"e27bda6e-531e-36d3-9cd7-b8ebc18e8c53\",\n        )\n        item.add(self.lib)\n\n        self.run_command(\"parentwork\")\n\n        item.load()\n        assert item[\"mb_parentworkid\"] == \"32c8943f-1b27-3a23-8660-4567f4847c94\"\n\n    def test_force_real(self):\n        self.config[\"parentwork\"][\"force\"] = True\n        item = Item(\n            path=\"/file\",\n            mb_workid=\"e27bda6e-531e-36d3-9cd7-b8ebc18e8c53\",\n            mb_parentworkid=\"XXX\",\n            parentwork_workid_current=\"e27bda6e-531e-36d3-9cd7-b8ebc18e8c53\",\n            parentwork=\"whatever\",\n        )\n        item.add(self.lib)\n\n        self.run_command(\"parentwork\")\n\n        item.load()\n        assert item[\"mb_parentworkid\"] == \"32c8943f-1b27-3a23-8660-4567f4847c94\"\n\n    def test_no_force_real(self):\n        self.config[\"parentwork\"][\"force\"] = False\n        item = Item(\n            path=\"/file\",\n            mb_workid=\"e27bda6e-531e-36d3-9cd7-b8ebc18e8c53\",\n            mb_parentworkid=\"XXX\",\n            parentwork_workid_current=\"e27bda6e-531e-36d3-9cd7-b8ebc18e8c53\",\n            parentwork=\"whatever\",\n        )\n        item.add(self.lib)\n\n        self.run_command(\"parentwork\")\n\n        item.load()\n        assert item[\"mb_parentworkid\"] == \"XXX\"\n\n\nclass ParentWorkTest(PluginTestCase):\n    plugin = \"parentwork\"\n\n    @pytest.fixture(autouse=True)\n    def patch_works(self, requests_mock):\n        requests_mock.get(\n            \"/ws/2/work/1?inc=work-rels%2Bartist-rels\",\n            json={\n                \"id\": \"1\",\n                \"title\": \"work\",\n                \"work-relations\": [\n                    {\n                        \"type\": \"parts\",\n                        \"direction\": \"backward\",\n                        \"work\": {\"id\": \"2\"},\n                    }\n                ],\n            },\n        )\n        requests_mock.get(\n            \"/ws/2/work/2?inc=work-rels%2Bartist-rels\",\n            json={\n                \"id\": \"2\",\n                \"title\": \"directparentwork\",\n                \"work-relations\": [\n                    {\n                        \"type\": \"parts\",\n                        \"direction\": \"backward\",\n                        \"work\": {\"id\": \"3\"},\n                    }\n                ],\n            },\n        )\n        requests_mock.get(\n            \"/ws/2/work/3?inc=work-rels%2Bartist-rels\",\n            json={\n                \"id\": \"3\",\n                \"title\": \"parentwork\",\n                \"artist-relations\": [\n                    {\n                        \"type\": \"composer\",\n                        \"artist\": {\n                            \"name\": \"random composer\",\n                            \"sort-name\": \"composer, random\",\n                        },\n                    }\n                ],\n            },\n        )\n\n    def test_normal_case(self):\n        item = Item(path=\"/file\", mb_workid=\"1\", parentwork_workid_current=\"1\")\n        item.add(self.lib)\n\n        self.run_command(\"parentwork\")\n\n        item.load()\n        assert item[\"mb_parentworkid\"] == \"3\"\n\n    def test_force(self):\n        self.config[\"parentwork\"][\"force\"] = True\n        item = Item(\n            path=\"/file\",\n            mb_workid=\"1\",\n            mb_parentworkid=\"XXX\",\n            parentwork_workid_current=\"1\",\n            parentwork=\"parentwork\",\n        )\n        item.add(self.lib)\n\n        self.run_command(\"parentwork\")\n\n        item.load()\n        assert item[\"mb_parentworkid\"] == \"3\"\n\n    def test_no_force(self):\n        self.config[\"parentwork\"][\"force\"] = False\n        item = Item(\n            path=\"/file\",\n            mb_workid=\"1\",\n            mb_parentworkid=\"XXX\",\n            parentwork_workid_current=\"1\",\n            parentwork=\"parentwork\",\n        )\n        item.add(self.lib)\n\n        self.run_command(\"parentwork\")\n\n        item.load()\n        assert item[\"mb_parentworkid\"] == \"XXX\"\n"
  },
  {
    "path": "test/plugins/test_permissions.py",
    "content": "\"\"\"Tests for the 'permissions' plugin.\"\"\"\n\nimport os\nimport platform\nfrom unittest.mock import Mock, patch\n\nfrom beets.test._common import touch\nfrom beets.test.helper import AsIsImporterMixin, ImportTestCase, PluginMixin\nfrom beetsplug.permissions import (\n    check_permissions,\n    convert_perm,\n    dirs_in_library,\n)\n\n\nclass PermissionsPluginTest(AsIsImporterMixin, PluginMixin, ImportTestCase):\n    plugin = \"permissions\"\n\n    def setUp(self):\n        super().setUp()\n\n        self.config[\"permissions\"] = {\"file\": \"777\", \"dir\": \"777\"}\n\n    def test_permissions_on_album_imported(self):\n        self.import_and_check_permissions()\n\n    def test_permissions_on_item_imported(self):\n        self.config[\"import\"][\"singletons\"] = True\n        self.import_and_check_permissions()\n\n    def import_and_check_permissions(self):\n        if platform.system() == \"Windows\":\n            self.skipTest(\"permissions not available on Windows\")\n\n        track_file = os.path.join(self.import_dir, b\"album\", b\"track_1.mp3\")\n        assert os.stat(track_file).st_mode & 0o777 != 511\n\n        self.run_asis_importer()\n        item = self.lib.items().get()\n\n        paths = (item.path, *dirs_in_library(self.lib.directory, item.path))\n        for path in paths:\n            assert os.stat(path).st_mode & 0o777 == 511\n\n    def test_convert_perm_from_string(self):\n        assert convert_perm(\"10\") == 8\n\n    def test_convert_perm_from_int(self):\n        assert convert_perm(10) == 8\n\n    def test_permissions_on_set_art(self):\n        self.do_set_art(True)\n\n    @patch(\"os.chmod\", Mock())\n    def test_failing_permissions_on_set_art(self):\n        self.do_set_art(False)\n\n    def do_set_art(self, expect_success):\n        if platform.system() == \"Windows\":\n            self.skipTest(\"permissions not available on Windows\")\n        self.run_asis_importer()\n        album = self.lib.albums().get()\n        artpath = os.path.join(self.temp_dir, b\"cover.jpg\")\n        touch(artpath)\n        album.set_art(artpath)\n        assert expect_success == check_permissions(album.artpath, 0o777)\n"
  },
  {
    "path": "test/plugins/test_play.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Jesse Weinstein\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for the play plugin\"\"\"\n\nimport os\nimport sys\nimport unittest\nfrom unittest.mock import ANY, patch\n\nimport pytest\n\nfrom beets.test.helper import CleanupModulesMixin, IOMixin, PluginTestCase\nfrom beets.ui import UserError\nfrom beets.util import open_anything\nfrom beetsplug.play import PlayPlugin\n\n\n@patch(\"beetsplug.play.util.interactive_open\")\nclass PlayPluginTest(IOMixin, CleanupModulesMixin, PluginTestCase):\n    modules = (PlayPlugin.__module__,)\n    plugin = \"play\"\n\n    def setUp(self):\n        super().setUp()\n        self.item = self.add_item(album=\"a nice älbum\", title=\"aNiceTitle\")\n        self.lib.add_album([self.item])\n        self.config[\"play\"][\"command\"] = \"echo\"\n\n    def run_and_assert(\n        self,\n        open_mock,\n        args=(\"title:aNiceTitle\",),\n        expected_cmd=\"echo\",\n        expected_playlist=None,\n    ):\n        self.run_command(\"play\", *args)\n\n        open_mock.assert_called_once_with(ANY, expected_cmd)\n        expected_playlist = expected_playlist or self.item.path.decode(\"utf-8\")\n        exp_playlist = f\"{expected_playlist}\\n\"\n        with open(open_mock.call_args[0][0][0], \"rb\") as playlist:\n            assert exp_playlist == playlist.read().decode(\"utf-8\")\n\n    def test_basic(self, open_mock):\n        self.run_and_assert(open_mock)\n\n    def test_album_option(self, open_mock):\n        self.run_and_assert(open_mock, [\"-a\", \"nice\"])\n\n    def test_args_option(self, open_mock):\n        self.run_and_assert(\n            open_mock, [\"-A\", \"foo\", \"title:aNiceTitle\"], \"echo foo\"\n        )\n\n    def test_args_option_in_middle(self, open_mock):\n        self.config[\"play\"][\"command\"] = \"echo $args other\"\n\n        self.run_and_assert(\n            open_mock, [\"-A\", \"foo\", \"title:aNiceTitle\"], \"echo foo other\"\n        )\n\n    def test_unset_args_option_in_middle(self, open_mock):\n        self.config[\"play\"][\"command\"] = \"echo $args other\"\n\n        self.run_and_assert(open_mock, [\"title:aNiceTitle\"], \"echo other\")\n\n    # FIXME: fails on windows\n    @unittest.skipIf(sys.platform == \"win32\", \"win32\")\n    def test_relative_to(self, open_mock):\n        self.config[\"play\"][\"command\"] = \"echo\"\n        self.config[\"play\"][\"relative_to\"] = \"/something\"\n\n        path = os.path.relpath(self.item.path, b\"/something\")\n        playlist = path.decode(\"utf-8\")\n        self.run_and_assert(\n            open_mock, expected_cmd=\"echo\", expected_playlist=playlist\n        )\n\n    def test_use_folders(self, open_mock):\n        self.config[\"play\"][\"command\"] = None\n        self.config[\"play\"][\"use_folders\"] = True\n        self.run_command(\"play\", \"-a\", \"nice\")\n\n        open_mock.assert_called_once_with(ANY, open_anything())\n        with open(open_mock.call_args[0][0][0], \"rb\") as f:\n            playlist = f.read().decode(\"utf-8\")\n        assert f\"{self.item.filepath.parent}\\n\" == playlist\n\n    def test_raw(self, open_mock):\n        self.config[\"play\"][\"raw\"] = True\n\n        self.run_command(\"play\", \"nice\")\n\n        open_mock.assert_called_once_with([self.item.path], \"echo\")\n\n    def test_pls_marker(self, open_mock):\n        self.config[\"play\"][\"command\"] = (\n            \"echo --some params --playlist=$playlist --some-more params\"\n        )\n\n        self.run_command(\"play\", \"nice\")\n\n        open_mock.assert_called_once\n\n        commandstr = open_mock.call_args_list[0][0][1]\n        assert commandstr.startswith(\"echo --some params --playlist=\")\n        assert commandstr.endswith(\" --some-more params\")\n\n    def test_not_found(self, open_mock):\n        self.run_command(\"play\", \"not found\")\n\n        open_mock.assert_not_called()\n\n    def test_warning_threshold(self, open_mock):\n        self.config[\"play\"][\"warning_threshold\"] = 1\n        self.add_item(title=\"another NiceTitle\")\n\n        self.io.addinput(\"a\")\n        self.run_command(\"play\", \"nice\")\n\n        open_mock.assert_not_called()\n\n    def test_skip_warning_threshold_bypass(self, open_mock):\n        self.config[\"play\"][\"warning_threshold\"] = 1\n        self.other_item = self.add_item(title=\"another NiceTitle\")\n\n        expected_playlist = f\"{self.item.filepath}\\n{self.other_item.filepath}\"\n\n        self.io.addinput(\"a\")\n        self.run_and_assert(\n            open_mock,\n            [\"-y\", \"NiceTitle\"],\n            expected_playlist=expected_playlist,\n        )\n\n    def _playlist_lines(self, open_mock):\n        \"\"\"Read the playlist file passed to interactive_open and return its lines.\"\"\"\n        # interactive_open is called as: interactive_open([playlist_path], command)\n        playlist_path = open_mock.call_args[0][0][0]\n        with open(playlist_path, \"rb\") as playlist:\n            return playlist.read().decode(\"utf-8\").splitlines()\n\n    def _add_many_ordered_items(self, *, count, album):\n        items = []\n        for track in range(1, count + 1):\n            items.append(\n                self.add_item(\n                    album=album,\n                    artist=\"randomize artist\",\n                    title=f\"randomize {track:03d}\",\n                    track=track,\n                )\n            )\n        return items\n\n    def test_randomize(self, open_mock):\n        album = \"randomize_test\"\n        items = self._add_many_ordered_items(count=100, album=album)\n        baseline = [str(item.filepath) for item in items]\n\n        self.run_command(\"play\", \"-R\", f\"album:{album}\")\n        lines = self._playlist_lines(open_mock)\n        assert sorted(lines) == sorted(baseline), (\n            \"playlist items are not the same\"\n        )\n        assert lines != baseline, \"playlist order hasn't changed\"\n\n    def test_command_failed(self, open_mock):\n        open_mock.side_effect = OSError(\"some reason\")\n\n        with pytest.raises(UserError):\n            self.run_command(\"play\", \"title:aNiceTitle\")\n"
  },
  {
    "path": "test/plugins/test_playlist.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Thomas Scholtes.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nimport os\nfrom shlex import quote\n\nimport beets\nfrom beets.test import _common\nfrom beets.test.helper import PluginTestCase\n\n\nclass PlaylistTestCase(PluginTestCase):\n    plugin = \"playlist\"\n    preload_plugin = False\n\n    def setUp(self):\n        super().setUp()\n\n        self.music_dir = os.path.expanduser(os.path.join(\"~\", \"Music\"))\n\n        i1 = _common.item()\n        i1.path = beets.util.normpath(\n            os.path.join(\n                self.music_dir,\n                \"a\",\n                \"b\",\n                \"c.mp3\",\n            )\n        )\n        i1.title = \"some item\"\n        i1.album = \"some album\"\n        self.lib.add(i1)\n        self.lib.add_album([i1])\n\n        i2 = _common.item()\n        i2.path = beets.util.normpath(\n            os.path.join(\n                self.music_dir,\n                \"d\",\n                \"e\",\n                \"f.mp3\",\n            )\n        )\n        i2.title = \"another item\"\n        i2.album = \"another album\"\n        self.lib.add(i2)\n        self.lib.add_album([i2])\n\n        i3 = _common.item()\n        i3.path = beets.util.normpath(\n            os.path.join(\n                self.music_dir,\n                \"x\",\n                \"y\",\n                \"z.mp3\",\n            )\n        )\n        i3.title = \"yet another item\"\n        i3.album = \"yet another album\"\n        self.lib.add(i3)\n        self.lib.add_album([i3])\n\n        self.playlist_dir = self.temp_dir_path / \"playlists\"\n        self.playlist_dir.mkdir(parents=True, exist_ok=True)\n        self.config[\"directory\"] = self.music_dir\n        self.config[\"playlist\"][\"playlist_dir\"] = str(self.playlist_dir)\n\n        self.setup_test()\n        self.load_plugins()\n\n    def setup_test(self):\n        raise NotImplementedError\n\n\nclass PlaylistQueryTest:\n    def test_name_query_with_absolute_paths_in_playlist(self):\n        q = \"playlist:absolute\"\n        results = self.lib.items(q)\n        assert {i.title for i in results} == {\"some item\", \"another item\"}\n\n    def test_path_query_with_absolute_paths_in_playlist(self):\n        q = f\"playlist:{quote(os.path.join(self.playlist_dir, 'absolute.m3u'))}\"\n        results = self.lib.items(q)\n        assert {i.title for i in results} == {\"some item\", \"another item\"}\n\n    def test_name_query_with_relative_paths_in_playlist(self):\n        q = \"playlist:relative\"\n        results = self.lib.items(q)\n        assert {i.title for i in results} == {\"some item\", \"another item\"}\n\n    def test_path_query_with_relative_paths_in_playlist(self):\n        q = f\"playlist:{quote(os.path.join(self.playlist_dir, 'relative.m3u'))}\"\n        results = self.lib.items(q)\n        assert {i.title for i in results} == {\"some item\", \"another item\"}\n\n    def test_name_query_with_nonexisting_playlist(self):\n        q = \"playlist:nonexisting\"\n        results = self.lib.items(q)\n        assert set(results) == set()\n\n    def test_path_query_with_nonexisting_playlist(self):\n        q = f\"playlist:{os.path.join(self.playlist_dir, 'nonexisting.m3u')!r}\"\n        results = self.lib.items(q)\n        assert set(results) == set()\n\n\nclass PlaylistTestRelativeToLib(PlaylistQueryTest, PlaylistTestCase):\n    def setup_test(self):\n        with open(os.path.join(self.playlist_dir, \"absolute.m3u\"), \"w\") as f:\n            f.writelines(\n                [\n                    os.path.join(self.music_dir, \"a\", \"b\", \"c.mp3\") + \"\\n\",\n                    os.path.join(self.music_dir, \"d\", \"e\", \"f.mp3\") + \"\\n\",\n                    os.path.join(self.music_dir, \"nonexisting.mp3\") + \"\\n\",\n                ]\n            )\n\n        with open(os.path.join(self.playlist_dir, \"relative.m3u\"), \"w\") as f:\n            f.writelines(\n                [\n                    os.path.join(\"a\", \"b\", \"c.mp3\") + \"\\n\",\n                    os.path.join(\"d\", \"e\", \"f.mp3\") + \"\\n\",\n                    \"nonexisting.mp3\\n\",\n                ]\n            )\n\n        self.config[\"playlist\"][\"relative_to\"] = \"library\"\n\n\nclass PlaylistTestRelativeToDir(PlaylistQueryTest, PlaylistTestCase):\n    def setup_test(self):\n        with open(os.path.join(self.playlist_dir, \"absolute.m3u\"), \"w\") as f:\n            f.writelines(\n                [\n                    os.path.join(self.music_dir, \"a\", \"b\", \"c.mp3\") + \"\\n\",\n                    os.path.join(self.music_dir, \"d\", \"e\", \"f.mp3\") + \"\\n\",\n                    os.path.join(self.music_dir, \"nonexisting.mp3\") + \"\\n\",\n                ]\n            )\n\n        with open(os.path.join(self.playlist_dir, \"relative.m3u\"), \"w\") as f:\n            f.writelines(\n                [\n                    os.path.join(\"a\", \"b\", \"c.mp3\") + \"\\n\",\n                    os.path.join(\"d\", \"e\", \"f.mp3\") + \"\\n\",\n                    \"nonexisting.mp3\\n\",\n                ]\n            )\n\n        self.config[\"playlist\"][\"relative_to\"] = self.music_dir\n\n\nclass PlaylistTestRelativeToPls(PlaylistQueryTest, PlaylistTestCase):\n    def setup_test(self):\n        with open(os.path.join(self.playlist_dir, \"absolute.m3u\"), \"w\") as f:\n            f.writelines(\n                [\n                    os.path.join(self.music_dir, \"a\", \"b\", \"c.mp3\") + \"\\n\",\n                    os.path.join(self.music_dir, \"d\", \"e\", \"f.mp3\") + \"\\n\",\n                    os.path.join(self.music_dir, \"nonexisting.mp3\") + \"\\n\",\n                ]\n            )\n\n        with open(os.path.join(self.playlist_dir, \"relative.m3u\"), \"w\") as f:\n            f.writelines(\n                [\n                    os.path.relpath(\n                        os.path.join(self.music_dir, \"a\", \"b\", \"c.mp3\"),\n                        start=self.playlist_dir,\n                    )\n                    + \"\\n\",\n                    os.path.relpath(\n                        os.path.join(self.music_dir, \"d\", \"e\", \"f.mp3\"),\n                        start=self.playlist_dir,\n                    )\n                    + \"\\n\",\n                    os.path.relpath(\n                        os.path.join(self.music_dir, \"nonexisting.mp3\"),\n                        start=self.playlist_dir,\n                    )\n                    + \"\\n\",\n                ]\n            )\n\n        self.config[\"playlist\"][\"relative_to\"] = \"playlist\"\n        self.config[\"playlist\"][\"playlist_dir\"] = str(self.playlist_dir)\n\n\nclass PlaylistUpdateTest:\n    def setup_test(self):\n        with open(os.path.join(self.playlist_dir, \"absolute.m3u\"), \"w\") as f:\n            f.writelines(\n                [\n                    os.path.join(self.music_dir, \"a\", \"b\", \"c.mp3\") + \"\\n\",\n                    os.path.join(self.music_dir, \"d\", \"e\", \"f.mp3\") + \"\\n\",\n                    os.path.join(self.music_dir, \"nonexisting.mp3\") + \"\\n\",\n                ]\n            )\n\n        with open(os.path.join(self.playlist_dir, \"relative.m3u\"), \"w\") as f:\n            f.writelines(\n                [\n                    os.path.join(\"a\", \"b\", \"c.mp3\") + \"\\n\",\n                    os.path.join(\"d\", \"e\", \"f.mp3\") + \"\\n\",\n                    \"nonexisting.mp3\\n\",\n                ]\n            )\n\n        self.config[\"playlist\"][\"auto\"] = True\n        self.config[\"playlist\"][\"relative_to\"] = \"library\"\n\n\nclass PlaylistTestItemMoved(PlaylistUpdateTest, PlaylistTestCase):\n    def test_item_moved(self):\n        # Emit item_moved event for an item that is in a playlist\n        results = self.lib.items(\n            f\"path:{quote(os.path.join(self.music_dir, 'd', 'e', 'f.mp3'))}\"\n        )\n        item = results[0]\n        beets.plugins.send(\n            \"item_moved\",\n            item=item,\n            source=item.path,\n            destination=beets.util.bytestring_path(\n                os.path.join(self.music_dir, \"g\", \"h\", \"i.mp3\")\n            ),\n        )\n\n        # Emit item_moved event for an item that is not in a playlist\n        results = self.lib.items(\n            f\"path:{quote(os.path.join(self.music_dir, 'x', 'y', 'z.mp3'))}\"\n        )\n        item = results[0]\n        beets.plugins.send(\n            \"item_moved\",\n            item=item,\n            source=item.path,\n            destination=beets.util.bytestring_path(\n                os.path.join(self.music_dir, \"u\", \"v\", \"w.mp3\")\n            ),\n        )\n\n        # Emit cli_exit event\n        beets.plugins.send(\"cli_exit\", lib=self.lib)\n\n        # Check playlist with absolute paths\n        playlist_path = os.path.join(self.playlist_dir, \"absolute.m3u\")\n        with open(playlist_path) as f:\n            lines = [line.strip() for line in f.readlines()]\n\n        assert lines == [\n            os.path.join(self.music_dir, \"a\", \"b\", \"c.mp3\"),\n            os.path.join(self.music_dir, \"g\", \"h\", \"i.mp3\"),\n            os.path.join(self.music_dir, \"nonexisting.mp3\"),\n        ]\n\n        # Check playlist with relative paths\n        playlist_path = os.path.join(self.playlist_dir, \"relative.m3u\")\n        with open(playlist_path) as f:\n            lines = [line.strip() for line in f.readlines()]\n\n        assert lines == [\n            os.path.join(\"a\", \"b\", \"c.mp3\"),\n            os.path.join(\"g\", \"h\", \"i.mp3\"),\n            \"nonexisting.mp3\",\n        ]\n\n\nclass PlaylistTestItemRemoved(PlaylistUpdateTest, PlaylistTestCase):\n    def test_item_removed(self):\n        # Emit item_removed event for an item that is in a playlist\n        results = self.lib.items(\n            f\"path:{quote(os.path.join(self.music_dir, 'd', 'e', 'f.mp3'))}\"\n        )\n        item = results[0]\n        beets.plugins.send(\"item_removed\", item=item)\n\n        # Emit item_removed event for an item that is not in a playlist\n        results = self.lib.items(\n            f\"path:{quote(os.path.join(self.music_dir, 'x', 'y', 'z.mp3'))}\"\n        )\n        item = results[0]\n        beets.plugins.send(\"item_removed\", item=item)\n\n        # Emit cli_exit event\n        beets.plugins.send(\"cli_exit\", lib=self.lib)\n\n        # Check playlist with absolute paths\n        playlist_path = os.path.join(self.playlist_dir, \"absolute.m3u\")\n        with open(playlist_path) as f:\n            lines = [line.strip() for line in f.readlines()]\n\n        assert lines == [\n            os.path.join(self.music_dir, \"a\", \"b\", \"c.mp3\"),\n            os.path.join(self.music_dir, \"nonexisting.mp3\"),\n        ]\n\n        # Check playlist with relative paths\n        playlist_path = os.path.join(self.playlist_dir, \"relative.m3u\")\n        with open(playlist_path) as f:\n            lines = [line.strip() for line in f.readlines()]\n\n        assert lines == [os.path.join(\"a\", \"b\", \"c.mp3\"), \"nonexisting.mp3\"]\n"
  },
  {
    "path": "test/plugins/test_plexupdate.py",
    "content": "import responses\n\nfrom beets.test.helper import PluginTestCase\nfrom beetsplug.plexupdate import get_music_section, update_plex\n\n\nclass PlexUpdateTest(PluginTestCase):\n    plugin = \"plexupdate\"\n\n    def add_response_get_music_section(self, section_name=\"Music\"):\n        \"\"\"Create response for mocking the get_music_section function.\"\"\"\n\n        escaped_section_name = section_name.replace('\"', '\\\\\"')\n\n        body = (\n            '<?xml version=\"1.0\" encoding=\"UTF-8\"?>'\n            '<MediaContainer size=\"3\" allowSync=\"0\" '\n            'identifier=\"com.plexapp.plugins.library\" '\n            'mediaTagPrefix=\"/system/bundle/media/flags/\" '\n            'mediaTagVersion=\"1413367228\" title1=\"Plex Library\">'\n            '<Directory allowSync=\"0\" art=\"/:/resources/movie-fanart.jpg\" '\n            'filters=\"1\" refreshing=\"0\" thumb=\"/:/resources/movie.png\" '\n            'key=\"3\" type=\"movie\" title=\"Movies\" '\n            'composite=\"/library/sections/3/composite/1416232668\" '\n            'agent=\"com.plexapp.agents.imdb\" scanner=\"Plex Movie Scanner\" '\n            'language=\"de\" uuid=\"92f68526-21eb-4ee2-8e22-d36355a17f1f\" '\n            'updatedAt=\"1416232668\" createdAt=\"1415720680\">'\n            '<Location id=\"3\" path=\"/home/marv/Media/Videos/Movies\" />'\n            \"</Directory>\"\n            '<Directory allowSync=\"0\" art=\"/:/resources/artist-fanart.jpg\" '\n            'filters=\"1\" refreshing=\"0\" thumb=\"/:/resources/artist.png\" '\n            f'key=\"2\" type=\"artist\" title=\"{escaped_section_name}\" '\n            'composite=\"/library/sections/2/composite/1416929243\" '\n            'agent=\"com.plexapp.agents.lastfm\" scanner=\"Plex Music Scanner\" '\n            'language=\"en\" uuid=\"90897c95-b3bd-4778-a9c8-1f43cb78f047\" '\n            'updatedAt=\"1416929243\" createdAt=\"1415691331\">'\n            '<Location id=\"2\" path=\"/home/marv/Media/Musik\" />'\n            \"</Directory>\"\n            '<Directory allowSync=\"0\" art=\"/:/resources/show-fanart.jpg\" '\n            'filters=\"1\" refreshing=\"0\" thumb=\"/:/resources/show.png\" '\n            'key=\"1\" type=\"show\" title=\"TV Shows\" '\n            'composite=\"/library/sections/1/composite/1416320800\" '\n            'agent=\"com.plexapp.agents.thetvdb\" scanner=\"Plex Series Scanner\" '\n            'language=\"de\" uuid=\"04d2249b-160a-4ae9-8100-106f4ec1a218\" '\n            'updatedAt=\"1416320800\" createdAt=\"1415690983\">'\n            '<Location id=\"1\" path=\"/home/marv/Media/Videos/Series\" />'\n            \"</Directory>\"\n            \"</MediaContainer>\"\n        )\n        status = 200\n        content_type = \"text/xml;charset=utf-8\"\n\n        responses.add(\n            responses.GET,\n            \"http://localhost:32400/library/sections\",\n            body=body,\n            status=status,\n            content_type=content_type,\n        )\n\n    def add_response_update_plex(self):\n        \"\"\"Create response for mocking the update_plex function.\"\"\"\n        body = \"\"\n        status = 200\n        content_type = \"text/html\"\n\n        responses.add(\n            responses.GET,\n            \"http://localhost:32400/library/sections/2/refresh\",\n            body=body,\n            status=status,\n            content_type=content_type,\n        )\n\n    def setUp(self):\n        super().setUp()\n\n        self.config[\"plex\"] = {\"host\": \"localhost\", \"port\": 32400}\n\n    @responses.activate\n    def test_get_music_section(self):\n        # Adding response.\n        self.add_response_get_music_section()\n\n        # Test if section key is \"2\" out of the mocking data.\n        assert (\n            get_music_section(\n                self.config[\"plex\"][\"host\"],\n                self.config[\"plex\"][\"port\"],\n                self.config[\"plex\"][\"token\"],\n                self.config[\"plex\"][\"library_name\"].get(),\n                self.config[\"plex\"][\"secure\"],\n                self.config[\"plex\"][\"ignore_cert_errors\"],\n            )\n            == \"2\"\n        )\n\n    @responses.activate\n    def test_get_named_music_section(self):\n        # Adding response.\n        self.add_response_get_music_section(\"My Music Library\")\n\n        assert (\n            get_music_section(\n                self.config[\"plex\"][\"host\"],\n                self.config[\"plex\"][\"port\"],\n                self.config[\"plex\"][\"token\"],\n                \"My Music Library\",\n                self.config[\"plex\"][\"secure\"],\n                self.config[\"plex\"][\"ignore_cert_errors\"],\n            )\n            == \"2\"\n        )\n\n    @responses.activate\n    def test_update_plex(self):\n        # Adding responses.\n        self.add_response_get_music_section()\n        self.add_response_update_plex()\n\n        # Testing status code of the mocking request.\n        assert (\n            update_plex(\n                self.config[\"plex\"][\"host\"],\n                self.config[\"plex\"][\"port\"],\n                self.config[\"plex\"][\"token\"],\n                self.config[\"plex\"][\"library_name\"].get(),\n                self.config[\"plex\"][\"secure\"],\n                self.config[\"plex\"][\"ignore_cert_errors\"],\n            ).status_code\n            == 200\n        )\n"
  },
  {
    "path": "test/plugins/test_plugin_mediafield.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests the facility that lets plugins add custom field to MediaFile.\"\"\"\n\nimport os\nimport shutil\n\nimport mediafile\nimport pytest\n\nfrom beets.library import Item\nfrom beets.plugins import BeetsPlugin\nfrom beets.test import _common\nfrom beets.test.helper import BeetsTestCase\nfrom beets.util import bytestring_path, syspath\n\nfield_extension = mediafile.MediaField(\n    mediafile.MP3DescStorageStyle(\"customtag\"),\n    mediafile.MP4StorageStyle(\"----:com.apple.iTunes:customtag\"),\n    mediafile.StorageStyle(\"customtag\"),\n    mediafile.ASFStorageStyle(\"customtag\"),\n)\n\nlist_field_extension = mediafile.ListMediaField(\n    mediafile.MP3ListDescStorageStyle(\"customlisttag\"),\n    mediafile.MP4ListStorageStyle(\"----:com.apple.iTunes:customlisttag\"),\n    mediafile.ListStorageStyle(\"customlisttag\"),\n    mediafile.ASFStorageStyle(\"customlisttag\"),\n)\n\n\nclass ExtendedFieldTestMixin(BeetsTestCase):\n    def _mediafile_fixture(self, name, extension=\"mp3\"):\n        name = bytestring_path(f\"{name}.{extension}\")\n        src = os.path.join(_common.RSRC, name)\n        target = os.path.join(self.temp_dir, name)\n        shutil.copy(syspath(src), syspath(target))\n        return mediafile.MediaFile(target)\n\n    def test_extended_field_write(self):\n        plugin = BeetsPlugin()\n        plugin.add_media_field(\"customtag\", field_extension)\n\n        try:\n            mf = self._mediafile_fixture(\"empty\")\n            mf.customtag = \"F#\"\n            mf.save()\n\n            mf = mediafile.MediaFile(mf.path)\n            assert mf.customtag == \"F#\"\n\n        finally:\n            delattr(mediafile.MediaFile, \"customtag\")\n            Item._media_fields.remove(\"customtag\")\n\n    def test_extended_list_field_write(self):\n        plugin = BeetsPlugin()\n        plugin.add_media_field(\"customlisttag\", list_field_extension)\n\n        try:\n            mf = self._mediafile_fixture(\"empty\")\n            mf.customlisttag = [\"a\", \"b\"]\n            mf.save()\n\n            mf = mediafile.MediaFile(mf.path)\n            assert mf.customlisttag == [\"a\", \"b\"]\n\n        finally:\n            delattr(mediafile.MediaFile, \"customlisttag\")\n            Item._media_fields.remove(\"customlisttag\")\n\n    def test_write_extended_tag_from_item(self):\n        plugin = BeetsPlugin()\n        plugin.add_media_field(\"customtag\", field_extension)\n\n        try:\n            mf = self._mediafile_fixture(\"empty\")\n            assert mf.customtag is None\n\n            item = Item(path=mf.path, customtag=\"Gb\")\n            item.write()\n            mf = mediafile.MediaFile(mf.path)\n            assert mf.customtag == \"Gb\"\n\n        finally:\n            delattr(mediafile.MediaFile, \"customtag\")\n            Item._media_fields.remove(\"customtag\")\n\n    def test_read_flexible_attribute_from_file(self):\n        plugin = BeetsPlugin()\n        plugin.add_media_field(\"customtag\", field_extension)\n\n        try:\n            mf = self._mediafile_fixture(\"empty\")\n            mf.update({\"customtag\": \"F#\"})\n            mf.save()\n\n            item = Item.from_path(mf.path)\n            assert item[\"customtag\"] == \"F#\"\n\n        finally:\n            delattr(mediafile.MediaFile, \"customtag\")\n            Item._media_fields.remove(\"customtag\")\n\n    def test_invalid_descriptor(self):\n        with pytest.raises(\n            ValueError, match=\"must be an instance of MediaField\"\n        ):\n            mediafile.MediaFile.add_field(\"somekey\", True)\n\n    def test_overwrite_property(self):\n        with pytest.raises(\n            ValueError, match='property \"artist\" already exists'\n        ):\n            mediafile.MediaFile.add_field(\"artist\", mediafile.MediaField())\n"
  },
  {
    "path": "test/plugins/test_random.py",
    "content": "# This file is part of beets.\n# Copyright 2019, Carl Suster\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Test the beets.random utilities associated with the random plugin.\"\"\"\n\nimport math\nimport random\n\nimport pytest\n\nfrom beets.test.helper import TestHelper\nfrom beetsplug.random import _equal_chance_permutation, random_objs\n\n\n@pytest.fixture(scope=\"class\")\ndef helper():\n    helper = TestHelper()\n    helper.setup_beets()\n\n    yield helper\n\n    helper.teardown_beets()\n\n\n@pytest.fixture(scope=\"module\", autouse=True)\ndef seed_random():\n    random.seed(12345)\n\n\nclass TestEqualChancePermutation:\n    \"\"\"Test the _equal_chance_permutation function.\"\"\"\n\n    @pytest.fixture(autouse=True)\n    def setup(self, helper):\n        \"\"\"Set up the test environment with items.\"\"\"\n        self.lib = helper.lib\n        self.artist1 = \"Artist 1\"\n        self.artist2 = \"Artist 2\"\n        self.item1 = helper.create_item(artist=self.artist1)\n        self.item2 = helper.create_item(artist=self.artist2)\n        self.items = [self.item1, self.item2]\n        for _ in range(8):\n            self.items.append(helper.create_item(artist=self.artist2))\n\n    def _stats(self, data):\n        mean = sum(data) / len(data)\n        stdev = math.sqrt(sum((p - mean) ** 2 for p in data) / (len(data) - 1))\n        quot, rem = divmod(len(data), 2)\n        if rem:\n            median = sorted(data)[quot]\n        else:\n            median = sum(sorted(data)[quot - 1 : quot + 1]) / 2\n        return mean, stdev, median\n\n    def test_equal_permutation(self):\n        \"\"\"We have a list of items where only one item is from artist1 and the\n        rest are from artist2. If we permute weighted by the artist field then\n        the solo track will almost always end up near the start. If we use a\n        different field then it'll be in the middle on average.\n        \"\"\"\n\n        def experiment(field, histogram=False):\n            \"\"\"Permutes the list of items 500 times and calculates the position\n            of self.item1 each time. Returns stats about that position.\n            \"\"\"\n            positions = []\n            for _ in range(500):\n                shuffled = list(\n                    _equal_chance_permutation(self.items, field=field)\n                )\n                positions.append(shuffled.index(self.item1))\n            # Print a histogram (useful for debugging).\n            if histogram:\n                for i in range(len(self.items)):\n                    print(f\"{i:2d} {'*' * positions.count(i)}\")\n            return self._stats(positions)\n\n        _, stdev1, median1 = experiment(\"artist\")\n        _, stdev2, median2 = experiment(\"track\")\n        assert 0 == pytest.approx(median1, abs=1)\n        assert len(self.items) // 2 == pytest.approx(median2, abs=1)\n        assert stdev2 > stdev1\n\n    @pytest.mark.parametrize(\n        \"input_items, field, expected\",\n        [\n            ([], \"artist\", []),\n            ([{\"artist\": \"Artist 1\"}], \"artist\", [{\"artist\": \"Artist 1\"}]),\n            # Missing field should not raise an error, but return empty\n            ([{\"artist\": \"Artist 1\"}], \"nonexistent\", []),\n            # Multiple items with the same field value\n            (\n                [{\"artist\": \"Artist 1\"}, {\"artist\": \"Artist 1\"}],\n                \"artist\",\n                [{\"artist\": \"Artist 1\"}, {\"artist\": \"Artist 1\"}],\n            ),\n        ],\n    )\n    def test_equal_permutation_items(\n        self, input_items, field, expected, helper\n    ):\n        \"\"\"Test _equal_chance_permutation with empty input.\"\"\"\n        result = list(\n            _equal_chance_permutation(\n                [helper.create_item(**i) for i in input_items], field\n            )\n        )\n\n        for item in expected:\n            for key, value in item.items():\n                assert any(getattr(r, key) == value for r in result)\n        assert len(result) == len(expected)\n\n\nclass TestRandomObjs:\n    \"\"\"Test the random_objs function.\"\"\"\n\n    @pytest.fixture(autouse=True)\n    def setup(self, helper):\n        \"\"\"Set up the test environment with items.\"\"\"\n        self.lib = helper.lib\n        self.artist1 = \"Artist 1\"\n        self.artist2 = \"Artist 2\"\n        self.items = [\n            helper.create_item(artist=self.artist1, length=180),  # 3 minutes\n            helper.create_item(artist=self.artist2, length=240),  # 4 minutes\n            helper.create_item(artist=self.artist2, length=300),  # 5 minutes\n        ]\n\n    def test_random_selection_by_count(self):\n        \"\"\"Test selecting a specific number of items.\"\"\"\n        selected = list(random_objs(self.items, \"artist\", number=2))\n        assert len(selected) == 2\n        assert all(item in self.items for item in selected)\n\n    def test_random_selection_by_time(self):\n        \"\"\"Test selecting items constrained by total time (minutes).\"\"\"\n        selected = list(\n            random_objs(self.items, \"artist\", time_minutes=6)\n        )  # 6 minutes\n        total_time = (\n            sum(item.length for item in selected) / 60\n        )  # Convert to minutes\n        assert total_time <= 6\n\n    def test_equal_chance_permutation(self, helper):\n        \"\"\"Test equal chance permutation ensures balanced artist selection.\"\"\"\n        # Add more items to make the test meaningful\n        for _ in range(5):\n            self.items.append(\n                helper.create_item(artist=self.artist1, length=180)\n            )\n\n        selected = list(\n            random_objs(self.items, \"artist\", number=10, equal_chance=True)\n        )\n        artist_counts = {}\n        for item in selected:\n            artist_counts[item.artist] = artist_counts.get(item.artist, 0) + 1\n\n        # Ensure both artists are represented (not strictly equal due to randomness)\n        assert len(artist_counts) >= 2\n\n    def test_empty_input_list(self):\n        \"\"\"Test behavior with an empty input list.\"\"\"\n        selected = list(random_objs([], \"artist\", number=1))\n        assert len(selected) == 0\n\n    def test_no_constraints_returns_all(self):\n        \"\"\"Test that no constraints return all items in random order.\"\"\"\n        selected = list(random_objs(self.items, \"artist\", number=3))\n        assert len(selected) == len(self.items)\n        assert set(selected) == set(self.items)\n"
  },
  {
    "path": "test/plugins/test_replace.py",
    "content": "import shutil\nfrom pathlib import Path\n\nimport pytest\nfrom mediafile import MediaFile\n\nfrom beets import ui\nfrom beets.test import _common\nfrom beetsplug.replace import ReplacePlugin\n\nreplace = ReplacePlugin()\n\n\nclass TestReplace:\n    @pytest.fixture(autouse=True)\n    def _fake_dir(self, tmp_path):\n        self.fake_dir = tmp_path\n\n    @pytest.fixture(autouse=True)\n    def _fake_file(self, tmp_path):\n        self.fake_file = tmp_path\n\n    def test_path_is_dir(self):\n        fake_directory = self.fake_dir / \"fakeDir\"\n        fake_directory.mkdir()\n        with pytest.raises(ui.UserError):\n            replace.file_check(fake_directory)\n\n    def test_path_is_unsupported_file(self):\n        fake_file = self.fake_file / \"fakefile.txt\"\n        fake_file.write_text(\"test\", encoding=\"utf-8\")\n        with pytest.raises(ui.UserError):\n            replace.file_check(fake_file)\n\n    def test_path_is_supported_file(self):\n        dest = self.fake_file / \"full.mp3\"\n        src = Path(_common.RSRC.decode()) / \"full.mp3\"\n        shutil.copyfile(src, dest)\n\n        mediafile = MediaFile(dest)\n        mediafile.albumartist = \"AAA\"\n        mediafile.disctitle = \"DDD\"\n        mediafile.genres = [\"a\", \"b\", \"c\"]\n        mediafile.composer = None\n        mediafile.save()\n\n        replace.file_check(Path(str(dest)))\n\n    def test_select_song_valid_choice(self, monkeypatch, capfd):\n        songs = [\"Song A\", \"Song B\", \"Song C\"]\n        monkeypatch.setattr(\"builtins.input\", lambda _: \"2\")\n\n        selected_song = replace.select_song(songs)\n\n        captured = capfd.readouterr()\n\n        assert \"1. Song A\" in captured.out\n        assert \"2. Song B\" in captured.out\n        assert \"3. Song C\" in captured.out\n        assert selected_song == \"Song B\"\n\n    def test_select_song_cancel(self, monkeypatch):\n        songs = [\"Song A\", \"Song B\", \"Song C\"]\n        monkeypatch.setattr(\"builtins.input\", lambda _: \"0\")\n\n        selected_song = replace.select_song(songs)\n\n        assert selected_song is None\n\n    def test_select_song_invalid_then_valid(self, monkeypatch, capfd):\n        songs = [\"Song A\", \"Song B\", \"Song C\"]\n        inputs = iter([\"invalid\", \"4\", \"3\"])\n        monkeypatch.setattr(\"builtins.input\", lambda _: next(inputs))\n\n        selected_song = replace.select_song(songs)\n\n        captured = capfd.readouterr()\n\n        assert \"Invalid input. Please type in a number.\" in captured.out\n        assert (\n            \"Invalid choice. Please enter a number between 1 and 3.\"\n            in captured.out\n        )\n        assert selected_song == \"Song C\"\n\n    def test_confirm_replacement_file_not_exist(self):\n        class Song:\n            path = b\"test123321.txt\"\n\n        song = Song()\n\n        with pytest.raises(ui.UserError):\n            replace.confirm_replacement(\"test\", song)\n\n    def test_confirm_replacement_yes(self, monkeypatch):\n        src = Path(_common.RSRC.decode()) / \"full.mp3\"\n        monkeypatch.setattr(\"builtins.input\", lambda _: \"YES    \")\n\n        class Song:\n            path = str(src).encode()\n\n        song = Song()\n\n        assert replace.confirm_replacement(\"test\", song) is True\n\n    def test_confirm_replacement_no(self, monkeypatch):\n        src = Path(_common.RSRC.decode()) / \"full.mp3\"\n        monkeypatch.setattr(\"builtins.input\", lambda _: \"test123\")\n\n        class Song:\n            path = str(src).encode()\n\n        song = Song()\n\n        assert replace.confirm_replacement(\"test\", song) is False\n"
  },
  {
    "path": "test/plugins/test_replaygain.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Thomas Scholtes\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nimport unittest\nfrom typing import Any, ClassVar\n\nimport pytest\nfrom mediafile import MediaFile\n\nfrom beets.test.helper import (\n    AsIsImporterMixin,\n    ImportTestCase,\n    PluginMixin,\n    has_program,\n)\nfrom beetsplug.replaygain import (\n    FatalGstreamerPluginReplayGainError,\n    GStreamerBackend,\n)\n\ntry:\n    import gi\n\n    gi.require_version(\"Gst\", \"1.0\")\n    GST_AVAILABLE = True\nexcept (ImportError, ValueError):\n    GST_AVAILABLE = False\n\n\nGAIN_PROG = next(\n    (\n        cmd\n        for cmd in [\"mp3gain\", \"mp3rgain\", \"aacgain\"]\n        if has_program(cmd, [\"-v\"])\n    ),\n    None,\n)\n\nFFMPEG_AVAILABLE = has_program(\"ffmpeg\", [\"-version\"])\n\n\ndef reset_replaygain(item):\n    item[\"rg_track_peak\"] = None\n    item[\"rg_track_gain\"] = None\n    item[\"rg_album_gain\"] = None\n    item[\"rg_album_gain\"] = None\n    item[\"r128_track_gain\"] = None\n    item[\"r128_album_gain\"] = None\n    item.write()\n    item.store()\n\n\nclass ReplayGainTestCase(PluginMixin, ImportTestCase):\n    db_on_disk = True\n    plugin = \"replaygain\"\n    preload_plugin = False\n\n    plugin_config: ClassVar[dict[str, Any]]\n\n    @property\n    def backend(self):\n        return self.plugin_config[\"backend\"]\n\n    def setUp(self):\n        # Implemented by Mixins, see above. This may decide to skip the test.\n        self.test_backend()\n\n        super().setUp()\n        self.config[\"replaygain\"].set(self.plugin_config)\n\n        self.load_plugins()\n\n\nclass ThreadedImportMixin:\n    def setUp(self):\n        super().setUp()\n        self.config[\"threaded\"] = True\n\n\nclass BackendMixin:\n    plugin_config: ClassVar[dict[str, Any]]\n    has_r128_support: bool\n\n    def test_backend(self):\n        \"\"\"Check whether the backend actually has all required functionality.\"\"\"\n\n\nclass GstBackendMixin(BackendMixin):\n    plugin_config: ClassVar[dict[str, Any]] = {\"backend\": \"gstreamer\"}\n    has_r128_support = True\n\n    def test_backend(self):\n        \"\"\"Check whether the backend actually has all required functionality.\"\"\"\n        try:\n            # Check if required plugins can be loaded by instantiating a\n            # GStreamerBackend (via its .__init__).\n            self.config[\"replaygain\"][\"targetlevel\"] = 89\n            GStreamerBackend(self.config[\"replaygain\"], None)\n        except FatalGstreamerPluginReplayGainError as e:\n            # Skip the test if plugins could not be loaded.\n            self.skipTest(str(e))\n\n\nclass CmdBackendMixin(BackendMixin):\n    plugin_config: ClassVar[dict[str, Any]] = {\n        \"backend\": \"command\",\n        \"command\": GAIN_PROG,\n    }\n    has_r128_support = False\n\n\nclass FfmpegBackendMixin(BackendMixin):\n    plugin_config: ClassVar[dict[str, Any]] = {\"backend\": \"ffmpeg\"}\n    has_r128_support = True\n\n\nclass ReplayGainCliTest:\n    FNAME: str\n\n    def _add_album(self, *args, **kwargs):\n        # Use a file with non-zero volume (most test assets are total silence)\n        album = self.add_album_fixture(*args, fname=self.FNAME, **kwargs)\n        for item in album.items():\n            reset_replaygain(item)\n\n        return album\n\n    def test_cli_saves_track_gain(self):\n        self._add_album(2)\n\n        for item in self.lib.items():\n            assert item.rg_track_peak is None\n            assert item.rg_track_gain is None\n            mediafile = MediaFile(item.path)\n            assert mediafile.rg_track_peak is None\n            assert mediafile.rg_track_gain is None\n\n        self.run_command(\"replaygain\")\n\n        # Skip the test if rg_track_peak and rg_track gain is None, assuming\n        # that it could only happen if the decoder plugins are missing.\n        if all(\n            i.rg_track_peak is None and i.rg_track_gain is None\n            for i in self.lib.items()\n        ):\n            self.skipTest(\"decoder plugins could not be loaded.\")\n\n        for item in self.lib.items():\n            assert item.rg_track_peak is not None\n            assert item.rg_track_gain is not None\n            mediafile = MediaFile(item.path)\n            assert mediafile.rg_track_peak == pytest.approx(\n                item.rg_track_peak, abs=1e-6\n            )\n            assert mediafile.rg_track_gain == pytest.approx(\n                item.rg_track_gain, abs=1e-2\n            )\n\n    def test_cli_skips_calculated_tracks(self):\n        album_rg = self._add_album(1)\n        item_rg = album_rg.items()[0]\n\n        if self.has_r128_support:\n            album_r128 = self._add_album(1, ext=\"opus\")\n            item_r128 = album_r128.items()[0]\n\n        self.run_command(\"replaygain\")\n\n        item_rg.load()\n        assert item_rg.rg_track_gain is not None\n        assert item_rg.rg_track_peak is not None\n        assert item_rg.r128_track_gain is None\n\n        item_rg.rg_track_gain += 1.0\n        item_rg.rg_track_peak += 1.0\n        item_rg.store()\n        rg_track_gain = item_rg.rg_track_gain\n        rg_track_peak = item_rg.rg_track_peak\n\n        if self.has_r128_support:\n            item_r128.load()\n            assert item_r128.r128_track_gain is not None\n            assert item_r128.rg_track_gain is None\n            assert item_r128.rg_track_peak is None\n\n            item_r128.r128_track_gain += 1.0\n            item_r128.store()\n            r128_track_gain = item_r128.r128_track_gain\n\n        self.run_command(\"replaygain\")\n\n        item_rg.load()\n        assert item_rg.rg_track_gain == rg_track_gain\n        assert item_rg.rg_track_peak == rg_track_peak\n\n        if self.has_r128_support:\n            item_r128.load()\n            assert item_r128.r128_track_gain == r128_track_gain\n\n    def test_cli_does_not_skip_wrong_tag_type(self):\n        \"\"\"Check that items that have tags of the wrong type won't be skipped.\"\"\"\n        if not self.has_r128_support:\n            # This test is a lot less interesting if the backend cannot write\n            # both tag types.\n            self.skipTest(\n                f\"r128 tags for opus not supported on backend {self.backend}\"\n            )\n\n        album_rg = self._add_album(1)\n        item_rg = album_rg.items()[0]\n\n        album_r128 = self._add_album(1, ext=\"opus\")\n        item_r128 = album_r128.items()[0]\n\n        item_rg.r128_track_gain = 0.0\n        item_rg.store()\n\n        item_r128.rg_track_gain = 0.0\n        item_r128.rg_track_peak = 42.0\n        item_r128.store()\n\n        self.run_command(\"replaygain\")\n        item_rg.load()\n        item_r128.load()\n\n        assert item_rg.rg_track_gain is not None\n        assert item_rg.rg_track_peak is not None\n        # FIXME: Should the plugin null this field?\n        # assert item_rg.r128_track_gain is None\n\n        assert item_r128.r128_track_gain is not None\n        # FIXME: Should the plugin null these fields?\n        # assert item_r128.rg_track_gain is None\n        # assert item_r128.rg_track_peak is None\n\n    def test_cli_saves_album_gain_to_file(self):\n        self._add_album(2)\n\n        for item in self.lib.items():\n            mediafile = MediaFile(item.path)\n            assert mediafile.rg_album_peak is None\n            assert mediafile.rg_album_gain is None\n\n        self.run_command(\"replaygain\", \"-a\")\n\n        peaks = []\n        gains = []\n        for item in self.lib.items():\n            mediafile = MediaFile(item.path)\n            peaks.append(mediafile.rg_album_peak)\n            gains.append(mediafile.rg_album_gain)\n\n        # Make sure they are all the same\n        assert max(peaks) == min(peaks)\n        assert max(gains) == min(gains)\n\n        assert max(gains) != 0.0\n        assert max(peaks) != 0.0\n\n    def test_cli_writes_only_r128_tags(self):\n        if not self.has_r128_support:\n            self.skipTest(\n                f\"r128 tags for opus not supported on backend {self.backend}\"\n            )\n\n        album = self._add_album(2, ext=\"opus\")\n\n        self.run_command(\"replaygain\", \"-a\")\n\n        for item in album.items():\n            mediafile = MediaFile(item.path)\n            # does not write REPLAYGAIN_* tags\n            assert mediafile.rg_track_gain is None\n            assert mediafile.rg_album_gain is None\n            # writes R128_* tags\n            assert mediafile.r128_track_gain is not None\n            assert mediafile.r128_album_gain is not None\n\n    def test_targetlevel_has_effect(self):\n        album = self._add_album(1)\n        item = album.items()[0]\n\n        def analyse(target_level):\n            self.config[\"replaygain\"][\"targetlevel\"] = target_level\n            self.run_command(\"replaygain\", \"-f\")\n            item.load()\n            return item.rg_track_gain\n\n        gain_relative_to_84 = analyse(84)\n        gain_relative_to_89 = analyse(89)\n\n        assert gain_relative_to_84 != gain_relative_to_89\n\n    def test_r128_targetlevel_has_effect(self):\n        if not self.has_r128_support:\n            self.skipTest(\n                f\"r128 tags for opus not supported on backend {self.backend}\"\n            )\n\n        album = self._add_album(1, ext=\"opus\")\n        item = album.items()[0]\n\n        def analyse(target_level):\n            self.config[\"replaygain\"][\"r128_targetlevel\"] = target_level\n            self.run_command(\"replaygain\", \"-f\")\n            item.load()\n            return item.r128_track_gain\n\n        gain_relative_to_84 = analyse(84)\n        gain_relative_to_89 = analyse(89)\n\n        assert gain_relative_to_84 != gain_relative_to_89\n\n    def test_per_disc(self):\n        # Use the per_disc option and add a little more concurrency.\n        album = self._add_album(track_count=4, disc_count=3)\n        self.config[\"replaygain\"][\"per_disc\"] = True\n        self.run_command(\"replaygain\", \"-a\")\n\n        # FIXME: Add fixtures with known track/album gain (within a suitable\n        # tolerance) so that we can actually check per-disc operation here.\n        for item in album.items():\n            assert item.rg_track_gain is not None\n            assert item.rg_album_gain is not None\n\n\n@unittest.skipIf(not GST_AVAILABLE, \"gstreamer cannot be found\")\nclass ReplayGainGstCliTest(\n    ReplayGainCliTest, ReplayGainTestCase, GstBackendMixin\n):\n    FNAME = \"full\"  # file contains only silence\n\n\n@unittest.skipIf(not GAIN_PROG, \"no *gain command found\")\nclass ReplayGainCmdCliTest(\n    ReplayGainCliTest, ReplayGainTestCase, CmdBackendMixin\n):\n    FNAME = \"full\"  # file contains only silence\n\n\n@unittest.skipIf(not FFMPEG_AVAILABLE, \"ffmpeg cannot be found\")\nclass ReplayGainFfmpegCliTest(\n    ReplayGainCliTest, ReplayGainTestCase, FfmpegBackendMixin\n):\n    FNAME = \"full\"  # file contains only silence\n\n\n@unittest.skipIf(not FFMPEG_AVAILABLE, \"ffmpeg cannot be found\")\nclass ReplayGainFfmpegNoiseCliTest(\n    ReplayGainCliTest, ReplayGainTestCase, FfmpegBackendMixin\n):\n    FNAME = \"whitenoise\"\n\n\nclass ImportTest(AsIsImporterMixin):\n    def test_import_converted(self):\n        self.run_asis_importer()\n        for item in self.lib.items():\n            # FIXME: Add fixtures with known track/album gain (within a\n            # suitable tolerance) so that we can actually check correct\n            # operation here.\n            assert item.rg_track_gain is not None\n            assert item.rg_album_gain is not None\n\n\n@unittest.skipIf(not GST_AVAILABLE, \"gstreamer cannot be found\")\nclass ReplayGainGstImportTest(ImportTest, ReplayGainTestCase, GstBackendMixin):\n    pass\n\n\n@unittest.skipIf(not GAIN_PROG, \"no *gain command found\")\nclass ReplayGainCmdImportTest(ImportTest, ReplayGainTestCase, CmdBackendMixin):\n    pass\n\n\n@unittest.skipIf(not FFMPEG_AVAILABLE, \"ffmpeg cannot be found\")\nclass ReplayGainFfmpegImportTest(\n    ImportTest, ReplayGainTestCase, FfmpegBackendMixin\n):\n    pass\n\n\n@unittest.skipIf(not FFMPEG_AVAILABLE, \"ffmpeg cannot be found\")\nclass ReplayGainFfmpegThreadedImportTest(\n    ThreadedImportMixin, ImportTest, ReplayGainTestCase, FfmpegBackendMixin\n):\n    pass\n"
  },
  {
    "path": "test/plugins/test_scrub.py",
    "content": "import os\n\nfrom mediafile import MediaFile\n\nfrom beets.test.helper import AsIsImporterMixin, ImportTestCase, PluginMixin\n\n\nclass ScrubbedImportTest(AsIsImporterMixin, PluginMixin, ImportTestCase):\n    db_on_disk = True\n    plugin = \"scrub\"\n\n    def test_tags_not_scrubbed(self):\n        with self.configure_plugin({\"auto\": False}):\n            self.run_asis_importer(write=True)\n\n        for item in self.lib.items():\n            imported_file = MediaFile(os.path.join(item.path))\n            assert imported_file.artist == \"Tag Artist\"\n            assert imported_file.album == \"Tag Album\"\n\n    def test_tags_restored(self):\n        with self.configure_plugin({\"auto\": True}):\n            self.run_asis_importer(write=True)\n\n        for item in self.lib.items():\n            imported_file = MediaFile(os.path.join(item.path))\n            assert imported_file.artist == \"Tag Artist\"\n            assert imported_file.album == \"Tag Album\"\n\n    def test_tags_not_restored(self):\n        with self.configure_plugin({\"auto\": True}):\n            self.run_asis_importer(write=False)\n\n        for item in self.lib.items():\n            imported_file = MediaFile(os.path.join(item.path))\n            assert imported_file.artist is None\n            assert imported_file.album is None\n"
  },
  {
    "path": "test/plugins/test_smartplaylist.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Bruno Cauet.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n# TODO: Tests in this fire are very bad. Stop using Mocks in this module.\n\nfrom os import path, remove\nfrom pathlib import Path\nfrom shutil import rmtree\nfrom tempfile import mkdtemp\nfrom unittest.mock import MagicMock, Mock, PropertyMock\n\nimport pytest\n\nfrom beets import config\nfrom beets.dbcore.query import FixedFieldSort, MultipleSort, NullSort\nfrom beets.library import Album, Item, parse_query_string\nfrom beets.test.helper import BeetsTestCase, IOMixin, PluginTestCase\nfrom beets.ui import UserError\nfrom beets.util import CHAR_REPLACE, syspath\nfrom beetsplug.smartplaylist import SmartPlaylistPlugin\n\n\nclass SmartPlaylistTest(BeetsTestCase):\n    def test_build_queries(self):\n        spl = SmartPlaylistPlugin()\n        assert spl._matched_playlists == set()\n        assert spl._unmatched_playlists == set()\n\n        config[\"smartplaylist\"][\"playlists\"].set([])\n        spl.build_queries()\n        assert spl._matched_playlists == set()\n        assert spl._unmatched_playlists == set()\n\n        config[\"smartplaylist\"][\"playlists\"].set(\n            [\n                {\"name\": \"foo\", \"query\": \"FOO foo\"},\n                {\"name\": \"bar\", \"album_query\": [\"BAR bar1\", \"BAR bar2\"]},\n                {\"name\": \"baz\", \"query\": \"BAZ baz\", \"album_query\": \"BAZ baz\"},\n            ]\n        )\n        spl.build_queries()\n        assert spl._matched_playlists == set()\n        foo_foo = parse_query_string(\"FOO foo\", Item)\n        baz_baz = parse_query_string(\"BAZ baz\", Item)\n        baz_baz2 = parse_query_string(\"BAZ baz\", Album)\n        # Multiple queries are now stored as a tuple of (query, sort) tuples\n        bar_queries = tuple(\n            [\n                parse_query_string(\"BAR bar1\", Album),\n                parse_query_string(\"BAR bar2\", Album),\n            ]\n        )\n        assert spl._unmatched_playlists == {\n            (\"foo\", foo_foo, (None, None)),\n            (\"baz\", baz_baz, baz_baz2),\n            (\"bar\", (None, None), (bar_queries, None)),\n        }\n\n    def test_build_queries_with_sorts(self):\n        spl = SmartPlaylistPlugin()\n        config[\"smartplaylist\"][\"playlists\"].set(\n            [\n                {\"name\": \"no_sort\", \"query\": \"foo\"},\n                {\"name\": \"one_sort\", \"query\": \"foo year+\"},\n                {\"name\": \"only_empty_sorts\", \"query\": [\"foo\", \"bar\"]},\n                {\"name\": \"one_non_empty_sort\", \"query\": [\"foo year+\", \"bar\"]},\n                {\n                    \"name\": \"multiple_sorts\",\n                    \"query\": [\"foo year+\", \"bar genres-\"],\n                },\n                {\n                    \"name\": \"mixed\",\n                    \"query\": [\"foo year+\", \"bar\", \"baz genres+ id-\"],\n                },\n            ]\n        )\n\n        spl.build_queries()\n\n        # Multiple queries now return a tuple of (query, sort) tuples, not combined\n        sorts = {}\n        for name, (query_data, sort), _ in spl._unmatched_playlists:\n            if isinstance(query_data, tuple):\n                # Tuple of queries - each has its own sort\n                sorts[name] = [s for _, s in query_data]\n            else:\n                sorts[name] = sort\n\n        sort = FixedFieldSort  # short cut since we're only dealing with this\n        assert sorts[\"no_sort\"] == NullSort()\n        assert sorts[\"one_sort\"] == sort(\"year\")\n        # Multiple queries store individual sorts in the tuple\n        assert all(isinstance(x, NullSort) for x in sorts[\"only_empty_sorts\"])\n        assert sorts[\"one_non_empty_sort\"] == [sort(\"year\"), NullSort()]\n        assert sorts[\"multiple_sorts\"] == [sort(\"year\"), sort(\"genres\", False)]\n        assert sorts[\"mixed\"] == [\n            sort(\"year\"),\n            NullSort(),\n            MultipleSort([sort(\"genres\"), sort(\"id\", False)]),\n        ]\n\n    def test_matches(self):\n        spl = SmartPlaylistPlugin()\n\n        a = MagicMock(Album)\n        i = MagicMock(Item)\n\n        assert not spl.matches(i, None, None)\n        assert not spl.matches(a, None, None)\n\n        query = Mock()\n        query.match.side_effect = {i: True}.__getitem__\n        assert spl.matches(i, query, None)\n        assert not spl.matches(a, query, None)\n\n        a_query = Mock()\n        a_query.match.side_effect = {a: True}.__getitem__\n        assert not spl.matches(i, None, a_query)\n        assert spl.matches(a, None, a_query)\n\n        assert spl.matches(i, query, a_query)\n        assert spl.matches(a, query, a_query)\n\n        # Test with list of queries\n        q1 = Mock()\n        q1.match.return_value = False\n        q2 = Mock()\n        q2.match.side_effect = {i: True}.__getitem__\n        queries_list = [(q1, None), (q2, None)]\n        assert spl.matches(i, queries_list, None)\n        assert not spl.matches(a, queries_list, None)\n\n    def test_db_changes(self):\n        spl = SmartPlaylistPlugin()\n\n        nones = None, None\n        pl1 = \"1\", (\"q1\", None), nones\n        pl2 = \"2\", (\"q2\", None), nones\n        pl3 = \"3\", (\"q3\", None), nones\n\n        spl._unmatched_playlists = {pl1, pl2, pl3}\n        spl._matched_playlists = set()\n\n        spl.matches = Mock(return_value=False)\n        spl.db_change(None, \"nothing\")\n        assert spl._unmatched_playlists == {pl1, pl2, pl3}\n        assert spl._matched_playlists == set()\n\n        spl.matches.side_effect = lambda _, q, __: q == \"q3\"\n        spl.db_change(None, \"matches 3\")\n        assert spl._unmatched_playlists == {pl1, pl2}\n        assert spl._matched_playlists == {pl3}\n\n        spl.matches.side_effect = lambda _, q, __: q == \"q1\"\n        spl.db_change(None, \"matches 3\")\n        assert spl._matched_playlists == {pl1, pl3}\n        assert spl._unmatched_playlists == {pl2}\n\n    def test_playlist_update(self):\n        spl = SmartPlaylistPlugin()\n\n        i = Mock(path=b\"/tagada.mp3\")\n        i.evaluate_template.side_effect = lambda pl, _: pl.replace(\n            b\"$title\", b\"ta:ga:da\"\n        ).decode()\n\n        lib = Mock()\n        lib.replacements = CHAR_REPLACE\n        lib.items.return_value = [i]\n        lib.albums.return_value = []\n\n        q = Mock()\n        a_q = Mock()\n        pl = b\"$title-my<playlist>.m3u\", (q, None), (a_q, None)\n        spl._matched_playlists = {pl}\n\n        dir = mkdtemp()\n        config[\"smartplaylist\"][\"relative_to\"] = False\n        config[\"smartplaylist\"][\"playlist_dir\"] = str(dir)\n        try:\n            spl.update_playlists(lib)\n        except Exception:\n            rmtree(syspath(dir))\n            raise\n\n        lib.items.assert_called_once_with(q, None)\n        lib.albums.assert_called_once_with(a_q, None)\n\n        m3u_filepath = Path(dir, \"ta_ga_da-my_playlist_.m3u\")\n        assert m3u_filepath.exists()\n        content = m3u_filepath.read_bytes()\n        rmtree(syspath(dir))\n\n        assert content == b\"/tagada.mp3\\n\"\n\n    def test_playlist_update_output_extm3u(self):\n        spl = SmartPlaylistPlugin()\n\n        i = MagicMock()\n        type(i).artist = PropertyMock(return_value=\"fake artist\")\n        type(i).title = PropertyMock(return_value=\"fake title\")\n        type(i).length = PropertyMock(return_value=300.123)\n        type(i).path = PropertyMock(return_value=b\"/tagada.mp3\")\n        i.evaluate_template.side_effect = lambda pl, _: pl.replace(\n            b\"$title\",\n            b\"ta:ga:da\",\n        ).decode()\n\n        lib = Mock()\n        lib.replacements = CHAR_REPLACE\n        lib.items.return_value = [i]\n        lib.albums.return_value = []\n\n        q = Mock()\n        a_q = Mock()\n        pl = b\"$title-my<playlist>.m3u\", (q, None), (a_q, None)\n        spl._matched_playlists = {pl}\n\n        dir = mkdtemp()\n        config[\"smartplaylist\"][\"output\"] = \"extm3u\"\n        config[\"smartplaylist\"][\"prefix\"] = \"http://beets:8337/files\"\n        config[\"smartplaylist\"][\"relative_to\"] = False\n        config[\"smartplaylist\"][\"playlist_dir\"] = str(dir)\n        try:\n            spl.update_playlists(lib)\n        except Exception:\n            rmtree(syspath(dir))\n            raise\n\n        lib.items.assert_called_once_with(q, None)\n        lib.albums.assert_called_once_with(a_q, None)\n\n        m3u_filepath = Path(dir, \"ta_ga_da-my_playlist_.m3u\")\n        assert m3u_filepath.exists()\n        content = m3u_filepath.read_bytes()\n        rmtree(syspath(dir))\n\n        assert content == (\n            b\"#EXTM3U\\n\"\n            b\"#EXTINF:300,fake artist - fake title\\n\"\n            b\"http://beets:8337/files/tagada.mp3\\n\"\n        )\n\n    def test_playlist_update_output_extm3u_fields(self):\n        spl = SmartPlaylistPlugin()\n\n        i = MagicMock()\n        type(i).artist = PropertyMock(return_value=\"Fake Artist\")\n        type(i).title = PropertyMock(return_value=\"fake Title\")\n        type(i).length = PropertyMock(return_value=300.123)\n        type(i).path = PropertyMock(return_value=b\"/tagada.mp3\")\n        a = {\"id\": 456, \"genres\": [\"Rock\", \"Pop\"]}\n        i.__getitem__.side_effect = a.__getitem__\n        i.evaluate_template.side_effect = lambda pl, _: pl.replace(\n            b\"$title\",\n            b\"ta:ga:da\",\n        ).decode()\n\n        lib = Mock()\n        lib.replacements = CHAR_REPLACE\n        lib.items.return_value = [i]\n        lib.albums.return_value = []\n\n        q = Mock()\n        a_q = Mock()\n        pl = b\"$title-my<playlist>.m3u\", (q, None), (a_q, None)\n        spl._matched_playlists = {pl}\n\n        dir = mkdtemp()\n        config[\"smartplaylist\"][\"output\"] = \"extm3u\"\n        config[\"smartplaylist\"][\"relative_to\"] = False\n        config[\"smartplaylist\"][\"playlist_dir\"] = str(dir)\n        config[\"smartplaylist\"][\"fields\"] = [\"id\", \"genres\"]\n        try:\n            spl.update_playlists(lib)\n        except Exception:\n            rmtree(syspath(dir))\n            raise\n\n        lib.items.assert_called_once_with(q, None)\n        lib.albums.assert_called_once_with(a_q, None)\n\n        m3u_filepath = Path(dir, \"ta_ga_da-my_playlist_.m3u\")\n        assert m3u_filepath.exists()\n        content = m3u_filepath.read_bytes()\n        rmtree(syspath(dir))\n\n        assert content == (\n            b\"#EXTM3U\\n\"\n            b'#EXTINF:300 id=\"456\" genres=\"Rock%3B%20Pop\",Fake Artist - fake Title\\n'\n            b\"/tagada.mp3\\n\"\n        )\n\n    def test_playlist_update_uri_format(self):\n        spl = SmartPlaylistPlugin()\n\n        i = MagicMock()\n        type(i).id = PropertyMock(return_value=3)\n        type(i).path = PropertyMock(return_value=b\"/tagada.mp3\")\n        i.evaluate_template.side_effect = lambda pl, _: pl.replace(\n            b\"$title\", b\"ta:ga:da\"\n        ).decode()\n\n        lib = Mock()\n        lib.replacements = CHAR_REPLACE\n        lib.items.return_value = [i]\n        lib.albums.return_value = []\n\n        q = Mock()\n        a_q = Mock()\n        pl = b\"$title-my<playlist>.m3u\", (q, None), (a_q, None)\n        spl._matched_playlists = {pl}\n\n        dir = mkdtemp()\n        tpl = \"http://beets:8337/item/$id/file\"\n        config[\"smartplaylist\"][\"uri_format\"] = tpl\n        config[\"smartplaylist\"][\"playlist_dir\"] = dir\n        # The following options should be ignored when uri_format is set\n        config[\"smartplaylist\"][\"relative_to\"] = \"/data\"\n        config[\"smartplaylist\"][\"prefix\"] = \"/prefix\"\n        config[\"smartplaylist\"][\"urlencode\"] = True\n        try:\n            spl.update_playlists(lib)\n        except Exception:\n            rmtree(syspath(dir))\n            raise\n\n        lib.items.assert_called_once_with(q, None)\n        lib.albums.assert_called_once_with(a_q, None)\n\n        m3u_filepath = Path(dir, \"ta_ga_da-my_playlist_.m3u\")\n        assert m3u_filepath.exists()\n        content = m3u_filepath.read_bytes()\n        rmtree(syspath(dir))\n\n        assert content == b\"http://beets:8337/item/3/file\\n\"\n\n    def test_playlist_update_multiple_queries_preserve_order(self):\n        \"\"\"Test that multiple queries preserve their order in the playlist.\"\"\"\n        spl = SmartPlaylistPlugin()\n\n        # Create three mock items\n        i1 = Mock(path=b\"/item1.mp3\", id=1)\n        i1.evaluate_template.return_value = \"ordered.m3u\"\n        i2 = Mock(path=b\"/item2.mp3\", id=2)\n        i2.evaluate_template.return_value = \"ordered.m3u\"\n        i3 = Mock(path=b\"/item3.mp3\", id=3)\n        i3.evaluate_template.return_value = \"ordered.m3u\"\n\n        lib = Mock()\n        lib.replacements = CHAR_REPLACE\n        lib.albums.return_value = []\n\n        # Set up lib.items to return different items for different queries\n        q1 = Mock()\n        q2 = Mock()\n        q3 = Mock()\n\n        def items_side_effect(query, sort):\n            if query == q1:\n                return [i1]\n            elif query == q2:\n                return [i2]\n            elif query == q3:\n                return [i3]\n            return []\n\n        lib.items.side_effect = items_side_effect\n\n        # Create playlist with multiple queries (stored as tuple)\n        queries_and_sorts = ((q1, None), (q2, None), (q3, None))\n        pl = \"ordered.m3u\", (queries_and_sorts, None), (None, None)\n        spl._matched_playlists = {pl}\n\n        dir = mkdtemp()\n        config[\"smartplaylist\"][\"relative_to\"] = False\n        config[\"smartplaylist\"][\"playlist_dir\"] = str(dir)\n        try:\n            spl.update_playlists(lib)\n        except Exception:\n            rmtree(syspath(dir))\n            raise\n\n        # Verify that lib.items was called with queries in the correct order\n        assert lib.items.call_count == 3\n        lib.items.assert_any_call(q1, None)\n        lib.items.assert_any_call(q2, None)\n        lib.items.assert_any_call(q3, None)\n\n        m3u_filepath = Path(dir, \"ordered.m3u\")\n        assert m3u_filepath.exists()\n        content = m3u_filepath.read_bytes()\n        rmtree(syspath(dir))\n\n        # Items should be in order: i1, i2, i3\n        assert content == b\"/item1.mp3\\n/item2.mp3\\n/item3.mp3\\n\"\n\n    def test_playlist_update_multiple_queries_no_duplicates(self):\n        \"\"\"Test that items matching multiple queries only appear once.\"\"\"\n        spl = SmartPlaylistPlugin()\n\n        # Create two mock items\n        i1 = Mock(path=b\"/item1.mp3\", id=1)\n        i1.evaluate_template.return_value = \"dedup.m3u\"\n        i2 = Mock(path=b\"/item2.mp3\", id=2)\n        i2.evaluate_template.return_value = \"dedup.m3u\"\n\n        lib = Mock()\n        lib.replacements = CHAR_REPLACE\n        lib.albums.return_value = []\n\n        # Set up lib.items so both queries return overlapping items\n        q1 = Mock()\n        q2 = Mock()\n\n        def items_side_effect(query, sort):\n            if query == q1:\n                return [i1, i2]  # Both items match q1\n            elif query == q2:\n                return [i2]  # Only i2 matches q2\n            return []\n\n        lib.items.side_effect = items_side_effect\n\n        # Create playlist with multiple queries (stored as tuple)\n        queries_and_sorts = ((q1, None), (q2, None))\n        pl = \"dedup.m3u\", (queries_and_sorts, None), (None, None)\n        spl._matched_playlists = {pl}\n\n        dir = mkdtemp()\n        config[\"smartplaylist\"][\"relative_to\"] = False\n        config[\"smartplaylist\"][\"playlist_dir\"] = str(dir)\n        try:\n            spl.update_playlists(lib)\n        except Exception:\n            rmtree(syspath(dir))\n            raise\n\n        m3u_filepath = Path(dir, \"dedup.m3u\")\n        assert m3u_filepath.exists()\n        content = m3u_filepath.read_bytes()\n        rmtree(syspath(dir))\n\n        # i2 should only appear once even though it matches both queries\n        # Order should be: i1 (from q1), i2 (from q1, skipped in q2)\n        assert content == b\"/item1.mp3\\n/item2.mp3\\n\"\n        # Verify i2 is not duplicated\n        assert content.count(b\"/item2.mp3\") == 1\n\n    def test_playlist_update_dest_regen(self):\n        spl = SmartPlaylistPlugin()\n\n        i = MagicMock()\n        type(i).artist = PropertyMock(return_value=\"fake artist\")\n        type(i).title = PropertyMock(return_value=\"fake title\")\n        type(i).length = PropertyMock(return_value=300.123)\n        # Set a path which is not equal to the one returned by `item.destination`.\n        type(i).path = PropertyMock(\n            return_value=b\"/imported/path/with/dont/move/tagada.mp3\"\n        )\n        # Set a path which would be equal to the one returned by `item.destination`.\n        type(i).destination = PropertyMock(return_value=lambda: b\"/tagada.mp3\")\n        i.evaluate_template.side_effect = lambda pl, _: pl.replace(\n            b\"$title\",\n            b\"ta:ga:da\",\n        ).decode()\n\n        lib = Mock()\n        lib.replacements = CHAR_REPLACE\n        lib.items.return_value = [i]\n        lib.albums.return_value = []\n\n        q = Mock()\n        a_q = Mock()\n        pl = b\"$title-my<playlist>.m3u\", (q, None), (a_q, None)\n        spl._matched_playlists = {pl}\n\n        dir = mkdtemp()\n        config[\"smartplaylist\"][\"output\"] = \"extm3u\"\n        config[\"smartplaylist\"][\"prefix\"] = \"http://beets:8337/files\"\n        config[\"smartplaylist\"][\"relative_to\"] = False\n        config[\"smartplaylist\"][\"playlist_dir\"] = str(dir)\n\n        # Test when `dest_regen` is set to True:\n        # Intended behavior is to use the path of `i.destination`.\n\n        config[\"smartplaylist\"][\"dest_regen\"] = True\n        try:\n            spl.update_playlists(lib)\n        except Exception:\n            rmtree(syspath(dir))\n            raise\n\n        lib.items.assert_called_once_with(q, None)\n        lib.albums.assert_called_once_with(a_q, None)\n\n        m3u_filepath = Path(dir, \"ta_ga_da-my_playlist_.m3u\")\n        assert m3u_filepath.exists()\n        with open(syspath(m3u_filepath), \"rb\") as f:\n            content = f.read()\n        rmtree(syspath(dir))\n\n        assert content == (\n            b\"#EXTM3U\\n\"\n            b\"#EXTINF:300,fake artist - fake title\\n\"\n            b\"http://beets:8337/files/tagada.mp3\\n\"\n        )\n\n        # Test when `dest_regen` is set to False:\n        # Intended behavior is to use the path of `i.path`.\n\n        config[\"smartplaylist\"][\"dest_regen\"] = False\n\n        try:\n            spl.update_playlists(lib)\n        except Exception:\n            rmtree(syspath(dir))\n            raise\n\n        m3u_filepath = Path(dir, \"ta_ga_da-my_playlist_.m3u\")\n        assert m3u_filepath.exists()\n        with open(syspath(m3u_filepath), \"rb\") as f:\n            content = f.read()\n        rmtree(syspath(dir))\n\n        assert content == (\n            b\"#EXTM3U\\n\"\n            b\"#EXTINF:300,fake artist - fake title\\n\"\n            b\"http://beets:8337/files/imported/path/with/dont/move/tagada.mp3\\n\"\n        )\n\n\nclass SmartPlaylistCLITest(IOMixin, PluginTestCase):\n    plugin = \"smartplaylist\"\n\n    def setUp(self):\n        super().setUp()\n\n        self.item = self.add_item()\n        config[\"smartplaylist\"][\"playlists\"].set(\n            [\n                {\"name\": \"my_playlist.m3u\", \"query\": self.item.title},\n                {\"name\": \"all.m3u\", \"query\": \"\"},\n            ]\n        )\n        config[\"smartplaylist\"][\"playlist_dir\"].set(str(self.temp_dir_path))\n\n    def test_splupdate(self):\n        with pytest.raises(UserError):\n            self.run_with_output(\"splupdate\", \"tagada\")\n\n        self.run_with_output(\"splupdate\", \"my_playlist\")\n        m3u_path = self.temp_dir_path / \"my_playlist.m3u\"\n        assert m3u_path.exists()\n        assert m3u_path.read_bytes() == self.item.path + b\"\\n\"\n        remove(syspath(m3u_path))\n\n        self.run_with_output(\"splupdate\", \"my_playlist.m3u\")\n        assert m3u_path.read_bytes() == self.item.path + b\"\\n\"\n        remove(syspath(m3u_path))\n\n        self.run_with_output(\"splupdate\")\n        for name in (b\"my_playlist.m3u\", b\"all.m3u\"):\n            with open(path.join(self.temp_dir, name), \"rb\") as f:\n                assert f.read() == self.item.path + b\"\\n\"\n"
  },
  {
    "path": "test/plugins/test_spotify.py",
    "content": "\"\"\"Tests for the 'spotify' plugin\"\"\"\n\nimport os\nfrom urllib.parse import parse_qs, urlparse\n\nimport responses\n\nfrom beets.library import Item\nfrom beets.test import _common\nfrom beets.test.helper import PluginTestCase\nfrom beetsplug import spotify\n\n\nclass ArgumentsMock:\n    def __init__(self, mode, show_failures):\n        self.mode = mode\n        self.show_failures = show_failures\n        self.verbose = 1\n\n\ndef _params(url):\n    \"\"\"Get the query parameters from a URL.\"\"\"\n    return parse_qs(urlparse(url).query)\n\n\nclass SpotifyPluginTest(PluginTestCase):\n    plugin = \"spotify\"\n\n    @responses.activate\n    def setUp(self):\n        responses.add(\n            responses.POST,\n            spotify.SpotifyPlugin.oauth_token_url,\n            status=200,\n            json={\n                \"access_token\": \"3XyiC3raJySbIAV5LVYj1DaWbcocNi3LAJTNXRnYY\"\n                \"GVUl6mbbqXNhW3YcZnQgYXNWHFkVGSMlc0tMuvq8CF\",\n                \"token_type\": \"Bearer\",\n                \"expires_in\": 3600,\n                \"scope\": \"\",\n            },\n        )\n        super().setUp()\n        self.spotify = spotify.SpotifyPlugin()\n        opts = ArgumentsMock(\"list\", False)\n        self.spotify._parse_opts(opts)\n\n    def test_args(self):\n        opts = ArgumentsMock(\"fail\", True)\n        assert not self.spotify._parse_opts(opts)\n        opts = ArgumentsMock(\"list\", False)\n        assert self.spotify._parse_opts(opts)\n\n    def test_empty_query(self):\n        assert self.spotify._match_library_tracks(self.lib, \"1=2\") is None\n\n    @responses.activate\n    def test_missing_request(self):\n        json_file = os.path.join(\n            _common.RSRC, b\"spotify\", b\"missing_request.json\"\n        )\n        with open(json_file, \"rb\") as f:\n            response_body = f.read()\n\n        responses.add(\n            responses.GET,\n            spotify.SpotifyPlugin.search_url,\n            body=response_body,\n            status=200,\n            content_type=\"application/json\",\n        )\n        item = Item(\n            mb_trackid=\"01234\",\n            album=\"lkajsdflakjsd\",\n            albumartist=\"ujydfsuihse\",\n            title=\"duifhjslkef\",\n            length=10,\n        )\n        item.add(self.lib)\n        assert [] == self.spotify._match_library_tracks(self.lib, \"\")\n\n        params = _params(responses.calls[0].request.url)\n        query = params[\"q\"][0]\n        assert \"duifhjslkef\" in query\n        assert \"artist:'ujydfsuihse'\" in query\n        assert \"album:'lkajsdflakjsd'\" in query\n        assert params[\"type\"] == [\"track\"]\n\n    @responses.activate\n    def test_track_request(self):\n        json_file = os.path.join(\n            _common.RSRC, b\"spotify\", b\"track_request.json\"\n        )\n        with open(json_file, \"rb\") as f:\n            response_body = f.read()\n\n        responses.add(\n            responses.GET,\n            spotify.SpotifyPlugin.search_url,\n            body=response_body,\n            status=200,\n            content_type=\"application/json\",\n        )\n        item = Item(\n            mb_trackid=\"01234\",\n            album=\"Despicable Me 2\",\n            albumartist=\"Pharrell Williams\",\n            title=\"Happy\",\n            length=10,\n        )\n        item.add(self.lib)\n        results = self.spotify._match_library_tracks(self.lib, \"Happy\")\n        assert 1 == len(results)\n        assert \"6NPVjNh8Jhru9xOmyQigds\" == results[0][\"id\"]\n        self.spotify._output_match_results(results)\n\n        params = _params(responses.calls[0].request.url)\n        query = params[\"q\"][0]\n        assert \"Happy\" in query\n        assert \"artist:'Pharrell Williams'\" in query\n        assert \"album:'Despicable Me 2'\" in query\n        assert params[\"type\"] == [\"track\"]\n\n    @responses.activate\n    def test_track_for_id(self):\n        \"\"\"Tests if plugin is able to fetch a track by its Spotify ID\"\"\"\n\n        # Mock the Spotify 'Get Track' call\n        json_file = os.path.join(_common.RSRC, b\"spotify\", b\"track_info.json\")\n        with open(json_file, \"rb\") as f:\n            response_body = f.read()\n\n        responses.add(\n            responses.GET,\n            f\"{spotify.SpotifyPlugin.track_url}6NPVjNh8Jhru9xOmyQigds\",\n            body=response_body,\n            status=200,\n            content_type=\"application/json\",\n        )\n\n        # Mock the Spotify 'Get Album' call\n        json_file = os.path.join(_common.RSRC, b\"spotify\", b\"album_info.json\")\n        with open(json_file, \"rb\") as f:\n            response_body = f.read()\n\n        responses.add(\n            responses.GET,\n            f\"{spotify.SpotifyPlugin.album_url}5l3zEmMrOhOzG8d8s83GOL\",\n            body=response_body,\n            status=200,\n            content_type=\"application/json\",\n        )\n\n        # Mock the Spotify 'Search' call\n        json_file = os.path.join(\n            _common.RSRC, b\"spotify\", b\"track_request.json\"\n        )\n        with open(json_file, \"rb\") as f:\n            response_body = f.read()\n\n        responses.add(\n            responses.GET,\n            spotify.SpotifyPlugin.search_url,\n            body=response_body,\n            status=200,\n            content_type=\"application/json\",\n        )\n\n        track_info = self.spotify.track_for_id(\"6NPVjNh8Jhru9xOmyQigds\")\n        item = Item(\n            mb_trackid=track_info.track_id,\n            albumartist=track_info.artist,\n            title=track_info.title,\n            length=track_info.length,\n        )\n        item.add(self.lib)\n\n        results = self.spotify._match_library_tracks(self.lib, \"Happy\")\n        assert 1 == len(results)\n        assert \"6NPVjNh8Jhru9xOmyQigds\" == results[0][\"id\"]\n\n    @responses.activate\n    def test_japanese_track(self):\n        \"\"\"Ensure non-ASCII characters remain unchanged in search queries\"\"\"\n\n        # Path to the mock JSON file for the Japanese track\n        json_file = os.path.join(\n            _common.RSRC, b\"spotify\", b\"japanese_track_request.json\"\n        )\n\n        # Load the mock JSON response\n        with open(json_file, \"rb\") as f:\n            response_body = f.read()\n\n        # Mock Spotify Search API response\n        responses.add(\n            responses.GET,\n            spotify.SpotifyPlugin.search_url,\n            body=response_body,\n            status=200,\n            content_type=\"application/json\",\n        )\n\n        # Create a mock item with Japanese metadata\n        item = Item(\n            mb_trackid=\"56789\",\n            album=\"盗作\",\n            albumartist=\"ヨルシカ\",\n            title=\"思想犯\",\n            length=10,\n        )\n        item.add(self.lib)\n\n        # Search without ascii encoding\n\n        with self.configure_plugin(\n            {\n                \"search_query_ascii\": False,\n            }\n        ):\n            assert self.spotify.config[\"search_query_ascii\"].get() is False\n            # Call the method to match library tracks\n            results = self.spotify._match_library_tracks(self.lib, item.title)\n\n            # Assertions to verify results\n            assert results is not None\n            assert 1 == len(results)\n            assert results[0][\"name\"] == item.title\n            assert results[0][\"artists\"][0][\"name\"] == item.albumartist\n            assert results[0][\"album\"][\"name\"] == item.album\n\n            # Verify search query parameters\n            params = _params(responses.calls[0].request.url)\n            query = params[\"q\"][0]\n            assert item.title in query\n            assert f\"artist:'{item.albumartist}'\" in query\n            assert f\"album:'{item.album}'\" in query\n            assert not query.isascii()\n\n        # Is not found in the library if ascii encoding is enabled\n        with self.configure_plugin(\n            {\n                \"search_query_ascii\": True,\n            }\n        ):\n            assert self.spotify.config[\"search_query_ascii\"].get() is True\n            results = self.spotify._match_library_tracks(self.lib, item.title)\n            params = _params(responses.calls[1].request.url)\n            query = params[\"q\"][0]\n\n            assert query.isascii()\n\n    @responses.activate\n    def test_multiartist_album_and_track(self):\n        \"\"\"Tests if plugin is able to map multiple artists in an album and\n        track info correctly\"\"\"\n\n        # Mock the Spotify 'Get Album' call\n        json_file = os.path.join(\n            _common.RSRC, b\"spotify\", b\"multiartist_album.json\"\n        )\n        with open(json_file, \"rb\") as f:\n            album_response_body = f.read()\n\n        responses.add(\n            responses.GET,\n            f\"{spotify.SpotifyPlugin.album_url}0yhKyyjyKXWUieJ4w1IAEa\",\n            body=album_response_body,\n            status=200,\n            content_type=\"application/json\",\n        )\n\n        # Mock the Spotify 'Get Track' call\n        json_file = os.path.join(\n            _common.RSRC, b\"spotify\", b\"multiartist_track.json\"\n        )\n        with open(json_file, \"rb\") as f:\n            track_response_body = f.read()\n\n        responses.add(\n            responses.GET,\n            f\"{spotify.SpotifyPlugin.track_url}6sjZfVJworBX6TqyjkxIJ1\",\n            body=track_response_body,\n            status=200,\n            content_type=\"application/json\",\n        )\n\n        album_info = self.spotify.album_for_id(\"0yhKyyjyKXWUieJ4w1IAEa\")\n        assert album_info is not None\n        assert album_info.artist == \"Project Skylate, Sugar Shrill\"\n        assert album_info.artists == [\"Project Skylate\", \"Sugar Shrill\"]\n        assert album_info.artist_id == \"6m8MRXIVKb6wQaPlBIDMr1\"\n        assert album_info.artists_ids == [\n            \"6m8MRXIVKb6wQaPlBIDMr1\",\n            \"4kkAIoQmNT5xEoNH5BuQLe\",\n        ]\n\n        assert len(album_info.tracks) == 1\n        assert album_info.tracks[0].artist == \"Foo, Bar\"\n        assert album_info.tracks[0].artists == [\"Foo\", \"Bar\"]\n        assert album_info.tracks[0].artist_id == \"12345\"\n        assert album_info.tracks[0].artists_ids == [\"12345\", \"67890\"]\n\n        track_info = self.spotify.track_for_id(\"6sjZfVJworBX6TqyjkxIJ1\")\n        assert track_info is not None\n        assert track_info.artist == \"Foo, Bar\"\n        assert track_info.artists == [\"Foo\", \"Bar\"]\n        assert track_info.artist_id == \"12345\"\n        assert track_info.artists_ids == [\"12345\", \"67890\"]\n"
  },
  {
    "path": "test/plugins/test_subsonicupdate.py",
    "content": "\"\"\"Tests for the 'subsonic' plugin.\"\"\"\n\nimport unittest\nfrom urllib.parse import parse_qs, urlparse\n\nimport responses\n\nfrom beets import config\nfrom beetsplug import subsonicupdate\n\n\nclass ArgumentsMock:\n    \"\"\"Argument mocks for tests.\"\"\"\n\n    def __init__(self, mode, show_failures):\n        \"\"\"Constructs ArgumentsMock.\"\"\"\n        self.mode = mode\n        self.show_failures = show_failures\n        self.verbose = 1\n\n\ndef _params(url):\n    \"\"\"Get the query parameters from a URL.\"\"\"\n    return parse_qs(urlparse(url).query)\n\n\nclass SubsonicPluginTest(unittest.TestCase):\n    \"\"\"Test class for subsonicupdate.\"\"\"\n\n    @responses.activate\n    def setUp(self):\n        \"\"\"Sets up config and plugin for test.\"\"\"\n        super().setUp()\n\n        config[\"subsonic\"][\"user\"] = \"admin\"\n        config[\"subsonic\"][\"pass\"] = \"admin\"\n        config[\"subsonic\"][\"url\"] = \"http://localhost:4040\"\n        responses.add(\n            responses.GET,\n            \"http://localhost:4040/rest/ping.view\",\n            status=200,\n            body=self.PING_BODY,\n        )\n        self.subsonicupdate = subsonicupdate.SubsonicUpdate()\n\n    PING_BODY = \"\"\"\n{\n    \"subsonic-response\": {\n        \"status\": \"failed\",\n        \"version\": \"1.15.0\"\n    }\n}\n\"\"\"\n    SUCCESS_BODY = \"\"\"\n{\n    \"subsonic-response\": {\n        \"status\": \"ok\",\n        \"version\": \"1.15.0\",\n        \"scanStatus\": {\n            \"scanning\": true,\n            \"count\": 1000\n        }\n    }\n}\n\"\"\"\n\n    FAILED_BODY = \"\"\"\n{\n    \"subsonic-response\": {\n        \"status\": \"failed\",\n        \"version\": \"1.15.0\",\n        \"error\": {\n            \"code\": 40,\n            \"message\": \"Wrong username or password.\"\n        }\n    }\n}\n\"\"\"\n\n    ERROR_BODY = \"\"\"\n{\n    \"timestamp\": 1599185854498,\n    \"status\": 404,\n    \"error\": \"Not Found\",\n    \"message\": \"No message available\",\n    \"path\": \"/rest/startScn\"\n}\n\"\"\"\n\n    @responses.activate\n    def test_start_scan(self):\n        \"\"\"Tests success path based on best case scenario.\"\"\"\n        responses.add(\n            responses.GET,\n            \"http://localhost:4040/rest/startScan\",\n            status=200,\n            body=self.SUCCESS_BODY,\n        )\n\n        self.subsonicupdate.start_scan()\n\n    @responses.activate\n    def test_start_scan_failed_bad_credentials(self):\n        \"\"\"Tests failed path based on bad credentials.\"\"\"\n        responses.add(\n            responses.GET,\n            \"http://localhost:4040/rest/startScan\",\n            status=200,\n            body=self.FAILED_BODY,\n        )\n\n        self.subsonicupdate.start_scan()\n\n    @responses.activate\n    def test_start_scan_failed_not_found(self):\n        \"\"\"Tests failed path based on resource not found.\"\"\"\n        responses.add(\n            responses.GET,\n            \"http://localhost:4040/rest/startScan\",\n            status=404,\n            body=self.ERROR_BODY,\n        )\n\n        self.subsonicupdate.start_scan()\n\n    def test_start_scan_failed_unreachable(self):\n        \"\"\"Tests failed path based on service not available.\"\"\"\n        self.subsonicupdate.start_scan()\n\n    @responses.activate\n    def test_url_with_context_path(self):\n        \"\"\"Tests success for included with contextPath.\"\"\"\n        config[\"subsonic\"][\"url\"] = \"http://localhost:4040/contextPath/\"\n\n        responses.add(\n            responses.GET,\n            \"http://localhost:4040/contextPath/rest/startScan\",\n            status=200,\n            body=self.SUCCESS_BODY,\n        )\n\n        self.subsonicupdate.start_scan()\n\n    @responses.activate\n    def test_url_with_trailing_forward_slash_url(self):\n        \"\"\"Tests success path based on trailing forward slash.\"\"\"\n        config[\"subsonic\"][\"url\"] = \"http://localhost:4040/\"\n\n        responses.add(\n            responses.GET,\n            \"http://localhost:4040/rest/startScan\",\n            status=200,\n            body=self.SUCCESS_BODY,\n        )\n\n        self.subsonicupdate.start_scan()\n\n    @responses.activate\n    def test_url_with_missing_port(self):\n        \"\"\"Tests failed path based on missing port.\"\"\"\n        config[\"subsonic\"][\"url\"] = \"http://localhost/airsonic\"\n\n        responses.add(\n            responses.GET,\n            \"http://localhost/airsonic/rest/startScan\",\n            status=200,\n            body=self.SUCCESS_BODY,\n        )\n\n        self.subsonicupdate.start_scan()\n\n    @responses.activate\n    def test_url_with_missing_schema(self):\n        \"\"\"Tests failed path based on missing schema.\"\"\"\n        config[\"subsonic\"][\"url\"] = \"localhost:4040/airsonic\"\n\n        responses.add(\n            responses.GET,\n            \"http://localhost:4040/rest/startScan\",\n            status=200,\n            body=self.SUCCESS_BODY,\n        )\n\n        self.subsonicupdate.start_scan()\n"
  },
  {
    "path": "test/plugins/test_substitute.py",
    "content": "# This file is part of beets.\n# Copyright 2024, Nicholas Boyd Isacsson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Test the substitute plugin regex functionality.\"\"\"\n\nfrom beets.test.helper import PluginTestCase\nfrom beetsplug.substitute import Substitute\n\n\nclass SubstitutePluginTest(PluginTestCase):\n    plugin = \"substitute\"\n    preload_plugin = False\n\n    def run_substitute(self, config, cases):\n        with self.configure_plugin(config):\n            for input, expected in cases:\n                assert Substitute().tmpl_substitute(input) == expected\n\n    def test_simple_substitute(self):\n        self.run_substitute(\n            {\n                \"a\": \"x\",\n                \"b\": \"y\",\n                \"c\": \"z\",\n            },\n            [(\"a\", \"x\"), (\"b\", \"y\"), (\"c\", \"z\")],\n        )\n\n    def test_case_insensitivity(self):\n        self.run_substitute({\"a\": \"x\"}, [(\"A\", \"x\")])\n\n    def test_unmatched_input_preserved(self):\n        self.run_substitute({\"a\": \"x\"}, [(\"c\", \"c\")])\n\n    def test_regex_to_static(self):\n        self.run_substitute(\n            {\".*jimi hendrix.*\": \"Jimi Hendrix\"},\n            [(\"The Jimi Hendrix Experience\", \"Jimi Hendrix\")],\n        )\n\n    def test_regex_capture_group(self):\n        self.run_substitute(\n            {\"^(.*?)(,| &| and).*\": r\"\\1\"},\n            [\n                (\"King Creosote & Jon Hopkins\", \"King Creosote\"),\n                (\n                    (\n                        \"Michael Hurley, The Holy Modal Rounders, Jeffrey\"\n                        \" Frederick & The Clamtones\"\n                    ),\n                    \"Michael Hurley\",\n                ),\n                (\"James Yorkston and the Athletes\", \"James Yorkston\"),\n            ],\n        )\n\n    def test_partial_substitution(self):\n        self.run_substitute({r\"\\.\": \"\"}, [(\"U.N.P.O.C.\", \"UNPOC\")])\n\n    def test_rules_applied_in_definition_order(self):\n        self.run_substitute(\n            {\n                \"a\": \"x\",\n                \"[ab]\": \"y\",\n                \"b\": \"z\",\n            },\n            [\n                (\"a\", \"x\"),\n                (\"b\", \"y\"),\n            ],\n        )\n\n    def test_rules_applied_in_sequence(self):\n        self.run_substitute(\n            {\"a\": \"b\", \"b\": \"c\", \"d\": \"a\"},\n            [\n                (\"a\", \"c\"),\n                (\"b\", \"c\"),\n                (\"d\", \"a\"),\n            ],\n        )\n"
  },
  {
    "path": "test/plugins/test_the.py",
    "content": "\"\"\"Tests for the 'the' plugin\"\"\"\n\nimport unittest\n\nfrom beets import config\nfrom beetsplug.the import FORMAT, PATTERN_A, PATTERN_THE, ThePlugin\n\n\nclass ThePluginTest(unittest.TestCase):\n    def test_unthe_with_default_patterns(self):\n        assert ThePlugin().unthe(\"\", PATTERN_THE) == \"\"\n        assert (\n            ThePlugin().unthe(\"The Something\", PATTERN_THE) == \"Something, The\"\n        )\n        assert ThePlugin().unthe(\"The The\", PATTERN_THE) == \"The, The\"\n        assert ThePlugin().unthe(\"The    The\", PATTERN_THE) == \"The, The\"\n        assert ThePlugin().unthe(\"The   The   X\", PATTERN_THE) == \"The   X, The\"\n        assert ThePlugin().unthe(\"the The\", PATTERN_THE) == \"The, the\"\n        assert (\n            ThePlugin().unthe(\"Protected The\", PATTERN_THE) == \"Protected The\"\n        )\n        assert ThePlugin().unthe(\"A Boy\", PATTERN_A) == \"Boy, A\"\n        assert ThePlugin().unthe(\"a girl\", PATTERN_A) == \"girl, a\"\n        assert ThePlugin().unthe(\"An Apple\", PATTERN_A) == \"Apple, An\"\n        assert ThePlugin().unthe(\"An A Thing\", PATTERN_A) == \"A Thing, An\"\n        assert ThePlugin().unthe(\"the An Arse\", PATTERN_A) == \"the An Arse\"\n        assert (\n            ThePlugin().unthe(\"TET - Travailleur\", PATTERN_THE)\n            == \"TET - Travailleur\"\n        )\n\n    def test_unthe_with_strip(self):\n        config[\"the\"][\"strip\"] = True\n        assert ThePlugin().unthe(\"The Something\", PATTERN_THE) == \"Something\"\n        assert ThePlugin().unthe(\"An A\", PATTERN_A) == \"A\"\n\n    def test_template_function_with_defaults(self):\n        ThePlugin().patterns = [PATTERN_THE, PATTERN_A]\n        assert ThePlugin().the_template_func(\"The The\") == \"The, The\"\n        assert ThePlugin().the_template_func(\"An A\") == \"A, An\"\n\n    def test_custom_pattern(self):\n        config[\"the\"][\"patterns\"] = [\"^test\\\\s\"]\n        config[\"the\"][\"format\"] = FORMAT\n        assert ThePlugin().the_template_func(\"test passed\") == \"passed, test\"\n\n    def test_custom_format(self):\n        config[\"the\"][\"patterns\"] = [PATTERN_THE, PATTERN_A]\n        config[\"the\"][\"format\"] = \"{1} ({0})\"\n        assert ThePlugin().the_template_func(\"The A\") == \"The (A)\"\n"
  },
  {
    "path": "test/plugins/test_thumbnails.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Bruno Cauet\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nimport os.path\nfrom shutil import rmtree\nfrom tempfile import mkdtemp\nfrom unittest.mock import Mock, call, patch\n\nimport pytest\n\nfrom beets.test.helper import BeetsTestCase\nfrom beets.util import bytestring_path, syspath\nfrom beetsplug.thumbnails import (\n    LARGE_DIR,\n    NORMAL_DIR,\n    GioURI,\n    PathlibURI,\n    ThumbnailsPlugin,\n)\n\n\nclass ThumbnailsTest(BeetsTestCase):\n    @patch(\"beetsplug.thumbnails.ArtResizer\")\n    @patch(\"beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok\", Mock())\n    @patch(\"beetsplug.thumbnails.os.stat\")\n    def test_add_tags(self, mock_stat, mock_artresizer):\n        plugin = ThumbnailsPlugin()\n        plugin.get_uri = Mock(\n            side_effect={b\"/path/to/cover\": \"COVER_URI\"}.__getitem__\n        )\n        album = Mock(artpath=b\"/path/to/cover\")\n        mock_stat.return_value.st_mtime = 12345\n\n        plugin.add_tags(album, b\"/path/to/thumbnail\")\n\n        metadata = {\"Thumb::URI\": \"COVER_URI\", \"Thumb::MTime\": \"12345\"}\n        mock_artresizer.shared.write_metadata.assert_called_once_with(\n            b\"/path/to/thumbnail\",\n            metadata,\n        )\n        mock_stat.assert_called_once_with(syspath(album.artpath))\n\n    @patch(\"beetsplug.thumbnails.os\")\n    @patch(\"beetsplug.thumbnails.ArtResizer\")\n    @patch(\"beetsplug.thumbnails.GioURI\")\n    def test_check_local_ok(self, mock_giouri, mock_artresizer, mock_os):\n        # test local resizing capability\n        mock_artresizer.shared.local = False\n        mock_artresizer.shared.can_write_metadata = False\n        plugin = ThumbnailsPlugin()\n        assert not plugin._check_local_ok()\n\n        # test dirs creation\n        mock_artresizer.shared.local = True\n        mock_artresizer.shared.can_write_metadata = True\n\n        def exists(path):\n            if path == syspath(NORMAL_DIR):\n                return False\n            if path == syspath(LARGE_DIR):\n                return True\n            raise ValueError(f\"unexpected path {path!r}\")\n\n        mock_os.path.exists = exists\n        plugin = ThumbnailsPlugin()\n        mock_os.makedirs.assert_called_once_with(syspath(NORMAL_DIR))\n        assert plugin._check_local_ok()\n\n        # test metadata writer function\n        mock_os.path.exists = lambda _: True\n\n        mock_artresizer.shared.local = True\n        mock_artresizer.shared.can_write_metadata = False\n        with pytest.raises(RuntimeError):\n            ThumbnailsPlugin()\n\n        mock_artresizer.shared.local = True\n        mock_artresizer.shared.can_write_metadata = True\n        assert ThumbnailsPlugin()._check_local_ok()\n\n        # test URI getter function\n        giouri_inst = mock_giouri.return_value\n        giouri_inst.available = True\n        assert ThumbnailsPlugin().get_uri == giouri_inst.uri\n\n        giouri_inst.available = False\n        assert ThumbnailsPlugin().get_uri.__self__.__class__ == PathlibURI\n\n    @patch(\"beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok\", Mock())\n    @patch(\"beetsplug.thumbnails.ArtResizer\")\n    @patch(\"beets.util.syspath\", Mock(side_effect=lambda x: x))\n    @patch(\"beetsplug.thumbnails.os\")\n    @patch(\"beetsplug.thumbnails.shutil\")\n    def test_make_cover_thumbnail(self, mock_shutils, mock_os, mock_artresizer):\n        thumbnail_dir = os.path.normpath(b\"/thumbnail/dir\")\n        md5_file = os.path.join(thumbnail_dir, b\"md5\")\n        path_to_art = os.path.normpath(b\"/path/to/art\")\n        path_to_resized_art = os.path.normpath(b\"/path/to/resized/artwork\")\n\n        mock_os.path.join = os.path.join  # don't mock that function\n        plugin = ThumbnailsPlugin()\n        plugin.add_tags = Mock()\n\n        album = Mock(artpath=path_to_art)\n        plugin.thumbnail_file_name = Mock(return_value=b\"md5\")\n        mock_os.path.exists.return_value = False\n\n        def os_stat(target):\n            if target == syspath(md5_file):\n                return Mock(st_mtime=1)\n            elif target == syspath(path_to_art):\n                return Mock(st_mtime=2)\n            else:\n                raise ValueError(f\"invalid target {target}\")\n\n        mock_os.stat.side_effect = os_stat\n\n        mock_resize = mock_artresizer.shared.resize\n        mock_resize.return_value = path_to_resized_art\n\n        plugin.make_cover_thumbnail(album, 12345, thumbnail_dir)\n\n        mock_os.path.exists.assert_called_once_with(syspath(md5_file))\n\n        mock_resize.assert_called_once_with(12345, path_to_art, md5_file)\n        plugin.add_tags.assert_called_once_with(album, path_to_resized_art)\n        mock_shutils.move.assert_called_once_with(\n            syspath(path_to_resized_art), syspath(md5_file)\n        )\n\n        # now test with recent thumbnail & with force\n        mock_os.path.exists.return_value = True\n        plugin.force = False\n        mock_resize.reset_mock()\n\n        def os_stat(target):\n            if target == syspath(md5_file):\n                return Mock(st_mtime=3)\n            elif target == syspath(path_to_art):\n                return Mock(st_mtime=2)\n            else:\n                raise ValueError(f\"invalid target {target}\")\n\n        mock_os.stat.side_effect = os_stat\n\n        plugin.make_cover_thumbnail(album, 12345, thumbnail_dir)\n        assert mock_resize.call_count == 0\n\n        # and with force\n        plugin.config[\"force\"] = True\n        plugin.make_cover_thumbnail(album, 12345, thumbnail_dir)\n        mock_resize.assert_called_once_with(12345, path_to_art, md5_file)\n\n    @patch(\"beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok\", Mock())\n    def test_make_dolphin_cover_thumbnail(self):\n        plugin = ThumbnailsPlugin()\n        tmp = bytestring_path(mkdtemp())\n        album = Mock(path=tmp, artpath=os.path.join(tmp, b\"cover.jpg\"))\n        plugin.make_dolphin_cover_thumbnail(album)\n        with open(os.path.join(tmp, b\".directory\"), \"rb\") as f:\n            assert f.read().splitlines() == [\n                b\"[Desktop Entry]\",\n                b\"Icon=./cover.jpg\",\n            ]\n\n        # not rewritten when it already exists (yup that's a big limitation)\n        album.artpath = b\"/my/awesome/art.tiff\"\n        plugin.make_dolphin_cover_thumbnail(album)\n        with open(os.path.join(tmp, b\".directory\"), \"rb\") as f:\n            assert f.read().splitlines() == [\n                b\"[Desktop Entry]\",\n                b\"Icon=./cover.jpg\",\n            ]\n\n        rmtree(syspath(tmp))\n\n    @patch(\"beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok\", Mock())\n    @patch(\"beetsplug.thumbnails.ArtResizer\")\n    def test_process_album(self, mock_artresizer):\n        get_size = mock_artresizer.shared.get_size\n\n        plugin = ThumbnailsPlugin()\n        make_cover = plugin.make_cover_thumbnail = Mock(return_value=True)\n        make_dolphin = plugin.make_dolphin_cover_thumbnail = Mock()\n\n        # no art\n        album = Mock(artpath=None)\n        plugin.process_album(album)\n        assert get_size.call_count == 0\n        assert make_dolphin.call_count == 0\n\n        # cannot get art size\n        album.artpath = b\"/path/to/art\"\n        get_size.return_value = None\n        plugin.process_album(album)\n        get_size.assert_called_once_with(b\"/path/to/art\")\n        assert make_cover.call_count == 0\n\n        # dolphin tests\n        plugin.config[\"dolphin\"] = False\n        plugin.process_album(album)\n        assert make_dolphin.call_count == 0\n\n        plugin.config[\"dolphin\"] = True\n        plugin.process_album(album)\n        make_dolphin.assert_called_once_with(album)\n\n        # small art\n        get_size.return_value = 200, 200\n        plugin.process_album(album)\n        make_cover.assert_called_once_with(album, 128, NORMAL_DIR)\n\n        # big art\n        make_cover.reset_mock()\n        get_size.return_value = 500, 500\n        plugin.process_album(album)\n        make_cover.assert_has_calls(\n            [call(album, 128, NORMAL_DIR), call(album, 256, LARGE_DIR)],\n            any_order=True,\n        )\n\n    @patch(\"beetsplug.thumbnails.ThumbnailsPlugin._check_local_ok\", Mock())\n    def test_invokations(self):\n        plugin = ThumbnailsPlugin()\n        plugin.process_album = Mock()\n        album = Mock()\n\n        plugin.process_album.reset_mock()\n        lib = Mock()\n        album2 = Mock()\n        lib.albums.return_value = [album, album2]\n        plugin.process_query(lib, Mock(), None)\n        plugin.process_album.assert_has_calls(\n            [call(album), call(album2)], any_order=True\n        )\n\n    @patch(\"beetsplug.thumbnails.BaseDirectory\")\n    def test_thumbnail_file_name(self, mock_basedir):\n        plug = ThumbnailsPlugin()\n        plug.get_uri = Mock(return_value=\"file:///my/uri\")\n        assert (\n            plug.thumbnail_file_name(b\"idontcare\")\n            == b\"9488f5797fbe12ffb316d607dfd93d04.png\"\n        )\n\n    def test_uri(self):\n        gio = GioURI()\n        if not gio.available:\n            self.skipTest(\"GIO library not found\")\n\n        import ctypes\n\n        with pytest.raises(ctypes.ArgumentError):\n            gio.uri(\"/foo\")\n        assert gio.uri(b\"/foo\") == \"file:///foo\"\n        assert gio.uri(b\"/foo!\") == \"file:///foo!\"\n        assert (\n            gio.uri(b\"/music/\\xec\\x8b\\xb8\\xec\\x9d\\xb4\")\n            == \"file:///music/%EC%8B%B8%EC%9D%B4\"\n        )\n\n\nclass TestPathlibURI:\n    \"\"\"Test PathlibURI class\"\"\"\n\n    def test_uri(self):\n        test_uri = PathlibURI()\n\n        # test it won't break if we pass it bytes for a path\n        test_uri.uri(b\"/\")\n"
  },
  {
    "path": "test/plugins/test_titlecase.py",
    "content": "# This file is part of beets.\n# Copyright 2025, Henry Oberholtzer\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for the 'titlecase' plugin\"\"\"\n\nfrom unittest.mock import patch\n\nfrom beets.autotag.hooks import AlbumInfo, TrackInfo\nfrom beets.importer import ImportSession, ImportTask\nfrom beets.library import Item\nfrom beets.test.helper import PluginTestCase\nfrom beetsplug.titlecase import TitlecasePlugin\n\ntitlecase_fields_testcases = [\n    (\n        {\n            \"fields\": [\n                \"artist\",\n                \"albumartist\",\n                \"title\",\n                \"album\",\n                \"mb_albumd\",\n                \"year\",\n            ],\n            \"force_lowercase\": True,\n        },\n        Item(\n            artist=\"OPHIDIAN\",\n            albumartist=\"ophiDIAN\",\n            format=\"CD\",\n            year=2003,\n            album=\"BLACKBOX\",\n            title=\"KhAmElEoN\",\n        ),\n        Item(\n            artist=\"Ophidian\",\n            albumartist=\"Ophidian\",\n            format=\"CD\",\n            year=2003,\n            album=\"Blackbox\",\n            title=\"Khameleon\",\n        ),\n    ),\n]\n\n\nclass TestTitlecasePlugin(PluginTestCase):\n    plugin = \"titlecase\"\n    preload_plugin = False\n\n    def test_auto(self):\n        \"\"\"Ensure automatic processing gets assigned\"\"\"\n        with self.configure_plugin({\"auto\": True, \"after_choice\": True}):\n            assert callable(TitlecasePlugin().import_stages[0])\n        with self.configure_plugin({\"auto\": False, \"after_choice\": False}):\n            assert len(TitlecasePlugin().import_stages) == 0\n        with self.configure_plugin({\"auto\": False, \"after_choice\": True}):\n            assert len(TitlecasePlugin().import_stages) == 0\n\n    def test_basic_titlecase(self):\n        \"\"\"Check that default behavior is as expected.\"\"\"\n        testcases = [\n            (\"a\", \"A\"),\n            (\"PENDULUM\", \"Pendulum\"),\n            (\"Aaron-carl\", \"Aaron-Carl\"),\n            (\"LTJ bukem\", \"LTJ Bukem\"),\n            (\"(original mix)\", \"(Original Mix)\"),\n            (\"ALL CAPS TITLE\", \"All Caps Title\"),\n        ]\n        for testcase in testcases:\n            given, expected = testcase\n            assert TitlecasePlugin().titlecase(given) == expected\n\n    def test_small_first_last(self):\n        \"\"\"Check the behavior for supporting small first last\"\"\"\n        testcases = [\n            (True, \"In a Silent Way\", \"In a Silent Way\"),\n            (False, \"In a Silent Way\", \"in a Silent Way\"),\n        ]\n        for testcase in testcases:\n            sfl, given, expected = testcase\n            cfg = {\"small_first_last\": sfl}\n            with self.configure_plugin(cfg):\n                assert TitlecasePlugin().titlecase(given) == expected\n\n    def test_preserve(self):\n        \"\"\"Test using given strings to preserve case\"\"\"\n        preserve_list = [\n            \"easyFun\",\n            \"A.D.O.R\",\n            \"D'Angelo\",\n            \"ABBA\",\n            \"LaTeX\",\n            \"O.R.B\",\n            \"PinkPantheress\",\n            \"THE PSYCHIC ED RUSH\",\n            \"LTJ Bukem\",\n        ]\n        for word in preserve_list:\n            with self.configure_plugin({\"preserve\": preserve_list}):\n                assert TitlecasePlugin().titlecase(word.upper()) == word\n                assert TitlecasePlugin().titlecase(word.lower()) == word\n\n    def test_separators(self):\n        testcases = [\n            ([], \"it / a / in / of / to / the\", \"It / a / in / of / to / The\"),\n            ([\"/\"], \"it / the test\", \"It / The Test\"),\n            (\n                [\"/\"],\n                \"it / a / in / of / to / the\",\n                \"It / A / In / Of / To / The\",\n            ),\n            ([\"/\"], \"//it/a/in/of/to/the\", \"//It/A/In/Of/To/The\"),\n            (\n                [\"/\", \";\", \"|\"],\n                \"it ; a / in | of / to | the\",\n                \"It ; A / In | Of / To | The\",\n            ),\n        ]\n        for testcase in testcases:\n            separators, given, expected = testcase\n            with self.configure_plugin({\"separators\": separators}):\n                assert TitlecasePlugin().titlecase(given) == expected\n\n    def test_all_caps(self):\n        testcases = [\n            (True, \"Unaffected\", \"Unaffected\"),\n            (True, \"RBMK1000\", \"RBMK1000\"),\n            (False, \"RBMK1000\", \"Rbmk1000\"),\n            (True, \"P A R I S!\", \"P A R I S!\"),\n            (True, \"pillow dub...\", \"Pillow Dub...\"),\n            (False, \"P A R I S!\", \"P a R I S!\"),\n        ]\n        for testcase in testcases:\n            all_caps, given, expected = testcase\n            with self.configure_plugin({\"all_caps\": all_caps}):\n                assert TitlecasePlugin().titlecase(given) == expected\n\n    def test_all_lowercase(self):\n        testcases = [\n            (True, \"Unaffected\", \"Unaffected\"),\n            (True, \"RBMK1000\", \"Rbmk1000\"),\n            (True, \"pillow dub...\", \"pillow dub...\"),\n            (False, \"pillow dub...\", \"Pillow Dub...\"),\n        ]\n        for testcase in testcases:\n            all_lowercase, given, expected = testcase\n            with self.configure_plugin({\"all_lowercase\": all_lowercase}):\n                assert TitlecasePlugin().titlecase(given) == expected\n\n    def test_received_info_handler(self):\n        testcases = [\n            (\n                TrackInfo(\n                    album=\"test album\",\n                    artist_credit=\"test artist credit\",\n                    artists=[\"artist one\", \"artist two\"],\n                ),\n                TrackInfo(\n                    album=\"Test Album\",\n                    artist_credit=\"Test Artist Credit\",\n                    artists=[\"Artist One\", \"Artist Two\"],\n                ),\n            ),\n            (\n                AlbumInfo(\n                    tracks=[\n                        TrackInfo(\n                            album=\"test album\",\n                            artist_credit=\"test artist credit\",\n                            artists=[\"artist one\", \"artist two\"],\n                        )\n                    ],\n                    album=\"test album\",\n                    artist_credit=\"test artist credit\",\n                    artists=[\"artist one\", \"artist two\"],\n                ),\n                AlbumInfo(\n                    tracks=[\n                        TrackInfo(\n                            album=\"Test Album\",\n                            artist_credit=\"Test Artist Credit\",\n                            artists=[\"Artist One\", \"Artist Two\"],\n                        )\n                    ],\n                    album=\"Test Album\",\n                    artist_credit=\"Test Artist Credit\",\n                    artists=[\"Artist One\", \"Artist Two\"],\n                ),\n            ),\n        ]\n        cfg = {\"fields\": [\"album\", \"artist_credit\", \"artists\"]}\n        for testcase in testcases:\n            given, expected = testcase\n            with self.configure_plugin(cfg):\n                TitlecasePlugin().received_info_handler(given)\n                assert given == expected\n\n    def test_titlecase_fields(self):\n        testcases = [\n            # Test with preserve, replace, and mb_albumid\n            # Test with the_artist\n            (\n                {\n                    \"preserve\": [\"D'Angelo\"],\n                    \"replace\": [(\"’\", \"'\")],\n                    \"fields\": [\"artist\", \"albumartist\", \"mb_albumid\"],\n                },\n                Item(\n                    artist=\"d’angelo and the vanguard\",\n                    mb_albumid=\"ab140e13-7b36-402a-a528-b69e3dee38a8\",\n                    albumartist=\"d’angelo\",\n                    format=\"CD\",\n                    album=\"the black messiah\",\n                    title=\"Till It's Done (Tutu)\",\n                ),\n                Item(\n                    artist=\"D'Angelo and The Vanguard\",\n                    mb_albumid=\"Ab140e13-7b36-402a-A528-B69e3dee38a8\",\n                    albumartist=\"D'Angelo\",\n                    format=\"CD\",\n                    album=\"the black messiah\",\n                    title=\"Till It's Done (Tutu)\",\n                ),\n            ),\n            # Test with force_lowercase, preserve, and an incorrect field\n            (\n                {\n                    \"force_lowercase\": True,\n                    \"fields\": [\n                        \"artist\",\n                        \"albumartist\",\n                        \"format\",\n                        \"title\",\n                        \"year\",\n                        \"label\",\n                        \"format\",\n                        \"INCORRECT_FIELD\",\n                    ],\n                    \"preserve\": [\"CD\"],\n                },\n                Item(\n                    artist=\"OPHIDIAN\",\n                    albumartist=\"OphiDIAN\",\n                    format=\"cd\",\n                    year=2003,\n                    album=\"BLACKBOX\",\n                    title=\"KhAmElEoN\",\n                    label=\"enzyme records\",\n                ),\n                Item(\n                    artist=\"Ophidian\",\n                    albumartist=\"Ophidian\",\n                    format=\"CD\",\n                    year=2003,\n                    album=\"Blackbox\",\n                    title=\"Khameleon\",\n                    label=\"Enzyme Records\",\n                ),\n            ),\n            # Test with no changes\n            (\n                {\n                    \"fields\": [\n                        \"artist\",\n                        \"artists\",\n                        \"albumartist\",\n                        \"format\",\n                        \"title\",\n                        \"year\",\n                        \"label\",\n                        \"format\",\n                        \"INCORRECT_FIELD\",\n                    ],\n                    \"preserve\": [\"CD\"],\n                },\n                Item(\n                    artist=\"Ophidian\",\n                    artists=[\"Ophidian\"],\n                    albumartist=\"Ophidian\",\n                    format=\"CD\",\n                    year=2003,\n                    album=\"Blackbox\",\n                    title=\"Khameleon\",\n                    label=\"Enzyme Records\",\n                ),\n                Item(\n                    artist=\"Ophidian\",\n                    artists=[\"Ophidian\"],\n                    albumartist=\"Ophidian\",\n                    format=\"CD\",\n                    year=2003,\n                    album=\"Blackbox\",\n                    title=\"Khameleon\",\n                    label=\"Enzyme Records\",\n                ),\n            ),\n            # Test with the_artist disabled\n            (\n                {\n                    \"the_artist\": False,\n                    \"fields\": [\n                        \"artist\",\n                        \"artists_sort\",\n                    ],\n                },\n                Item(\n                    artists_sort=[\"b-52s, the\"],\n                    artist=\"a day in the park\",\n                ),\n                Item(\n                    artists_sort=[\"B-52s, The\"],\n                    artist=\"A Day in the Park\",\n                ),\n            ),\n            # Test to make sure preserve and the_artist\n            # dont target the middle of sentences\n            # show that The artist applies to any field\n            # with artist mentioned\n            (\n                {\n                    \"preserve\": [\"PANTHER\"],\n                    \"fields\": [\"artist\", \"artists\", \"artists_ids\"],\n                },\n                Item(\n                    artist=\"pinkpantheress\",\n                    artists=[\"pinkpantheress\", \"artist_two\"],\n                    artists_ids=[\"the the\", \"the the\"],\n                ),\n                Item(\n                    artist=\"Pinkpantheress\",\n                    artists=[\"Pinkpantheress\", \"Artist_two\"],\n                    artists_ids=[\"The The\", \"The The\"],\n                ),\n            ),\n        ]\n        for testcase in testcases:\n            cfg, given, expected = testcase\n            with self.configure_plugin(cfg):\n                TitlecasePlugin().titlecase_fields(given)\n                assert given.artist == expected.artist\n                assert given.artists == expected.artists\n                assert given.artists_sort == expected.artists_sort\n                assert given.albumartist == expected.albumartist\n                assert given.artists_ids == expected.artists_ids\n                assert given.format == expected.format\n                assert given.year == expected.year\n                assert given.title == expected.title\n                assert given.label == expected.label\n\n    def test_cli_write(self):\n        given = Item(\n            album=\"retrodelica 2: back 2 the future\",\n            artist=\"blue planet corporation\",\n            title=\"generator\",\n        )\n        expected = Item(\n            album=\"Retrodelica 2: Back 2 the Future\",\n            artist=\"Blue Planet Corporation\",\n            title=\"Generator\",\n        )\n        cfg = {\"fields\": [\"album\", \"artist\", \"title\"]}\n        with self.configure_plugin(cfg):\n            given.add(self.lib)\n            self.run_command(\"titlecase\")\n            assert self.lib.items().get().artist == expected.artist\n            assert self.lib.items().get().album == expected.album\n            assert self.lib.items().get().title == expected.title\n            self.lib.items().get().remove()\n\n    def test_cli_no_write(self):\n        given = Item(\n            album=\"retrodelica 2: back 2 the future\",\n            artist=\"blue planet corporation\",\n            title=\"generator\",\n        )\n        expected = Item(\n            album=\"retrodelica 2: back 2 the future\",\n            artist=\"blue planet corporation\",\n            title=\"generator\",\n        )\n        cfg = {\"fields\": [\"album\", \"artist\", \"title\"]}\n        with self.configure_plugin(cfg):\n            given.add(self.lib)\n            self.run_command(\"-p\", \"titlecase\")\n            assert self.lib.items().get().artist == expected.artist\n            assert self.lib.items().get().album == expected.album\n            assert self.lib.items().get().title == expected.title\n            self.lib.items().get().remove()\n\n    def test_imported(self):\n        given = Item(\n            album=\"retrodelica 2: back 2 the future\",\n            artist=\"blue planet corporation\",\n            title=\"generator\",\n        )\n        expected = Item(\n            album=\"Retrodelica 2: Back 2 the Future\",\n            artist=\"Blue Planet Corporation\",\n            title=\"Generator\",\n        )\n        p = patch(\"beets.importer.ImportTask.imported_items\", lambda x: [given])\n        p.start()\n        with self.configure_plugin({\"fields\": [\"album\", \"artist\", \"title\"]}):\n            import_session = ImportSession(\n                self.lib, loghandler=None, paths=None, query=None\n            )\n            import_task = ImportTask(toppath=None, paths=None, items=[given])\n            TitlecasePlugin().imported(import_session, import_task)\n            import_task.add(self.lib)\n            item = self.lib.items().get()\n            assert item.artist == expected.artist\n            assert item.album == expected.album\n            assert item.title == expected.title\n        p.stop()\n"
  },
  {
    "path": "test/plugins/test_types_plugin.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Thomas Scholtes.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nimport time\nfrom datetime import datetime\n\nimport pytest\nfrom confuse import ConfigValueError\n\nfrom beets.test.helper import IOMixin, PluginTestCase\n\n\nclass TypesPluginTest(IOMixin, PluginTestCase):\n    plugin = \"types\"\n\n    def test_integer_modify_and_query(self):\n        self.config[\"types\"] = {\"myint\": \"int\"}\n        item = self.add_item(artist=\"aaa\")\n\n        # Do not match unset values\n        out = self.list(\"myint:1..3\")\n        assert \"\" == out\n\n        self.modify(\"myint=2\")\n        item.load()\n        assert item[\"myint\"] == 2\n\n        # Match in range\n        out = self.list(\"myint:1..3\")\n        assert \"aaa\" in out\n\n    def test_album_integer_modify_and_query(self):\n        self.config[\"types\"] = {\"myint\": \"int\"}\n        album = self.add_album(albumartist=\"aaa\")\n\n        # Do not match unset values\n        out = self.list_album(\"myint:1..3\")\n        assert \"\" == out\n\n        self.modify(\"-a\", \"myint=2\")\n        album.load()\n        assert album[\"myint\"] == 2\n\n        # Match in range\n        out = self.list_album(\"myint:1..3\")\n        assert \"aaa\" in out\n\n    def test_float_modify_and_query(self):\n        self.config[\"types\"] = {\"myfloat\": \"float\"}\n        item = self.add_item(artist=\"aaa\")\n\n        # Do not match unset values\n        out = self.list(\"myfloat:10..0\")\n        assert \"\" == out\n\n        self.modify(\"myfloat=-9.1\")\n        item.load()\n        assert item[\"myfloat\"] == -9.1\n\n        # Match in range\n        out = self.list(\"myfloat:-10..0\")\n        assert \"aaa\" in out\n\n    def test_bool_modify_and_query(self):\n        self.config[\"types\"] = {\"mybool\": \"bool\"}\n        true = self.add_item(artist=\"true\")\n        false = self.add_item(artist=\"false\")\n        self.add_item(artist=\"unset\")\n\n        # Do not match unset values\n        out = self.list(\"mybool:true, mybool:false\")\n        assert \"\" == out\n\n        # Set true\n        self.modify(\"mybool=1\", \"artist:true\")\n        true.load()\n        assert true[\"mybool\"]\n\n        # Set false\n        self.modify(\"mybool=false\", \"artist:false\")\n        false.load()\n        assert not false[\"mybool\"]\n\n        # Query bools\n        out = self.list(\"mybool:true\", \"$artist $mybool\")\n        assert \"true True\" == out\n\n        out = self.list(\"mybool:false\", \"$artist $mybool\")\n\n        # Dealing with unset fields?\n        # assert 'false False' == out\n        # out = self.list('mybool:', '$artist $mybool')\n        # assert 'unset $mybool' in out\n\n    def test_date_modify_and_query(self):\n        self.config[\"types\"] = {\"mydate\": \"date\"}\n        # FIXME parsing should also work with default time format\n        self.config[\"time_format\"] = \"%Y-%m-%d\"\n        old = self.add_item(artist=\"prince\")\n        new = self.add_item(artist=\"britney\")\n\n        # Do not match unset values\n        out = self.list(\"mydate:..2000\")\n        assert \"\" == out\n\n        self.modify(\"mydate=1999-01-01\", \"artist:prince\")\n        old.load()\n        assert old[\"mydate\"] == mktime(1999, 1, 1)\n\n        self.modify(\"mydate=1999-12-30\", \"artist:britney\")\n        new.load()\n        assert new[\"mydate\"] == mktime(1999, 12, 30)\n\n        # Match in range\n        out = self.list(\"mydate:..1999-07\", \"$artist $mydate\")\n        assert \"prince 1999-01-01\" == out\n\n        # FIXME some sort of timezone issue here\n        # out = self.list('mydate:1999-12-30', '$artist $mydate')\n        # assert 'britney 1999-12-30' == out\n\n    def test_unknown_type_error(self):\n        self.config[\"types\"] = {\"flex\": \"unkown type\"}\n        with pytest.raises(ConfigValueError):\n            self.add_item(flex=\"test\")\n\n    def test_template_if_def(self):\n        # Tests for a subtle bug when using %ifdef in templates along with\n        # types that have truthy default values (e.g. '0', '0.0', 'False')\n        # https://github.com/beetbox/beets/issues/3852\n        self.config[\"types\"] = {\n            \"playcount\": \"int\",\n            \"rating\": \"float\",\n            \"starred\": \"bool\",\n        }\n\n        with_fields = self.add_item(artist=\"prince\")\n        self.modify(\"playcount=10\", \"artist=prince\")\n        self.modify(\"rating=5.0\", \"artist=prince\")\n        self.modify(\"starred=yes\", \"artist=prince\")\n        with_fields.load()\n\n        without_fields = self.add_item(artist=\"britney\")\n\n        int_template = \"%ifdef{playcount,Play count: $playcount,Not played}\"\n        assert with_fields.evaluate_template(int_template) == \"Play count: 10\"\n        assert without_fields.evaluate_template(int_template) == \"Not played\"\n\n        float_template = \"%ifdef{rating,Rating: $rating,Not rated}\"\n        assert with_fields.evaluate_template(float_template) == \"Rating: 5.0\"\n        assert without_fields.evaluate_template(float_template) == \"Not rated\"\n\n        bool_template = \"%ifdef{starred,Starred: $starred,Not starred}\"\n        assert with_fields.evaluate_template(bool_template).lower() in (\n            \"starred: true\",\n            \"starred: yes\",\n            \"starred: y\",\n        )\n        assert without_fields.evaluate_template(bool_template) == \"Not starred\"\n\n    def modify(self, *args):\n        return self.run_with_output(\n            \"modify\", \"--yes\", \"--nowrite\", \"--nomove\", *args\n        )\n\n    def list(self, query, fmt=\"$artist - $album - $title\"):\n        return self.run_with_output(\"ls\", \"-f\", fmt, query).strip()\n\n    def list_album(self, query, fmt=\"$albumartist - $album - $title\"):\n        return self.run_with_output(\"ls\", \"-a\", \"-f\", fmt, query).strip()\n\n\ndef mktime(*args):\n    return time.mktime(datetime(*args).timetuple())\n"
  },
  {
    "path": "test/plugins/test_web.py",
    "content": "\"\"\"Tests for the 'web' plugin\"\"\"\n\nimport json\nimport os.path\nimport platform\nimport shutil\nfrom collections import Counter\n\nfrom beets import logging\nfrom beets.library import Album, Item\nfrom beets.test import _common\nfrom beets.test.helper import ItemInDBTestCase\nfrom beetsplug import web\n\n\nclass WebPluginTest(ItemInDBTestCase):\n    def setUp(self):\n        super().setUp()\n        self.log = logging.getLogger(\"beets.web\")\n\n        if platform.system() == \"Windows\":\n            self.path_prefix = \"C:\"\n        else:\n            self.path_prefix = \"\"\n\n        # Add fixtures\n        for track in self.lib.items():\n            track.remove()\n\n        # Add library elements. Note that self.lib.add overrides any \"id=<n>\"\n        # and assigns the next free id number.\n        # The following adds will create items #1, #2 and #3\n        path1 = (\n            self.path_prefix + os.sep + os.path.join(b\"path_1\").decode(\"utf-8\")\n        )\n        self.lib.add(\n            Item(title=\"title\", path=path1, album_id=2, artist=\"AAA Singers\")\n        )\n        path2 = (\n            self.path_prefix\n            + os.sep\n            + os.path.join(b\"somewhere\", b\"a\").decode(\"utf-8\")\n        )\n        self.lib.add(\n            Item(title=\"another title\", path=path2, artist=\"AAA Singers\")\n        )\n        path3 = (\n            self.path_prefix\n            + os.sep\n            + os.path.join(b\"somewhere\", b\"abc\").decode(\"utf-8\")\n        )\n        self.lib.add(\n            Item(title=\"and a third\", testattr=\"ABC\", path=path3, album_id=2)\n        )\n        # The following adds will create albums #1 and #2\n        self.lib.add(Album(album=\"album\", albumtest=\"xyz\"))\n        path4 = (\n            self.path_prefix\n            + os.sep\n            + os.path.join(b\"somewhere2\", b\"art_path_2\").decode(\"utf-8\")\n        )\n        self.lib.add(Album(album=\"other album\", artpath=path4))\n\n        web.app.config[\"TESTING\"] = True\n        web.app.config[\"lib\"] = self.lib\n        web.app.config[\"INCLUDE_PATHS\"] = False\n        web.app.config[\"READONLY\"] = True\n        self.client = web.app.test_client()\n\n    def test_config_include_paths_true(self):\n        web.app.config[\"INCLUDE_PATHS\"] = True\n        response = self.client.get(\"/item/1\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        expected_path = (\n            self.path_prefix + os.sep + os.path.join(b\"path_1\").decode(\"utf-8\")\n        )\n\n        assert response.status_code == 200\n        assert res_json[\"path\"] == expected_path\n\n        web.app.config[\"INCLUDE_PATHS\"] = False\n\n    def test_config_include_artpaths_true(self):\n        web.app.config[\"INCLUDE_PATHS\"] = True\n        response = self.client.get(\"/album/2\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        expected_path = (\n            self.path_prefix\n            + os.sep\n            + os.path.join(b\"somewhere2\", b\"art_path_2\").decode(\"utf-8\")\n        )\n\n        assert response.status_code == 200\n        assert res_json[\"artpath\"] == expected_path\n\n        web.app.config[\"INCLUDE_PATHS\"] = False\n\n    def test_config_include_paths_false(self):\n        web.app.config[\"INCLUDE_PATHS\"] = False\n        response = self.client.get(\"/item/1\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        assert \"path\" not in res_json\n\n    def test_config_include_artpaths_false(self):\n        web.app.config[\"INCLUDE_PATHS\"] = False\n        response = self.client.get(\"/album/2\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        assert \"artpath\" not in res_json\n\n    def test_get_all_items(self):\n        response = self.client.get(\"/item/\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        assert len(res_json[\"items\"]) == 3\n\n    def test_get_unique_item_artist(self):\n        response = self.client.get(\"/item/values/artist\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        assert res_json[\"values\"] == [\"\", \"AAA Singers\"]\n\n    def test_get_single_item_by_id(self):\n        response = self.client.get(\"/item/1\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        assert res_json[\"id\"] == 1\n        assert res_json[\"title\"] == \"title\"\n\n    def test_get_multiple_items_by_id(self):\n        response = self.client.get(\"/item/1,2\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        assert len(res_json[\"items\"]) == 2\n        response_titles = {item[\"title\"] for item in res_json[\"items\"]}\n        assert response_titles == {\"title\", \"another title\"}\n\n    def test_get_single_item_not_found(self):\n        response = self.client.get(\"/item/4\")\n        assert response.status_code == 404\n\n    def test_get_single_item_by_path(self):\n        data_path = os.path.join(_common.RSRC, b\"full.mp3\")\n        self.lib.add(Item.from_path(data_path))\n        response = self.client.get(f\"/item/path/{data_path.decode('utf-8')}\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        assert res_json[\"title\"] == \"full\"\n\n    def test_get_single_item_by_path_not_found_if_not_in_library(self):\n        data_path = os.path.join(_common.RSRC, b\"full.mp3\")\n        # data_path points to a valid file, but we have not added the file\n        # to the library.\n        response = self.client.get(f\"/item/path/{data_path.decode('utf-8')}\")\n\n        assert response.status_code == 404\n\n    def test_get_item_empty_query(self):\n        response = self.client.get(\"/item/query/\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        assert len(res_json[\"items\"]) == 3\n\n    def test_get_simple_item_query(self):\n        response = self.client.get(\"/item/query/another\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        assert len(res_json[\"results\"]) == 1\n        assert res_json[\"results\"][0][\"title\"] == \"another title\"\n\n    def test_query_item_string(self):\n        response = self.client.get(\"/item/query/testattr%3aABC\")  # testattr:ABC\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        assert len(res_json[\"results\"]) == 1\n        assert res_json[\"results\"][0][\"title\"] == \"and a third\"\n\n    def test_query_item_regex(self):\n        response = self.client.get(\n            \"/item/query/testattr%3a%3a[A-C]%2b\"\n        )  # testattr::[A-C]+\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        assert len(res_json[\"results\"]) == 1\n        assert res_json[\"results\"][0][\"title\"] == \"and a third\"\n\n    def test_query_item_regex_backslash(self):\n        response = self.client.get(\n            \"/item/query/testattr%3a%3a%5cw%2b\"\n        )  # testattr::\\w+\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        assert len(res_json[\"results\"]) == 1\n        assert res_json[\"results\"][0][\"title\"] == \"and a third\"\n\n    def test_query_item_path(self):\n        \"\"\"Note: path queries are special: the query item must match the path\n        from the root all the way to a directory, so this matches 1 item\"\"\"\n        \"\"\" Note: filesystem separators in the query must be '\\' \"\"\"\n\n        response = self.client.get(\n            \"/item/query/path:\" + self.path_prefix + \"\\\\somewhere\\\\a\"\n        )\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        assert len(res_json[\"results\"]) == 1\n        assert res_json[\"results\"][0][\"title\"] == \"another title\"\n\n    def test_get_all_albums(self):\n        response = self.client.get(\"/album/\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        response_albums = [album[\"album\"] for album in res_json[\"albums\"]]\n        assert Counter(response_albums) == {\"album\": 1, \"other album\": 1}\n\n    def test_get_single_album_by_id(self):\n        response = self.client.get(\"/album/2\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        assert res_json[\"id\"] == 2\n        assert res_json[\"album\"] == \"other album\"\n\n    def test_get_multiple_albums_by_id(self):\n        response = self.client.get(\"/album/1,2\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        response_albums = [album[\"album\"] for album in res_json[\"albums\"]]\n        assert Counter(response_albums) == {\"album\": 1, \"other album\": 1}\n\n    def test_get_album_empty_query(self):\n        response = self.client.get(\"/album/query/\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        assert len(res_json[\"albums\"]) == 2\n\n    def test_get_simple_album_query(self):\n        response = self.client.get(\"/album/query/other\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        assert len(res_json[\"results\"]) == 1\n        assert res_json[\"results\"][0][\"album\"] == \"other album\"\n        assert res_json[\"results\"][0][\"id\"] == 2\n\n    def test_get_album_details(self):\n        response = self.client.get(\"/album/2?expand\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        assert len(res_json[\"items\"]) == 2\n        assert res_json[\"items\"][0][\"album\"] == \"other album\"\n        assert res_json[\"items\"][1][\"album\"] == \"other album\"\n        response_track_titles = {item[\"title\"] for item in res_json[\"items\"]}\n        assert response_track_titles == {\"title\", \"and a third\"}\n\n    def test_query_album_string(self):\n        response = self.client.get(\n            \"/album/query/albumtest%3axy\"\n        )  # albumtest:xy\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        assert len(res_json[\"results\"]) == 1\n        assert res_json[\"results\"][0][\"album\"] == \"album\"\n\n    def test_query_album_artpath_regex(self):\n        response = self.client.get(\n            \"/album/query/artpath%3a%3aart_\"\n        )  # artpath::art_\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        assert len(res_json[\"results\"]) == 1\n        assert res_json[\"results\"][0][\"album\"] == \"other album\"\n\n    def test_query_album_regex_backslash(self):\n        response = self.client.get(\n            \"/album/query/albumtest%3a%3a%5cw%2b\"\n        )  # albumtest::\\w+\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        assert len(res_json[\"results\"]) == 1\n        assert res_json[\"results\"][0][\"album\"] == \"album\"\n\n    def test_get_stats(self):\n        response = self.client.get(\"/stats\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n\n        assert response.status_code == 200\n        assert res_json[\"items\"] == 3\n        assert res_json[\"albums\"] == 2\n\n    def test_delete_item_id(self):\n        web.app.config[\"READONLY\"] = False\n\n        # Create a temporary item\n        item_id = self.lib.add(\n            Item(title=\"test_delete_item_id\", test_delete_item_id=1)\n        )\n\n        # Check we can find the temporary item we just created\n        response = self.client.get(f\"/item/{item_id}\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n        assert res_json[\"id\"] == item_id\n\n        # Delete item by id\n        response = self.client.delete(f\"/item/{item_id}\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n\n        # Check the item has gone\n        response = self.client.get(f\"/item/{item_id}\")\n        assert response.status_code == 404\n        # Note: if this fails, the item may still be around\n        # and may cause other tests to fail\n\n    def test_delete_item_without_file(self):\n        web.app.config[\"READONLY\"] = False\n\n        # Create an item with a file\n        ipath = os.path.join(self.temp_dir, b\"testfile1.mp3\")\n        shutil.copy(os.path.join(_common.RSRC, b\"full.mp3\"), ipath)\n        assert os.path.exists(ipath)\n        item_id = self.lib.add(Item.from_path(ipath))\n\n        # Check we can find the temporary item we just created\n        response = self.client.get(f\"/item/{item_id}\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n        assert res_json[\"id\"] == item_id\n\n        # Delete item by id, without deleting file\n        response = self.client.delete(f\"/item/{item_id}\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n\n        # Check the item has gone\n        response = self.client.get(f\"/item/{item_id}\")\n        assert response.status_code == 404\n\n        # Check the file has not gone\n        assert os.path.exists(ipath)\n        os.remove(ipath)\n\n    def test_delete_item_with_file(self):\n        web.app.config[\"READONLY\"] = False\n\n        # Create an item with a file\n        ipath = os.path.join(self.temp_dir, b\"testfile2.mp3\")\n        shutil.copy(os.path.join(_common.RSRC, b\"full.mp3\"), ipath)\n        assert os.path.exists(ipath)\n        item_id = self.lib.add(Item.from_path(ipath))\n\n        # Check we can find the temporary item we just created\n        response = self.client.get(f\"/item/{item_id}\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n        assert res_json[\"id\"] == item_id\n\n        # Delete item by id, with file\n        response = self.client.delete(f\"/item/{item_id}?delete\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n\n        # Check the item has gone\n        response = self.client.get(f\"/item/{item_id}\")\n        assert response.status_code == 404\n\n        # Check the file has gone\n        assert not os.path.exists(ipath)\n\n    def test_delete_item_query(self):\n        web.app.config[\"READONLY\"] = False\n\n        # Create a temporary item\n        self.lib.add(\n            Item(title=\"test_delete_item_query\", test_delete_item_query=1)\n        )\n\n        # Check we can find the temporary item we just created\n        response = self.client.get(\"/item/query/test_delete_item_query\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n        assert len(res_json[\"results\"]) == 1\n\n        # Delete item by query\n        response = self.client.delete(\"/item/query/test_delete_item_query\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n\n        # Check the item has gone\n        response = self.client.get(\"/item/query/test_delete_item_query\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n        assert len(res_json[\"results\"]) == 0\n\n    def test_delete_item_all_fails(self):\n        \"\"\"DELETE is not supported for list all\"\"\"\n\n        web.app.config[\"READONLY\"] = False\n\n        # Delete all items\n        response = self.client.delete(\"/item/\")\n        assert response.status_code == 405\n\n        # Note: if this fails, all items have gone and rest of\n        # tests will fail!\n\n    def test_delete_item_id_readonly(self):\n        web.app.config[\"READONLY\"] = True\n\n        # Create a temporary item\n        item_id = self.lib.add(\n            Item(title=\"test_delete_item_id_ro\", test_delete_item_id_ro=1)\n        )\n\n        # Check we can find the temporary item we just created\n        response = self.client.get(f\"/item/{item_id}\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n        assert res_json[\"id\"] == item_id\n\n        # Try to delete item by id\n        response = self.client.delete(f\"/item/{item_id}\")\n        assert response.status_code == 405\n\n        # Check the item has not gone\n        response = self.client.get(f\"/item/{item_id}\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n        assert res_json[\"id\"] == item_id\n\n        # Remove it\n        self.lib.get_item(item_id).remove()\n\n    def test_delete_item_query_readonly(self):\n        web.app.config[\"READONLY\"] = True\n\n        # Create a temporary item\n        item_id = self.lib.add(\n            Item(title=\"test_delete_item_q_ro\", test_delete_item_q_ro=1)\n        )\n\n        # Check we can find the temporary item we just created\n        response = self.client.get(\"/item/query/test_delete_item_q_ro\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n        assert len(res_json[\"results\"]) == 1\n\n        # Try to delete item by query\n        response = self.client.delete(\"/item/query/test_delete_item_q_ro\")\n        assert response.status_code == 405\n\n        # Check the item has not gone\n        response = self.client.get(\"/item/query/test_delete_item_q_ro\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n        assert len(res_json[\"results\"]) == 1\n\n        # Remove it\n        self.lib.get_item(item_id).remove()\n\n    def test_delete_album_id(self):\n        web.app.config[\"READONLY\"] = False\n\n        # Create a temporary album\n        album_id = self.lib.add(\n            Album(album=\"test_delete_album_id\", test_delete_album_id=1)\n        )\n\n        # Check we can find the temporary album we just created\n        response = self.client.get(f\"/album/{album_id}\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n        assert res_json[\"id\"] == album_id\n\n        # Delete album by id\n        response = self.client.delete(f\"/album/{album_id}\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n\n        # Check the album has gone\n        response = self.client.get(f\"/album/{album_id}\")\n        assert response.status_code == 404\n        # Note: if this fails, the album may still be around\n        # and may cause other tests to fail\n\n    def test_delete_album_query(self):\n        web.app.config[\"READONLY\"] = False\n\n        # Create a temporary album\n        self.lib.add(\n            Album(album=\"test_delete_album_query\", test_delete_album_query=1)\n        )\n\n        # Check we can find the temporary album we just created\n        response = self.client.get(\"/album/query/test_delete_album_query\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n        assert len(res_json[\"results\"]) == 1\n\n        # Delete album\n        response = self.client.delete(\"/album/query/test_delete_album_query\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n\n        # Check the album has gone\n        response = self.client.get(\"/album/query/test_delete_album_query\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n        assert len(res_json[\"results\"]) == 0\n\n    def test_delete_album_all_fails(self):\n        \"\"\"DELETE is not supported for list all\"\"\"\n\n        web.app.config[\"READONLY\"] = False\n\n        # Delete all albums\n        response = self.client.delete(\"/album/\")\n        assert response.status_code == 405\n\n        # Note: if this fails, all albums have gone and rest of\n        # tests will fail!\n\n    def test_delete_album_id_readonly(self):\n        web.app.config[\"READONLY\"] = True\n\n        # Create a temporary album\n        album_id = self.lib.add(\n            Album(album=\"test_delete_album_id_ro\", test_delete_album_id_ro=1)\n        )\n\n        # Check we can find the temporary album we just created\n        response = self.client.get(f\"/album/{album_id}\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n        assert res_json[\"id\"] == album_id\n\n        # Try to delete album by id\n        response = self.client.delete(f\"/album/{album_id}\")\n        assert response.status_code == 405\n\n        # Check the item has not gone\n        response = self.client.get(f\"/album/{album_id}\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n        assert res_json[\"id\"] == album_id\n\n        # Remove it\n        self.lib.get_album(album_id).remove()\n\n    def test_delete_album_query_readonly(self):\n        web.app.config[\"READONLY\"] = True\n\n        # Create a temporary album\n        album_id = self.lib.add(\n            Album(\n                album=\"test_delete_album_query_ro\", test_delete_album_query_ro=1\n            )\n        )\n\n        # Check we can find the temporary album we just created\n        response = self.client.get(\"/album/query/test_delete_album_query_ro\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n        assert len(res_json[\"results\"]) == 1\n\n        # Try to delete album\n        response = self.client.delete(\"/album/query/test_delete_album_query_ro\")\n        assert response.status_code == 405\n\n        # Check the album has not gone\n        response = self.client.get(\"/album/query/test_delete_album_query_ro\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n        assert len(res_json[\"results\"]) == 1\n\n        # Remove it\n        self.lib.get_album(album_id).remove()\n\n    def test_patch_item_id(self):\n        # Note: PATCH is currently only implemented for track items, not albums\n\n        web.app.config[\"READONLY\"] = False\n\n        # Create a temporary item\n        item_id = self.lib.add(\n            Item(\n                title=\"test_patch_item_id\", test_patch_f1=1, test_patch_f2=\"Old\"\n            )\n        )\n\n        # Check we can find the temporary item we just created\n        response = self.client.get(f\"/item/{item_id}\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n        assert res_json[\"id\"] == item_id\n        assert res_json[\"test_patch_f1\"] == \"1\"\n        assert res_json[\"test_patch_f2\"] == \"Old\"\n\n        # Patch item by id\n        # patch_json = json.JSONEncoder().encode({\"test_patch_f2\": \"New\"}]})\n        response = self.client.patch(\n            f\"/item/{item_id}\", json={\"test_patch_f2\": \"New\"}\n        )\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n        assert res_json[\"id\"] == item_id\n        assert res_json[\"test_patch_f1\"] == \"1\"\n        assert res_json[\"test_patch_f2\"] == \"New\"\n\n        # Check the update has really worked\n        response = self.client.get(f\"/item/{item_id}\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n        assert res_json[\"id\"] == item_id\n        assert res_json[\"test_patch_f1\"] == \"1\"\n        assert res_json[\"test_patch_f2\"] == \"New\"\n\n        # Remove the item\n        self.lib.get_item(item_id).remove()\n\n    def test_patch_item_id_readonly(self):\n        # Note: PATCH is currently only implemented for track items, not albums\n\n        web.app.config[\"READONLY\"] = True\n\n        # Create a temporary item\n        item_id = self.lib.add(\n            Item(\n                title=\"test_patch_item_id_ro\",\n                test_patch_f1=2,\n                test_patch_f2=\"Old\",\n            )\n        )\n\n        # Check we can find the temporary item we just created\n        response = self.client.get(f\"/item/{item_id}\")\n        res_json = json.loads(response.data.decode(\"utf-8\"))\n        assert response.status_code == 200\n        assert res_json[\"id\"] == item_id\n        assert res_json[\"test_patch_f1\"] == \"2\"\n        assert res_json[\"test_patch_f2\"] == \"Old\"\n\n        # Patch item by id\n        # patch_json = json.JSONEncoder().encode({\"test_patch_f2\": \"New\"})\n        response = self.client.patch(\n            f\"/item/{item_id}\", json={\"test_patch_f2\": \"New\"}\n        )\n        assert response.status_code == 405\n\n        # Remove the item\n        self.lib.get_item(item_id).remove()\n\n    def test_get_item_file(self):\n        ipath = os.path.join(self.temp_dir, b\"testfile2.mp3\")\n        shutil.copy(os.path.join(_common.RSRC, b\"full.mp3\"), ipath)\n        assert os.path.exists(ipath)\n        item_id = self.lib.add(Item.from_path(ipath))\n\n        response = self.client.get(f\"/item/{item_id}/file\")\n\n        assert response.status_code == 200\n"
  },
  {
    "path": "test/plugins/test_zero.py",
    "content": "\"\"\"Tests for the 'zero' plugin\"\"\"\n\nfrom mediafile import MediaFile\n\nfrom beets.library import Item\nfrom beets.test.helper import IOMixin, PluginTestCase\nfrom beets.util import syspath\nfrom beetsplug.zero import ZeroPlugin\n\n\nclass ZeroPluginTest(IOMixin, PluginTestCase):\n    plugin = \"zero\"\n    preload_plugin = False\n\n    def test_no_patterns(self):\n        item = self.add_item_fixture(\n            comments=\"test comment\",\n            title=\"Title\",\n            month=1,\n            year=2000,\n        )\n        item.write()\n\n        with self.configure_plugin({\"fields\": [\"comments\", \"month\"]}):\n            item.write()\n\n        mf = MediaFile(syspath(item.path))\n        assert mf.comments is None\n        assert mf.month is None\n        assert mf.title == \"Title\"\n        assert mf.year == 2000\n\n    def test_pattern_match(self):\n        item = self.add_item_fixture(comments=\"encoded by encoder\")\n        item.write()\n\n        with self.configure_plugin(\n            {\"fields\": [\"comments\"], \"comments\": [\"encoded by\"]}\n        ):\n            item.write()\n\n        mf = MediaFile(syspath(item.path))\n        assert mf.comments is None\n\n    def test_pattern_nomatch(self):\n        item = self.add_item_fixture(comments=\"recorded at place\")\n        item.write()\n\n        with self.configure_plugin(\n            {\"fields\": [\"comments\"], \"comments\": [\"encoded_by\"]}\n        ):\n            item.write()\n\n        mf = MediaFile(syspath(item.path))\n        assert mf.comments == \"recorded at place\"\n\n    def test_do_not_change_database(self):\n        item = self.add_item_fixture(year=2000)\n        item.write()\n\n        with self.configure_plugin({\"fields\": [\"year\"]}):\n            item.write()\n\n        assert item[\"year\"] == 2000\n\n    def test_change_database(self):\n        item = self.add_item_fixture(year=2000)\n        item.write()\n\n        with self.configure_plugin(\n            {\"fields\": [\"year\"], \"update_database\": True}\n        ):\n            item.write()\n\n        assert item[\"year\"] == 0\n\n    def test_album_art(self):\n        path = self.create_mediafile_fixture(images=[\"jpg\"])\n        item = Item.from_path(path)\n\n        with self.configure_plugin({\"fields\": [\"images\"]}):\n            item.write()\n\n        mf = MediaFile(syspath(path))\n        assert not mf.images\n\n    def test_auto_false(self):\n        item = self.add_item_fixture(year=2000)\n        item.write()\n\n        with self.configure_plugin(\n            {\"fields\": [\"year\"], \"update_database\": True, \"auto\": False}\n        ):\n            item.write()\n\n        assert item[\"year\"] == 2000\n\n    def test_subcommand_update_database_true(self):\n        item = self.add_item_fixture(\n            year=2016, day=13, month=3, comments=\"test comment\"\n        )\n        item.write()\n        item_id = item.id\n\n        with self.configure_plugin(\n            {\"fields\": [\"comments\"], \"update_database\": True, \"auto\": False}\n        ):\n            self.io.addinput(\"y\")\n            self.run_command(\"zero\")\n\n        mf = MediaFile(syspath(item.path))\n        item = self.lib.get_item(item_id)\n\n        assert item[\"year\"] == 2016\n        assert mf.year == 2016\n        assert mf.comments is None\n        assert item[\"comments\"] == \"\"\n\n    def test_subcommand_update_database_false(self):\n        item = self.add_item_fixture(\n            year=2016, day=13, month=3, comments=\"test comment\"\n        )\n        item.write()\n        item_id = item.id\n\n        with self.configure_plugin(\n            {\n                \"fields\": [\"comments\"],\n                \"update_database\": False,\n                \"auto\": False,\n            }\n        ):\n            self.io.addinput(\"y\")\n            self.run_command(\"zero\")\n\n        mf = MediaFile(syspath(item.path))\n        item = self.lib.get_item(item_id)\n\n        assert item[\"year\"] == 2016\n        assert mf.year == 2016\n        assert item[\"comments\"] == \"test comment\"\n        assert mf.comments is None\n\n    def test_subcommand_query_include(self):\n        item = self.add_item_fixture(\n            year=2016, day=13, month=3, comments=\"test comment\"\n        )\n\n        item.write()\n\n        with self.configure_plugin(\n            {\"fields\": [\"comments\"], \"update_database\": False, \"auto\": False}\n        ):\n            self.run_command(\"zero\", \"year: 2016\")\n\n        mf = MediaFile(syspath(item.path))\n\n        assert mf.year == 2016\n        assert mf.comments is None\n\n    def test_subcommand_query_exclude(self):\n        item = self.add_item_fixture(\n            year=2016, day=13, month=3, comments=\"test comment\"\n        )\n\n        item.write()\n\n        with self.configure_plugin(\n            {\"fields\": [\"comments\"], \"update_database\": False, \"auto\": False}\n        ):\n            self.run_command(\"zero\", \"year: 0000\")\n\n        mf = MediaFile(syspath(item.path))\n\n        assert mf.year == 2016\n        assert mf.comments == \"test comment\"\n\n    def test_no_fields(self):\n        item = self.add_item_fixture(year=2016)\n        item.write()\n        mediafile = MediaFile(syspath(item.path))\n        assert mediafile.year == 2016\n\n        item_id = item.id\n\n        with self.configure_plugin({\"fields\": []}):\n            self.io.addinput(\"y\")\n            self.run_command(\"zero\")\n\n        item = self.lib.get_item(item_id)\n\n        assert item[\"year\"] == 2016\n        assert mediafile.year == 2016\n\n    def test_whitelist_and_blacklist(self):\n        item = self.add_item_fixture(year=2016)\n        item.write()\n        mf = MediaFile(syspath(item.path))\n        assert mf.year == 2016\n\n        item_id = item.id\n\n        with self.configure_plugin(\n            {\"fields\": [\"year\"], \"keep_fields\": [\"comments\"]}\n        ):\n            self.io.addinput(\"y\")\n            self.run_command(\"zero\")\n\n        item = self.lib.get_item(item_id)\n\n        assert item[\"year\"] == 2016\n        assert mf.year == 2016\n\n    def test_keep_fields(self):\n        item = self.add_item_fixture(year=2016, comments=\"test comment\")\n        tags = {\n            \"comments\": \"test comment\",\n            \"year\": 2016,\n        }\n\n        with self.configure_plugin(\n            {\"fields\": None, \"keep_fields\": [\"year\"], \"update_database\": True}\n        ):\n            z = ZeroPlugin()\n            z.write_event(item, item.path, tags)\n\n        assert tags[\"comments\"] is None\n        assert tags[\"year\"] == 2016\n\n    def test_keep_fields_removes_preserved_tags(self):\n        self.config[\"zero\"][\"keep_fields\"] = [\"year\"]\n        self.config[\"zero\"][\"fields\"] = None\n        self.config[\"zero\"][\"update_database\"] = True\n\n        z = ZeroPlugin()\n\n        assert \"id\" not in z.fields_to_progs\n\n    def test_fields_removes_preserved_tags(self):\n        self.config[\"zero\"][\"fields\"] = [\"year id\"]\n        self.config[\"zero\"][\"update_database\"] = True\n\n        z = ZeroPlugin()\n\n        assert \"id\" not in z.fields_to_progs\n\n    def test_omit_single_disc_with_tags_single(self):\n        item = self.add_item_fixture(\n            disctotal=1, disc=1, comments=\"test comment\"\n        )\n        item.write()\n        with self.configure_plugin(\n            {\"omit_single_disc\": True, \"fields\": [\"comments\"]}\n        ):\n            item.write()\n\n        mf = MediaFile(syspath(item.path))\n        assert mf.comments is None\n        assert mf.disc is None\n        assert mf.disctotal is None\n\n    def test_omit_single_disc_with_tags_multi(self):\n        item = self.add_item_fixture(\n            disctotal=4, disc=1, comments=\"test comment\"\n        )\n        item.write()\n        with self.configure_plugin(\n            {\"omit_single_disc\": True, \"fields\": [\"comments\"]}\n        ):\n            item.write()\n\n        mf = MediaFile(syspath(item.path))\n        assert mf.comments is None\n        assert mf.disc == 1\n        assert mf.disctotal == 4\n\n    def test_omit_single_disc_only_change_single(self):\n        item = self.add_item_fixture(disctotal=1, disc=1)\n        item.write()\n\n        with self.configure_plugin({\"omit_single_disc\": True}):\n            item.write()\n\n        mf = MediaFile(syspath(item.path))\n        assert mf.disc is None\n        assert mf.disctotal is None\n\n    def test_omit_single_disc_only_change_multi(self):\n        item = self.add_item_fixture(disctotal=4, disc=1)\n        item.write()\n\n        with self.configure_plugin({\"omit_single_disc\": True}):\n            item.write()\n\n        mf = MediaFile(syspath(item.path))\n        assert mf.disc == 1\n        assert mf.disctotal == 4\n\n    def test_empty_query_n_response_no_changes(self):\n        item = self.add_item_fixture(\n            year=2016, day=13, month=3, comments=\"test comment\"\n        )\n        item.write()\n        item_id = item.id\n        with self.configure_plugin(\n            {\"fields\": [\"comments\"], \"update_database\": True, \"auto\": False}\n        ):\n            self.io.addinput(\"n\")\n            self.run_command(\"zero\")\n\n        mf = MediaFile(syspath(item.path))\n        item = self.lib.get_item(item_id)\n\n        assert item[\"year\"] == 2016\n        assert mf.year == 2016\n        assert mf.comments == \"test comment\"\n        assert item[\"comments\"] == \"test comment\"\n"
  },
  {
    "path": "test/plugins/utils/__init__.py",
    "content": ""
  },
  {
    "path": "test/plugins/utils/test_musicbrainz.py",
    "content": "import pytest\n\nfrom beetsplug._utils.musicbrainz import MusicBrainzAPI\n\n\ndef test_group_relations():\n    raw_release = {\n        \"id\": \"r1\",\n        \"relations\": [\n            {\"target-type\": \"artist\", \"type\": \"vocal\", \"name\": \"A\"},\n            {\"target-type\": \"url\", \"type\": \"streaming\", \"url\": \"http://s\"},\n            {\"target-type\": \"url\", \"type\": \"purchase\", \"url\": \"http://p\"},\n            {\n                \"target-type\": \"work\",\n                \"type\": \"performance\",\n                \"work\": {\n                    \"relations\": [\n                        {\n                            \"artist\": {\"name\": \"幾田りら\"},\n                            \"target-type\": \"artist\",\n                            \"type\": \"composer\",\n                        },\n                        {\n                            \"target-type\": \"url\",\n                            \"type\": \"lyrics\",\n                            \"url\": {\n                                \"resource\": \"https://utaten.com/lyric/tt24121002/\"\n                            },\n                        },\n                        {\n                            \"artist\": {\"name\": \"幾田りら\"},\n                            \"target-type\": \"artist\",\n                            \"type\": \"lyricist\",\n                        },\n                        {\n                            \"target-type\": \"url\",\n                            \"type\": \"lyrics\",\n                            \"url\": {\n                                \"resource\": \"https://www.uta-net.com/song/366579/\"\n                            },\n                        },\n                    ],\n                    \"title\": \"百花繚乱\",\n                    \"type\": \"Song\",\n                },\n            },\n        ],\n    }\n\n    assert MusicBrainzAPI._group_relations(raw_release) == {\n        \"id\": \"r1\",\n        \"artist-relations\": [{\"type\": \"vocal\", \"name\": \"A\"}],\n        \"url-relations\": [\n            {\"type\": \"streaming\", \"url\": \"http://s\"},\n            {\"type\": \"purchase\", \"url\": \"http://p\"},\n        ],\n        \"work-relations\": [\n            {\n                \"type\": \"performance\",\n                \"work\": {\n                    \"artist-relations\": [\n                        {\"type\": \"composer\", \"artist\": {\"name\": \"幾田りら\"}},\n                        {\"type\": \"lyricist\", \"artist\": {\"name\": \"幾田りら\"}},\n                    ],\n                    \"url-relations\": [\n                        {\n                            \"type\": \"lyrics\",\n                            \"url\": {\n                                \"resource\": \"https://utaten.com/lyric/tt24121002/\"\n                            },\n                        },\n                        {\n                            \"type\": \"lyrics\",\n                            \"url\": {\n                                \"resource\": \"https://www.uta-net.com/song/366579/\"\n                            },\n                        },\n                    ],\n                    \"title\": \"百花繚乱\",\n                    \"type\": \"Song\",\n                },\n            },\n        ],\n    }\n\n\n@pytest.mark.parametrize(\n    \"field, term, expected\",\n    [\n        (\"artist\", '  AC/DC + \"[Live]\"  ', r\"artist:(ac\\/dc \\+ \\\"\\[live\\]\\\")\"),\n        (\"\", \"Foo:Bar\", r\"foo\\:bar\"),\n        (\"artist\", \"   \", \"\"),\n    ],\n)\ndef test_format_search_term(field, term, expected):\n    assert MusicBrainzAPI.format_search_term(field, term) == expected\n"
  },
  {
    "path": "test/plugins/utils/test_vfs.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for the virtual filesystem builder..\"\"\"\n\nfrom beets.test import _common\nfrom beets.test.helper import BeetsTestCase\nfrom beetsplug._utils import vfs\n\n\nclass VFSTest(BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n        self.lib.path_formats = [\n            (\"default\", \"albums/$album/$title\"),\n            (\"singleton:true\", \"tracks/$artist/$title\"),\n        ]\n        self.lib.add(_common.item())\n        self.lib.add_album([_common.item()])\n        self.tree = vfs.libtree(self.lib)\n\n    def test_singleton_item(self):\n        assert (\n            self.tree.dirs[\"tracks\"].dirs[\"the artist\"].files[\"the title\"] == 1\n        )\n\n    def test_album_item(self):\n        assert (\n            self.tree.dirs[\"albums\"].dirs[\"the album\"].files[\"the title\"] == 2\n        )\n"
  },
  {
    "path": "test/rsrc/acousticbrainz/data.json",
    "content": "{\n    \"tonal\":{\n        \"thpcp\":[\n            1,\n            0.638657510281,\n            0.293813556433,\n            0.259863913059,\n            0.21968896687,\n            0.218203336,\n            0.252398610115,\n            0.22969686985,\n            0.447383195162,\n            0.749422073364,\n            0.580664932728,\n            0.310822367668,\n            0.238883554935,\n            0.178785249591,\n            0.194924846292,\n            0.299323320389,\n            0.282649427652,\n            0.18946044147,\n            0.181915551424,\n            0.231100782752,\n            0.554247200489,\n            0.831909179688,\n            0.589426040649,\n            0.387799620628,\n            0.422363936901,\n            0.429372549057,\n            0.408978521824,\n            0.326897829771,\n            0.266663640738,\n            0.429461866617,\n            0.633336126804,\n            0.477401226759,\n            0.261826515198,\n            0.238164439797,\n            0.287726253271,\n            0.690547764301\n        ],\n        \"chords_number_rate\":0.00194468453992,\n        \"chords_scale\":\"minor\",\n        \"chords_changes_rate\":0.0445116683841,\n        \"key_strength\":0.636936545372,\n        \"tuning_diatonic_strength\":0.495492935181,\n        \"hpcp_entropy\":{\n            \"min\":0,\n            \"max\":4.48086500168,\n            \"dvar2\":0.867867648602,\n            \"median\":2.02990412712,\n            \"dmean2\":1.14721953869,\n            \"dmean\":0.68769723177,\n            \"var\":0.635742008686,\n            \"dvar\":0.33780092001,\n            \"mean\":2.00384068489\n        },\n        \"key_scale\":\"minor\",\n        \"chords_strength\":{\n            \"min\":0.24240244925,\n            \"max\":0.793840110302,\n            \"dvar2\":9.58399032243e-05,\n            \"median\":0.586153388023,\n            \"dmean2\":0.0106231365353,\n            \"dmean\":0.00929547380656,\n            \"var\":0.00910324696451,\n            \"dvar\":6.61800950184e-05,\n            \"mean\":0.576524615288\n        },\n        \"key_key\":\"A\",\n        \"tuning_nontempered_energy_ratio\":0.721719145775,\n        \"tuning_equal_tempered_deviation\":0.0515233427286,\n        \"chords_histogram\":[\n            56.2445983887,\n            8.10285186768,\n            1.79343128204,\n            0.0864304229617,\n            0,\n            0.605012953281,\n            2.20397591591,\n            12.1650819778,\n            0.0216076057404,\n            0.0216076057404,\n            0,\n            0,\n            0,\n            0,\n            0,\n            0,\n            2.67934322357,\n            0.21607606113,\n            10.8686256409,\n            0,\n            2.07433009148,\n            0.0864304229617,\n            0.648228168488,\n            2.1823682785\n        ],\n        \"chords_key\":\"A\",\n        \"tuning_frequency\":441.272583008,\n        \"hpcp\":{\n            \"min\":[\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0,\n                0\n            ],\n            \"max\":[\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1,\n                1\n            ],\n            \"dvar2\":[\n                0.159377709031,\n                0.118723139167,\n                0.0969077348709,\n                0.0841393470764,\n                0.0857475027442,\n                0.0681946650147,\n                0.0922033339739,\n                0.0763805955648,\n                0.08953332901,\n                0.134413808584,\n                0.117157392204,\n                0.0784328207374,\n                0.0576078519225,\n                0.0540019907057,\n                0.0537950210273,\n                0.0788798704743,\n                0.0758254900575,\n                0.0659088715911,\n                0.0595937520266,\n                0.0897909551859,\n                0.117696471512,\n                0.141075149179,\n                0.116812512279,\n                0.143778041005,\n                0.157332316041,\n                0.206293225288,\n                0.187901929021,\n                0.16186593473,\n                0.119209326804,\n                0.107413217425,\n                0.119033068419,\n                0.101279519498,\n                0.102868333459,\n                0.108767814934,\n                0.105039291084,\n                0.133028581738\n            ],\n            \"median\":[\n                0.200053632259,\n                0.138681918383,\n                0.0352100357413,\n                0.0198299698532,\n                0.0150980614126,\n                0.0205171480775,\n                0.026256872341,\n                0.0286095552146,\n                0.0424337461591,\n                0.0602139532566,\n                0.0744276791811,\n                0.0494470596313,\n                0.0350396670401,\n                0.0212194472551,\n                0.0196186862886,\n                0.0201223343611,\n                0.0170610919595,\n                0.0120322443545,\n                0.0105668697506,\n                0.0191436801106,\n                0.0846247002482,\n                0.135070502758,\n                0.113241240382,\n                0.0424886606634,\n                0.0378469713032,\n                0.02946896106,\n                0.0286043733358,\n                0.0198593121022,\n                0.0253958441317,\n                0.0672319456935,\n                0.0957452505827,\n                0.0639808028936,\n                0.026175301522,\n                0.0180807597935,\n                0.0324410535395,\n                0.125212281942\n            ],\n            \"dmean2\":[\n                0.353859990835,\n                0.294070899487,\n                0.217189013958,\n                0.180236533284,\n                0.162956178188,\n                0.144071772695,\n                0.172789543867,\n                0.171253859997,\n                0.22391949594,\n                0.284728109837,\n                0.274570196867,\n                0.198656648397,\n                0.154297873378,\n                0.124744221568,\n                0.124510832131,\n                0.157537952065,\n                0.158150732517,\n                0.135058999062,\n                0.123808719218,\n                0.177607372403,\n                0.277576059103,\n                0.324962347746,\n                0.298212528229,\n                0.308668285608,\n                0.311420857906,\n                0.344731658697,\n                0.335885316133,\n                0.283822774887,\n                0.227637752891,\n                0.252195805311,\n                0.288448363543,\n                0.249307155609,\n                0.212308287621,\n                0.205825775862,\n                0.223324626684,\n                0.315198987722\n            ],\n            \"dmean\":[\n                0.206140458584,\n                0.16949801147,\n                0.121237404644,\n                0.101308584213,\n                0.0920756608248,\n                0.0820758640766,\n                0.0983714461327,\n                0.0957674309611,\n                0.130757495761,\n                0.167563140392,\n                0.159212738276,\n                0.113431841135,\n                0.0875298455358,\n                0.0698468312621,\n                0.0709747001529,\n                0.092557400465,\n                0.0927894487977,\n                0.0761438235641,\n                0.0697580873966,\n                0.0992580577731,\n                0.162719354033,\n                0.194310605526,\n                0.174188151956,\n                0.171439617872,\n                0.174783751369,\n                0.191449582577,\n                0.185274213552,\n                0.155063658953,\n                0.124152831733,\n                0.143824338913,\n                0.167870178819,\n                0.144021183252,\n                0.116702638566,\n                0.112653404474,\n                0.125104308128,\n                0.184472203255\n            ],\n            \"var\":[\n                0.143021538854,\n                0.0637424811721,\n                0.0320195667446,\n                0.037381041795,\n                0.0286979582161,\n                0.0301742851734,\n                0.0303927082568,\n                0.0223984327167,\n                0.052778493613,\n                0.13409627974,\n                0.0731517747045,\n                0.0302681028843,\n                0.0220995694399,\n                0.0194978509098,\n                0.0208596177399,\n                0.0510722063482,\n                0.0462048053741,\n                0.0236530210823,\n                0.0288583170623,\n                0.0295663233846,\n                0.0644663274288,\n                0.119045428932,\n                0.0609758161008,\n                0.0493184439838,\n                0.0607626289129,\n                0.070556551218,\n                0.0664848089218,\n                0.0493576526642,\n                0.0335861295462,\n                0.0447917319834,\n                0.0859667509794,\n                0.0526875928044,\n                0.0304508917034,\n                0.0315008088946,\n                0.0328483134508,\n                0.0794815197587\n            ],\n            \"dvar\":[\n                0.0686646401882,\n                0.0436623394489,\n                0.0358338914812,\n                0.0332863405347,\n                0.0316647030413,\n                0.027725789696,\n                0.0338032282889,\n                0.0273791830987,\n                0.0345646068454,\n                0.0615020208061,\n                0.0457257218659,\n                0.0297911148518,\n                0.0221415478736,\n                0.0201385207474,\n                0.0201426595449,\n                0.033235758543,\n                0.0318886972964,\n                0.0243560094386,\n                0.0229729004204,\n                0.0327679589391,\n                0.0460740588605,\n                0.0626438856125,\n                0.0435314439237,\n                0.0521778166294,\n                0.0578555390239,\n                0.0772548541427,\n                0.0728902295232,\n                0.05926451087,\n                0.0424739196897,\n                0.0389089211822,\n                0.0483759790659,\n                0.0381603203714,\n                0.036982499063,\n                0.0398408733308,\n                0.0387278683484,\n                0.0517609193921\n            ],\n            \"mean\":[\n                0.366864055395,\n                0.234300479293,\n                0.107789635658,\n                0.0953347310424,\n                0.080595985055,\n                0.0800509601831,\n                0.0925959795713,\n                0.084267526865,\n                0.164128810167,\n                0.274936020374,\n                0.213025093079,\n                0.114029549062,\n                0.0876377895474,\n                0.0655898824334,\n                0.0715109184384,\n                0.109810970724,\n                0.103693917394,\n                0.0695062279701,\n                0.0667382776737,\n                0.0847825706005,\n                0.203333377838,\n                0.305197566748,\n                0.216239228845,\n                0.142269745469,\n                0.154950141907,\n                0.157521352172,\n                0.15003952384,\n                0.119927063584,\n                0.0978293046355,\n                0.157554119825,\n                0.232348263264,\n                0.175141349435,\n                0.0960547402501,\n                0.0873739719391,\n                0.105556420982,\n                0.253337144852\n            ]\n        }\n    },\n    \"rhythm\":{\n        \"bpm_histogram_second_peak_bpm\":{\n            \"min\":167,\n            \"max\":167,\n            \"dvar2\":0,\n            \"median\":167,\n            \"dmean2\":0,\n            \"dmean\":0,\n            \"var\":0,\n            \"dvar\":0,\n            \"mean\":167\n        },\n        \"bpm_histogram_second_peak_spread\":{\n            \"min\":0,\n            \"max\":0,\n            \"dvar2\":0,\n            \"median\":0,\n            \"dmean2\":0,\n            \"dmean\":0,\n            \"var\":0,\n            \"dvar\":0,\n            \"mean\":0\n        },\n        \"beats_count\":577,\n        \"beats_loudness\":{\n            \"min\":4.48232695405e-09,\n            \"max\":0.181520029902,\n            \"dvar2\":0.00434844521806,\n            \"median\":0.0302296206355,\n            \"dmean2\":0.0709233134985,\n            \"dmean\":0.0362482257187,\n            \"var\":0.0014705004869,\n            \"dvar\":0.00124853104353,\n            \"mean\":0.0407853461802\n        },\n        \"bpm\":162.532119751,\n        \"bpm_histogram_first_peak_spread\":{\n            \"min\":0.164835140109,\n            \"max\":0.164835140109,\n            \"dvar2\":0,\n            \"median\":0.164835140109,\n            \"dmean2\":0,\n            \"dmean\":0,\n            \"var\":0,\n            \"dvar\":0,\n            \"mean\":0.164835140109\n        },\n        \"danceability\":1.14192211628,\n        \"bpm_histogram_first_peak_bpm\":{\n            \"min\":161,\n            \"max\":161,\n            \"dvar2\":0,\n            \"median\":161,\n            \"dmean2\":0,\n            \"dmean\":0,\n            \"var\":0,\n            \"dvar\":0,\n            \"mean\":161\n        },\n        \"beats_loudness_band_ratio\":{\n            \"min\":[\n                0.00683269277215,\n                0.00988945644349,\n                0.00177479430567,\n                0.000523661612533,\n                0.000248342636041,\n                0.00070228939876\n            ],\n            \"max\":[\n                0.970164954662,\n                0.725397408009,\n                0.739950060844,\n                0.658194899559,\n                0.676319360733,\n                0.622089266777\n            ],\n            \"dvar2\":[\n                0.0699004009366,\n                0.0290632098913,\n                0.035205449909,\n                0.0226975940168,\n                0.0168868545443,\n                0.0086750369519\n            ],\n            \"median\":[\n                0.619332075119,\n                0.176913484931,\n                0.0926762372255,\n                0.0303400773555,\n                0.0398489944637,\n                0.0262520890683\n            ],\n            \"dmean2\":[\n                0.369491040707,\n                0.230186283588,\n                0.192849993706,\n                0.106605380774,\n                0.107576675713,\n                0.0616580061615\n            ],\n            \"dmean\":[\n                0.207789510489,\n                0.129003107548,\n                0.110624760389,\n                0.0578341074288,\n                0.0602345280349,\n                0.035002540797\n            ],\n            \"var\":[\n                0.0565228238702,\n                0.0165011454374,\n                0.0193302389234,\n                0.0086074648425,\n                0.00634109321982,\n                0.00296737346798\n            ],\n            \"dvar\":[\n                0.0250691790134,\n                0.00962078291923,\n                0.0126609709114,\n                0.00782771967351,\n                0.00567667419091,\n                0.00305109703913\n            ],\n            \"mean\":[\n                0.577607989311,\n                0.195795580745,\n                0.138790622354,\n                0.0610357522964,\n                0.065284781158,\n                0.04240244627\n            ]\n        },\n        \"onset_rate\":5.17941665649,\n        \"beats_position\":[\n            0.383129239082,\n            0.789478421211,\n            1.16099774837,\n            1.53251695633,\n            1.90403628349,\n            2.27555561066,\n            2.6470746994,\n            3.01859402657,\n            3.39011335373,\n            3.76163268089,\n            4.13315200806,\n            4.5046710968,\n            4.87619018555,\n            5.24770975113,\n            5.60761880875,\n            5.9675283432,\n            6.33904743195,\n            6.71056699753,\n            7.08208608627,\n            7.45360517502,\n            7.81351470947,\n            8.17342376709,\n            8.54494285583,\n            8.91646194458,\n            9.287981987,\n            9.64789104462,\n            10.0194101334,\n            10.379319191,\n            10.7508392334,\n            11.110748291,\n            11.4822673798,\n            11.8421764374,\n            12.2136955261,\n            12.5852155685,\n            12.9567346573,\n            13.3166437149,\n            13.6765527725,\n            14.0364627838,\n            14.3963718414,\n            14.7678909302,\n            15.1394100189,\n            15.5109291077,\n            15.870839119,\n            16.2307472229,\n            16.602268219,\n            16.9621772766,\n            17.3220863342,\n            17.693605423,\n            18.0535144806,\n            18.4134235382,\n            18.7733325958,\n            19.1332416534,\n            19.4931507111,\n            19.853061676,\n            20.2245807648,\n            20.5844898224,\n            20.9560089111,\n            21.3159179688,\n            21.6874370575,\n            22.0473461151,\n            22.4072551727,\n            22.7787742615,\n            23.1386852264,\n            23.4985942841,\n            23.8701133728,\n            24.2416324615,\n            24.6131515503,\n            24.984670639,\n            25.3561897278,\n            25.7277088165,\n            26.0992279053,\n            26.4591369629,\n            26.830657959,\n            27.2021770477,\n            27.5736961365,\n            27.9452152252,\n            28.316734314,\n            28.6766433716,\n            29.0481624603,\n            29.4196815491,\n            29.7912006378,\n            30.1627197266,\n            30.5342407227,\n            30.9057598114,\n            31.265668869,\n            31.6255779266,\n            31.9854869843,\n            32.3453979492,\n            32.6704750061,\n            33.0071640015,\n            33.3554649353,\n            33.7153739929,\n            34.0868911743,\n            34.4351921082,\n            34.8067131042,\n            35.1898422241,\n            35.5961914062,\n            35.9793205261,\n            36.362449646,\n            36.7339668274,\n            37.1054878235,\n            37.4886169434,\n            37.8601341248,\n            38.2316551208,\n            38.6147842407,\n            38.9863014221,\n            39.3578224182,\n            39.7293434143,\n            40.1124725342,\n            40.4723815918,\n            40.8438987732,\n            41.2154197693,\n            41.5869369507,\n            41.9584579468,\n            42.3299751282,\n            42.7014961243,\n            43.0730133057,\n            43.4445343018,\n            43.8160552979,\n            44.1875724792,\n            44.5590934753,\n            44.9306106567,\n            45.3021316528,\n            45.6736488342,\n            46.0335578918,\n            46.3934669495,\n            46.7533760071,\n            47.1132888794,\n            47.4848060608,\n            47.8563270569,\n            48.2394561768,\n            48.6225852966,\n            48.994102478,\n            49.3656234741,\n            49.748752594,\n            50.1318817139,\n            50.5033988953,\n            50.8749198914,\n            51.2580490112,\n            51.6411781311,\n            52.0126991272,\n            52.3842163086,\n            52.7557373047,\n            53.1272544861,\n            53.4987754822,\n            53.8702926636,\n            54.2534217834,\n            54.6365509033,\n            55.0080718994,\n            55.3795890808,\n            55.7511100769,\n            56.122631073,\n            56.4941482544,\n            56.8656692505,\n            57.2487983704,\n            57.6203155518,\n            58.0034446716,\n            58.3749656677,\n            58.7464828491,\n            59.1180038452,\n            59.4895210266,\n            59.8610420227,\n            60.2325630188,\n            60.6040802002,\n            60.9756011963,\n            61.3471183777,\n            61.7186393738,\n            62.0901565552,\n            62.4616775513,\n            62.8331947327,\n            63.2163238525,\n            63.5994529724,\n            63.9709739685,\n            64.3424911499,\n            64.714012146,\n            65.0855331421,\n            65.4570541382,\n            65.8285675049,\n            66.200088501,\n            66.5716094971,\n            66.9431304932,\n            67.3146438599,\n            67.686164856,\n            68.0344696045,\n            68.4175949097,\n            68.8007278442,\n            69.1722412109,\n            69.5321502686,\n            69.8920593262,\n            70.2635803223,\n            70.6351013184,\n            70.995010376,\n            71.3549194336,\n            71.7264404297,\n            72.1095657349,\n            72.481086731,\n            72.8642196655,\n            73.2357330322,\n            73.6072540283,\n            73.9787750244,\n            74.3502960205,\n            74.7218093872,\n            75.1049423218,\n            75.488067627,\n            75.859588623,\n            76.2311096191,\n            76.6026306152,\n            76.9741516113,\n            77.345664978,\n            77.7171859741,\n            78.1003189087,\n            78.4834442139,\n            78.85496521,\n            79.2264862061,\n            79.5979995728,\n            79.9695205688,\n            80.3410415649,\n            80.712562561,\n            81.0840835571,\n            81.4555969238,\n            81.8271179199,\n            82.198638916,\n            82.5701599121,\n            82.9416732788,\n            83.3131942749,\n            83.684715271,\n            84.0678405762,\n            84.4509735107,\n            84.8224945068,\n            85.1824035645,\n            85.5423126221,\n            85.9138336182,\n            86.2853469849,\n            86.656867981,\n            87.0283889771,\n            87.3999099731,\n            87.7714233398,\n            88.1429443359,\n            88.514465332,\n            88.8859863281,\n            89.2575073242,\n            89.6290206909,\n            89.9889297485,\n            90.3488388062,\n            90.7203598022,\n            91.0918807983,\n            91.451789856,\n            91.8116989136,\n            92.1832199097,\n            92.5547409058,\n            92.9262542725,\n            93.2861633301,\n            93.6460723877,\n            94.0175933838,\n            94.3891143799,\n            94.760635376,\n            95.1205444336,\n            95.4804534912,\n            95.8403625488,\n            96.2002716064,\n            96.5717926025,\n            96.9433059692,\n            97.3148269653,\n            97.6863479614,\n            98.0578689575,\n            98.4293823242,\n            98.8009033203,\n            99.1724243164,\n            99.532333374,\n            99.9038543701,\n            100.263763428,\n            100.635284424,\n            101.006797791,\n            101.378318787,\n            101.738227844,\n            102.098136902,\n            102.469657898,\n            102.841178894,\n            103.21269989,\n            103.584213257,\n            103.944122314,\n            104.304031372,\n            104.675552368,\n            105.047073364,\n            105.406982422,\n            105.766891479,\n            106.138412476,\n            106.509933472,\n            106.881446838,\n            107.252967834,\n            107.624488831,\n            107.984397888,\n            108.355918884,\n            108.715827942,\n            109.087341309,\n            109.458862305,\n            109.818771362,\n            110.17868042,\n            110.538589478,\n            110.898498535,\n            111.270019531,\n            111.641540527,\n            112.001449585,\n            112.361358643,\n            112.732879639,\n            113.104400635,\n            113.464309692,\n            113.82421875,\n            114.184127808,\n            114.544036865,\n            114.915550232,\n            115.287071228,\n            115.658592224,\n            116.006889343,\n            116.366798401,\n            116.726707458,\n            117.086616516,\n            117.446525574,\n            117.794830322,\n            118.15473938,\n            118.514648438,\n            118.886169434,\n            119.269294739,\n            119.652427673,\n            120.035552979,\n            120.418685913,\n            120.813423157,\n            121.2081604,\n            121.602897644,\n            121.997642517,\n            122.380767822,\n            122.763900757,\n            123.147026062,\n            123.530158997,\n            123.92489624,\n            124.319633484,\n            124.69115448,\n            125.085891724,\n            125.480628967,\n            125.863761902,\n            126.246894836,\n            126.630020142,\n            127.013153076,\n            127.396278381,\n            127.779411316,\n            128.17414856,\n            128.568893433,\n            128.963623047,\n            129.346755981,\n            129.729888916,\n            130.113006592,\n            130.496139526,\n            130.867660522,\n            131.239181519,\n            131.610702515,\n            131.982223511,\n            132.353744507,\n            132.725265503,\n            133.09677124,\n            133.456680298,\n            133.816589355,\n            134.188110352,\n            134.559631348,\n            134.931152344,\n            135.30267334,\n            135.674194336,\n            136.045715332,\n            136.417236328,\n            136.788757324,\n            137.160263062,\n            137.520172119,\n            137.880081177,\n            138.251602173,\n            138.623123169,\n            138.994644165,\n            139.366165161,\n            139.737686157,\n            140.109207153,\n            140.480728149,\n            140.852249146,\n            141.223754883,\n            141.595275879,\n            141.943572998,\n            142.315093994,\n            142.675003052,\n            143.034912109,\n            143.406433105,\n            143.777954102,\n            144.149475098,\n            144.520996094,\n            144.89251709,\n            145.264038086,\n            145.635559082,\n            146.007064819,\n            146.378585815,\n            146.750106812,\n            147.121627808,\n            147.493148804,\n            147.8646698,\n            148.236190796,\n            148.607711792,\n            148.979232788,\n            149.362350464,\n            149.745483398,\n            150.117004395,\n            150.488525391,\n            150.860046387,\n            151.231567383,\n            151.603088379,\n            151.974594116,\n            152.346115112,\n            152.717636108,\n            153.089157104,\n            153.460678101,\n            153.832199097,\n            154.203720093,\n            154.575241089,\n            154.946762085,\n            155.318267822,\n            155.689788818,\n            156.061309814,\n            156.432830811,\n            156.804351807,\n            157.175872803,\n            157.547393799,\n            157.918914795,\n            158.290420532,\n            158.661941528,\n            159.033462524,\n            159.404983521,\n            159.776504517,\n            160.148025513,\n            160.519546509,\n            160.891067505,\n            161.262588501,\n            161.634094238,\n            162.005615234,\n            162.37713623,\n            162.737045288,\n            163.096954346,\n            163.468475342,\n            163.839996338,\n            164.211517334,\n            164.58303833,\n            164.954559326,\n            165.326080322,\n            165.674377441,\n            166.045898438,\n            166.417419434,\n            166.788925171,\n            167.160446167,\n            167.531967163,\n            167.903488159,\n            168.275009155,\n            168.646530151,\n            169.018051147,\n            169.389572144,\n            169.772689819,\n            170.155822754,\n            170.52734375,\n            170.898864746,\n            171.270385742,\n            171.641906738,\n            172.025024414,\n            172.408157349,\n            172.779678345,\n            173.151199341,\n            173.522720337,\n            173.894241333,\n            174.265762329,\n            174.637283325,\n            175.032012939,\n            175.415145874,\n            175.798278809,\n            176.169799805,\n            176.541305542,\n            176.9012146,\n            177.272735596,\n            177.632644653,\n            178.004165649,\n            178.375686646,\n            178.747207642,\n            179.130340576,\n            179.513473511,\n            179.884979248,\n            180.256500244,\n            180.62802124,\n            180.999542236,\n            181.382675171,\n            181.754196167,\n            182.137313843,\n            182.508834839,\n            182.880355835,\n            183.251876831,\n            183.623397827,\n            183.994918823,\n            184.378051758,\n            184.761169434,\n            185.13269043,\n            185.504211426,\n            185.875732422,\n            186.247253418,\n            186.618774414,\n            186.99029541,\n            187.361816406,\n            187.733337402,\n            188.10484314,\n            188.476364136,\n            188.847885132,\n            189.219406128,\n            189.590927124,\n            189.96244812,\n            190.345581055,\n            190.72869873,\n            191.100219727,\n            191.471740723,\n            191.843261719,\n            192.214782715,\n            192.597915649,\n            192.981033325,\n            193.352554321,\n            193.724075317,\n            194.095596313,\n            194.478729248,\n            194.861862183,\n            195.23336792,\n            195.604888916,\n            195.976409912,\n            196.347930908,\n            196.719451904,\n            197.0909729,\n            197.474105835,\n            197.857223511,\n            198.228744507,\n            198.600265503,\n            198.971786499,\n            199.343307495,\n            199.714828491,\n            200.086349487,\n            200.457870483,\n            200.829391479,\n            201.200897217,\n            201.572418213,\n            201.943939209,\n            202.303848267,\n            202.663757324,\n            203.03527832,\n            203.406799316,\n            203.778320312,\n            204.149841309,\n            204.521362305,\n            204.892883301,\n            205.264389038,\n            205.647521973,\n            206.030654907,\n            206.402175903,\n            206.773696899,\n            207.145217896,\n            207.516723633,\n            207.888244629,\n            208.259765625,\n            208.631286621,\n            209.002807617,\n            209.385940552,\n            209.757461548,\n            210.128982544,\n            210.500488281,\n            210.883621216,\n            211.255142212,\n            211.626663208,\n            212.009796143,\n            212.392913818,\n            212.776046753,\n            213.170791626,\n            213.565536499,\n            213.971878052,\n            214.378234863\n        ],\n        \"bpm_histogram_second_peak_weight\":{\n            \"min\":0.163194447756,\n            \"max\":0.163194447756,\n            \"dvar2\":0,\n            \"median\":0.163194447756,\n            \"dmean2\":0,\n            \"dmean\":0,\n            \"var\":0,\n            \"dvar\":0,\n            \"mean\":0.163194447756\n        },\n        \"bpm_histogram_first_peak_weight\":{\n            \"min\":0.659722208977,\n            \"max\":0.659722208977,\n            \"dvar2\":0,\n            \"median\":0.659722208977,\n            \"dmean2\":0,\n            \"dmean\":0,\n            \"var\":0,\n            \"dvar\":0,\n            \"mean\":0.659722208977\n        }\n    },\n    \"lowlevel\":{\n        \"spectral_complexity\":{\n            \"min\":0,\n            \"max\":51,\n            \"dvar2\":34.109500885,\n            \"median\":15,\n            \"dmean2\":5.03252983093,\n            \"dmean\":3.31208133698,\n            \"var\":108.135475159,\n            \"dvar\":16.1623840332,\n            \"mean\":15.1260938644\n        },\n        \"silence_rate_20dB\":{\n            \"min\":1,\n            \"max\":1,\n            \"dvar2\":0,\n            \"median\":1,\n            \"dmean2\":0,\n            \"dmean\":0,\n            \"var\":0,\n            \"dvar\":0,\n            \"mean\":1\n        },\n        \"average_loudness\":0.815025985241,\n        \"erbbands_spread\":{\n            \"min\":0.907018482685,\n            \"max\":163.113571167,\n            \"dvar2\":511.106719971,\n            \"median\":34.9861183167,\n            \"dmean2\":17.4016342163,\n            \"dmean\":11.4227361679,\n            \"var\":572.351501465,\n            \"dvar\":220.769592285,\n            \"mean\":39.7502365112\n        },\n        \"spectral_kurtosis\":{\n            \"min\":-1.22816824913,\n            \"max\":77.2247085571,\n            \"dvar2\":28.6044521332,\n            \"median\":4.62249898911,\n            \"dmean2\":4.51546525955,\n            \"dmean\":2.82785224915,\n            \"var\":33.1767959595,\n            \"dvar\":13.1654214859,\n            \"mean\":6.00768327713\n        },\n        \"barkbands_kurtosis\":{\n            \"min\":-1.96124887466,\n            \"max\":732.31427002,\n            \"dvar2\":588.577392578,\n            \"median\":2.02856016159,\n            \"dmean2\":8.05669307709,\n            \"dmean\":5.11299228668,\n            \"var\":374.321380615,\n            \"dvar\":247.842544556,\n            \"mean\":7.06718301773\n        },\n        \"spectral_strongpeak\":{\n            \"min\":1.71490974199e-10,\n            \"max\":16.1200447083,\n            \"dvar2\":3.3518705368,\n            \"median\":0.39955753088,\n            \"dmean2\":0.941311240196,\n            \"dmean\":0.545010268688,\n            \"var\":2.96148967743,\n            \"dvar\":1.33243703842,\n            \"mean\":0.93408870697\n        },\n        \"spectral_spread\":{\n            \"min\":1388334.875,\n            \"max\":41575076,\n            \"dvar2\":1619247235070.0,\n            \"median\":3420763.25,\n            \"dmean2\":983456.875,\n            \"dmean\":732694.4375,\n            \"var\":4945877663740.0,\n            \"dvar\":854983901184,\n            \"mean\":4104934.25\n        },\n        \"spectral_rms\":{\n            \"min\":3.15950651059e-12,\n            \"max\":0.0132316453382,\n            \"dvar2\":3.71212809114e-06,\n            \"median\":0.00425734650344,\n            \"dmean2\":0.0013317943085,\n            \"dmean\":0.000880118692294,\n            \"var\":5.96411746301e-06,\n            \"dvar\":1.66359927789e-06,\n            \"mean\":0.0039903158322\n        },\n        \"erbbands\":{\n            \"min\":[\n                1.77816177391e-22,\n                1.16505097519e-21,\n                1.25622764484e-20,\n                2.42817334007e-20,\n                1.54258117469e-20,\n                1.0398112018e-19,\n                1.45132858053e-19,\n                2.45289269564e-19,\n                4.01068342766e-19,\n                8.21050273636e-19,\n                3.81530310848e-19,\n                8.81511903137e-19,\n                1.07499647945e-18,\n                8.4783438187e-19,\n                1.00474413342e-18,\n                2.63416224414e-18,\n                2.7020495774e-18,\n                3.62243276547e-18,\n                3.19863277569e-18,\n                3.43176763428e-18,\n                6.46998299903e-18,\n                5.15079866686e-18,\n                8.52004633608e-18,\n                1.02257547977e-17,\n                1.00997123247e-17,\n                1.05472856921e-17,\n                1.00685656659e-17,\n                1.10003920706e-17,\n                1.51110777052e-17,\n                1.5046216819e-17,\n                1.31580720189e-17,\n                1.30844504628e-17,\n                1.13508668418e-17,\n                1.02681982621e-17,\n                8.04728449187e-18,\n                4.73097927698e-18,\n                2.75997186582e-18,\n                1.08864568334e-18,\n                2.8601904839e-19,\n                3.5584382605e-20\n            ],\n            \"max\":[\n                2.05614852905,\n                10.580906868,\n                60.5060195923,\n                152.843505859,\n                450.042755127,\n                175.338409424,\n                237.807418823,\n                238.884460449,\n                443.897521973,\n                525.930541992,\n                518.611022949,\n                1031.67468262,\n                954.232910156,\n                1123.45935059,\n                556.857299805,\n                356.125640869,\n                342.635467529,\n                1293.60327148,\n                1103.80981445,\n                1287.36242676,\n                2169.76928711,\n                1266.18579102,\n                601.754516602,\n                1258.96850586,\n                444.097320557,\n                240.62600708,\n                303.504180908,\n                56.5521354675,\n                72.2374343872,\n                85.4121780396,\n                25.407957077,\n                19.8146400452,\n                46.2547264099,\n                16.3226490021,\n                16.8903636932,\n                5.15064907074,\n                0.893250703812,\n                0.284754753113,\n                0.106889992952,\n                0.00452945847064\n            ],\n            \"dvar2\":[\n                0.0137821119279,\n                4.26913785934,\n                97.7259292603,\n                622.611999512,\n                3656.42504883,\n                376.053527832,\n                307.56942749,\n                320.293243408,\n                2445.90405273,\n                1300.15063477,\n                729.621582031,\n                2693.6159668,\n                2013.25512695,\n                3353.13598633,\n                573.952941895,\n                269.22253418,\n                457.528259277,\n                1406.78710938,\n                1557.82128906,\n                793.165527344,\n                5019.24316406,\n                4039.60839844,\n                970.312316895,\n                1538.21313477,\n                721.875244141,\n                195.41003418,\n                88.4793777466,\n                31.4306335449,\n                42.7237434387,\n                32.8801956177,\n                6.04226541519,\n                1.52690029144,\n                1.47338938713,\n                0.542356073856,\n                0.211627364159,\n                0.0269440039992,\n                0.00465576630086,\n                0.000269315525657,\n                1.98406451091e-05,\n                4.38383267465e-08\n            ],\n            \"median\":[\n                0.00466703856364,\n                0.101253904402,\n                1.64267027378,\n                5.13034963608,\n                6.49109458923,\n                4.258228302,\n                3.18711066246,\n                4.03815889359,\n                7.81102657318,\n                7.54452610016,\n                5.75877952576,\n                8.21393203735,\n                11.6316785812,\n                9.69087696075,\n                4.23464298248,\n                1.69191777706,\n                2.67072510719,\n                4.43968248367,\n                4.24516201019,\n                7.75287914276,\n                16.0199928284,\n                12.5332527161,\n                9.28721427917,\n                9.18504428864,\n                6.87661838531,\n                4.15781497955,\n                3.11850094795,\n                0.707400023937,\n                0.422007799149,\n                0.326348185539,\n                0.171605303884,\n                0.0764799118042,\n                0.0530340671539,\n                0.0369812250137,\n                0.0170799326152,\n                0.00388540537097,\n                0.000607917027082,\n                0.000106923354906,\n                2.54503829638e-05,\n                2.41674279096e-05\n            ],\n            \"dmean2\":[\n                0.0468679144979,\n                1.00080478191,\n                4.60071754456,\n                11.530172348,\n                27.0302009583,\n                10.1726131439,\n                8.18716049194,\n                9.02602672577,\n                21.6493034363,\n                17.5054721832,\n                11.9851293564,\n                23.2072429657,\n                23.1678962708,\n                24.8249855042,\n                9.62737751007,\n                5.9968290329,\n                8.08820819855,\n                12.5483970642,\n                11.0777654648,\n                12.6770420074,\n                31.1724281311,\n                22.2209968567,\n                15.6441850662,\n                17.6994724274,\n                11.4936075211,\n                6.59135293961,\n                5.08022689819,\n                2.30506944656,\n                2.95904040337,\n                2.46585559845,\n                1.15297198296,\n                0.531184136868,\n                0.382049500942,\n                0.275081396103,\n                0.141834661365,\n                0.0498343594372,\n                0.0225136969239,\n                0.00503297196701,\n                0.000977287418209,\n                5.82068387303e-05\n            ],\n            \"dmean\":[\n                0.0350424982607,\n                0.580041825771,\n                2.61035442352,\n                6.80757236481,\n                16.318693161,\n                6.28967905045,\n                5.23286628723,\n                5.70099258423,\n                13.4230766296,\n                10.8768568039,\n                7.74926900864,\n                15.0034389496,\n                15.0271100998,\n                16.6018199921,\n                6.02632713318,\n                3.79855132103,\n                5.38237333298,\n                8.43670463562,\n                7.43314123154,\n                8.2191324234,\n                20.6921386719,\n                13.7538080215,\n                9.77100944519,\n                11.3139772415,\n                7.25735664368,\n                4.1218457222,\n                3.17433810234,\n                1.45581579208,\n                1.87883663177,\n                1.61556243896,\n                0.779599308968,\n                0.371349811554,\n                0.264476120472,\n                0.193502560258,\n                0.100708886981,\n                0.0353026203811,\n                0.0144239943475,\n                0.00325388251804,\n                0.000670222449116,\n                3.95594870497e-05\n            ],\n            \"var\":[\n                0.0116662262008,\n                1.27503490448,\n                25.6204109192,\n                206.069885254,\n                1430.30371094,\n                234.442382812,\n                206.473526001,\n                288.805267334,\n                1444.05480957,\n                817.272949219,\n                625.690795898,\n                2697.92749023,\n                2397.11669922,\n                7130.13330078,\n                515.86315918,\n                346.633789062,\n                809.448486328,\n                2584.76660156,\n                2616.26757812,\n                3138.03320312,\n                20583.1132812,\n                3381.78515625,\n                1288.27880859,\n                2477.11401367,\n                682.099060059,\n                216.604751587,\n                97.8326339722,\n                13.7823019028,\n                19.9818115234,\n                16.6690158844,\n                4.43811273575,\n                1.2346996069,\n                1.27807950974,\n                0.517578363419,\n                0.177246898413,\n                0.0190207827836,\n                0.00211605615914,\n                0.000168058744748,\n                1.65621640917e-05,\n                3.32350325039e-08\n            ],\n            \"dvar\":[\n                0.00734147289768,\n                1.59334981441,\n                34.9658050537,\n                236.386993408,\n                1519.1640625,\n                157.498901367,\n                126.366111755,\n                127.708183289,\n                921.020629883,\n                497.140319824,\n                303.813903809,\n                1122.60107422,\n                868.916992188,\n                1545.74194336,\n                241.721923828,\n                118.562904358,\n                218.458129883,\n                734.227844238,\n                762.639587402,\n                405.900024414,\n                2714.4777832,\n                1520.69287109,\n                428.032836914,\n                696.146972656,\n                272.255096436,\n                80.3479690552,\n                38.2512245178,\n                12.4188299179,\n                17.8483753204,\n                13.7868928909,\n                2.77658462524,\n                0.731714189053,\n                0.67433899641,\n                0.263227701187,\n                0.108624838293,\n                0.0132785160094,\n                0.00197272375226,\n                0.000125091624795,\n                1.03948887045e-05,\n                2.18264126772e-08\n            ],\n            \"mean\":[\n                0.0470404699445,\n                0.51214581728,\n                2.94917726517,\n                8.56711673737,\n                16.8939933777,\n                9.74743175507,\n                8.58794498444,\n                10.7102499008,\n                23.4056625366,\n                18.6381034851,\n                13.9866991043,\n                27.1086997986,\n                28.1797733307,\n                34.376159668,\n                10.7114057541,\n                6.6013917923,\n                10.7804918289,\n                16.5257949829,\n                15.1251926422,\n                19.3569641113,\n                49.2880210876,\n                27.8654499054,\n                20.180316925,\n                22.9017887115,\n                15.1088733673,\n                8.45034122467,\n                5.79618597031,\n                1.91187810898,\n                2.04077577591,\n                1.82269322872,\n                0.964613378048,\n                0.475511223078,\n                0.359998822212,\n                0.270887732506,\n                0.138232842088,\n                0.0431671403348,\n                0.0138024548069,\n                0.00314460648224,\n                0.000714557303581,\n                5.64505244256e-05\n            ]\n        },\n        \"zerocrossingrate\":{\n            \"min\":0.00244140625,\n            \"max\":0.525390625,\n            \"dvar2\":0.000216632659431,\n            \"median\":0.04736328125,\n            \"dmean2\":0.0121012274176,\n            \"dmean\":0.0100563038141,\n            \"var\":0.00131242838688,\n            \"dvar\":0.000186675912119,\n            \"mean\":0.0536084286869\n        },\n        \"spectral_contrast_coeffs\":{\n            \"min\":[\n                -0.981265366077,\n                -0.957678437233,\n                -0.97364538908,\n                -0.967074155807,\n                -0.96257597208,\n                -0.963937044144\n            ],\n            \"max\":[\n                -0.232828617096,\n                -0.450794398785,\n                -0.464783608913,\n                -0.367899239063,\n                -0.556100070477,\n                -0.624147236347\n            ],\n            \"dvar2\":[\n                0.0063711241819,\n                0.00358002260327,\n                0.00270585925318,\n                0.0017158848932,\n                0.00113679806236,\n                0.00189194327686\n            ],\n            \"median\":[\n                -0.592801511288,\n                -0.736532092094,\n                -0.745844006538,\n                -0.781389117241,\n                -0.797656714916,\n                -0.772757589817\n            ],\n            \"dmean2\":[\n                0.0916584655643,\n                0.074229337275,\n                0.0648957416415,\n                0.0500396862626,\n                0.0380141288042,\n                0.0374387130141\n            ],\n            \"dmean\":[\n                0.0592447705567,\n                0.0465519614518,\n                0.0414465926588,\n                0.0322540737689,\n                0.024834824726,\n                0.0243127625436\n            ],\n            \"var\":[\n                0.00911337323487,\n                0.00655136583373,\n                0.00618056347594,\n                0.00577284349129,\n                0.00380114861764,\n                0.00231571029872\n            ],\n            \"dvar\":[\n                0.00270068016835,\n                0.00148758781143,\n                0.00118501938414,\n                0.000784370582551,\n                0.000524090020917,\n                0.000761664705351\n            ],\n            \"mean\":[\n                -0.594863533974,\n                -0.735448241234,\n                -0.745020091534,\n                -0.775078713894,\n                -0.790849328041,\n                -0.779701292515\n            ]\n        },\n        \"dissonance\":{\n            \"min\":0.155567497015,\n            \"max\":0.500000119209,\n            \"dvar2\":0.0011708199745,\n            \"median\":0.469446510077,\n            \"dmean2\":0.0329371914268,\n            \"dmean\":0.0200165640563,\n            \"var\":0.00120261416305,\n            \"dvar\":0.000480501999846,\n            \"mean\":0.460001558065\n        },\n        \"spectral_energyband_high\":{\n            \"min\":7.11060368084e-21,\n            \"max\":0.00607443926856,\n            \"dvar2\":2.99654345781e-07,\n            \"median\":0.000152749445988,\n            \"dmean2\":0.000292699754937,\n            \"dmean\":0.000196773456992,\n            \"var\":2.3810963512e-07,\n            \"dvar\":1.35860290129e-07,\n            \"mean\":0.000318794423947\n        },\n        \"gfcc\":{\n            \"mean\":[\n                -75.1009368896,\n                108.134063721,\n                -114.922393799,\n                35.9068107605,\n                -53.9456176758,\n                -7.74362421036,\n                -37.9716300964,\n                9.9239988327,\n                -24.244802475,\n                -10.4905223846,\n                -23.3989257812,\n                -9.61326217651,\n                -9.66184806824\n            ],\n            \"icov\":[\n                [\n                    8.00217167125e-05,\n                    -0.00014524954895,\n                    0.000159682429512,\n                    -0.000167002261151,\n                    9.01917956071e-05,\n                    -0.000181695446372,\n                    0.000171542298631,\n                    -0.000129780688439,\n                    0.000197500339709,\n                    -0.000278050283669,\n                    0.000315256824251,\n                    -0.000206819575396,\n                    0.000114160277008\n                ],\n                [\n                    -0.00014524954895,\n                    0.00178359099664,\n                    -0.000247094896622,\n                    0.00111800106242,\n                    -0.000636783661321,\n                    0.000774645654019,\n                    -0.000396145915147,\n                    0.000103466474684,\n                    -0.00112135277595,\n                    0.00229536322877,\n                    -0.00234188255854,\n                    0.00232740258798,\n                    -0.00131408020388\n                ],\n                [\n                    0.000159682429512,\n                    -0.000247094896622,\n                    0.00116660131607,\n                    -0.000234406528762,\n                    0.000154832057888,\n                    -0.00112980930135,\n                    0.000650760543067,\n                    -5.60496459912e-07,\n                    0.000167754784343,\n                    -0.000856044876855,\n                    0.0014209097717,\n                    -0.00128671491984,\n                    0.00078388658585\n                ],\n                [\n                    -0.000167002261151,\n                    0.00111800106242,\n                    -0.000234406528762,\n                    0.00312983314507,\n                    -0.000794405816123,\n                    -0.00058439996792,\n                    2.24104460358e-05,\n                    0.000424961413955,\n                    -0.00152624503244,\n                    0.00239069177769,\n                    -0.00202889670618,\n                    0.00110484787729,\n                    -0.00148455286399\n                ],\n                [\n                    9.01917956071e-05,\n                    -0.000636783661321,\n                    0.000154832057888,\n                    -0.000794405816123,\n                    0.00397709663957,\n                    -0.000828172138426,\n                    -0.000291677570203,\n                    -0.000975954462774,\n                    0.00101370830089,\n                    -0.00107421039138,\n                    -0.000894718454219,\n                    0.00066822959343,\n                    -0.000943991879467\n                ],\n                [\n                    -0.000181695446372,\n                    0.000774645654019,\n                    -0.00112980930135,\n                    -0.00058439996792,\n                    -0.000828172138426,\n                    0.00597975216806,\n                    -0.00177403702401,\n                    -0.000384801533073,\n                    0.00153620564379,\n                    -0.00195281521883,\n                    0.00158954621293,\n                    0.000321330269799,\n                    0.000257747073192\n                ],\n                [\n                    0.000171542298631,\n                    -0.000396145915147,\n                    0.000650760543067,\n                    2.24104460358e-05,\n                    -0.000291677570203,\n                    -0.00177403702401,\n                    0.0080866701901,\n                    -0.00180288043339,\n                    7.330061635e-05,\n                    0.00111615261994,\n                    -0.0011511715129,\n                    -0.000984402606264,\n                    0.000181279159733\n                ],\n                [\n                    -0.000129780688439,\n                    0.000103466474684,\n                    -5.60496459912e-07,\n                    0.000424961413955,\n                    -0.000975954462774,\n                    -0.000384801533073,\n                    -0.00180288043339,\n                    0.00851836241782,\n                    -0.00320849893615,\n                    0.00141180318315,\n                    -0.00124999764375,\n                    0.000833041733131,\n                    -0.00159861764405\n                ],\n                [\n                    0.000197500339709,\n                    -0.00112135277595,\n                    0.000167754784343,\n                    -0.00152624503244,\n                    0.00101370830089,\n                    0.00153620564379,\n                    7.330061635e-05,\n                    -0.00320849893615,\n                    0.0134673845023,\n                    -0.00816399604082,\n                    0.00590222747996,\n                    -0.00234723254107,\n                    -0.000930359237827\n                ],\n                [\n                    -0.000278050283669,\n                    0.00229536322877,\n                    -0.000856044876855,\n                    0.00239069177769,\n                    -0.00107421039138,\n                    -0.00195281521883,\n                    0.00111615261994,\n                    0.00141180318315,\n                    -0.00816399604082,\n                    0.0195522867143,\n                    -0.0122134555131,\n                    0.00327054248191,\n                    0.000577625120059\n                ],\n                [\n                    0.000315256824251,\n                    -0.00234188255854,\n                    0.0014209097717,\n                    -0.00202889670618,\n                    -0.000894718454219,\n                    0.00158954621293,\n                    -0.0011511715129,\n                    -0.00124999764375,\n                    0.00590222747996,\n                    -0.0122134555131,\n                    0.021111080423,\n                    -0.00721056666225,\n                    0.00365835893899\n                ],\n                [\n                    -0.000206819575396,\n                    0.00232740258798,\n                    -0.00128671491984,\n                    0.00110484787729,\n                    0.00066822959343,\n                    0.000321330269799,\n                    -0.000984402606264,\n                    0.000833041733131,\n                    -0.00234723254107,\n                    0.00327054248191,\n                    -0.00721056666225,\n                    0.015932681039,\n                    -0.00565396249294\n                ],\n                [\n                    0.000114160277008,\n                    -0.00131408020388,\n                    0.00078388658585,\n                    -0.00148455286399,\n                    -0.000943991879467,\n                    0.000257747073192,\n                    0.000181279159733,\n                    -0.00159861764405,\n                    -0.000930359237827,\n                    0.000577625120059,\n                    0.00365835893899,\n                    -0.00565396249294,\n                    0.0153731228784\n                ]\n            ],\n            \"cov\":[\n                [\n                    21821.4921875,\n                    1302.12060547,\n                    -2790.62133789,\n                    583.293151855,\n                    63.9840736389,\n                    26.9333572388,\n                    -119.419998169,\n                    277.984161377,\n                    -110.454193115,\n                    -76.0569458008,\n                    -16.7917041779,\n                    -162.89515686,\n                    121.933448792\n                ],\n                [\n                    1302.12060547,\n                    1445.02355957,\n                    -661.351135254,\n                    -311.094665527,\n                    156.139587402,\n                    -306.730987549,\n                    60.9609375,\n                    92.3673324585,\n                    3.47471880913,\n                    -108.692207336,\n                    66.6088409424,\n                    -147.776809692,\n                    75.2410125732\n                ],\n                [\n                    -2790.62133789,\n                    -661.351135254,\n                    1889.47399902,\n                    92.8338088989,\n                    -92.8102874756,\n                    368.803192139,\n                    -78.5678482056,\n                    -95.1785125732,\n                    24.1375102997,\n                    86.254699707,\n                    -94.2749252319,\n                    117.902046204,\n                    -80.0259933472\n                ],\n                [\n                    583.293151855,\n                    -311.094665527,\n                    92.8338088989,\n                    515.187255859,\n                    60.5850448608,\n                    128.642654419,\n                    -2.62003684044,\n                    6.46807575226,\n                    13.7108755112,\n                    -3.07335114479,\n                    -7.74705600739,\n                    28.8581352234,\n                    29.7626171112\n                ],\n                [\n                    63.9840736389,\n                    156.139587402,\n                    -92.8102874756,\n                    60.5850448608,\n                    339.564758301,\n                    28.9691524506,\n                    45.5025405884,\n                    54.3213920593,\n                    -9.63641166687,\n                    12.0036411285,\n                    43.2324829102,\n                    -22.4280166626,\n                    29.361246109\n                ],\n                [\n                    26.9333572388,\n                    -306.730987549,\n                    368.803192139,\n                    128.642654419,\n                    28.9691524506,\n                    324.149993896,\n                    20.1203899384,\n                    -3.79410982132,\n                    -15.0688257217,\n                    43.0340042114,\n                    -26.3511829376,\n                    28.6324996948,\n                    -22.8170604706\n                ],\n                [\n                    -119.419998169,\n                    60.9609375,\n                    -78.5678482056,\n                    -2.62003684044,\n                    45.5025405884,\n                    20.1203899384,\n                    155.001724243,\n                    42.1054992676,\n                    -1.47882866859,\n                    -8.76181983948,\n                    19.2513332367,\n                    2.83816099167,\n                    11.5606393814\n                ],\n                [\n                    277.984161377,\n                    92.3673324585,\n                    -95.1785125732,\n                    6.46807575226,\n                    54.3213920593,\n                    -3.79410982132,\n                    42.1054992676,\n                    155.586120605,\n                    33.8702697754,\n                    -2.8296790123,\n                    9.17068958282,\n                    -6.04741716385,\n                    28.1404056549\n                ],\n                [\n                    -110.454193115,\n                    3.47471880913,\n                    24.1375102997,\n                    13.7108755112,\n                    -9.63641166687,\n                    -15.0688257217,\n                    -1.47882866859,\n                    33.8702697754,\n                    114.661575317,\n                    34.3392486572,\n                    -7.06085252762,\n                    10.0253458023,\n                    15.4272651672\n                ],\n                [\n                    -76.0569458008,\n                    -108.692207336,\n                    86.254699707,\n                    -3.07335114479,\n                    12.0036411285,\n                    43.0340042114,\n                    -8.76181983948,\n                    -2.8296790123,\n                    34.3392486572,\n                    112.38079071,\n                    43.6220817566,\n                    14.6831378937,\n                    -20.7214431763\n                ],\n                [\n                    -16.7917041779,\n                    66.6088409424,\n                    -94.2749252319,\n                    -7.74705600739,\n                    43.2324829102,\n                    -26.3511829376,\n                    19.2513332367,\n                    9.17068958282,\n                    -7.06085252762,\n                    43.6220817566,\n                    99.527053833,\n                    15.1529960632,\n                    -6.4773888588\n                ],\n                [\n                    -162.89515686,\n                    -147.776809692,\n                    117.902046204,\n                    28.8581352234,\n                    -22.4280166626,\n                    28.6324996948,\n                    2.83816099167,\n                    -6.04741716385,\n                    10.0253458023,\n                    14.6831378937,\n                    15.1529960632,\n                    101.876480103,\n                    16.7505264282\n                ],\n                [\n                    121.933448792,\n                    75.2410125732,\n                    -80.0259933472,\n                    29.7626171112,\n                    29.361246109,\n                    -22.8170604706,\n                    11.5606393814,\n                    28.1404056549,\n                    15.4272651672,\n                    -20.7214431763,\n                    -6.4773888588,\n                    16.7505264282,\n                    91.9189758301\n                ]\n            ]\n        },\n        \"spectral_flux\":{\n            \"min\":6.40516528705e-11,\n            \"max\":0.355690479279,\n            \"dvar2\":0.00278266565874,\n            \"median\":0.0625253766775,\n            \"dmean2\":0.0423415489495,\n            \"dmean\":0.0282820314169,\n            \"var\":0.00376007612795,\n            \"dvar\":0.00135926820803,\n            \"mean\":0.0749769806862\n        },\n        \"silence_rate_30dB\":{\n            \"min\":0,\n            \"max\":1,\n            \"dvar2\":0.0354892387986,\n            \"median\":1,\n            \"dmean2\":0.0246406570077,\n            \"dmean\":0.0123189976439,\n            \"var\":0.00654759025201,\n            \"dvar\":0.0121672395617,\n            \"mean\":0.993408977985\n        },\n        \"spectral_energyband_middle_high\":{\n            \"min\":1.27805372214e-21,\n            \"max\":0.0389622859657,\n            \"dvar2\":9.42645056057e-06,\n            \"median\":0.00293085677549,\n            \"dmean2\":0.00200005969964,\n            \"dmean\":0.00133119279053,\n            \"var\":2.5883437047e-05,\n            \"dvar\":4.14980877395e-06,\n            \"mean\":0.00445337453857\n        },\n        \"barkbands_spread\":{\n            \"min\":0.167884364724,\n            \"max\":139.821090698,\n            \"dvar2\":253.175750732,\n            \"median\":18.0125980377,\n            \"dmean2\":10.9470348358,\n            \"dmean\":6.75650119781,\n            \"var\":180.234161377,\n            \"dvar\":101.147232056,\n            \"mean\":20.320306778\n        },\n        \"spectral_centroid\":{\n            \"min\":111.030769348,\n            \"max\":11065.2148438,\n            \"dvar2\":701335.5625,\n            \"median\":1143.22497559,\n            \"dmean2\":567.022827148,\n            \"dmean\":354.07824707,\n            \"var\":626410.0625,\n            \"dvar\":292997.4375,\n            \"mean\":1266.39624023\n        },\n        \"pitch_salience\":{\n            \"min\":0.103756688535,\n            \"max\":0.928133249283,\n            \"dvar2\":0.013650230132,\n            \"median\":0.569122076035,\n            \"dmean2\":0.124187774956,\n            \"dmean\":0.0767409279943,\n            \"var\":0.0128469280899,\n            \"dvar\":0.00529233785346,\n            \"mean\":0.566493272781\n        },\n        \"erbbands_skewness\":{\n            \"min\":-8.09688186646,\n            \"max\":6.17588758469,\n            \"dvar2\":1.00871086121,\n            \"median\":0.283587485552,\n            \"dmean2\":0.786845207214,\n            \"dmean\":0.512230575085,\n            \"var\":1.42545306683,\n            \"dvar\":0.427418738604,\n            \"mean\":0.235957682133\n        },\n        \"erbbands_crest\":{\n            \"min\":2.31988024712,\n            \"max\":34.2482185364,\n            \"dvar2\":16.2832355499,\n            \"median\":8.70411396027,\n            \"dmean2\":4.28936433792,\n            \"dmean\":2.70263242722,\n            \"var\":24.3617858887,\n            \"dvar\":6.85065603256,\n            \"mean\":9.92293930054\n        },\n        \"melbands\":{\n            \"min\":[\n                1.7312522864e-24,\n                4.69779527492e-24,\n                4.46513652814e-24,\n                3.32025441336e-24,\n                6.6967940523e-24,\n                5.54350662457e-24,\n                5.62380004294e-24,\n                7.24197266845e-24,\n                4.37425935743e-24,\n                6.31036856572e-24,\n                4.53679113075e-24,\n                3.08454218323e-24,\n                4.01757944901e-24,\n                5.05271286808e-24,\n                5.25895581858e-24,\n                3.18568913964e-24,\n                7.13208118891e-24,\n                4.5636159514e-24,\n                5.04583400098e-24,\n                6.17228950832e-24,\n                6.96855978959e-24,\n                4.44096109684e-24,\n                6.9472211021e-24,\n                7.30462636813e-24,\n                7.82425851273e-24,\n                7.32260214157e-24,\n                5.87433201125e-24,\n                6.79884662102e-24,\n                6.23345462746e-24,\n                6.36714722381e-24,\n                5.56132344254e-24,\n                7.90887173342e-24,\n                7.08392990812e-24,\n                8.85545433259e-24,\n                6.01856418372e-24,\n                6.31963413149e-24,\n                6.85279050744e-24,\n                7.75657976909e-24,\n                8.50479856047e-24,\n                7.94873918585e-24\n            ],\n            \"max\":[\n                0.0150078302249,\n                0.0248268786818,\n                0.0372853949666,\n                0.0202775932848,\n                0.00924268271774,\n                0.00459495186806,\n                0.00642714649439,\n                0.00673051225021,\n                0.00483322422951,\n                0.0058145863004,\n                0.00487699825317,\n                0.00474451202899,\n                0.00216139364056,\n                0.00089383253362,\n                0.000890302297194,\n                0.00114067236427,\n                0.00265396363102,\n                0.00179244857281,\n                0.00171541213058,\n                0.00327429641038,\n                0.00261263479479,\n                0.00140622933395,\n                0.000604341796134,\n                0.000546872266568,\n                0.00119282689411,\n                0.000385722087231,\n                0.000307112553855,\n                0.000185638607945,\n                0.000196822540602,\n                7.07383733243e-05,\n                3.59257646778e-05,\n                4.41324045823e-05,\n                5.90648887737e-05,\n                5.03649462189e-05,\n                1.98958878173e-05,\n                1.04367582026e-05,\n                1.49458364831e-05,\n                3.54613039235e-05,\n                2.11343995034e-05,\n                1.33671064759e-05\n            ],\n            \"dvar2\":[\n                1.97027497961e-06,\n                1.79839098564e-05,\n                3.41721388395e-05,\n                6.39068775854e-06,\n                4.12703258235e-07,\n                1.60991760367e-07,\n                5.84672534387e-07,\n                1.35801229817e-07,\n                5.67447884237e-08,\n                9.04100758703e-08,\n                5.12682660769e-08,\n                6.00754361813e-08,\n                8.33282598478e-09,\n                2.19900075926e-09,\n                2.22529794591e-09,\n                2.68628164157e-09,\n                6.38194253e-09,\n                4.48827552901e-09,\n                1.61791646747e-09,\n                5.6524527281e-09,\n                7.00124225261e-09,\n                5.1307971205e-09,\n                1.8114484357e-09,\n                6.22804863237e-10,\n                1.41800715614e-09,\n                5.03870334345e-10,\n                3.42071621029e-10,\n                8.80266623482e-11,\n                5.10801435871e-11,\n                1.6358601973e-11,\n                1.09966497713e-11,\n                1.34551406475e-11,\n                2.12651927317e-11,\n                8.70376774126e-12,\n                2.70498077062e-12,\n                8.23377390574e-13,\n                6.18267915163e-13,\n                8.94622212682e-13,\n                5.74140724113e-13,\n                4.04918747187e-13\n            ],\n            \"median\":[\n                6.78846045048e-05,\n                0.000686653598677,\n                0.00100371893495,\n                0.000425793463364,\n                0.000121567500173,\n                8.67325506988e-05,\n                0.000119830452604,\n                7.64572032494e-05,\n                4.73126528959e-05,\n                4.95156273246e-05,\n                5.10508471052e-05,\n                3.72932154278e-05,\n                1.39631702041e-05,\n                4.18296076532e-06,\n                3.36458288075e-06,\n                5.95153596805e-06,\n                7.06666241967e-06,\n                5.36886500413e-06,\n                6.55584017295e-06,\n                1.27752928165e-05,\n                1.80421066034e-05,\n                1.10708388092e-05,\n                9.13756139198e-06,\n                6.40786538497e-06,\n                6.87108331476e-06,\n                5.39385519005e-06,\n                3.31015212396e-06,\n                2.2452804842e-06,\n                2.11795463656e-06,\n                8.45357135404e-07,\n                1.55302529947e-07,\n                2.16630468231e-07,\n                2.0936421663e-07,\n                1.59076819273e-07,\n                1.03014848207e-07,\n                5.60769883862e-08,\n                4.16746956944e-08,\n                3.91173387015e-08,\n                3.08294119122e-08,\n                2.99958919925e-08\n            ],\n            \"dmean2\":[\n                0.000655197829474,\n                0.00202016695403,\n                0.00265697692521,\n                0.00127012608573,\n                0.000302697066218,\n                0.000204388590646,\n                0.000336306344252,\n                0.000177521447768,\n                0.000107648374978,\n                0.000137986353366,\n                0.000116639275802,\n                0.000105825478386,\n                3.63973231288e-05,\n                1.78256286745e-05,\n                1.63039130712e-05,\n                1.98472516786e-05,\n                2.55192408076e-05,\n                1.86820070667e-05,\n                1.45677859109e-05,\n                2.93159864668e-05,\n                3.81224999728e-05,\n                2.55198028754e-05,\n                1.86311717698e-05,\n                1.29478112285e-05,\n                1.63034928846e-05,\n                1.03988468254e-05,\n                6.90801107339e-06,\n                4.38494134869e-06,\n                3.91557023249e-06,\n                1.95062216335e-06,\n                1.26417035062e-06,\n                1.61554555689e-06,\n                1.957419272e-06,\n                1.21927041619e-06,\n                7.50478704958e-07,\n                4.08925643569e-07,\n                3.15493565495e-07,\n                2.87596407134e-07,\n                2.50877462804e-07,\n                2.38469112901e-07\n            ],\n            \"dmean\":[\n                0.000433199049439,\n                0.00115317711607,\n                0.00158822687808,\n                0.000778516754508,\n                0.000194163410924,\n                0.000129944470245,\n                0.00020776965539,\n                0.000110655011667,\n                7.02645556885e-05,\n                8.88201902853e-05,\n                7.56665394874e-05,\n                7.11579850758e-05,\n                2.26343472605e-05,\n                1.1214566257e-05,\n                1.06519555629e-05,\n                1.3082231817e-05,\n                1.72533091245e-05,\n                1.2507844076e-05,\n                9.44854855334e-06,\n                1.93181331269e-05,\n                2.54146216321e-05,\n                1.58707625815e-05,\n                1.14972417578e-05,\n                8.10282745078e-06,\n                1.0418824786e-05,\n                6.57239706925e-06,\n                4.29072770203e-06,\n                2.71895851256e-06,\n                2.42956934926e-06,\n                1.20713775686e-06,\n                7.98163171112e-07,\n                1.02098283605e-06,\n                1.22477297282e-06,\n                8.0222929455e-07,\n                4.99395412135e-07,\n                2.76074587191e-07,\n                2.18049038381e-07,\n                1.94722304059e-07,\n                1.73989079144e-07,\n                1.64999249819e-07\n            ],\n            \"var\":[\n                1.04273829038e-06,\n                4.88182786285e-06,\n                1.22067667689e-05,\n                3.09852111968e-06,\n                2.95254579896e-07,\n                1.60118688086e-07,\n                3.36397903311e-07,\n                9.02441499306e-08,\n                5.12204714198e-08,\n                8.87765096991e-08,\n                6.13447141973e-08,\n                1.30928043518e-07,\n                7.16093628839e-09,\n                2.90166846106e-09,\n                3.30743610277e-09,\n                3.75769282357e-09,\n                1.11862910046e-08,\n                7.38158290048e-09,\n                3.41670158832e-09,\n                2.24571348184e-08,\n                3.01072198283e-08,\n                4.89972462603e-09,\n                1.57648072374e-09,\n                7.79673825502e-10,\n                2.14637396745e-09,\n                5.14293607701e-10,\n                2.73522648975e-10,\n                8.45902653479e-11,\n                5.53298171169e-11,\n                1.09233455614e-11,\n                4.45680905375e-12,\n                6.09628753728e-12,\n                8.53084356628e-12,\n                4.52617804347e-12,\n                1.82034248786e-12,\n                6.15677430912e-13,\n                4.4449592119e-13,\n                6.71954570979e-13,\n                5.48820374997e-13,\n                3.48484424911e-13\n            ],\n            \"dvar\":[\n                8.33708043046e-07,\n                6.49838420941e-06,\n                1.36113221743e-05,\n                2.76258151644e-06,\n                1.75387782519e-07,\n                6.53309797372e-08,\n                2.18858644985e-07,\n                5.17116269805e-08,\n                2.40723956324e-08,\n                3.69722101823e-08,\n                2.22425278196e-08,\n                2.84932291095e-08,\n                3.64641872252e-09,\n                9.5759922214e-10,\n                9.86244308443e-10,\n                1.3016818734e-09,\n                3.31316440949e-09,\n                2.19745110996e-09,\n                7.73902275597e-10,\n                3.32642535739e-09,\n                3.88597554135e-09,\n                1.99106442444e-09,\n                7.00074609394e-10,\n                2.79730322239e-10,\n                6.32938867984e-10,\n                2.10190226335e-10,\n                1.22236526456e-10,\n                3.66416376407e-11,\n                2.22450686344e-11,\n                6.52528248449e-12,\n                4.34595848892e-12,\n                5.58480735616e-12,\n                8.46765019213e-12,\n                3.64534357561e-12,\n                1.20417478072e-12,\n                3.81005014864e-13,\n                2.8428255301e-13,\n                3.87186458025e-13,\n                2.78560404994e-13,\n                1.87431545436e-13\n            ],\n            \"mean\":[\n                0.00047609579633,\n                0.00126094208099,\n                0.00185862951912,\n                0.000971358967945,\n                0.000324478023686,\n                0.000246531388257,\n                0.000361267186236,\n                0.000190427192138,\n                0.000126440427266,\n                0.000159403600264,\n                0.000136641858262,\n                0.000143978060805,\n                3.71072310372e-05,\n                1.87067053048e-05,\n                1.93349878828e-05,\n                2.34964645642e-05,\n                3.1896517612e-05,\n                2.36660616793e-05,\n                1.83704396477e-05,\n                4.34660541941e-05,\n                5.81043132115e-05,\n                2.89009076369e-05,\n                2.15983145608e-05,\n                1.50616651808e-05,\n                1.95628890651e-05,\n                1.24749267343e-05,\n                8.04845058155e-06,\n                4.90500542583e-06,\n                4.20188916905e-06,\n                1.82525320724e-06,\n                8.70177814249e-07,\n                1.09259167402e-06,\n                1.25471365209e-06,\n                9.23780646644e-07,\n                6.02008753958e-07,\n                3.45971272964e-07,\n                2.69976595746e-07,\n                2.55056221476e-07,\n                2.36791535713e-07,\n                2.26877631349e-07\n            ]\n        },\n        \"spectral_entropy\":{\n            \"min\":4.61501169205,\n            \"max\":9.81467628479,\n            \"dvar2\":0.184129029512,\n            \"median\":7.38190746307,\n            \"dmean2\":0.352754920721,\n            \"dmean\":0.243219792843,\n            \"var\":0.300432950258,\n            \"dvar\":0.0902535244823,\n            \"mean\":7.3010840416\n        },\n        \"spectral_rolloff\":{\n            \"min\":64.599609375,\n            \"max\":21037.9394531,\n            \"dvar2\":3396222,\n            \"median\":861.328125,\n            \"dmean2\":1057.10974121,\n            \"dmean\":604.693481445,\n            \"var\":2179523.25,\n            \"dvar\":1425528,\n            \"mean\":1383.6763916\n        },\n        \"barkbands\":{\n            \"min\":[\n                6.93473619499e-25,\n                5.19568601854e-24,\n                1.02416202049e-23,\n                4.28463410897e-24,\n                3.04471689542e-23,\n                2.72335547301e-23,\n                3.08580014027e-23,\n                1.15517745957e-23,\n                5.01556722243e-23,\n                3.01743335215e-23,\n                3.20834273992e-23,\n                6.39242832255e-23,\n                4.96037188707e-23,\n                6.11980114915e-23,\n                7.73605723709e-23,\n                1.27850093686e-22,\n                8.33913319498e-23,\n                1.55698133196e-22,\n                1.80609161514e-22,\n                2.27535985033e-22,\n                3.03975723224e-22,\n                3.99622470031e-22,\n                4.5185483121e-22,\n                7.15649816942e-22,\n                1.01532623561e-21,\n                1.53862207745e-21,\n                2.31706194078e-21\n            ],\n            \"max\":[\n                0.00229191919789,\n                0.047907166183,\n                0.0691591277719,\n                0.0995827168226,\n                0.0820566862822,\n                0.0232072826475,\n                0.0308443717659,\n                0.0302771702409,\n                0.0344947054982,\n                0.0269099473953,\n                0.0134304724634,\n                0.00715320091695,\n                0.00838674418628,\n                0.0218261200935,\n                0.019664183259,\n                0.0362881943583,\n                0.0182446800172,\n                0.0173479039222,\n                0.0146563379094,\n                0.00540218688548,\n                0.00442544184625,\n                0.00217473763041,\n                0.0013186649885,\n                0.00208773952909,\n                0.00195873784833,\n                0.000584891589824,\n                0.00050722778542\n            ],\n            \"dvar2\":[\n                2.09710808718e-08,\n                5.00840687891e-05,\n                9.50076937443e-05,\n                0.000209878431633,\n                8.73877215781e-05,\n                3.97830672227e-06,\n                1.24859725474e-05,\n                1.86764350474e-06,\n                3.62891637451e-06,\n                3.01378167933e-06,\n                5.96735674208e-07,\n                1.57074438789e-07,\n                2.46402947823e-07,\n                4.87388831516e-07,\n                4.96838595154e-07,\n                1.52867119141e-06,\n                1.2727361991e-06,\n                3.34951494096e-07,\n                4.58745859078e-07,\n                1.02385989464e-07,\n                2.90899002664e-08,\n                3.47254207611e-08,\n                1.03255741735e-08,\n                3.98843491567e-09,\n                4.08773503935e-09,\n                1.11437570283e-09,\n                4.70612881998e-10\n            ],\n            \"median\":[\n                1.01329169411e-06,\n                0.000326245411998,\n                0.00187892094254,\n                0.00155476410873,\n                0.0016892075073,\n                0.000451811589301,\n                0.000548189680558,\n                0.000192572828382,\n                0.000307663634885,\n                0.000414336362155,\n                9.44759449339e-05,\n                2.73611021839e-05,\n                5.38471249456e-05,\n                6.47374472464e-05,\n                8.72917589732e-05,\n                0.000232024758589,\n                0.000200124268304,\n                0.000144036966958,\n                0.000164192693774,\n                8.08068361948e-05,\n                4.40170915681e-05,\n                1.11471890705e-05,\n                6.5616086431e-06,\n                3.2568784718e-06,\n                2.92447043648e-06,\n                4.78593108255e-07,\n                7.76246977807e-08\n            ],\n            \"dmean2\":[\n                5.63530775253e-05,\n                0.00340586039238,\n                0.00441808719188,\n                0.00632638018578,\n                0.0048057041131,\n                0.00103038607631,\n                0.00156902591698,\n                0.000596007099375,\n                0.000905426510144,\n                0.000906587869395,\n                0.000307931040879,\n                0.000146102538565,\n                0.000193360538105,\n                0.000240257213591,\n                0.000224526840611,\n                0.00052686111303,\n                0.000436997273937,\n                0.000281702930806,\n                0.000314143690048,\n                0.000150212654262,\n                8.88355425559e-05,\n                8.39097774588e-05,\n                4.62743046228e-05,\n                2.28419812629e-05,\n                2.21090576815e-05,\n                1.11555727926e-05,\n                5.03497130921e-06\n            ],\n            \"dmean\":[\n                4.36054288002e-05,\n                0.00207182555459,\n                0.00250360206701,\n                0.00372918182984,\n                0.0029777479358,\n                0.00065627245931,\n                0.000966914638411,\n                0.000378262484446,\n                0.000581912638154,\n                0.000613346113823,\n                0.000192098785192,\n                9.08574875211e-05,\n                0.000126861719764,\n                0.000160341092851,\n                0.000145415237057,\n                0.00034682394471,\n                0.000273586803814,\n                0.000176411645953,\n                0.000198787456611,\n                9.40177123994e-05,\n                5.56063205295e-05,\n                5.36341467523e-05,\n                3.11664407491e-05,\n                1.60014496942e-05,\n                1.57560625667e-05,\n                7.42050679037e-06,\n                3.44123100149e-06\n            ],\n            \"var\":[\n                1.73105885182e-08,\n                1.84976615856e-05,\n                2.50716802839e-05,\n                7.04820486135e-05,\n                4.76136665384e-05,\n                3.67056259165e-06,\n                6.74153125146e-06,\n                1.57238355314e-06,\n                3.58573174708e-06,\n                5.54312009626e-06,\n                5.51931407244e-07,\n                1.78970466891e-07,\n                4.17550438669e-07,\n                9.57703605309e-07,\n                8.59389047037e-07,\n                6.6176985456e-06,\n                1.22271069358e-06,\n                4.2584204607e-07,\n                5.58315321086e-07,\n                9.85875061588e-08,\n                2.47446703128e-08,\n                1.64356386279e-08,\n                7.04793423623e-09,\n                3.46884565516e-09,\n                3.82823417411e-09,\n                6.34027108593e-10,\n                3.8906944333e-10\n            ],\n            \"dvar\":[\n                1.15370024645e-08,\n                1.92330826394e-05,\n                3.36813718604e-05,\n                8.24065355118e-05,\n                3.870560613e-05,\n                1.55360748977e-06,\n                4.66681467515e-06,\n                7.47769490772e-07,\n                1.50442451741e-06,\n                1.46062268414e-06,\n                2.81505151634e-07,\n                6.65834889446e-08,\n                1.17960382795e-07,\n                2.50351632758e-07,\n                2.41593681949e-07,\n                8.20324487449e-07,\n                5.11046778229e-07,\n                1.56924727435e-07,\n                1.97066171381e-07,\n                4.1844121057e-08,\n                1.20861924913e-08,\n                1.45208129965e-08,\n                4.66174743252e-09,\n                1.88047488692e-09,\n                2.07477035552e-09,\n                5.05100294923e-10,\n                2.44846420916e-10\n            ],\n            \"mean\":[\n                5.19759996678e-05,\n                0.00199001817964,\n                0.00309076649137,\n                0.00392009504139,\n                0.0039071268402,\n                0.00123883492779,\n                0.00161516538355,\n                0.000634168100078,\n                0.00102682900615,\n                0.00122780457605,\n                0.000289549614536,\n                0.000142228382174,\n                0.000236654945184,\n                0.000305703491904,\n                0.000276061356999,\n                0.000822361966129,\n                0.000508801313117,\n                0.000334064679919,\n                0.000397378782509,\n                0.00017829038552,\n                8.88610738912e-05,\n                5.81649801461e-05,\n                3.79984667234e-05,\n                2.1309551812e-05,\n                2.22307517106e-05,\n                7.82890947448e-06,\n                3.57463068212e-06\n            ]\n        },\n        \"melbands_flatness_db\":{\n            \"min\":0.00437760027125,\n            \"max\":0.607957184315,\n            \"dvar2\":0.00331180845387,\n            \"median\":0.219427987933,\n            \"dmean2\":0.0528259426355,\n            \"dmean\":0.034728333354,\n            \"var\":0.00492821913213,\n            \"dvar\":0.00150177872274,\n            \"mean\":0.230123117566\n        },\n        \"melbands_skewness\":{\n            \"min\":-2.44428515434,\n            \"max\":15.3962888718,\n            \"dvar2\":2.92571592331,\n            \"median\":2.28337860107,\n            \"dmean2\":1.4999755621,\n            \"dmean\":0.987386882305,\n            \"var\":4.25560426712,\n            \"dvar\":1.34734094143,\n            \"mean\":2.84234952927\n        },\n        \"barkbands_skewness\":{\n            \"min\":-6.20005989075,\n            \"max\":18.9707603455,\n            \"dvar2\":2.02687716484,\n            \"median\":1.45346200466,\n            \"dmean2\":1.15091514587,\n            \"dmean\":0.750691831112,\n            \"var\":2.42782378197,\n            \"dvar\":0.866045475006,\n            \"mean\":1.71697402\n        },\n        \"silence_rate_60dB\":{\n            \"min\":0,\n            \"max\":1,\n            \"dvar2\":0.0371209047735,\n            \"median\":0,\n            \"dmean2\":0.0337187945843,\n            \"dmean\":0.0168575756252,\n            \"var\":0.179377868772,\n            \"dvar\":0.0165733974427,\n            \"mean\":0.234251752496\n        },\n        \"spectral_energyband_low\":{\n            \"min\":4.89638611687e-23,\n            \"max\":0.116083092988,\n            \"dvar2\":0.000277574028587,\n            \"median\":0.00443472480401,\n            \"dmean2\":0.00833203457296,\n            \"dmean\":0.00496239587665,\n            \"var\":9.57977899816e-05,\n            \"dvar\":0.000101656405604,\n            \"mean\":0.00667642848566\n        },\n        \"spectral_energyband_middle_low\":{\n            \"min\":2.98093395713e-22,\n            \"max\":0.171243280172,\n            \"dvar2\":0.000569899275433,\n            \"median\":0.0082081919536,\n            \"dmean2\":0.0117948763072,\n            \"dmean\":0.00737277884036,\n            \"var\":0.000299356790492,\n            \"dvar\":0.000242799738771,\n            \"mean\":0.0127554573119\n        },\n        \"melbands_kurtosis\":{\n            \"min\":-1.94107413292,\n            \"max\":380.680023193,\n            \"dvar2\":1038.95019531,\n            \"median\":7.13754844666,\n            \"dmean2\":17.6919975281,\n            \"dmean\":11.5336751938,\n            \"var\":1275.55053711,\n            \"dvar\":493.653686523,\n            \"mean\":19.2873706818\n        },\n        \"spectral_decrease\":{\n            \"min\":-4.64023344193e-08,\n            \"max\":6.78438804261e-19,\n            \"dvar2\":6.20802821665e-17,\n            \"median\":-4.53350113006e-09,\n            \"dmean2\":4.26796598063e-09,\n            \"dmean\":2.69736544212e-09,\n            \"var\":3.69683427013e-17,\n            \"dvar\":2.61311086979e-17,\n            \"mean\":-5.59147705914e-09\n        },\n        \"erbbands_kurtosis\":{\n            \"min\":-1.86338639259,\n            \"max\":171.201263428,\n            \"dvar2\":23.2438850403,\n            \"median\":-0.320037484169,\n            \"dmean2\":2.39585089684,\n            \"dmean\":1.52739417553,\n            \"var\":35.0008544922,\n            \"dvar\":11.0155954361,\n            \"mean\":1.24987971783\n        },\n        \"melbands_crest\":{\n            \"min\":1.85922825336,\n            \"max\":32.9627304077,\n            \"dvar2\":27.1548709869,\n            \"median\":12.5910797119,\n            \"dmean2\":5.60343551636,\n            \"dmean\":3.39730739594,\n            \"var\":20.3299713135,\n            \"dvar\":10.5175638199,\n            \"mean\":13.3324451447\n        },\n        \"melbands_spread\":{\n            \"min\":0.26147004962,\n            \"max\":299.135498047,\n            \"dvar2\":1027.73010254,\n            \"median\":17.4329528809,\n            \"dmean2\":16.3484096527,\n            \"dmean\":10.0459423065,\n            \"var\":529.263366699,\n            \"dvar\":403.605499268,\n            \"mean\":23.2355747223\n        },\n        \"spectral_energy\":{\n            \"min\":1.02320440393e-20,\n            \"max\":0.179453358054,\n            \"dvar2\":0.000923860818148,\n            \"median\":0.0185781233013,\n            \"dmean2\":0.0165870357305,\n            \"dmean\":0.0105188144371,\n            \"var\":0.000572183460463,\n            \"dvar\":0.00039060486597,\n            \"mean\":0.0224339049309\n        },\n        \"mfcc\":{\n            \"mean\":[\n                -715.191650391,\n                133.878646851,\n                -2.74888682365,\n                38.7127075195,\n                3.85699295998,\n                -10.4443674088,\n                -5.89105558395,\n                4.36424779892,\n                2.37372231483,\n                1.66640949249,\n                -7.12301874161,\n                -9.74494552612,\n                -3.86338329315\n            ],\n            \"icov\":[\n                [\n                    7.98077235231e-05,\n                    -5.86888636462e-05,\n                    0.000145378129673,\n                    -9.86643281067e-05,\n                    3.03972447e-05,\n                    -0.000187377940165,\n                    0.000134168396471,\n                    -0.000129314183141,\n                    1.27186794998e-05,\n                    -7.31398395146e-05,\n                    1.38320649512e-06,\n                    0.000111114335596,\n                    -8.96842757356e-05\n                ],\n                [\n                    -5.86888636462e-05,\n                    0.000914695789106,\n                    -1.55892048497e-05,\n                    0.000330161477905,\n                    -0.000239893066464,\n                    0.00115360564087,\n                    -0.000499361369293,\n                    0.000324924068991,\n                    -1.15215043479e-05,\n                    -0.000243278453127,\n                    0.000191972227185,\n                    -9.64893956734e-07,\n                    0.000270225456916\n                ],\n                [\n                    0.000145378129673,\n                    -1.55892048497e-05,\n                    0.0013757571578,\n                    7.94086445239e-05,\n                    -0.000468786456622,\n                    -0.00135524733923,\n                    0.00098228675779,\n                    -0.000316469959216,\n                    -0.000591932330281,\n                    0.000769039965235,\n                    -0.000745072495192,\n                    0.000638137804344,\n                    0.000274067162536\n                ],\n                [\n                    -9.86643281067e-05,\n                    0.000330161477905,\n                    7.94086445239e-05,\n                    0.00385635741986,\n                    -0.00140862353146,\n                    0.000500513473526,\n                    0.000532710750122,\n                    -0.00153480307199,\n                    0.000852926750667,\n                    -0.000837227795273,\n                    0.00151946756523,\n                    -0.00070260866778,\n                    0.000143392870086\n                ],\n                [\n                    3.03972447e-05,\n                    -0.000239893066464,\n                    -0.000468786456622,\n                    -0.00140862353146,\n                    0.0056857005693,\n                    -0.00172490451951,\n                    -0.00111165409908,\n                    0.000201825780096,\n                    0.000247466086876,\n                    -0.00217686733231,\n                    0.00112909392919,\n                    -0.000958321907092,\n                    -0.000166401950992\n                ],\n                [\n                    -0.000187377940165,\n                    0.00115360564087,\n                    -0.00135524733923,\n                    0.000500513473526,\n                    -0.00172490451951,\n                    0.00837340857834,\n                    -0.00406587868929,\n                    0.00216831197031,\n                    -0.0023798532784,\n                    0.00346444058232,\n                    -0.00286303297617,\n                    0.00123248388991,\n                    -0.000868651026394\n                ],\n                [\n                    0.000134168396471,\n                    -0.000499361369293,\n                    0.00098228675779,\n                    0.000532710750122,\n                    -0.00111165409908,\n                    -0.00406587868929,\n                    0.00986782740802,\n                    -0.00542975915596,\n                    0.00409825751558,\n                    -0.0044681020081,\n                    0.00282385596074,\n                    -0.00354772782885,\n                    0.00168551225215\n                ],\n                [\n                    -0.000129314183141,\n                    0.000324924068991,\n                    -0.000316469959216,\n                    -0.00153480307199,\n                    0.000201825780096,\n                    0.00216831197031,\n                    -0.00542975915596,\n                    0.0144132943824,\n                    -0.00864001736045,\n                    0.00550767453387,\n                    -0.00397388311103,\n                    0.00314126117155,\n                    -0.00206855195574\n                ],\n                [\n                    1.27186794998e-05,\n                    -1.15215043479e-05,\n                    -0.000591932330281,\n                    0.000852926750667,\n                    0.000247466086876,\n                    -0.0023798532784,\n                    0.00409825751558,\n                    -0.00864001736045,\n                    0.0176015142351,\n                    -0.0112102692947,\n                    0.0083336783573,\n                    -0.00373222492635,\n                    0.00208288733847\n                ],\n                [\n                    -7.31398395146e-05,\n                    -0.000243278453127,\n                    0.000769039965235,\n                    -0.000837227795273,\n                    -0.00217686733231,\n                    0.00346444058232,\n                    -0.0044681020081,\n                    0.00550767453387,\n                    -0.0112102692947,\n                    0.0205363947898,\n                    -0.0105180032551,\n                    0.00535045098513,\n                    -0.00213157944381\n                ],\n                [\n                    1.38320649512e-06,\n                    0.000191972227185,\n                    -0.000745072495192,\n                    0.00151946756523,\n                    0.00112909392919,\n                    -0.00286303297617,\n                    0.00282385596074,\n                    -0.00397388311103,\n                    0.0083336783573,\n                    -0.0105180032551,\n                    0.0160211343318,\n                    -0.00672314595431,\n                    0.00358490552753\n                ],\n                [\n                    0.000111114335596,\n                    -9.64893956734e-07,\n                    0.000638137804344,\n                    -0.00070260866778,\n                    -0.000958321907092,\n                    0.00123248388991,\n                    -0.00354772782885,\n                    0.00314126117155,\n                    -0.00373222492635,\n                    0.00535045098513,\n                    -0.00672314595431,\n                    0.0181408431381,\n                    -0.00800348073244\n                ],\n                [\n                    -8.96842757356e-05,\n                    0.000270225456916,\n                    0.000274067162536,\n                    0.000143392870086,\n                    -0.000166401950992,\n                    -0.000868651026394,\n                    0.00168551225215,\n                    -0.00206855195574,\n                    0.00208288733847,\n                    -0.00213157944381,\n                    0.00358490552753,\n                    -0.00800348073244,\n                    0.0156487096101\n                ]\n            ],\n            \"cov\":[\n                [\n                    18717.6230469,\n                    1395.88500977,\n                    -2566.72802734,\n                    557.851989746,\n                    -126.109024048,\n                    -374.194091797,\n                    26.1053237915,\n                    164.080841064,\n                    80.1878204346,\n                    192.214401245,\n                    -182.145751953,\n                    -47.5592041016,\n                    152.686767578\n                ],\n                [\n                    1395.88500977,\n                    1649.59350586,\n                    -550.320007324,\n                    -49.1516876221,\n                    -56.1948204041,\n                    -331.535217285,\n                    9.86240959167,\n                    -34.6940498352,\n                    -6.41311454773,\n                    74.1020736694,\n                    -56.9916381836,\n                    -16.3797187805,\n                    -19.4202880859\n                ],\n                [\n                    -2566.72802734,\n                    -550.320007324,\n                    1546.81799316,\n                    -94.7429580688,\n                    133.984313965,\n                    339.89239502,\n                    -54.6255912781,\n                    10.0923881531,\n                    34.4428405762,\n                    -69.080619812,\n                    85.0990753174,\n                    -36.5973091125,\n                    -56.1265907288\n                ],\n                [\n                    557.851989746,\n                    -49.1516876221,\n                    -94.7429580688,\n                    342.6902771,\n                    83.1379013062,\n                    -15.0537748337,\n                    17.4024486542,\n                    46.2917900085,\n                    21.9955101013,\n                    12.8328580856,\n                    -40.3507461548,\n                    8.21014022827,\n                    19.1214065552\n                ],\n                [\n                    -126.109024048,\n                    -56.1948204041,\n                    133.984313965,\n                    83.1379013062,\n                    261.195220947,\n                    98.0570144653,\n                    69.749458313,\n                    21.4776115417,\n                    23.9828796387,\n                    28.7067451477,\n                    2.99768400192,\n                    18.3751049042,\n                    10.1154251099\n                ],\n                [\n                    -374.194091797,\n                    -331.535217285,\n                    339.89239502,\n                    -15.0537748337,\n                    98.0570144653,\n                    288.139526367,\n                    71.6913833618,\n                    15.3370637894,\n                    9.08637428284,\n                    -21.2720279694,\n                    42.7490844727,\n                    11.0748538971,\n                    0.872315227985\n                ],\n                [\n                    26.1053237915,\n                    9.86240959167,\n                    -54.6255912781,\n                    17.4024486542,\n                    69.749458313,\n                    71.6913833618,\n                    187.006271362,\n                    49.2006340027,\n                    4.36477804184,\n                    25.2697525024,\n                    7.5669798851,\n                    29.0018577576,\n                    7.81967544556\n                ],\n                [\n                    164.080841064,\n                    -34.6940498352,\n                    10.0923881531,\n                    46.2917900085,\n                    21.4776115417,\n                    15.3370637894,\n                    49.2006340027,\n                    122.378990173,\n                    54.7077331543,\n                    6.68141078949,\n                    -6.7650809288,\n                    -0.911539852619,\n                    7.60771179199\n                ],\n                [\n                    80.1878204346,\n                    -6.41311454773,\n                    34.4428405762,\n                    21.9955101013,\n                    23.9828796387,\n                    9.08637428284,\n                    4.36477804184,\n                    54.7077331543,\n                    120.954666138,\n                    41.4050827026,\n                    -25.8017272949,\n                    -5.81494665146,\n                    -0.236202299595\n                ],\n                [\n                    192.214401245,\n                    74.1020736694,\n                    -69.080619812,\n                    12.8328580856,\n                    28.7067451477,\n                    -21.2720279694,\n                    25.2697525024,\n                    6.68141078949,\n                    41.4050827026,\n                    102.835891724,\n                    31.3814048767,\n                    -2.45175004005,\n                    -1.74627566338\n                ],\n                [\n                    -182.145751953,\n                    -56.9916381836,\n                    85.0990753174,\n                    -40.3507461548,\n                    2.99768400192,\n                    42.7490844727,\n                    7.5669798851,\n                    -6.7650809288,\n                    -25.8017272949,\n                    31.3814048767,\n                    120.884010315,\n                    22.8159122467,\n                    -8.79971408844\n                ],\n                [\n                    -47.5592041016,\n                    -16.3797187805,\n                    -36.5973091125,\n                    8.21014022827,\n                    18.3751049042,\n                    11.0748538971,\n                    29.0018577576,\n                    -0.911539852619,\n                    -5.81494665146,\n                    -2.45175004005,\n                    22.8159122467,\n                    87.9682235718,\n                    38.346157074\n                ],\n                [\n                    152.686767578,\n                    -19.4202880859,\n                    -56.1265907288,\n                    19.1214065552,\n                    10.1154251099,\n                    0.872315227985,\n                    7.81967544556,\n                    7.60771179199,\n                    -0.236202299595,\n                    -1.74627566338,\n                    -8.79971408844,\n                    38.346157074,\n                    87.662071228\n                ]\n            ]\n        },\n        \"spectral_contrast_valleys\":{\n            \"min\":[\n                -27.644701004,\n                -27.7437572479,\n                -27.5804691315,\n                -27.6421985626,\n                -27.3466243744,\n                -27.4174346924\n            ],\n            \"max\":[\n                -5.21189641953,\n                -4.43200492859,\n                -5.37789392471,\n                -5.91939544678,\n                -5.84870004654,\n                -8.253657341\n            ],\n            \"dvar2\":[\n                0.418128162622,\n                0.42821636796,\n                0.406443417072,\n                0.387645244598,\n                0.499291837215,\n                0.697984993458\n            ],\n            \"median\":[\n                -7.62850189209,\n                -6.92876195908,\n                -7.67359733582,\n                -8.07751655579,\n                -7.46400737762,\n                -11.0671672821\n            ],\n            \"dmean2\":[\n                0.747626721859,\n                0.710296332836,\n                0.67908090353,\n                0.625626146793,\n                0.595694363117,\n                0.680769026279\n            ],\n            \"dmean\":[\n                0.493877381086,\n                0.47190451622,\n                0.458809673786,\n                0.433981686831,\n                0.418061226606,\n                0.529967188835\n            ],\n            \"var\":[\n                2.56318879128,\n                2.56686043739,\n                2.68657803535,\n                2.71048474312,\n                2.86386156082,\n                2.03321886063\n            ],\n            \"dvar\":[\n                0.185303419828,\n                0.210249379277,\n                0.205063581467,\n                0.197276696563,\n                0.240655809641,\n                0.327577888966\n            ],\n            \"mean\":[\n                -7.9825963974,\n                -7.30778980255,\n                -8.13263988495,\n                -8.62220096588,\n                -8.10116863251,\n                -11.1167650223\n            ]\n        },\n        \"barkbands_flatness_db\":{\n            \"min\":0.00845116842538,\n            \"max\":0.463767468929,\n            \"dvar2\":0.00153901893646,\n            \"median\":0.152025014162,\n            \"dmean2\":0.0385256558657,\n            \"dmean\":0.0259312130511,\n            \"var\":0.00293617043644,\n            \"dvar\":0.000700293399859,\n            \"mean\":0.161552548409\n        },\n        \"dynamic_complexity\":5.97568511963,\n        \"spectral_skewness\":{\n            \"min\":-0.258594423532,\n            \"max\":7.78090715408,\n            \"dvar2\":0.501113593578,\n            \"median\":1.41668355465,\n            \"dmean2\":0.598508477211,\n            \"dmean\":0.382409095764,\n            \"var\":0.599681735039,\n            \"dvar\":0.224964693189,\n            \"mean\":1.57063114643\n        },\n        \"erbbands_flatness_db\":{\n            \"min\":0.0321905463934,\n            \"max\":0.434577912092,\n            \"dvar2\":0.00118384102825,\n            \"median\":0.181439816952,\n            \"dmean2\":0.0333808884025,\n            \"dmean\":0.024810899049,\n            \"var\":0.00294912024401,\n            \"dvar\":0.000594827288296,\n            \"mean\":0.178649738431\n        },\n        \"hfc\":{\n            \"min\":1.12110872149e-16,\n            \"max\":96.712890625,\n            \"dvar2\":109.576454163,\n            \"median\":11.9836702347,\n            \"dmean2\":6.97959280014,\n            \"dmean\":4.67007303238,\n            \"var\":197.084396362,\n            \"dvar\":50.9854736328,\n            \"mean\":14.6673564911\n        },\n        \"barkbands_crest\":{\n            \"min\":2.1863629818,\n            \"max\":25.4534358978,\n            \"dvar2\":11.5983400345,\n            \"median\":8.56367301941,\n            \"dmean2\":3.74820661545,\n            \"dmean\":2.30216050148,\n            \"var\":11.5966396332,\n            \"dvar\":4.54739236832,\n            \"mean\":9.14883136749\n        }\n    },\n    \"highlevel\":{\n        \"timbre\":{\n            \"all\":{\n                \"dark\":0.0808309540153,\n                \"bright\":0.919169068336\n            },\n            \"version\":{\n                \"essentia\":\"2.1-beta1\",\n                \"extractor\":\"music 1.0\",\n                \"gaia_git_sha\":\"857329b\",\n                \"models_essentia_git_sha\":\"v2.1_beta1\",\n                \"essentia_git_sha\":\"v2.1_beta1-228-g260734a\",\n                \"essentia_build_sha\":\"8e24b98b71ad84f3024c7541412f02124a26d327\",\n                \"gaia\":\"2.4-dev\"\n            },\n            \"value\":\"bright\",\n            \"probability\":0.919169068336\n        },\n        \"ismir04_rhythm\":{\n            \"all\":{\n                \"Rumba-American\":0.0406456775963,\n                \"VienneseWaltz\":0.338310062885,\n                \"Samba\":0.0297329928726,\n                \"Rumba-Misc\":0.0135653112084,\n                \"Rumba-International\":0.0278510767967,\n                \"Tango\":0.330144882202,\n                \"Waltz\":0.00898563489318,\n                \"ChaChaCha\":0.0889096781611,\n                \"Jive\":0.11033257097,\n                \"Quickstep\":0.0115221142769\n            },\n            \"version\":{\n                \"essentia\":\"2.1-beta1\",\n                \"extractor\":\"music 1.0\",\n                \"gaia_git_sha\":\"857329b\",\n                \"models_essentia_git_sha\":\"v2.1_beta1\",\n                \"essentia_git_sha\":\"v2.1_beta1-228-g260734a\",\n                \"essentia_build_sha\":\"8e24b98b71ad84f3024c7541412f02124a26d327\",\n                \"gaia\":\"2.4-dev\"\n            },\n            \"value\":\"VienneseWaltz\",\n            \"probability\":0.338310062885\n        },\n        \"voice_instrumental\":{\n            \"all\":{\n                \"instrumental\":0.999981045723,\n                \"voice\":1.89501206478e-05\n            },\n            \"version\":{\n                \"essentia\":\"2.1-beta1\",\n                \"extractor\":\"music 1.0\",\n                \"gaia_git_sha\":\"857329b\",\n                \"models_essentia_git_sha\":\"v2.1_beta1\",\n                \"essentia_git_sha\":\"v2.1_beta1-228-g260734a\",\n                \"essentia_build_sha\":\"8e24b98b71ad84f3024c7541412f02124a26d327\",\n                \"gaia\":\"2.4-dev\"\n            },\n            \"value\":\"instrumental\",\n            \"probability\":0.999981045723\n        },\n        \"gender\":{\n            \"all\":{\n                \"male\":0.108683988452,\n                \"female\":0.891315996647\n            },\n            \"version\":{\n                \"essentia\":\"2.1-beta1\",\n                \"extractor\":\"music 1.0\",\n                \"gaia_git_sha\":\"857329b\",\n                \"models_essentia_git_sha\":\"v2.1_beta1\",\n                \"essentia_git_sha\":\"v2.1_beta1-228-g260734a\",\n                \"essentia_build_sha\":\"8e24b98b71ad84f3024c7541412f02124a26d327\",\n                \"gaia\":\"2.4-dev\"\n            },\n            \"value\":\"female\",\n            \"probability\":0.891315996647\n        },\n        \"genre_rosamerica\":{\n            \"all\":{\n                \"hip\":0.070330247283,\n                \"rhy\":0.225707545877,\n                \"jaz\":0.0771619826555,\n                \"dan\":0.0574826933444,\n                \"roc\":0.270465612411,\n                \"cla\":0.0938607081771,\n                \"pop\":0.175827592611,\n                \"spe\":0.0291636306792\n            },\n            \"version\":{\n                \"essentia\":\"2.1-beta1\",\n                \"extractor\":\"music 1.0\",\n                \"gaia_git_sha\":\"857329b\",\n                \"models_essentia_git_sha\":\"v2.1_beta1\",\n                \"essentia_git_sha\":\"v2.1_beta1-228-g260734a\",\n                \"essentia_build_sha\":\"8e24b98b71ad84f3024c7541412f02124a26d327\",\n                \"gaia\":\"2.4-dev\"\n            },\n            \"value\":\"roc\",\n            \"probability\":0.270465612411\n        },\n        \"mood_electronic\":{\n            \"all\":{\n                \"electronic\":0.339881360531,\n                \"not_electronic\":0.660118639469\n            },\n            \"version\":{\n                \"essentia\":\"2.1-beta1\",\n                \"extractor\":\"music 1.0\",\n                \"gaia_git_sha\":\"857329b\",\n                \"models_essentia_git_sha\":\"v2.1_beta1\",\n                \"essentia_git_sha\":\"v2.1_beta1-228-g260734a\",\n                \"essentia_build_sha\":\"8e24b98b71ad84f3024c7541412f02124a26d327\",\n                \"gaia\":\"2.4-dev\"\n            },\n            \"value\":\"not_electronic\",\n            \"probability\":0.660118639469\n        },\n        \"genre_electronic\":{\n            \"all\":{\n                \"house\":0.187250360847,\n                \"trance\":0.185409858823,\n                \"dnb\":0.00702595943585,\n                \"techno\":0.0184047427028,\n                \"ambient\":0.601909101009\n            },\n            \"version\":{\n                \"essentia\":\"2.1-beta1\",\n                \"extractor\":\"music 1.0\",\n                \"gaia_git_sha\":\"857329b\",\n                \"models_essentia_git_sha\":\"v2.1_beta1\",\n                \"essentia_git_sha\":\"v2.1_beta1-228-g260734a\",\n                \"essentia_build_sha\":\"8e24b98b71ad84f3024c7541412f02124a26d327\",\n                \"gaia\":\"2.4-dev\"\n            },\n            \"value\":\"ambient\",\n            \"probability\":0.601909101009\n        },\n        \"mood_sad\":{\n            \"all\":{\n                \"not_sad\":0.700305402279,\n                \"sad\":0.299694597721\n            },\n            \"version\":{\n                \"essentia\":\"2.1-beta1\",\n                \"extractor\":\"music 1.0\",\n                \"gaia_git_sha\":\"857329b\",\n                \"models_essentia_git_sha\":\"v2.1_beta1\",\n                \"essentia_git_sha\":\"v2.1_beta1-228-g260734a\",\n                \"essentia_build_sha\":\"8e24b98b71ad84f3024c7541412f02124a26d327\",\n                \"gaia\":\"2.4-dev\"\n            },\n            \"value\":\"not_sad\",\n            \"probability\":0.700305402279\n        },\n        \"tonal_atonal\":{\n            \"all\":{\n                \"atonal\":0.125749841332,\n                \"tonal\":0.874250173569\n            },\n            \"version\":{\n                \"essentia\":\"2.1-beta1\",\n                \"extractor\":\"music 1.0\",\n                \"gaia_git_sha\":\"857329b\",\n                \"models_essentia_git_sha\":\"v2.1_beta1\",\n                \"essentia_git_sha\":\"v2.1_beta1-228-g260734a\",\n                \"essentia_build_sha\":\"8e24b98b71ad84f3024c7541412f02124a26d327\",\n                \"gaia\":\"2.4-dev\"\n            },\n            \"value\":\"tonal\",\n            \"probability\":0.874250173569\n        },\n        \"mood_party\":{\n            \"all\":{\n                \"party\":0.234383180737,\n                \"not_party\":0.765616834164\n            },\n            \"version\":{\n                \"essentia\":\"2.1-beta1\",\n                \"extractor\":\"music 1.0\",\n                \"gaia_git_sha\":\"857329b\",\n                \"models_essentia_git_sha\":\"v2.1_beta1\",\n                \"essentia_git_sha\":\"v2.1_beta1-228-g260734a\",\n                \"essentia_build_sha\":\"8e24b98b71ad84f3024c7541412f02124a26d327\",\n                \"gaia\":\"2.4-dev\"\n            },\n            \"value\":\"not_party\",\n            \"probability\":0.765616834164\n        },\n        \"moods_mirex\":{\n            \"all\":{\n                \"Cluster2\":0.0673071071506,\n                \"Cluster3\":0.397048592567,\n                \"Cluster1\":0.061667304486,\n                \"Cluster4\":0.190215244889,\n                \"Cluster5\":0.283761769533\n            },\n            \"version\":{\n                \"essentia\":\"2.1-beta1\",\n                \"extractor\":\"music 1.0\",\n                \"gaia_git_sha\":\"857329b\",\n                \"models_essentia_git_sha\":\"v2.1_beta1\",\n                \"essentia_git_sha\":\"v2.1_beta1-228-g260734a\",\n                \"essentia_build_sha\":\"8e24b98b71ad84f3024c7541412f02124a26d327\",\n                \"gaia\":\"2.4-dev\"\n            },\n            \"value\":\"Cluster3\",\n            \"probability\":0.397048592567\n        },\n        \"danceability\":{\n            \"all\":{\n                \"danceable\":0.143928021193,\n                \"not_danceable\":0.85607200861\n            },\n            \"version\":{\n                \"essentia\":\"2.1-beta1\",\n                \"extractor\":\"music 1.0\",\n                \"gaia_git_sha\":\"857329b\",\n                \"models_essentia_git_sha\":\"v2.1_beta1\",\n                \"essentia_git_sha\":\"v2.1_beta1-228-g260734a\",\n                \"essentia_build_sha\":\"8e24b98b71ad84f3024c7541412f02124a26d327\",\n                \"gaia\":\"2.4-dev\"\n            },\n            \"value\":\"not_danceable\",\n            \"probability\":0.85607200861\n        },\n        \"genre_dortmund\":{\n            \"all\":{\n                \"raphiphop\":4.74844455312e-05,\n                \"electronic\":0.984485208988,\n                \"jazz\":0.000787914788816,\n                \"pop\":0.000125292601297,\n                \"folkcountry\":0.00235203420743,\n                \"rock\":0.0010081063956,\n                \"alternative\":0.00961782038212,\n                \"funksoulrnb\":6.0458383814e-05,\n                \"blues\":0.00151570443995\n            },\n            \"version\":{\n                \"essentia\":\"2.1-beta1\",\n                \"extractor\":\"music 1.0\",\n                \"gaia_git_sha\":\"857329b\",\n                \"models_essentia_git_sha\":\"v2.1_beta1\",\n                \"essentia_git_sha\":\"v2.1_beta1-228-g260734a\",\n                \"essentia_build_sha\":\"8e24b98b71ad84f3024c7541412f02124a26d327\",\n                \"gaia\":\"2.4-dev\"\n            },\n            \"value\":\"electronic\",\n            \"probability\":0.984485208988\n        },\n        \"mood_acoustic\":{\n            \"all\":{\n                \"acoustic\":0.415711194277,\n                \"not_acoustic\":0.584288835526\n            },\n            \"version\":{\n                \"essentia\":\"2.1-beta1\",\n                \"extractor\":\"music 1.0\",\n                \"gaia_git_sha\":\"857329b\",\n                \"models_essentia_git_sha\":\"v2.1_beta1\",\n                \"essentia_git_sha\":\"v2.1_beta1-228-g260734a\",\n                \"essentia_build_sha\":\"8e24b98b71ad84f3024c7541412f02124a26d327\",\n                \"gaia\":\"2.4-dev\"\n            },\n            \"value\":\"not_acoustic\",\n            \"probability\":0.584288835526\n        },\n        \"mood_happy\":{\n            \"all\":{\n                \"not_happy\":0.910523295403,\n                \"happy\":0.0894767045975\n            },\n            \"version\":{\n                \"essentia\":\"2.1-beta1\",\n                \"extractor\":\"music 1.0\",\n                \"gaia_git_sha\":\"857329b\",\n                \"models_essentia_git_sha\":\"v2.1_beta1\",\n                \"essentia_git_sha\":\"v2.1_beta1-228-g260734a\",\n                \"essentia_build_sha\":\"8e24b98b71ad84f3024c7541412f02124a26d327\",\n                \"gaia\":\"2.4-dev\"\n            },\n            \"value\":\"not_happy\",\n            \"probability\":0.910523295403\n        },\n        \"mood_aggressive\":{\n            \"all\":{\n                \"not_aggressive\":0.922077834606,\n                \"aggressive\":0.0779221653938\n            },\n            \"version\":{\n                \"essentia\":\"2.1-beta1\",\n                \"extractor\":\"music 1.0\",\n                \"gaia_git_sha\":\"857329b\",\n                \"models_essentia_git_sha\":\"v2.1_beta1\",\n                \"essentia_git_sha\":\"v2.1_beta1-228-g260734a\",\n                \"essentia_build_sha\":\"8e24b98b71ad84f3024c7541412f02124a26d327\",\n                \"gaia\":\"2.4-dev\"\n            },\n            \"value\":\"not_aggressive\",\n            \"probability\":0.922077834606\n        },\n        \"genre_tzanetakis\":{\n            \"all\":{\n                \"hip\":0.154464527965,\n                \"jaz\":0.308918893337,\n                \"bl\":0.0514711923897,\n                \"roc\":0.0772226303816,\n                \"cla\":0.0343172624707,\n                \"pop\":0.0617748275399,\n                \"met\":0.0441242903471,\n                \"co\":0.102957598865,\n                \"reg\":0.0617764480412,\n                \"dis\":0.102972343564\n            },\n            \"version\":{\n                \"essentia\":\"2.1-beta1\",\n                \"extractor\":\"music 1.0\",\n                \"gaia_git_sha\":\"857329b\",\n                \"models_essentia_git_sha\":\"v2.1_beta1\",\n                \"essentia_git_sha\":\"v2.1_beta1-228-g260734a\",\n                \"essentia_build_sha\":\"8e24b98b71ad84f3024c7541412f02124a26d327\",\n                \"gaia\":\"2.4-dev\"\n            },\n            \"value\":\"jaz\",\n            \"probability\":0.308918893337\n        },\n        \"mood_relaxed\":{\n            \"all\":{\n                \"not_relaxed\":0.87636756897,\n                \"relaxed\":0.123632438481\n            },\n            \"version\":{\n                \"essentia\":\"2.1-beta1\",\n                \"extractor\":\"music 1.0\",\n                \"gaia_git_sha\":\"857329b\",\n                \"models_essentia_git_sha\":\"v2.1_beta1\",\n                \"essentia_git_sha\":\"v2.1_beta1-228-g260734a\",\n                \"essentia_build_sha\":\"8e24b98b71ad84f3024c7541412f02124a26d327\",\n                \"gaia\":\"2.4-dev\"\n            },\n            \"value\":\"not_relaxed\",\n            \"probability\":0.87636756897\n        }\n    },\n    \"metadata\":{\n        \"audio_properties\":{\n            \"analysis_sample_rate\":44100,\n            \"length\":214.866668701,\n            \"downmix\":\"mix\",\n            \"bit_rate\":0,\n            \"codec\":\"flac\",\n            \"md5_encoded\":\"2b46dab358c1b79a3decd5bd93d7221f\",\n            \"equal_loudness\":0,\n            \"replay_gain\":-14.4778690338,\n            \"lossless\":1\n        },\n        \"version\":{\n            \"lowlevel\":{\n                \"essentia\":\"2.1-beta1\",\n                \"extractor\":\"music 1.0\",\n                \"essentia_build_sha\":\"50a0fbec89d6a9cedea3d45b6611406f7e8c7b1a\",\n                \"essentia_git_sha\":\"v2.1_beta1-7-ge0e83e8-dirty\"\n            },\n            \"highlevel\":{\n                \"essentia\":\"2.1-beta1\",\n                \"extractor\":\"music 1.0\",\n                \"gaia_git_sha\":\"857329b\",\n                \"models_essentia_git_sha\":\"v2.1_beta1\",\n                \"essentia_git_sha\":\"v2.1_beta1-228-g260734a\",\n                \"essentia_build_sha\":\"8e24b98b71ad84f3024c7541412f02124a26d327\",\n                \"gaia\":\"2.4-dev\"\n            }\n        },\n        \"tags\":{\n            \"albumartistsort\":[\n                \"Various Artists\"\n            ],\n            \"disctotal\":[\n                \"1\"\n            ],\n            \"file_name\":\"04 La Grange.flac\",\n            \"artists\":[\n                \"ZZ Top\"\n            ],\n            \"musicbrainz_workid\":[\n                \"42722fe8-9de7-3729-a506-3c7f41c617a9\"\n            ],\n            \"releasecountry\":[\n                \"DE\"\n            ],\n            \"totaldiscs\":[\n                \"1\"\n            ],\n            \"albumartist\":[\n                \"Various Artists\"\n            ],\n            \"musicbrainz_albumartistid\":[\n                \"89ad4ac3-39f7-470e-963a-56509c546377\"\n            ],\n            \"composer\":[\n                \"Dusty Hill\",\n                \"Frank Beard\",\n                \"Billy Gibbons\"\n            ],\n            \"catalognumber\":[\n                \"491384 2\"\n            ],\n            \"tracknumber\":[\n                \"4\"\n            ],\n            \"replaygain_track_peak\":[\n                \"0.999969\"\n            ],\n            \"engineer\":[\n                \"Terry Manning\",\n                \"Robin Hood Brians\"\n            ],\n            \"album\":[\n                \"Armageddon:The Album\"\n            ],\n            \"asin\":[\n                \"B000024C3A\"\n            ],\n            \"replaygain_album_gain\":[\n                \"-9.32 dB\"\n            ],\n            \"musicbrainz_artistid\":[\n                \"a81259a0-a2f5-464b-866e-71220f2739f1\"\n            ],\n            \"producer\":[\n                \"Bill Ham\"\n            ],\n            \"script\":[\n                \"Latn\"\n            ],\n            \"media\":[\n                \"CD\"\n            ],\n            \"label\":[\n                \"Columbia\"\n            ],\n            \"artistsort\":[\n                \"ZZ Top\"\n            ],\n            \"acoustid_id\":[\n                \"3ed3441e-facc-4fcd-9ef7-9fbc68c206a2\"\n            ],\n            \"replaygain_album_peak\":[\n                \"0.999969\"\n            ],\n            \"lyricist\":[\n                \"Dusty Hill\",\n                \"Frank Beard\",\n                \"Billy Gibbons\"\n            ],\n            \"musicbrainz_releasegroupid\":[\n                \"f51d56e4-0211-3533-a9a5-08c02d8bb04a\"\n            ],\n            \"compilation\":[\n                \"1\"\n            ],\n            \"barcode\":[\n                \"5099749138421\"\n            ],\n            \"releasestatus\":[\n                \"official\"\n            ],\n            \"composersort\":[\n                \"Hill, Dusty\",\n                \"Beard, Frank\",\n                \"Gibbons, Billy\"\n            ],\n            \"date\":[\n                \"1998\"\n            ],\n            \"isrc\":[\n                \"USWB10505222\"\n            ],\n            \"discnumber\":[\n                \"1\"\n            ],\n            \"musicbrainz_recordingid\":[\n                \"cd3f5efa-bc5e-4064-a765-960494ad4bb4\"\n            ],\n            \"tracktotal\":[\n                \"14\"\n            ],\n            \"originaldate\":[\n                \"1998-06-23\"\n            ],\n            \"language\":[\n                \"eng\"\n            ],\n            \"artist\":[\n                \"ZZ Top\"\n            ],\n            \"title\":[\n                \"La Grange\"\n            ],\n            \"releasetype\":[\n                \"album\",\n                \"soundtrack\"\n            ],\n            \"musicbrainz_albumid\":[\n                \"cfc31187-aebd-309f-a92f-7138c17df7c2\"\n            ],\n            \"work\":[\n                \"La Grange\"\n            ],\n            \"totaltracks\":[\n                \"14\"\n            ],\n            \"replaygain_track_gain\":[\n                \"-9.38 dB\"\n            ],\n            \"musicbrainz_releasetrackid\":[\n                \"befe2741-462b-3568-ba06-c8cc8e4f6eaf\"\n            ]\n        }\n    }\n}\n"
  },
  {
    "path": "test/rsrc/beetsplug/test.py",
    "content": "from beets import ui\nfrom beets.plugins import BeetsPlugin\n\n\nclass TestPlugin(BeetsPlugin):\n    def __init__(self):\n        super().__init__()\n        self.is_test_plugin = True\n\n    def commands(self):\n        test = ui.Subcommand(\"test\")\n        test.func = lambda *args: None\n\n        # Used in CompletionTest\n        test.parser.add_option(\"-o\", \"--option\", dest=\"my_opt\")\n\n        plugin = ui.Subcommand(\"plugin\")\n        plugin.func = lambda *args: None\n        return [test, plugin]\n"
  },
  {
    "path": "test/rsrc/convert_stub.py",
    "content": "#!/usr/bin/env python3\n\n\"\"\"A tiny tool used to test the `convert` plugin. It copies a file and appends\na specified text tag.\n\"\"\"\n\nimport sys\n\n\ndef convert(in_file, out_file, tag):\n    \"\"\"Copy `in_file` to `out_file` and append the string `tag`.\"\"\"\n    if not isinstance(tag, bytes):\n        tag = tag.encode(\"utf-8\")\n\n    with open(out_file, \"wb\") as out_f:\n        with open(in_file, \"rb\") as in_f:\n            out_f.write(in_f.read())\n        out_f.write(tag)\n\n\nif __name__ == \"__main__\":\n    convert(sys.argv[1], sys.argv[2], sys.argv[3])\n"
  },
  {
    "path": "test/rsrc/itunes_library_unix.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>Major Version</key><integer>1</integer>\n\t<key>Minor Version</key><integer>1</integer>\n\t<key>Date</key><date>2015-05-08T14:36:28Z</date>\n\t<key>Application Version</key><string>12.1.2.27</string>\n\t<key>Features</key><integer>5</integer>\n\t<key>Show Content Ratings</key><true/>\n\t<key>Music Folder</key><string>file:////Music/</string>\n\t<key>Library Persistent ID</key><string>1ABA8417E4946A32</string>\n\t<key>Tracks</key>\n\t<dict>\n\t\t<key>634</key>\n\t\t<dict>\n\t\t\t<key>Track ID</key><integer>634</integer>\n\t\t\t<key>Name</key><string>Tessellate</string>\n\t\t\t<key>Artist</key><string>alt-J</string>\n\t\t\t<key>Album Artist</key><string>alt-J</string>\n\t\t\t<key>Album</key><string>An Awesome Wave</string>\n\t\t\t<key>Genre</key><string>Alternative</string>\n\t\t\t<key>Kind</key><string>MPEG audio file</string>\n\t\t\t<key>Size</key><integer>5525212</integer>\n\t\t\t<key>Total Time</key><integer>182674</integer>\n\t\t\t<key>Disc Number</key><integer>1</integer>\n\t\t\t<key>Disc Count</key><integer>1</integer>\n\t\t\t<key>Track Number</key><integer>3</integer>\n\t\t\t<key>Track Count</key><integer>13</integer>\n\t\t\t<key>Year</key><integer>2012</integer>\n\t\t\t<key>Date Modified</key><date>2015-02-02T15:23:08Z</date>\n\t\t\t<key>Date Added</key><date>2014-04-24T09:28:38Z</date>\n\t\t\t<key>Bit Rate</key><integer>238</integer>\n\t\t\t<key>Sample Rate</key><integer>44100</integer>\n\t\t\t<key>Play Count</key><integer>0</integer>\n\t\t\t<key>Play Date</key><integer>3513593824</integer>\n\t\t\t<key>Skip Count</key><integer>3</integer>\n\t\t\t<key>Skip Date</key><date>2015-02-05T15:41:04Z</date>\n\t\t\t<key>Rating</key><integer>80</integer>\n\t\t\t<key>Album Rating</key><integer>80</integer>\n\t\t\t<key>Album Rating Computed</key><true/>\n\t\t\t<key>Artwork Count</key><integer>1</integer>\n\t\t\t<key>Sort Album</key><string>Awesome Wave</string>\n\t\t\t<key>Sort Artist</key><string>alt-J</string>\n\t\t\t<key>Persistent ID</key><string>20E89D1580C31363</string>\n\t\t\t<key>Track Type</key><string>File</string>\n\t\t\t<key>Location</key><string>file:///Music/Alt-J/An%20Awesome%20Wave/03%20Tessellate.mp3</string>\n\t\t\t<key>File Folder Count</key><integer>4</integer>\n\t\t\t<key>Library Folder Count</key><integer>2</integer>\n\t\t</dict>\n\t\t<key>636</key>\n\t\t<dict>\n\t\t\t<key>Track ID</key><integer>636</integer>\n\t\t\t<key>Name</key><string>Breezeblocks</string>\n\t\t\t<key>Artist</key><string>alt-J</string>\n\t\t\t<key>Album Artist</key><string>alt-J</string>\n\t\t\t<key>Album</key><string>An Awesome Wave</string>\n\t\t\t<key>Genre</key><string>Alternative</string>\n\t\t\t<key>Kind</key><string>MPEG audio file</string>\n\t\t\t<key>Size</key><integer>6827195</integer>\n\t\t\t<key>Total Time</key><integer>227082</integer>\n\t\t\t<key>Disc Number</key><integer>1</integer>\n\t\t\t<key>Disc Count</key><integer>1</integer>\n\t\t\t<key>Track Number</key><integer>4</integer>\n\t\t\t<key>Track Count</key><integer>13</integer>\n\t\t\t<key>Year</key><integer>2012</integer>\n\t\t\t<key>Date Modified</key><date>2015-02-02T15:23:08Z</date>\n\t\t\t<key>Date Added</key><date>2014-04-24T09:28:38Z</date>\n\t\t\t<key>Bit Rate</key><integer>237</integer>\n\t\t\t<key>Sample Rate</key><integer>44100</integer>\n\t\t\t<key>Play Count</key><integer>31</integer>\n\t\t\t<key>Play Date</key><integer>3513594051</integer>\n\t\t\t<key>Play Date UTC</key><date>2015-05-04T12:20:51Z</date>\n\t\t\t<key>Skip Count</key><integer>0</integer>\n\t\t\t<key>Rating</key><integer>100</integer>\n\t\t\t<key>Album Rating</key><integer>80</integer>\n\t\t\t<key>Album Rating Computed</key><true/>\n\t\t\t<key>Artwork Count</key><integer>1</integer>\n\t\t\t<key>Sort Album</key><string>Awesome Wave</string>\n\t\t\t<key>Sort Artist</key><string>alt-J</string>\n\t\t\t<key>Persistent ID</key><string>D7017B127B983D38</string>\n\t\t\t<key>Track Type</key><string>File</string>\n\t\t\t<key>Location</key><string>file://localhost/Music/Alt-J/An%20Awesome%20Wave/04%20Breezeblocks.mp3</string>\n\t\t\t<key>File Folder Count</key><integer>4</integer>\n\t\t\t<key>Library Folder Count</key><integer>2</integer>\n\t\t</dict>\n        <key>638</key>\n\t\t<dict>\n\t\t\t<key>Track ID</key><integer>638</integer>\n\t\t\t<key>Name</key><string>❦ (Ripe &#38; Ruin)</string>\n\t\t\t<key>Artist</key><string>alt-J</string>\n\t\t\t<key>Album Artist</key><string>alt-J</string>\n\t\t\t<key>Album</key><string>An Awesome Wave</string>\n\t\t\t<key>Kind</key><string>MPEG audio file</string>\n\t\t\t<key>Size</key><integer>2173293</integer>\n\t\t\t<key>Total Time</key><integer>72097</integer>\n\t\t\t<key>Disc Number</key><integer>1</integer>\n\t\t\t<key>Disc Count</key><integer>1</integer>\n\t\t\t<key>Track Number</key><integer>2</integer>\n\t\t\t<key>Track Count</key><integer>13</integer>\n\t\t\t<key>Year</key><integer>2012</integer>\n\t\t\t<key>Date Modified</key><date>2015-05-09T17:04:53Z</date>\n\t\t\t<key>Date Added</key><date>2015-02-02T15:28:39Z</date>\n\t\t\t<key>Bit Rate</key><integer>233</integer>\n\t\t\t<key>Sample Rate</key><integer>44100</integer>\n\t\t\t<key>Play Count</key><integer>8</integer>\n\t\t\t<key>Play Date</key><integer>3514109973</integer>\n\t\t\t<key>Play Date UTC</key><date>2015-05-10T11:39:33Z</date>\n\t\t\t<key>Skip Count</key><integer>1</integer>\n\t\t\t<key>Skip Date</key><date>2015-02-02T15:29:10Z</date>\n\t\t\t<key>Album Rating</key><integer>80</integer>\n\t\t\t<key>Album Rating Computed</key><true/>\n\t\t\t<key>Artwork Count</key><integer>1</integer>\n\t\t\t<key>Sort Album</key><string>Awesome Wave</string>\n\t\t\t<key>Sort Artist</key><string>alt-J</string>\n\t\t\t<key>Persistent ID</key><string>183699FA0554D0E6</string>\n\t\t\t<key>Track Type</key><string>File</string>\n\t\t\t<key>Location</key><string>file:///Music/Alt-J/An%20Awesome%20Wave/02%20%E2%9D%A6%20(Ripe%20&#38;%20Ruin).mp3</string>\n\t\t\t<key>File Folder Count</key><integer>4</integer>\n\t\t\t<key>Library Folder Count</key><integer>2</integer>\n\t\t</dict>\n\t</dict>\n\t<key>Playlists</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>Name</key><string>Library</string>\n\t\t\t<key>Master</key><true/>\n\t\t\t<key>Playlist ID</key><integer>11480</integer>\n\t\t\t<key>Playlist Persistent ID</key><string>CD6FF684E7A6A166</string>\n\t\t\t<key>Visible</key><false/>\n\t\t\t<key>All Items</key><true/>\n\t\t\t<key>Playlist Items</key>\n\t\t\t<array>\n\t\t\t\t<dict>\n\t\t\t\t\t<key>Track ID</key><integer>634</integer>\n\t\t\t\t</dict>\n\t\t\t\t<dict>\n\t\t\t\t\t<key>Track ID</key><integer>636</integer>\n\t\t\t\t</dict>\n                <dict>\n\t\t\t\t\t<key>Track ID</key><integer>638</integer>\n\t\t\t\t</dict>\n\t\t\t</array>\n\t\t</dict>\n\t\t<dict>\n\t\t\t<key>Name</key><string>Music</string>\n\t\t\t<key>Playlist ID</key><integer>16906</integer>\n\t\t\t<key>Playlist Persistent ID</key><string>4FB2E64E0971DD45</string>\n\t\t\t<key>Distinguished Kind</key><integer>4</integer>\n\t\t\t<key>Music</key><true/>\n\t\t\t<key>All Items</key><true/>\n\t\t\t<key>Playlist Items</key>\n\t\t\t<array>\n\t\t\t\t<dict>\n\t\t\t\t\t<key>Track ID</key><integer>634</integer>\n\t\t\t\t</dict>\n\t\t\t\t<dict>\n\t\t\t\t\t<key>Track ID</key><integer>636</integer>\n\t\t\t\t</dict>\n                <dict>\n\t\t\t\t\t<key>Track ID</key><integer>638</integer>\n\t\t\t\t</dict>\n\t\t\t</array>\n\t\t</dict>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "test/rsrc/itunes_library_windows.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\r\n<plist version=\"1.0\">\r\n<dict>\r\n\t<key>Major Version</key><integer>1</integer>\r\n\t<key>Minor Version</key><integer>1</integer>\r\n\t<key>Date</key><date>2015-05-11T15:27:14Z</date>\r\n\t<key>Application Version</key><string>12.1.2.27</string>\r\n\t<key>Features</key><integer>5</integer>\r\n\t<key>Show Content Ratings</key><true/>\r\n\t<key>Music Folder</key><string>file://localhost/C:/Documents%20and%20Settings/Owner/My%20Documents/My%20Music/iTunes/iTunes%20Media/</string>\r\n\t<key>Library Persistent ID</key><string>B4C9F3EE26EFAF78</string>\r\n\t<key>Tracks</key>\r\n\t<dict>\r\n\t\t<key>180</key>\r\n\t\t<dict>\r\n\t\t\t<key>Track ID</key><integer>180</integer>\r\n            <key>Name</key><string>Tessellate</string>\r\n\t\t\t<key>Artist</key><string>alt-J</string>\r\n\t\t\t<key>Album Artist</key><string>alt-J</string>\r\n\t\t\t<key>Album</key><string>An Awesome Wave</string>\r\n\t\t\t<key>Genre</key><string>Alternative</string>\r\n\t\t\t<key>Kind</key><string>MPEG audio file</string>\r\n\t\t\t<key>Size</key><integer>5525212</integer>\r\n\t\t\t<key>Total Time</key><integer>182674</integer>\r\n\t\t\t<key>Disc Number</key><integer>1</integer>\r\n\t\t\t<key>Disc Count</key><integer>1</integer>\r\n\t\t\t<key>Track Number</key><integer>3</integer>\r\n\t\t\t<key>Track Count</key><integer>13</integer>\r\n\t\t\t<key>Year</key><integer>2012</integer>\r\n\t\t\t<key>Date Modified</key><date>2015-02-02T15:23:08Z</date>\r\n\t\t\t<key>Date Added</key><date>2014-04-24T09:28:38Z</date>\r\n\t\t\t<key>Bit Rate</key><integer>238</integer>\r\n\t\t\t<key>Sample Rate</key><integer>44100</integer>\r\n\t\t\t<key>Play Count</key><integer>0</integer>\r\n\t\t\t<key>Play Date</key><integer>3513593824</integer>\r\n\t\t\t<key>Skip Count</key><integer>3</integer>\r\n\t\t\t<key>Skip Date</key><date>2015-02-05T15:41:04Z</date>\r\n\t\t\t<key>Rating</key><integer>80</integer>\r\n\t\t\t<key>Album Rating</key><integer>80</integer>\r\n\t\t\t<key>Album Rating Computed</key><true/>\r\n\t\t\t<key>Artwork Count</key><integer>1</integer>\r\n\t\t\t<key>Sort Album</key><string>Awesome Wave</string>\r\n\t\t\t<key>Sort Artist</key><string>alt-J</string>\r\n\t\t\t<key>Persistent ID</key><string>20E89D1580C31363</string>\r\n\t\t\t<key>Track Type</key><string>File</string>\r\n\t\t\t<key>Location</key><string>file://localhost/G:/Music/Alt-J/An%20Awesome%20Wave/03%20Tessellate.mp3</string>\r\n\t\t\t<key>File Folder Count</key><integer>-1</integer>\r\n\t\t\t<key>Library Folder Count</key><integer>-1</integer>\r\n\t\t</dict>\r\n\t\t<key>183</key>\r\n\t\t<dict>\r\n\t\t\t<key>Track ID</key><integer>183</integer>\r\n\t\t\t<key>Name</key><string>Breezeblocks</string>\r\n\t\t\t<key>Artist</key><string>alt-J</string>\r\n\t\t\t<key>Album Artist</key><string>alt-J</string>\r\n\t\t\t<key>Album</key><string>An Awesome Wave</string>\r\n\t\t\t<key>Genre</key><string>Alternative</string>\r\n\t\t\t<key>Kind</key><string>MPEG audio file</string>\r\n\t\t\t<key>Size</key><integer>6827195</integer>\r\n\t\t\t<key>Total Time</key><integer>227082</integer>\r\n\t\t\t<key>Disc Number</key><integer>1</integer>\r\n\t\t\t<key>Disc Count</key><integer>1</integer>\r\n\t\t\t<key>Track Number</key><integer>4</integer>\r\n\t\t\t<key>Track Count</key><integer>13</integer>\r\n\t\t\t<key>Year</key><integer>2012</integer>\r\n\t\t\t<key>Date Modified</key><date>2015-02-02T15:23:08Z</date>\r\n\t\t\t<key>Date Added</key><date>2014-04-24T09:28:38Z</date>\r\n\t\t\t<key>Bit Rate</key><integer>237</integer>\r\n\t\t\t<key>Sample Rate</key><integer>44100</integer>\r\n\t\t\t<key>Play Count</key><integer>31</integer>\r\n\t\t\t<key>Play Date</key><integer>3513594051</integer>\r\n\t\t\t<key>Play Date UTC</key><date>2015-05-04T12:20:51Z</date>\r\n\t\t\t<key>Skip Count</key><integer>0</integer>\r\n\t\t\t<key>Rating</key><integer>100</integer>\r\n\t\t\t<key>Album Rating</key><integer>80</integer>\r\n\t\t\t<key>Album Rating Computed</key><true/>\r\n\t\t\t<key>Artwork Count</key><integer>1</integer>\r\n\t\t\t<key>Sort Album</key><string>Awesome Wave</string>\r\n\t\t\t<key>Sort Artist</key><string>alt-J</string>\r\n\t\t\t<key>Persistent ID</key><string>D7017B127B983D38</string>\r\n\t\t\t<key>Track Type</key><string>File</string>\r\n\t\t\t<key>Location</key><string>file://localhost/G:/Music/Alt-J/An%20Awesome%20Wave/04%20Breezeblocks.mp3</string>\r\n\t\t\t<key>File Folder Count</key><integer>-1</integer>\r\n\t\t\t<key>Library Folder Count</key><integer>-1</integer>\r\n\t\t</dict>\r\n        <key>638</key>\r\n\t\t<dict>\r\n\t\t\t<key>Track ID</key><integer>638</integer>\r\n\t\t\t<key>Name</key><string>❦ (Ripe &#38; Ruin)</string>\r\n\t\t\t<key>Artist</key><string>alt-J</string>\r\n\t\t\t<key>Album Artist</key><string>alt-J</string>\r\n\t\t\t<key>Album</key><string>An Awesome Wave</string>\r\n\t\t\t<key>Kind</key><string>MPEG audio file</string>\r\n\t\t\t<key>Size</key><integer>2173293</integer>\r\n\t\t\t<key>Total Time</key><integer>72097</integer>\r\n\t\t\t<key>Disc Number</key><integer>1</integer>\r\n\t\t\t<key>Disc Count</key><integer>1</integer>\r\n\t\t\t<key>Track Number</key><integer>2</integer>\r\n\t\t\t<key>Track Count</key><integer>13</integer>\r\n\t\t\t<key>Year</key><integer>2012</integer>\r\n\t\t\t<key>Date Modified</key><date>2015-05-09T17:04:53Z</date>\r\n\t\t\t<key>Date Added</key><date>2015-02-02T15:28:39Z</date>\r\n\t\t\t<key>Bit Rate</key><integer>233</integer>\r\n\t\t\t<key>Sample Rate</key><integer>44100</integer>\r\n\t\t\t<key>Play Count</key><integer>8</integer>\r\n\t\t\t<key>Play Date</key><integer>3514109973</integer>\r\n\t\t\t<key>Play Date UTC</key><date>2015-05-10T11:39:33Z</date>\r\n\t\t\t<key>Skip Count</key><integer>1</integer>\r\n\t\t\t<key>Skip Date</key><date>2015-02-02T15:29:10Z</date>\r\n\t\t\t<key>Album Rating</key><integer>80</integer>\r\n\t\t\t<key>Album Rating Computed</key><true/>\r\n\t\t\t<key>Artwork Count</key><integer>1</integer>\r\n\t\t\t<key>Sort Album</key><string>Awesome Wave</string>\r\n\t\t\t<key>Sort Artist</key><string>alt-J</string>\r\n\t\t\t<key>Persistent ID</key><string>183699FA0554D0E6</string>\r\n\t\t\t<key>Track Type</key><string>File</string>\r\n\t\t\t<key>Location</key><string>file://localhost/G:/Experiments/Alt-J/An%20Awesome%20Wave/02%20%E2%9D%A6%20(Ripe%20&#38;%20Ruin).mp3</string>\r\n\t\t\t<key>File Folder Count</key><integer>4</integer>\r\n\t\t\t<key>Library Folder Count</key><integer>2</integer>\r\n\t\t</dict>\r\n\t</dict>\r\n\t<key>Playlists</key>\r\n\t<array>\r\n\t\t<dict>\r\n\t\t\t<key>Name</key><string>Bibliotheek</string>\r\n\t\t\t<key>Master</key><true/>\r\n\t\t\t<key>Playlist ID</key><integer>72</integer>\r\n\t\t\t<key>Playlist Persistent ID</key><string>728AA5B1D00ED23B</string>\r\n\t\t\t<key>Visible</key><false/>\r\n\t\t\t<key>All Items</key><true/>\r\n\t\t\t<key>Playlist Items</key>\r\n\t\t\t<array>\r\n\t\t\t\t<dict>\r\n\t\t\t\t\t<key>Track ID</key><integer>180</integer>\r\n\t\t\t\t</dict>\r\n\t\t\t\t<dict>\r\n\t\t\t\t\t<key>Track ID</key><integer>183</integer>\r\n\t\t\t\t</dict>\r\n                <dict>\r\n\t\t\t\t\t<key>Track ID</key><integer>638</integer>\r\n\t\t\t\t</dict>\r\n\t\t\t</array>\r\n\t\t</dict>\r\n\t\t<dict>\r\n\t\t\t<key>Name</key><string>Muziek</string>\r\n\t\t\t<key>Playlist ID</key><integer>103</integer>\r\n\t\t\t<key>Playlist Persistent ID</key><string>8120A002B0486AD7</string>\r\n\t\t\t<key>Distinguished Kind</key><integer>4</integer>\r\n\t\t\t<key>Music</key><true/>\r\n\t\t\t<key>All Items</key><true/>\r\n\t\t\t<key>Playlist Items</key>\r\n\t\t\t<array>\r\n\t\t\t\t<dict>\r\n\t\t\t\t\t<key>Track ID</key><integer>180</integer>\r\n\t\t\t\t</dict>\r\n\t\t\t\t<dict>\r\n\t\t\t\t\t<key>Track ID</key><integer>183</integer>\r\n\t\t\t\t</dict>\r\n                <dict>\r\n\t\t\t\t\t<key>Track ID</key><integer>638</integer>\r\n\t\t\t\t</dict>\r\n\t\t\t</array>\r\n\t\t</dict>\r\n\t</array>\r\n</dict>\r\n</plist>\r\n"
  },
  {
    "path": "test/rsrc/lyrics/examplecom/beetssong.txt",
    "content": "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"\r\n\"http://www.w3.org/TR/html4/loose.dtd\">\r\n<html>\r\n<head>\r\n<title>John Doe - beets song Lyrics</title>\r\n  <!--//\r\n  John Doe beets song lyrics\r\n  //-->\r\n  <meta http-equiv=\"Content-Type\" content=\"text/html;charset=utf-8\">\r\n  <META HTTP-EQUIV=\"Window-Target\" CONTENT=\"_top\">\r\n  <META NAME=\"ROBOTS\" CONTENT=\"INDEX,FOLLOW\">\r\n  <META NAME=\"Description\" CONTENT=\"John Doe beets song lyrics starting with beets song, children at your feet  Wonder how you manage to make ends meet  Who find the money when yo\">\r\n  <META NAME=\"Title\" CONTENT=\"John Doe - beets song Lyrics\">\r\n  <META NAME=\"Keywords\" CONTENT=\"John Doe - beets song lyrics, John Doe lyrics, beets song lyrics, lyrics, song lyric, music lyric, mp3 search, mp3 downloads, music videos\">\r\n<link rel=\"stylesheet\" href=\"Styles.css\" type=\"text/css\">\r\n<SCRIPT LANGUAGE=\"JavaScript\" type=\"text/javascript\">\r\n<!--\r\n\r\n//Function creates new image object\r\nvar url=\"http://www.example.com/Song18148.aspx\";\r\nvar title=\"John Doe - beets song lyrics\";\r\n\r\nfunction newImage(arg) {\r\n\tif (document.images) {\r\n\t\trslt = new Image();\r\n\t\trslt.src = arg;\r\n\t\treturn rslt;\r\n\t}\r\n}\r\n\r\n//Function will flip the button image\r\nfunction changeImages() {\r\n    if (document.images && (preloadFlag == true)) {\r\n\t\tfor (var i=0; i<changeImages.arguments.length; i+=2) {\r\n\t\t\tdocument[changeImages.arguments[i]].src = changeImages.arguments[i+1];\r\n\t\t}\r\n\t}\r\n}\r\n\r\nfunction favorites(){\r\nif(document.all)\r\nwindow.external.AddFavorite(url,title);\r\n}\r\n//Function preloads the image specified\r\nvar preloadFlag = false;\r\nfunction preloadImages() {\r\n\t\tpreloadFlag = true;\r\n\tif (document.images) {\r\n\t}\r\n}\r\n\r\nfunction emoticon(text) {\r\n\tvar txtarea = document.post.message;\r\n\ttext = ' ' + text + ' ';\r\n\tif (txtarea.createTextRange && txtarea.caretPos) {\r\n\t\tvar caretPos = txtarea.caretPos;\r\n\t\tcaretPos.text = caretPos.text.charAt(caretPos.text.length - 1) == ' ' ? caretPos.text + text + ' ' : caretPos.text + text;\r\n\t\ttxtarea.focus();\r\n\t} else {\r\n\t\ttxtarea.value  += text;\r\n\t\ttxtarea.focus();\r\n\t}\r\n}\r\nfunction storeCaret(textEl) {\r\n\tif (textEl.createTextRange) textEl.caretPos = document.selection.createRange().duplicate();\r\n}\r\nfunction callSelect(controlID)\r\n{\r\n   var control = document.getElementById(controlID);\r\n\tif (control) control.select();\r\n}\r\n\r\n-->\r\n</SCRIPT>\r\n<script src=\"functions.js\" type=\"text/javascript\">\r\n</script>\r\n\r\n<style type=\"text/css\">\r\n.black {font:8pt arial; color:#000000;}\r\n.grey {font:8pt arial; color:#989898;}\r\n</style>\r\n</head>\r\n<body onLoad=\"\">\r\n<div style=\"width:100%; float:left\" align=\"center\"><div style=\"width:1003px; text-align:left\"><h1 class=\"Text2\">beets song Lyrics</h1><div id=\"logo\"> <div id=\"logoimg\"><a href=\"http://www.example.com\"><img src=\"images/example.gif\" alt=\"Music Lyrics\" width=\"193\" height=\"89\"></a></div><div id=\"menuads\" align=\"center\" ><script type=\"text/javascript\"><!--\r\ne9 = new Object();\r\ne9.size = \"728x90,468x60\";\r\n//--></script>\r\n\r\n</div>  <script type=\"text/javascript\">\r\nfunction hidePass()\r\n{\r\n  document.getElementById('first').style.display = \"none\";\r\n  document.getElementById('sec').style.display = \"block\";\r\n  document.getElementById('pass1').focus();\r\n}\r\nfunction hideUsername()\r\n{\r\n  if(document.getElementById('uname').value == \"Username\")\r\n  {\r\n      document.getElementById('uname').value = \"\";\r\n      document.getElementById('uname').className = 'black';\r\n  }\r\n}\r\nfunction showUsername()\r\n{\r\n  if(document.getElementById('uname').value.length == 0)\r\n  {\r\n      document.getElementById('uname').className = 'grey';\r\n      document.getElementById('uname').value = \"Username\";\r\n  }\r\n}\r\nfunction showPass()\r\n{\r\n  if(document.getElementById('pass1').value.length == 0)\r\n  {\r\n      document.getElementById('sec').style.display = \"none\";\r\n      document.getElementById('first').style.display = \"block\";\r\n  }\r\n}\r\n</script>\r\n      <table width=\"69px\" cellspacing=0 style=\"border-color:#BDCCDC; padding-left:2px;\" border=\"0\">\r\n        <tr>\r\n          <td>\r\n            <form name=\"login\" action=\"Login.aspx\" method=\"post\"><table width=\"100%\" cellspacing=\"0\">\r\n              <tr><td><input type=\"hidden\" name=\"Submitted\" value=\"1\">\r\n                <input type=\"text\" id=\"uname\"  class=\"grey\" name=\"Username\" value=\"Username\" style=\"width:66px\" onFocus=\"hideUsername()\" onBlur=\"showUsername()\">\r\n              </td></tr><tr>\r\n                <td><div id=\"first\"><input type=\"text\" name=\"ptext\" value=\"Password\" id=\"pass\" class=\"grey\" style=\"width:66px\" onFocus=\"hidePass()\"></div><div id=\"sec\" style=\"display:none\"><input type=\"password\"  class=\"black\" name=\"Password\" value=\"\" style=\"width:66px;\" id=\"pass1\" onBlur=\"showPass()\">\r\n              </div></td></tr><tr><td align=\"left\"><input type=\"submit\" value=\"LOGIN\" id=\"Button\">\r\n            </td></tr><tr>\r\n                <td align=\"left\">\r\n                  <a href=\"Default.aspx?Page=Login&amp;Cmd=Register\" class=\"FrmLink\" style=\"font-size:8pt;\">Register</a>\r\n                </td></tr></table></form></td></tr>\r\n      </table>\r\n</div><div id=\"maindiv\">\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n        <div id=\"leftdiv\">\r\n        <div id=\"b2\"><ul><li><div class=\"bhz\"><span class=\"ht1\">&nbsp;</span><span class=\"hw2\">Navigation </span></div></li>     <li><a href=\"http://www.example.com/\" title=\"Music Lyrics\"\r\n       >Music Lyrics</a></li>\r\n      <li><a href=\"New50.aspx\" title=\"New Lyrics\"\r\n       >New Lyrics</a></li>\r\n      <li><a href=\"Popular50.aspx\" title=\"Most Popular\"\r\n       >Most Popular</a></li>\r\n      <li><a href=\"Add.aspx\" title=\"Add Lyrics\"\r\n       >Add Lyrics</a></li>\r\n      <li><a href=\"About.aspx\" title=\"About\"\r\n       >About</a></li>\r\n      <li><a href=\"Contact.aspx\" title=\"Contact\"\r\n       >Contact</a></li>\r\n                  </ul>\r\n\r\n\r\n\r\n        <br>\r\n\r\n\r\n         <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"160px\"  style=\"border: 0px solid #D60000; font-family: verdana; color: #333333; text-align: justify\">\r\n         <tr>\r\n           <td>\r\n                    <iframe align=\"middle\" width=\"163px\" scrolling=\"no\" height=\"220\" frameborder=\"0\"\r\n                        marginwidth=\"0\" marginheight=\"0\" style=\"border: 0px none; padding-left:4px\"\r\n                      src=\"http://jmn.jangonetwork.com/sm?cust_params=j_artist=John Doe&amp;j_title=beets song\"></iframe>\r\n                    </td>\r\n             </tr>\r\n         </table><br>\r\n\r\n         <div id=\"leftsideadtext\"></div>\r\n\r\n          </div>\r\n\r\n        <div style=\"width:163px;\" align=\"center\">\r\n       <div style=\"padding-bottom:10px;padding-left:5px;\">\r\n\r\n           <script type=\"text/javascript\"><!--\r\ne9 = new Object();\r\ne9.size = \"120x600, 160x600\";\r\n//--></script>\r\n<script type=\"text/javascript\" src=\"http://somelink.com/tags/lyricsbaycom/examplecom/tags.js\">\r\n</script>\r\n\r\n       </div>\r\n       </div>\r\n    <br><table width=\"157px\" align=\"center\" style=\"border-color:#BDCCDC\" border=\"1\"><tr><td colspan=\"2\" class=\"SubTitle\" bgcolor=\"#BDCCDC\" align=\"center\">Other lyrics by John Doe</td></tr><tr><td valign=\"top\" class=\"Text1\" align=\"left\" style=\"border: medium none\"><font color=\"black\" face=\"arial\" size=\"2\">1. </font></td><td class=\"Text1\" align=\"left\" style=\"border: medium none\"><a class=\"regular\" title=\"Boys Lyrics\" href=\"Song18045-The-John Doe-Boys-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Boys lyrics</font></a></td></tr><tr><td valign=\"top\" class=\"Text1\" align=\"left\" style=\"border: medium none\"><font color=\"black\" face=\"arial\" size=\"2\">2. </font></td><td class=\"Text1\" align=\"left\" style=\"border: medium none\"><a class=\"regular\" title=\"Crying, Waiting, Hoping Lyrics\" href=\"Song18054-The-John Doe-Crying-Waiting-Hoping-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Crying, Waiting, Hoping lyrics</font></a></td></tr><tr><td valign=\"top\" class=\"Text1\" align=\"left\" style=\"border: medium none\"><font color=\"black\" face=\"arial\" size=\"2\">3. </font></td><td class=\"Text1\" align=\"left\" style=\"border: medium none\"><a class=\"regular\" title=\"Helter Skelter Lyrics\" href=\"Song18092-The-John Doe-Helter-Skelter-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Helter Skelter lyrics</font></a></td></tr><tr><td valign=\"top\" class=\"Text1\" align=\"left\" style=\"border: medium none\"><font color=\"black\" face=\"arial\" size=\"2\">4. </font></td><td class=\"Text1\" align=\"left\" style=\"border: medium none\"><a class=\"regular\" title=\"I Forgot To Remember To Forget Lyrics\" href=\"Song18107-The-John Doe-I-Forgot-To-Remember-To-Forget-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">I Forgot To Remember To Forget lyrics</font></a></td></tr><tr><td valign=\"top\" class=\"Text1\" align=\"left\" style=\"border: medium none\"><font color=\"black\" face=\"arial\" size=\"2\">5. </font></td><td class=\"Text1\" align=\"left\" style=\"border: medium none\"><a class=\"regular\" title=\"Johnny B. Goode Lyrics\" href=\"Song18142-The-John Doe-Johnny-B-Goode-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Johnny B. Goode lyrics</font></a></td></tr><tr><td valign=\"top\" class=\"Text1\" align=\"left\" style=\"border: medium none\"><font color=\"black\" face=\"arial\" size=\"2\">6. </font></td><td class=\"Text1\" align=\"left\" style=\"border: medium none\"><a class=\"regular\" title=\"Maggie Mae Lyrics\" href=\"Song18163-The-John Doe-Maggie-Mae-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Maggie Mae lyrics</font></a></td></tr><tr><td valign=\"top\" class=\"Text1\" align=\"left\" style=\"border: medium none\"><font color=\"black\" face=\"arial\" size=\"2\">7. </font></td><td class=\"Text1\" align=\"left\" style=\"border: medium none\"><a class=\"regular\" title=\"Mailman, Bring Me No More Blues Lyrics\" href=\"Song18165-The-John Doe-Mailman-Bring-Me-No-More-Blues-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Mailman, Bring Me No More Blues lyrics</font></a></td></tr><tr><td valign=\"top\" class=\"Text1\" align=\"left\" style=\"border: medium none\"><font color=\"black\" face=\"arial\" size=\"2\">8. </font></td><td class=\"Text1\" align=\"left\" style=\"border: medium none\"><a class=\"regular\" title=\"Memphis, Tennessee Lyrics\" href=\"Song18170-The-John Doe-Memphis-Tennessee-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Memphis, Tennessee lyrics</font></a></td></tr><tr><td valign=\"top\" class=\"Text1\" align=\"left\" style=\"border: medium none\"><font color=\"black\" face=\"arial\" size=\"2\">9. </font></td><td class=\"Text1\" align=\"left\" style=\"border: medium none\"><a class=\"regular\" title=\"Oh Darling Lyrics\" href=\"Song18187-The-John Doe-Oh-Darling-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Oh Darling lyrics</font></a></td></tr><tr><td valign=\"top\" class=\"Text1\" align=\"left\" style=\"border: medium none\"><font color=\"black\" face=\"arial\" size=\"2\">10. </font></td><td class=\"Text1\" align=\"left\" style=\"border: medium none\"><a class=\"regular\" title=\"Till There Was You Lyrics\" href=\"Song18258-The-John Doe-Till-There-Was-You-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Till There Was You lyrics</font></a></td></tr><tr><td valign=\"top\" class=\"Text1\" align=\"left\" style=\"border: medium none\"><font color=\"black\" face=\"arial\" size=\"2\">11. </font></td><td class=\"Text1\" align=\"left\" style=\"border: medium none\"><a class=\"regular\" title=\"Yesterday Lyrics\" href=\"Song18281-The-John Doe-Yesterday-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Yesterday lyrics</font></a></td></tr><tr><td valign=\"top\" class=\"Text1\" align=\"left\" style=\"border: medium none\"><font color=\"black\" face=\"arial\" size=\"2\">12. </font></td><td class=\"Text1\" align=\"left\" style=\"border: medium none\"><a class=\"regular\" title=\"The Streets Of London Lyrics\" href=\"Song155676-The-John Doe-The-Streets-Of-London-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">The Streets Of London lyrics</font></a></td></tr><tr><td valign=\"top\" class=\"Text1\" align=\"left\" style=\"border: medium none\"><font color=\"black\" face=\"arial\" size=\"2\">13. </font></td><td class=\"Text1\" align=\"left\" style=\"border: medium none\"><a class=\"regular\" title=\"When I'm Sixty-Four (Lennon/McCartney) Lyrics\" href=\"Song478768-The-John Doe-When-Im-Sixty-Four-LennonMcCartney-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">When I'm Sixty-Four (Lennon/McCartney) lyrics</font></a></td></tr><tr><td valign=\"top\" class=\"Text1\" align=\"left\" style=\"border: medium none\"><font color=\"black\" face=\"arial\" size=\"2\">14. </font></td><td class=\"Text1\" align=\"left\" style=\"border: medium none\"><a class=\"regular\" title=\"Being for the Benefit of Mr. Kite! (Lennon/McCartney) Lyrics\" href=\"Song478833-The-John Doe-Being-for-the-Benefit-of-Mr-Kite-LennonMcCartney-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Being for the Benefit of Mr. Kite! (Lennon/McCartney) lyrics</font></a></td></tr><tr><td valign=\"top\" class=\"Text1\" align=\"left\" style=\"border: medium none\"><font color=\"black\" face=\"arial\" size=\"2\">15. </font></td><td class=\"Text1\" align=\"left\" style=\"border: medium none\"><a class=\"regular\" title=\"Blue Jay Way (Harrison) Lyrics\" href=\"Song478837-The-John Doe-Blue-Jay-Way-Harrison-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Blue Jay Way (Harrison) lyrics</font></a></td></tr></table><br>\r\n    </div>\r\n    <div id=\"centertdivNew\">\r\n    <div align=\"center\" style=\"margin-left:0px;margin-bottom:5px;\">\r\n\r\n        <div style=\"margin-left:0px;margin-bottom:5px;width:100%;\" align=\"center\">\r\n\r\n          </div>\r\n            <div style=\"width:100%;\" align=\"center\">\r\n                <div>\r\n\r\n    </div>\r\n    <div style=\"width:100%;\" align=\"center\">\r\n                    [<a class=\"regular\" href=\"http://www.example.com/New50.aspx\" >New lyrics</a>]\r\n                   </div></div></div>\r\n    <br><br><strong>John Doe beets song lyrics</strong>\r\n\r\n\r\n\r\n                    <form action=\"Default.aspx\" method=\"get\">\r\n                    <input type=\"hidden\" name=\"Page\" value=\"Search\">\r\n                    <input type=\"radio\" name=\"W\" value=\"Lyrics\" checked> Lyrics\r\n                    search for Artist - Song: <input type=\"text\" name=\"Criteria\" size=15>\r\n                    <input type=\"button\" onClick=\"verify2()\" value=\"SEARCH\" id=\"Button2\">\r\n                    </form>\r\n\r\n        <hr><strong>Back to the: <a title=\"Music Lyrics\" href=\"http://www.example.com/\">Music Lyrics</a></strong> <strong> > </strong> <a class=\"\" href=\"Artist1638-The-John Doe-lyrics.aspx\" title=\"John Doe Lyrics\"> <strong>John Doe lyrics</strong></a><strong> > </strong><strong>beets song lyrics</strong><br>\r\n        <table width=\"100%\">\r\n                      <tr>\r\n                       <td class=\"Text\" valign=\"top\" align=\"center\">\r\n\r\n        <h2>John Doe <br> beets song lyrics</h2>\r\n<img src=\"images/phone-left.gif\" alt=\"Ringtones left icon\" width=\"16\" height=\"17\"> <a href=\"http://www.ringtonematcher.com/go/?sid=LBSMros&amp;artist=The+John Doe&amp;song=Beets+Song\" target=\"_blank\"><b><font size=\"+1\" color=\"red\" face=\"arial\">Send \"beets song\" Ringtone to your Cell</font></b></a> <img src=\"images/phone-right.gif\" alt=\"Ringtones right icon\" width=\"16\" height=\"17\"><br><br><center>Beets is the media library management system for obsessive music geeks.<br>\r\nThe purpose of beets is to get your music collection right once and for all. It catalogs your collection, automatically improving its metadata as it goes. It then provides a suite of tools for manipulating and accessing your music.<br>\r\n<div class='flow breaker'>           </div>\r\nHere's an example of beets' brainy tag corrector doing its thing:\r\nBecause beets is designed as a library, it can do almost anything you can imagine for your music collection. Via plugins, beets becomes a panacea</center>\r\n<img src=\"images/phone-left.gif\" alt=\"Ringtones left icon\" width=\"16\" height=\"17\"> <a href=\"http://www.ringtonematcher.com/go/?sid=LBSMros&amp;artist=The+John Doe&amp;song=Beets+Song\" target=\"_blank\"><b><font size=\"+1\" color=\"red\" face=\"arial\">Send \"beets song\" Ringtone to your Cell</font></b></a> <img src=\"images/phone-right.gif\" alt=\"Ringtones right icon\" width=\"16\" height=\"17\"><br><br>\r\n        <center>\r\n         <font color=\"black\" size=\"2\" face=\"arial\">Share <strong>beets song lyrics</strong></font><br><p style=\"height: 1px; margin: 3pt; padding: 0pt;\"></p>\r\n\r\n                    <iframe src=\"http://www.facebook.com/plugins/like.php?href=&amp;layout=standard&amp;show_faces=true&amp;action=like&amp;font=arial&amp;colorscheme=light&amp;height=40\" scrolling=\"no\" frameborder=\"0\" style=\"border:none; overflow:hidden;height:67px;\"></iframe>\r\n\r\n                    </center>&nbsp;\r\n\r\n                                 RATE THIS SONG! <form action=\"Default.aspx?Page=Vote\" method=\"post\">\r\n                    </select>\r\n                    <input type=\"submit\" value=\"RATE\" id=\"Submit1\" style=\"width:60px;height:26px;background-color:#28414B;color:White;font-size:7pt;text-align:center;vertical-align:middle;font-weight:bold;\"></form><br>\r\n\r\n\r\n       <center style=\"margin-bottom: 10px;\">\r\n       <table cellpadding=\"0\" cellspacing=\"0\">\r\n                      <tr>\r\n                       <td width=\"30\"></td>\r\n                        <td width=\"25\" align=\"center\"><a href=\"javascript:favorites();\"><img src=\"images/favorites.gif\" width=\"20\" height=\"20\" border=\"0\" alt=\"Add to Favorites Lyrics\"></a></td>\r\n\r\n\r\n\r\n                        <td width=25 align=\"center\"><a href=\"Default.aspx?Page=Email&amp;URL=/Song18148-The-John Doe-Lady-Madonna.aspx\"><img src=\"images/email.gif\" width=\"20\" height=\"20\" border=0 alt=\"Email to a Friend John Doe - beets song Lyrics\"></a></td>\r\n                        <td width=\"30\"></td></tr>\r\n                     <tr>\r\n                       <td height=\"15\" colspan=\"7\"></td></tr>\r\n                      <tr>\r\n\r\n\r\n                    <td valign=\"top\" align=\"center\" class=\"Text\" colspan=\"7\">Rating:\r\n<img height=\"20\" width=\"20\" src=\"images/vote.gif\" alt=\"\"><img height=\"20\" width=\"20\" src=\"images/vote.gif\" alt=\"\"><img height=\"20\" width=\"20\" src=\"images/vote.gif\" alt=\"\"><img height=\"20\" width=\"20\" src=\"images/vote.gif\" alt=\"\"></td></tr>\r\n\r\n                    </table></center>\r\n\r\n\r\n\r\n                    <br><center> Use the following form to post your meaning of this song, rate it, or submit comments about this song.</center><br><br>\r\n\r\n                           0<input type=\"hidden\" name=\"ID\" value=\"18148\">\r\n\r\n                    <form  method=\"post\" action=\"\">\r\n                    <table width=\"100%\">\r\n                      <tr>\r\n                    <td></td>\r\n                  </tr>\r\n                  <tr>\r\n                   <td class=\"Text\" align=\"right\" width=\"50%\">Name: </td>\r\n                    <td><input type=\"text\" name=\"Name\" maxlength=\"50\"></td>\r\n                 </tr>\r\n                 <tr>\r\n                   <td class=\"Text\" align=\"right\" width=\"50%\">Comment: </td>\r\n                   <td><textarea name=\"Comment\" rows=\"5\" cols=\"40\"></textarea></td>\r\n                  </tr>\r\n                  <tr>\r\n                    <td class=\"Text\" align=\"right\" width=\"0%\">Type <b>maps</b> backwards (spam prevention): </td>\r\n                    <td><input type=\"text\" name=\"SP\" maxlength=\"50\"></td>\r\n                  </tr>\r\n                  <tr>\r\n                    <td></td>\r\n                   <td><input type=\"submit\" value=\"SEND COMMENTS\" id=\"Button1\"><br></td>\r\n                  </tr>\r\n                  <tr>\r\n                    <td colspan=\"2\"><hr size=\"1\"></td></tr>\r\n                      <tr>\r\n    <td align=\"center\" colspan=2 class=\"Text\">There are no comments for this song yet.\r\n\r\n\r\n                                    </td></tr></table></form>\r\n                    </table>\r\n\r\n    </div>\r\n\r\n    <div style=\"width:29%;float:left;\">\r\n     <div style=\"width:100%;float:left;margin-bottom:5px;\">\r\n\r\n\r\n         <script type=\"text/javascript\"><!--\r\ne9 = new Object();\r\ne9.size = \"336x280,300x250\";\r\n//--></script>\r\n<script type=\"text/javascript\" src=\"http://example.com/tags/lyricsbaycom/examplecom/tags.js\">\r\n</script>\r\n\r\n     </div>\r\n\r\n     <!--<div style=\"width:100%;float:left;margin-bottom:5px;\">\r\n\r\n\r\n        <iframe src=\"\"  frameborder=\"0\" height=\"500\" width=\"300\" scrolling=\"no\"></iframe>\r\n       </div>-->\r\n\r\n        <div style=\"width:100%;float:left;padding-bottom:10px;\">\r\n\r\n           <table width=\"100%\" align=\"left\" style=\"border-color:#BDCCDC\" border=\"0\"><tr><td><table width=\"100%\" style=\"border-color:#BDCCDC\" border=\"1\"><tr>   <td colspan=\"2\" class=\"SubTitle\" bgcolor=\"#BDCCDC\" align=\"center\">People who viewed John Doe lyrics have also visited</td></tr><tr><td class=\"Text1\" align=\"left\" valign=\"top\"  style=\"border:none\"><font color=\"black\" face=\"arial\" size=\"2\">1. </font></td><td class=\"Text1\" align=\"left\" valign=\"top\" style=\"border:none\"><a class=\"regular\" title=\"Luther Vandross % Janet Jackson Lyrics\" href=\"Artist241-Luther-Vandross--Janet-Jackson-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Luther Vandross % Janet Jackson lyrics</font></a></td></tr><tr><td class=\"Text1\" align=\"left\" valign=\"top\"  style=\"border:none\"><font color=\"black\" face=\"arial\" size=\"2\">2. </font></td><td class=\"Text1\" align=\"left\" valign=\"top\" style=\"border:none\"><a class=\"regular\" title=\"Baby Lyrics\" href=\"Artist7594-Baby-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Baby lyrics</font></a></td></tr><tr><td class=\"Text1\" align=\"left\" valign=\"top\"  style=\"border:none\"><font color=\"black\" face=\"arial\" size=\"2\">3. </font></td><td class=\"Text1\" align=\"left\" valign=\"top\" style=\"border:none\"><a class=\"regular\" title=\"Edie Brickell & The New Bohemians Lyrics\" href=\"Artist9570-Edie-Brickell--The-New-Bohemians-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Edie Brickell & The New Bohemians lyrics</font></a></td></tr><tr><td class=\"Text1\" align=\"left\" valign=\"top\"  style=\"border:none\"><font color=\"black\" face=\"arial\" size=\"2\">4. </font></td><td class=\"Text1\" align=\"left\" valign=\"top\" style=\"border:none\"><a class=\"regular\" title=\"Al Tariq Lyrics\" href=\"Artist16911-Al-Tariq-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Al Tariq lyrics</font></a></td></tr><tr><td class=\"Text1\" align=\"left\" valign=\"top\"  style=\"border:none\"><font color=\"black\" face=\"arial\" size=\"2\">5. </font></td><td class=\"Text1\" align=\"left\" valign=\"top\" style=\"border:none\"><a class=\"regular\" title=\"Defari feat Xzibit The Alkaholiks Phil Da Agony Lyrics\" href=\"Artist20357-Defari-feat-Xzibit-The-Alkaholiks_Phil-Da-Agony-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Defari feat Xzibit The Alkaholiks Phil Da Agony lyrics</font></a></td></tr><tr><td class=\"Text1\" align=\"left\" valign=\"top\"  style=\"border:none\"><font color=\"black\" face=\"arial\" size=\"2\">6. </font></td><td class=\"Text1\" align=\"left\" valign=\"top\" style=\"border:none\"><a class=\"regular\" title=\"Ferradini Marco Lyrics\" href=\"Artist21790-Ferradini-Marco-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Ferradini Marco lyrics</font></a></td></tr><tr><td class=\"Text1\" align=\"left\" valign=\"top\"  style=\"border:none\"><font color=\"black\" face=\"arial\" size=\"2\">7. </font></td><td class=\"Text1\" align=\"left\" valign=\"top\" style=\"border:none\"><a class=\"regular\" title=\"Orkest Klein Lyrics\" href=\"Artist27414-Orkest-Klein-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Orkest Klein lyrics</font></a></td></tr><tr><td class=\"Text1\" align=\"left\" valign=\"top\"  style=\"border:none\"><font color=\"black\" face=\"arial\" size=\"2\">8. </font></td><td class=\"Text1\" align=\"left\" valign=\"top\" style=\"border:none\"><a class=\"regular\" title=\"Missey Elliot f/ Ciara Lyrics\" href=\"Artist32978-Missey-Elliot-f-Ciara-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Missey Elliot f/ Ciara lyrics</font></a></td></tr><tr><td class=\"Text1\" align=\"left\" valign=\"top\"  style=\"border:none\"><font color=\"black\" face=\"arial\" size=\"2\">9. </font></td><td class=\"Text1\" align=\"left\" valign=\"top\" style=\"border:none\"><a class=\"regular\" title=\"MC Ren f/ RBX, Snoop Dogg Lyrics\" href=\"Artist40126-MC-Ren-f-RBX-Snoop-Dogg-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">MC Ren f/ RBX, Snoop Dogg lyrics</font></a></td></tr><tr><td class=\"Text1\" align=\"left\" valign=\"top\"  style=\"border:none\"><font color=\"black\" face=\"arial\" size=\"2\">10. </font></td><td class=\"Text1\" align=\"left\" valign=\"top\" style=\"border:none\"><a class=\"regular\" title=\"DJ Paul f/ Crunchy Black, Lord Infamous Lyrics\" href=\"Artist40793-DJ_Paul-f-Crunchy-Black-Lord-Infamous-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">DJ Paul f/ Crunchy Black, Lord Infamous lyrics</font></a></td></tr><tr><td class=\"Text1\" align=\"left\" valign=\"top\"  style=\"border:none\"><font color=\"black\" face=\"arial\" size=\"2\">11. </font></td><td class=\"Text1\" align=\"left\" valign=\"top\" style=\"border:none\"><a class=\"regular\" title=\"Figurines Lyrics\" href=\"Artist45605-Figurines-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Figurines lyrics</font></a></td></tr><tr><td class=\"Text1\" align=\"left\" valign=\"top\"  style=\"border:none\"><font color=\"black\" face=\"arial\" size=\"2\">12. </font></td><td class=\"Text1\" align=\"left\" valign=\"top\" style=\"border:none\"><a class=\"regular\" title=\"Asher Roth f/ Miguel Lyrics\" href=\"Artist47431-Asher-Roth-f-Miguel-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Asher Roth f/ Miguel lyrics</font></a></td></tr><tr><td class=\"Text1\" align=\"left\" valign=\"top\"  style=\"border:none\"><font color=\"black\" face=\"arial\" size=\"2\">13. </font></td><td class=\"Text1\" align=\"left\" valign=\"top\" style=\"border:none\"><a class=\"regular\" title=\"Barton Fink Lyrics\" href=\"Artist50836-Barton-Fink-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Barton Fink lyrics</font></a></td></tr><tr><td class=\"Text1\" align=\"left\" valign=\"top\"  style=\"border:none\"><font color=\"black\" face=\"arial\" size=\"2\">14. </font></td><td class=\"Text1\" align=\"left\" valign=\"top\" style=\"border:none\"><a class=\"regular\" title=\"Chloe Lyrics\" href=\"Artist54874-Chloe-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Chloe lyrics</font></a></td></tr><tr><td class=\"Text1\" align=\"left\" valign=\"top\"  style=\"border:none\"><font color=\"black\" face=\"arial\" size=\"2\">15. </font></td><td class=\"Text1\" align=\"left\" valign=\"top\" style=\"border:none\"><a class=\"regular\" title=\"Stack Bundles Lyrics\" href=\"Artist55155-Stack-Bundles-lyrics.aspx\"><font color=\"black\" face=\"arial\" size=\"2\">Stack Bundles lyrics</font></a></td></tr></table></td> </tr>\r\n                      </table></div>\r\n\r\n\r\n   <br><br>\r\n               <object id=\"final\" align=\"middle\"type=\"application/x-shockwave-flash\" style=\"width:240px; height:250px;\" data=\"http://www.example.com/scrollsongs.swf?lid=18148\"><param name=\"movie\" value=\"http://www.example.com/scrollsongs.swf?lid=18148\"></object>\r\n\r\n               <table align=\"center\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"292px\"  style=\"padding-top:10px;border: 0px solid #D60000; font-family: verdana; color: #333333; text-align: justify\">\r\n                   <tr>\r\n                       <td>\r\n                           <font color=\"black\" size=\"2\" face=\"arial\"><b>Put this scroller on your page!</b></font><br><br>\r\n\r\n                       <input type=\"text\" name=\"embedcpy\" id=\"embedid\" onclick=\"javascript:document.getElementById('embedid').select();\" value=\"&lt;object codebase='http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0' width='152' height='210' id='final' align='middle'&gt;&lt;embed src='http://www.example.com/scrollsongs.swf?lid=18148' quality='high' width='200' height='210' name='Songs' align='middle' allowScriptAccess='sameDomain' type='application/x-shockwave-flash' pluginspage='http://www.example.com/go/getflashplayer' /&gt; &lt;/object&gt;&lt;br&gt;&lt;a href='http://www.example.com' target='window.open(this.href); return false;'&gt;example&lt;/a&gt; | &lt;a href=&quot;http://www.example.com/Song18148-The-John Doe-Lady-Madonna.aspx&quot; onclick='window.open(this.href); return false;'&gt;beets song lyrics&lt;/a&gt;\" style=\"width: 292px;\" READONLY>\r\n                       </td>\r\n                   </tr>\r\n               </table>\r\n\r\n                </div>\r\n\r\n\r\n\r\n\r\n    </div></div></div>\r\n</div>\r\n<script type=\"text/javascript\">\r\nvar _gaq = _gaq || [];_gaq.push(['_setAccount', 'xxxxxx']);_gaq.push(['_trackPageview']);(function() {var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);})();\r\n</script>\r\n<script type=\"text/javascript\">\r\nvar pageTracker = _gaq._getTracker(\"xxxxx\");\r\npageTracker._initData();\r\npageTracker._trackPageview();\r\n</script>\r\n<script type=\"text/javascript\">\r\ncf_page_artist = 'John Doe';\r\ncf_page_song = 'beets song';\r\ncf_adunit_id = '39381388';\r\ncf_flex = true\r\n</script><script type=\"text/javascript\" src=\"//example.com/showads/showad.php\"></script>\r\n<center><img src=\"images/example.jpg\" alt=\"ToneFuse Music\" title=\"ToneFuse Music\" width=\"150\" height=\"30\"><p style=\"height: 5px; margin: 0pt; padding: 0pt;\"></p></center>\r\n</body>\r\n</html>\r\n"
  },
  {
    "path": "test/rsrc/lyrics/geniuscom/2pacalleyezonmelyrics.txt",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>2Pac – All Eyez On Me Lyrics | Genius Lyrics</title>\n\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n<meta content='width=device-width,initial-scale=1' name='viewport'>\n\n  <meta name=\"apple-itunes-app\" content=\"app-id=709482991\">\n\n<link href=\"https://assets.genius.com/images/apple-touch-icon.png?1720540228\" rel=\"apple-touch-icon\" />\n\n\n  \n\n  <link href=\"https://assets.genius.com/images/apple-touch-icon.png?1720540228\" rel=\"apple-touch-icon\" />\n\n  \n\n  <!-- Mobile IE allows us to activate ClearType technology for smoothing fonts for easy reading -->\n  <meta http-equiv=\"cleartype\" content=\"on\">\n\n\n\n\n<META name=\"y_key\" content=\"f63347d284f184b0\">\n\n<meta property=\"og:site_name\" content=\"Genius\"/>\n<meta property=\"fb:app_id\" content=\"265539304824\" />\n<meta property=\"fb:pages\" content=\"308252472676410\" />\n\n<link title=\"Genius\" type=\"application/opensearchdescription+xml\" rel=\"search\" href=\"https://genius.com/opensearch.xml\">\n\n<script>\n!function(){if('PerformanceLongTaskTiming' in window){var g=window.__tti={e:[]};\ng.o=new PerformanceObserver(function(l){g.e=g.e.concat(l.getEntries())});\ng.o.observe({entryTypes:['longtask']})}}();\n</script>\n\n\n    <!--sse-->\n\n  <script>window['Genius.ads'] = window['Genius.ads'] || [];</script>\n\n\n\n  \n  \n  <script>\n    (function () {\n      const generateUUID = () =>\n        \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, function (c) {\n          const r = (Math.random() * 16) | 0;\n          const v = c === \"x\" ? r : (r & 0x3) | 0x8;\n          return v.toString(16);\n        });\n  \n      let isLocalStorageAvailable = false;\n      const isAvailable = () => {\n        if (isLocalStorageAvailable) {\n          return true;\n        }\n  \n        try {\n          const x = \"__storage_test__\";\n          window.localStorage.setItem(x, x);\n          window.localStorage.removeItem(x);\n          isLocalStorageAvailable = true;\n          return true;\n        } catch (e) {\n          return false;\n        }\n      };\n  \n      const unsafeSetItem = (key, value) => {\n        window.localStorage.setItem(key, JSON.stringify(value));\n      };\n  \n      const unsafeGetItem = (key) => {\n        return JSON.parse(window.localStorage.getItem(key) || \"null\");\n      };\n  \n      const setSafeItem = (key, value) => {\n        if (isAvailable()) {\n          unsafeSetItem(key, value);\n        }\n      };\n  \n      const getSafeItem = (key, defaultValue) => {\n        return isAvailable() ? unsafeGetItem(key) : defaultValue;\n      };\n  \n      const ANA_UID_KEY = \"ana_uid\";\n      let currentUid = getSafeItem(ANA_UID_KEY, \"\");\n      if (!currentUid) {\n        currentUid = generateUUID();\n        setSafeItem(ANA_UID_KEY, currentUid);\n      }\n      window.getAnaUid = () => currentUid;\n    })();\n  </script>\n\n\n  \n  <link as=\"script\" href=\"https://securepubads.g.doubleclick.net/tag/js/gpt.js\" rel=\"preload\" /><script async=\"true\" src=\"https://securepubads.g.doubleclick.net/tag/js/gpt.js\" type=\"text/javascript\"></script>\n  <script>\n!function(a9,a,p,s,t,A,g){if(a[a9])return;function q(c,r){a[a9]._Q.push([c,r])}a[a9]={init:function(){q(\"i\",arguments)},fetchBids:function(){q(\"f\",arguments)},setDisplayBids:function(){},targetingKeys:function(){return[]},_Q:[]};A=p.createElement(s);A.async=!0;A.src=t;g=p.getElementsByTagName(s)[0];g.parentNode.insertBefore(A,g)}(\"apstag\",window,document,\"script\",\"//c.amazon-adsystem.com/aax2/apstag.js\");\n</script>\n\n  <link as=\"script\" href=\"https://d3l739e8r8y9v7.cloudfront.net/script.js\" rel=\"preload\" /><script async=\"true\" src=\"https://d3l739e8r8y9v7.cloudfront.net/script.js\" type=\"text/javascript\"></script>\n\n  <script>\n    window['Genius.ads'].push(function(ads) {\n      var config = {\"ad_placements\":{\"amp_article_sticky\":{\"sizes\":[[300,50],[320,50]],\"a9\":true,\"kv\":{\"is_atf\":false}},\"amp_article_footer\":{\"sizes\":[[300,250]],\"a9\":true,\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"amp_mw_article_footer_prebid\"}}]},\"amp_album_sticky\":{\"sizes\":[[320,50]],\"a9\":true,\"prebid_placements\":[{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"amp_mw_album_sticky_prebid\"}}]},\"amp_album_footer\":{\"sizes\":[[300,250]],\"a9\":true,\"prebid_placements\":[{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"amp_mw_album_footer_prebid\"}}]},\"amp_song_annotation\":{\"sizes\":[[300,250]],\"a9\":true,\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11294868\"}},{\"bidder\":\"ix\",\"params\":{\"id\":10,\"siteID\":194719}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"107816\",\"zoneId\":\"613918\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"amp_song_annotation_prebid\"}}]},\"amp_song_below_player\":{\"sizes\":[[300,250]],\"a9\":true,\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"amp_song_below_player_prebid\"}}]},\"amp_song_below_song_bio\":{\"sizes\":[[300,250]],\"a9\":true,\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11294916\"}},{\"bidder\":\"ix\",\"params\":{\"id\":8,\"siteID\":194716}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"107816\",\"zoneId\":\"613910\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"amp_song_below_song_bio_prebid\"}}]},\"amp_song_leaderboard\":{\"sizes\":[[320,50]],\"a9\":true,\"kv\":{\"is_atf\":true},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"13937404\"}},{\"bidder\":\"ix\",\"params\":{\"id\":25,\"siteID\":300787}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"107816\",\"zoneId\":\"1051268\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"amp_song_leaderboard_prebid\"}}]},\"amp_song_medium1\":{\"sizes\":[[300,250],[320,480]],\"a9\":true,\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11294891\"}},{\"bidder\":\"ix\",\"params\":{\"id\":1,\"siteID\":194712}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"107816\",\"zoneId\":\"613900\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"amp_song_medium1_prebid\"}}]},\"amp_song_medium2\":{\"sizes\":[[300,250]],\"a9\":true,\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11294897\"}},{\"bidder\":\"ix\",\"params\":{\"id\":5,\"siteID\":194713}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"107816\",\"zoneId\":\"613902\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"amp_song_medium2_prebid\"}}]},\"amp_song_medium3\":{\"sizes\":[[300,250]],\"a9\":true,\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11294900\"}},{\"bidder\":\"ix\",\"params\":{\"id\":6,\"siteID\":194714}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"107816\",\"zoneId\":\"613906\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"amp_song_medium3_prebid\"}}]},\"amp_song_medium_footer\":{\"sizes\":[[300,250]],\"a9\":true,\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11294876\"}},{\"bidder\":\"ix\",\"params\":{\"id\":7,\"siteID\":194715}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"107816\",\"zoneId\":\"613908\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"amp_song_medium_footer_prebid\"}}]},\"amp_song_q_and_a\":{\"sizes\":[[300,250]],\"a9\":true,\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11294908\"}},{\"bidder\":\"ix\",\"params\":{\"id\":11,\"siteID\":194720}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"107816\",\"zoneId\":\"613920\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"amp_song_q_and_a_prebid\"}}]},\"amp_song_sticky\":{\"sizes\":[[320,50]],\"a9\":true,\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11294881\"}},{\"bidder\":\"ix\",\"params\":{\"id\":3,\"siteID\":194711}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"107816\",\"zoneId\":\"613898\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"amp_song_sticky_prebid\"}}]},\"amp_song_interstitial\":{\"sizes\":[[320,480]],\"kv\":{\"is_atf\":true}},\"amp_song_medium1_interscroller\":{\"sizes\":[[320,480]],\"kv\":{\"is_atf\":false}},\"amp_video_leaderboard\":{\"sizes\":[[320,50]],\"a9\":true,\"prebid_placements\":[{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"amp_mw_video_leaderboard_prebid\"}}]},\"android_song_inread2\":{\"sizes\":\"MEDIUM_RECTANGLE\"},\"android_song_inread3\":{\"sizes\":\"MEDIUM_RECTANGLE\"},\"android_song_inread\":{\"sizes\":\"MEDIUM_RECTANGLE\"},\"app_song_inread2\":{\"sizes\":\"MEDIUM_RECTANGLE\"},\"app_song_inread3\":{\"sizes\":\"MEDIUM_RECTANGLE\"},\"app_song_inread\":{\"sizes\":\"MEDIUM_RECTANGLE\"},\"genius_ios_app_song_inread\":{\"sizes\":\"MEDIUM_RECTANGLE\"},\"genius_ios_app_song_inread2\":{\"sizes\":\"MEDIUM_RECTANGLE\"},\"genius_ios_app_song_inread3\":{\"sizes\":\"MEDIUM_RECTANGLE\"},\"brightcove_article_web_player\":{\"exclude_assembly\":true,\"sizes\":[[640,360]]},\"brightcove_modal_web_player\":{\"exclude_assembly\":true,\"sizes\":[[640,360]]},\"brightcove_article_list_web_player\":{\"exclude_assembly\":true,\"sizes\":[[640,360]]},\"brightcove_mobile_thumbnail_web_player\":{\"exclude_assembly\":true,\"sizes\":[[640,360]]},\"desktop_album_leaderboard\":{\"master_on_page\":true,\"sizes\":[[970,250],[728,90],[970,90],[970,1]],\"a9\":true,\"placeholder_size\":[728,90],\"kv\":{\"is_atf\":true},\"prebid_placements\":[{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"desktop_album_leaderboard_prebid\"}}]},\"desktop_album_sidebar\":{\"sizes\":[[300,250]],\"a9\":true,\"placeholder_size\":[300,250],\"kv\":{\"is_atf\":true},\"prebid_placements\":[{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"desktop_album_sidebar_prebid\"}}]},\"desktop_article_sidebar\":{\"sizes\":[[300,600],[300,250]],\"a9\":true,\"prebid_placements\":[{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"desktop_article_sidebar_prebid\"}}]},\"desktop_article_leaderboard\":{\"master_on_page\":true,\"sizes\":[[970,250],[728,90],[970,90],[970,1]],\"a9\":true,\"placeholder_size\":[728,90],\"kv\":{\"is_atf\":true}},\"desktop_article_skin\":{\"sizes\":[[1700,800],[1700,1200]],\"kv\":{\"is_atf\":true}},\"desktop_artist_leaderboard\":{\"sizes\":[[970,250],[728,90],[970,90],[970,1]],\"a9\":true,\"placeholder_size\":[728,90],\"prebid_placements\":[{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"desktop_artist_leaderboard_prebid\"}}]},\"desktop_artist_sidebar\":{\"sizes\":[[300,250]],\"a9\":true,\"placeholder_size\":[300,250],\"prebid_placements\":[{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"desktop_artist_sidebar_prebid\"}}]},\"desktop_discussion_leaderboard\":{\"sizes\":[[728,90]],\"a9\":true,\"placeholder_size\":[728,90],\"prebid_placements\":[{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"desktop_discussion_leaderboard_prebid\"}}]},\"desktop_discussion_sidebar\":{\"sizes\":[[300,250]],\"a9\":true,\"placeholder_size\":[300,250]},\"desktop_forum_leaderboard\":{\"sizes\":[[728,90]],\"a9\":true,\"placeholder_size\":[728,90]},\"desktop_forum_medium1\":{\"sizes\":[[300,250]],\"a9\":true,\"prebid_placements\":[{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"desktop_forum_medium1_prebid\"}}]},\"desktop_home_footer\":{\"sizes\":[[728,90]],\"a9\":true,\"placeholder_size\":[728,90]},\"desktop_home_leaderboard\":{\"master_on_page\":true,\"sizes\":[[970,250],[728,90],[970,1],[4,1]],\"a9\":true,\"kv\":{\"is_atf\":true}},\"desktop_home_promo_unit\":{\"refresh_unsafe\":true,\"sizes\":[[2,1]]},\"mobile_home_promo_unit\":{\"refresh_unsafe\":true,\"sizes\":[[2,1]]},\"desktop_home_skin\":{\"sizes\":[[1700,800],[1700,1200]],\"kv\":{\"is_atf\":true}},\"desktop_search_leaderboard\":{\"sizes\":[[970,250],[728,90]],\"a9\":true,\"placeholder_size\":[728,90],\"prebid_placements\":[{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"desktop_search_leaderboard_prebid\"}}]},\"desktop_search_sidebar\":{\"sizes\":[[300,250],[300,600]],\"a9\":true,\"prebid_placements\":[{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"desktop_search_sidebar_prebid\"}}]},\"desktop_song_about_leaderboard\":{\"sizes\":[[970,250],[728,90]],\"placeholder_size\":[728,90],\"has_only_dynamic_placements\":true,\"a9\":true,\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"18783318\"}}]},\"desktop_song_about_sidebar\":{\"sizes\":[[300,250],[300,600]],\"placeholder_size\":[300,250],\"has_only_dynamic_placements\":true,\"a9\":true,\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"18783319\"}}]},\"desktop_song_annotation\":{\"sizes\":[[300,250]],\"a9\":true,\"placeholder_size\":[300,250],\"has_only_dynamic_placements\":true,\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11294796\"}},{\"bidder\":\"ix\",\"params\":{\"siteId\":194718,\"size\":[300,250]}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"135638\",\"zoneId\":\"639856\"}},{\"bidder\":\"undertone\",\"params\":{\"publisherId\":\"3603\",\"placementId\":\"399321791\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"desktop_song_annotation_prebid\"}}]},\"desktop_song_comments\":{\"sizes\":[[300,250],[336,280],[468,60]],\"a9\":true,\"placeholder_size\":[300,250],\"has_only_dynamic_placements\":true,\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11294806\"}},{\"bidder\":\"ix\",\"params\":{\"siteId\":194722,\"size\":[300,250]}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"135638\",\"zoneId\":\"639858\"}},{\"bidder\":\"undertone\",\"params\":{\"publisherId\":\"3603\",\"placementId\":\"408703511\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"desktop_song_comments_prebid\"}}]},\"desktop_song_comments_leaderboard\":{\"sizes\":[[970,250],[728,90]],\"placeholder_size\":[728,90],\"has_only_dynamic_placements\":true,\"a9\":true,\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"18783321\"}}]},\"desktop_song_comments_sidebar\":{\"sizes\":[[300,250],[300,600]],\"placeholder_size\":[300,250],\"has_only_dynamic_placements\":true,\"a9\":true},\"desktop_song_inread\":{\"sizes\":[[1,1],[1,2],[300,250],[336,280],[320,480],[468,60]],\"a9\":true,\"placeholder_size\":[300,250],\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11476342\"}},{\"bidder\":\"ix\",\"params\":{\"siteId\":201300,\"size\":[300,250]}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"135638\",\"zoneId\":\"660418\"}},{\"bidder\":\"undertone\",\"params\":{\"publisherId\":\"3603\",\"placementId\":\"351297791\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"desktop_song_inread_prebid\"}}]},\"desktop_song_inread2\":{\"sizes\":[[300,250],[336,280],[468,60]],\"a9\":true,\"placeholder_size\":[300,250],\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11476344\"}},{\"bidder\":\"ix\",\"params\":{\"siteId\":201301,\"size\":[300,250]}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"135638\",\"zoneId\":\"660420\"}},{\"bidder\":\"undertone\",\"params\":{\"publisherId\":\"3603\",\"placementId\":\"464408951\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"desktop_song_inread2_prebid\"}}]},\"desktop_song_inread3\":{\"sizes\":[[300,250],[336,280],[468,60]],\"a9\":true,\"placeholder_size\":[300,250],\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11476345\"}},{\"bidder\":\"ix\",\"params\":{\"id\":21,\"size\":[300,250]}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"135638\",\"zoneId\":\"660424\"}},{\"bidder\":\"undertone\",\"params\":{\"publisherId\":\"3603\",\"placementId\":\"464409071\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"desktop_song_inread3_prebid\"}}]},\"desktop_song_lyrics_inread\":{\"sizes\":[[1,1],[300,250],[728,90],[970,250],[1300,1]],\"placeholder_size\":[300,250],\"has_only_dynamic_placements\":true,\"a9\":true},\"desktop_song_lyrics_inread2\":{\"sizes\":[[300,250],[728,90],[970,250],[1300,1]],\"placeholder_size\":[300,250],\"has_only_dynamic_placements\":true,\"a9\":true,\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"18783310\"}}]},\"desktop_song_lyrics_inread3\":{\"sizes\":[[300,250],[728,90],[970,250],[1300,1]],\"placeholder_size\":[300,250],\"has_only_dynamic_placements\":true,\"a9\":true,\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"18783314\"}}]},\"desktop_song_leaderboard\":{\"sizes\":[[970,250],[728,90],[970,90],[970,1]],\"a9\":true,\"kv\":{\"is_atf\":true},\"master_on_page\":true,\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11294811\"}},{\"bidder\":\"ix\",\"params\":{\"siteId\":197229,\"size\":[728,90]}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"135638\",\"zoneId\":\"639864\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"desktop_song_leaderboard_prebid\"}}]},\"desktop_song_combined_leaderboard\":{\"sizes\":[[728,90],[970,250],[4,1]],\"has_only_dynamic_placements\":true,\"a9\":true,\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"18783264\"}}]},\"desktop_song_lyrics_sidebar\":{\"sizes\":[[300,250],[300,600]],\"placeholder_size\":[300,250],\"has_only_dynamic_placements\":true,\"a9\":true,\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"18783298\"}}]},\"desktop_song_lyrics_sidebar2\":{\"sizes\":[[300,250]],\"placeholder_size\":[300,250],\"has_only_dynamic_placements\":true,\"a9\":true,\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"18783301\"}}]},\"desktop_song_lyrics_sidebar3\":{\"sizes\":[[300,250]],\"placeholder_size\":[300,250],\"has_only_dynamic_placements\":true,\"a9\":true,\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"18783306\"}}]},\"desktop_song_lyrics_sidebar4\":{\"sizes\":[[300,250]],\"placeholder_size\":[300,250],\"has_only_dynamic_placements\":true,\"a9\":true,\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"18783307\"}}]},\"desktop_song_lyrics_footer\":{\"sizes\":[[300,250],[300,600],[336,280],[468,60]],\"a9\":true,\"placeholder_size\":[300,250],\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11294823\"}},{\"bidder\":\"ix\",\"params\":{\"siteId\":194710,\"size\":[300,250]}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"135638\",\"zoneId\":\"639860\"}},{\"bidder\":\"undertone\",\"params\":{\"publisherId\":\"3603\",\"placementId\":\"397849871\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"desktop_song_lyrics_footer_prebid\"}}]},\"desktop_song_marquee\":{\"sizes\":[[4,1]],\"refresh_unsafe\":true,\"kv\":{\"is_atf\":true}},\"desktop_song_medium1\":{\"sizes\":[[300,600],[300,250]],\"a9\":true,\"placeholder_size\":[300,250],\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11294830\"}},{\"bidder\":\"ix\",\"params\":{\"siteId\":194709,\"size\":[300,250]}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"135638\",\"zoneId\":\"639862\"}},{\"bidder\":\"undertone\",\"params\":{\"publisherId\":\"3603\",\"placementId\":\"346199471\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"desktop_song_medium1_prebid\"}}]},\"primis_desktop_song_player\":{\"sizes\":[[640,360]],\"assembly_video\":{\"position\":\"below_the_fold\"},\"a9\":{\"media_type\":\"video\"}},\"primis_mobile_song_player\":{\"sizes\":[[640,360]],\"assembly_video\":{\"position\":\"below_the_fold\"},\"a9\":{\"media_type\":\"video\"}},\"desktop_song_q_and_a\":{\"sizes\":[[300,250]],\"a9\":true,\"has_only_dynamic_placements\":true,\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11474887\"}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"135638\",\"zoneId\":\"660286\"}},{\"bidder\":\"undertone\",\"params\":{\"publisherId\":\"3603\",\"placementId\":\"464408711\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"desktop_song_q_and_a_prebid\"}}]},\"desktop_song_sidebar_top\":{\"sizes\":[[300,250]],\"a9\":true,\"placeholder_size\":[300,250],\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11621942\"}},{\"bidder\":\"ix\",\"params\":{\"siteId\":221945,\"size\":[300,250]}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"135638\",\"zoneId\":\"682610\"}},{\"bidder\":\"undertone\",\"params\":{\"publisherId\":\"3603\",\"placementId\":\"21615783137\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"desktop_song_sidebar_top_prebid\"}}]},\"desktop_user_leaderboard\":{\"master_on_page\":true,\"sizes\":[[728,90]],\"a9\":true,\"placeholder_size\":[728,90],\"prebid_placements\":[{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"desktop_user_leaderboard_prebid\"}}]},\"desktop_user_sidebar\":{\"sizes\":[[300,250]],\"a9\":true,\"placeholder_size\":[300,250]},\"desktop_video_sidebar\":{\"sizes\":[[300,250],[300,600]],\"a9\":true,\"placeholder_size\":[300,250],\"prebid_placements\":[{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"desktop_video_sidebar_prebid\"}}]},\"desktop_sitewide_interstitial\":{\"out_of_page\":true,\"out_of_page_format\":\"INTERSTITIAL\",\"has_only_dynamic_placements\":true,\"sizes\":[[320,480],[300,250],[336,280]],\"a9\":false,\"kv\":{\"is_atf\":true}},\"mobile_artist_leaderboard\":{\"master_on_page\":true,\"sizes\":[[320,50]],\"a9\":true,\"placeholder_size\":[320,50],\"prebid_placements\":[{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"mobile_artist_leaderboard_prebid\"}}]},\"mobile_artist_footer\":{\"sizes\":[[300,250]],\"a9\":true,\"placeholder_size\":[300,250],\"prebid_placements\":[{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"mobile_artist_footer_prebid\"}}]},\"mobile_discussion_leaderboard\":{\"sizes\":[[320,50]],\"a9\":true,\"placeholder_size\":[320,50],\"prebid_placements\":[{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"mobile_discussion_leaderboard_prebid\"}}]},\"mobile_home_adhesion\":{\"master_on_page\":true,\"sizes\":[[320,50],[320,100]],\"a9\":true,\"kv\":{\"is_atf\":true}},\"mobile_home_footer\":{\"sizes\":[[300,250]],\"a9\":true,\"kv\":{\"is_atf\":false}},\"mobile_forum_leaderboard\":{\"sizes\":[[320,50]],\"a9\":true,\"placeholder_size\":[320,50]},\"mobile_search_leaderboard\":{\"sizes\":[[320,50]],\"a9\":true,\"placeholder_size\":[320,50],\"prebid_placements\":[{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"mobile_search_leaderboard_prebid\"}}]},\"mobile_search_footer\":{\"sizes\":[[300,250]],\"a9\":true,\"placeholder_size\":[300,250],\"prebid_placements\":[{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"mobile_search_footer_prebid\"}}]},\"mobile_song_about\":{\"sizes\":[[300,250],[300,50],[320,50],[320,100]],\"placeholder_size\":[[300,250]],\"has_only_dynamic_placements\":true,\"a9\":true,\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"21969024\"}}]},\"mobile_song_lyrics_footer\":{\"sizes\":[[300,250],[300,50],[320,50],[320,100]],\"placeholder_size\":[[300,250]],\"has_only_dynamic_placements\":true,\"a9\":true,\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"21969013\"}}]},\"mobile_song_annotation\":{\"sizes\":[[300,250]],\"a9\":true,\"placeholder_size\":[300,250],\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11294868\"}},{\"bidder\":\"ix\",\"params\":{\"siteId\":194719,\"size\":[300,250]}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"107816\",\"zoneId\":\"613918\"}},{\"bidder\":\"undertone\",\"params\":{\"publisherId\":\"3603\",\"placementId\":\"399321431\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"mobile_song_annotation_prebid\"}}]},\"mobile_sitewide_interstitial\":{\"out_of_page\":true,\"out_of_page_format\":\"INTERSTITIAL\",\"has_only_dynamic_placements\":true,\"sizes\":[[320,480],[300,250],[336,280]],\"kv\":{\"is_atf\":true}},\"mobile_song_comments\":{\"sizes\":[[300,250],[300,50],[320,50],[320,100]],\"a9\":true,\"placeholder_size\":[300,250],\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11294870\"}},{\"bidder\":\"ix\",\"params\":{\"siteId\":194721,\"size\":[300,250]}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"107816\",\"zoneId\":\"613922\"}},{\"bidder\":\"undertone\",\"params\":{\"publisherId\":\"3603\",\"placementId\":\"408703391\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"mobile_song_comments_prebid\"}}]},\"mobile_song_comments_leaderboard\":{\"sizes\":[[300,250],[300,50],[320,50],[320,100]],\"placeholder_size\":[300,250],\"has_only_dynamic_placements\":true,\"a9\":true,\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"2196986\"}}]},\"mobile_song_comments_footer\":{\"sizes\":[[300,250],[300,50],[320,50],[320,100]],\"placeholder_size\":[300,250],\"has_only_dynamic_placements\":true,\"a9\":true,\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"21969092\"}}]},\"mobile_song_credits\":{\"sizes\":[[300,250],[300,50],[320,50],[320,100]],\"placeholder_size\":[300,250],\"has_only_dynamic_placements\":true,\"a9\":true,\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"21969063\"}}]},\"mobile_song_footer\":{\"sizes\":[[300,250],[300,50],[320,50],[320,100]],\"a9\":true,\"placeholder_size\":[300,250],\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11294876\"}},{\"bidder\":\"ix\",\"params\":{\"siteId\":194715,\"size\":[300,250]}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"107816\",\"zoneId\":\"613908\"}},{\"bidder\":\"undertone\",\"params\":{\"publisherId\":\"3603\",\"placementId\":\"395129471\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"mobile_song_footer_prebid\"}}]},\"mobile_song_lyrics_header\":{\"sizes\":[[320,50]],\"placeholder_size\":[320,50],\"a9\":true,\"kv\":{\"is_atf\":true},\"master_on_page\":true,\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"13937403\"}},{\"bidder\":\"ix\",\"params\":{\"id\":26,\"siteID\":300788}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"107816\",\"zoneId\":\"1051274\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"mobile_song_lyrics_header_prebid\"}}]},\"mobile_song_lyrics_header_adhesion\":{\"sizes\":[[300,50],[320,50],[320,100]],\"a9\":true,\"kv\":{\"is_atf\":true},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11294881\"}},{\"bidder\":\"ix\",\"params\":{\"siteId\":194711,\"size\":[300,50]}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"107816\",\"zoneId\":\"613898\"}}]},\"mobile_song_lyrics_inread\":{\"sizes\":[[1,1],[320,480],[1300,1]],\"placeholder_size\":[300,250],\"has_only_dynamic_placements\":true,\"a9\":true},\"mobile_song_lyrics_inread2\":{\"sizes\":[[1,1],[300,250],[320,480],[1300,1]],\"placeholder_size\":[300,250],\"has_only_dynamic_placements\":true,\"a9\":true,\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"21969011\"}}]},\"mobile_song_lyrics_inread3\":{\"sizes\":[[1,1],[300,250],[320,480],[1300,1]],\"placeholder_size\":[300,250],\"has_only_dynamic_placements\":true,\"a9\":true,\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"21969012\"}}]},\"mobile_song_medium1\":{\"sizes\":[[1,1],[1,2],[300,250],[320,480]],\"a9\":true,\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11294891\"}},{\"bidder\":\"ix\",\"params\":{\"siteId\":194712,\"size\":[300,250]}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"107816\",\"zoneId\":\"613900\"}},{\"bidder\":\"undertone\",\"params\":{\"publisherId\":\"3603\",\"placementId\":\"346200911\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"mobile_song_medium1_prebid\"}}]},\"mobile_song_medium2\":{\"sizes\":[[300,250]],\"a9\":true,\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11294897\"}},{\"bidder\":\"ix\",\"params\":{\"siteId\":194713,\"size\":[300,250]}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"107816\",\"zoneId\":\"613902\"}},{\"bidder\":\"undertone\",\"params\":{\"publisherId\":\"3603\",\"placementId\":\"346201631\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"mobile_song_medium2_prebid\"}}]},\"mobile_song_medium3\":{\"sizes\":[[300,250]],\"a9\":true,\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11294900\"}},{\"bidder\":\"ix\",\"params\":{\"siteId\":194714,\"size\":[300,250]}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"107816\",\"zoneId\":\"613906\"}},{\"bidder\":\"undertone\",\"params\":{\"publisherId\":\"3603\",\"placementId\":\"408703151\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"mobile_song_medium3_prebid\"}}]},\"mobile_song_q_and_a\":{\"sizes\":[[300,250],[300,50],[320,50],[320,100]],\"a9\":true,\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11294908\"}},{\"bidder\":\"ix\",\"params\":{\"siteId\":194720,\"size\":[300,250]}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"107816\",\"zoneId\":\"613920\"}},{\"bidder\":\"undertone\",\"params\":{\"publisherId\":\"3603\",\"placementId\":\"408703271\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"mobile_song_q_and_a_prebid\"}}]},\"mobile_song_song_bio\":{\"sizes\":[[300,250],[300,50],[320,50],[320,100]],\"a9\":true,\"kv\":{\"is_atf\":false},\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"11294916\"}},{\"bidder\":\"ix\",\"params\":{\"siteId\":194716,\"size\":[300,250]}},{\"bidder\":\"rubicon\",\"params\":{\"accountId\":\"15874\",\"siteId\":\"107816\",\"zoneId\":\"613910\"}},{\"bidder\":\"undertone\",\"params\":{\"publisherId\":\"3603\",\"placementId\":\"408703991\"}},{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"mobile_song_song_bio_prebid\"}}]},\"mobile_song_tracklist\":{\"sizes\":[[300,250],[300,50],[320,50],[320,100]],\"placeholder_size\":[300,250],\"has_only_dynamic_placements\":true,\"a9\":true,\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"21969042\"}}]},\"mobile_song_sticky\":{\"sizes\":[[320,50],[300,50]],\"placeholder_size\":[320,50],\"has_only_dynamic_placements\":true,\"a9\":true,\"prebid_placements\":[{\"bidder\":\"appnexus\",\"params\":{\"placementId\":\"23160297\"}}]},\"mobile_user_leaderboard\":{\"master_on_page\":true,\"sizes\":[[320,50]],\"a9\":true,\"prebid_placements\":[{\"bidder\":\"triplelift\",\"params\":{\"inventoryCode\":\"mobile_user_leaderboard_prebid\"}}]},\"mobile_user_footer\":{\"sizes\":[[300,250]],\"a9\":true},\"desktop_sitewide_primis_video\":{\"sizes\":[[640,360]],\"assembly_video\":{\"position\":\"above_the_fold\"},\"a9\":{\"media_type\":\"video\"}},\"mobile_sitewide_primis_video\":{\"sizes\":[[640,360]],\"assembly_video\":{\"position\":\"above_the_fold\"},\"a9\":{\"media_type\":\"video\"}},\"web_annotator_annotation\":{\"sizes\":[[300,250]],\"a9\":true,\"kv\":{\"is_atf\":false}},\"desktop_home_charts_song\":{\"sizes\":[[1,77]],\"refresh_unsafe\":true,\"exclude_assembly\":true},\"mobile_home_charts_song\":{\"sizes\":[[1,78]],\"refresh_unsafe\":true,\"exclude_assembly\":true},\"desktop_song_recommended_song\":{\"sizes\":[[1,114]],\"refresh_unsafe\":true,\"exclude_assembly\":true},\"mobile_song_recommended_song\":{\"sizes\":[[1,110]],\"refresh_unsafe\":true,\"exclude_assembly\":true},\"desktop_home_sponsored_charts\":{\"sizes\":[[1,58]],\"refresh_unsafe\":true,\"exclude_assembly\":true},\"mobile_home_sponsored_charts\":{\"sizes\":[[1,36]],\"refresh_unsafe\":true,\"exclude_assembly\":true},\"desktop_song_sponsored_minicharts\":{\"sizes\":[[1,56]],\"refresh_unsafe\":true,\"exclude_assembly\":true},\"mobile_song_sponsored_minicharts\":{\"sizes\":[[1,50]],\"refresh_unsafe\":true,\"exclude_assembly\":true},\"tonefuse_android_mobile_web_320x50_sticky_banner\":{\"sizes\":[[320,50]],\"exclude_assembly\":true},\"genius_desktop_song_annotation_leaderboard\":{\"sizes\":[[320,50]]},\"genius_mobile_song_annotation_leaderboard\":{\"sizes\":[[320,50],[300,50]],\"breakpoints\":[{\"maxScreenWidth\":390,\"maxAdWidth\":300}]}},\"dfp_network_id\":\"342026871\",\"a9_pub_id\":\"3459\",\"prebid\":{\"priceGranularity\":{\"buckets\":[{\"max\":1.0,\"increment\":0.01},{\"max\":5.0,\"increment\":0.02},{\"max\":20.0,\"increment\":0.1}]},\"bidderSettings\":{\"districtmDMX\":{\"bidCpmAdjustment\":0.9}},\"sizes\":[[970,250],[970,90],[728,90],[468,60],[336,280],[300,50],[300,250],[300,600],[320,100],[320,50],[1,2]],\"experimental_bidders\":[]},\"platform\":\"desktop\",\"prebid_timeout_ms\":1000,\"prebid_terminal_timeout_ms\":1500,\"header_bidding_enabled\":true,\"prebid_server_enabled\":false,\"prebid_server_timeout\":400,\"prebid_server\":{\"appnexus\":{\"account_id\":\"c438d9e0-0182-4737-8ad7-68bcaf49d76e\",\"endpoint\":\"https://prebid.adnxs.com/pbs/v1/openrtb2/auction\",\"sync_endpoint\":\"https://prebid.adnxs.com/pbs/v1/cookie_sync\"},\"rubicon\":{\"account_id\":\"15874\",\"endpoint\":\"https://prebid-server.rubiconproject.com/openrtb2/auction\",\"sync_endpoint\":\"https://prebid-server.rubiconproject.com/cookie_sync\"}},\"ias_enabled\":true,\"ias_pubid\":\"927569\",\"cmp_enabled\":false,\"consent_timeout\":10000,\"ana_api_key\":\"056363cfdcfcf7de5cea11820138b4d2daf3ca\",\"ana_host_rewrite_map\":{\"genius.com\":\"genius.com\"},\"ana_base_url\":\"https://ads.assemblyexchange.com\",\"masquarade_as_ana_sdk_version\":\"web_2.1.3\",\"disabled_ad_units\":[\"desktop_home_charts_song\",\"mobile_home_charts_song\",\"desktop_home_sponsored_charts\",\"mobile_home_sponsored_charts\",\"desktop_song_recommended_song\",\"mobile_song_recommended_song\",\"desktop_song_sponsored_minicharts\",\"mobile_song_sponsored_minicharts\",\"desktop_home_skin\"]};\n      var targeting_list = [{\"name\":\"song_id\",\"values\":[\"6576\"]},{\"name\":\"song_title\",\"values\":[\"All Eyez On Me\"]},{\"name\":\"artist_id\",\"values\":[\"59\"]},{\"name\":\"artist_name\",\"values\":[\"2Pac\"]},{\"name\":\"is_explicit\",\"values\":[\"true\"]},{\"name\":\"pageviews\",\"values\":[\"1212666\"]},{\"name\":\"primary_tag_id\",\"values\":[\"1434\"]},{\"name\":\"primary_tag\",\"values\":[\"rap\"]},{\"name\":\"tag_id\",\"values\":[\"3783\",\"3171\",\"798\",\"2741\",\"1434\"]},{\"name\":\"song_tier\",\"values\":[\"C\"]},{\"name\":\"topic\",\"values\":[\"weed\"]},{\"name\":\"in_top_10\",\"values\":[\"false\"]},{\"name\":\"artist_in_top_10\",\"values\":[\"false\"]},{\"name\":\"album_in_top_10\",\"values\":[\"false\"]},{\"name\":\"new_release\",\"values\":[\"false\"]},{\"name\":\"release_month\",\"values\":[\"199602\"]},{\"name\":\"release_year\",\"values\":[\"1996\"]},{\"name\":\"release_decade\",\"values\":[\"1990\"]},{\"name\":\"stubhub_is_active\",\"values\":[\"false\"]},{\"name\":\"in_top_10_rap\",\"values\":[\"false\"]},{\"name\":\"in_top_10_rock\",\"values\":[\"false\"]},{\"name\":\"in_top_10_country\",\"values\":[\"false\"]},{\"name\":\"in_top_10_r_and_b\",\"values\":[\"false\"]},{\"name\":\"in_top_10_pop\",\"values\":[\"false\"]},{\"name\":\"environment\",\"values\":[\"production\"]},{\"name\":\"platform\",\"values\":[\"web\"]},{\"name\":\"platform_variant\",\"values\":[\"desktop_react\"]},{\"name\":\"interstitial_variant\",\"values\":[\"control\"]},{\"name\":\"ad_page_type\",\"values\":[\"song\"]}];\n      var initial_units = [\"desktop_song_combined_leaderboard\",\"desktop_song_lyrics_sidebar\"];\n      var targeting = {};\n      targeting_list.forEach(function(pair) {\n        targeting[pair.name] = pair.values;\n      });\n      var initial_last_reset_at = 1 || Date.now();\n\n      ads.initialize({config: config, targeting: targeting, initial_units: initial_units, initial_last_reset_at: initial_last_reset_at});\n      \n        initial_units.forEach(unit => ads.render(unit, unit));\n      \n\n      \n    });\n\n    \n      window.addEventListener('load', () => {\n        setTimeout(function() {\n          window['Genius.ads'].push(function(ads) { ads.trigger_user_syncs(); });\n        }, 1000);\n      });\n    \n  </script>\n\n  <script async src=\"https://cdn.adsafeprotected.com/iasPET.1.js\"></script>\n\n  <script>\n  function initialize_wunderkind(d) {\n    window.addEventListener('load', function() {\n      var e = d.createElement('script');\n      e.src = d.location.protocol + '//tag.wknd.ai/5453/i.js';\n      e.async = true;\n      d.getElementsByTagName(\"head\")[0].appendChild(e);\n    });\n  }\n\n  function determine_wunderkind_eligibility() {\n    if (window.location.search.includes('enable_wunderkind')) {\n      initialize_wunderkind(document);\n      return;\n    }\n\n    let wunderkind_segment = 0;\n    try { wunderkind_segment = localStorage.getItem('genius_wunderkind_segment'); } catch (e) {}\n\n    if (typeof(wunderkind_segment) === 'string') {\n      wunderkind_segment = Number(wunderkind_segment);\n    } else {\n      wunderkind_segment = Math.random();\n      try { localStorage.setItem('genius_wunderkind_segment', wunderkind_segment); } catch (e) {}\n    }\n\n    if ((wunderkind_segment * 100) < 100.0) {\n      window['com.Genius.wunderkind_cohort'] = 'wunderkind';\n      initialize_wunderkind(document);\n    } else {\n      window['com.Genius.wunderkind_cohort'] = 'control';\n    }\n  }\n\n  determine_wunderkind_eligibility();\n</script>\n\n\n<!--/sse-->\n\n    \n      <link as=\"script\" href=\"https://assets.genius.com/javascripts/compiled/reactSongClient.desktop-97cc6ed0ef19ecc3691b.js\" rel=\"preload\" /><script defer=\"true\" src=\"https://assets.genius.com/javascripts/compiled/reactSongClient.desktop-97cc6ed0ef19ecc3691b.js\" type=\"text/javascript\"></script>\n    \n      <link as=\"script\" href=\"https://assets.genius.com/javascripts/compiled/reactVendors.desktop-157fb5501c93d0b5d084.js\" rel=\"preload\" /><script defer=\"true\" src=\"https://assets.genius.com/javascripts/compiled/reactVendors.desktop-157fb5501c93d0b5d084.js\" type=\"text/javascript\"></script>\n    \n      <link as=\"script\" href=\"https://assets.genius.com/javascripts/compiled/reactPageVendors.desktop-789943a5e6b11d89eb69.js\" rel=\"preload\" /><script defer=\"true\" src=\"https://assets.genius.com/javascripts/compiled/reactPageVendors.desktop-789943a5e6b11d89eb69.js\" type=\"text/javascript\"></script>\n    \n      <link as=\"script\" href=\"https://assets.genius.com/javascripts/compiled/reactPage.desktop-ee2300c098c7e05a5f52.js\" rel=\"preload\" /><script defer=\"true\" src=\"https://assets.genius.com/javascripts/compiled/reactPage.desktop-ee2300c098c7e05a5f52.js\" type=\"text/javascript\"></script>\n    \n      <link as=\"script\" href=\"https://assets.genius.com/javascripts/compiled/reactAds.desktop-49c6a906a78355c0347f.js\" rel=\"preload\" /><script defer=\"true\" src=\"https://assets.genius.com/javascripts/compiled/reactAds.desktop-49c6a906a78355c0347f.js\" type=\"text/javascript\"></script>\n    \n    <style>\n  @font-face {\n    font-family: 'Programme';\n    src: url(https://assets.genius.com/fonts/programme_bold.woff2?1720540228) format('woff2'),\n      url(https://assets.genius.com/fonts/programme_bold.woff?1720540228) format('woff');\n    font-style: normal;\n    font-weight: bold;\n  }\n\n  @font-face {\n    font-family: 'Programme';\n    src: url(https://assets.genius.com/fonts/programme_normal.woff2?1720540228) format('woff2'),\n      url(https://assets.genius.com/fonts/programme_normal.woff?1720540228) format('woff');\n    font-style: normal;\n    font-weight: normal;\n  }\n\n  @font-face {\n    font-family: 'Programme';\n    src: url(https://assets.genius.com/fonts/programme_normal_italic.woff2?1720540228) format('woff2'),\n      url(https://assets.genius.com/fonts/programme_normal_italic.woff?1720540228) format('woff');\n    font-style: italic;\n    font-weight: normal;\n  }\n\n  @font-face {\n    font-family: 'Programme';\n    src: url(data:font/woff2;base64,d09GMgABAAAAAGIkAA8AAAABbawAAGHBAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP0ZGVE0cGnIbgd5IHMseBmAAhy4RCAqC3EiCg2cLiQYAATYCJAOSCAQgBYkwB6QbW707cQI97QZ47rYBBC/8WhYzVqhu6VDubhWCgGNbwI1xGpxHACuKn87+//+0ZGMMA7Q7FGymW1v/AkWAQqGGO11AOFuXiBxxvhDzmnG9luyI2O+AdIlxgrLWwDx1C1mhPUzgk4bXywJCAMxb0qXBK26YQFgo6OZKRSh+u3XTImklaZFuUbZ9yf5d6mqK+qyWMJvAosriZz3qT/nh4lELatt+nGGPaRHFgHnDTJQvn1/pZb/DfvS/hn4bqJSCAq8Ydh5tGRi7DEpEGyvOenkC1uL3Znb3RL85eKNpVOtiEiJeCZVWxUogqtwRbdru3MEhekE8xyEaPARLAkSldSqmqaWvpjWxSEXT+vA0t3/EgaIoUe3GRuSCxS1vGwu2wahJjlGjDUCFVkRi+NlEUUpFjPwGNipmYCUqVvP/H/e8rn3uJwhHNJyZJFOQAlWttQLom37Or9ALYBumEn/Kz4fOarTdPUAaf3Kn7iTLFNyXdydcjWonC9w/3epXE8IDLxqKak3Kc6tvAgSNDYQvKIMDxJySTNYL4J+wbbfB52I0GpoYtb3M+l/mwUggxlZ2T682CGCu3fbqgpnWQiiJ7veXEd8A/wIBJ8Yl0cLybnV5SfEtzsL/6nz/tMOydNn+NBDgA6S82GEDieBKil04m3MMCghd2aqmQm3Kd0C6cCXlRpfZl//Q7b7twPqKbVZgUTDU/SWUbkAnp2ETpKLdSf1kEdref02tqm5poKrbnt01DbEhwOwPLX1FbVm25UTD0Nq9ScmNyRDiCQAIhuHdlxjsiG37h2RtPMMytTez5YPhsYRhu4VmAwIYCfx7Va0WME2HrE3yZXluuCkWjZ0vxQA+EBCBTwgkIBIMyrY1lulASc6bBAKgICgsSIq084aYFDYFh435cg6VVhdDtbPlVrfbhSu6FOrmuuqK/qpQFPXB//+v9WZrB6vDXCvqy14RBq34fRpvvU8drv9CVAl3h/qHpipAakih0p0eQFAgUY0wI9zIkXLcGD3Kxn9aq7Tbt9d37gKko+wBSrPzquvPNtR0CGE+VPN2AEGxjo6MVbEGUAb+01P78oEuDwtYALvMLgMq4EjG87ET5J7dhwpQwwKkCQ8nhPu/ae29U/tufu7oCoxCGOQkVk1+CbtTEkpyvUhaH/qsy67i4ZClSWzzBo1tI2pHSYFr4UNCloheRNrtq5cYVnvt3tNbXkUkL0gQGUIY7Pkp02svpP4306eFPfr27tRZZ9WpUTVGREREjKh5//9f77FvfYMLupE4UWi/is+8G9e0YPeww0mRrYLFaoeP70U0N41p1bLXcjt+z3YbIFgqICAoICbzEG3OzbXmJiEjTeJxmW848W8mBOL77/UCxBd/bweIr98DWxGGbCCvQEqKwIcJCAChJbB2tXQ9HBWc2SkBmZMqOAuzCGRRFsNZnCUgS9JBJ6+mi25ebwTcRJOgqWZgJKSQQFZatYE77nr+Hklee9rIkQEJcUghxeLc+shNAtiP298JYD9vPCeA/bnxkgD21+2nBLDQIYUBQqLfZ3EBsAij7T4WKTMbRABVI0smQqpwxi7zXW7fweFj8sybxsH/fxjBID5ggAVhICBUmShJglRiUQ5xA04jBMEm4mDcSId1J1eINwWifSuCp6JCUKoMUYUqJIutVadXENgJE9BOmYIxYQbWWXNwJi3Au2AJwSUriK5YQ3LNBrIbtlDcsoPqjj009xygu+8Iw0MnmB47w/LUBbbnrnC8dIPrtTs8Dx7wPXki8OKF0Js3Ih8+iH35IiH9P/1U3x9/NPQv2oEONdEz1gwy1cLQUqvZtlaa42iVee7amKikZ0bMwIKckRU1Ext6ZnbMLBzYWTlxs3HhZ+cmzMFDnJOXNBcf9dz8NPMI0M4rSDcflH5+wYYFYI0LCndUSKSTwqLlReCcF0NAKC4OqQQKSikwuDQ6ehlMzLJEpLVIklSnZMl1SZVat1z17dOoqeuptHYzvY5uZ3OkqRZtKvRzp8rfOxfGpBzgYE0poLC2fOBjXYUgxPpKQIoNlZeOqYoqwlRVVczVVBO2uppiqaXeOOtvaUIqbObEzYiE/N7XPBBqap741p7olX4f7AZmGDiUD0uGhmHlsGXYM7wc/cb0qBxTq8bKsXpcOR7ElFfGh1PwlJ6Eq9wkn0ancT0xW2Y+sw2z3/PQeft8w0KfZ1pYF+5FYBFdpMC0DibFIZFhXZiYMUngjcWoNPHL0ogvW+qWzGB7ZF+mln2XWWO1j1u2Rws9qpZ9yx3LEysDXnTFrBKr6lXX6trCA5tZ/V17rH1j+xrLE15YP1y/noJxPtfXtge2t3Z2u/IdfcHb8Xdpu4JJ+KxlV7pr2HvtY7xT7GMX0WnZnkR2zaHTRu2H7Uv3hSm/Hh3lK/kJ/k8DEu6juqqw7MsQJ1z78n8a7EqOeuXZMA3lsux7NKFa27D3qNrh2jNyrLkvP0cL6KtpNx2ja8UUcYnY1u5pJ+kz9E2p8pV8zLtAzJuCj9tBzJamvovQ4yle68Lp+IecQXKII2LZfjyDL1B7iZ0F7uNtL30tOd7WHa46xfETeZ0Kg8KO5+EaoJCyEnlgtjIPNqEwnRWhawv14QqEFUUp9oDsEdYeAWHnAhaQNZULzFp3yRvL2bgwJcRozGrASXFzbhjU+lvVKXSstKxnNp1tp1TzwIlHhvYm09EMgwBkG+RSeaKIyXNNLkgnwlB82LTxRoeSB5ZcK7gt2o6vlNoL9sp/LgdJTdFy7Jp0LlOTow7VEwnwxoZ4cq8xQqyIQt1K27pVGnhg4pGOPXwGGoiykTwcgQ/L4qa1N0JHeWe0KwOW5SCylKsSvCMoDzf4cNVI8lUbGrpxutDk1tti8WXUKPuYTlyWdTAaSvHarCsIYRVkTkVTuTGj/7RWqTAlA7wz92Pmt8bVNNlhe6w71xNNwwB8WpboqJHyAbpyGHyCHGZkG2nPM6Zds4ksKpLQVPrZ3gcPQfOdBdS8tzG2YkqYDJZv8VRRzfOUlnawrTtenASWcH2Snl21R0EPONzzaTIQk+adGRuo29zvgwJtsYpXTOvMxrb29+yQSk0PaB7hPSsCD70ioVJeAlAIrSQWoedqeQKhdyAC1ZiAbAh18DjI4xIuXiKtm7r9PdRdzu6o7kyt3mJwvcfBOtXWGIfSazSn1rTfozH//F3B16OKHx5IqVjh4Zce6Hgk9exi195yNYNafi9xmeQfm/zR8JdE6kAJphm5iBJn1GcVcxLctKTcbOdu6+PUF6l55/dMwbMSW781+m9/h1Zqe6Dtkdizo2qqeqJ0OMM4kwlcZVHZ4lwiz6RX9Euq7agx6AQM6/dWwbES2eGYaRJSIq24hEslHig7QRO/5dTHwRdBQbNiWadKlgcSj9iDqHncdizRPPh96p9/K0QWtZW6fxf+/7dNsA1Kdf9B657dr2ZYz3WS0UpipMeASRcWLCseo0odj/pn8em4DCXNqGFVuiE1Ky5IiifWsR2RC4Y/fNNeczqZoQzwbGWevFfiiOZzNS8gE1k4lb5IyIqO05BYdTDX9DleEnyJfnoL8E4l9yp1PWB7RJYeNLIjtnYnsEAVJoP3DyJHZbXyEtPCGwVLeq03KWhf2lbKGlIKCRDfgDFPFFz3SiCj862oG3w08OidZkoTXn2zvXg6yICy0TxNRPxc8YU6xR/p63HS2N13R6lV6ynAKFf/MRl04FjhO+C0KJ122ZU8PS5CSKJYLPa19tCyLtyQmB1Lqq0nKbrfVu01RsLn/mLojsU7Z+yaIvY9Gsa7Gr+SCn5zttE8vJY3ME3c3RlBRoVkzVelQB0N9bhThSQcP3dlUT8waDQ3NqvfqZZLianAG+wpOftUhRI4YolfGCQEBWsJrwMIG93Q+eVMXEe7gfaGvk+QfqF5gZ7SYJ+SfWNG0E/76G8SCPhmhLw4IutKHnJpo0lttrhrb8Stn9R+ydipqi7ZjkFTtQ8tQv9n2WXpdKY64HIUoWoiisKzYJUl1aTVCHykP5CPZqeHEGhiqocbbDCPzuy4WJQqIlUkppIKsKqUMU7WGFWqNKjSqF6Tes2qtCg3nMcIw0zgMkXAYmYf07FClR7MiGJNkt9U+bWZVjdqr/TutfK+cK8Hc8aofoEs87EsqGrBLAsrXoQFSUrNii9LnDYqoEmkHuFAE1F6mJhOSqqlxwU+vADeAgGWF8oL14oQRHFiGTgtCgMm0Uh0EoPHJOKThKQEhpSTKEgSyIBkXj5ruWCFoE5QL2gQNAqaeM28Dk4Xr5un5vXw1vJ6eesZI4xdpBOCU4IJwVneJO8C7xLvGuch7zHvKe8Z0XN+anznMJNmZoYFVsXWZE7aXHDDI/IS8in2S9Bmio5itOEMXzeCGykzyjLYjL7DtNRKWsrWeVoXbGi5EceW6N6SvcttxvmYfib7gvQr4QdW/WSQc6h5Q2N2avRAfA4a82vhzZu3g9kzDjjiZIEUBP0JGZKwsdLJMMkyGUFbG10dJGQoCiIKEilWQRlRWSGNKCMUWEyqh2Ha+OrA1omuh2HFiGqYiABFSERIIswqX8ipywjWC2xoEz6sbha3UDgb9cIPJ2enEeixeDVshPcjFem/+Q9vOb2gnvpIKr9U7Z1wDmtzruSQzLKgj0Ykg5JFGPFY5SEQNyYHVsveTn+l9tLbHsVIxEiiHKnuGSWykqqINRiNNNHsWnSUQieo0/1YK2KM34O+NtFeZ5wO7jrpi5oVG8HXClPg6Em0Uc76uTyRD1JAMcVvplgwlWnFMyyaZ1ujCZh0FLrYsyZqiyH2QePW+f73qr0KjVrqrGakRuuF6kxH/ObqsknjApdUuz0kHttTCp6fxdpCsKfZRnLQvdSeMmc2ymx6GGAwGnJjJDpru3NM2tub7p8t9AXz1WLfz86ffqsMBihZlIiTbBTzcJ2RzVGmzjnuYngH7CLvxTy+nJh8Pjo+DRLnrJDSpMsoaaE5vvfGVR97+SSw0n/Aw7Z6krCK77gEoObYPcxs5phHynPcP/MQx28wlKavr2SUsLo5Ndxq9yRFBOK1RSoK2RHtzCJvoCgqKjTSzN6fgptJMHAc4FVwg+nQRtn0s9OOwKgRIYPsmgGt7uzCZfs09UbTzDKwcQCl9MhXVny2g6mkeCzyY+qqemsmGWNRHQQCzBZwuwxwhF5AiZsQOrwhvlEjvBkmTic3o3IAu2tII1vOYMQUQQ32BnLAnhzX4K2dPmMd7n2v2l8MNmcTjBP0D48alucDrYCHBASv9gVjADvIKu5EaXzT9fwSKcZxGgzgFYEoxsmNMyYSZlv42dK6x8LuwrjgeCkhB0l+s4lt656zsR3+l+ORQaQJ3yVhtB4npiWiKew58DrSHuAe/QoqRDKFGiQcnFzcPLx8/AKCwmLiEpJS0jKyikqqauoamlo6unr6BoYIKKhgNHQMLGwILp54fAIiCaQSJZFJliLVQjnyFVKpstQyImISUjJyBiYWVjZ2Dk4ufgFBCa3atOvQb8yEKTNmrVizbsOmA4dOnLlw5Y6efPTs1UeffPPdDz/98Q+Dyewhkik0OoPJ5nD5QrFUXUPTxKEjJ07lzl0igdExSSVJlipbjlwKSvlKlSnXoFOXjTYZMGjYZltsNWrMTrsdMW7UmHETJk2ZNuPAsTsPHj178cFHn33xzXc//JydJKTSxnlVEplCpTOYLDaHKxDq6Bo4curCRUTQPXOgD1rarFOgpMB2aYWM1ihNCBPl7/9NFkL79SPYjXeMXFn2VdymAIJ7HbXLiRV6Kjkn+L+5DuY6TfM7x+rXLqFtFj+x601qaLQRZ5em4SiTjoXHqCipM2ngtV9Xrtlno8Br6+HCBg8WyP80iZpagAkl5Pka44GrNyiID15h8/mO9T2cl1KOBQksB6UgAVWfDNlnJdi8Ms2A8FLPQVgzlNNOgtnxM7wURy8a9OOONcxfXPI6Nen3BoN/C74mQc4WLIsGU7+DQ0YSvyAzBDpGXQn8HD+8xJrMTUOms1Kzyg6LrMWpRrO+ogkpHO9nJZqL3gNN8y+XvQRxGwxB734Edab5QnNGfe7FitKc5pn0JNZeArepCKj0q1vK497WUzHJrUfF2TboaHyO7OXXpfgWHjdbcHPSsbIjwNzbLZt8O8eks8qoXci6imljRye+H7GfRpoBHUBXnrQ9nOl2BAq/8MNKXktoh3pWJi5hNIuLv2FFPs2SRqkmsE4g28bgtcBodliwR/GVJaY4aiMbczQ46CEIlFLTMBXAWM5YGKs75Rtc9Vwjk4lRaHC+bS5leAIbSITbt3kSMpDfZ53xAeqIr9Ul93uhVExJbIjOVWTBMdU+kPzkVesaNMw3aAVJCpTVJgg5E4kVMyTMum7OImZaYLz6Qq7ggQuLZYQ2oCKTrq7ttmjKvqlapP8rA2I3tUoS+6Lb/NhemTstrdmAmZm3c5rdDjekF46gWaK09CJ0zAagzXjz608Gkn7DehnwDKKDK+9dYw922/NYQ5pse7Xq5iHNDmvRDk7W8eg67t7T736PH5HbJNnSjx+Tp6PXOlw2LO0Q3M26y2n5tX/gtblcSCYODKannd+24G5SN5+xM/Wy7fnNPFdcPw3b/6Rd9S4NG3s4cLntaKP1HVTBkkOD+WlFDLEfqN4LfyeYivU2D119HtH+mXNbvu8vG/9Pyg0lkc/87VsTYZXSAsDzZiLJzXwM/kzU5fqeln927vvPtU9pS5+xPhNQnK/ha9IHynuh+x0ry5VzrHMUzogt5fscn0V47dPxMAfxRy/Rz/1hx77c2CDK+1XXfqXlUbbc+sEXdvW/ft7h0cd2XSo8SrnZbfH4S1fW7bi6n7EgrNFMiy6IkOEj4O8epxzo2XV1mEPqyR3UFutf/5vnXTlOXSyiM5l6+E6ZNwWWksB0JmggUT7wG8TMntPIIttFLTXuUI+sFCjlMq7PafZUVxbrMu2Zq0aA/qTZJ2d/5ViEOM5o7t41BanoVUNbLcTSihGZwzldiXQPLzg2LNRXYjHrNgPWaWf9S4qom1+XnDZVC4VuujhaMH6L8pcaVUWKHsOrS1AwLnE9dOlNT2h8Eg2POI9cKdI5HXRRCmElJSvnuSdyx2nr7o3umQBWnIDQSPVUjf+QcaOZKGe+c3vKZAD1CaAo+URrTPmhUmo6y9eoRPahEc8XDxoHsXOg+FBfZCSBWwd1VpN93hKFI1HK3ySAOP/iE66rPH2pvVaQJedZfwEd8H3raNp/jW3bgnrIxYgzkKI7NxwCf7qTwK/+GjP0PVTyCLDkyMSZBbvuXdi0bceu7ZAOVabIHTNLoSsvMIBsrZEHlVuTkuj20K41QYXq0OpkQmKNzkjKmTlNkTIyPyRzYxzpXkOp64T6NHoIy3oiPyrjG8l6GEWpkV5SdVrtoEwc9UarnkFYo19kbzGgvM2hYMI0iOIlMqpjqOyxB63CzKm8WjHfZUhUa5SYq/3cIEcdynervCymvZ695m3MBX2uvalHyyw7NSe3iKQL5ZORn/5EW5KiC0N21FeJn+DxQD95zC9Y5LoQo3i0yhG5UsQoTneeWHM+rlFpB9KMaypbtD7WrMuMMxlXMG6G0EhMVcE9RwckFxIVjUs5u0+zQrUOjCJgrfF0VFTwgrz1eb/7XycFX9kKXCo1bUEUlPuzgJEI1fNenq7ZuO0lCKJT+ILC3GcBb66T+tZgLtAGEK5qlpPrf3quhlk3kN5C8YooMLCr3kaLCIdN/1c+/ZHML7+9jPApbpdPLfGkBiiff+7oodZodMlmucBdlxrlm4KfF1ZIvl5siUb3E3ny35wnQpda1Eh9bDrOm92RBx1+S5B++IbKBqkvqBYF+TdlPT5Wqv1J4U4rnA9kk7/UNoTq2M/8Quk0WjiZLNJpqCv2FYFD4PO5AO7ynj+V+mzKsKHtnOxfGXV/npfwHSFeUg70n1koaqPJGAC9oHjvi/xetYtPi1mJL9KNmV1Af/o4A4em+tYRpolmoer6MZO4EB2Ou/+F5Ow2ShzvNDg132vZfpfqPr+f2HC/3irETTg03NXsbv6fiV7gN/W/wcyKF1EDQKyadPnl16meA3FrSBy6MgxKHdoqj2J6ljWMIeeaIDSuF9NlAYB2IZI+6/ofgP4zPLpJ9+27+Cf80wYyLTmjmDsJFYhHr68mHIYhokrxL6Ylv5y7cFHRzqNxTVEq2CpkDXzD9IQ4bF3oe21DEOr53Fdz0sgphLO12qKMO8rndQtaAtrM+y8HrW9cgnM/qdlsc0sYSKI9q9M50UkerA9ZZVAlRbUjTzjqGRML7buS0BH+DwMQIkVjyLVeXXbwy/1DXH5OUN+9gsmWjfYdVMLjfghXD77sWKQazmR4go1ML/l/ho5J40uIRh7Q7IsOy+uPkzW/6SwzaJx5Htnc9vtxIFXbP0TfxDoPxqvdW1tgyJhFaN2mTlWQ0+jQc22ogC41UKk1iNbczNrqOuX5+yPadnztpY02z0oi69j4CFrTGyyoXvXT8zIxLhcQTaIFxvLhpkuTPFowlK8iJCwiOi+LEYcVOnSrjimiaDORFoQ+4UU0YRjuaeo5PKLKJK1Ce1LyZ5wdQnyAcyl4uACwh4OLS0iIJ5VEOp4kIgkSiTlQggAwuXSRuTYKM+tA2+2Mdp3XthkisjUUdeXTYIOejXtMECdDvE2GT2YF6ARosAH28QKJhJoZZeeGifiLR+NGz8humIq3ZKitgWNdfMBLBAANm2RMkmiHQCiMsQ9Qq5AhFW6YRZIZ4ebGI3jLxTlAkJhJIUagZeAGpHlUXoAB7jSTDBlFG0uhwYtMCdMNh7fRJh5OjyFBJfTzAT8eGFzoiXMHlpNXUFSypQmFS323DzgIm9mUzbfcZTFWsdGFnXYu4JpcsouuIgYIeLRMLGRKu/Nsb5UPKhR6hYMB76yO4+fkAlBVIwAtJunQpij74Qm3JRbEdcSlmDlEQB4URO0zt4IBADAXSHTIqKHhkdrTMbw5+VUCnYlMQC4EPQMA4NXk88vVMjS1+jVVtDkfvDXx05mwkfKo+ABFvLiXxfEDHYtpNILMKaZevCJSpAJ0NtrAyHnP5Q41eTv7rC2UkE6DDXxqAZUjzUTYN2zkG9zGDRm5QAYFg27hasgxCF9xPJ/4NiBCNBny7MxVfswUcDdeV1uuDAZHZYaCiJYXV7JpHpUXYIA7zSRDGB5jDVDpQjryyKXXOu1D7j6kxTrCdQ+hjLUAg9QCGr0Hj806lJiSDcGsDhBF4sCxtm+SpjbY5cBmJMSsGGnshuoqwbVjW9r06pRflVQEGkGaxaix5tnbC73hKhsFeoVDeBNfMFraxi2gzM1DZE72bcJ/ygkwBgchEABVQXKJEStzMIxlLKE2pYp0ZyGmfPlYChViq1IFsdRSHMs41OuVBbdLH7aD1ohs2OC05YHLkyftXrzo8OZNpw8fbJw9vvpqr+++2+enX/b7kx76OxAWoRXNw1kbIoXlwXJJy2w8PgUxzZyUxTavpTv2+SRb4pjvMhqv/JCxeGdZJuOTPXmRkHTma6j53rm4d+eBFI+vab2Z2KD68njRDeaJhjeWJxvd4jxVfAlpoQNZNTQ9tYde5/745aEAQwQoEn5E51FS09IzyGdmYePg4ublExQSERWTlFIqvf+eZZzlhw43u+mkzTpP1MCJRePgkZKjKaho6BiZDL1mKzsnj4KRGg4rVKS9cLESGYYBr/hhEIfrv8DNn9DNH9/1nmia7G/KzXjlswvWdaWejbX6jf9ZszWMt7xWl3S8Y4UHXpODGI+v8NRrk/HxBcs1yx3LQ+H5m3k0Dcv/+GZRLsc/AxMw7RAns4A12X21+8RqhU7vfrlCg/UnwWswhEiIgTTVYqGR3obAIEYZChOYYRjMo91weIznjIBX1jt7jYKP+Eo0/MBvBsN/rBIDm9ghFg5wwhA4xwVD4QZ3DSNIBIYTNBojCGY59wZF+M7Iu1/rfhp9kt6Tg5jS/5x9Kd9+83VDEamgDKWKelSIjI4chgPhhgwRgKaTBxFcAeGxRMSkQR19dBoYY9AkPyYZ5sfwHwvpj6CNZWUjjdo5YtfJFadunrj1KohXH//43hCgE4SEDAknciMKsaKKSDtidOIuCROSSZlSnJL+Abix0jKkkSydMigePmlAQnP6U6lqLyW2tW1opB0d6Jg5Dz78Tt/pwxZrGzt1DRNfIjkFFbU+Q8bsM27Gih0nbrz4zuUFsABZNWXqbKCxpguX04SmNKsFLW91G9pWdfs61LHu63gneqm3+rAvO9PvA0CBpHMQkIwf0IFk+gP8Y5kd4xTuq910bmYzS88KZurZ+Ozl3GQemw+bN8zH5rcWrHfBBQqsiErjPOUzK+pUllyskvkP/awSWj+ZgAvZr7at/q4D1iPW6vWNjcWmdlO9Obw5wG/jYOMs4wHjY5s9bIvgtlx+rG5AbHzwLLJSnz0m3fGyvsZW3yxz+YFJKCzR5nBNx06XDS9YsWBMHGlU2o3V/djP5SZ8ZQB9HXbtd7lHh68JoOubYlhgO398axy73Qbtsdc+r8J468lznGnji/7JqG0pffbFV91e+jZpLs7x6f+cDmN22O7BqNh8kUPZ2OD81Yh82cgmG8sv9a2lznoazO38e3I3f/I3d3MkN3M7N3KrAAIABJDAAS5QwAM+CEAIInwgLzpvrvnB5FSe5X6eZ2rCeY46rCB1dUj9ybflZL1zon453oAcq0eO1iuHisqdOpVpdPI6MyKPqJ0Ny3/Zn/9zsIH15Y1iWtxi2FNmExKTkKF/bRey8w7pjIgoHgunZVVOs+fqgwMq9zPAQIOn2GeMMJmZpmUzzLZIA+1q+GngPxpa6/TpN2KLrUZts9v/DjnsiHF77XfQYUcdc9wJJ51y2hlndehyw8z6g2kWFmARFmPppHpeHywWAmeFJ94UxBd/AgkmlHAiKUw0RSkLNfSwIgHyW2C2Yd/cSlmZVWlzQXVDvo39BwB2hatqjfobZpwpZmnTrk6LdmpaGwwYMWqHPQ447Jg1Nthihz0OOOKEM7pBNuco0qLhms40wpwuZBxhbhdzDGRbjoNszwmQHTkJsjOnQHblNMjuTICm0gVSW0eQhjrD1DAlTAPTwnQwPcwAM8JMubqr82GgWsmCZ6amKJkg14w+grB9CeMQiEaAQLyfsEDVAqt6P27eolOz8dNbTyBBpjaMMMOCkWFjiT2OpJzah4COBRUDKzckkDvgRsYCRfMiciFLJx4o6LB+kHcqc5JFvbOcjmBRQKO7CtWXXBPVdUQb3zTX0Fg5NNSDt6wBNNKuB1mRhnTD9b6ZoaHutwwQoUX7medhxF4e6hKVJUhTLL6/tGMku4hKvZNbrSOg3CEkrALAHDiap6X4Rcc6m4vLllzMpVzOlVzNtVwHsB1jk994aspqGcsysfgtxccOi7O8+EC+U4espvb8LMrlD16h0WPWMUodnhBCNpBdOTKwaC9GvR0b5Y8oZBtI5NCh/A7NtsTb/t/aaQ/kdkAAtQm/7eUBXKK8RldLnXQ1wrC63LEfgotm4BOTVQ8jsxiwlOMLWXoxZXya/pCZP1Qp7Z5P8c+B129u/AH98dLbKDjach3unsBggFhXUscWQcPmFYJuGOYdmOI3C8MZR8g3YLfRhokdyXS1EtWLbfixLQ6LsYM7dr0c69TD9Rzn+oCjjkKmqxc5LLUQ94MwmkSbbvfv7TD773P4no6/DyMMvsOPWsZM51XexCv0SMUq5JVuExTbUOPBieKoMiqpEpMohCNdauzRfEIIIYQQQqqMQoviD3JEJc0D6p2QKiImIw96pKIO1BRwjJ1shkVhhtKoNr7Iji6UAguctTr22JdAQokkGlKwLZEEjNB0f285qgC1XFPKnYEwgVpViv1miOyC14vSS4ASdcUVoH71fZEMJzdqADwQgAhhHPWOO+m0sz9ATLOCvJ47eYEawl/VK1fvqTHI7pImPX5AwGhpq9n63COgQGVD7sbtewCj9PJr0BybDYDGCm/Wsupqd71+2UYf4yq6ZVSIATSA+ro3wDgDYTaeKBybPMnjPMrDPMg9mNBAkfo0BcafYMJXt1ZPBhCVc/7iVRJ477Wy6n+gSGAveAcStpnfDd9XXlMCkIs1hYGA2y9AmeWvGtEnhdIKL9eiqmqCRWkusVW9CmSW1p8rbkGsCQr5YiJsmNoNuQjVoavuJeM4ymko4RUygr4zGUJ7sNsxuQwMqDzJ0yv+Bx0Wd/qxsuvysQadkA+3eHBC9yOPM/qwfqWRytyJjD0YG2AdlzUb2TkQK7+mZF49oncJkh2zabWEAFFy3TO5y1qeo/oqIYDy/YvdZ0ihRG8TtBtxDUmApsNautdctaVerf37XK32Qe2Bgw+trPtQIBBo0T2zbVorS1Q/ZtqSAsG0JQSC3IKmtSzMeHW/jWif1rJpGQ9bSUdtqoxRVDQXKbPT3VcU/XmfZ9fFyyTk0wbHEkdMVB8vDtRPmwC5f2k5TwjlgromCJgNyY74Kz789f3yJ/2f6hUeGqy7Y+68QZ8B5MdqAZcjIQNkgFyAIfI5PRhyAZknAE9I4zBBtha3JO3591Tff3q6U0PHMDwcHMYvPXr95E/MijnB6BgTwzJ2JsAkmHJ/y+lmtptfPntuddkZu2ePsTrWxHrY6jAlXBietG24AYF2//cKbDE+SHt+1qb/9FRv7X147dePfpklc5zRMAaGuXWOL/nC2ZV5lNV2luP7F8fDwVd22H/V3v17yaP/gRdnbo7vn2FLTZaw+HhxY0cu5njk2OaTxheYPr47FuAqU6pAnlRkQUc9Rzbtz5RdWnTOOuhIV5hVgO5EoMGVA1wXIz6d8ojqPakpuEidkWsd86ao/zxEqbt95pDw5epGiQn6XmYuV3HNOlc/6Zsn1Hea1QA01HzhehgQsJzr1U9Oc611tra+NrWxgYYabLjNbW2LD/pdtL2xdrSz3e1tT/s60P4Od6gjjYePFCRoSMUxClYUWmW0UFbWVK/mSqzSVnZ0y9WrI9GSMo6EEh1sWQvDaRKn1Yry41IFSlR9Paxo3pdDKV8uRf3aO16N71QAnFV/u1wUU54CJUla2YpW1dLq2ltTW92pddSVtnVp+hJCEBEXT3wsQnwc4JAcXilEb4MY3wfGO6HrIXf8B4AH2ID6+4VTDCpCF5YEagK4NLsnEIhII/rrQWKf43wEccQEDWEZKSSkCsBTSAQKs2sgKYzu3vH8EE52cdgkKdKpcKoHKldeSMeAYwWWHVxCjcMQ+ZQXyDCAW4KXqyQQw0J0nhoi8AoSpwKTJ+lu+VTuCruMRy9nEShVQ7wWP1Jpl/g0DSh2hfK+1HgNuZ3ksgRLWqO0rJYUMwbFSk59tDh/AGBrwPKaUgNO8TXh1nMGo1boDckD+t8Cx57hKAIDdnxYsiTXKiHn2gGDF66X9P8oJpamYpYMFVAd9hP0rqrmim6bo8nysJklLYX1E0vaSyJBBdLi9eS1Dotfe5BN4LF389rr/DJ3GoU8ce6wy/3GBfhKZPJxMVKQPBM0x+VA35jQjEykXCiJhxxGxw1TbPCTTSuHMotDGN1ZXFpTkOHbYIbA6Acsbu+bp2C0qoVMiOo5LThgig32f5pbjC9JJ4EOwNRNNgefvWWt0Q8OaEeGmwfMoO4OI6hd14FAE/xj/VmJiUUTMflLwjxBJVI5c5r1nbaieo3wSbRbh19J6OB1Ii6P6qrcRbeia2xRubYPN/bBBnu0XN9jx/BqYOKPgKxAEGjU7NaGy2UEssSzF/acP/tWhOD4tVez+pcTZm4IO7LIzYFhshtT1sgAh6RZ8YQXfdIXeZIXQ9CJpMxCnVGOMnRDQYcSXRRjZyxYt4bjTncNTUpzIAxsGTUVj8ZBo0pGF7wk1RgcgD7IXzZDNZKoz1jE0S/YgxlQkIyiAoTsbYb3Rpova8Yx9p3DkRxPG7kujrLfue/7TcXKRcBlzxomofv0iNEEVP2IW3fOVjjPARh8sHfMI6XvzXDdyF4K/0kW1CzVBGkuofyX2MDFSxBmTBgCEHZs68zPXB0hm+TGLN8Ztcutwhh4SsoZ0A5ThlEGMxZ2IzxohHvTMn9s0Xq4IQNwA/daFiGiEpmvN6aWBQcHIvx3pke7YmXkR5YoYZjxMwSJnCuCxGCokEpPBpd7seLIOU/WGj+qNDA54ZbEXeQS8RNHmC9+zcN+pnTskissG+TzRHHKL4LOXHemK6tjTuX6RabYLVDBZ4vymVPcE8FxwigWpLNcTYK6I3dn0R2IiYqzLEFYnIXcWeCWMHenqSKmpJGhMBKoq0jkwYmjz0uxbmHyQqYYChYK2SQebqjW1nGLcWgnTL8KNC9PI17HtpyfIlfjYAj6CHWZgFAtRTMnieNAfnZK5lACSIIihnw+Na3EUilTVMhZf8QIXyKDlkLOUGKWVoaRgYGAkKRmYVxwruV/HDvNP6THTqEvs3kZIZ92chfNPZajOURhXQ7NqXOYy0hB2J3KVo6PMKgddEGonUBOKYlEGlOrU2yJ3aKT8lhHry1fQ7YLDlCB7I+oT4vDHlIVfsNehRgUkkbyctBt/0u6YMSLq1j8MnHugKMUIBee4ce6ETz0AS88MgnvmVTJOB+9Sie/GrXsaRY6AdSMSQZFMWAPWCebkRPQRKa7e3/adbUKONgdQ45XrIAIM+c+Z5H/alalhXDGz0W2glyThVmoGWAOky1U2DU/SSVevICUI3xaUrNkT4Vw1boib26ucGZvTvGD7j2nTd5NQNPrAe2ac4OkcYIKOV/FJfYphOb9/GzBgb4z1l13ZDoSutaMSlGg3HayfJgt12YeFj5tBRtqjxOD8CC3oRv1bksrUFTA5ixKseuEOeSmlu9/1HCI7MyjmWqq6eNRdNmqVVWPUFAZrAVazUSW7n2Vl+GPumygbVev+ujhrXLlTzkC0cIPi9YVFhdAkuiTWXG3WUMpht4XyAENC4UsSRyaFGcbxayTHIkARckiNEpXXNp06SpO5X5ql8KE5UClUEhsmSa4C+46zhG5vPSkYT+ysrC2crU2HCn03YwLEaNo8Ykl+RqP4j5HDIOnl7V4hKmoiWLoLLLIR7fdBSedALqfgUIuLWSJHCbk+1JKkOGIfFRCinUaW7zDbcRjKjuD8Jb8pSO8XUhFQzGTwrxauGMEXpXLDOhpHXAvmZvNcD3Z8JlAN/WCUidg6OyqBhPNDxEqMWjcomJ0kdpKqHHqBGCxLIOZzQ2nXrXnYRvx3BaIgSyyCVltQFvBCG3OMLob0zmyUId+pbffRcjcrQvgiRyozda3me5cZ9CR/c4NxsJcxjrswJauuXp7CoQEUmNWl8GwschmRsGRSx8+oRsU8bO+2pU37noCuTiTztQ8nK5LiAiXrKKV50BnQWbn6c6RBKKFBiTyAKBMg1RYyW0DJV1gy7KERXbqAFcVievg6EB55Q591wtamOgWZpq6OiwxVvB8lg4AthD1XSjCurpy0maLsOM5uFRzBF1Hy4zwoVeG4GbnRTHIUjOICJAShFqSjl2IJDgpB90WU1F5BQp2TgQq04zMrhdw+27kW7zD+R2qJUn3s8bM9OqqqzDPJHyTgBx4HrYo8NWSFY8QvLRbE37G3bp1aWflFW5vhg5i1rKuZdu2EZJsLgo8UJPZCyPNYBTFOAXxLE8sdxl5ImUZC6ql6RJmnMmMHzV65K3czHbUiDNgDGWNuk7Myht9tiC9SwODPHvyGUitwZCVGbLZwxzZx4udnKX4wRSPTo7HFU7MlDc4wR1lJpgpD58KlMpTHFe6OD9McotGySQygjmg6OYCJ4qjyBPyByiiABF2+Oguiv95zOAomT0SHIWSBtymmXakZpAXNrm/xFoKRk0lqxJAioBsUqT1Bc59xpIidP5VDFUW5fLBiQB7yjAoSBIyzkKQcMExDIR6effy1cuXY2b4cg3hKSJj5oi0uND2kmHIJ3oKHtnnYInrHlQTsk+a2W/EMKDwkzdhz9+IE+g+ut9WUZpByYCE4TiLbPHVvKqQVcGd3L25FryttVDOF6QuUlIbJWvXtKJGMGbeQHXPx+McQ/ur9Y8CpkAmleVt3UMxpBG6o2NPT+c1MHBb5ew/1d3w2W79Szz9lWkAJ8XleBNoUpw2aI0rRtYrRm+RXUQHfP09bl0n98mJa24TQEZFBg2lgyH5ZZQ+hrmn+Y4AJAqa68p2BJ25pE2e0LryBb9OaCZQk0iBcLD4yD4DbcjvHW2SRUb/byCIaebZTwoui/cjTQ0ERFFbVGqQbX0l1zY8TposajWFWuODp2g8bIKFpDhtsDQ0sdYPSQ/qa8XiRn+27likhJyjxDytGqVOZNWcUY1jEGocOUevCQYyNRZsK5lZqpaC4bExPimOD1JDs6NGUcWczCChJoaHoectzZL8z22UHe4h1KiDy70CZhoQ3vILuFVTKZVGEw1zWDS6GB+pUxiNNPBjj8XDn1sMRHF3ToYCH3765aw6ns8NhDYodaxOG+5KG2K6DkfcCg6d6hlseRqmtuR5JjGjiwxjhgN1Xj6fbDVSxi4ON77mbEVu3/Af3FP3FXLnmc2fWzpvVaKf9r6YHQd88DYib8LpU6lrdpDVptSD1Kj1aLqCg2jRU4a+6mxdUpv1l69Q2qUWAaeOHjki03x+vaIOxppjRD6RGecd5ZZMww+OkuKkle73wUiOTmLo1zqCZsJ7JmX5anvKR9YqndWayvm11opJZIwQK6Arx7fsoMs7gFGLbpfSlz3DK82et/4ob5iDDBjEtq+fNjqvf5Fhi6E21Ii9Y7BAO+BgHGtHlOz6yaJQLXybm/KQnhUNFqqiBWiJNRWsv8vTCZiW//M+YEvRFKq2iufIFoWqOc26pvxwSdK8rOepuqX5ugbflbg+86GDbpNRspBmgdb7a8wZEk80F1Y1mvS7GbwiMvW8gJOA3qrYT/6K8QeiuFnl4cNBz/4zV00MZXTmeJEwj1yx8+GaGMPKYeYRuGlgrfWlXm/oXCTcnUZsX09akPYw+eHsvm8Hox7UXBmloePKpeXSgr+PLV10/M8abEJOJ1YXmVE/4bqYzRY0JOTWkNqvCfP+3u9998Etwq8X2nKaGp3bRUyhnmameeDKBl3HH7PcHFSJekpEHJRAu+OCPXH7PdUXtp8i9iWlG20G7XCEqZ5tMSiWt1RhtlypKfwQovl7ZbfY1vSLSxVqc5kGMe8utPUn2HesWxDUXDdRFjErCpqn3ufzN9b7aqEYUKOJ0eRc6ed/GTGtKqvgExES7MlI8ExI5LH/rHgMWhDsUlw8zPM1Glf5tnWaXY9DTvddadpBiVD/jK37suP+dQtj98vV+g1iVUL6WpzLoDDBKYjKLYDYdRDuwdqWe5pwHlUqARJqhohbG9w4C1gqaeY2hBeQTUTkcGjglwds5FcTTv5+s2JtptHkHShjNTLZc0svh+D8OJnOldKRSnz0qqMwlz7A7hoEL67v9cVC3RyoMCImmFhasdyERDC+1GjwvS/zZER5rawVHv4WwODwmKRXQ4lcgj9HHFd5V2RGF8GAphj9HI/XBaY27fz47F7CYU+rbG5vQYzWWB2TOva/jVho8hmhQJjv5CezKYh0WrZDJM+bjPSLvAMhToOlZ7W5N2/lnnrWHMtnRco5tI2+/PGnKhjsi1i/hPwUDP81KYVe+8duzVTrJFrXlqME5Ubb1VBG9IobTNOEFHF6SiOyAyKeZAKQ8DzFPxUmPWMUL6L4IkpjrvDQ82teg4zvlw7EN5ekF5TxDXGK/du+dDs4yNjYcnF4wnnA/poFqT6Cm1JWUNfwS3t1bSLnG/yGy+FdUswrfGcL9kHzriq2kRxYwE3eTNroJ06SI9b5R2coUo61nPfnJvhW1lYOZb4gbUYL3jn+zFEcvhUmKLyeJRYcefszY1kuZenS54ISavvpNaZBJbUuSKa9T1zj7gtC/8cIg1xaEeE3wf5UvARU9mzhwWEbmFYR8jJMtZnCt1ku0OtkP0t9BKl+9LRFNsu+/TXTifdVp+pi4Upbk3WxWa866IJuDVn29Ye7ZJusmGe7cytUa6plXkpUNbrWnCrTqqrnWs68atmmU3NbdOUcx26s9JFdhjWEu5Bm4tEvU9gJjcXovwElW4RLINWGPYxllT2sLEPIkTEco6Tic8cA6K6bguNk54kr7CM2ERqZ5lRMhup8+QmiBtXuDbplwe68BoYjhIjrzbEj+jgSqzcd0yQZmhJAzR9YE4SyIUw2o3nbmomwiVLEo6gRCftY6V+lxYV/5hf56fWtWzWVAI69dSzMuBK8q0KMecGind/dleWnQqL0itLyw13fBcauyRSYDY/2eU0+W3yCV8qqG6mAJ0VILWVPZh4Vvpg7T9D0EM+5i+RHFfcVh0ya+bAUhV5Q2EXcjnncbjl86mgg8/CwIKxpJrhInXxxV9fIDJk3uQlGs1mf72AsDS8x9h7xUzt2XD+Sf67jORdKDIdTGeUwt5kzhH6VdCxMCAA7MjMPr9yf3BuN28xiuwx2x8KWO7ZIzYUFJOXmYLk/C0Fy5K3xncdcpHfRyV9H8s/QHCOPw1J06N6TIIZYN7SBw43CnSFCDps6BO1Ir1el6J3UIG4cESh6id7VRvHuhKbqUHqIn/L4kspnG2uBLyDGbWHLmfUL4uh6DI5JIUMwRgKNG4fHaOUCta5vmk33Gx89n//QYg9+eBCHPH4MVRZyoez+1wMFz/MxPBNhewfhXOMeyPp8t20cni4NQ0srJLLK2VnPLj5QtHxcaCTlOYtD3Xa6VPONYEcrwfC8sbC6RZf3mKVzs/cV1XVTNhbYnrawlxsXzWcy0VHQne6u8xlokXkvS3TtrFubVbxrtzZgV1Gm1xyQmTMrGP0p06Z6ZlVwcR2cXB/qvppK16sdJGwtjG2brnKq5plhjERd1FzK+Yp9ILZc/tsIYsRPQ75KJj/vEvJU1FsAqyYc1m5nXLzunTh4Zwfv0YW8I3GoTgw5WktbaxZDrdRfbu+hHQK5+f+BiZF6AQ+Qk7paMK/dvytlnWMf4PDms57cyMV9PCNHvQ+nqjvOAlqdG9kLM0V1QjPHIijqqAUctTBB1J67u2h/tTxw+/Y648zVLoyIkmaLwTcn7bwbJbNL/jdVGa6/uNTzLkceH34PAsskfgf4e81t6HzWXlS7AVr/rB93CrEMku/DtGjDGyDgW6kTTAh65/xICeKhsdAeKcqdmiAsImxTI6RiLcOH2QWpnvg9BwuV0Q8EEBan/bwmfR1cNPL/OkaYphCFXY89MvRhqCy84IDm47MsVJ62HLs1Z4CDE7MTSvz85drOmyGkxzpUFn75bmv/thFLv7hi4KV43DO+lXWllX7zmt9CFn81HxxD6Ho+cv5lRyRdiwXAxbNe/bCleyebf7/FaxverG5zjUG44Y0o8r4PZbFB7yJr1MT9brI2pHlnE0IpNmx2E0PnM4gm485k9LvY0eXE1Qt27ZvnYOqAdsA4uLQlWIsKDYUUvdQVXkLRiTg6DfXJKQTpGuQ5YtEf6mpnG4wNtrUDDyZnX4JnXzp3GgVYXifLGaM0tZLS66zppPS7baU0qPze30zpcf3tlL6EO2sYneV85gCHTEZC+mw1F5IrPNFMMSEquGsNYv9faCUuSUY3FjLiJbOPqjNc7BiAp0UGOcxC/0pz6w5p4ihzRLPBnnJf37gcPxIngIwN9SMqIuq4C1kIR06KO2xihOVXfC5XhYe61WR4qgC5+hDCxnOq8upqJE3dZFIMOVLAj46JZ+P9I7lV6yfro3hUOXOMpz/+fksMJQSXKKTJfZ+5Jrh4KJxcY4h78wDc+hMEXFsh4fqxVyVJDzk8AcRF0h8+T74ELNohGmdwPheE54YEUSLjLnlVMQWIDVKj11PWt25LIi5gYm19Yk1ydaNsaTca+HkePMk4eQg+ZF65g8Atb8FdmfQuwHkawAZgTZ//L24hR0/DAYPG36tOdnXWTJRyC1Mwa9O6jgwr2Vy3clqwWaInJw4VEMkWOJWQaa7lwOctFN+ZiNciOPAfFQv/F/PDPjfGLZJFJMlpLIg3kukXKi9VClxdKI4QqOWHXjrrAJfD20YS9CpLxPK6mpxsAooD7RE5+yPiBAI+MZ4R8hk4XJ1/RnFWDstbkJbAgP23Cm8D8mJoKMVnASuNz4aFApo/2Wsav1AF+4/rkpwC5EtrMnJba5XYVJdTYIm6/ZaXZ1JDjTyrte5Sa10WDQVP+Ikk9rBM2iSTtclkZFicAGiOAjJ8BK8BwvRyCt4PRSZUkgkov9r7JBEjLErIaBIwosL59P7Mg4UKKgbNdD4VK44SVKsyM2pL+JGEpH4XFBtFBcehioz0TvynwQ25Ddu/YegiV8CrjtkHyMiFJydjyoFvDkMIpP2iz1NDCCBV74X3KskPItixKt8fcWBG8epAu1MwMToGHR9YCfEaEr1RSXk5NH8P5sxcbstNlDliGz4Pi6ny70IwZcr0psbiE/m1JYqU6moBq8+P54+ELsDk8BuOjXeuO3thXc/E6U4lPUiXy/XOOfj50+7/P/89l7yeklUc2QpXpq662XoohODsHscTxZLj+Xh3khswmqfhmGj4uH4arv8pTkd5KCI3iXvJOP7SUWcekGpnxQjd1DZOmHA+a4rMiXGJOiN2w5nHj90bIxNJgSh8xFfPpxO0aMICNJlAAi3hJbQwCOFgVOqJ0z3q0xPqfG4wAp2Uu0SUrtW++08LJJpjWzfv6+ka2b3j8P/xCqOCuuX1lbOARLObvnCSS+d+zqEfABLNIF00SaQTYzmgJVzUfOoY3TgsHtfQcxrjVEUsNAcih6pwQcrKezQgm4NX4jlajoXSggM2jyceoDXvIv7VGgmsZW6LDlYNFp8EEWoDG6tKSPqZwWb/yUfGYKMl4HMPzDefBtoME9cWdkWptJw/HsBits7mjB5cPFegFq+w6NG5kZUxrD7DVd1R9MPARakzy0o34gRHK/I7VCuP1bEymj3P+AhHG4+eCCRfBzbhzzUbVMWa9c9Vk8WqTW/9zp3t7pg859NEdnSDhL+j/6reCX9zFQVJ6WV5xvw5/Wvql/eCW3OhJAN+aE7n94NUZy282QpJmGIarQrTRwrNhlKBHIyIO5jk6JilLWvP4E/MhRKPzi804Wg/yAuUEk5yDhhFi8SdI2nIy8rjYiXpCaQtykhMIQYAFof/iu5Kfwm+hqvjeewTnBPFS8sfMlM8nQFsUJT5RZOgHces8nGVvOkdF4EszdK+civ2gzCyd2NFHwmesu2S5r9xSF3cx7uhQSp67flVltbJTVW9/Nsa4KBJPKC7XzcJeh6qmwgl3vC75pcEgcRnoeBfocmCBP97HvIUQ++yUBzgwafdTOMn0Z6k84EVi3ww7iC5cD8olmUel/aFKfRHgVh28r/bX/a+io1vZQNc3AbCBmDoQS8vV4JfH6PYwUHeMyh2Q+Y+5rgsGmLtTxbniPg8ut8LEawQ0JFKEZ92KygD+O8KZnsZ/hoVszNyuAkKOQnO7FmtKuzoUEjx4f50LXLtKgiKkvhCrD1iVZs6L6N1XaWghAmJtAq5KD47CwTuoivkCdyMHLZ49Jch4hUCi69eQ7R0f3y4VNHRUajqWQ0q7P31iFQBEScVj2blddRXlTa1p+WWdJbN0l0EkeV8evokxLwNlO9NYvrddfHe3ygCJxdjp0jbZkrzCDKSRAELdrKyxfGcXAlDsniKwvQcizory4+id2Yx3NnQeJqVb5EsNzToqlTRvqywRF2TEZTgke4PejQ34hmuflKZXEBCwvB5Md4Mn68liBYwZpAZWQrqb1P6BwViX8+s+Xxd5FvsBotHZDcv8hMHv+kgcOY9dP31y59QyuNPnxEtor7yCmj0OQF6ioqRGQbaGvbyow9OXoulSen7g5JgJDb6dZgn3sPBw+rZ7f6lrIuqZEfpQRYcr6BGHCLw/lbY0ZGv7OjKL1qzJr/AfqiyC5EshTozl8NbmN2bkQ38o+gKuYiXkcNOOPLLkO0ZShNfPYd0DmsTgVHBbC+DX0cS2CU7yhMp5CTa6PXpRM49B3S+JwciknlF3J2OEi92IjIDkm+xoRmajaevjq2DI4vtpKeJJqZxeCi+nZIF4cUOnqgVHgv2MhxIcbioMBmL4sewApODduFzz6Ccn1CvB9Du9yB+SlhZi9RWw6ClsnP/7q5AXP3FyWIhG4krKvan+YDolOPvBdpBzWA8QBfODL+XFS0vXJ625T27ta+qj1/a19QHXF5D4rp9K/aJoJGnJwAk0VzVXhVCR/+xG/lw/9UZ4cDqvtUiMEymrLEaNqSstBgGinkAqNkNIU9No+azcHjChItro1Wjq8sEAYdnzY9yv4ptygKb2hlxDFCm35BtBYCW1YS96g75QWP6FFDf6dHnhBovG7nxkbDELeSTHms6FPsR/WhXNsYoEAxuRKaQ9DzF80ahZjKSZaYFK1MSFJ1pZskMKUXPZS6wCJ88MrmwHsDgCUI/BI0UJomZBAxPP4cbRgkNCEaxhgkrulABWH6grhVtE74nIC/EFNCxsZiGdw2kRaSuXA8xFkdUrwKxeA8/EtXEKuXyVQyPk4JkFKH6sJegiDzutZQwghs5BrZztqupcWA4j0lkMv7h0yxAIqB4mr13UJWDC3N5SoQ4LD6MQB5mYBlEGZdBz+BAU5QFIa4ua13iQ/BOqNQcV3oUg5zI5dJo3p6DmH4BNoPqHkbw5qFE8wBJEjQV8DZoOmCbQ3SAIBjNXHfnduzd2+u0j8uN0tTv2VXfkFW3Z2dD/Reh/T/1XaPxgvhaAY8QrYKIHFLi4pJ9VaWJOA7gSfTDfKPCCCHBpyJsLoYF7QgIomJC3bNNwwnBcQrXtUGWdt5ZMTCo/LHkIMSoPlp3kg5hxqO1AG35IQljZJbGp7Um51ZLDATnbojrJ3SpiTQSRia1rtuxlBWodI/hg5J0dCQaEONL2ey/FrBZTr/g4TVKP+LpAWKKaBv3Uvaup63ffZH6e3kPeQ9oS/wP+9a3stXdPPMoA5nl3ObmEHVMH1eCiyLjMEHR4QRgo5ImekfHBcbwKNGRikh9PNZrjqv7d2/ncF0PEok0PI1pw94DKz3o/HIlWDdzbLF7DiL9bzRgtfGpfcqiBIGoXCwXYDnzCfOz5gN8EBN+cvdHENR9kcnAyz9CHB5YEomiBICAiQaJhBgnEZNJkoQ6215/b36YkO7r5wNmopccXDYO0etOVh9l7NX/cDrAQEnXX/oPNBYNR8KDwzM6F34zq7NqTfwdSZaL94da1Tg7NyAA03IJO1GgM7v2y8Tndr4iY4h86RoC0ulmI/EGBEHi5lwNkHIVqxxlhJhgZz/15pzgv2GZVJ+NzR8SfgSygZJUns5zQaKz8RzuhQnT+VjLM7PcPf3fya2caK/qwRP33ZpxE/TcDt47ZxVoRPysNjXK47YRnijRIaQ3mke7V5ZPg1eDD1j/VhGwJdeDhvQLmJo5WmU9N2qnubQiypJro6LFjeGSx7HnA+WbVt8YEHnirPUxxi0CExV9JwmvS2bR+VJhOAbyc+JjSrQtGKEI0gATeq3o/90Uj7cTtr/QpDXpp0lvN06Heq1VCHaspfpsjDokfNOIY6dma2Ou14BGrhg9qSeb+Ff8APm/jo1e3dvcTJ9tY27kKyFbAs2mhaKopNeRGRnFPlJ3LE6i5b1eIkqr7z53dlNqIYOT28uIkBZr1ps0Fms2qORRtGx1nL9JLDyj94hyMjY6OHxJQO8B5byE+tWtiK4lu7B093VVW3laXtW2zOJSkbSsSD4qMltH5caR49hkGnAMiaOxwvFMGjGA4hb9KhSDjwqt2n6Ac0KRvjRTosiCc5K9deYxxbQEVYHMf4KSbTanM6xA03amFA89vrSvoH9iVSBpeS81xQo54eyBQY8VYqTJHRjl6YFqSz08oUwZtGdJxkuJIpokkATlqiCwk2u8RTiRYiNgngSVSfk+qwf1G5Fc7xKBhehboNSratYcyE8FN4AvoHTBYzxS14qgjRdpTqm/c0VCQy2oTNFwZjhgWfNQ6rvOp3zBu/gL81+b+FjOdH22tl8NyW4muGXSHXKab1o4s91zVzbfX9WcK29emeRREsNpCBFVBBUq2enpDGZqOpslEr3DMDPkW5WkuSdYStsGRnhypw3aQLATQ8o35dzhh6TOnNqkYBlz0oY/sY1JP5oeeVi8+E1vYydSjsPHcU0rwAf5dPgS4Sxyh8g7Hbnbh0pnK2k739lNhbObl6YsLonwxqYBy8zM3SgJC8aHSFUOJsX5ujPTnrz8O5Aq5MTga4eQE0XNZISJ22R9GA/7UdTZqQgRuj2a5MRxrn8QuIXkdHdw8/kZCnYYy1ffQJ/lG8bOyAOlLG3Oe4xFvV4jtD/St2MkUAT/bbCqiTWN3aBCcAk4H6GPV5n3GDOyBgWFHBW8xr+EW/dYmnnrkv2cFjxHRxNLnBG834l1FtxPNvDRSjErxusjgtZla24M+61NMPWyTmS9ZIjegTemm8U0Kcn0JRYJWOhpKfnvIZfznA0a6auDWaO5+VKv525NobHpbFJSeXoTJV4qjBorqrTiV65m+9SutNs62r9eZkq+Kj5/xFdl6wnrQ5AQMUHsg5wgFya/Z8DhuBU6El2w3ZbLjm4yGwCrZOIrbtBknLIcSzzikZG6J4xPboT3MtrpA48nLr11riRmwXtqRfGbqBD4GaPtQGcg1dFaDTk+5GjnOd/yom0STE84kyet0Lynnj2reToUP6RoOm01192SqWmfe8TOaLBlrFLMn6rOIH0E075srEqfRf9zih57ysv5NMlpd2wsad+0uxzVLpO2Y4x+iB17wdLupfdbZztyM5nEe+1n6HxSG+NVb9mdyvQPgZvMSVba4SKC6C/qa/AvUqxUKUGPJlIdrU4pEvZJi06L3GImR8tNnNr5hkbYO2QGmUHxFJT9PIEZRkx4MRk3sFErBlsm25MqL9wpYESj3aZrz0EO5/TMKy3skrwea1zUJNkc6VqYhCO/Grfu88it3I2ccDzpjvRqYu96HAnppsCSR/6J9CZh1gMkhiyXFYy+kvU898TR1GVNDbL6B2cOLOvO8xecGc7UxXvFPighuUWlkVZ2VP7qSMoNsEdy6dGgR3I5RyULB8/JbhJayKp/o87eW3d7iEz2IWWfmoLuM2rQRI77WTaDD/k5jd5jCiymGNXoOMRzOYbRzFUQYuI4EPqS6qXOUBaq1VmaMIjm9HaoO4t6SXmazhL5hmxUh8dka5o/ODu925aUtTGWRsFnyuBoNTAYZpv8NksWD8kJGiAQglu8MpxDk+PWbgtternAtJ1VBifl7PT6PUUs/u0dMnMIbMKehDGn8UydWb5KMefeATzy1Zoxesokk878vJC+OyO1bKN3c0c6NqZWlP+urGVw2bgDV0gqbC1Ys6aI+F4mKaINbqZHxai3+JNQVdJw4JKoZVdU6D3YHAWPkFCATdOCuHPev4lsl8nbn0TC0UQtZ8knhAIV0E0OWzOQgW6c7iDH/Vv0fE3TwCe9W7rd8i/do59K1a3QINK8am3Zrq+2/HTvPVOu61ix9PYWlBn7PXITfnKSdJFmWZpPMcqmd9tFfGvjVe4w5i5/lEkD6/Ya9yoRRMM8RRBeGkeouXh2X6yZIkkcErg0mJOJr856wMtzcgopuyOTdpYu/6rW20kLpyGVsQrhjUJNDQkh4F+GIkr8HbrkUoUcjx4HUcaT5Huhy04VqSRm/nDKGE0cugBUhcm8Yzw/G8MOZ2WoEufQPE2fimmJckNlqQKnA0/0rXZ0vCagns1pqqgP09BxuTh55TKftX5KVzL6cnBDd6rgTDrHbe0hUV3XtRnuhCBzoWfK/Hn4A/2RZaZg0e63zziuiGlyLUzOLs3edONbZDLUW0NJQzXPMm+6RBXlzi94Nsy/a1292H+ICnnZhURvZQylUpRDXseqg8XKmqEeCui5hR408kF1WjWQg4KjRZN9hb+oI4lwJcMIc9bep2VtsqVG5W6W4BdMpM04mUWzM8Riv0ciZGuKxdYtKAwvQom9OA6Z9AuUb8zDOnIf7NP5aLGaNzvd64urzMhujFWFaM0DIUfD/HRN7G4CJGWQAMzb7FQ5RDA78KVHfmzUIl2KnjNh4x3/giubLsp3TguWIo2JZgVnzRZfN4fmH7jLp3t7KxJKerjWZAxjl25Oamty++mJ0MbdnjxH5OjuYmDIYSLVuIQK3/vJujxsEY6/6+/8BL7uR32kM4ax3zCCjJRQ/Clgtg80PYzt0kRV6IvwB4slZiFkwpvwtqoihZkPRb4x2qOqimxqp3P24rAUvFwkefxXM96lXIbzfAm/7CYYhAfBYQ3DcKGMI3qkXeg016Nvcc+Ofrqmq2nNWLZTcNtI28Yp410IoNu/o6gY/fxKg0SnSBZLxWZFRrDZKhY7LFia63ruz7jxYG+o9FCwa3SbAkR+mCUOtARg5IADDl/P5p1PYNoV9LRmiwgBbP1kGTMnzYpjSDAzsrI1MiMYWLJzkxhJG/XZRLQ4s7fBVskiEOfYWBmBv4YxyU1hvLDmGFnMLGedSrg/mt6fozN/VkwXR/B/gavgAHdOTKWderJ1CEq4fHUnQNOkT8A3tydDW78Q4H8C3LJ6+HsAfEuQVX7NP5j2FfD1PmCmfH2nMB/0+AIwvETAN+ULFkd8ViD+cuY1dy0i/VdJtj7zXCW8Nr1jFZX7I6Y2cr5v/m4PoYixgFEEGn/rCgsZFxnGoyugkkJA8ghClgXSnOEdwyWUzcvrdEexzAuaBHD/MyuNLiWFEYhoVBwhPK56DmsrW5yfy+HmKwT4xMhNqSKa6TonWh4BFUwkRAQIgVRy8zgW7rcE4C9p3mrUnO6tQpsAtiiWFzp4GqJUzRjE936vw/K0SBBfPY2poXATK+AN/nkX4NeY2lXf2nq4fyEvgmjyEcqRpP19EJxYmbgIhvavB4Svbv0/5CQr+8W5yEwLg7CXlLR0CvXvjNRUOALkIc1mOriqZhOd66ajbJbd5AHSZSUyGv2/1/nzD3M6ed7nWGZc+3v3klog8en7oEnP4FDvHiXcYf6GwOh+ZjiTYcQMY4KVsXS2TAl0ACsCFdHkWyube6i7Nlaj9qh7nf9t0LQsAWHrT0v98J0JU/cFr7AQZIH8dP/IyG+amAXEMDk3syAWemPn8AaQpPWjrKetbe7eVXmhoarzMmWVtihNlZFp4fRwMhjQvGfQw2llpDQD2KADyUUA3DtijamJDUtr1NeaI1Zy+DgQCzNb4Mm7LuLaO4p8DmbHgX0+ooPAqRbTDkSmuhh3FFLYzAubFEiZsyPp909hI3E+mVdfxA/Fhfhbj90woK1tZPIx6t0o6gHa+gC7xJQmdr42SFNu18EgwHTals2lRj7J8Od+lSWYmsXLFcTi8K7e+BAMj4mHuHBEGqKQkm2cUo1nU7Bzv0VAMAhquK/uKihSdz5UdxYVwBA3W4GwFUqtQtmbowQjgwMb+FCJSnRTiAgF3jrylgEsqLaDaUMyWLYJ2RQYsPoW7TbYe2sInjRM9hVTNtoy6foZ3bGVVzdCLRefbvmYq45ZLFyBIASEmbZvnhbqhK/2gHHfM3RL4No0AfV1O/Xbc6vO0OgdhM/Q4Q6FRdbS6x9IGOJj8nCBHP9eRkz9zNch+Gxc5cn3eXxJRy4mNj8XTrb/olssyGu3NNDNPa0TrLRdZ6mRgw52qPMUXAaBVRsHX/yiYvozej5fVYsk2asEui/zW44Bfd3hM3ZIZId3YHkHArUbNdmb/vaZCg3yzf7SDNfW7ZCPW/GlO2TUIzWNbIvDl8QiO00ofGDkXphCoDaXNl8y61Pp/b96q+g6/1weuofT8DmzkEJDTi+OXZzfVJvOTSmh2qpfltHfz+3ukM2lpvPjkg+P6bAZfBDxseN0Kl9MTvSm4vGS26BeCd8VDsyCmr51f2HWgjmfrnPCeVjcWyVzAXxDiwhBS7jM/pQrXREmxtV68d65aAf6JJ8DXsUkJnG0nFtEynsnKWTJ8KJCoNn0Fp1XMTdnWOzXuQ1Krvkvq5duz/5jMzJZVmsek0IjRzDweM0YY0oHSkJnZHK0v9Jyi3jiIiUQJULLWaIsiR6jW5jaXKcs6lydF8i3cWRAIiFFKdSjFHDlrStzVOrWPMnqs++pRF9RHBF2nhITgVTL4QfxOVpQYW+5KBpHSeeLGpMNaTkcUvIbiKnOU3avmu5elR2Z4isASs0pSlmWKfI4SQLyjXBS/05p3prlBSVrazJQQneSP73zoZiVLYnn5EoZgSz37AcUpifo2XgKLmMhsoLRczDr8fgBgwvvqynVaYvgcyNKirIWCQapUO0jJokrSqzxsibOJYE5Y4rLA0K8Sp/6twkSguEJttXaL+nYnAEk6WVwvlbIhPxekaIVQVySsXZkd/JoShE1kAmAbI6nhIWhtZaf86zJM4Db17O3ZKrDiY4UXRvEtBU5FPAmryeplbhEfCDiohxT0Dgb9ffu0+Buy7wKRxM0VmGQej8PLK4aVSM/jqCtjNhwoDd7E8YqIQhtBLMYrIsviog2u7+Y8bTPPbdZIwy3hLKnn+MfT6cFhCkf+9V1r5HJzl+WnNn+3J0Td4pRJAw8+nNDINzkBu0sLCU/0pBUtDJ/zZqi/I4uJTkv2sBQERVHS0fed5KXkd27MJvHycxVZynwRaynUe4KrSdnPS3MUJ31UWMB8cnjVtFnMbbio8azZBtL83Qxk/mXr8jP8j19GB8XAt1ZPpouBycIVJcqMomRlrj+7Mf9qm363ARXHNW1Bi69r4arUxbB54bstaLp5vpETZo99IQEKpZJd87Yp7/KMqsp1Tuj4V9KijIzRwTMiSqw2slRv6YvN3Q0ybE/Kbl58fUNvh8vMunl23jnQbIgcNx5YECy0cOk+za5BfOKvVLQEqejNPcTiEGlrCuw1rNOTWxU7HHxhb2PkvT80Se9P7aeo5oOBiFOj2gPqN7WYcQdvxdbirJBaC5m8GpYNm/G8MWRqqwTmCUxhEL9ABr4aC8sUaYvhTOdth6Fl5otXAP3ZTOPwKCEdzeeox3uNp64v0q4m3wKoNy0Wo32p+VNblMaLbP3sgcPNF3X93pBMXoBsSneqT4zSV5DTlGR3GxmAMbHA9P3h8KmANL4dXv+gxrtQZBGYYkwDT5RV3fqYN81mZ6WHLmxQMnl5efF622g5HFtENLzDS5vSqminB3YMIAMlND96aBR2vjjKG3pYfwRGuDfNYi7NCd0/brmpeLnGuCSg6kviSSC/24f8140tppyq2Hq5XtzmhhsePYa5CBd0MAU7/8S2MQRH6+c0M2tSwY+g1esVdgAy+D6eaDm/6MoEtbDSesrfQPeuIgZZLV7PA7/KmBT0XamL67cX6zX/llf//rRv5FziHP/dZujeLCcixw6HIMsAMHx+154XrUJ9noC+Dlf6N6WezQQefBLf/v9C+SMppKU7ks+o1DkvNhNEbNGaUXcFeHnBt/peFxk6TF4FyR6hev6izSQdQQqHEaxmRlnVLkYvo5hVyi0Yk634ZuHCrrPZRkV/RXZ/zxuJc8eZtT5clvMs09JR5VbkNzW8d8quQxQrnpWa5SbEcqds9zOyZ0Nz1rkzopnObkZoPBqKPaY+C2mvP9YbTxoC/WU8orJ1cG5GHP/Qo+VjiXjrHffQHVNc2wg53RcDURvEPfs6pyW1PHEe6ZOl8bDqntdMhEnBcofI/VR17DgRpHNoXLWNBehH0bCMlRtpU7NGUa5rDlGoQq9cW8+qHwAXTcOioylk5rNHA8N6RZXy1Ucl69x/HXxLa4CyFfm7/WYLuJfWEkTlTZYcQ0qxjJgi+W7g5CrCtd596rAukR2GLtr3I5CwX7jgbnS+/DhucMDGio/saoKizJ4EEJUW3hgTjUfM4eCXNg/t9AiGrJJ0NpHbh7qML9hCq9BSRRk0iJnniqUKDz5qfgsLsiH0aTUIhoySbB/w9QNqlNH5SbVraNvLomfPomzdnFF2Ki8W1nEZfHpppJ43jGvw8lfR2PNjhk8IE4/J7PPPCNMkPdBdqepaErq1sqjFyTlUarGPYmIp81+xauMUK2ukpObSI5rLCRs9T/cXhUlhcIxP+3GUzxFpfDMHcLX/PCWS36SQDVN5Mc+SUoWN5Uk17JkaulkC00TJSy6bkeS1CU3gaAu+QdsXQ739eE3qsV18a1WS25J+1wSujvyezASulooCt/gQGHm7XvZ3iAB6e2u4VhGZsmUdV7XgOEHHeZvPvgl3Q/1JenzltYt6KQiV3n2NnQvCesjnp1hnklxX2IeIvF6iHthvsIibYT6Auc9LXy4eLf0wtUOCcEAm8i6AdJRya3TmWfn0mRpxs++BMx9JrIOwmZVLFnvxSpeiWzHxbrOM4uK8xJaKXoV6NnVZWZkVBXvHOcAu4PKLa7jJc2r4xi0hSvacujnvr4LsSiytMIoUTlfRFdpeOTKyn+sIDEaXmbPBYpMxTUZ9vtrmahdmWGjqp5QFX3x5qEVr0LFcEo6IrQZ/N2EmYxyEpSyqLn7GUYrhGsop8C7rPFJJBtopnXQteCliUjs2JZJkm8tP7PXDbbU3guH2++vw2nJrsFuitDdlRqdQhfO6xmWq7e6INdnDtx7ZLcJp0VJ8mX7aS1FX5blXT/dEtySHvVsg+ReIq2Zpa4MMm33/nulPpHjAqXlvat4teZThN7iU7pmoLm1KDLWPjn8fYK6yzR6fmqXBeMo2WLGjhOni95F5u0i7Ar4CsCweJHaqOVFiVhqYYQhTptRR4SPamLKt+3yHqOTv2W4bCFVayvKBNtA5wtmzK0nbRNxpLYZaYptc5ZetS1wsxZfWi/Y1mRjmA2svWxbmr2G76zHbUfwB8mRH8UZrAh45CPkHJRsmInbBiQG2ggRMxgS1pUM5856FeP2zGijVMay8YjyJ8OHdYtNiOm+TYRZ0CYW2ChGwtbvbTL0bp8FcoJB40vBAoLCqpagDQVUsuXJF4Q/xS16a6GPkLz8INhUJF+mIqny5FnIxapsmbKoADU4cpWVRw7AtoUylVBIVQQ4fFcGoCSveL6kOjZw8LFENR4gkC3tPwrQRXxUF2uglmfRa5n9NKNI8RbzKbkMw7EOMLJXbJsqx9Pak2Iurrj4LTFyQXlxMLk0yw8ZoctilG9sivQGBEtIehbyehconB0ALEhVKpuCwkIqLuKCtpUopgIWrgduKwtVLvHgNhWrwB9eVJUCkQoKPkYYeyML8OFyfXz4y3cj7gcWeMWDk24hpWILuQWiU23lHaVJVQ29BpgxNVlRdNy5f6Fq/5QWFwABEsAEQmEYpP+qDNDvRMQGWdyQ0FByiwsFoF/tETJ6807NSmbckVceCZ8+fAlImzUtAyBdG5BzwCbMWbZg0ZJbEFetWJUFNUNvy4ZNMG88kkPIliuPglIfrEIFihQroVKqzGs0lSpUWWyRffy4lqjG9+CJku9jYLHa7I7++Ntcl9urf9J5VIlgbzKFShBstR8/cuYLhIRiJlGu5v5BUYo3rj83OcWVjxw7kZlGrfrMebXOXXRD7IrMUMVFQ/8eYC1tx8KG4CCUivAJ9Dzt329PXdYggZiEVKIkMslSpEqTTm6hDJmynEGG1y+PxXT4BV25FlGoSDGVkgxAprVXqmrHqX0LkZO66ziWxZbktFS1GrWWWW6FOvUaJKW6aCahT/ieU3dCR7s7SdAx0jZ3ylBHOqi1ltZhXhdktXZrdOhM3VBr6+m9PmI31V5vg62qo07d7afW07jRHP7TS0NrnT7rbdAvJMIMKT+SmW269u122GmX3Vjp0H4HELKzP+Swn0jZrDnzZLhIxD1ZtmzbsWvPvgOHjhw7cerMOWcufOXajRYVfq8O6zjST79I03X9B3NcchbG1iZmxlbcRX5mImYSbq2mrqGppZ1a7+kbGBoZm2DTuCPSGjpFO7Ln0LGKLl35TlBnre4yYGJc+ycmWa9WF6vhyGssif+5Qg9c5yMp7a9nEdXDQ9JkrLIw2KBlZxKUfumo+v+1B+39ZlvJlaFZV0Q07XAHqRml57pzKQzuU4M1bhmCprBna1Tm0dSLDIAGmJnDcx8MiXSuGfN8wTE6bUM8Ou+DyUITN87MlHYz0T6KLFPznKF+0mErF1kPlu/VllnQ/3RiPU76Bmz22jDhOCkaHz1kPzD5dyRsZcil+13D0HSecNwk6TQwwMpR040zN1dmNrUt8klhyfm5BXy1DKEUbRdmpvt0xaaaLKewXuIg49kKgVr/yQ2qQZa0GwSng3FXX4ti8XDk4twBsT/OPiFQZx8SFWonlej19WHjrm1RiukvHdi0EGcvAKAMMgJQdG0OLVt2bU6nSRfylKMOQ2EHqiZ6nj53vdS+FlW5U3/Prgx3/r9mSylgaFPf1rB0456u2e0Q9Jz21UJbSSPrm/XJyc7ZRe/SLeJVofKYte1555y5OH94eYXynkNbY+gZ+jzF6vmY5HnNepVqgTybFpVI7husYpTtX1ySEs63stlIl7HfejY4i4aOtPMxX9HCaXS1M4vPLpG6a+3f9jFzGKwO2h6T1cwYgpYzQ4n3N+vzwcmO2UmdUQ2m0BobqqVgfDi/+fZ0e4vE4m8NeK/N0VZS2nXjmbMyu7MYMitPeKkhtxiBRoXAkxUusSQhaJyiLC8Wr8phzDcjTEiltYTELR1IJnU28d8ClHkfqrsmLhL/dcXkSlQOjfoVoYyeKDgf0yJC4Q5UubRyIuC9PlK5DHe+N4L9KEg7mrWMTwgDMTEuEgbIG4HaUm2w+03b9Zvatd2y++8bklHiLLHRGiNSNzLMBMfSbUz0zqJMtPVARNN6uNGDEGJFoeBbcK6TKmfP9/1KBPVghzaW+q/XX7XQqi/Iz7qrs82aI7BEy8DUc3jPOut/SciObnxzqAVCurDU0iRsWfpW1mg5n98iUrOzRG0bXuBnR9cRnrhy1hznOKfHOEdAc7rSnBBwjqAfDOaGTgtk3lQBGtpwNL0CBKYGFYlbKgCB4LwRaYm5kYdk4gTGozcpBZ1d43IiqokrNvxCSWTFAWlFKRkLv2WctvgTRsCS7zlzafrqWLvWeCN13+TVQrOu7ejIu4pr4w6tlGfF4Y0z2OBABDEkAEICDDgMcIRLDKMF541OgxTu/YDpmiRt8LlwMiwyoSANLQG0JJD4DSxGkrUAWrIg/Y4CRWUPDJp94PTnDfYX8CRsjFWrwnIN94172nqcGjymRG+IvPCps7Qq1OFVwjRWqnZMDz+lKew8NEyELL6nSeFoXwKJkIYu3MbymnOqcvksVx5quDwX6y+xtcuSnxiOSUDceNQ4EZhUDmQkPyjiqQwCJS38ScB0PDSGuITlwdm/RqzTjtWu4h7aJ4/vxh+pIin8x1knqMxT3vujTriY0eOWctB4rWUt8xfAW7wJTdwefmHRdfgaYyOAM2iWmrUZH/MtHbGTHoGRs0fb31UVnb8n4mIEPTy3MOLprPU9HdwLwwAAAAA=) format('woff2'),\n      url(https://assets.genius.com/fonts/programme_light.woff?1720540228) format('woff');\n    font-style: normal;\n    font-weight: 100;\n  }\n\n  @font-face {\n    font-family: 'Programme';\n    src: url(https://assets.genius.com/fonts/programme_light_italic.woff2?1720540228) format('woff2'),\n      url(https://assets.genius.com/fonts/programme_light_italic.woff?1720540228) format('woff');\n    font-style: italic;\n    font-weight: 100;\n  }\n</style>\n\n    \n    <script>\n  window['Genius.cmp'] = window['Genius.cmp'] || [];\n</script>\n\n\n\n    <style data-styled=\"true\" data-styled-version=\"5.1.0\">.kiNXoS{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:100%;line-height:0;}/*!sc*/\n.hOMcjE{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:100%;height:415px;line-height:0;}/*!sc*/\n.dTXQYT{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:100%;height:250px;line-height:0;}/*!sc*/\ndata-styled.g1[id=\"DfpAd__Container-sc-1tnbv7f-0\"]{content:\"kiNXoS,hOMcjE,dTXQYT,\"}/*!sc*/\n.ilfajN{position:absolute;left:50%;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%);background:#e9e9e9;}/*!sc*/\n.hoVEOg{position:absolute;left:50%;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%);width:300px;height:250px;background:#e9e9e9;}/*!sc*/\n.bIwkeM{position:absolute;left:50%;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%);width:728px;height:90px;background:#e9e9e9;}/*!sc*/\n.dQxFmL{position:absolute;left:50%;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%);background:#601216;}/*!sc*/\ndata-styled.g2[id=\"DfpAd__Placeholder-sc-1tnbv7f-1\"]{content:\"ilfajN,hoVEOg,bIwkeM,dQxFmL,\"}/*!sc*/\n.dIgauN{width:100%;}/*!sc*/\ndata-styled.g4[id=\"LeaderboardOrMarquee__Sticky-yjd3i4-0\"]{content:\"dIgauN,\"}/*!sc*/\n.jOhzET{background-color:black;min-height:calc(3rem + 90px);display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}/*!sc*/\ndata-styled.g6[id=\"Leaderboard__LeaderboardOrMarquee-da326u-0\"]{content:\"jOhzET,\"}/*!sc*/\n.kNjZBr{font-size:.75rem;font-weight:100;line-height:1;-webkit-letter-spacing:1px;-moz-letter-spacing:1px;-ms-letter-spacing:1px;letter-spacing:1px;text-transform:capitalize;color:#fff;}/*!sc*/\n.kokouQ{font-size:.75rem;font-weight:400;line-height:1;-webkit-letter-spacing:1px;-moz-letter-spacing:1px;-ms-letter-spacing:1px;letter-spacing:1px;text-transform:uppercase;color:#fff;}/*!sc*/\ndata-styled.g7[id=\"TextLabel-sc-8kw9oj-0\"]{content:\"kNjZBr,kokouQ,\"}/*!sc*/\n.jDxAhO svg{display:block;fill:none;height:2.25rem;margin:auto;stroke:#9a9a9a;stroke-width:.05rem;width:2.25rem;}/*!sc*/\n.jDxAhO svg circle{-webkit-animation:hFBEL 2s ease-out infinite;animation:hFBEL 2s ease-out infinite;background-color:inherit;}/*!sc*/\ndata-styled.g18[id=\"PlaceholderSpinner__Container-r4gz6r-0\"]{content:\"jDxAhO,\"}/*!sc*/\n.FnKAk{position:relative;background-position:center;padding-bottom:100%;background-color:#601216;background-size:cover;}/*!sc*/\ndata-styled.g25[id=\"SizedImage__Container-sc-1hyeaua-0\"]{content:\"FnKAk,\"}/*!sc*/\n.iMdmgx:not([src]){visibility:hidden;}/*!sc*/\ndata-styled.g26[id=\"SizedImage__Image-sc-1hyeaua-1\"]{content:\"iMdmgx,\"}/*!sc*/\n.UJCmI{position:absolute;width:100%;height:100%;object-fit:cover;}/*!sc*/\ndata-styled.g27[id=\"SizedImage__NoScript-sc-1hyeaua-2\"]{content:\"UJCmI,\"}/*!sc*/\n.dPICWx{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;background:#fff;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:calc(10 * 1rem + .75rem);height:1.5rem;}/*!sc*/\n.dPICWx .styleAnchors__PageHeaderDropdownMenu-sc-16isfwt-1{right:0;}/*!sc*/\ndata-styled.g37[id=\"StickyNavSearchdesktop__Form-sc-1wddxfx-0\"]{content:\"dPICWx,\"}/*!sc*/\n.QoIjR{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;position:relative;cursor:pointer;padding:0 .75rem;}/*!sc*/\n.QoIjR svg{fill:#000;width:.625rem;}/*!sc*/\ndata-styled.g38[id=\"StickyNavSearchdesktop__Icon-sc-1wddxfx-1\"]{content:\"QoIjR,\"}/*!sc*/\n.cmGDeX{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;position:relative;border:none;border-radius:0;background:#fff;color:#000;font-family:'Programme',Arial,sans-serif;font-feature-settings:'ss07','ss08','ss11','ss12','ss14','ss15','ss16','ss18','ss19','ss20','ss21';font-size:inherit;font-weight:inherit;line-height:inherit;-webkit-appearance:none;overflow:hidden;resize:none;outline:none;padding-left:.75rem;}/*!sc*/\n.cmGDeX::-webkit-input-placeholder,.cmGDeX::-webkit-input-placeholder{color:#000;font-size:.75rem;}/*!sc*/\n.cmGDeX::-moz-placeholder,.cmGDeX::-webkit-input-placeholder{color:#000;font-size:.75rem;}/*!sc*/\n.cmGDeX:-ms-input-placeholder,.cmGDeX::-webkit-input-placeholder{color:#000;font-size:.75rem;}/*!sc*/\n.cmGDeX::placeholder,.cmGDeX::-webkit-input-placeholder{color:#000;font-size:.75rem;}/*!sc*/\n.cmGDeX::-ms-input-placeholder{color:#000;font-size:.75rem;}/*!sc*/\n.cmGDeX:focus::-webkit-input-placeholder{color:#9a9a9a;font-size:.75rem;}/*!sc*/\n.cmGDeX:focus::-moz-placeholder{color:#9a9a9a;font-size:.75rem;}/*!sc*/\n.cmGDeX:focus:-ms-input-placeholder{color:#9a9a9a;font-size:.75rem;}/*!sc*/\n.cmGDeX:focus::placeholder{color:#9a9a9a;font-size:.75rem;}/*!sc*/\ndata-styled.g39[id=\"StickyNavSearchdesktop__Input-sc-1wddxfx-2\"]{content:\"cmGDeX,\"}/*!sc*/\n.gexBFu{-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:auto;background-color:transparent;border-radius:1.25rem;padding:.5rem 1.313rem;border:1px solid #000;font-family:'HelveticaNeue',Arial,sans-serif;font-size:1rem;font-weight:400;line-height:1.1;color:#000;cursor:pointer;}/*!sc*/\n.gexBFu svg{fill:currentColor;}/*!sc*/\n.gexBFu:hover:enabled{background-color:#000;color:#fff;}/*!sc*/\n.gexBFu:hover:enabled span{color:#fff;}/*!sc*/\n.gexBFu:hover:enabled svg{fill:#fff;}/*!sc*/\n.efSZT{-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:auto;background-color:transparent;border-radius:1.25rem;padding:.5rem 1.313rem;border:1px solid #fff;font-family:'HelveticaNeue',Arial,sans-serif;font-size:1rem;font-weight:400;line-height:1.1;color:#fff;cursor:pointer;}/*!sc*/\n.efSZT svg{fill:currentColor;}/*!sc*/\n.efSZT:hover:enabled{background-color:#fff;color:#000;mix-blend-mode:screen;}/*!sc*/\n.efSZT:hover:enabled span{color:#000;}/*!sc*/\n.efSZT:hover:enabled svg{fill:#000;}/*!sc*/\ndata-styled.g56[id=\"Button__Container-rtu9rw-0\"]{content:\"gexBFu,efSZT,\"}/*!sc*/\n.jTJfqD{font-weight:100;color:inherit;}/*!sc*/\n.jTJfqD svg{fill:#fff;}/*!sc*/\n.iegxRM{font-weight:100;-webkit-text-decoration:underline;text-decoration:underline;color:inherit;}/*!sc*/\n.iegxRM:hover{-webkit-text-decoration:none;text-decoration:none;}/*!sc*/\n.iegxRM svg{fill:#fff;}/*!sc*/\n.fqTa-dX{font-weight:100;color:#fff;}/*!sc*/\n.fqTa-dX svg{fill:#fff;}/*!sc*/\n.ietQTa{font-weight:100;-webkit-text-decoration:underline;text-decoration:underline;color:inherit;}/*!sc*/\n.ietQTa:hover{-webkit-text-decoration:none;text-decoration:none;}/*!sc*/\n.ietQTa svg{fill:#000;}/*!sc*/\ndata-styled.g62[id=\"StyledLink-sc-3ea0mt-0\"]{content:\"jTJfqD,iegxRM,fqTa-dX,ietQTa,\"}/*!sc*/\n.blSbzj svg{height:1.33em;margin-left:calc(-1em * 0.548625);vertical-align:top;}/*!sc*/\n.blSbzj::before{content:'\\2003';font-size:0.548625em;}/*!sc*/\ndata-styled.g63[id=\"InlineSvg__Wrapper-sc-1342j7p-0\"]{content:\"blSbzj,\"}/*!sc*/\n.hVAZmF{font-family:inherit;font-size:inherit;font-weight:inherit;color:inherit;line-height:1;}/*!sc*/\n.ixMmYX{font-family:inherit;font-size:inherit;font-weight:inherit;color:inherit;line-height:1;-webkit-text-decoration:underline;text-decoration:underline;}/*!sc*/\ndata-styled.g66[id=\"TextButton-sc-192nsqv-0\"]{content:\"hVAZmF,ixMmYX,\"}/*!sc*/\n.bwWQCs{font-size:.75rem;font-weight:100;line-height:1;-webkit-letter-spacing:1px;-moz-letter-spacing:1px;-ms-letter-spacing:1px;letter-spacing:1px;text-transform:capitalize;color:#fff;}/*!sc*/\ndata-styled.g77[id=\"StickyNavSignUp__Button-sc-14apwk1-0\"]{content:\"bwWQCs,\"}/*!sc*/\n.iIgbGO svg{display:block;fill:#fff;}/*!sc*/\ndata-styled.g78[id=\"PageHeaderLogo__Link-sc-175tsd3-0\"]{content:\"iIgbGO,\"}/*!sc*/\n.hiGcDl{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;font-weight:100;font-size:.75rem;height:3rem;background-color:#e02324;color:#fff;position:-webkit-sticky;position:sticky;top:calc(0);z-index:6;}/*!sc*/\ndata-styled.g79[id=\"StickyNavdesktop__Container-sc-9maqdk-0\"]{content:\"hiGcDl,\"}/*!sc*/\n.ckFWax{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-left:1rem;}/*!sc*/\ndata-styled.g80[id=\"StickyNavdesktop__Left-sc-9maqdk-1\"]{content:\"ckFWax,\"}/*!sc*/\n.JliIh{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-right:1rem;}/*!sc*/\ndata-styled.g81[id=\"StickyNavdesktop__Right-sc-9maqdk-2\"]{content:\"JliIh,\"}/*!sc*/\n.clweAx{margin-right:1.5rem;line-height:1.33;color:#fff;}/*!sc*/\n.clweAx span{white-space:nowrap;color:#fff;}/*!sc*/\n.clweAx:hover{-webkit-text-decoration:underline;text-decoration:underline;}/*!sc*/\ndata-styled.g83[id=\"StickyNavdesktop__SiteLink-sc-9maqdk-4\"]{content:\"clweAx,\"}/*!sc*/\n.RakDs{margin-right:1.5rem;line-height:1.33;color:#fff;}/*!sc*/\n.RakDs span{white-space:nowrap;color:#fff;}/*!sc*/\n.RakDs:hover{-webkit-text-decoration:underline;text-decoration:underline;}/*!sc*/\ndata-styled.g84[id=\"StickyNavdesktop__AuthTextButton-sc-9maqdk-5\"]{content:\"RakDs,\"}/*!sc*/\n.dwjYdZ{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-left:2.25rem;}/*!sc*/\ndata-styled.g85[id=\"StickyNavdesktop__Subnavigation-sc-9maqdk-6\"]{content:\"dwjYdZ,\"}/*!sc*/\n.hRWuAt svg{height:13px;}/*!sc*/\ndata-styled.g86[id=\"StickyNavdesktop__Logo-sc-9maqdk-7\"]{content:\"hRWuAt,\"}/*!sc*/\n.hrQuZg{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;font-size:.75rem;font-family:'Programme',Arial,sans-serif;color:#fff;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;gap:.25rem;}/*!sc*/\n.hrQuZg svg{display:block;height:1.313rem;fill:#fff;}/*!sc*/\n.hrQuZg:disabled{color:rgba(255,255,255,0.5);}/*!sc*/\n.gcYcIR{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;font-size:.75rem;font-family:'Programme',Arial,sans-serif;color:#fff;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;gap:.25rem;}/*!sc*/\n.gcYcIR svg{display:block;height:1em;fill:#fff;}/*!sc*/\n.gcYcIR:disabled{color:rgba(255,255,255,0.5);}/*!sc*/\n.bSorfX{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;font-size:.75rem;font-family:'Programme',Arial,sans-serif;color:#000;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;gap:.25rem;}/*!sc*/\n.bSorfX svg{display:block;height:.75rem;fill:#000;}/*!sc*/\n.bSorfX:disabled{color:#9a9a9a;}/*!sc*/\ndata-styled.g100[id=\"LabelWithIcon__Container-hjli77-0\"]{content:\"hrQuZg,gcYcIR,bSorfX,\"}/*!sc*/\n.jwfWMJ{-webkit-text-decoration:underline;text-decoration:underline;font-weight:100;font-size:.75rem;color:inherit;}/*!sc*/\n.hgsvkF{-webkit-text-decoration:none;text-decoration:none;font-weight:100;font-size:.75rem;color:inherit;}/*!sc*/\ndata-styled.g101[id=\"LabelWithIcon__Label-hjli77-1\"]{content:\"jwfWMJ,hgsvkF,\"}/*!sc*/\n.cRrFdP{display:block;position:relative;}/*!sc*/\ndata-styled.g102[id=\"Tooltip__Container-sc-1uvy5c2-0\"]{content:\"cRrFdP,\"}/*!sc*/\n.dvOJud{cursor:pointer;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}/*!sc*/\ndata-styled.g104[id=\"Tooltip__Children-sc-1uvy5c2-2\"]{content:\"dvOJud,\"}/*!sc*/\n.eMjKRh{position:relative;}/*!sc*/\ndata-styled.g105[id=\"Pyong__Container-yq95kq-0\"]{content:\"eMjKRh,\"}/*!sc*/\n.hjExsS{-webkit-align-items:baseline;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;width:100%;margin-bottom:.5rem;}/*!sc*/\ndata-styled.g128[id=\"HeaderArtistAndTracklistdesktop__Container-sc-4vdeb8-0\"]{content:\"hjExsS,\"}/*!sc*/\n.bYBBwt{font-size:1rem;font-weight:100;}/*!sc*/\n.bYBBwt [object Object]{font-size:1rem;line-height:1.33;white-space:nowrap;}/*!sc*/\ndata-styled.g129[id=\"HeaderArtistAndTracklistdesktop__ListArtists-sc-4vdeb8-1\"]{content:\"bYBBwt,\"}/*!sc*/\n.glZsJC{font-size:1rem;line-height:1.33;font-weight:100;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}/*!sc*/\n.glZsJC:before{content:\"•\";margin:0 .5rem;}/*!sc*/\ndata-styled.g130[id=\"HeaderArtistAndTracklistdesktop__Tracklist-sc-4vdeb8-2\"]{content:\"glZsJC,\"}/*!sc*/\n.iVIbmD{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-column-gap:1.5rem;column-gap:1.5rem;}/*!sc*/\ndata-styled.g131[id=\"HeaderCredits__Container-wx7h8g-0\"]{content:\"iVIbmD,\"}/*!sc*/\n.jwiCow{font-weight:100;font-size:.75rem;line-height:1.33;color:#fff;max-width:50%;}/*!sc*/\ndata-styled.g132[id=\"HeaderCredits__Section-wx7h8g-1\"]{content:\"jwiCow,\"}/*!sc*/\n.ghcavQ{display:block;line-height:1.2;font-size:.75rem;color:#fff;}/*!sc*/\ndata-styled.g133[id=\"HeaderCredits__Label-wx7h8g-2\"]{content:\"ghcavQ,\"}/*!sc*/\n.cTzqde{font-size:1rem;}/*!sc*/\ndata-styled.g134[id=\"HeaderCredits__List-wx7h8g-3\"]{content:\"cTzqde,\"}/*!sc*/\n.cDJyol{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-column-gap:1rem;column-gap:1rem;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}/*!sc*/\n.cDJyol svg{fill:#fff;}/*!sc*/\ndata-styled.g135[id=\"MetadataStats__Container-sc-1t7d8ac-0\"]{content:\"cDJyol,\"}/*!sc*/\n.gZUIou{text-transform:capitalize;}/*!sc*/\n.gZUIou svg{height:1em;}/*!sc*/\ndata-styled.g138[id=\"MetadataStats__LabelWithIcon-sc-1t7d8ac-3\"]{content:\"gZUIou,\"}/*!sc*/\n.cujBpY{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;padding:1.5rem 0;grid-column:left-start / left-end;-webkit-transform-origin:top;-ms-transform-origin:top;transform-origin:top;-webkit-transition-property:-webkit-transform,max-height,padding,opacity;-webkit-transition-property:transform,max-height,padding,opacity;transition-property:transform,max-height,padding,opacity;-webkit-transition-duration:100ms;transition-duration:100ms;-webkit-transition-timing-function:ease;transition-timing-function:ease;-webkit-transform:none;-ms-transform:none;transform:none;max-height:100vh;opacity:1;pointer-events:auto;}/*!sc*/\n.cujBpY .desktop_song_inread_prebid_container,.cujBpY .desktop_song_inread2_prebid_container,.cujBpY .desktop_song_inread3_prebid_container{margin:auto !important;}/*!sc*/\ndata-styled.g140[id=\"InreadContainer__Container-sc-19040w5-0\"]{content:\"cujBpY,\"}/*!sc*/\n.csMTdh{width:100%;}/*!sc*/\ndata-styled.g142[id=\"PrimisPlayer__Container-sc-1tvdtf7-1\"]{content:\"csMTdh,\"}/*!sc*/\n.jzPNvv{text-align:center;color:#fff;}/*!sc*/\n.jzPNvv .ExpandableContent__TextButton-sc-1165iv-2{margin-top:.75rem;}/*!sc*/\n.jzPNvv .ExpandableContent__TextButton-sc-1165iv-2,.jzPNvv .ExpandableContent__Button-sc-1165iv-1{line-height:1.25;}/*!sc*/\n.jzPNvv svg{fill:#fff;}/*!sc*/\ndata-styled.g179[id=\"ExpandableContent__ButtonContainer-sc-1165iv-3\"]{content:\"jzPNvv,\"}/*!sc*/\n.huhsMa{overflow:hidden;max-height:250px;-webkit-mask:linear-gradient(rgba(0,0,0,1) 0%,transparent);mask:linear-gradient(rgba(0,0,0,1) 0%,transparent);}/*!sc*/\ndata-styled.g180[id=\"ExpandableContent__Content-sc-1165iv-4\"]{content:\"huhsMa,\"}/*!sc*/\n.jecoie{display:grid;grid-template-columns: [page-start] 2.25rem [grid-start header-left-start] 1fr [left-start] repeat(2,1fr) [header-left-end header-right-start] repeat(4,1fr) [left-end right-start] repeat(4,1fr) [right-end] 1fr [grid-end header-right-end] 2.25rem [page-end];grid-gap:0.75rem;}/*!sc*/\n@media screen and (min-width:1164px){.jecoie{grid-template-columns: [page-start] 1fr [grid-start header-left-start] 5rem [left-start] repeat(2,5rem) [header-left-end header-right-start] repeat(4,5rem) [left-end right-start] repeat(4,5rem) [right-end] 5rem [grid-end header-right-end] 1fr [page-end];}}/*!sc*/\n@media screen and (min-width:1526px){.jecoie{grid-template-columns: [page-start] 1fr [grid-start header-left-start] 6rem [left-start] repeat(2,6rem) [header-left-end header-right-start] repeat(4,6rem) [left-end right-start] repeat(4,6rem) [right-end] 6rem [grid-end header-right-end] 1fr [page-end];}}/*!sc*/\ndata-styled.g184[id=\"SongPageGriddesktop-sc-1px5b71-0\"]{content:\"jecoie,\"}/*!sc*/\n.jrQaBI{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:flex-end;-webkit-box-align:flex-end;-ms-flex-align:flex-end;align-items:flex-end;}/*!sc*/\n@media screen and (min-width:1526px){.jrQaBI{width:calc(6 * 5rem + 2 * 0.75rem);}}/*!sc*/\ndata-styled.g188[id=\"HeaderBio__Container-oaxemt-0\"]{content:\"jrQaBI,\"}/*!sc*/\n.brNcYU{font-size:1rem;font-weight:100;line-height:1.33;-webkit-text-decoration:none;text-decoration:none;color:#fff;width:100%;}/*!sc*/\n.brNcYU:hover span{-webkit-text-decoration:none;text-decoration:none;}/*!sc*/\ndata-styled.g189[id=\"HeaderBio__Wrapper-oaxemt-1\"]{content:\"brNcYU,\"}/*!sc*/\n.dggIzN{-webkit-text-decoration:underline;text-decoration:underline;white-space:nowrap;}/*!sc*/\n.dggIzN svg{fill:#fff;}/*!sc*/\ndata-styled.g190[id=\"HeaderBio__ViewBio-oaxemt-2\"]{content:\"dggIzN,\"}/*!sc*/\n.hNLJnc{position:absolute;width:100%;bottom:calc(0 + 3rem);z-index:-2;}/*!sc*/\ndata-styled.g193[id=\"StickyNavSentinel__Sentinel-sc-1yh9i7p-0\"]{content:\"hNLJnc,\"}/*!sc*/\n.jJZPKi{z-index:4;background-image:linear-gradient(#e02324,#601216);padding-top:.75rem;padding-bottom:1.5rem;position:relative;color:#fff;}/*!sc*/\ndata-styled.g194[id=\"SongHeaderdesktop__Container-sc-1effuo1-0\"]{content:\"jJZPKi,\"}/*!sc*/\n.diUihk{width:100%;height:100%;position:relative;grid-column:header-left-start / header-left-end;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;}/*!sc*/\n@media screen and (min-width:1526px){.diUihk{margin-bottom:-3rem;}}/*!sc*/\ndata-styled.g195[id=\"SongHeaderdesktop__Left-sc-1effuo1-1\"]{content:\"diUihk,\"}/*!sc*/\n.lfjman{grid-column:header-right-start / header-right-end;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;}/*!sc*/\ndata-styled.g196[id=\"SongHeaderdesktop__Right-sc-1effuo1-2\"]{content:\"lfjman,\"}/*!sc*/\n.hEtDoX{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;row-gap:1rem;}/*!sc*/\ndata-styled.g197[id=\"SongHeaderdesktop__Bottom-sc-1effuo1-3\"]{content:\"hEtDoX,\"}/*!sc*/\n.ieJVb{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;max-width:32rem;width:100%;row-gap:1rem;}/*!sc*/\ndata-styled.g198[id=\"SongHeaderdesktop__Information-sc-1effuo1-4\"]{content:\"ieJVb,\"}/*!sc*/\n.dhqXbj{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;row-gap:.5rem;}/*!sc*/\ndata-styled.g199[id=\"SongHeaderdesktop__SongDetails-sc-1effuo1-5\"]{content:\"dhqXbj,\"}/*!sc*/\n.jLdecJ{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end;width:100%;}/*!sc*/\n@media screen and (min-width:1526px){.jLdecJ{max-height:calc(100% + .75rem);}}/*!sc*/\ndata-styled.g200[id=\"SongHeaderdesktop__CoverArtContainer-sc-1effuo1-6\"]{content:\"jLdecJ,\"}/*!sc*/\n.fVjbnr{line-height:0;}/*!sc*/\n.fVjbnr img{display:inline-block;box-shadow:0 0 .75rem 0 rgba(0,0,0,0.05);}/*!sc*/\ndata-styled.g201[id=\"SongHeaderdesktop__CoverArt-sc-1effuo1-7\"]{content:\"fVjbnr,\"}/*!sc*/\n.isLvDW{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;font-size:2rem;font-weight:400;font-feature-settings:'ss07','ss08','ss11','ss12','ss14','ss15','ss16','ss18','ss19','ss20','ss21';color:#fff;line-height:1.125;word-wrap:break-word;}/*!sc*/\ndata-styled.g202[id=\"SongHeaderdesktop__Title-sc-1effuo1-8\"]{content:\"isLvDW,\"}/*!sc*/\n.iMpFIj{opacity:1;}/*!sc*/\ndata-styled.g205[id=\"SongHeaderdesktop__HiddenMask-sc-1effuo1-11\"]{content:\"iMpFIj,\"}/*!sc*/\n.hLhFfl{position:absolute;right:calc(100% + 1rem);top:.75rem;}/*!sc*/\ndata-styled.g206[id=\"SongHeaderdesktop__PyongWrapper-sc-1effuo1-12\"]{content:\"hLhFfl,\"}/*!sc*/\n.eVLetJ{height:100%;width:100%;}/*!sc*/\n@media screen and (min-width:1164px){.eVLetJ{height:252px;width:252px;}}/*!sc*/\n@media screen and (min-width:1526px){.eVLetJ{height:340px;width:340px;}}/*!sc*/\ndata-styled.g207[id=\"SongHeaderdesktop__SizedImage-sc-1effuo1-13\"]{content:\"eVLetJ,\"}/*!sc*/\n.jDbgEP{width:100%;}/*!sc*/\ndata-styled.g208[id=\"SongHeaderdesktop__HeaderBio-sc-1effuo1-14\"]{content:\"jDbgEP,\"}/*!sc*/\n.nPqjG{max-width:400px;height:calc((400px / 1.7777777777777777) + 59px + 7px);width:400px;min-width:400px;margin-left:1.5rem;}/*!sc*/\ndata-styled.g209[id=\"SongHeaderdesktop__PrimisContainer-sc-1effuo1-15\"]{content:\"nPqjG,\"}/*!sc*/\n.gGGmJL{position:relative;height:100%;}/*!sc*/\ndata-styled.g210[id=\"Dropdown__Container-ugfjuc-0\"]{content:\"gGGmJL,\"}/*!sc*/\n.gjsNwk{display:none;}/*!sc*/\ndata-styled.g211[id=\"Dropdown__ContentContainer-ugfjuc-1\"]{content:\"gjsNwk,\"}/*!sc*/\n.ikbJXY{display:block;height:100%;}/*!sc*/\ndata-styled.g212[id=\"Dropdown__Toggle-ugfjuc-2\"]{content:\"ikbJXY,\"}/*!sc*/\n.iRUflX{width:100%;}/*!sc*/\n.iRUflX:empty{height:calc((.75rem * 1.125 + .5rem + 1px) * 2);}/*!sc*/\ndata-styled.g315[id=\"StubhubLink__Container-sc-1n6qb9w-0\"]{content:\"iRUflX,\"}/*!sc*/\n.bvjTUg{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-column-gap:.5rem;column-gap:.5rem;}/*!sc*/\ndata-styled.g325[id=\"ContributorsCreditSong__Container-sc-12hq27v-0\"]{content:\"bvjTUg,\"}/*!sc*/\n.joWKqm{font-family:'Programme',Arial,sans-serif;font-size:.75rem;font-weight:100;text-align:left;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}/*!sc*/\ndata-styled.g326[id=\"ContributorsCreditSong__Label-sc-12hq27v-1\"]{content:\"joWKqm,\"}/*!sc*/\n.hymydu{-webkit-text-decoration:underline;text-decoration:underline;}/*!sc*/\n.hymydu:hover{-webkit-text-decoration:none;text-decoration:none;}/*!sc*/\ndata-styled.g327[id=\"ContributorsCreditSong__ContributorsReference-sc-12hq27v-2\"]{content:\"hymydu,\"}/*!sc*/\n.fScxZH{margin-right:.375rem;}/*!sc*/\ndata-styled.g328[id=\"ContributorsCreditSong__People-sc-12hq27v-3\"]{content:\"fScxZH,\"}/*!sc*/\n.dddWnX{font-size:.75rem;font-weight:100;line-height:1;-webkit-letter-spacing:1px;-moz-letter-spacing:1px;-ms-letter-spacing:1px;letter-spacing:1px;text-transform:none;color:#000;-webkit-align-content:center;-ms-flex-line-pack:center;align-content:center;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-letter-spacing:0.025rem;-moz-letter-spacing:0.025rem;-ms-letter-spacing:0.025rem;letter-spacing:0.025rem;line-height:1.33;}/*!sc*/\ndata-styled.g335[id=\"LyricsHeader__Title-ejidji-0\"]{content:\"dddWnX,\"}/*!sc*/\n.hbjTyd{-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:end;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end;-webkit-flex-direction:row-reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse;grid-column:left-start / left-end;margin-top:0;gap:.75rem 1.313rem;-webkit-flex-wrap:wrap-reverse;-ms-flex-wrap:wrap-reverse;flex-wrap:wrap-reverse;}/*!sc*/\ndata-styled.g336[id=\"LyricsHeader__Container-ejidji-1\"]{content:\"hbjTyd,\"}/*!sc*/\n.dzLEIC{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;gap:.375rem;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}/*!sc*/\ndata-styled.g337[id=\"LyricsHeader__TitleContainer-ejidji-2\"]{content:\"dzLEIC,\"}/*!sc*/\n.jeEHLx{-webkit-align-self:start;-ms-flex-item-align:start;align-self:start;height:1.313rem;}/*!sc*/\ndata-styled.g338[id=\"LyricsHeader__MetadataTooltip-ejidji-3\"]{content:\"jeEHLx,\"}/*!sc*/\n.fSmeGU{z-index:4;background-color:#fff;margin-top:1rem;font-size:.75rem;position:absolute;border:1px solid #000;min-width:100%;cursor:pointer;white-space:nowrap;left:0;}/*!sc*/\ndata-styled.g339[id=\"LyricsHeader__DropdownContents-ejidji-4\"]{content:\"fSmeGU,\"}/*!sc*/\n.SfdQZ{color:#000;}/*!sc*/\n.SfdQZ:hover{color:#fff;background-color:#000;}/*!sc*/\n.SfdQZ:hover:first-child:before{background-color:#000;color:#fff;}/*!sc*/\n.SfdQZ:first-child:before{content:'';position:absolute;width:8px;height:8px;background-color:#fff;border-top:1px solid #000;border-right:1px solid #000;top:0;left:calc(50% + 0px);-webkit-transform:translateX(-50%) translateY(calc(-50% - 1px)) rotate(-45deg);-ms-transform:translateX(-50%) translateY(calc(-50% - 1px)) rotate(-45deg);transform:translateX(-50%) translateY(calc(-50% - 1px)) rotate(-45deg);left:1rem;}/*!sc*/\ndata-styled.g340[id=\"LyricsHeader__DropdownItem-ejidji-5\"]{content:\"SfdQZ,\"}/*!sc*/\n.kUKuYL{color:#000;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;line-height:1rem;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;font-family:'Programme',Arial,sans-serif;font-weight:100;-webkit-text-decoration:underline;text-decoration:underline;-webkit-letter-spacing:0.025rem;-moz-letter-spacing:0.025rem;-ms-letter-spacing:0.025rem;letter-spacing:0.025rem;font-size:.75rem;gap:.375rem;}/*!sc*/\ndata-styled.g341[id=\"LyricsHeader__TranslationsContainer-ejidji-6\"]{content:\"kUKuYL,\"}/*!sc*/\n.kGzSQz{color:#000;font-family:'Programme',Arial,sans-serif;font-weight:100;-webkit-text-decoration:underline;text-decoration:underline;-webkit-letter-spacing:0.025rem;-moz-letter-spacing:0.025rem;-ms-letter-spacing:0.025rem;letter-spacing:0.025rem;font-size:.75rem;}/*!sc*/\ndata-styled.g342[id=\"LyricsHeader__TranslationsText-ejidji-7\"]{content:\"kGzSQz,\"}/*!sc*/\n.crJrtp{height:.75rem;}/*!sc*/\ndata-styled.g343[id=\"LyricsHeader__Languages-ejidji-8\"]{content:\"crJrtp,\"}/*!sc*/\n.dKqQxi svg{width:.5rem;}/*!sc*/\ndata-styled.g344[id=\"LyricsHeader__MenuIcon-ejidji-9\"]{content:\"dKqQxi,\"}/*!sc*/\n.iljvxT{width:100%;padding:.375rem .5rem;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;}/*!sc*/\ndata-styled.g345[id=\"LyricsHeader__TextButton-ejidji-10\"]{content:\"iljvxT,\"}/*!sc*/\n.fdKPz{max-width:25ch;word-wrap:break-word;word-break:break-word;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}/*!sc*/\ndata-styled.g346[id=\"LyricsHeader__TextEllipsis-ejidji-11\"]{content:\"fdKPz,\"}/*!sc*/\n.EZWbQ{word-wrap:break-word;word-break:break-word;font:100 1rem/1.5 'Programme',Arial,sans-serif;}/*!sc*/\n.EZWbQ h1{font-size:1.5rem;}/*!sc*/\n.EZWbQ h2{font-size:1.25rem;}/*!sc*/\n.EZWbQ h3{font-size:1.125rem;}/*!sc*/\n.EZWbQ h4,.EZWbQ h5,.EZWbQ h6{font-size:1rem;}/*!sc*/\n.EZWbQ h1,.EZWbQ h2,.EZWbQ h3,.EZWbQ h4,.EZWbQ h5,.EZWbQ h6{font-family:'Programme',Arial,sans-serif;font-weight:600;margin:1rem 0 0;}/*!sc*/\n.EZWbQ a{color:#fff;-webkit-text-decoration:underline;text-decoration:underline;}/*!sc*/\n.EZWbQ p{margin:1rem 0;}/*!sc*/\n.EZWbQ p:empty{display:none;}/*!sc*/\n.EZWbQ small{font-size:.75rem;}/*!sc*/\n.EZWbQ img{display:block;height:auto;margin-left:auto;margin-right:auto;max-height:325px;width:auto;}/*!sc*/\n.EZWbQ blockquote{margin:1rem 0 0 .5rem;padding-left:1rem;position:relative;display:block;}/*!sc*/\n.EZWbQ blockquote:before{content:'\\201C';position:absolute;top:-.1em;left:-0.3em;font:bold 2.25rem/1 \"Times New Roman\";color:inherit;opacity:.75;}/*!sc*/\n.EZWbQ pre{font-family:inherit;margin:1rem 0;}/*!sc*/\n.EZWbQ code{font-family:'Consolas','Monaco','Lucida Console','Liberation Mono','DejaVu Sans Mono','Bitstream Vera Sans Mono','Courier New',monospace;font-size:.75rem;}/*!sc*/\n.EZWbQ table{margin:1rem 0;width:100%;}/*!sc*/\n.EZWbQ th,.EZWbQ td{padding:.5rem .5rem 0;}/*!sc*/\n.EZWbQ th{border-bottom:1px solid #601216;font-weight:bold;text-align:left;}/*!sc*/\n.EZWbQ ul,.EZWbQ ol{list-style-type:none;margin:1rem 0;padding:0;}/*!sc*/\n.EZWbQ ul ul,.EZWbQ ol ul,.EZWbQ ul ol,.EZWbQ ol ol{margin:0;padding:0 0 0 1rem;}/*!sc*/\n.EZWbQ ul li,.EZWbQ ol li{padding-left:1.6em;position:relative;}/*!sc*/\n.EZWbQ ul li:before{content:'\\2022';left:calc(1.6em / 2.5);position:absolute;}/*!sc*/\n.EZWbQ ol{counter-reset:ordered_list_counter;}/*!sc*/\n.EZWbQ ol li:before{font-feature-settings:'tnum';content:counter(ordered_list_counter) '. ';counter-increment:ordered_list_counter;left:-.5rem;position:absolute;text-align:right;width:1.6em;}/*!sc*/\n.EZWbQ dd{margin:0 0 0 1rem;}/*!sc*/\n.EZWbQ hr{border:1px solid currentcolor;border-width:1px 0 0;}/*!sc*/\n.EZWbQ iframe{max-width:100%;}/*!sc*/\n.EZWbQ ins,.EZWbQ ins > *{-webkit-text-decoration:none;text-decoration:none;background:inherit !important;color:#24c609;}/*!sc*/\n.EZWbQ ins img,.EZWbQ ins > * img{border:2px solid #24c609;}/*!sc*/\n.EZWbQ del,.EZWbQ del > *{background:inherit !important;color:#ff1414;}/*!sc*/\n.EZWbQ del img,.EZWbQ del > * img{border:2px solid #ff1414;}/*!sc*/\n.EZWbQ .embedly_preview{font-size:1rem;margin-top:1rem;margin-bottom:1rem;}/*!sc*/\n.EZWbQ .embedly_preview a{font-weight:bold;}/*!sc*/\n.EZWbQ .embedly_preview iframe{display:block;margin:auto;width:100%;}/*!sc*/\n.EZWbQ .embedly_preview--video,.EZWbQ .embedly_preview--vertical-video{position:relative;width:100%;height:0;}/*!sc*/\n.EZWbQ .embedly_preview--video iframe,.EZWbQ .embedly_preview--vertical-video iframe{position:absolute;top:0;left:0;width:100%;height:100%;}/*!sc*/\n.EZWbQ .embedly_preview--vertical-video{padding-bottom:177.77777777777777%;}/*!sc*/\n.EZWbQ .embedly_preview--video{padding-bottom:56.25%;}/*!sc*/\n.EZWbQ .embedly_preview-thumb{position:relative;height:0;width:100%;padding-bottom:50%;overflow:hidden;border-bottom:1px solid #fff;}/*!sc*/\n.EZWbQ .embedly_preview-thumb img{position:absolute;top:0;left:0;min-width:100%;min-height:100%;}/*!sc*/\n.EZWbQ .embedly_preview .gray_container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;border:1px solid #fff;font-weight:400;color:inherit;word-wrap:break-word;word-break:break-word;}/*!sc*/\n.EZWbQ .embedly_preview .gray_container:hover{background-color:#fff;color:#e02324;}/*!sc*/\n.EZWbQ .embedly_preview > a{-webkit-text-decoration:none;text-decoration:none;}/*!sc*/\n.EZWbQ .embedly_preview .gray_container .embedly_preview-text{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:.75rem;font-weight:100;}/*!sc*/\n.EZWbQ .embedly_preview .gray_container .embedly_preview-text .embedly_preview-provider{-webkit-order:1;-ms-flex-order:1;order:1;font-size:.75rem;margin-bottom:.25rem;}/*!sc*/\n.EZWbQ .embedly_preview .gray_container .embedly_preview-text .embedly_preview-dash{display:none;}/*!sc*/\n.EZWbQ .embedly_preview .gray_container .embedly_preview-text .embedly_preview-title{-webkit-order:2;-ms-flex-order:2;order:2;margin-bottom:.25rem;line-height:1.33;}/*!sc*/\n.EZWbQ .embedly_preview .gray_container .embedly_preview-text .embedly_preview-description{-webkit-order:3;-ms-flex-order:3;order:3;font-size:.75rem;line-height:1.5;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;max-height:4.5em;overflow:hidden;}/*!sc*/\ndata-styled.g462[id=\"RichText__Container-oz284w-0\"]{content:\"EZWbQ,\"}/*!sc*/\n.dhIhSa{font-size:.75rem;color:#fff;}/*!sc*/\ndata-styled.g473[id=\"Attribution__Container-sc-1nmry9o-0\"]{content:\"dhIhSa,\"}/*!sc*/\n.iPNsXE{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;margin-bottom:1rem;}/*!sc*/\ndata-styled.g474[id=\"Attribution__Header-sc-1nmry9o-1\"]{content:\"iPNsXE,\"}/*!sc*/\n.eQBXrC{font-size:.75rem;font-weight:100;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;cursor:default;gap:.75rem;height:1rem;}/*!sc*/\n.eQBXrC svg{fill:#fff;display:block;height:1em;cursor:pointer;}/*!sc*/\ndata-styled.g513[id=\"VotingActions__Container-sc-1w1brao-0\"]{content:\"eQBXrC,\"}/*!sc*/\n.hPgYeJ{all:unset;cursor:pointer;font-family:inherit;-webkit-text-decoration:underline;text-decoration:underline;min-width:1rem;text-align:center;}/*!sc*/\n.hPgYeJ:hover{-webkit-text-decoration:none;text-decoration:none;}/*!sc*/\ndata-styled.g515[id=\"VotingActions__VotingModalButton-sc-1w1brao-2\"]{content:\"hPgYeJ,\"}/*!sc*/\n.hEQpIA{opacity:1;}/*!sc*/\ndata-styled.g516[id=\"VotingActions__ThumbsUp-sc-1w1brao-3\"]{content:\"hEQpIA,\"}/*!sc*/\n.hKZXIk{opacity:1;}/*!sc*/\ndata-styled.g517[id=\"VotingActions__ThumbsDown-sc-1w1brao-4\"]{content:\"hKZXIk,\"}/*!sc*/\n.gHSnUN{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;font-size:inherit;line-height:1;white-space:pre;font-weight:100;width:100%;}/*!sc*/\n@media screen and (max-width:1164px){.gHSnUN{line-height:1.33;}}/*!sc*/\n.gHSnUN:hover{background-color:#000;color:#fff;}/*!sc*/\n.gHSnUN:hover span{color:#fff;}/*!sc*/\n.gHSnUN:hover svg{fill:#fff;}/*!sc*/\n.gHSnUN svg{display:block;height:1rem;}/*!sc*/\n.gHSnUN:disabled{opacity:.5;}/*!sc*/\ndata-styled.g526[id=\"ShareButton-a0stpn-0\"]{content:\"gHSnUN,\"}/*!sc*/\n.dyewdM{margin-bottom:1.5rem;width:100%;font-weight:100;font-size:.75rem;}/*!sc*/\n@media screen and (min-width:1164px){.dyewdM{font-size:1rem;}}/*!sc*/\ndata-styled.g533[id=\"ShareButtons__Root-jws18q-0\"]{content:\"dyewdM,\"}/*!sc*/\n.ePvBqA{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;gap:.75rem;}/*!sc*/\ndata-styled.g534[id=\"ShareButtons__Container-jws18q-1\"]{content:\"ePvBqA,\"}/*!sc*/\n.kdajqn{-webkit-flex:1;-ms-flex:1;flex:1;text-align:center;font-size:inherit;}/*!sc*/\n.kdajqn .react-share__ShareButton{width:100%;}/*!sc*/\ndata-styled.g535[id=\"ShareButtons__ButtonWrapper-jws18q-2\"]{content:\"kdajqn,\"}/*!sc*/\n.tqfBP{font-size:.75rem;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}/*!sc*/\n.tqfBP svg{display:block;height:1em;}/*!sc*/\n.tqfBP > *{margin-right:1rem;}/*!sc*/\n.tqfBP > *:last-child{margin-right:0;}/*!sc*/\n.tqfBP:last-child{margin-bottom:0;}/*!sc*/\ndata-styled.g557[id=\"AnnotationActions__Container-sc-1597gb-0\"]{content:\"tqfBP,\"}/*!sc*/\n.kljthJ{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;}/*!sc*/\ndata-styled.g558[id=\"AnnotationActions__FlexSpace-sc-1597gb-1\"]{content:\"kljthJ,\"}/*!sc*/\n.jQIgzS{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}/*!sc*/\ndata-styled.g559[id=\"AnnotationActions__Pyong-sc-1597gb-2\"]{content:\"jQIgzS,\"}/*!sc*/\n.cesxpW{position:relative;padding:calc(((1.5 * 1em) - 1.125rem) / 2) 0;-webkit-scroll-margin:calc(max(10vw,0) + 3rem + 0px + (1.5 * 1em));-moz-scroll-margin:calc(max(10vw,0) + 3rem + 0px + (1.5 * 1em));-ms-scroll-margin:calc(max(10vw,0) + 3rem + 0px + (1.5 * 1em));scroll-margin:calc(max(10vw,0) + 3rem + 0px + (1.5 * 1em));}/*!sc*/\ndata-styled.g578[id=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0\"]{content:\"cesxpW,\"}/*!sc*/\n.jAzSMw{color:#000;background-color:#e9e9e9;padding-top:calc(((((1.5 * 1em) - 1.125rem) / 2) - (.125rem / 2)) - 0.75px);padding-bottom:calc(((((1.5 * 1em) - 1.125rem) / 2) - (.125rem / 2)) - 0.25px);}/*!sc*/\ndata-styled.g579[id=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1\"]{content:\"jAzSMw,\"}/*!sc*/\n.fuSJbt{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;row-gap:1rem;grid-column:auto / right-end;justify-self:right;min-width:300px;}/*!sc*/\ndata-styled.g591[id=\"RightSidebar__Container-pajcl2-0\"]{content:\"fuSJbt,\"}/*!sc*/\n.kvuklz{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;width:300px;padding-top:0.75rem;}/*!sc*/\n.cFlBmm{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;width:300px;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:0.75rem 0;}/*!sc*/\ndata-styled.g592[id=\"SidebarAd__Container-sc-1cw85h6-0\"]{content:\"kvuklz,cFlBmm,\"}/*!sc*/\n.bJESjt{position:-webkit-sticky;position:sticky;top:calc(0 + 3rem + 0px + 0.75rem);}/*!sc*/\ndata-styled.g593[id=\"SidebarAd__StickyContainer-sc-1cw85h6-1\"]{content:\"bJESjt,bZuAng,\"}/*!sc*/\n.fUyrrM{border:1px solid #000;font-size:.75rem;width:100%;}/*!sc*/\ndata-styled.g614[id=\"RecommendedSongs__Container-fhtuij-0\"]{content:\"fUyrrM,\"}/*!sc*/\n.fVWWod{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;}/*!sc*/\ndata-styled.g615[id=\"RecommendedSongs__Body-fhtuij-1\"]{content:\"fVWWod,\"}/*!sc*/\n.lfAvEQ{background:#000;color:#fff;font-size:1rem;padding:.25rem 1rem;}/*!sc*/\ndata-styled.g616[id=\"RecommendedSongs__Header-fhtuij-2\"]{content:\"lfAvEQ,\"}/*!sc*/\n.jUHZcK{-webkit-undefined;-ms-flex-undefined;undefined;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;padding-bottom:1.5rem;}/*!sc*/\ndata-styled.g617[id=\"LyricsSidebarAd__RecommendationsContainer-sc-1duvwla-0\"]{content:\"jUHZcK,\"}/*!sc*/\n.hAVRTF{position:-webkit-sticky;position:sticky;top:calc(0 + 3rem + 0px + 0.75rem);}/*!sc*/\ndata-styled.g618[id=\"LyricsSidebarAd__Recommendations-sc-1duvwla-1\"]{content:\"hAVRTF,\"}/*!sc*/\n.gBtUIQ{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;grid-column:left-start / left-end;}/*!sc*/\ndata-styled.g624[id=\"LyricsFooter__Container-iqbcge-0\"]{content:\"gBtUIQ,\"}/*!sc*/\n.bfWZQI.bfWZQI{margin:0.75rem 0 calc(0.75rem + 1.313rem);}/*!sc*/\ndata-styled.g625[id=\"LyricsFooter__FooterShareButtons-iqbcge-1\"]{content:\"bfWZQI,\"}/*!sc*/\n.iEyyHq{display:grid;grid-template-columns: [page-start] 2.25rem [grid-start header-left-start] 1fr [left-start] repeat(2,1fr) [header-left-end header-right-start] repeat(4,1fr) [left-end right-start] repeat(4,1fr) [right-end] 1fr [grid-end header-right-end] 2.25rem [page-end];grid-gap:0.75rem;font:100 1.125rem/1.5 'Programme',Arial,sans-serif;padding:0;padding-top:1.5rem;grid-auto-rows:max-content;min-height:min(var(--annotation-height),100vh);position:relative;width:100%;word-wrap:break-word;word-break:break-word;}/*!sc*/\n@media screen and (min-width:1164px){.iEyyHq{grid-template-columns: [page-start] 1fr [grid-start header-left-start] 5rem [left-start] repeat(2,5rem) [header-left-end header-right-start] repeat(4,5rem) [left-end right-start] repeat(4,5rem) [right-end] 5rem [grid-end header-right-end] 1fr [page-end];}}/*!sc*/\n@media screen and (min-width:1526px){.iEyyHq{grid-template-columns: [page-start] 1fr [grid-start header-left-start] 6rem [left-start] repeat(2,6rem) [header-left-end header-right-start] repeat(4,6rem) [left-end right-start] repeat(4,6rem) [right-end] 6rem [grid-end header-right-end] 1fr [page-end];}}/*!sc*/\ndata-styled.g628[id=\"Lyrics__Root-sc-1ynbvzw-0\"]{content:\"iEyyHq,\"}/*!sc*/\n.kUgSbL{grid-column:left-start / left-end;padding-top:.5rem;padding-bottom:1.5rem;padding-right:1.5rem;}/*!sc*/\ndata-styled.g629[id=\"Lyrics__Container-sc-1ynbvzw-1\"]{content:\"kUgSbL,\"}/*!sc*/\n.hXQMRu{font-size:.75rem;}/*!sc*/\n.hXQMRu ul{padding-left:1rem;margin-bottom:1rem;}/*!sc*/\n.hXQMRu li{list-style-type:disc;}/*!sc*/\ndata-styled.g630[id=\"LyricsEditExplainer__Container-sc-1aeph76-0\"]{content:\"hXQMRu,\"}/*!sc*/\n.eDBQeK{font-weight:700;}/*!sc*/\ndata-styled.g631[id=\"LyricsEditExplainer__Bold-sc-1aeph76-1\"]{content:\"eDBQeK,\"}/*!sc*/\n.rncXA{font-style:italic;}/*!sc*/\ndata-styled.g632[id=\"LyricsEditExplainer__Italic-sc-1aeph76-2\"]{content:\"rncXA,\"}/*!sc*/\n.lmbLpy{display:none;grid-column:page-start / page-end;font-weight:100;margin-bottom:3rem;margin-top:1.313rem;}/*!sc*/\ndata-styled.g634[id=\"LyricsEditdesktop__Container-sc-19lxrhp-0\"]{content:\"lmbLpy,\"}/*!sc*/\n.cOOPzk{grid-column:left-start / left-end;position:relative;}/*!sc*/\ndata-styled.g635[id=\"LyricsEditdesktop__Editor-sc-19lxrhp-1\"]{content:\"cOOPzk,\"}/*!sc*/\n.hGRoZE{grid-column:right-start / right-end;margin-left:1.313rem;}/*!sc*/\ndata-styled.g636[id=\"LyricsEditdesktop__ControlsContainer-sc-19lxrhp-2\"]{content:\"hGRoZE,\"}/*!sc*/\n.cEVdKO{position:-webkit-sticky;position:sticky;top:calc(0 + 3rem + 0px + 0.75rem);}/*!sc*/\ndata-styled.g637[id=\"LyricsEditdesktop__Controls-sc-19lxrhp-3\"]{content:\"cEVdKO,\"}/*!sc*/\n.kpOoZB{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;padding:.5rem 1rem;margin:0;margin-bottom:.75rem;}/*!sc*/\ndata-styled.g638[id=\"LyricsEditdesktop__Button-sc-19lxrhp-4\"]{content:\"kpOoZB,\"}/*!sc*/\n.LNnRX{margin-top:1.313rem;}/*!sc*/\ndata-styled.g640[id=\"LyricsEditdesktop__Explainer-sc-19lxrhp-6\"]{content:\"LNnRX,\"}/*!sc*/\n.cSKAwQ{display:grid;grid-template-columns:1fr [center-start] 1fr [center-end] 1fr;background-color:black;}/*!sc*/\ndata-styled.g647[id=\"SectionLeaderboard__Container-sc-1pjk0bw-0\"]{content:\"cSKAwQ,\"}/*!sc*/\n.fPpEQG{padding:.75rem 0;grid-column:center-start / center-end;}/*!sc*/\ndata-styled.g648[id=\"SectionLeaderboard__Center-sc-1pjk0bw-1\"]{content:\"fPpEQG,\"}/*!sc*/\n.hYPlhL{color:#fff;-webkit-scroll-margin-top:calc(0 + 3rem);-moz-scroll-margin-top:calc(0 + 3rem);-ms-scroll-margin-top:calc(0 + 3rem);scroll-margin-top:calc(0 + 3rem);}/*!sc*/\ndata-styled.g653[id=\"SongDescription__Container-sc-615rvk-0\"]{content:\"hYPlhL,\"}/*!sc*/\n.kRzyD{text-align:left;}/*!sc*/\ndata-styled.g655[id=\"SongDescription__Content-sc-615rvk-2\"]{content:\"kRzyD,\"}/*!sc*/\n.nTuKb{margin-top:.75rem;}/*!sc*/\ndata-styled.g659[id=\"SongDescription__AnnotationEditActions-sc-615rvk-6\"]{content:\"nTuKb,\"}/*!sc*/\n.bEHmko{margin:1rem 0 1.313rem;}/*!sc*/\ndata-styled.g660[id=\"SongDescription__AnnotationActions-sc-615rvk-7\"]{content:\"bEHmko,\"}/*!sc*/\n.iyrTpw{border-bottom:1px solid #fff;margin-bottom:3rem;padding-bottom:3rem;}/*!sc*/\n.vrxkS{border-bottom:1px solid #000;margin-bottom:3rem;padding-bottom:3rem;}/*!sc*/\ndata-styled.g661[id=\"InnerSectionDivider-sc-1x4onqw-0\"]{content:\"iyrTpw,vrxkS,\"}/*!sc*/\n.dxxZLD{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;gap:1rem;}/*!sc*/\n@media screen and (max-width:1164px){.dxxZLD{-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}}/*!sc*/\ndata-styled.g683[id=\"QuestionFormControls__Buttons-sc-79jhlr-1\"]{content:\"dxxZLD,\"}/*!sc*/\n.cgTISK{text-align:left;border:1px solid #fff;-webkit-scroll-margin-top:calc(0 + 3rem);-moz-scroll-margin-top:calc(0 + 3rem);-ms-scroll-margin-top:calc(0 + 3rem);scroll-margin-top:calc(0 + 3rem);}/*!sc*/\n.cgTISK:not(:last-child){border-bottom:none;}/*!sc*/\ndata-styled.g689[id=\"QuestionTemplate__Container-sc-1is5n1p-1\"]{content:\"cgTISK,\"}/*!sc*/\n.fRjQxS{font-size:1rem;color:#e02324;background-color:transparent;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;cursor:pointer;-webkit-text-decoration:none;text-decoration:none;line-height:1.33;padding:.75rem;background-color:#fff;}/*!sc*/\ndata-styled.g692[id=\"QuestionTemplate__Title-sc-1is5n1p-4\"]{content:\"fRjQxS,\"}/*!sc*/\n.ibRgSN{padding-left:.25rem;margin-left:auto;font-size:1.5rem;}/*!sc*/\n.ibRgSN svg{display:block;width:.75rem;height:.75rem;fill:#e02324;}/*!sc*/\ndata-styled.g693[id=\"QuestionTemplate__ExpandButton-sc-1is5n1p-5\"]{content:\"ibRgSN,\"}/*!sc*/\n.iHvEbL{padding:1.5rem;}/*!sc*/\ndata-styled.g714[id=\"Answer__Container-sc-2neq6u-0\"]{content:\"iHvEbL,\"}/*!sc*/\n.fzIWba{margin:1rem 0;}/*!sc*/\ndata-styled.g715[id=\"Answer__Body-sc-2neq6u-1\"]{content:\"fzIWba,\"}/*!sc*/\n.doQBGH{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;gap:1rem;}/*!sc*/\ndata-styled.g717[id=\"Answer__AttributionLabel-sc-2neq6u-3\"]{content:\"doQBGH,\"}/*!sc*/\n.dTXIVU{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;gap:1.313rem;padding:1.5rem;padding-top:0;}/*!sc*/\n.dTXIVU:empty{display:none;}/*!sc*/\ndata-styled.g720[id=\"QuestionAnswerActions__Container-sc-6ne2ji-0\"]{content:\"dTXIVU,\"}/*!sc*/\n.eDiRSs{text-align:left;border:1px solid #fff;}/*!sc*/\n.eDiRSs:not(:last-child){border-bottom:none;}/*!sc*/\ndata-styled.g736[id=\"MetadataQuestionTemplate__Container-sc-3cdvbz-0\"]{content:\"eDiRSs,\"}/*!sc*/\n.emVCuo{font-size:1rem;color:#fff;background-color:transparent;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;cursor:pointer;line-height:1.33;padding:.75rem;-webkit-text-decoration:none;text-decoration:none;}/*!sc*/\ndata-styled.g737[id=\"MetadataQuestionTemplate__Title-sc-3cdvbz-1\"]{content:\"emVCuo,\"}/*!sc*/\n.jTUclM{padding-left:.25rem;margin-left:auto;font-size:1.5rem;}/*!sc*/\n.jTUclM svg{display:block;width:.75rem;height:.75rem;fill:#fff;}/*!sc*/\ndata-styled.g738[id=\"MetadataQuestionTemplate__ExpandButton-sc-3cdvbz-2\"]{content:\"jTUclM,\"}/*!sc*/\n.dbVnsR{position:relative;font-weight:100;color:#fff;}/*!sc*/\ndata-styled.g740[id=\"QuestionList__Container-sc-1a58vti-0\"]{content:\"dbVnsR,\"}/*!sc*/\n.cTVsHz{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:1.5rem;}/*!sc*/\ndata-styled.g741[id=\"QuestionList__Header-sc-1a58vti-1\"]{content:\"cTVsHz,\"}/*!sc*/\n.dssCKz{font-size:5rem;font-weight:400;}/*!sc*/\ndata-styled.g742[id=\"QuestionList__Title-sc-1a58vti-2\"]{content:\"dssCKz,\"}/*!sc*/\n.dZHUCL{max-width:22rem;text-align:center;}/*!sc*/\ndata-styled.g743[id=\"QuestionList__SubTitle-sc-1a58vti-3\"]{content:\"dZHUCL,\"}/*!sc*/\n.geLHRn{margin-top:2.25rem;}/*!sc*/\ndata-styled.g745[id=\"QuestionList__Table-sc-1a58vti-5\"]{content:\"geLHRn,\"}/*!sc*/\n.kGJQLs{grid-column:grid-start / grid-end;text-align:start;-webkit-column-count:2;column-count:2;font-size:.75rem;}/*!sc*/\ndata-styled.g747[id=\"AlbumTracklist__Container-sc-123giuo-0\"]{content:\"kGJQLs,\"}/*!sc*/\n.iLxPGk{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-bottom:.75rem;-webkit-break-inside:avoid;break-inside:avoid;color:#fff;padding:0 0.75rem;}/*!sc*/\ndata-styled.g748[id=\"AlbumTracklist__Track-sc-123giuo-1\"]{content:\"iLxPGk,\"}/*!sc*/\n.bPLsDz{display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;position:relative;padding:.25rem 0;}/*!sc*/\n.guEaas{display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;position:relative;padding:.25rem 0;}/*!sc*/\n.guEaas:after{content:'';position:absolute;top:0;right:-0.75rem;bottom:0;left:-0.75rem;border:1px solid;}/*!sc*/\ndata-styled.g749[id=\"AlbumTracklist__TrackName-sc-123giuo-2\"]{content:\"bPLsDz,guEaas,\"}/*!sc*/\n.epTVob{padding-right:.5rem;}/*!sc*/\ndata-styled.g750[id=\"AlbumTracklist__TrackNumber-sc-123giuo-3\"]{content:\"epTVob,\"}/*!sc*/\n.ekePUw{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;grid-column:1 / -1;padding-top:2.25rem;}/*!sc*/\n.ekePUw span{vertical-align:1px;}/*!sc*/\ndata-styled.g758[id=\"OtherAlbums__ButtonContainer-sc-2n6wdo-1\"]{content:\"ekePUw,\"}/*!sc*/\n.kqeVkf{position:relative;display:grid;grid-template-columns:[grid-start] repeat(2,1fr) [grid-end];text-align:start;}/*!sc*/\ndata-styled.g759[id=\"PrimaryAlbum__Container-cuci8p-0\"]{content:\"kqeVkf,\"}/*!sc*/\n.lkEvgY{position:absolute;top:calc(-1.5 * 2.25rem);}/*!sc*/\ndata-styled.g760[id=\"PrimaryAlbum__SectionAnchor-cuci8p-1\"]{content:\"lkEvgY,\"}/*!sc*/\n.fMIxkO{margin:0 3rem 2.5rem 3rem;}/*!sc*/\ndata-styled.g761[id=\"PrimaryAlbum__CoverArt-cuci8p-2\"]{content:\"fMIxkO,\"}/*!sc*/\n.hGGOcK{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;margin-bottom:2.5rem;}/*!sc*/\ndata-styled.g762[id=\"PrimaryAlbum__AlbumDetails-cuci8p-3\"]{content:\"hGGOcK,\"}/*!sc*/\n.NcWGs{font-size:1rem;color:#fff;}/*!sc*/\ndata-styled.g763[id=\"PrimaryAlbum__Title-cuci8p-4\"]{content:\"NcWGs,\"}/*!sc*/\n.hyzSGh{font-size:.75rem;color:#fff;}/*!sc*/\ndata-styled.g764[id=\"PrimaryAlbum__Artist-cuci8p-5\"]{content:\"hyzSGh,\"}/*!sc*/\n.ceKRFE{font-size:.625rem;margin:.75rem 0;text-align:left;}/*!sc*/\ndata-styled.g765[id=\"SongTags__Title-xixwg3-0\"]{content:\"ceKRFE,\"}/*!sc*/\n.bZsZHM{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;grid-gap:.75rem;}/*!sc*/\ndata-styled.g766[id=\"SongTags__Container-xixwg3-1\"]{content:\"bZsZHM,\"}/*!sc*/\n.evrydK{color:#fff;border:1px solid #fff;padding:.5rem;font-size:.75rem;font-weight:700;}/*!sc*/\n.evrydK:visited{color:#fff;}/*!sc*/\n.evrydK:hover{background-color:#fff;color:#000;mix-blend-mode:screen;}/*!sc*/\n.evrydK:hover span{color:#000;}/*!sc*/\n.evrydK:hover svg{fill:#000;}/*!sc*/\n.kykqAa{color:#fff;border:1px solid #fff;padding:.5rem;font-size:.75rem;}/*!sc*/\n.kykqAa:visited{color:#fff;}/*!sc*/\n.kykqAa:hover{background-color:#fff;color:#000;mix-blend-mode:screen;}/*!sc*/\n.kykqAa:hover span{color:#000;}/*!sc*/\n.kykqAa:hover svg{fill:#000;}/*!sc*/\ndata-styled.g767[id=\"SongTags__Tag-xixwg3-2\"]{content:\"evrydK,kykqAa,\"}/*!sc*/\n.kJtCmu{position:relative;font-weight:100;padding:0 1.313rem 0 1.313rem;color:#fff;}/*!sc*/\ndata-styled.g768[id=\"SongInfo__Container-nekw6x-0\"]{content:\"kJtCmu,\"}/*!sc*/\n.iRKrFW{font-size:2.25rem;font-weight:400;margin-bottom:2.25rem;line-height:1.1;}/*!sc*/\ndata-styled.g769[id=\"SongInfo__Title-nekw6x-1\"]{content:\"iRKrFW,\"}/*!sc*/\n.dWcYSx{display:grid;text-align:left;grid-template-columns:1fr 1fr;grid-gap:0.75rem;}/*!sc*/\ndata-styled.g770[id=\"SongInfo__Columns-nekw6x-2\"]{content:\"dWcYSx,\"}/*!sc*/\n.fognin{font-size:.75rem;}/*!sc*/\n.fognin:not(:last-child){margin-bottom:1.5rem;}/*!sc*/\ndata-styled.g771[id=\"SongInfo__Credit-nekw6x-3\"]{content:\"fognin,\"}/*!sc*/\n.kOJa-dB{display:block;font-size:.625rem;color:#fff;}/*!sc*/\ndata-styled.g772[id=\"SongInfo__Label-nekw6x-4\"]{content:\"kOJa-dB,\"}/*!sc*/\n.lopKUj{position:absolute;top:calc(-1.5 * (0 + 3rem));}/*!sc*/\ndata-styled.g773[id=\"SongInfo__SectionAnchor-nekw6x-5\"]{content:\"lopKUj,\"}/*!sc*/\n.tYzLI{display:block;text-align:left;font-size:.75rem;margin:.75rem 0 1.313rem;}/*!sc*/\ndata-styled.g774[id=\"SongInfo__RelationshipsPageLink-nekw6x-6\"]{content:\"tYzLI,\"}/*!sc*/\n.cYtdTH{margin:1.313rem 0 1rem 0;}/*!sc*/\ndata-styled.g777[id=\"MusicVideo__Container-sc-1980jex-0\"]{content:\"cYtdTH,\"}/*!sc*/\n.icvVds{position:relative;}/*!sc*/\ndata-styled.g778[id=\"SectionScrollSentinel__Container-eoe1bv-0\"]{content:\"icvVds,\"}/*!sc*/\n.gTVDnZ{position:absolute;top:calc(100vh - (0 + 3rem));bottom:calc(0 + 3rem);width:100%;z-index:-2;}/*!sc*/\ndata-styled.g779[id=\"SectionScrollSentinel__Element-eoe1bv-1\"]{content:\"gTVDnZ,\"}/*!sc*/\n.difnmr{position:absolute;top:calc(1px - (0 + 3rem));}/*!sc*/\ndata-styled.g780[id=\"SectionScrollSentinel__SectionAnchor-eoe1bv-2\"]{content:\"difnmr,\"}/*!sc*/\n.jdSpXa{font-weight:100;background-image:linear-gradient(#e02324,#601216);padding-bottom:2.25rem;}/*!sc*/\ndata-styled.g781[id=\"About__Grid-ut4i9m-0\"]{content:\"jdSpXa,\"}/*!sc*/\n.eSiFpi{grid-column:left-start / left-end;padding-top:calc(2.25rem + .375rem);}/*!sc*/\ndata-styled.g782[id=\"About__Container-ut4i9m-1\"]{content:\"eSiFpi,\"}/*!sc*/\n.kcXwIY{font-size:5rem;text-align:center;font-weight:normal;color:#fff;}/*!sc*/\ndata-styled.g783[id=\"About__Title-ut4i9m-2\"]{content:\"kcXwIY,\"}/*!sc*/\n.leoUZe{font-size:1rem;border:1px solid #000;color:#000;padding:.75rem;width:100%;text-align:left;cursor:text;}/*!sc*/\ndata-styled.g790[id=\"CommentForm__FakeTextField-sc-1rfrnj0-5\"]{content:\"leoUZe,\"}/*!sc*/\n.bIlJhm{padding-bottom:1.313rem;}/*!sc*/\ndata-styled.g791[id=\"SongComments__Grid-sc-131p4fy-0\"]{content:\"bIlJhm,\"}/*!sc*/\n.lgbAKX{grid-column:left-start / left-end;padding-top:calc(2.25rem + .375rem);}/*!sc*/\ndata-styled.g792[id=\"SongComments__Container-sc-131p4fy-1\"]{content:\"lgbAKX,\"}/*!sc*/\n.kojbqH{font-size:5rem;margin-bottom:1rem;text-align:center;}/*!sc*/\ndata-styled.g793[id=\"SongComments__Title-sc-131p4fy-2\"]{content:\"kojbqH,\"}/*!sc*/\n.jZrfsi{font-size:2.25rem;font-weight:400;line-height:1.1;}/*!sc*/\ndata-styled.g794[id=\"SongComments__CTASubHeading-sc-131p4fy-3\"]{content:\"jZrfsi,\"}/*!sc*/\n.euQZer{margin-top:2.25rem;text-align:center;font-weight:100;color:#000;}/*!sc*/\ndata-styled.g797[id=\"SongComments__CTA-sc-131p4fy-6\"]{content:\"euQZer,\"}/*!sc*/\n.gRiFtA{position:relative;height:480px;}/*!sc*/\ndata-styled.g800[id=\"SongComments__SpinnerContainer-sc-131p4fy-9\"]{content:\"gRiFtA,\"}/*!sc*/\n.fOsBvT{position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%);-ms-transform:translate(-50%);transform:translate(-50%);}/*!sc*/\ndata-styled.g801[id=\"SongComments__InitialLoadSpinner-sc-131p4fy-10\"]{content:\"fOsBvT,\"}/*!sc*/\n.gTBWpu{text-align:center;padding:0 1rem;}/*!sc*/\ndata-styled.g802[id=\"SongComments__Footer-sc-131p4fy-11\"]{content:\"gTBWpu,\"}/*!sc*/\n.fHiIPi{margin:1rem 0;}/*!sc*/\ndata-styled.g803[id=\"SongComments__SignUpButton-sc-131p4fy-12\"]{content:\"fHiIPi,\"}/*!sc*/\n.bZruNU{z-index:4;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;pointer-events:none;position:fixed;left:0;right:0;bottom:0;}/*!sc*/\ndata-styled.g813[id=\"MediaPlayersContainer__Container-sc-1tibexe-0\"]{content:\"bZruNU,\"}/*!sc*/\n.cziiuX svg{display:block;height:22px;fill:#fff;}/*!sc*/\n.fRTMWj svg{display:block;height:19px;fill:#fff;}/*!sc*/\ndata-styled.g821[id=\"SocialLinks__Link-jwyj6b-1\"]{content:\"cziiuX,fRTMWj,\"}/*!sc*/\n.iuNSEV{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}/*!sc*/\n.iuNSEV .SocialLinks__Link-jwyj6b-1 + .SocialLinks__Link-jwyj6b-1{margin-left:1.5rem;}/*!sc*/\ndata-styled.g822[id=\"SocialLinks__Container-jwyj6b-2\"]{content:\"iuNSEV,\"}/*!sc*/\n.uGviF{font-size:2.25rem;font-weight:100;line-height:1.125;margin-bottom:2.25rem;}/*!sc*/\ndata-styled.g823[id=\"PageFooterSocial__Slogan-sc-14u22mq-0\"]{content:\"uGviF,\"}/*!sc*/\n.kTXFZQ{display:block;color:inherit;margin:0 .25rem;}/*!sc*/\ndata-styled.g824[id=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0\"]{content:\"kTXFZQ,\"}/*!sc*/\n.hAxKUd{display:block;color:inherit;}/*!sc*/\n.hAxKUd + .PageFooterHotSongLinks__Link-sc-1adazwo-0:before{content:'•';margin:0 .75rem;}/*!sc*/\ndata-styled.g825[id=\"PageFooterHotSongLinks__Link-sc-1adazwo-0\"]{content:\"hAxKUd,\"}/*!sc*/\n.boDKcJ{background-color:#121212;}/*!sc*/\ndata-styled.g829[id=\"PageFooterdesktop__Container-hz1fx1-0\"]{content:\"boDKcJ,\"}/*!sc*/\n.gwrcCS{display:block;color:inherit;font-weight:100;cursor:pointer;}/*!sc*/\ndata-styled.g830[id=\"PageFooterdesktop__Link-hz1fx1-1\"]{content:\"gwrcCS,\"}/*!sc*/\n.cjEbTp{display:grid;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;grid-template-columns:[grid-start] repeat(3,minmax(14px,44px)) [center-start] repeat(6,minmax(14px,44px)) [center-end] repeat(3,minmax(14px,44px)) [grid-end];grid-column-gap:60px;grid-row-gap:40px;color:#fff;padding:3rem 60px 2.25rem;}/*!sc*/\n.cjEbTp .PageFooterdesktop__Link-hz1fx1-1 + .PageFooterdesktop__Link-hz1fx1-1{margin-top:1rem;}/*!sc*/\ndata-styled.g831[id=\"PageFooterdesktop__Section-hz1fx1-2\"]{content:\"cjEbTp,\"}/*!sc*/\n.mJdfj{grid-column:span 6;}/*!sc*/\ndata-styled.g832[id=\"PageFooterdesktop__Half-hz1fx1-3\"]{content:\"mJdfj,\"}/*!sc*/\n.hMUvCn{grid-column:span 3;}/*!sc*/\ndata-styled.g833[id=\"PageFooterdesktop__Quarter-hz1fx1-4\"]{content:\"hMUvCn,\"}/*!sc*/\n.diwZPD{grid-column:span 3;grid-column:7 / span 3;}/*!sc*/\ndata-styled.g834[id=\"PageFooterdesktop__OffsetQuarter-hz1fx1-5\"]{content:\"diwZPD,\"}/*!sc*/\n.iDkyVM{display:block;color:#9a9a9a;font-weight:100;font-size:.625rem;}/*!sc*/\ndata-styled.g835[id=\"PageFooterdesktop__FinePrint-hz1fx1-6\"]{content:\"iDkyVM,\"}/*!sc*/\n.yRyiP{display:grid;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;grid-template-columns:[grid-start] repeat(3,minmax(14px,44px)) [center-start] repeat(6,minmax(14px,44px)) [center-end] repeat(3,minmax(14px,44px)) [grid-end];grid-column-gap:60px;grid-row-gap:40px;color:#fff;padding:3rem 60px 2.25rem;border-top:.15rem solid #2a2a2a;padding-top:2.25rem;padding-bottom:2.25rem;}/*!sc*/\n.yRyiP .PageFooterdesktop__Link-hz1fx1-1 + .PageFooterdesktop__Link-hz1fx1-1{margin-top:1rem;}/*!sc*/\ndata-styled.g836[id=\"PageFooterdesktop__Bottom-hz1fx1-7\"]{content:\"yRyiP,\"}/*!sc*/\n.eIiYRJ{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;grid-column:1 / -1;-webkit-align-items:baseline;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline;}/*!sc*/\ndata-styled.g837[id=\"PageFooterdesktop__Row-hz1fx1-8\"]{content:\"eIiYRJ,\"}/*!sc*/\n.hNrwqx{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-flex:1;-ms-flex:1;flex:1;}/*!sc*/\ndata-styled.g838[id=\"PageFooterdesktop__FlexWrap-hz1fx1-9\"]{content:\"hNrwqx,\"}/*!sc*/\n.bMBKQI{color:inherit;}/*!sc*/\n.bMBKQI:after{content:'•';margin:0 1rem;}/*!sc*/\ndata-styled.g839[id=\"PageFooterdesktop__VerifiedArtists-hz1fx1-10\"]{content:\"bMBKQI,\"}/*!sc*/\n.dcpJwP{margin-right:1rem;}/*!sc*/\ndata-styled.g840[id=\"PageFooterdesktop__Label-hz1fx1-11\"]{content:\"dcpJwP,\"}/*!sc*/\n.gloXyw{height:1.5rem;}/*!sc*/\ndata-styled.g842[id=\"SongPage__HeaderSpace-sc-19xhmoi-1\"]{content:\"gloXyw,\"}/*!sc*/\n.mkDeY{position:relative;}/*!sc*/\ndata-styled.g843[id=\"SongPage__LyricsWrapper-sc-19xhmoi-2\"]{content:\"mkDeY,\"}/*!sc*/\n.llTPXF{padding-bottom:3rem;}/*!sc*/\ndata-styled.g844[id=\"SongPage__PageFooter-sc-19xhmoi-3\"]{content:\"llTPXF,\"}/*!sc*/\nhtml,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,input,button,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;}/*!sc*/\nh1,h2,h3,h4,h5,h6{font-size:100%;font-weight:inherit;}/*!sc*/\nhtml{line-height:1;}/*!sc*/\nol,ul{list-style:none;}/*!sc*/\ntable{border-collapse:collapse;border-spacing:0;}/*!sc*/\ncaption,th,td{text-align:left;font-weight:normal;vertical-align:middle;}/*!sc*/\nq,blockquote{quotes:none;}/*!sc*/\nq:before,blockquote:before,q:after,blockquote:after{content:\"\";content:none;}/*!sc*/\na img{border:none;}/*!sc*/\narticle,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block;}/*!sc*/\nbutton{background:unset;box-shadow:unset;border:unset;text-shadow:unset;cursor:pointer;}/*!sc*/\nbody{overflow-x:hidden;background-color:#fff;color:#000;font-family:'Programme',Arial,sans-serif;line-height:1.45;-webkit-text-size-adjust:100%;}/*!sc*/\nimg{max-width:100%;}/*!sc*/\nli{list-style:none;}/*!sc*/\na{color:#7d8fe8;-webkit-text-decoration:none;text-decoration:none;-webkit-tap-highlight-color:rgba(0,0,0,0);}/*!sc*/\n*,*:before,*:after{box-sizing:border-box;}/*!sc*/\nhr{border:1px solid #e9e9e9;border-width:1px 0 0;margin:1rem 0;}/*!sc*/\npre{white-space:pre-wrap;}/*!sc*/\n::selection{background-color:#b2d7fe;}/*!sc*/\n.noscroll{overflow:hidden;position:absolute;height:100vh;width:100vw;}/*!sc*/\n.noscroll-fixed{overflow:hidden;position:fixed;width:100%;}/*!sc*/\n.grecaptcha-badge{visibility:hidden;}/*!sc*/\ndata-styled.g1088[id=\"sc-global-ixFuEb1\"]{content:\"sc-global-ixFuEb1,\"}/*!sc*/\n#cf_alert_div{display:none !important;}/*!sc*/\ndata-styled.g1089[id=\"sc-global-gdfBvm1\"]{content:\"sc-global-gdfBvm1,\"}/*!sc*/\nhtml{font-size:16px;}/*!sc*/\n@media screen and (min-width:1526px){html{font-size:18px;}}/*!sc*/\ndata-styled.g1090[id=\"sc-global-PeqFD1\"]{content:\"sc-global-PeqFD1,\"}/*!sc*/\n@-webkit-keyframes hFBEL{0%{stroke-dasharray:150%;stroke-dashoffset:0%;}50%{stroke-dasharray:300%;}100%{stroke-dasharray:150%;stroke-dashoffset:600%;}}/*!sc*/\n@keyframes hFBEL{0%{stroke-dasharray:150%;stroke-dashoffset:0%;}50%{stroke-dasharray:300%;}100%{stroke-dasharray:150%;stroke-dashoffset:600%;}}/*!sc*/\ndata-styled.g1091[id=\"sc-keyframes-hFBEL\"]{content:\"hFBEL,\"}/*!sc*/\n</style>\n\n    \n\n\n  \n    <meta content=\"https://genius.com/2pac-all-eyez-on-me-lyrics\" property=\"og:url\" />\n  \n\n  \n    <meta content=\"music.song\" property=\"og:type\" />\n  \n\n  \n    <meta content=\"2Pac (Ft. Big Syke) – All Eyez On Me\" property=\"og:title\" />\n  \n\n  \n    <meta content=\"The title track off of 2Pac’s album All Eyez on Me samples Linda Clifford’s “Never Gonna Stop” (also used for Nas&#39; “Street Dreams,” which was released a few months later in July\" property=\"og:description\" />\n  \n\n  \n    <meta content=\"https://images.genius.com/51289a294de3498eb6e1ff4e3a769c28.1000x1000x1.png\" property=\"og:image\" />\n  \n\n\n\n\n\n  <meta content=\"https://genius.com/2pac-all-eyez-on-me-lyrics\" property=\"twitter:url\" />\n\n  <meta content=\"music.song\" property=\"twitter:type\" />\n\n  <meta content=\"2Pac (Ft. Big Syke) – All Eyez On Me\" property=\"twitter:title\" />\n\n  <meta content=\"The title track off of 2Pac’s album All Eyez on Me samples Linda Clifford’s “Never Gonna Stop” (also used for Nas&#39; “Street Dreams,” which was released a few months later in July\" property=\"twitter:description\" />\n\n  <meta content=\"https://images.genius.com/51289a294de3498eb6e1ff4e3a769c28.1000x1000x1.png\" property=\"twitter:image\" />\n\n  <meta content=\"@Genius\" property=\"twitter:site\" />\n\n  <meta content=\"summary_large_image\" property=\"twitter:card\" />\n\n\n  <meta content=\"Genius\" property=\"twitter:app:name:iphone\" />\n  <meta content=\"709482991\" property=\"twitter:app:id:iphone\" />\n  <meta content=\"genius://songs/6576\" property=\"twitter:app:url:iphone\" />\n\n<meta name=\"description\" content=\"All Eyez On Me Lyrics: Big Syke, &#39;Nook, Hank, Bogart, Big Sur (yeah) / Y&#39;all know how this shit go (you know) / All eyes on me / Motherfuckin&#39; OG / Roll up in the club and shit, is that right? / All eyes\" /><meta name=\"theme-color\" content=\"#e02324\" />\n  <link href=\"ios-app://709482991/genius/songs/6576\" rel=\"alternate\" />\n  \n  <link href=\"https://genius.com/2pac-all-eyez-on-me-lyrics\" rel=\"canonical\" />\n  \n\n\n    \n<script async src=\"https://www.googletagmanager.com/gtag/js?id=G-BJ6QSCFYD0\"></script>\n\n\n<script>\n  window.dataLayer = window.dataLayer || [];\n  function gtag(){dataLayer.push(arguments);}\n  gtag('js', new Date());\n\n  gtag('set', {\n    user_signed_in: \"false\",\n    controller_action: \"songs#show\",\n    primary_tag: \"rap\",\n    music: \"true\",\n    client_routed: 'false',\n    experiment: \"react\",\n    platform_variant: \"desktop_react\",\n    hot_song: \"false\",\n\n    origin: 'firebase',\n    update: true,\n  });\n\n  \n    gtag('config', \"G-BJ6QSCFYD0\");\n  \n    gtag('config', \"G-JRDWPGGXWW\");\n  \n</script>\n\n    <script type=\"text/javascript\">\n  let fullstory_segment;\n  try { fullstory_segment = localStorage.getItem('genius_fullstory_segment'); } catch (e) {}\n\n  if (typeof(fullstory_segment) === 'string') {\n    fullstory_segment = Number(fullstory_segment);\n  } else {\n    fullstory_segment = Math.random();\n    try { localStorage.setItem('genius_fullstory_segment', fullstory_segment); } catch (e) {}\n  }\n\n  if ((fullstory_segment * 100) < 0.001) {\n    window['_fs_debug'] = false;\nwindow['_fs_host'] = 'fullstory.com';\nwindow['_fs_script'] = 'edge.fullstory.com/s/fs.js';\nwindow['_fs_org'] = 'AGFQ9';\nwindow['_fs_namespace'] = 'FS';\n(function(m,n,e,t,l,o,g,y){\n  if (e in m) {if(m.console && m.console.log) { m.console.log('FullStory namespace conflict. Please set window[\"_fs_namespace\"].');} return;}\n  g=m[e]=function(a,b,s){g.q?g.q.push([a,b,s]):g._api(a,b,s);};g.q=[];\n  o=n.createElement(t);o.async=1;o.crossOrigin='anonymous';o.src='https://'+_fs_script;\n  y=n.getElementsByTagName(t)[0];y.parentNode.insertBefore(o,y);\n  g.identify=function(i,v,s){g(l,{uid:i},s);if(v)g(l,v,s)};g.setUserVars=function(v,s){g(l,v,s)};g.event=function(i,v,s){g('event',{n:i,p:v},s)};\n  g.shutdown=function(){g(\"rec\",!1)};g.restart=function(){g(\"rec\",!0)};\n  g.log = function(a,b) { g(\"log\", [a,b]) };\n  g.consent=function(a){g(\"consent\",!arguments.length||a)};\n  g.identifyAccount=function(i,v){o='account';v=v||{};v.acctId=i;g(o,v)};\n  g.clearUserCookie=function(){};\n})(window,document,window['_fs_namespace'],'script','user');\n\n\n    let current_user = null;\n\n    if (current_user) {\n      FS.identify('user:' + current_user.id, {\n        displayName: current_user.login,\n        contrib_bool: current_user.is_contrib,\n        editor_bool: current_user.is_editor,\n      });\n    } else {\n      FS.identify(false);\n    }\n  }\n</script>\n\n    \n  <script type=\"text/javascript\">\n  var _qevents = _qevents || [];\n  (function() {\n    var elem = document.createElement('script');\n    elem.src = (document.location.protocol == 'https:' ? 'https://secure' : 'http://edge') + '.quantserve.com/quant.js';\n    elem.async = true;\n    elem.type = 'text/javascript';\n    var scpt = document.getElementsByTagName('script')[0];\n    scpt.parentNode.insertBefore(elem, scpt);\n  })();\n</script>\n\n\n\n    \n\n<script>\n  (function() {\n    const scriptUrl = \"https://js.assemblyexchange.com/wana.5.2.1.js\";\n    const anaScript = document.createElement('script');\n    anaScript.setAttribute('src', scriptUrl);\n    anaScript.setAttribute('id', 'wana-script');\n    anaScript.setAttribute('async', true);\n\n    let anaWebInstance = null;\n    document.addEventListener('ANAReady', () => {\n      anaWebInstance = new window.ANAWeb({\n        lightMode: true,\n        syncCookies: false,\n        anaUid: window.getAnaUid(),\n      });\n    });\n\n    document.head.appendChild(anaScript);\n\n    window.getAnaWebInstance = () => {\n      if (anaWebInstance) {\n        return Promise.resolve(anaWebInstance);\n      }\n      return new Promise(resolve => {\n        document.addEventListener('ANAReady', () => {\n          resolve(anaWebInstance);\n        });\n      });\n    };\n  })();\n</script>\n\n\n    <meta itemprop=\"long-tail-cache\" content=\"XX\">\n    <meta itemprop=\"cf-cache-status\" content=\"HIT\">\n    <meta itemprop=\"rendered-with-cache\" content=\"false\">\n    <meta itemprop=\"cf-latitude\" content=\"-27.44370\">\n    <meta itemprop=\"cf-longitude\" content=\"153.02440\">\n    <meta itemprop=\"cf-country\" content=\"AU\">\n  </head>\n  <body>\n    <div id=\"application\"><div class=\"LeaderboardOrMarquee__Sticky-yjd3i4-0 dIgauN Leaderboard__LeaderboardOrMarquee-da326u-0 jOhzET\"><div class=\"LeaderboardOrMarquee__Container-yjd3i4-1 cLZLgM\"><div class=\"DfpAd__Container-sc-1tnbv7f-0 kiNXoS\"><div id=\"div-gpt-ad-desktop_song_combined_leaderboard-desktop_song_combined_leaderboard-1\"></div><div class=\"DfpAd__Placeholder-sc-1tnbv7f-1 ilfajN\"></div></div></div></div><div id=\"sticky-nav\" class=\"StickyNavdesktop__Container-sc-9maqdk-0 hiGcDl\"><div class=\"StickyNavdesktop__Left-sc-9maqdk-1 ckFWax\"><a href=\"https://genius.com\" class=\"PageHeaderLogo__Link-sc-175tsd3-0 iIgbGO StickyNavdesktop__Logo-sc-9maqdk-7 hRWuAt\"><svg viewBox=\"0 0 100 15\"><path d=\"M11.7 2.9s0-.1 0 0c-.8-.8-1.7-1.2-2.8-1.2-1.1 0-2.1.4-2.8 1.1-.2.2-.3.4-.5.6v.1c0 .1.1.1.1.1.4-.2.9-.3 1.4-.3 1.1 0 2.2.5 2.9 1.2h1.6c.1 0 .1-.1.1-.1V2.9c.1 0 0 0 0 0zm-.1 4.6h-1.5c-.8 0-1.4-.6-1.5-1.4.1 0 0-.1 0-.1-.3 0-.6.2-.8.4v.2c-.6 1.8.1 2.4.9 2.4h1.1c.1 0 .1.1.1.1v.4c0 .1.1.1.1.1.6-.1 1.2-.4 1.7-.8V7.6c.1 0 0-.1-.1-.1z\"></path><path d=\"M11.6 11.9s-.1 0 0 0c-.1 0-.1 0 0 0-.1 0-.1 0 0 0-.8.3-1.6.5-2.5.5-3.7 0-6.8-3-6.8-6.8 0-.9.2-1.7.5-2.5 0-.1-.1-.1-.2-.1h-.1C1.4 4.2.8 5.7.8 7.5c0 3.6 2.9 6.4 6.4 6.4 1.7 0 3.3-.7 4.4-1.8V12c.1 0 0-.1 0-.1zm13.7-3.1h3.5c.8 0 1.4-.5 1.4-1.3v-.2c0-.1-.1-.1-.1-.1h-4.8c-.1 0-.1.1-.1.1v1.4c-.1 0 0 .1.1.1zm5.1-6.7h-5.2c-.1 0-.1.1-.1.1v1.4c0 .1.1.1.1.1H29c.8 0 1.4-.5 1.4-1.3v-.2c.1-.1.1-.1 0-.1z\"></path><path d=\"M30.4 12.3h-6.1c-1 0-1.6-.6-1.6-1.6V1c0-.1-.1-.1-.1-.1-1.1 0-1.8.7-1.8 1.8V12c0 1.1.7 1.8 1.8 1.8H29c.8 0 1.4-.6 1.4-1.3v-.1c.1 0 .1-.1 0-.1zm12 0c-.6-.1-.9-.6-.9-1.3V1.1s0-.1-.1-.1H41c-.9 0-1.5.6-1.5 1.5v9.9c0 .9.6 1.5 1.5 1.5.8 0 1.4-.6 1.5-1.5 0-.1 0-.1-.1-.1zm8.2 0h-.2c-.9 0-1.4-.4-1.8-1.1l-4.5-7.4-.1-.1c-.1 0-.1.1-.1.1V8l2.8 4.7c.4.6.9 1.2 2 1.2 1 0 1.7-.5 2-1.4 0-.2-.1-.2-.1-.2zm-.9-3.8c.1 0 .1-.1.1-.1V1.1c0-.1 0-.1-.1-.1h-.4c-.9 0-1.5.6-1.5 1.5v3.1l1.7 2.8c.1 0 .1.1.2.1zm13 3.8c-.6-.1-.9-.6-.9-1.2v-10c0-.1 0-.1-.1-.1h-.3c-.9 0-1.5.6-1.5 1.5v9.9c0 .9.6 1.5 1.5 1.5.8 0 1.4-.6 1.5-1.5l-.2-.1zm18.4-.5H81c-.7.3-1.5.5-2.5.5-1.6 0-2.9-.5-3.7-1.4-.9-1-1.4-2.4-1.4-4.2V1c0-.1 0-.1-.1-.1H73c-.9 0-1.5.6-1.5 1.5V8c0 3.7 2 5.9 5.4 5.9 1.9 0 3.4-.7 4.3-1.9v-.1c0-.1 0-.1-.1-.1z\"></path><path d=\"M81.2.9h-.3c-.9 0-1.5.6-1.5 1.5v5.7c0 .7-.1 1.3-.3 1.8 0 .1.1.1.1.1 1.4-.3 2.1-1.4 2.1-3.3V1c0-.1-.1-.1-.1-.1zm12.7 7.6l1.4.3c1.5.3 1.6.8 1.6 1.2 0 .1.1.1.1.1 1.1-.1 1.8-.7 1.8-1.5s-.6-1.2-1.9-1.5l-1.4-.3c-3.2-.6-3.8-2.3-3.8-3.6 0-.7.2-1.3.6-1.9v-.2c0-.1-.1-.1-.1-.1-1.5.7-2.3 1.9-2.3 3.4-.1 2.3 1.3 3.7 4 4.1zm5.2 3.2c-.1.1-.1.1 0 0-.9.4-1.8.6-2.8.6-1.6 0-3-.5-4.3-1.4-.3-.3-.5-.6-.5-1 0-.1 0-.1-.1-.1s-.3-.1-.4-.1c-.4 0-.8.2-1.1.6-.2.3-.4.7-.3 1.1.1.4.3.7.6 1 1.4 1 2.8 1.5 4.5 1.5 2 0 3.7-.7 4.5-1.9v-.1c0-.1 0-.2-.1-.2z\"></path><path d=\"M94.1 3.2c0 .1.1.1.1.1h.2c1.1 0 1.7.3 2.4.8.3.2.6.3 1 .3s.8-.2 1.1-.6c.2-.3.3-.6.3-.9 0-.1 0-.1-.1-.1-.2 0-.3-.1-.5-.2-.8-.6-1.4-.9-2.6-.9-1.2 0-2 .6-2 1.4.1 0 .1 0 .1.1z\"></path></svg></a><div class=\"StickyNavdesktop__Subnavigation-sc-9maqdk-6 dwjYdZ\"><a href=\"/#featured-stories\" rel=\"noopener\" class=\"StyledLink-sc-3ea0mt-0 jTJfqD StickyNavdesktop__SiteLink-sc-9maqdk-4 clweAx\" font-weight=\"light\"><span font-weight=\"light\" class=\"TextLabel-sc-8kw9oj-0 kNjZBr\">Featured</span></a><a href=\"/#top-songs\" rel=\"noopener\" class=\"StyledLink-sc-3ea0mt-0 jTJfqD StickyNavdesktop__SiteLink-sc-9maqdk-4 clweAx\" font-weight=\"light\"><span font-weight=\"light\" class=\"TextLabel-sc-8kw9oj-0 kNjZBr\">Charts</span></a><a href=\"/#videos\" rel=\"noopener\" class=\"StyledLink-sc-3ea0mt-0 jTJfqD StickyNavdesktop__SiteLink-sc-9maqdk-4 clweAx\" font-weight=\"light\"><span font-weight=\"light\" class=\"TextLabel-sc-8kw9oj-0 kNjZBr\">Videos</span></a><a href=\"https://promote.genius.com\" target=\"_blank\" rel=\"noopener\" class=\"StyledLink-sc-3ea0mt-0 jTJfqD StickyNavdesktop__SiteLink-sc-9maqdk-4 clweAx\" font-weight=\"light\"><span font-weight=\"light\" class=\"TextLabel-sc-8kw9oj-0 kNjZBr\">Promote Your Music</span></a></div></div><div class=\"StickyNavdesktop__Right-sc-9maqdk-2 JliIh\"><button class=\"TextButton-sc-192nsqv-0 hVAZmF StickyNavSignUp__Button-sc-14apwk1-0 bwWQCs StickyNavdesktop__AuthTextButton-sc-9maqdk-5 RakDs\" type=\"button\">Sign Up</button><form action=\"/search\" method=\"get\" class=\"StickyNavSearchdesktop__Form-sc-1wddxfx-0 dPICWx\"><input name=\"q\" placeholder=\"Search lyrics &amp; more\" autoComplete=\"off\" required=\"\" class=\"StickyNavSearchdesktop__Input-sc-1wddxfx-2 cmGDeX\"/><div class=\"StickyNavSearchdesktop__Icon-sc-1wddxfx-1 QoIjR\"><svg viewBox=\"0 0 21.48 21.59\"><path d=\"M21.48 20.18L14.8 13.5a8.38 8.38 0 1 0-1.43 1.4l6.69 6.69zM2 8.31a6.32 6.32 0 1 1 6.32 6.32A6.32 6.32 0 0 1 2 8.31z\"></path></svg></div></form></div></div><main class=\"SongPage__Container-sc-19xhmoi-0 buKnHw\"><div class=\"PageGriddesktop-a6v82w-0 SongPageGriddesktop-sc-1px5b71-0 jecoie SongHeaderdesktop__Container-sc-1effuo1-0 jJZPKi\"><div class=\"StickyNavSentinel__Sentinel-sc-1yh9i7p-0 hNLJnc\"></div><div class=\"SongHeaderdesktop__Left-sc-1effuo1-1 diUihk\"><div class=\"SongHeaderdesktop__PyongWrapper-sc-1effuo1-12 hLhFfl\"><div class=\"Pyong__Container-yq95kq-0 eMjKRh\"><div class=\"Tooltip__Container-sc-1uvy5c2-0 cRrFdP\"><div class=\"Tooltip__Children-sc-1uvy5c2-2 dvOJud\"><button class=\"LabelWithIcon__Container-hjli77-0 hrQuZg\" height=\"1.313rem\"><svg viewBox=\"0 0 11.37 22\"><path d=\"M0 7l6.16-7 3.3 7H6.89S5.5 12.1 5.5 12.17h5.87L6.09 22l.66-7H.88l2.89-8z\"></path></svg><span class=\"LabelWithIcon__Label-hjli77-1 jwfWMJ\">111</span></button></div></div></div></div><div class=\"SongHeaderdesktop__CoverArtContainer-sc-1effuo1-6 jLdecJ\"><div class=\"SongHeaderdesktop__CoverArt-sc-1effuo1-7 fVjbnr\"><img alt=\"Cover art for All Eyez On Me by 2Pac\" class=\"SizedImage__Image-sc-1hyeaua-1 iMdmgx SongHeaderdesktop__SizedImage-sc-1effuo1-13 eVLetJ\"/></div></div></div><div class=\"SongHeaderdesktop__Right-sc-1effuo1-2 lfjman\"><div class=\"SongHeaderdesktop__Information-sc-1effuo1-4 ieJVb\"><div class=\"SongHeaderdesktop__SongDetails-sc-1effuo1-5 dhqXbj\"><h1 font-size=\"medium\" class=\"SongHeaderdesktop__Title-sc-1effuo1-8 isLvDW\"><span class=\"SongHeaderdesktop__HiddenMask-sc-1effuo1-11 iMpFIj\">All Eyez On Me</span></h1><div class=\"HeaderArtistAndTracklistdesktop__Container-sc-4vdeb8-0 hjExsS\"><div class=\"HeaderArtistAndTracklistdesktop__ListArtists-sc-4vdeb8-1 bYBBwt\"><span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/2pac\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">2Pac</a></span></span></div><div class=\"HeaderArtistAndTracklistdesktop__Tracklist-sc-4vdeb8-2 glZsJC\">Track 24 on<!-- --> <a href=\"#primary-album\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">All Eyez On Me<!-- --> <span class=\"InlineSvg__Wrapper-sc-1342j7p-0 blSbzj\"><svg viewBox=\"0 0 6.6 16\"><path d=\"M1.6 8.8l.6-.6 1 1 .5.7V6H0v-.8h4.5v4.6l.5-.6 1-1 .6.5L4 11.3 1.6 8.8z\"></path></svg></span></a></div></div><div class=\"HeaderCredits__Container-wx7h8g-0 iVIbmD\"><div class=\"HeaderCredits__Section-wx7h8g-1 jwiCow\"><p class=\"HeaderCredits__Label-wx7h8g-2 ghcavQ\">Featuring</p><div class=\"HeaderCredits__List-wx7h8g-3 cTzqde\"><span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Big-syke\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Big Syke</a></span></span></div></div><div class=\"HeaderCredits__Section-wx7h8g-1 jwiCow\"><p class=\"HeaderCredits__Label-wx7h8g-2 ghcavQ\">Producer</p><div class=\"HeaderCredits__List-wx7h8g-3 cTzqde\"><span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Johnny-j\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Johnny J</a></span></span></div></div></div></div><div class=\"SongHeaderdesktop__Bottom-sc-1effuo1-3 hEtDoX\"><div class=\"HeaderBio__Container-oaxemt-0 jrQaBI SongHeaderdesktop__HeaderBio-sc-1effuo1-14 jDbgEP\"><a href=\"#about\" class=\"StyledLink-sc-3ea0mt-0 fqTa-dX HeaderBio__Wrapper-oaxemt-1 brNcYU SongHeaderdesktop__HeaderBio-sc-1effuo1-14 jDbgEP\" font-weight=\"light\">The title track off of 2Pac’s album All Eyez on Me samples Linda Clifford’s “Never Gonna Stop” (also used for Nas&#x27; “Street Dreams,” which was released a few months later in… <span class=\"HeaderBio__ViewBio-oaxemt-2 dggIzN\">Read More<!-- --> <span class=\"InlineSvg__Wrapper-sc-1342j7p-0 blSbzj\"><svg viewBox=\"0 0 6.6 16\"><path d=\"M1.6 8.8l.6-.6 1 1 .5.7V6H0v-.8h4.5v4.6l.5-.6 1-1 .6.5L4 11.3 1.6 8.8z\"></path></svg></span></span></a></div><div class=\"MetadataStats__Container-sc-1t7d8ac-0 cDJyol\"><span class=\"LabelWithIcon__Container-hjli77-0 gcYcIR MetadataStats__LabelWithIcon-sc-1t7d8ac-3 gZUIou\" height=\"1em\"><svg fill=\"currentColor\" viewBox=\"0 0 18 18\"><path d=\"M15.923 1.385h-2.77V0H11.77v1.385H6.231V0H4.846v1.385h-2.77c-.76 0-1.384.623-1.384 1.384v13.846c0 .762.623 1.385 1.385 1.385h13.846c.762 0 1.385-.623 1.385-1.385V2.77c0-.761-.623-1.384-1.385-1.384Zm0 15.23H2.077V6.923h13.846v9.692Zm0-11.077H2.077V2.77h2.77v1.385H6.23V2.769h5.538v1.385h1.385V2.769h2.77v2.77Z\"></path></svg><span class=\"LabelWithIcon__Label-hjli77-1 hgsvkF\">Feb. 13, 1996</span></span><span class=\"LabelWithIcon__Container-hjli77-0 gcYcIR MetadataStats__LabelWithIcon-sc-1t7d8ac-3 gZUIou\" height=\"1em\"><svg fill=\"currentColor\" viewBox=\"0 0 18 18\"><path fill-rule=\"evenodd\" d=\"M4 16.483A9 9 0 1 0 14 1.518 9 9 0 0 0 4 16.483Zm.714-13.897a7.714 7.714 0 1 1 8.572 12.828A7.714 7.714 0 0 1 4.714 2.586Zm3.643 6.678 3.594 3.593.906-.906L9.643 8.73V3.214H8.357v6.05Z\" clip-rule=\"evenodd\"></path></svg><span class=\"LabelWithIcon__Label-hjli77-1 hgsvkF\">1 viewer</span></span><span class=\"LabelWithIcon__Container-hjli77-0 gcYcIR MetadataStats__LabelWithIcon-sc-1t7d8ac-3 gZUIou\" height=\"1em\"><svg fill=\"currentColor\" viewBox=\"0 0 26 18\"><path fill-rule=\"evenodd\" d=\"M20.418 2.53a13.655 13.655 0 0 1 4.806 6.192.818.818 0 0 1 0 .556A13.655 13.655 0 0 1 13 18 13.655 13.655 0 0 1 .776 9.278a.818.818 0 0 1 0-.556A13.655 13.655 0 0 1 13 0c2.667.1 5.246.98 7.418 2.53ZM2.421 9C4.08 13.148 8.664 16.364 13 16.364S21.918 13.148 23.58 9C21.917 4.852 17.335 1.636 13 1.636S4.082 4.852 2.42 9Zm7.852-4.082a4.91 4.91 0 1 1 5.454 8.164 4.91 4.91 0 0 1-5.454-8.164Zm.909 6.803a3.272 3.272 0 1 0 3.636-5.442 3.272 3.272 0 0 0-3.636 5.442Z\" clip-rule=\"evenodd\"></path></svg><span class=\"LabelWithIcon__Label-hjli77-1 hgsvkF\">1.2M views</span></span></div></div></div><div class=\"SongHeaderdesktop__PrimisContainer-sc-1effuo1-15 nPqjG\"><div class=\"PrimisPlayer__Container-sc-1tvdtf7-1 csMTdh\"></div></div></div></div><div class=\"SectionScrollSentinel__Container-eoe1bv-0 icvVds\"><div class=\"SectionScrollSentinel__Element-eoe1bv-1 gTVDnZ\"></div><div id=\"lyrics\" class=\"SectionScrollSentinel__SectionAnchor-eoe1bv-2 difnmr\"></div><p class=\"SongPage__HeaderSpace-sc-19xhmoi-1 gloXyw\"></p><div id=\"annotation-portal-target\" class=\"SongPage__LyricsWrapper-sc-19xhmoi-2 mkDeY\"><div id=\"lyrics-root-pin-spacer\"><div id=\"lyrics-root\" class=\"PageGriddesktop-a6v82w-0 SongPageGriddesktop-sc-1px5b71-0 Lyrics__Root-sc-1ynbvzw-0 iEyyHq\"><div class=\"LyricsHeader__Container-ejidji-1 hbjTyd\"><button class=\"ContributorsCreditSong__Container-sc-12hq27v-0 bvjTUg\"><span class=\"ContributorsCreditSong__Label-sc-12hq27v-1 joWKqm\"><svg viewBox=\"0 0 22 16.47\" height=\"0.75rem\" class=\"ContributorsCreditSong__People-sc-12hq27v-3 fScxZH\"><path d=\"M12.55 6.76a4 4 0 1 0 0-4.59 4.41 4.41 0 0 1 0 4.59zm3.07 2.91v5.17H22V9.66l-6.38.01M7 9a4.43 4.43 0 0 0 3.87-2.23 4.41 4.41 0 0 0 0-4.59 4.47 4.47 0 0 0-8.38 2.3A4.48 4.48 0 0 0 7 9zm-7 1.35v6.12h13.89v-6.14l-6.04.01-7.85.01\"></path></svg><span class=\"ContributorsCreditSong__ContributorsReference-sc-12hq27v-2 hymydu\">156 Contributors</span></span></button><div><div class=\"Dropdown__Container-ugfjuc-0 gGGmJL\"><button class=\"Dropdown__Toggle-ugfjuc-2 ikbJXY\"><div class=\"LyricsHeader__TranslationsContainer-ejidji-6 kUKuYL\"><svg fill=\"currentColor\" viewBox=\"0 0 20 19\" class=\"LyricsHeader__Languages-ejidji-8 crJrtp\"><path fill-rule=\"evenodd\" d=\"M11.335 2.6v1.333H9.2A11.76 11.76 0 0 1 6.588 9.02a9.654 9.654 0 0 0 3.413 2.247l-.473 1.226a11.279 11.279 0 0 1-3.84-2.56 12.314 12.314 0 0 1-3.853 2.574l-.5-1.24a11.227 11.227 0 0 0 3.44-2.28 10.98 10.98 0 0 1-2-3.72h1.4A9 9 0 0 0 5.7 8.053a9.807 9.807 0 0 0 2.127-4.12H.668V2.6h4.667v-2h1.333v2h4.667Zm7.997 16h-1.433l-1.067-2.667h-4.567L11.2 18.6H9.765l4-10h1.567l4 10Zm-4.787-8.373L12.8 14.6h3.5l-1.754-4.373Z\" clip-rule=\"evenodd\"></path></svg><span class=\"LyricsHeader__TranslationsText-ejidji-7 kGzSQz\">Translations</span><span class=\"LyricsHeader__MenuIcon-ejidji-9 dKqQxi\"><svg viewBox=\"0 0 9 7\"><path d=\"M4.488 7 0 0h8.977L4.488 7Z\"></path></svg></span></div></button><div class=\"Dropdown__ContentContainer-ugfjuc-1 gjsNwk\"><ul class=\"LyricsHeader__DropdownContents-ejidji-4 fSmeGU\"><li class=\"LyricsHeader__DropdownItem-ejidji-5 SfdQZ\"><a href=\"https://genius.com/Genius-brasil-traducoes-2pac-all-eyez-on-me-ft-big-syke-traducao-em-portugues-lyrics\" title=\"2Pac - All Eyez on Me ft. Big Syke (Tradução em Português)\" class=\"TextButton-sc-192nsqv-0 hVAZmF LyricsHeader__TextButton-ejidji-10 iljvxT\" type=\"button\"><div class=\"LyricsHeader__TextEllipsis-ejidji-11 fdKPz\">Português</div></a></li></ul></div></div></div><div class=\"LyricsHeader__TitleContainer-ejidji-2 dzLEIC\"><h2 font-weight=\"light\" class=\"TextLabel-sc-8kw9oj-0 LyricsHeader__Title-ejidji-0 dddWnX\">All Eyez On Me Lyrics</h2></div></div><div data-lyrics-container=\"true\" class=\"Lyrics__Container-sc-1ynbvzw-1 kUgSbL\">[Intro: 2Pac]<br/><a href=\"/18437859/2pac-all-eyez-on-me/Big-syke-nook-hank-bogart-big-sur-yeah\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">Big Syke, &#x27;Nook, Hank, Bogart, Big Sur (yeah)</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span><br/><a href=\"/24858294/2pac-all-eyez-on-me/Yall-know-how-this-shit-go-you-know\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">Y&#x27;all know how this shit go (you know)</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span><br/>All eyes on me<br/>Motherfuckin&#x27; OG<br/>Roll up in the club and shit, is that right?<br/>All eyes on me<br/>All eyes on me<br/>But you know what?<br/><br/>[Verse 1: 2Pac]<br/><a href=\"/18907858/2pac-all-eyez-on-me/I-bet-you-got-it-twisted-you-dont-know-who-to-trust-so-many-player-hatin-niggas-tryna-sound-like-us\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">I bet you got it twisted, you don&#x27;t know who to trust<br/>So many player-hatin&#x27; niggas tryna sound like us</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span><br/><a href=\"/255696/2pac-all-eyez-on-me/Say-they-ready-for-the-funk-but-i-dont-think-they-knowin-straight-to-the-depths-of-hell-is-where-those-cowards-goin-well-are-you-still-down-nigga-holla-when-you-see-me\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">Say they ready for the funk, but I don&#x27;t think they knowin&#x27;<br/>Straight to the depths of Hell is where those cowards goin&#x27;<br/>Well, are you still down? Nigga, holla when you see me</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span><br/><a href=\"/2269422/2pac-all-eyez-on-me/And-let-these-devils-be-sorry-for-the-day-they-finally-freed-me\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">And let these devils be sorry for the day they finally freed me</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span><br/><a href=\"/19617841/2pac-all-eyez-on-me/I-got-a-caravan-of-niggas-every-time-we-ride\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">I got a caravan of niggas every time we ride</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span><br/>Hittin&#x27; motherfuckers up when we pass by<br/>Until I die, live the life of a boss player &#x27;cause even when I&#x27;m high<br/>Fuck with me and get crossed later, the futures in my eyes<br/>&#x27;cause all I want is cash and things<br/><a href=\"/20748281/2pac-all-eyez-on-me/A-five-double-0-benz-flauntin-flashy-rings-uhh\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">A five-double-0 Benz, flauntin&#x27; flashy rings, uhh</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span><br/>Bitches pursue me like a dream<br/>Been known to disappear before your eyes just like a dope fiend<br/>It seems, my main thing was to be major paid<br/>The game sharper than a motherfuckin&#x27; razor blade<br/>Say money bring bitches and bitches bring lies<br/><a href=\"/20901107/2pac-all-eyez-on-me/One-niggas-gettin-jealous-and-motherfuckers-died\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">One nigga&#x27;s gettin&#x27; jealous and motherfuckers died</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span><br/><a href=\"/2689751/2pac-all-eyez-on-me/Depend-on-me-like-the-first-and-fifteenth-they-might-hold-me-for-a-second-but-these-punks-wont-get-me\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">Depend on me like the first and fifteenth<br/>They might hold me for a second, but these punks won&#x27;t get me</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span><br/>We got foe niggas and low riders in ski masks<br/>Screamin&#x27;, &quot;Thug Life!&quot; every time they pass, all eyes on me<br/></div><div class=\"RightSidebar__Container-pajcl2-0 fuSJbt\"><div class=\"StubhubLink__Container-sc-1n6qb9w-0 iRUflX\"></div><div class=\"SidebarAd__Container-sc-1cw85h6-0 kvuklz\"><div class=\"SidebarAd__StickyContainer-sc-1cw85h6-1 bJESjt\"><div class=\"DfpAd__Container-sc-1tnbv7f-0 kiNXoS\"><div id=\"div-gpt-ad-desktop_song_lyrics_sidebar-desktop_song_lyrics_sidebar-1\"></div><div class=\"DfpAd__Placeholder-sc-1tnbv7f-1 ilfajN\"></div></div></div></div><div class=\"LyricsSidebarAd__RecommendationsContainer-sc-1duvwla-0 jUHZcK\"><aside class=\"RecommendedSongs__Container-fhtuij-0 fUyrrM LyricsSidebarAd__Recommendations-sc-1duvwla-1 hAVRTF\"><div class=\"RecommendedSongs__Header-fhtuij-2 lfAvEQ\">You might also like</div><div class=\"RecommendedSongs__Body-fhtuij-1 fVWWod\"></div></aside></div></div><div data-exclude-from-selection=\"true\" class=\"InreadContainer__Container-sc-19040w5-0 cujBpY\"><div class=\"DfpAd__Container-sc-1tnbv7f-0 hOMcjE\"><div id=\"div-gpt-ad-desktop_song_lyrics_inread-desktop_song_lyrics_inread-1\"></div><div class=\"DfpAd__Placeholder-sc-1tnbv7f-1 hoVEOg\"></div></div></div><div data-lyrics-container=\"true\" class=\"Lyrics__Container-sc-1ynbvzw-1 kUgSbL\">[Chorus: 2Pac]<br/>Live the life of a thug nigga until the day I die<br/>Live the life of a boss player (All eyes on me) &#x27;cause even gettin&#x27; high<br/>All eyes on me<br/>Live the life of a thug nigga until the day I die<br/>Live the life of a boss player &#x27;cause even gettin&#x27; high<br/><br/>[Interlude: Big Syke]<br/>Hey, to my nigga Pac<br/><br/>[Verse 2: Big Syke]<br/>So much trouble in the world, nigga<br/>Can&#x27;t nobody feel your pain<br/>The world&#x27;s changin&#x27; every day, time&#x27;s movin&#x27; fast<br/>My girl said I need a raise, how long will she last?<br/><a href=\"/2432432/2pac-all-eyez-on-me/Im-caught-between-my-woman-and-my-pistol-and-my-chips\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">I&#x27;m caught between my woman and my pistol and my chips</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span><br/><a href=\"/22058141/2pac-all-eyez-on-me/Triple-beam\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">Triple beam</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span>, got some smokers on, whistle as I dip<br/>I&#x27;m lost in the land with no plan, livin&#x27; life flawless<br/>Crime boss, contraband, let me toss this<br/>Mediocres got a lot of nerve, let my bucket swerve<br/>I&#x27;m takin&#x27; off from the curb<br/><a href=\"/13839497/2pac-all-eyez-on-me/The-nervousness-neglect-make-me-pack-a-tec\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">The nervousness neglect make me pack a TEC</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span><br/><a href=\"/15698084/2pac-all-eyez-on-me/Devoted-to-servin-this-moet-and-pay-checks\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">Devoted to servin&#x27; this Moët and pay checks</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span><br/><a href=\"/9137652/2pac-all-eyez-on-me/Like-akai-satellite-nigga-im-forever-ballin\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">Like Akai satellite, nigga, I&#x27;m forever ballin&#x27;</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span><br/>It ain&#x27;t right: parasites, triggers, and fleas crawlin&#x27;<br/>Sucker, duck and get busted, no emotion<br/>My devotion is handlin&#x27; my business, nigga, keep on coastin&#x27;<br/>Where you goin&#x27;, I been there, came back as lonely, homie<br/>Steady flowin&#x27; against the grain, niggas still don&#x27;t know me<br/>It&#x27;s about the money in this rap shit, this crap shit<br/>It ain&#x27;t funny, niggas don&#x27;t even know how to act, shit<br/>What can I do? What can I say? Is there another way?<br/>Blunts and gin all day, 24 parlay<br/>My little homie G, can&#x27;t you see I&#x27;m buster-free?<br/>Niggas can&#x27;t stand me; all eyes on me<br/></div><div class=\"RightSidebar__Container-pajcl2-0 fuSJbt\"><div class=\"SidebarAd__Container-sc-1cw85h6-0 cFlBmm\"><div class=\"SidebarAd__StickyContainer-sc-1cw85h6-1 bZuAng\"><div class=\"DfpAd__Container-sc-1tnbv7f-0 kiNXoS\"><div id=\"div-gpt-ad-desktop_song_lyrics_sidebar2-desktop_song_lyrics_sidebar2-1\"></div><div class=\"DfpAd__Placeholder-sc-1tnbv7f-1 ilfajN\"></div></div></div></div></div><div data-exclude-from-selection=\"true\" class=\"InreadContainer__Container-sc-19040w5-0 cujBpY\"><div class=\"DfpAd__Container-sc-1tnbv7f-0 hOMcjE\"><div id=\"div-gpt-ad-desktop_song_lyrics_inread2-desktop_song_lyrics_inread2-1\"></div><div class=\"DfpAd__Placeholder-sc-1tnbv7f-1 hoVEOg\"></div></div></div><div data-lyrics-container=\"true\" class=\"Lyrics__Container-sc-1ynbvzw-1 kUgSbL\">[Chorus: 2Pac]<br/>Live the life of a thug nigga until the day I die<br/>Live the life of a boss player &#x27;cause even gettin&#x27; high<br/>All eyes on me<br/>All eyes on me<br/>Live the life of a thug nigga until the day I die<br/>Live the life of a boss player &#x27;cause even gettin&#x27; high<br/>All eyes on me<br/><br/>[Verse 3: 2Pac]<br/><a href=\"/293434/2pac-all-eyez-on-me/The-feds-is-watchin-niggas-plottin-to-get-me\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">The feds is watchin&#x27;, niggas plottin&#x27; to get me</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span><br/>Will I survive? Will I die? Come on, let&#x27;s picture the possibility<br/><a href=\"/12128274/2pac-all-eyez-on-me/Givin-me-charges-lawyers-makin-a-grip\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">Givin&#x27; me charges, lawyers makin&#x27; a grip</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span><br/>I told the judge I was raised wrong and that&#x27;s why I blaze shit<br/>Was hyper as a kid, cold as a teenager<br/>On my mobile, callin&#x27; big shots on the scene major<br/>Packin&#x27; hundreds in my drawers, fuck the law<br/>Bitches, I fuck with a passion, I&#x27;m livin&#x27; rough and raw<br/><a href=\"/20748314/2pac-all-eyez-on-me/Catchin-cases-at-a-fast-rate-ballin-in-the-fast-lane\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">Catchin&#x27; cases at a fast rate, ballin&#x27; in the fast lane</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span><br/>Hustle &#x27;til the mornin&#x27;, never stopped until the cash came<br/>Live my life as a thug nigga until the day I die<br/>Live my life as a boss player, &#x27;cause even gettin&#x27; high<br/>These niggas got me tossin&#x27; shit<br/>I put the top down, now it&#x27;s time to floss my shit<br/><a href=\"/2966483/2pac-all-eyez-on-me/Keep-your-head-up-nigga-make-these-motherfuckers-suffer\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">Keep your head up, nigga, make these motherfuckers suffer</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span><br/><a href=\"/2432384/2pac-all-eyez-on-me/Up-in-the-benz-burnin-rubber\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">Up in the Benz, burnin&#x27; rubber</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span><br/><a href=\"/5049714/2pac-all-eyez-on-me/The-money-is-mandatory-the-hoes-is-for-the-stress\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">The money is mandatory, the hoes is for the stress</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span><br/><a href=\"/3082776/2pac-all-eyez-on-me/This-criminal-lifestyle-equipped-with-the-bulletproof-vest\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">This criminal lifestyle, equipped with the bulletproof vest</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span><br/>Make sure your eyes is on the meal ticket<br/>Get your money, motherfucker, let&#x27;s get rich and we&#x27;ll kick it<br/>All eyes on me<br/></div><div class=\"RightSidebar__Container-pajcl2-0 fuSJbt\"><div class=\"SidebarAd__Container-sc-1cw85h6-0 cFlBmm\"><div class=\"SidebarAd__StickyContainer-sc-1cw85h6-1 bZuAng\"><div class=\"DfpAd__Container-sc-1tnbv7f-0 kiNXoS\"><div id=\"div-gpt-ad-desktop_song_lyrics_sidebar3-desktop_song_lyrics_sidebar3-1\"></div><div class=\"DfpAd__Placeholder-sc-1tnbv7f-1 ilfajN\"></div></div></div></div></div><div data-exclude-from-selection=\"true\" class=\"InreadContainer__Container-sc-19040w5-0 cujBpY\"><div class=\"DfpAd__Container-sc-1tnbv7f-0 hOMcjE\"><div id=\"div-gpt-ad-desktop_song_lyrics_inread3-desktop_song_lyrics_inread3-1\"></div><div class=\"DfpAd__Placeholder-sc-1tnbv7f-1 hoVEOg\"></div></div></div><div data-lyrics-container=\"true\" class=\"Lyrics__Container-sc-1ynbvzw-1 kUgSbL\">[Chorus: 2Pac]<br/>Live the life as a thug nigga until the day I die<br/>Live the life as a boss player &#x27;cause even gettin&#x27; high<br/>All eyes on me<br/>All eyes on me<br/>Live the life of a thug nigga until the day I die<br/>Live the life of a boss player &#x27;cause even gettin&#x27; high<br/>All eyes on me<br/><br/>[Outro: 2Pac]<br/><a href=\"/11537155/2pac-all-eyez-on-me/Pay-attention-my-niggas-see-how-that-shit-go-nigga-walk-up-in-this-motherfucker-and-it-be-like-bing-cops-bitches-every-motherfuckin-body\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">Pay attention, my niggas! See how that shit go?<br/>Nigga walk up in this motherfucker and it be like, &quot;Bing!&quot;<br/>Cops, bitches, every-motherfuckin&#x27;-body</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span><br/>(Live my life as a thug nigga until the day I die)<br/>(Live my life as a boss playa, &#x27;cause even gettin&#x27; high)<br/>I got bustas, hoes, and police watchin&#x27; a nigga, you know?<br/>(I live my life as a thug nigga until the day I die)<br/>(Livin&#x27; life as a boss playa, &#x27;cause even gettin&#x27; high)<br/>He he he, it&#x27;s like what they think<br/><a href=\"/12859897/2pac-all-eyez-on-me/Im-walkin-around-with-some-kis-in-my-pocket-or-somethin\" class=\"ReferentFragmentdesktop__ClickTarget-sc-110r0d9-0 cesxpW\"><span class=\"ReferentFragmentdesktop__Highlight-sc-110r0d9-1 jAzSMw\">I&#x27;m walkin&#x27; around with some ki&#x27;s in my pocket or somethin&#x27;</span></a><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span><span tabindex=\"0\" style=\"position:absolute;opacity:0;width:0;height:0;pointer-events:none;z-index:-1\"></span></span><br/>They think I&#x27;m goin&#x27; back to jail, they really on that dope<br/>(Live my life as a thug nigga until the day I die)<br/>(Live my life as a boss playa)<br/>I know y&#x27;all watchin&#x27;, I know y&#x27;all got me in the scopes<br/>(Live my life as a thug nigga until the day I die)<br/>(Live my life as a boss playa, &#x27;cause even gettin&#x27; high)<br/>I know y&#x27;all know this is Thug Life, baby!<br/>Y&#x27;all got me under surveillance, huh?<br/>All eyes on me, but I&#x27;m knowin&#x27;</div><div class=\"RightSidebar__Container-pajcl2-0 fuSJbt\"><div class=\"SidebarAd__Container-sc-1cw85h6-0 cFlBmm\"><div class=\"SidebarAd__StickyContainer-sc-1cw85h6-1 bZuAng\"><div class=\"DfpAd__Container-sc-1tnbv7f-0 kiNXoS\"><div id=\"div-gpt-ad-desktop_song_lyrics_sidebar4-desktop_song_lyrics_sidebar4-1\"></div><div class=\"DfpAd__Placeholder-sc-1tnbv7f-1 ilfajN\"></div></div></div></div></div><div class=\"LyricsFooter__Container-iqbcge-0 gBtUIQ\"><div class=\"ShareButtons__Root-jws18q-0 dyewdM LyricsFooter__FooterShareButtons-iqbcge-1 bfWZQI\"><div class=\"ShareButtons__Container-jws18q-1 ePvBqA\"><div class=\"ShareButtons__ButtonWrapper-jws18q-2 kdajqn\"><button aria-label=\"facebook\" class=\"react-share__ShareButton\" style=\"background-color:transparent;border:none;padding:0;font:inherit;color:inherit;cursor:pointer\"><div type=\"div\" class=\"Button__Container-rtu9rw-0 gexBFu ShareButton-a0stpn-0 gHSnUN\"><svg viewBox=\"0 0 9.95 20\"><path d=\"M8.09 3.81c-1.4 0-1.58.84-1.58 1.67v1.3h3.35L9.49 11h-3v9H2.33v-9H0V6.88h2.42V3.81C2.42 1.3 3.81 0 6.6 0H10v3.81z\"></path></svg></div></button></div><div class=\"ShareButtons__ButtonWrapper-jws18q-2 kdajqn\"><button aria-label=\"twitter\" class=\"react-share__ShareButton\" style=\"background-color:transparent;border:none;padding:0;font:inherit;color:inherit;cursor:pointer\"><div type=\"div\" class=\"Button__Container-rtu9rw-0 gexBFu ShareButton-a0stpn-0 gHSnUN\"><svg viewBox=\"0 0 20 16.43\"><path d=\"M20 1.89l-2.3 2.16v.68a12.28 12.28 0 0 1-3.65 8.92c-5 5.13-13.1 1.76-14.05.81 0 0 3.78.14 5.81-1.76A4.15 4.15 0 0 1 2.3 9.86h2S.81 9.05.81 5.81A11 11 0 0 0 3 6.35S-.14 4.05 1.49.95a11.73 11.73 0 0 0 8.37 4.19A3.69 3.69 0 0 1 13.51 0a3.19 3.19 0 0 1 2.57 1.08 12.53 12.53 0 0 0 3.24-.81l-1.75 1.89A10.46 10.46 0 0 0 20 1.89z\"></path></svg></div></button></div><div class=\"ShareButtons__ButtonWrapper-jws18q-2 kdajqn\"><div type=\"div\" class=\"Button__Container-rtu9rw-0 gexBFu ShareButton-a0stpn-0 gHSnUN\"><div class=\"Pyong__Container-yq95kq-0 eMjKRh\"><div class=\"Tooltip__Container-sc-1uvy5c2-0 cRrFdP\"><div class=\"Tooltip__Children-sc-1uvy5c2-2 dvOJud\"><button class=\"LabelWithIcon__Container-hjli77-0 bSorfX\" height=\".75rem\"><svg viewBox=\"0 0 11.37 22\"><path d=\"M0 7l6.16-7 3.3 7H6.89S5.5 12.1 5.5 12.17h5.87L6.09 22l.66-7H.88l2.89-8z\"></path></svg><span class=\"LabelWithIcon__Label-hjli77-1 jwfWMJ\">111</span></button></div></div></div></div></div><div class=\"ShareButtons__ButtonWrapper-jws18q-2 kdajqn\"><div type=\"div\" class=\"Button__Container-rtu9rw-0 gexBFu ShareButton-a0stpn-0 gHSnUN\">Embed</div></div></div></div></div></div></div><form class=\"PageGriddesktop-a6v82w-0 SongPageGriddesktop-sc-1px5b71-0 jecoie LyricsEditdesktop__Container-sc-19lxrhp-0 lmbLpy\"><div class=\"LyricsEditdesktop__Editor-sc-19lxrhp-1 cOOPzk\"></div><div class=\"LyricsEditdesktop__ControlsContainer-sc-19lxrhp-2 hGRoZE\"><div class=\"LyricsEditdesktop__Controls-sc-19lxrhp-3 cEVdKO\"><button class=\"Button__Container-rtu9rw-0 gexBFu LyricsEditdesktop__Button-sc-19lxrhp-4 kpOoZB\" type=\"button\">Cancel</button><div class=\"LyricsEditExplainer__Container-sc-1aeph76-0 hXQMRu LyricsEditdesktop__Explainer-sc-19lxrhp-6 LNnRX\"><p>How to Format Lyrics:</p><ul><li>Type out all lyrics, even repeating song parts like the chorus</li><li>Lyrics should be broken down into individual lines</li><li>Use section headers above different song parts like [Verse], [Chorus], etc.</li><li>Use <span class=\"LyricsEditExplainer__Italic-sc-1aeph76-2 rncXA\">italics</span> (<!-- -->&lt;i&gt;lyric&lt;/i&gt;<!-- -->) and <span class=\"LyricsEditExplainer__Bold-sc-1aeph76-1 eDBQeK\">bold</span> (<!-- -->&lt;b&gt;lyric&lt;/b&gt;<!-- -->) to distinguish between different vocalists in the same song part</li><li>If you don’t understand a lyric, use [?]</li></ul><p>To learn more, check out our <a href=\"https://genius.com/Genius-how-to-add-songs-to-genius-annotated\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 ietQTa\">transcription guide</a> or visit our <a href=\"https://genius.com/transcribers\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 ietQTa\">transcribers forum</a></p></div></div></div></form></div><div class=\"SectionLeaderboard__Container-sc-1pjk0bw-0 cSKAwQ\"><div class=\"SectionLeaderboard__Center-sc-1pjk0bw-1 fPpEQG\"><div class=\"DfpAd__Container-sc-1tnbv7f-0 dTXQYT\"><div id=\"div-gpt-ad-desktop_song_about_leaderboard-desktop_song_about_leaderboard-1\"></div><div class=\"DfpAd__Placeholder-sc-1tnbv7f-1 bIwkeM\"></div></div></div></div></div><div class=\"SectionScrollSentinel__Container-eoe1bv-0 icvVds\"><div class=\"SectionScrollSentinel__Element-eoe1bv-1 gTVDnZ\"></div><div id=\"about\" class=\"SectionScrollSentinel__SectionAnchor-eoe1bv-2 difnmr\"></div><div class=\"PageGriddesktop-a6v82w-0 SongPageGriddesktop-sc-1px5b71-0 jecoie About__Grid-ut4i9m-0 jdSpXa\"><div class=\"About__Container-ut4i9m-1 eSiFpi\"><h1 font-size=\"xxLargeHeadline\" class=\"About__Title-ut4i9m-2 kcXwIY\">About</h1><div class=\"SongDescription__Container-sc-615rvk-0 hYPlhL\"><div class=\"Attribution__Container-sc-1nmry9o-0 dhIhSa\"><div class=\"Attribution__Header-sc-1nmry9o-1 iPNsXE\"><span>Genius Annotation</span><button text-decoration=\"underline\" type=\"button\" class=\"TextButton-sc-192nsqv-0 ixMmYX\">5 contributors</button></div></div><div class=\"ExpandableContent__Container-sc-1165iv-0 ikywhQ\"><div class=\"ExpandableContent__Content-sc-1165iv-4 huhsMa\"><div class=\"SongDescription__Content-sc-615rvk-2 kRzyD\"><div class=\"RichText__Container-oz284w-0 EZWbQ\"><p>The title track off of 2Pac’s album <em>All Eyez on Me</em> samples Linda Clifford’s “Never Gonna Stop” (also used for Nas' <a href=\"https://genius.com/Nas-street-dreams-lyrics\" rel=\"noopener\" data-api_path=\"/songs/979\">“Street Dreams,”</a> which was released a few months later in July 1996). Producer Johnny J <a href=\"https://2paclegacy.net/the-making-of-tupacs-all-eyez-on-me-xxl/\" rel=\"noopener nofollow\">recalls connecting with 2Pac for this track</a>:</p>\n\n<blockquote><p>That was the very first track I laid when we got together at Death Row. When he just got out of jail, just got released, two days later he’s like, “‘J’, get to the studio, I’m with Death Row now.” I assumed it was a joke, somebody perpetrating Tupac. I’m like “Hell no – ‘Pac is locked up!” He’s like “J, I’m out” I walk in, 15 minutes into the session, the first beat I put in the drum machine is “All Eyez On Me.” I wasn’t going to show him the track, honestly. I was like, “This track? Nah, it’s not finished. It’s incomplete.” My wife says, “Hey, it’s a dope beat!” So I just pop it in, titles just come right off his fuckin’ head.</p></blockquote>\n\n<p>This classic gives us an idea of what the media was doing with Pac’s life. At this moment, all eyes in the music world were on him due to the intrigue around his release from prison, signing with the notorious Death Row Records, as well as the 2Pac/Death Row/West Coast vs. Biggie/Bad Boy/East Coast beef.</p></div></div></div><div class=\"ExpandableContent__ButtonContainer-sc-1165iv-3 jzPNvv\"><button class=\"Button__Container-rtu9rw-0 efSZT ExpandableContent__Button-sc-1165iv-1 gBcitk\" type=\"button\">Expand<!-- --> <span class=\"InlineSvg__Wrapper-sc-1342j7p-0 blSbzj\"><svg viewBox=\"0 0 6.6 16\"><path d=\"M1.6 8.8l.6-.6 1 1 .5.7V6H0v-.8h4.5v4.6l.5-.6 1-1 .6.5L4 11.3 1.6 8.8z\"></path></svg></span></button></div></div><div class=\"AnnotationActions__Container-sc-1597gb-0 tqfBP SongDescription__AnnotationActions-sc-615rvk-7 bEHmko\"><div class=\"VotingActions__Container-sc-1w1brao-0 eQBXrC\"><svg viewBox=\"0 0 21.62 21.36\" aria-label=\"upvote\" aria-pressed=\"false\" class=\"VotingActions__ThumbsUp-sc-1w1brao-3 hEQpIA\"><path d=\"M16.52 21.29H6V8.5l.84-.13a3.45 3.45 0 0 0 1.82-1.09 13.16 13.16 0 0 0 .82-1.85c1.06-2.69 2-4.78 3.52-5.31a2.06 2.06 0 0 1 1.74.17c2.5 1.42 1 5 .16 6.95-.11.27-.25.6-.31.77a.78.78 0 0 0 .6.36h4.1a2.29 2.29 0 0 1 2.37 2.37c0 .82-1.59 5.4-2.92 9.09a2.39 2.39 0 0 1-2.22 1.46zm-8.52-2h8.56a.48.48 0 0 0 .31-.17c1.31-3.65 2.73-7.82 2.79-8.44 0-.22-.1-.32-.37-.32h-4.1A2.61 2.61 0 0 1 12.54 8 4.29 4.29 0 0 1 13 6.46c.45-1.06 1.64-3.89.7-4.43-.52 0-1.3 1.4-2.38 4.14a10 10 0 0 1-1.13 2.38A5.28 5.28 0 0 1 8 10.11zM0 8.4h4.86v12.96H0z\"></path></svg><button class=\"VotingActions__Button-sc-1w1brao-1 VotingActions__VotingModalButton-sc-1w1brao-2 hPgYeJ\">+184</button><svg viewBox=\"0 0 21.62 21.36\" aria-label=\"downvote\" aria-pressed=\"false\" class=\"VotingActions__ThumbsDown-sc-1w1brao-4 hKZXIk\"><path d=\"M8 21.36a2.12 2.12 0 0 1-1.06-.29c-2.5-1.42-1-5-.16-6.95.11-.27.25-.6.31-.77a.78.78 0 0 0-.6-.36H2.37A2.29 2.29 0 0 1 0 10.64c0-.82 1.59-5.4 2.92-9.09A2.39 2.39 0 0 1 5.1.07h10.56v12.79l-.84.13A3.45 3.45 0 0 0 13 14.08a13.16 13.16 0 0 0-.82 1.85c-1.06 2.69-2 4.79-3.49 5.31a2.06 2.06 0 0 1-.69.12zM5.1 2.07a.48.48 0 0 0-.31.17C3.48 5.89 2.07 10.06 2 10.68c0 .22.1.32.37.32h4.1a2.61 2.61 0 0 1 2.61 2.4 4.29 4.29 0 0 1-.48 1.51c-.46 1.09-1.65 3.89-.7 4.42.52 0 1.3-1.4 2.38-4.14a10 10 0 0 1 1.13-2.38 5.27 5.27 0 0 1 2.25-1.56V2.07zM16.76 0h4.86v12.96h-4.86z\"></path></svg></div><div class=\"AnnotationActions__FlexSpace-sc-1597gb-1 kljthJ\"></div><button class=\"LabelWithIcon__Container-hjli77-0 gcYcIR\" height=\"1em\"><svg viewBox=\"0 0 21.2 22\"><path d=\"M19.29 1.91v11.46H7.69l-.57.7L5 16.64v-3.27H1.91V1.91h17.38M21.2 0H0v15.28h3.12V22l5.48-6.72h12.6V0z\"></path><path d=\"M4.14 4.29h12.93V6.2H4.14zm0 4.09h12.93v1.91H4.14z\"></path></svg><span class=\"LabelWithIcon__Label-hjli77-1 jwfWMJ\">2</span></button><div class=\"Pyong__Container-yq95kq-0 eMjKRh AnnotationActions__Pyong-sc-1597gb-2 jQIgzS\"><div class=\"Tooltip__Container-sc-1uvy5c2-0 cRrFdP\"><div class=\"Tooltip__Children-sc-1uvy5c2-2 dvOJud\"><button class=\"LabelWithIcon__Container-hjli77-0 gcYcIR\" height=\"1em\"><svg viewBox=\"0 0 11.37 22\"><path d=\"M0 7l6.16-7 3.3 7H6.89S5.5 12.1 5.5 12.17h5.87L6.09 22l.66-7H.88l2.89-8z\"></path></svg></button></div></div></div><button class=\"LabelWithIcon__Container-hjli77-0 gcYcIR\" height=\"1em\"><svg viewBox=\"0 0 17.94 22\"><path d=\"M16.03 7.39v12.7H1.91V7.39H0V22h17.94V7.39h-1.91\"></path><path d=\"M8.08 3.7v11.81h1.91V3.63l2.99 2.98 1.35-1.35L9.07 0 3.61 5.46l1.36 1.35L8.08 3.7\"></path></svg><span class=\"LabelWithIcon__Label-hjli77-1 jwfWMJ\">Share</span></button></div></div><div class=\"SectionScrollSentinel__Container-eoe1bv-0 icvVds\"><div class=\"SectionScrollSentinel__Element-eoe1bv-1 gTVDnZ\"></div><div id=\"questions\" class=\"SectionScrollSentinel__SectionAnchor-eoe1bv-2 difnmr\"></div><div class=\"InnerSectionDivider-sc-1x4onqw-0 iyrTpw\"></div><div class=\"QuestionList__Container-sc-1a58vti-0 dbVnsR\"><div class=\"QuestionList__Header-sc-1a58vti-1 cTVsHz\"><h2 font-size=\"xxLargeHeadline\" class=\"QuestionList__Title-sc-1a58vti-2 dssCKz\">Q&amp;A</h2><p class=\"QuestionList__SubTitle-sc-1a58vti-3 dZHUCL\">Find answers to frequently asked questions about the song and explore its deeper meaning</p></div><div class=\"QuestionFormControls__Buttons-sc-79jhlr-1 dxxZLD\"><button type=\"button\" class=\"Button__Container-rtu9rw-0 efSZT\">Ask a question</button></div><div class=\"QuestionList__Table-sc-1a58vti-5 geLHRn\"><div id=\"artist_comment:song:6576\" class=\"QuestionTemplate__Container-sc-1is5n1p-1 cgTISK\"><div class=\"QuestionTemplate__Title-sc-1is5n1p-4 fRjQxS\"><span>What did 2Pac say about &quot;All Eyez On Me&quot;?</span><button class=\"TextButton-sc-192nsqv-0 hVAZmF QuestionTemplate__ExpandButton-sc-1is5n1p-5 ibRgSN\" type=\"button\"><svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M0 10h24v4h-24z\"></path></svg></button></div><div class=\"Answer__Container-sc-2neq6u-0 iHvEbL\"><div class=\"Attribution__Container-sc-1nmry9o-0 dhIhSa\"><div class=\"Attribution__Header-sc-1nmry9o-1 iPNsXE\"><span><div class=\"Answer__AttributionLabel-sc-2neq6u-3 doQBGH\">Genius Answer</div></span><button text-decoration=\"underline\" type=\"button\" class=\"TextButton-sc-192nsqv-0 ixMmYX\">2 contributors</button></div></div><div class=\"Answer__Body-sc-2neq6u-1 fzIWba\"><div class=\"RichText__Container-oz284w-0 EZWbQ\"><p>Kendrick on the Impact of Tupacs All Eyez on Me and California Love Video Shoot</p>\n\n<p> “I was 8 yrs old when I first saw you. I couldn’t describe how I felt at that moment. So many emotions. Full of excitement. Full of joy and eagerness. 20 yrs later I understand exactly what that feeling was. INSPIRED,” Lamar wrote (via Pitchfork). “The people that you touched on that small intersection changed lives forever. I told myself I wanted to be a voice for man one day. Whoever knew I was speaking out loud for u to listen. Thank you, K.L.”</p>\n\n<p>As Lamar told Rolling Stone, he was eight years old when he sat atop his father’s shoulder and witnessed Tupac and Dr. Dre film the video for <a href=\"https://genius.com/2pac-california-love-lyrics\" rel=\"noopener\" data-api_path=\"/songs/244\">“California Love”</a> at the Compton Swap Meet.  “I want to say they were in a white Bentley,” Lamar said. “These motorcycle cops trying to conduct traffic but one almost scraped the car, and Pac stood up on the passenger seat, like, ‘Yo, what the fuck!’ Yelling at the police, just like on his motherfucking songs. He gave us what we wanted.” Lamar would later shoot a scene at that same swap meet for his <a href=\"https://genius.com/Kendrick-lamar-king-kunta-lyrics\" rel=\"noopener\" data-api_path=\"/songs/721659\">“King Kunta”</a> music video, a nod to 2Pac.</p></div></div></div><div class=\"QuestionAnswerActions__Container-sc-6ne2ji-0 dTXIVU\"></div></div><div class=\"MetadataQuestionTemplate__Container-sc-3cdvbz-0 eDiRSs\"><a href=\"/2pac-all-eyez-on-me-lyrics/q/producer\" class=\"MetadataQuestionTemplate__Title-sc-3cdvbz-1 emVCuo\"><span class=\"MetadataQuestionTemplate__QuestionBody-sc-3cdvbz-3 iLvtam\">Who produced “All Eyez On Me” by 2Pac?</span><button class=\"TextButton-sc-192nsqv-0 hVAZmF MetadataQuestionTemplate__ExpandButton-sc-3cdvbz-2 jTUclM\" type=\"button\"><svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M24 10h-10v-10h-4v10h-10v4h10v10h4v-10h10z\"></path></svg></button></a></div><div class=\"MetadataQuestionTemplate__Container-sc-3cdvbz-0 eDiRSs\"><a href=\"/2pac-all-eyez-on-me-lyrics/q/release-date\" class=\"MetadataQuestionTemplate__Title-sc-3cdvbz-1 emVCuo\"><span class=\"MetadataQuestionTemplate__QuestionBody-sc-3cdvbz-3 iLvtam\">When did 2Pac release “All Eyez On Me”?</span><button class=\"TextButton-sc-192nsqv-0 hVAZmF MetadataQuestionTemplate__ExpandButton-sc-3cdvbz-2 jTUclM\" type=\"button\"><svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M24 10h-10v-10h-4v10h-10v4h10v10h4v-10h10z\"></path></svg></button></a></div><div class=\"MetadataQuestionTemplate__Container-sc-3cdvbz-0 eDiRSs\"><a href=\"/2pac-all-eyez-on-me-lyrics/q/writer\" class=\"MetadataQuestionTemplate__Title-sc-3cdvbz-1 emVCuo\"><span class=\"MetadataQuestionTemplate__QuestionBody-sc-3cdvbz-3 iLvtam\">Who wrote “All Eyez On Me” by 2Pac?</span><button class=\"TextButton-sc-192nsqv-0 hVAZmF MetadataQuestionTemplate__ExpandButton-sc-3cdvbz-2 jTUclM\" type=\"button\"><svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\"><path d=\"M24 10h-10v-10h-4v10h-10v4h10v10h4v-10h10z\"></path></svg></button></a></div></div></div></div><div class=\"InnerSectionDivider-sc-1x4onqw-0 iyrTpw\"></div><div class=\"PrimaryAlbum__Container-cuci8p-0 kqeVkf\"><div id=\"primary-album\" class=\"PrimaryAlbum__SectionAnchor-cuci8p-1 lkEvgY\"></div><div class=\"PrimaryAlbum__CoverArt-cuci8p-2 fMIxkO\"><div role=\"img\" class=\"SizedImage__Container-sc-1hyeaua-0 FnKAk\"><noscript><img src=\"https://images.genius.com/51289a294de3498eb6e1ff4e3a769c28.1000x1000x1.png\" class=\"SizedImage__NoScript-sc-1hyeaua-2 UJCmI\"/></noscript></div></div><div class=\"PrimaryAlbum__AlbumDetails-cuci8p-3 hGGOcK\"><a href=\"https://genius.com/albums/2pac/All-eyez-on-me\" class=\"PrimaryAlbum__Title-cuci8p-4 NcWGs\">All Eyez On Me<!-- --> <!-- -->(1996)</a><div class=\"PrimaryAlbum__Artist-cuci8p-5 hyzSGh\"><a href=\"https://genius.com/artists/2pac\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">2Pac</a></div></div><ol class=\"AlbumTracklist__Container-sc-123giuo-0 kGJQLs\"><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">1. </div><a href=\"https://genius.com/2pac-ambitionz-az-a-ridah-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Ambitionz Az a Ridah</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">2. </div><a href=\"https://genius.com/2pac-all-about-u-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">All About U</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">3. </div><a href=\"https://genius.com/2pac-skandalouz-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Skandalouz</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">4. </div><a href=\"https://genius.com/2pac-got-my-mind-made-up-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Got My Mind Made Up</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">5. </div><a href=\"https://genius.com/2pac-how-do-u-want-it-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">How Do U Want It</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">6. </div><a href=\"https://genius.com/2pac-2-of-amerikaz-most-wanted-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">2 of Amerikaz Most Wanted</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">7. </div><a href=\"https://genius.com/2pac-no-more-pain-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">No More Pain</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">8. </div><a href=\"https://genius.com/2pac-heartz-of-men-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Heartz of Men</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">9. </div><a href=\"https://genius.com/2pac-life-goes-on-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Life Goes On</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">10. </div><a href=\"https://genius.com/2pac-only-god-can-judge-me-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Only God Can Judge Me</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">11. </div><a href=\"https://genius.com/2pac-tradin-war-stories-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Tradin’ War Stories</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">12. </div><a href=\"https://genius.com/2pac-california-love-remix-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">California Love (Remix)</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">13. </div><a href=\"https://genius.com/2pac-i-aint-mad-at-cha-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">I Ain’t Mad At Cha</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">14. </div><a href=\"https://genius.com/2pac-whatz-ya-phone-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">What’z Ya Phone #</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">15. </div><a href=\"https://genius.com/2pac-cant-c-me-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Can’t C Me</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">16. </div><a href=\"https://genius.com/2pac-shorty-wanna-be-a-thug-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Shorty Wanna Be a Thug</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">17. </div><a href=\"https://genius.com/2pac-holla-at-me-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Holla At Me</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">18. </div><a href=\"https://genius.com/2pac-wonda-why-they-call-u-bitch-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Wonda Why They Call U Bitch</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">19. </div><a href=\"https://genius.com/2pac-when-we-ride-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">When We Ride</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">20. </div><a href=\"https://genius.com/2pac-thug-passion-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Thug Passion</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">21. </div><a href=\"https://genius.com/2pac-picture-me-rollin-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Picture Me Rollin’</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">22. </div><a href=\"https://genius.com/2pac-check-out-time-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Check Out Time</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">23. </div><a href=\"https://genius.com/2pac-ratha-be-ya-nigga-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Ratha Be Ya Nigga</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 guEaas\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">24. </div>All Eyez On Me</div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">25. </div><a href=\"https://genius.com/2pac-run-tha-streetz-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Run Tha Streetz</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">26. </div><a href=\"https://genius.com/2pac-aint-hard-2-find-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Ain’t Hard 2 Find</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">27. </div><a href=\"https://genius.com/2pac-heaven-aint-hard-2-find-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Heaven Ain’t Hard 2 Find</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">28. </div><a href=\"https://genius.com/2pac-california-love-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">California Love</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><a href=\"https://genius.com/2pac-how-do-u-want-it-original-version-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">How Do U Want It (Original Version)</a></div></li></ol><div class=\"OtherAlbums__ButtonContainer-sc-2n6wdo-1 ekePUw\"><button type=\"button\" class=\"Button__Container-rtu9rw-0 efSZT\">Expand<!-- --> <span class=\"InlineSvg__Wrapper-sc-1342j7p-0 blSbzj\"><svg viewBox=\"0 0 6.6 16\"><path d=\"M1.6 8.8l.6-.6 1 1 .5.7V6H0v-.8h4.5v4.6l.5-.6 1-1 .6.5L4 11.3 1.6 8.8z\"></path></svg></span></button></div></div><div class=\"InnerSectionDivider-sc-1x4onqw-0 iyrTpw\"></div><div class=\"ExpandableContent__Container-sc-1165iv-0 ikywhQ\"><div class=\"ExpandableContent__Content-sc-1165iv-4 huhsMa\"><div class=\"SongInfo__Container-nekw6x-0 kJtCmu\"><div id=\"song-info\" class=\"SongInfo__SectionAnchor-nekw6x-5 lopKUj\"></div><div class=\"SongInfo__Title-nekw6x-1 iRKrFW\">Credits</div><div class=\"SongInfo__Columns-nekw6x-2 dWcYSx\"><div class=\"SongInfo__Credit-nekw6x-3 fognin\"><div class=\"SongInfo__Label-nekw6x-4 kOJa-dB\">Featuring</div><div><span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Big-syke\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Big Syke</a></span></span></div></div><div class=\"SongInfo__Credit-nekw6x-3 fognin\"><div class=\"SongInfo__Label-nekw6x-4 kOJa-dB\">Producer</div><div><span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Johnny-j\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Johnny J</a></span></span></div></div><div class=\"SongInfo__Credit-nekw6x-3 fognin\"><div class=\"SongInfo__Label-nekw6x-4 kOJa-dB\">Writers</div><div><span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Big-syke\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Big Syke</a></span></span>, <span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/2pac\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">2Pac</a></span></span>, <span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Johnny-j\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Johnny J</a></span></span>, <span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Jp-pennington\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">J.P. Pennington</a></span></span>, <span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Thor-baldursson\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Thor Baldursson</a></span></span> &amp; <span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Jurgen-koppers\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Jürgen Koppers</a></span></span></div></div><div class=\"SongInfo__Credit-nekw6x-3 fognin\"><div class=\"SongInfo__Label-nekw6x-4 kOJa-dB\">Publisher</div><div><span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Joshuas-dream\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Joshua’s Dream</a></span></span>, <span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Interscope-pearl-music\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Interscope Pearl Music</a></span></span>, <span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Warner-tamerlane-publishing-corp\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Warner-Tamerlane Publishing Corp.</a></span></span>, <span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Black-hispanic-music\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Black/Hispanic Music</a></span></span> &amp; <span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Careers-bmg-music-publishing\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Careers-BMG Music Publishing</a></span></span></div></div><div class=\"SongInfo__Credit-nekw6x-3 fognin\"><div class=\"SongInfo__Label-nekw6x-4 kOJa-dB\">Copyright ©</div><div><span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Death-row-records\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Death Row Records</a></span></span> &amp; <span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Interscope-records\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Interscope Records</a></span></span></div></div><div class=\"SongInfo__Credit-nekw6x-3 fognin\"><div class=\"SongInfo__Label-nekw6x-4 kOJa-dB\">Phonographic Copyright ℗</div><div><span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Death-row-records\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Death Row Records</a></span></span> &amp; <span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Interscope-records\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Interscope Records</a></span></span></div></div><div class=\"SongInfo__Credit-nekw6x-3 fognin\"><div class=\"SongInfo__Label-nekw6x-4 kOJa-dB\">Label</div><div><span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Interscope-records\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Interscope Records</a></span></span> &amp; <span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Death-row-records\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Death Row Records</a></span></span></div></div><div class=\"SongInfo__Credit-nekw6x-3 fognin\"><div class=\"SongInfo__Label-nekw6x-4 kOJa-dB\">Mixing Engineer</div><div><span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Dave-aron\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Dave Aron</a></span></span> &amp; <span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Johnny-j\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Johnny J</a></span></span></div></div><div class=\"SongInfo__Credit-nekw6x-3 fognin\"><div class=\"SongInfo__Label-nekw6x-4 kOJa-dB\">Engineer</div><div><span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Rick-clifford\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Rick Clifford</a></span></span></div></div><div class=\"SongInfo__Credit-nekw6x-3 fognin\"><div class=\"SongInfo__Label-nekw6x-4 kOJa-dB\">Assistant Engineer</div><div><span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Alvin-mcgill\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Alvin McGill</a></span></span></div></div><div class=\"SongInfo__Credit-nekw6x-3 fognin\"><div class=\"SongInfo__Label-nekw6x-4 kOJa-dB\">Mastering Engineer</div><div><span class=\"PortalTooltip__Container-yc1x8c-0 bOCNdp\"><span class=\"PortalTooltip__Trigger-yc1x8c-1 ekJBqv\"><a href=\"https://genius.com/artists/Brian-gardner\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Brian Gardner</a></span></span></div></div><div class=\"SongInfo__Credit-nekw6x-3 fognin\"><div class=\"SongInfo__Label-nekw6x-4 kOJa-dB\">Recorded At</div><div>Can-Am Studios (Tarzana, CA)</div></div><div class=\"SongInfo__Credit-nekw6x-3 fognin\"><div class=\"SongInfo__Label-nekw6x-4 kOJa-dB\">Release Date</div><div>February 13, 1996</div></div><div class=\"SongInfo__Credit-nekw6x-3 fognin\"><a href=\"https://genius.com/2pac-all-eyez-on-me-sample/samples\" class=\"SongInfo__Label-nekw6x-4 kOJa-dB\">All Eyez On Me Samples</a><div><a href=\"https://genius.com/Linda-clifford-never-gonna-stop-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Never Gonna Stop by Linda Clifford</a></div></div><div class=\"SongInfo__Credit-nekw6x-3 fognin\"><a href=\"https://genius.com/2pac-all-eyez-on-me-sample/samples\" class=\"SongInfo__Label-nekw6x-4 kOJa-dB\">Songs That Sample All Eyez On Me</a><div><a href=\"https://genius.com/Tupac-thug-theory-2pac-ft-ice-cube-gangsta-rap-made-me-do-it-ft-eminem-eazy-e-biggie-snoop-dogg-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">2Pac ft. Ice Cube - Gangsta Rap Made Me Do It (ft. Eminem, Eazy E, Biggie, Snoop Dogg) by Tupac Thug Theory (Ft. 2Pac, Eazy-E, Eminem, Ice Cube, MC Ren, The Notorious B.I.G. &amp; Snoop Dogg)</a>, <a href=\"https://genius.com/Young-dre-the-truth-all-eyez-on-me-the-truth-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">All Eyez On Me (The Truth) by Young Dre The Truth (Ft. 2Pac &amp; BJ the Chicago Kid)</a>, <a href=\"https://genius.com/Bonez-mc-and-gzuz-intro-high-and-hungrig-3-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Intro (High &amp; Hungrig 3) by Bonez MC &amp; Gzuz</a>, <a href=\"https://genius.com/Bonez-mc-gzuz-and-maxwell-das-ist-gang-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Das ist Gang by Bonez MC, Gzuz &amp; Maxwell</a>, <a href=\"https://genius.com/101barz-sepa-wintersessie-2018-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Sepa - Wintersessie 2018 by 101Barz (Ft. Sepa)</a>, <a href=\"https://genius.com/Jari-frisco-shit-freestyle-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Frisco Shit Freestyle by Jari$</a>, <a href=\"https://genius.com/Tory-lanez-florida-shit-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Florida Shit by Tory Lanez</a>, <a href=\"https://genius.com/King-keil-leben-im-beton-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Leben im Beton by King Keil</a>, <a href=\"https://genius.com/18-karat-all-eyez-on-me-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">ALL EYEZ ON ME by 18 Karat</a>, <a href=\"https://genius.com/Lil-shrimp-all-eyez-on-me-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">​all eyez on me by Lil Shrimp</a>, <a href=\"https://genius.com/Kage-dq-na-mnie-20-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Na mnie 2.0 by Kage (dq)</a>, <a href=\"https://genius.com/Valkirin-fuck-traitors-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Нахуй предателей (Fuck traitors) by Валькирин (Valkirin)</a>, <a href=\"https://genius.com/Bonez-mc-and-gzuz-grabstein-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Grabstein by Bonez MC &amp; Gzuz</a>, <a href=\"https://genius.com/Lexpair-zermi-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Zermi by Lexpair</a>, <a href=\"https://genius.com/Lexpair-interlude-silence-radio-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Interlude (Silence Radio) by Lexpair</a>, <a href=\"https://genius.com/Dellafuente-corazon-mio-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Corazón Mío by DELLAFUENTE</a>, <a href=\"https://genius.com/Kxng-crooked-i-think-im-big-syke-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">I Think I&#x27;m Big Syke by KXNG Crooked &amp; KXNG Crooked</a>, <a href=\"https://genius.com/Tantrum-ta-outlawz-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Outlawz by Tantrum T.A. (Ft. Sleepy West)</a>, <a href=\"https://genius.com/Dima-bamberg-on-your-knees-mortes-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">На колени, мортес (On your knees, mortes) by дима бамберг (dima bamberg)</a>, <a href=\"https://genius.com/Jul-all-eyez-on-me-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">All Eyez on Me by JuL (Ft. DJ Kayz)</a>, <a href=\"https://genius.com/Crooked-i-all-eyez-on-me-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">All Eyez On Me by Crooked I</a>, <a href=\"https://genius.com/Ti-my-life-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">My Life by T.I. (Ft. Daz Dillinger)</a>, <a href=\"https://genius.com/Joe-street-dreams-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Street Dreams by Joe</a> &amp; <a href=\"https://genius.com/Meek-mill-all-eyes-on-you-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">All Eyes On You by Meek Mill (Ft. Chris Brown &amp; Nicki Minaj)</a></div></div><div class=\"SongInfo__Credit-nekw6x-3 fognin\"><a href=\"https://genius.com/2pac-all-eyez-on-me-sample/interpolations\" class=\"SongInfo__Label-nekw6x-4 kOJa-dB\">Songs That Interpolate All Eyez On Me</a><div><a href=\"https://genius.com/Pharaoh-full-clip-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Фул клип (Full Clip) by PHARAOH</a>, <a href=\"https://genius.com/Kxng-crooked-i-think-im-big-syke-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">I Think I&#x27;m Big Syke by KXNG Crooked &amp; KXNG Crooked</a>, <a href=\"https://genius.com/Teuterekordz-ipod-classic-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">​iPod Classic by Teuterekordz (Ft. Beko101, Dispo (Teuterekordz) &amp; Lucky (Teuterekordz))</a>, <a href=\"https://genius.com/Chief-keef-all-eyes-on-me-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">All Eyes On Me by Chief Keef</a>, <a href=\"https://genius.com/Gucci-mane-pick-up-the-pieces-outro-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Pick Up the Pieces (Outro) by Gucci Mane</a>, <a href=\"https://genius.com/Pr-sad-r6-and-dopesmoke-punch-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">PUNCH by PR SAD, R6 &amp; dopesmoke</a>, <a href=\"https://genius.com/Snoop-dogg-straight-ballin-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Straight Ballin&#x27; by Snoop Dogg</a>, <a href=\"https://genius.com/Kayspinit-task-force-x-intro-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Task Force X (Intro) by KaySpinIt</a>, <a href=\"https://genius.com/Yhung-to-paranoia-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Paranoia by Yhung T.O.</a>, <a href=\"https://genius.com/Verde-babii-and-armani-depaul-all-eyes-on-me-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">All Eyes On Me by Verde Babii &amp; Armani Depaul</a>, <a href=\"https://genius.com/Sus-caught-inda-rain-2-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Caught Inda Rain 2 by Sus (Ft. Broadday, Lil Dotz, Malty 2BZ, RondoMontana, Strika &amp; Workrate)</a>, <a href=\"https://genius.com/Fumez-the-engineer-and-hazey-hazey-x-fumez-the-engineer-plugged-in-pt-1-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Hazey x Fumez the Engineer - Plugged In, Pt. 1 by Fumez The Engineer &amp; Hazey</a>, <a href=\"https://genius.com/Chief-keef-me-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Me by Chief Keef (Ft. Tadoe)</a>, <a href=\"https://genius.com/Francuz-mordo-all-eyez-on-me-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">All eyez on me by Francuz Mordo</a>, <a href=\"https://genius.com/Al-doms-bff-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">BFF by Al-Doms</a>, <a href=\"https://genius.com/Freddie-gibbs-rearview-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Rearview by Freddie Gibbs</a>, <a href=\"https://genius.com/Alonestar-hands-high-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Hands High by ALONESTAR (Ft. Ed Sheeran)</a>, <a href=\"https://genius.com/50-cent-be-a-gentleman-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Be A Gentleman by 50 Cent</a>, <a href=\"https://genius.com/Mc-ridin-rapper-destiny-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Destiny by MC Ridin (Rapper)</a>, <a href=\"https://genius.com/Bright-campa-broke-bitches-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Broke Bitches by Bright Campa</a>, <a href=\"https://genius.com/Darkside-e-all-eyez-on-e-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">All Eyez On E by Darkside E</a>, <a href=\"https://genius.com/Lil-wayne-start-this-shit-off-right-2014-version-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Start This Shit Off Right (2014 Version) by Lil Wayne (Ft. Christina Milian &amp; Mannie Fresh)</a>, <a href=\"https://genius.com/Arma-blanca-el-musicologo-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">El Musicólogo by Arma Blanca (Ft. Nach)</a>, <a href=\"https://genius.com/2pac-syke-interlude-t2001-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Syke Interlude T2001 by 2Pac (Ft. Big Syke)</a>, <a href=\"https://genius.com/Lil-wayne-start-this-shit-off-right-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Start This Shit Off Right by Lil Wayne (Ft. Ashanti &amp; Mack Maine)</a> &amp; <a href=\"https://genius.com/Royce-da-59-airplanes-freestyle-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Airplanes (Freestyle) by Royce Da 5&#x27;9&quot;</a></div></div><div class=\"SongInfo__Credit-nekw6x-3 fognin\"><div class=\"SongInfo__Label-nekw6x-4 kOJa-dB\">All Eyez On Me Covers</div><div><a href=\"https://genius.com/Tsu-surf-tsu-surf-funk-flex-freestyle021-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Tsu Surf | Funk Flex | #Freestyle021 by Tsu Surf</a> &amp; <a href=\"https://genius.com/Hichkas-cheshma-roo-man-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Cheshma Roo Man by Hichkas (Ft. Salome Mc)</a></div></div><div class=\"SongInfo__Credit-nekw6x-3 fognin\"><a href=\"https://genius.com/2pac-all-eyez-on-me-sample/remixes\" class=\"SongInfo__Label-nekw6x-4 kOJa-dB\">All Eyez On Me Remixes</a><div><a href=\"https://genius.com/Imanbek-all-eyez-on-me-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">All Eyez On Me by Imanbek</a>, <a href=\"https://genius.com/Dark-boy-other-position-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Other Position by Dark Boy</a>, <a href=\"https://genius.com/Dj-belite-all-eyes-on-me-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">All Eyes on Me by Dj Belite</a>, <a href=\"https://genius.com/Mister-you-freestyle-you-2021-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Freestyle You 2021 by Mister You</a>, <a href=\"https://genius.com/Kage-dq-na-mnie-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Na mnie by Kage (dq)</a>, <a href=\"https://genius.com/Spider-loc-all-eyez-on-us-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">All Eyez On Us by Spider Loc (Ft. Big Syke)</a>, <a href=\"https://genius.com/Hugo-toxxx-vsechny-oci-na-mne-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">Všechny oči na mně by Hugo Toxxx</a>, <a href=\"https://genius.com/Muslim-all-eyes-on-me-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">All Eyes On Me by Muslim - مسلم</a>, <a href=\"https://genius.com/2pac-all-eyez-on-me-nu-mixx-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">All Eyez On Me (Nu-Mixx) by 2Pac (Ft. Big Syke)</a>, <a href=\"https://genius.com/Dax-all-eyez-on-me-remix-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">All Eyez On Me (Remix) by Dax</a> &amp; <a href=\"https://genius.com/Still-fresh-cest-la-demer-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">C&#x27;est la demer by Still Fresh (Ft. Sultan)</a></div></div><div class=\"SongInfo__Credit-nekw6x-3 fognin\"><div class=\"SongInfo__Label-nekw6x-4 kOJa-dB\">All Eyez On Me Translations</div><div><a href=\"https://genius.com/Genius-brasil-traducoes-2pac-all-eyez-on-me-ft-big-syke-traducao-em-portugues-lyrics\" font-weight=\"light\" class=\"StyledLink-sc-3ea0mt-0 iegxRM\">2Pac - All Eyez on Me ft. Big Syke (Tradução em Português) by Genius Brasil Traduções</a></div></div></div><a href=\"https://genius.com/2pac-all-eyez-on-me-sample\" class=\"StyledLink-sc-3ea0mt-0 iegxRM SongInfo__RelationshipsPageLink-nekw6x-6 tYzLI\" font-weight=\"light\">View All Eyez On Me samples</a><div class=\"SongTags__Title-xixwg3-0 ceKRFE\">Tags</div><div class=\"SongTags__Container-xixwg3-1 bZsZHM\"><a href=\"https://genius.com/tags/rap\" class=\"SongTags__Tag-xixwg3-2 evrydK\">Rap</a><a href=\"https://genius.com/tags/hip-hop\" class=\"SongTags__Tag-xixwg3-2 kykqAa\">Hip-Hop</a><a href=\"https://genius.com/tags/hardcore-rap\" class=\"SongTags__Tag-xixwg3-2 kykqAa\">Hardcore Rap</a><a href=\"https://genius.com/tags/west-coast-rap\" class=\"SongTags__Tag-xixwg3-2 kykqAa\">West Coast Rap</a><a href=\"https://genius.com/tags/gangsta-rap\" class=\"SongTags__Tag-xixwg3-2 kykqAa\">Gangsta Rap</a></div></div></div><div class=\"ExpandableContent__ButtonContainer-sc-1165iv-3 jzPNvv\"><button class=\"Button__Container-rtu9rw-0 efSZT ExpandableContent__Button-sc-1165iv-1 gBcitk\" type=\"button\">Expand<!-- --> <span class=\"InlineSvg__Wrapper-sc-1342j7p-0 blSbzj\"><svg viewBox=\"0 0 6.6 16\"><path d=\"M1.6 8.8l.6-.6 1 1 .5.7V6H0v-.8h4.5v4.6l.5-.6 1-1 .6.5L4 11.3 1.6 8.8z\"></path></svg></span></button></div></div><div class=\"InnerSectionDivider-sc-1x4onqw-0 iyrTpw\"></div><div class=\"MusicVideo__Container-sc-1980jex-0 cYtdTH\"></div></div><div class=\"RightSidebar__Container-pajcl2-0 fuSJbt\"><div class=\"SidebarAd__Container-sc-1cw85h6-0 kvuklz\"><div class=\"SidebarAd__StickyContainer-sc-1cw85h6-1 bJESjt\"><div class=\"DfpAd__Container-sc-1tnbv7f-0 kiNXoS\"><div id=\"div-gpt-ad-desktop_song_about_sidebar-desktop_song_about_sidebar-1\"></div><div class=\"DfpAd__Placeholder-sc-1tnbv7f-1 dQxFmL\"></div></div></div></div></div></div><div class=\"SectionLeaderboard__Container-sc-1pjk0bw-0 cSKAwQ\"><div class=\"SectionLeaderboard__Center-sc-1pjk0bw-1 fPpEQG\"><div class=\"DfpAd__Container-sc-1tnbv7f-0 dTXQYT\"><div id=\"div-gpt-ad-desktop_song_comments_leaderboard-desktop_song_comments_leaderboard-1\"></div><div class=\"DfpAd__Placeholder-sc-1tnbv7f-1 bIwkeM\"></div></div></div></div></div><div class=\"SectionScrollSentinel__Container-eoe1bv-0 icvVds\"><div class=\"SectionScrollSentinel__Element-eoe1bv-1 gTVDnZ\"></div><div id=\"comments\" class=\"SectionScrollSentinel__SectionAnchor-eoe1bv-2 difnmr\"></div><div class=\"PageGriddesktop-a6v82w-0 SongPageGriddesktop-sc-1px5b71-0 jecoie SongComments__Grid-sc-131p4fy-0 bIlJhm\"><div class=\"SongComments__Container-sc-131p4fy-1 lgbAKX\"><div font-size=\"xxLargeHeadline\" class=\"SongComments__Title-sc-131p4fy-2 kojbqH\">Comments</div><button class=\"CommentForm__FakeTextField-sc-1rfrnj0-5 leoUZe\">Add a comment</button><div class=\"SongComments__SpinnerContainer-sc-131p4fy-9 gRiFtA\"><div class=\"SongComments__InitialLoadSpinner-sc-131p4fy-10 fOsBvT\"><div class=\"PlaceholderSpinner__Container-r4gz6r-0 jDxAhO\"><svg viewBox=\"64 0 20 20\"><circle cx=\"74\" cy=\"10\" r=\"9\"></circle></svg></div></div></div><div class=\"InnerSectionDivider-sc-1x4onqw-0 vrxkS\"></div><div class=\"SongComments__Footer-sc-131p4fy-11 gTBWpu\"><div class=\"SongComments__CTASubHeading-sc-131p4fy-3 jZrfsi\">Sign Up And Drop Knowledge 🤓</div><div class=\"SongComments__CTA-sc-131p4fy-6 euQZer\">Genius is the ultimate source of music knowledge, created by scholars like you who share facts and insight about the songs and artists they love.</div><button class=\"Button__Container-rtu9rw-0 gexBFu SongComments__SignUpButton-sc-131p4fy-12 fHiIPi\" type=\"button\">Sign Up</button></div></div><div class=\"RightSidebar__Container-pajcl2-0 fuSJbt\"><div class=\"SidebarAd__Container-sc-1cw85h6-0 kvuklz\"><div class=\"SidebarAd__StickyContainer-sc-1cw85h6-1 bJESjt\"><div class=\"DfpAd__Container-sc-1tnbv7f-0 kiNXoS\"><div id=\"div-gpt-ad-desktop_song_comments_sidebar-desktop_song_comments_sidebar-1\"></div><div class=\"DfpAd__Placeholder-sc-1tnbv7f-1 ilfajN\"></div></div></div></div></div></div></div><div class=\"MediaPlayersContainer__Container-sc-1tibexe-0 bZruNU\"></div></main><div class=\"PageFooterdesktop__Container-hz1fx1-0 boDKcJ SongPage__PageFooter-sc-19xhmoi-3 llTPXF\"><div class=\"PageFootershared__FooterGridDesktop-bwy1q9-1 PageFooterdesktop__Section-hz1fx1-2 cjEbTp\"><div class=\"PageFooterdesktop__Half-hz1fx1-3 mJdfj\"><h1 class=\"PageFooterSocial__Slogan-sc-14u22mq-0 uGviF\">Genius is the world’s biggest collection of song lyrics and musical knowledge</h1><div class=\"SocialLinks__Container-jwyj6b-2 iuNSEV\"><a href=\"https://www.facebook.com/Genius/\" height=\"22\" target=\"_blank\" rel=\"noopener\" class=\"SocialLinks__Link-jwyj6b-1 cziiuX\"><svg viewBox=\"0 0 9.95 20\"><path d=\"M8.09 3.81c-1.4 0-1.58.84-1.58 1.67v1.3h3.35L9.49 11h-3v9H2.33v-9H0V6.88h2.42V3.81C2.42 1.3 3.81 0 6.6 0H10v3.81z\"></path></svg></a><a href=\"https://twitter.com/Genius\" height=\"22\" target=\"_blank\" rel=\"noopener\" class=\"SocialLinks__Link-jwyj6b-1 cziiuX\"><svg viewBox=\"0 0 20 16.43\"><path d=\"M20 1.89l-2.3 2.16v.68a12.28 12.28 0 0 1-3.65 8.92c-5 5.13-13.1 1.76-14.05.81 0 0 3.78.14 5.81-1.76A4.15 4.15 0 0 1 2.3 9.86h2S.81 9.05.81 5.81A11 11 0 0 0 3 6.35S-.14 4.05 1.49.95a11.73 11.73 0 0 0 8.37 4.19A3.69 3.69 0 0 1 13.51 0a3.19 3.19 0 0 1 2.57 1.08 12.53 12.53 0 0 0 3.24-.81l-1.75 1.89A10.46 10.46 0 0 0 20 1.89z\"></path></svg></a><a href=\"https://www.instagram.com/genius/\" height=\"22\" target=\"_blank\" rel=\"noopener\" class=\"SocialLinks__Link-jwyj6b-1 cziiuX\"><svg viewBox=\"0 0 20 20\"><path d=\"M10 0c2.724 0 3.062 0 4.125.06.83.017 1.65.175 2.426.467.668.254 1.272.65 1.77 1.162.508.498.902 1.1 1.153 1.768.292.775.45 1.595.467 2.424.06 1.063.06 1.41.06 4.123 0 2.712-.06 3.06-.06 4.123-.017.83-.175 1.648-.467 2.424-.52 1.34-1.58 2.402-2.922 2.92-.776.293-1.596.45-2.425.468-1.063.06-1.41.06-4.125.06-2.714 0-3.062-.06-4.125-.06-.83-.017-1.65-.175-2.426-.467-.668-.254-1.272-.65-1.77-1.162-.508-.498-.902-1.1-1.153-1.768-.292-.775-.45-1.595-.467-2.424C0 13.055 0 12.708 0 9.995c0-2.712 0-3.04.06-4.123.017-.83.175-1.648.467-2.424.25-.667.645-1.27 1.153-1.77.5-.507 1.103-.9 1.77-1.15C4.225.234 5.045.077 5.874.06 6.958 0 7.285 0 10 0zm0 1.798h.01c-2.674 0-2.992.06-4.046.06-.626.02-1.245.15-1.83.377-.434.16-.828.414-1.152.746-.337.31-.602.69-.775 1.113-.222.595-.34 1.224-.348 1.858-.06 1.064-.06 1.372-.06 4.045s.06 2.99.06 4.044c.007.633.125 1.262.347 1.857.17.434.434.824.775 1.142.31.33.692.587 1.113.754.596.222 1.224.34 1.86.348 1.063.06 1.37.06 4.045.06 2.674 0 2.992-.06 4.046-.06.635-.008 1.263-.126 1.86-.348.87-.336 1.56-1.025 1.897-1.897.217-.593.332-1.218.338-1.848.06-1.064.06-1.372.06-4.045s-.06-2.99-.06-4.044c-.01-.623-.128-1.24-.347-1.827-.16-.435-.414-.83-.745-1.152-.318-.34-.71-.605-1.143-.774-.596-.222-1.224-.34-1.86-.348-1.063-.06-1.37-.06-4.045-.06zm0 3.1c1.355 0 2.655.538 3.613 1.496.958.958 1.496 2.257 1.496 3.61 0 2.82-2.288 5.108-5.11 5.108-2.822 0-5.11-2.287-5.11-5.107 0-2.82 2.288-5.107 5.11-5.107zm0 8.415c.878 0 1.72-.348 2.34-.97.62-.62.97-1.46.97-2.338 0-1.827-1.482-3.31-3.31-3.31s-3.31 1.483-3.31 3.31 1.482 3.308 3.31 3.308zm6.51-8.633c0 .658-.533 1.192-1.192 1.192-.66 0-1.193-.534-1.193-1.192 0-.66.534-1.193 1.193-1.193.316 0 .62.126.844.35.223.223.35.526.35.843z\"></path></svg></a><a href=\"https://www.youtube.com/genius\" height=\"19\" target=\"_blank\" rel=\"noopener\" class=\"SocialLinks__Link-jwyj6b-1 fRTMWj\"><svg viewBox=\"0 0 20.01 14.07\"><path d=\"M19.81 3A4.32 4.32 0 0 0 19 1a2.86 2.86 0 0 0-2-.8C14.21 0 10 0 10 0S5.8 0 3 .2A2.87 2.87 0 0 0 1 1a4.32 4.32 0 0 0-.8 2S0 4.51 0 6.06V8a30 30 0 0 0 .2 3 4.33 4.33 0 0 0 .8 2 3.39 3.39 0 0 0 2.2.85c1.46.14 5.9.19 6.68.2h.4c1 0 4.35 0 6.72-.21a2.87 2.87 0 0 0 2-.84 4.32 4.32 0 0 0 .8-2 30.31 30.31 0 0 0 .2-3.21V6.28A30.31 30.31 0 0 0 19.81 3zM7.94 9.63V4l5.41 2.82z\"></path></svg></a></div></div><div class=\"PageFooterdesktop__Quarter-hz1fx1-4 hMUvCn\"><a href=\"/about\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">About Genius</a><a href=\"/contributor_guidelines\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Contributor Guidelines</a><a href=\"/press\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Press</a><a href=\"https://shop.genius.com\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Shop</a><a href=\"mailto:inquiry@genius.com\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Advertise</a><a href=\"/static/privacy_policy\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Privacy Policy</a></div><div class=\"PageFooterdesktop__Quarter-hz1fx1-4 hMUvCn\"><a href=\"/static/licensing\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Licensing</a><a href=\"/jobs\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Jobs</a><a href=\"/developers\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Developers</a><a href=\"/static/copyright\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Copyright Policy</a><a href=\"/feedback/new\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Contact Us</a><a href=\"/login\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Sign In</a><a href=\"/static/ccpa\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Do Not Sell My Personal Information</a></div><div class=\"PageFooterdesktop__Quarter-hz1fx1-4 PageFooterdesktop__OffsetQuarter-hz1fx1-5 diwZPD\"><div class=\"PageFooterdesktop__FinePrint-hz1fx1-6 iDkyVM\">© 2024 ML Genius Holdings, LLC</div></div><div class=\"PageFooterdesktop__Quarter-hz1fx1-4 hMUvCn\"><a href=\"/static/terms\" class=\"PageFooterdesktop__FinePrint-hz1fx1-6 iDkyVM\">Terms of Use</a></div></div><div class=\"PageFootershared__FooterGridDesktop-bwy1q9-1 PageFooterdesktop__Section-hz1fx1-2 PageFooterdesktop__Bottom-hz1fx1-7 yRyiP\"><div class=\"PageFooterdesktop__Row-hz1fx1-8 eIiYRJ\"><a href=\"/verified-artists\" class=\"PageFooterdesktop__VerifiedArtists-hz1fx1-10 bMBKQI\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">Verified Artists</span></a><div class=\"PageFooterdesktop__FlexWrap-hz1fx1-9 hNrwqx\"><div class=\"PageFooterdesktop__Label-hz1fx1-11 dcpJwP\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">All Artists:</span></div><a href=\"/artists-index/a\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">a</span></a><a href=\"/artists-index/b\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">b</span></a><a href=\"/artists-index/c\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">c</span></a><a href=\"/artists-index/d\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">d</span></a><a href=\"/artists-index/e\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">e</span></a><a href=\"/artists-index/f\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">f</span></a><a href=\"/artists-index/g\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">g</span></a><a href=\"/artists-index/h\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">h</span></a><a href=\"/artists-index/i\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">i</span></a><a href=\"/artists-index/j\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">j</span></a><a href=\"/artists-index/k\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">k</span></a><a href=\"/artists-index/l\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">l</span></a><a href=\"/artists-index/m\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">m</span></a><a href=\"/artists-index/n\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">n</span></a><a href=\"/artists-index/o\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">o</span></a><a href=\"/artists-index/p\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">p</span></a><a href=\"/artists-index/q\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">q</span></a><a href=\"/artists-index/r\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">r</span></a><a href=\"/artists-index/s\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">s</span></a><a href=\"/artists-index/t\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">t</span></a><a href=\"/artists-index/u\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">u</span></a><a href=\"/artists-index/v\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">v</span></a><a href=\"/artists-index/w\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">w</span></a><a href=\"/artists-index/x\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">x</span></a><a href=\"/artists-index/y\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">y</span></a><a href=\"/artists-index/z\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">z</span></a><a href=\"/artists-index/0\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">#</span></a></div></div><div class=\"PageFooterdesktop__Row-hz1fx1-8 eIiYRJ\"><div class=\"PageFooterdesktop__Label-hz1fx1-11 dcpJwP\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">Hot Songs:</span></div><div class=\"PageFooterdesktop__FlexWrap-hz1fx1-9 hNrwqx\"><a href=\"https://genius.com/Eminem-big-sean-and-babytron-tobey-lyrics\" class=\"PageFooterHotSongLinks__Link-sc-1adazwo-0 hAxKUd\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">Tobey</span></a><a href=\"https://genius.com/Eminem-houdini-lyrics\" class=\"PageFooterHotSongLinks__Link-sc-1adazwo-0 hAxKUd\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">Houdini</span></a><a href=\"https://genius.com/Kendrick-lamar-not-like-us-lyrics\" class=\"PageFooterHotSongLinks__Link-sc-1adazwo-0 hAxKUd\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">Not Like Us</span></a><a href=\"https://genius.com/Natanael-cano-and-oscar-maydon-giza-lyrics\" class=\"PageFooterHotSongLinks__Link-sc-1adazwo-0 hAxKUd\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">Giza</span></a><a href=\"https://genius.com/Sabrina-carpenter-please-please-please-lyrics\" class=\"PageFooterHotSongLinks__Link-sc-1adazwo-0 hAxKUd\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">Please Please Please</span></a><a href=\"/hot-songs\" class=\"PageFooterHotSongLinks__Link-sc-1adazwo-0 hAxKUd\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">View All</span></a></div></div></div></div></div>\n    <script>\n      window.__PRELOADED_STATE__ = JSON.parse('{\\\"deviceType\\\":\\\"desktop\\\",\\\"session\\\":{\\\"cmpEnabled\\\":false,\\\"showAds\\\":true,\\\"logClientMetrics\\\":false,\\\"fringeEnabled\\\":true,\\\"features\\\":[\\\"song_stories_public_launch\\\"]},\\\"currentPage\\\":\\\"songPage\\\",\\\"songPage\\\":{\\\"longTailCacheExperiment\\\":null,\\\"song\\\":6576,\\\"pinnedQuestions\\\":[99350],\\\"metadataQuestions\\\":[\\\"producer:song:6576\\\",\\\"release-date:song:6576\\\",\\\"writer:song:6576\\\"],\\\"lyricsData\\\":{\\\"referents\\\":[18437859,24858294,18907858,255696,2269422,19617841,20748281,20901107,2689751,2432432,22058141,13839497,15698084,9137652,293434,12128274,20748314,2966483,2432384,5049714,3082776,11537155,12859897],\\\"body\\\":{\\\"html\\\":\\\"<p>[Intro: 2Pac]<br>\\\\n<a href=\\\\\\\"/18437859/2pac-all-eyez-on-me/Big-syke-nook-hank-bogart-big-sur-yeah\\\\\\\" data-id=\\\\\\\"18437859\\\\\\\" class=\\\\\\\"has_comments\\\\\\\" data-editorial-state=\\\\\\\"accepted\\\\\\\" data-classification=\\\\\\\"accepted\\\\\\\">Big Syke, \\'Nook, Hank, Bogart, Big Sur (yeah)<\\/a><br>\\\\n<a href=\\\\\\\"/24858294/2pac-all-eyez-on-me/Yall-know-how-this-shit-go-you-know\\\\\\\" data-id=\\\\\\\"24858294\\\\\\\" data-editorial-state=\\\\\\\"pending\\\\\\\" data-classification=\\\\\\\"unreviewed\\\\\\\">Y\\'all know how this shit go (you know)<\\/a><br>\\\\nAll eyes on me<br>\\\\nMotherfuckin\\' OG<br>\\\\nRoll up in the club and shit, is that right?<br>\\\\nAll eyes on me<br>\\\\nAll eyes on me<br>\\\\nBut you know what?<br>\\\\n<br>\\\\n[Verse 1: 2Pac]<br>\\\\n<a href=\\\\\\\"/18907858/2pac-all-eyez-on-me/I-bet-you-got-it-twisted-you-dont-know-who-to-trust-so-many-player-hatin-niggas-tryna-sound-like-us\\\\\\\" data-id=\\\\\\\"18907858\\\\\\\" class=\\\\\\\"has_comments\\\\\\\" data-editorial-state=\\\\\\\"pending\\\\\\\" data-classification=\\\\\\\"unreviewed\\\\\\\">I bet you got it twisted, you don\\'t know who to trust<br>\\\\nSo many player-hatin\\' niggas tryna sound like us<\\/a><br>\\\\n<a href=\\\\\\\"/255696/2pac-all-eyez-on-me/Say-they-ready-for-the-funk-but-i-dont-think-they-knowin-straight-to-the-depths-of-hell-is-where-those-cowards-goin-well-are-you-still-down-nigga-holla-when-you-see-me\\\\\\\" data-id=\\\\\\\"255696\\\\\\\" class=\\\\\\\"has_comments\\\\\\\" data-editorial-state=\\\\\\\"accepted\\\\\\\" data-classification=\\\\\\\"accepted\\\\\\\">Say they ready for the funk, but I don\\'t think they knowin\\'<br>\\\\nStraight to the depths of Hell is where those cowards goin\\'<br>\\\\nWell, are you still down? Nigga, holla when you see me<\\/a><br>\\\\n<a href=\\\\\\\"/2269422/2pac-all-eyez-on-me/And-let-these-devils-be-sorry-for-the-day-they-finally-freed-me\\\\\\\" data-id=\\\\\\\"2269422\\\\\\\" data-editorial-state=\\\\\\\"accepted\\\\\\\" data-classification=\\\\\\\"accepted\\\\\\\">And let these devils be sorry for the day they finally freed me<\\/a><br>\\\\n<a href=\\\\\\\"/19617841/2pac-all-eyez-on-me/I-got-a-caravan-of-niggas-every-time-we-ride\\\\\\\" data-id=\\\\\\\"19617841\\\\\\\" class=\\\\\\\"has_comments\\\\\\\" data-editorial-state=\\\\\\\"pending\\\\\\\" data-classification=\\\\\\\"unreviewed\\\\\\\">I got a caravan of niggas every time we ride<\\/a><br>\\\\nHittin\\' motherfuckers up when we pass by<br>\\\\nUntil I die, live the life of a boss player \\'cause even when I\\'m high<br>\\\\nFuck with me and get crossed later, the futures in my eyes<br>\\\\n\\'cause all I want is cash and things<br>\\\\n<a href=\\\\\\\"/20748281/2pac-all-eyez-on-me/A-five-double-0-benz-flauntin-flashy-rings-uhh\\\\\\\" data-id=\\\\\\\"20748281\\\\\\\" class=\\\\\\\"has_pending_edits\\\\\\\" data-editorial-state=\\\\\\\"pending\\\\\\\" data-classification=\\\\\\\"unreviewed\\\\\\\">A five-double-0 Benz, flauntin\\' flashy rings, uhh<\\/a><br>\\\\nBitches pursue me like a dream<br>\\\\nBeen known to disappear before your eyes just like a dope fiend<br>\\\\nIt seems, my main thing was to be major paid<br>\\\\nThe game sharper than a motherfuckin\\' razor blade<br>\\\\nSay money bring bitches and bitches bring lies<br>\\\\n<a href=\\\\\\\"/20901107/2pac-all-eyez-on-me/One-niggas-gettin-jealous-and-motherfuckers-died\\\\\\\" data-id=\\\\\\\"20901107\\\\\\\" data-editorial-state=\\\\\\\"pending\\\\\\\" data-classification=\\\\\\\"unreviewed\\\\\\\">One nigga\\'s gettin\\' jealous and motherfuckers died<\\/a><br>\\\\n<a href=\\\\\\\"/2689751/2pac-all-eyez-on-me/Depend-on-me-like-the-first-and-fifteenth-they-might-hold-me-for-a-second-but-these-punks-wont-get-me\\\\\\\" data-id=\\\\\\\"2689751\\\\\\\" class=\\\\\\\"has_comments\\\\\\\" data-editorial-state=\\\\\\\"accepted\\\\\\\" data-classification=\\\\\\\"accepted\\\\\\\">Depend on me like the first and fifteenth<br>\\\\nThey might hold me for a second, but these punks won\\'t get me<\\/a><br>\\\\nWe got foe niggas and low riders in ski masks<br>\\\\nScreamin\\', \\\\\\\"Thug Life!\\\\\\\" every time they pass, all eyes on me<br>\\\\n<br>\\\\n[Chorus: 2Pac]<br>\\\\nLive the life of a thug nigga until the day I die<br>\\\\nLive the life of a boss player (All eyes on me) \\'cause even gettin\\' high<br>\\\\nAll eyes on me<br>\\\\nLive the life of a thug nigga until the day I die<br>\\\\nLive the life of a boss player \\'cause even gettin\\' high<br>\\\\n<br>\\\\n[Interlude: Big Syke]<br>\\\\nHey, to my nigga Pac<br>\\\\n<br>\\\\n[Verse 2: Big Syke]<br>\\\\nSo much trouble in the world, nigga<br>\\\\nCan\\'t nobody feel your pain<br>\\\\nThe world\\'s changin\\' every day, time\\'s movin\\' fast<br>\\\\nMy girl said I need a raise, how long will she last?<br>\\\\n<a href=\\\\\\\"/2432432/2pac-all-eyez-on-me/Im-caught-between-my-woman-and-my-pistol-and-my-chips\\\\\\\" data-id=\\\\\\\"2432432\\\\\\\" data-editorial-state=\\\\\\\"accepted\\\\\\\" data-classification=\\\\\\\"accepted\\\\\\\">I\\'m caught between my woman and my pistol and my chips<\\/a><br>\\\\n<a href=\\\\\\\"/22058141/2pac-all-eyez-on-me/Triple-beam\\\\\\\" data-id=\\\\\\\"22058141\\\\\\\" data-editorial-state=\\\\\\\"pending\\\\\\\" data-classification=\\\\\\\"unreviewed\\\\\\\">Triple beam<\\/a>, got some smokers on, whistle as I dip<br>\\\\nI\\'m lost in the land with no plan, livin\\' life flawless<br>\\\\nCrime boss, contraband, let me toss this<br>\\\\nMediocres got a lot of nerve, let my bucket swerve<br>\\\\nI\\'m takin\\' off from the curb<br>\\\\n<a href=\\\\\\\"/13839497/2pac-all-eyez-on-me/The-nervousness-neglect-make-me-pack-a-tec\\\\\\\" data-id=\\\\\\\"13839497\\\\\\\" data-editorial-state=\\\\\\\"accepted\\\\\\\" data-classification=\\\\\\\"accepted\\\\\\\">The nervousness neglect make me pack a TEC<\\/a><br>\\\\n<a href=\\\\\\\"/15698084/2pac-all-eyez-on-me/Devoted-to-servin-this-moet-and-pay-checks\\\\\\\" data-id=\\\\\\\"15698084\\\\\\\" class=\\\\\\\"has_comments\\\\\\\" data-editorial-state=\\\\\\\"accepted\\\\\\\" data-classification=\\\\\\\"accepted\\\\\\\">Devoted to servin\\' this Moët and pay checks<\\/a><br>\\\\n<a href=\\\\\\\"/9137652/2pac-all-eyez-on-me/Like-akai-satellite-nigga-im-forever-ballin\\\\\\\" data-id=\\\\\\\"9137652\\\\\\\" data-editorial-state=\\\\\\\"accepted\\\\\\\" data-classification=\\\\\\\"accepted\\\\\\\">Like Akai satellite, nigga, I\\'m forever ballin\\'<\\/a><br>\\\\nIt ain\\'t right: parasites, triggers, and fleas crawlin\\'<br>\\\\nSucker, duck and get busted, no emotion<br>\\\\nMy devotion is handlin\\' my business, nigga, keep on coastin\\'<br>\\\\nWhere you goin\\', I been there, came back as lonely, homie<br>\\\\nSteady flowin\\' against the grain, niggas still don\\'t know me<br>\\\\nIt\\'s about the money in this rap shit, this crap shit<br>\\\\nIt ain\\'t funny, niggas don\\'t even know how to act, shit<br>\\\\nWhat can I do? What can I say? Is there another way?<br>\\\\nBlunts and gin all day, 24 parlay<br>\\\\nMy little homie G, can\\'t you see I\\'m buster-free?<br>\\\\nNiggas can\\'t stand me; all eyes on me<br>\\\\n<br>\\\\n[Chorus: 2Pac]<br>\\\\nLive the life of a thug nigga until the day I die<br>\\\\nLive the life of a boss player \\'cause even gettin\\' high<br>\\\\nAll eyes on me<br>\\\\nAll eyes on me<br>\\\\nLive the life of a thug nigga until the day I die<br>\\\\nLive the life of a boss player \\'cause even gettin\\' high<br>\\\\nAll eyes on me<br>\\\\n<br>\\\\n[Verse 3: 2Pac]<br>\\\\n<a href=\\\\\\\"/293434/2pac-all-eyez-on-me/The-feds-is-watchin-niggas-plottin-to-get-me\\\\\\\" data-id=\\\\\\\"293434\\\\\\\" class=\\\\\\\"has_comments\\\\\\\" data-editorial-state=\\\\\\\"accepted\\\\\\\" data-classification=\\\\\\\"accepted\\\\\\\">The feds is watchin\\', niggas plottin\\' to get me<\\/a><br>\\\\nWill I survive? Will I die? Come on, let\\'s picture the possibility<br>\\\\n<a href=\\\\\\\"/12128274/2pac-all-eyez-on-me/Givin-me-charges-lawyers-makin-a-grip\\\\\\\" data-id=\\\\\\\"12128274\\\\\\\" data-editorial-state=\\\\\\\"accepted\\\\\\\" data-classification=\\\\\\\"accepted\\\\\\\">Givin\\' me charges, lawyers makin\\' a grip<\\/a><br>\\\\nI told the judge I was raised wrong and that\\'s why I blaze shit<br>\\\\nWas hyper as a kid, cold as a teenager<br>\\\\nOn my mobile, callin\\' big shots on the scene major<br>\\\\nPackin\\' hundreds in my drawers, fuck the law<br>\\\\nBitches, I fuck with a passion, I\\'m livin\\' rough and raw<br>\\\\n<a href=\\\\\\\"/20748314/2pac-all-eyez-on-me/Catchin-cases-at-a-fast-rate-ballin-in-the-fast-lane\\\\\\\" data-id=\\\\\\\"20748314\\\\\\\" class=\\\\\\\"has_pending_edits\\\\\\\" data-editorial-state=\\\\\\\"pending\\\\\\\" data-classification=\\\\\\\"unreviewed\\\\\\\">Catchin\\' cases at a fast rate, ballin\\' in the fast lane<\\/a><br>\\\\nHustle \\'til the mornin\\', never stopped until the cash came<br>\\\\nLive my life as a thug nigga until the day I die<br>\\\\nLive my life as a boss player, \\'cause even gettin\\' high<br>\\\\nThese niggas got me tossin\\' shit<br>\\\\nI put the top down, now it\\'s time to floss my shit<br>\\\\n<a href=\\\\\\\"/2966483/2pac-all-eyez-on-me/Keep-your-head-up-nigga-make-these-motherfuckers-suffer\\\\\\\" data-id=\\\\\\\"2966483\\\\\\\" data-editorial-state=\\\\\\\"accepted\\\\\\\" data-classification=\\\\\\\"accepted\\\\\\\">Keep your head up, nigga, make these motherfuckers suffer<\\/a><br>\\\\n<a href=\\\\\\\"/2432384/2pac-all-eyez-on-me/Up-in-the-benz-burnin-rubber\\\\\\\" data-id=\\\\\\\"2432384\\\\\\\" class=\\\\\\\"has_comments\\\\\\\" data-editorial-state=\\\\\\\"accepted\\\\\\\" data-classification=\\\\\\\"accepted\\\\\\\">Up in the Benz, burnin\\' rubber<\\/a><br>\\\\n<a href=\\\\\\\"/5049714/2pac-all-eyez-on-me/The-money-is-mandatory-the-hoes-is-for-the-stress\\\\\\\" data-id=\\\\\\\"5049714\\\\\\\" class=\\\\\\\"has_comments\\\\\\\" data-editorial-state=\\\\\\\"accepted\\\\\\\" data-classification=\\\\\\\"accepted\\\\\\\">The money is mandatory, the hoes is for the stress<\\/a><br>\\\\n<a href=\\\\\\\"/3082776/2pac-all-eyez-on-me/This-criminal-lifestyle-equipped-with-the-bulletproof-vest\\\\\\\" data-id=\\\\\\\"3082776\\\\\\\" data-editorial-state=\\\\\\\"accepted\\\\\\\" data-classification=\\\\\\\"accepted\\\\\\\">This criminal lifestyle, equipped with the bulletproof vest<\\/a><br>\\\\nMake sure your eyes is on the meal ticket<br>\\\\nGet your money, motherfucker, let\\'s get rich and we\\'ll kick it<br>\\\\nAll eyes on me<br>\\\\n<br>\\\\n[Chorus: 2Pac]<br>\\\\nLive the life as a thug nigga until the day I die<br>\\\\nLive the life as a boss player \\'cause even gettin\\' high<br>\\\\nAll eyes on me<br>\\\\nAll eyes on me<br>\\\\nLive the life of a thug nigga until the day I die<br>\\\\nLive the life of a boss player \\'cause even gettin\\' high<br>\\\\nAll eyes on me<br>\\\\n<br>\\\\n[Outro: 2Pac]<br>\\\\n<a href=\\\\\\\"/11537155/2pac-all-eyez-on-me/Pay-attention-my-niggas-see-how-that-shit-go-nigga-walk-up-in-this-motherfucker-and-it-be-like-bing-cops-bitches-every-motherfuckin-body\\\\\\\" data-id=\\\\\\\"11537155\\\\\\\" data-editorial-state=\\\\\\\"accepted\\\\\\\" data-classification=\\\\\\\"accepted\\\\\\\">Pay attention, my niggas! See how that shit go?<br>\\\\nNigga walk up in this motherfucker and it be like, \\\\\\\"Bing!\\\\\\\"<br>\\\\nCops, bitches, every-motherfuckin\\'-body<\\/a><br>\\\\n(Live my life as a thug nigga until the day I die)<br>\\\\n(Live my life as a boss playa, \\'cause even gettin\\' high)<br>\\\\nI got bustas, hoes, and police watchin\\' a nigga, you know?<br>\\\\n(I live my life as a thug nigga until the day I die)<br>\\\\n(Livin\\' life as a boss playa, \\'cause even gettin\\' high)<br>\\\\nHe he he, it\\'s like what they think<br>\\\\n<a href=\\\\\\\"/12859897/2pac-all-eyez-on-me/Im-walkin-around-with-some-kis-in-my-pocket-or-somethin\\\\\\\" data-id=\\\\\\\"12859897\\\\\\\" class=\\\\\\\"has_comments\\\\\\\" data-editorial-state=\\\\\\\"accepted\\\\\\\" data-classification=\\\\\\\"accepted\\\\\\\">I\\'m walkin\\' around with some ki\\'s in my pocket or somethin\\'<\\/a><br>\\\\nThey think I\\'m goin\\' back to jail, they really on that dope<br>\\\\n(Live my life as a thug nigga until the day I die)<br>\\\\n(Live my life as a boss playa)<br>\\\\nI know y\\'all watchin\\', I know y\\'all got me in the scopes<br>\\\\n(Live my life as a thug nigga until the day I die)<br>\\\\n(Live my life as a boss playa, \\'cause even gettin\\' high)<br>\\\\nI know y\\'all know this is Thug Life, baby!<br>\\\\nY\\'all got me under surveillance, huh?<br>\\\\nAll eyes on me, but I\\'m knowin\\'<\\/p>\\\\n\\\\n\\\",\\\"children\\\":[{\\\"children\\\":[\\\"[Intro: 2Pac]\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"Big Syke, \\'Nook, Hank, Bogart, Big Sur (yeah)\\\"],\\\"data\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":\\\"18437859\\\"},\\\"attributes\\\":{\\\"class\\\":\\\"has_comments\\\",\\\"href\\\":\\\"/18437859/2pac-all-eyez-on-me/Big-syke-nook-hank-bogart-big-sur-yeah\\\"},\\\"tag\\\":\\\"a\\\"},{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"Y\\'all know how this shit go (you know)\\\"],\\\"data\\\":{\\\"classification\\\":\\\"unreviewed\\\",\\\"editorialState\\\":\\\"pending\\\",\\\"id\\\":\\\"24858294\\\"},\\\"attributes\\\":{\\\"href\\\":\\\"/24858294/2pac-all-eyez-on-me/Yall-know-how-this-shit-go-you-know\\\"},\\\"tag\\\":\\\"a\\\"},{\\\"tag\\\":\\\"br\\\"},\\\"All eyes on me\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Motherfuckin\\' OG\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Roll up in the club and shit, is that right?\\\",{\\\"tag\\\":\\\"br\\\"},\\\"All eyes on me\\\",{\\\"tag\\\":\\\"br\\\"},\\\"All eyes on me\\\",{\\\"tag\\\":\\\"br\\\"},\\\"But you know what?\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"tag\\\":\\\"br\\\"},\\\"[Verse 1: 2Pac]\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"I bet you got it twisted, you don\\'t know who to trust\\\",{\\\"tag\\\":\\\"br\\\"},\\\"So many player-hatin\\' niggas tryna sound like us\\\"],\\\"data\\\":{\\\"classification\\\":\\\"unreviewed\\\",\\\"editorialState\\\":\\\"pending\\\",\\\"id\\\":\\\"18907858\\\"},\\\"attributes\\\":{\\\"class\\\":\\\"has_comments\\\",\\\"href\\\":\\\"/18907858/2pac-all-eyez-on-me/I-bet-you-got-it-twisted-you-dont-know-who-to-trust-so-many-player-hatin-niggas-tryna-sound-like-us\\\"},\\\"tag\\\":\\\"a\\\"},{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"Say they ready for the funk, but I don\\'t think they knowin\\'\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Straight to the depths of Hell is where those cowards goin\\'\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Well, are you still down? Nigga, holla when you see me\\\"],\\\"data\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":\\\"255696\\\"},\\\"attributes\\\":{\\\"class\\\":\\\"has_comments\\\",\\\"href\\\":\\\"/255696/2pac-all-eyez-on-me/Say-they-ready-for-the-funk-but-i-dont-think-they-knowin-straight-to-the-depths-of-hell-is-where-those-cowards-goin-well-are-you-still-down-nigga-holla-when-you-see-me\\\"},\\\"tag\\\":\\\"a\\\"},{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"And let these devils be sorry for the day they finally freed me\\\"],\\\"data\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":\\\"2269422\\\"},\\\"attributes\\\":{\\\"href\\\":\\\"/2269422/2pac-all-eyez-on-me/And-let-these-devils-be-sorry-for-the-day-they-finally-freed-me\\\"},\\\"tag\\\":\\\"a\\\"},{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"I got a caravan of niggas every time we ride\\\"],\\\"data\\\":{\\\"classification\\\":\\\"unreviewed\\\",\\\"editorialState\\\":\\\"pending\\\",\\\"id\\\":\\\"19617841\\\"},\\\"attributes\\\":{\\\"class\\\":\\\"has_comments\\\",\\\"href\\\":\\\"/19617841/2pac-all-eyez-on-me/I-got-a-caravan-of-niggas-every-time-we-ride\\\"},\\\"tag\\\":\\\"a\\\"},{\\\"tag\\\":\\\"br\\\"},\\\"Hittin\\' motherfuckers up when we pass by\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Until I die, live the life of a boss player \\'cause even when I\\'m high\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Fuck with me and get crossed later, the futures in my eyes\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\'cause all I want is cash and things\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"A five-double-0 Benz, flauntin\\' flashy rings, uhh\\\"],\\\"data\\\":{\\\"classification\\\":\\\"unreviewed\\\",\\\"editorialState\\\":\\\"pending\\\",\\\"id\\\":\\\"20748281\\\"},\\\"attributes\\\":{\\\"class\\\":\\\"has_pending_edits\\\",\\\"href\\\":\\\"/20748281/2pac-all-eyez-on-me/A-five-double-0-benz-flauntin-flashy-rings-uhh\\\"},\\\"tag\\\":\\\"a\\\"},{\\\"tag\\\":\\\"br\\\"},\\\"Bitches pursue me like a dream\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Been known to disappear before your eyes just like a dope fiend\\\",{\\\"tag\\\":\\\"br\\\"},\\\"It seems, my main thing was to be major paid\\\",{\\\"tag\\\":\\\"br\\\"},\\\"The game sharper than a motherfuckin\\' razor blade\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Say money bring bitches and bitches bring lies\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"One nigga\\'s gettin\\' jealous and motherfuckers died\\\"],\\\"data\\\":{\\\"classification\\\":\\\"unreviewed\\\",\\\"editorialState\\\":\\\"pending\\\",\\\"id\\\":\\\"20901107\\\"},\\\"attributes\\\":{\\\"href\\\":\\\"/20901107/2pac-all-eyez-on-me/One-niggas-gettin-jealous-and-motherfuckers-died\\\"},\\\"tag\\\":\\\"a\\\"},{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"Depend on me like the first and fifteenth\\\",{\\\"tag\\\":\\\"br\\\"},\\\"They might hold me for a second, but these punks won\\'t get me\\\"],\\\"data\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":\\\"2689751\\\"},\\\"attributes\\\":{\\\"class\\\":\\\"has_comments\\\",\\\"href\\\":\\\"/2689751/2pac-all-eyez-on-me/Depend-on-me-like-the-first-and-fifteenth-they-might-hold-me-for-a-second-but-these-punks-wont-get-me\\\"},\\\"tag\\\":\\\"a\\\"},{\\\"tag\\\":\\\"br\\\"},\\\"We got foe niggas and low riders in ski masks\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Screamin\\', \\\\\\\"Thug Life!\\\\\\\" every time they pass, all eyes on me\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"data\\\":{\\\"name\\\":\\\"desktop_song_lyrics_inread\\\"},\\\"tag\\\":\\\"inread-ad\\\"},\\\"[Chorus: 2Pac]\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Live the life of a thug nigga until the day I die\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Live the life of a boss player (All eyes on me) \\'cause even gettin\\' high\\\",{\\\"tag\\\":\\\"br\\\"},\\\"All eyes on me\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Live the life of a thug nigga until the day I die\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Live the life of a boss player \\'cause even gettin\\' high\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"tag\\\":\\\"br\\\"},\\\"[Interlude: Big Syke]\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Hey, to my nigga Pac\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"tag\\\":\\\"br\\\"},\\\"[Verse 2: Big Syke]\\\",{\\\"tag\\\":\\\"br\\\"},\\\"So much trouble in the world, nigga\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Can\\'t nobody feel your pain\\\",{\\\"tag\\\":\\\"br\\\"},\\\"The world\\'s changin\\' every day, time\\'s movin\\' fast\\\",{\\\"tag\\\":\\\"br\\\"},\\\"My girl said I need a raise, how long will she last?\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"I\\'m caught between my woman and my pistol and my chips\\\"],\\\"data\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":\\\"2432432\\\"},\\\"attributes\\\":{\\\"href\\\":\\\"/2432432/2pac-all-eyez-on-me/Im-caught-between-my-woman-and-my-pistol-and-my-chips\\\"},\\\"tag\\\":\\\"a\\\"},{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"Triple beam\\\"],\\\"data\\\":{\\\"classification\\\":\\\"unreviewed\\\",\\\"editorialState\\\":\\\"pending\\\",\\\"id\\\":\\\"22058141\\\"},\\\"attributes\\\":{\\\"href\\\":\\\"/22058141/2pac-all-eyez-on-me/Triple-beam\\\"},\\\"tag\\\":\\\"a\\\"},\\\", got some smokers on, whistle as I dip\\\",{\\\"tag\\\":\\\"br\\\"},\\\"I\\'m lost in the land with no plan, livin\\' life flawless\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Crime boss, contraband, let me toss this\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Mediocres got a lot of nerve, let my bucket swerve\\\",{\\\"tag\\\":\\\"br\\\"},\\\"I\\'m takin\\' off from the curb\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"The nervousness neglect make me pack a TEC\\\"],\\\"data\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":\\\"13839497\\\"},\\\"attributes\\\":{\\\"href\\\":\\\"/13839497/2pac-all-eyez-on-me/The-nervousness-neglect-make-me-pack-a-tec\\\"},\\\"tag\\\":\\\"a\\\"},{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"Devoted to servin\\' this Moët and pay checks\\\"],\\\"data\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":\\\"15698084\\\"},\\\"attributes\\\":{\\\"class\\\":\\\"has_comments\\\",\\\"href\\\":\\\"/15698084/2pac-all-eyez-on-me/Devoted-to-servin-this-moet-and-pay-checks\\\"},\\\"tag\\\":\\\"a\\\"},{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"Like Akai satellite, nigga, I\\'m forever ballin\\'\\\"],\\\"data\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":\\\"9137652\\\"},\\\"attributes\\\":{\\\"href\\\":\\\"/9137652/2pac-all-eyez-on-me/Like-akai-satellite-nigga-im-forever-ballin\\\"},\\\"tag\\\":\\\"a\\\"},{\\\"tag\\\":\\\"br\\\"},\\\"It ain\\'t right: parasites, triggers, and fleas crawlin\\'\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Sucker, duck and get busted, no emotion\\\",{\\\"tag\\\":\\\"br\\\"},\\\"My devotion is handlin\\' my business, nigga, keep on coastin\\'\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Where you goin\\', I been there, came back as lonely, homie\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Steady flowin\\' against the grain, niggas still don\\'t know me\\\",{\\\"tag\\\":\\\"br\\\"},\\\"It\\'s about the money in this rap shit, this crap shit\\\",{\\\"tag\\\":\\\"br\\\"},\\\"It ain\\'t funny, niggas don\\'t even know how to act, shit\\\",{\\\"tag\\\":\\\"br\\\"},\\\"What can I do? What can I say? Is there another way?\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Blunts and gin all day, 24 parlay\\\",{\\\"tag\\\":\\\"br\\\"},\\\"My little homie G, can\\'t you see I\\'m buster-free?\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Niggas can\\'t stand me; all eyes on me\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"data\\\":{\\\"name\\\":\\\"desktop_song_lyrics_inread2\\\"},\\\"tag\\\":\\\"inread-ad\\\"},\\\"[Chorus: 2Pac]\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Live the life of a thug nigga until the day I die\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Live the life of a boss player \\'cause even gettin\\' high\\\",{\\\"tag\\\":\\\"br\\\"},\\\"All eyes on me\\\",{\\\"tag\\\":\\\"br\\\"},\\\"All eyes on me\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Live the life of a thug nigga until the day I die\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Live the life of a boss player \\'cause even gettin\\' high\\\",{\\\"tag\\\":\\\"br\\\"},\\\"All eyes on me\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"tag\\\":\\\"br\\\"},\\\"[Verse 3: 2Pac]\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"The feds is watchin\\', niggas plottin\\' to get me\\\"],\\\"data\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":\\\"293434\\\"},\\\"attributes\\\":{\\\"class\\\":\\\"has_comments\\\",\\\"href\\\":\\\"/293434/2pac-all-eyez-on-me/The-feds-is-watchin-niggas-plottin-to-get-me\\\"},\\\"tag\\\":\\\"a\\\"},{\\\"tag\\\":\\\"br\\\"},\\\"Will I survive? Will I die? Come on, let\\'s picture the possibility\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"Givin\\' me charges, lawyers makin\\' a grip\\\"],\\\"data\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":\\\"12128274\\\"},\\\"attributes\\\":{\\\"href\\\":\\\"/12128274/2pac-all-eyez-on-me/Givin-me-charges-lawyers-makin-a-grip\\\"},\\\"tag\\\":\\\"a\\\"},{\\\"tag\\\":\\\"br\\\"},\\\"I told the judge I was raised wrong and that\\'s why I blaze shit\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Was hyper as a kid, cold as a teenager\\\",{\\\"tag\\\":\\\"br\\\"},\\\"On my mobile, callin\\' big shots on the scene major\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Packin\\' hundreds in my drawers, fuck the law\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Bitches, I fuck with a passion, I\\'m livin\\' rough and raw\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"Catchin\\' cases at a fast rate, ballin\\' in the fast lane\\\"],\\\"data\\\":{\\\"classification\\\":\\\"unreviewed\\\",\\\"editorialState\\\":\\\"pending\\\",\\\"id\\\":\\\"20748314\\\"},\\\"attributes\\\":{\\\"class\\\":\\\"has_pending_edits\\\",\\\"href\\\":\\\"/20748314/2pac-all-eyez-on-me/Catchin-cases-at-a-fast-rate-ballin-in-the-fast-lane\\\"},\\\"tag\\\":\\\"a\\\"},{\\\"tag\\\":\\\"br\\\"},\\\"Hustle \\'til the mornin\\', never stopped until the cash came\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Live my life as a thug nigga until the day I die\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Live my life as a boss player, \\'cause even gettin\\' high\\\",{\\\"tag\\\":\\\"br\\\"},\\\"These niggas got me tossin\\' shit\\\",{\\\"tag\\\":\\\"br\\\"},\\\"I put the top down, now it\\'s time to floss my shit\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"Keep your head up, nigga, make these motherfuckers suffer\\\"],\\\"data\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":\\\"2966483\\\"},\\\"attributes\\\":{\\\"href\\\":\\\"/2966483/2pac-all-eyez-on-me/Keep-your-head-up-nigga-make-these-motherfuckers-suffer\\\"},\\\"tag\\\":\\\"a\\\"},{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"Up in the Benz, burnin\\' rubber\\\"],\\\"data\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":\\\"2432384\\\"},\\\"attributes\\\":{\\\"class\\\":\\\"has_comments\\\",\\\"href\\\":\\\"/2432384/2pac-all-eyez-on-me/Up-in-the-benz-burnin-rubber\\\"},\\\"tag\\\":\\\"a\\\"},{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"The money is mandatory, the hoes is for the stress\\\"],\\\"data\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":\\\"5049714\\\"},\\\"attributes\\\":{\\\"class\\\":\\\"has_comments\\\",\\\"href\\\":\\\"/5049714/2pac-all-eyez-on-me/The-money-is-mandatory-the-hoes-is-for-the-stress\\\"},\\\"tag\\\":\\\"a\\\"},{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"This criminal lifestyle, equipped with the bulletproof vest\\\"],\\\"data\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":\\\"3082776\\\"},\\\"attributes\\\":{\\\"href\\\":\\\"/3082776/2pac-all-eyez-on-me/This-criminal-lifestyle-equipped-with-the-bulletproof-vest\\\"},\\\"tag\\\":\\\"a\\\"},{\\\"tag\\\":\\\"br\\\"},\\\"Make sure your eyes is on the meal ticket\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Get your money, motherfucker, let\\'s get rich and we\\'ll kick it\\\",{\\\"tag\\\":\\\"br\\\"},\\\"All eyes on me\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"data\\\":{\\\"name\\\":\\\"desktop_song_lyrics_inread3\\\"},\\\"tag\\\":\\\"inread-ad\\\"},\\\"[Chorus: 2Pac]\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Live the life as a thug nigga until the day I die\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Live the life as a boss player \\'cause even gettin\\' high\\\",{\\\"tag\\\":\\\"br\\\"},\\\"All eyes on me\\\",{\\\"tag\\\":\\\"br\\\"},\\\"All eyes on me\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Live the life of a thug nigga until the day I die\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Live the life of a boss player \\'cause even gettin\\' high\\\",{\\\"tag\\\":\\\"br\\\"},\\\"All eyes on me\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"tag\\\":\\\"br\\\"},\\\"[Outro: 2Pac]\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"Pay attention, my niggas! See how that shit go?\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Nigga walk up in this motherfucker and it be like, \\\\\\\"Bing!\\\\\\\"\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Cops, bitches, every-motherfuckin\\'-body\\\"],\\\"data\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":\\\"11537155\\\"},\\\"attributes\\\":{\\\"href\\\":\\\"/11537155/2pac-all-eyez-on-me/Pay-attention-my-niggas-see-how-that-shit-go-nigga-walk-up-in-this-motherfucker-and-it-be-like-bing-cops-bitches-every-motherfuckin-body\\\"},\\\"tag\\\":\\\"a\\\"},{\\\"tag\\\":\\\"br\\\"},\\\"(Live my life as a thug nigga until the day I die)\\\",{\\\"tag\\\":\\\"br\\\"},\\\"(Live my life as a boss playa, \\'cause even gettin\\' high)\\\",{\\\"tag\\\":\\\"br\\\"},\\\"I got bustas, hoes, and police watchin\\' a nigga, you know?\\\",{\\\"tag\\\":\\\"br\\\"},\\\"(I live my life as a thug nigga until the day I die)\\\",{\\\"tag\\\":\\\"br\\\"},\\\"(Livin\\' life as a boss playa, \\'cause even gettin\\' high)\\\",{\\\"tag\\\":\\\"br\\\"},\\\"He he he, it\\'s like what they think\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"children\\\":[\\\"I\\'m walkin\\' around with some ki\\'s in my pocket or somethin\\'\\\"],\\\"data\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":\\\"12859897\\\"},\\\"attributes\\\":{\\\"class\\\":\\\"has_comments\\\",\\\"href\\\":\\\"/12859897/2pac-all-eyez-on-me/Im-walkin-around-with-some-kis-in-my-pocket-or-somethin\\\"},\\\"tag\\\":\\\"a\\\"},{\\\"tag\\\":\\\"br\\\"},\\\"They think I\\'m goin\\' back to jail, they really on that dope\\\",{\\\"tag\\\":\\\"br\\\"},\\\"(Live my life as a thug nigga until the day I die)\\\",{\\\"tag\\\":\\\"br\\\"},\\\"(Live my life as a boss playa)\\\",{\\\"tag\\\":\\\"br\\\"},\\\"I know y\\'all watchin\\', I know y\\'all got me in the scopes\\\",{\\\"tag\\\":\\\"br\\\"},\\\"(Live my life as a thug nigga until the day I die)\\\",{\\\"tag\\\":\\\"br\\\"},\\\"(Live my life as a boss playa, \\'cause even gettin\\' high)\\\",{\\\"tag\\\":\\\"br\\\"},\\\"I know y\\'all know this is Thug Life, baby!\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Y\\'all got me under surveillance, huh?\\\",{\\\"tag\\\":\\\"br\\\"},\\\"All eyes on me, but I\\'m knowin\\'\\\"],\\\"tag\\\":\\\"p\\\"},\\\"\\\"],\\\"tag\\\":\\\"root\\\"},\\\"lyricsPlaceholderReason\\\":null,\\\"clientTimestamps\\\":{\\\"updatedByHumanAt\\\":1720296086,\\\"lyricsUpdatedAt\\\":1720296086}},\\\"hotSongsPreview\\\":[{\\\"url\\\":\\\"https://genius.com/Eminem-big-sean-and-babytron-tobey-lyrics\\\",\\\"title\\\":\\\"Tobey\\\",\\\"id\\\":10590343},{\\\"url\\\":\\\"https://genius.com/Eminem-houdini-lyrics\\\",\\\"title\\\":\\\"Houdini\\\",\\\"id\\\":10465609},{\\\"url\\\":\\\"https://genius.com/Kendrick-lamar-not-like-us-lyrics\\\",\\\"title\\\":\\\"Not Like Us\\\",\\\"id\\\":10359264},{\\\"url\\\":\\\"https://genius.com/Natanael-cano-and-oscar-maydon-giza-lyrics\\\",\\\"title\\\":\\\"Giza\\\",\\\"id\\\":10299143},{\\\"url\\\":\\\"https://genius.com/Sabrina-carpenter-please-please-please-lyrics\\\",\\\"title\\\":\\\"Please Please Please\\\",\\\"id\\\":10488398}],\\\"featuredQuestion\\\":null,\\\"defaultQuestions\\\":[],\\\"unansweredDefaultQuestions\\\":[\\\"live_performance:song:6576\\\",\\\"song_meaning:song:6576\\\"],\\\"showFeaturedQuestion\\\":false,\\\"pendingQuestionCount\\\":0,\\\"dfpKv\\\":[{\\\"values\\\":[\\\"6576\\\"],\\\"name\\\":\\\"song_id\\\"},{\\\"values\\\":[\\\"All Eyez On Me\\\"],\\\"name\\\":\\\"song_title\\\"},{\\\"values\\\":[\\\"59\\\"],\\\"name\\\":\\\"artist_id\\\"},{\\\"values\\\":[\\\"2Pac\\\"],\\\"name\\\":\\\"artist_name\\\"},{\\\"values\\\":[\\\"true\\\"],\\\"name\\\":\\\"is_explicit\\\"},{\\\"values\\\":[\\\"1212666\\\"],\\\"name\\\":\\\"pageviews\\\"},{\\\"values\\\":[\\\"1434\\\"],\\\"name\\\":\\\"primary_tag_id\\\"},{\\\"values\\\":[\\\"rap\\\"],\\\"name\\\":\\\"primary_tag\\\"},{\\\"values\\\":[\\\"3783\\\",\\\"3171\\\",\\\"798\\\",\\\"2741\\\",\\\"1434\\\"],\\\"name\\\":\\\"tag_id\\\"},{\\\"values\\\":[\\\"C\\\"],\\\"name\\\":\\\"song_tier\\\"},{\\\"values\\\":[\\\"weed\\\"],\\\"name\\\":\\\"topic\\\"},{\\\"values\\\":[\\\"false\\\"],\\\"name\\\":\\\"in_top_10\\\"},{\\\"values\\\":[\\\"false\\\"],\\\"name\\\":\\\"artist_in_top_10\\\"},{\\\"values\\\":[\\\"false\\\"],\\\"name\\\":\\\"album_in_top_10\\\"},{\\\"values\\\":[\\\"false\\\"],\\\"name\\\":\\\"new_release\\\"},{\\\"values\\\":[\\\"199602\\\"],\\\"name\\\":\\\"release_month\\\"},{\\\"values\\\":[\\\"1996\\\"],\\\"name\\\":\\\"release_year\\\"},{\\\"values\\\":[\\\"1990\\\"],\\\"name\\\":\\\"release_decade\\\"},{\\\"values\\\":[\\\"false\\\"],\\\"name\\\":\\\"stubhub_is_active\\\"},{\\\"values\\\":[\\\"false\\\"],\\\"name\\\":\\\"in_top_10_rap\\\"},{\\\"values\\\":[\\\"false\\\"],\\\"name\\\":\\\"in_top_10_rock\\\"},{\\\"values\\\":[\\\"false\\\"],\\\"name\\\":\\\"in_top_10_country\\\"},{\\\"values\\\":[\\\"false\\\"],\\\"name\\\":\\\"in_top_10_r_and_b\\\"},{\\\"values\\\":[\\\"false\\\"],\\\"name\\\":\\\"in_top_10_pop\\\"},{\\\"values\\\":[\\\"production\\\"],\\\"name\\\":\\\"environment\\\"},{\\\"values\\\":[\\\"web\\\"],\\\"name\\\":\\\"platform\\\"},{\\\"values\\\":[\\\"desktop_react\\\"],\\\"name\\\":\\\"platform_variant\\\"},{\\\"values\\\":[\\\"control\\\"],\\\"name\\\":\\\"interstitial_variant\\\"},{\\\"values\\\":[\\\"song\\\"],\\\"name\\\":\\\"ad_page_type\\\"}],\\\"trackingData\\\":[{\\\"value\\\":6576,\\\"key\\\":\\\"Song ID\\\"},{\\\"value\\\":\\\"All Eyez On Me\\\",\\\"key\\\":\\\"Title\\\"},{\\\"value\\\":\\\"2Pac\\\",\\\"key\\\":\\\"Primary Artist\\\"},{\\\"value\\\":59,\\\"key\\\":\\\"Primary Artist ID\\\"},{\\\"value\\\":[\\\"2Pac\\\"],\\\"key\\\":\\\"Primary Artists\\\"},{\\\"value\\\":[59],\\\"key\\\":\\\"Primary Artists IDs\\\"},{\\\"value\\\":\\\"All Eyez On Me\\\",\\\"key\\\":\\\"Primary Album\\\"},{\\\"value\\\":38,\\\"key\\\":\\\"Primary Album ID\\\"},{\\\"value\\\":\\\"rap\\\",\\\"key\\\":\\\"Tag\\\"},{\\\"value\\\":\\\"rap\\\",\\\"key\\\":\\\"Primary Tag\\\"},{\\\"value\\\":1434,\\\"key\\\":\\\"Primary Tag ID\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"Music?\\\"},{\\\"value\\\":\\\"Song\\\",\\\"key\\\":\\\"Annotatable Type\\\"},{\\\"value\\\":6576,\\\"key\\\":\\\"Annotatable ID\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"featured_video\\\"},{\\\"value\\\":[],\\\"key\\\":\\\"cohort_ids\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"has_verified_callout\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"has_featured_annotation\\\"},{\\\"value\\\":\\\"2011-04-09T02:50:15Z\\\",\\\"key\\\":\\\"created_at\\\"},{\\\"value\\\":\\\"2011-04-01\\\",\\\"key\\\":\\\"created_month\\\"},{\\\"value\\\":2011,\\\"key\\\":\\\"created_year\\\"},{\\\"value\\\":\\\"C\\\",\\\"key\\\":\\\"song_tier\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"Has Recirculated Articles\\\"},{\\\"value\\\":\\\"en\\\",\\\"key\\\":\\\"Lyrics Language\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"Has Apple Match\\\"},{\\\"value\\\":\\\"1996-02-13\\\",\\\"key\\\":\\\"Release Date\\\"},{\\\"value\\\":null,\\\"key\\\":\\\"NRM Tier\\\"},{\\\"value\\\":null,\\\"key\\\":\\\"NRM Target Date\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"Has Description\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"Has Youtube URL\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"Has Translation Q&A\\\"},{\\\"value\\\":35,\\\"key\\\":\\\"Comment Count\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"hot\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"has_recommendations\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"has_stubhub_artist\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"has_stubhub_link\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"Translation\\\"},{\\\"value\\\":\\\"mixpanel\\\",\\\"key\\\":\\\"recommendation_strategy\\\"},{\\\"value\\\":\\\"control\\\",\\\"key\\\":\\\"web_interstitial_variant\\\"},{\\\"value\\\":\\\"desktop_react\\\",\\\"key\\\":\\\"platform_variant\\\"}],\\\"title\\\":\\\"2Pac – All Eyez On Me Lyrics | Genius Lyrics\\\",\\\"path\\\":\\\"/2pac-all-eyez-on-me-lyrics\\\",\\\"pageType\\\":\\\"song\\\",\\\"initialAdUnits\\\":[\\\"desktop_song_inread\\\",\\\"desktop_song_inread2\\\",\\\"desktop_song_inread3\\\",\\\"desktop_song_leaderboard\\\",\\\"desktop_song_lyrics_footer\\\",\\\"desktop_song_marquee\\\",\\\"desktop_song_medium1\\\",\\\"desktop_song_sidebar_top\\\",\\\"desktop_song_recommended_song\\\",\\\"desktop_song_sponsored_minicharts\\\"],\\\"hotSongsLink\\\":\\\"/hot-songs\\\",\\\"headerBidPlacements\\\":[],\\\"dmpDataLayer\\\":{\\\"page\\\":{\\\"type\\\":\\\"song\\\"}},\\\"controllerAndAction\\\":\\\"songs#show\\\",\\\"chartbeat\\\":{\\\"title\\\":\\\"2Pac – All Eyez On Me Lyrics | Genius Lyrics\\\",\\\"sections\\\":\\\"songs,tag:rap\\\",\\\"authors\\\":\\\"2Pac,Big Syke\\\"},\\\"unreviewedTopAnnotations\\\":{}},\\\"entities\\\":{\\\"artists\\\":{\\\"59\\\":{\\\"url\\\":\\\"https://genius.com/artists/2pac\\\",\\\"slug\\\":\\\"2pac\\\",\\\"name\\\":\\\"2Pac\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"0\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/9118a64d842f1611d83f5090989a3475.788x788x1.jpg\\\",\\\"id\\\":59,\\\"headerImageUrl\\\":\\\"https://images.genius.com/66dab5428172d59b83ed49304aacfa05.932x718x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/59\\\",\\\"type\\\":\\\"artist\\\"}},\\\"songs\\\":{\\\"244\\\":{\\\"url\\\":\\\"https://genius.com/2pac-california-love-lyrics\\\",\\\"title\\\":\\\"California Love\\\",\\\"path\\\":\\\"/2pac-california-love-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":244,\\\"apiPath\\\":\\\"/songs/244\\\",\\\"type\\\":\\\"song\\\"},\\\"312\\\":{\\\"url\\\":\\\"https://genius.com/2pac-got-my-mind-made-up-lyrics\\\",\\\"title\\\":\\\"Got My Mind Made Up\\\",\\\"path\\\":\\\"/2pac-got-my-mind-made-up-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":312,\\\"apiPath\\\":\\\"/songs/312\\\",\\\"type\\\":\\\"song\\\"},\\\"389\\\":{\\\"url\\\":\\\"https://genius.com/2pac-2-of-amerikaz-most-wanted-lyrics\\\",\\\"title\\\":\\\"2 of Amerikaz Most Wanted\\\",\\\"path\\\":\\\"/2pac-2-of-amerikaz-most-wanted-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":389,\\\"apiPath\\\":\\\"/songs/389\\\",\\\"type\\\":\\\"song\\\"},\\\"544\\\":{\\\"url\\\":\\\"https://genius.com/2pac-cant-c-me-lyrics\\\",\\\"title\\\":\\\"Can’t C Me\\\",\\\"path\\\":\\\"/2pac-cant-c-me-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":544,\\\"apiPath\\\":\\\"/songs/544\\\",\\\"type\\\":\\\"song\\\"},\\\"567\\\":{\\\"url\\\":\\\"https://genius.com/2pac-whatz-ya-phone-lyrics\\\",\\\"title\\\":\\\"What’z Ya Phone #\\\",\\\"path\\\":\\\"/2pac-whatz-ya-phone-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":567,\\\"apiPath\\\":\\\"/songs/567\\\",\\\"type\\\":\\\"song\\\"},\\\"698\\\":{\\\"url\\\":\\\"https://genius.com/2pac-picture-me-rollin-lyrics\\\",\\\"title\\\":\\\"Picture Me Rollin’\\\",\\\"path\\\":\\\"/2pac-picture-me-rollin-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":698,\\\"apiPath\\\":\\\"/songs/698\\\",\\\"type\\\":\\\"song\\\"},\\\"712\\\":{\\\"url\\\":\\\"https://genius.com/2pac-how-do-u-want-it-lyrics\\\",\\\"title\\\":\\\"How Do U Want It\\\",\\\"path\\\":\\\"/2pac-how-do-u-want-it-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":712,\\\"apiPath\\\":\\\"/songs/712\\\",\\\"type\\\":\\\"song\\\"},\\\"780\\\":{\\\"url\\\":\\\"https://genius.com/2pac-ambitionz-az-a-ridah-lyrics\\\",\\\"title\\\":\\\"Ambitionz Az a Ridah\\\",\\\"path\\\":\\\"/2pac-ambitionz-az-a-ridah-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":780,\\\"apiPath\\\":\\\"/songs/780\\\",\\\"type\\\":\\\"song\\\"},\\\"819\\\":{\\\"url\\\":\\\"https://genius.com/2pac-heartz-of-men-lyrics\\\",\\\"title\\\":\\\"Heartz of Men\\\",\\\"path\\\":\\\"/2pac-heartz-of-men-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":819,\\\"apiPath\\\":\\\"/songs/819\\\",\\\"type\\\":\\\"song\\\"},\\\"843\\\":{\\\"url\\\":\\\"https://genius.com/2pac-thug-passion-lyrics\\\",\\\"title\\\":\\\"Thug Passion\\\",\\\"path\\\":\\\"/2pac-thug-passion-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":843,\\\"apiPath\\\":\\\"/songs/843\\\",\\\"type\\\":\\\"song\\\"},\\\"2626\\\":{\\\"url\\\":\\\"https://genius.com/2pac-only-god-can-judge-me-lyrics\\\",\\\"title\\\":\\\"Only God Can Judge Me\\\",\\\"path\\\":\\\"/2pac-only-god-can-judge-me-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":2626,\\\"apiPath\\\":\\\"/songs/2626\\\",\\\"type\\\":\\\"song\\\"},\\\"3428\\\":{\\\"url\\\":\\\"https://genius.com/2pac-tradin-war-stories-lyrics\\\",\\\"title\\\":\\\"Tradin’ War Stories\\\",\\\"path\\\":\\\"/2pac-tradin-war-stories-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":3428,\\\"apiPath\\\":\\\"/songs/3428\\\",\\\"type\\\":\\\"song\\\"},\\\"5377\\\":{\\\"url\\\":\\\"https://genius.com/2pac-i-aint-mad-at-cha-lyrics\\\",\\\"title\\\":\\\"I Ain’t Mad At Cha\\\",\\\"path\\\":\\\"/2pac-i-aint-mad-at-cha-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":5377,\\\"apiPath\\\":\\\"/songs/5377\\\",\\\"type\\\":\\\"song\\\"},\\\"5533\\\":{\\\"url\\\":\\\"https://genius.com/2pac-all-about-u-lyrics\\\",\\\"title\\\":\\\"All About U\\\",\\\"path\\\":\\\"/2pac-all-about-u-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":5533,\\\"apiPath\\\":\\\"/songs/5533\\\",\\\"type\\\":\\\"song\\\"},\\\"6568\\\":{\\\"url\\\":\\\"https://genius.com/2pac-aint-hard-2-find-lyrics\\\",\\\"title\\\":\\\"Ain’t Hard 2 Find\\\",\\\"path\\\":\\\"/2pac-aint-hard-2-find-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6568,\\\"apiPath\\\":\\\"/songs/6568\\\",\\\"type\\\":\\\"song\\\"},\\\"6569\\\":{\\\"url\\\":\\\"https://genius.com/2pac-check-out-time-lyrics\\\",\\\"title\\\":\\\"Check Out Time\\\",\\\"path\\\":\\\"/2pac-check-out-time-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6569,\\\"apiPath\\\":\\\"/songs/6569\\\",\\\"type\\\":\\\"song\\\"},\\\"6573\\\":{\\\"url\\\":\\\"https://genius.com/2pac-life-goes-on-lyrics\\\",\\\"title\\\":\\\"Life Goes On\\\",\\\"path\\\":\\\"/2pac-life-goes-on-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6573,\\\"apiPath\\\":\\\"/songs/6573\\\",\\\"type\\\":\\\"song\\\"},\\\"6574\\\":{\\\"url\\\":\\\"https://genius.com/2pac-heaven-aint-hard-2-find-lyrics\\\",\\\"title\\\":\\\"Heaven Ain’t Hard 2 Find\\\",\\\"path\\\":\\\"/2pac-heaven-aint-hard-2-find-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6574,\\\"apiPath\\\":\\\"/songs/6574\\\",\\\"type\\\":\\\"song\\\"},\\\"6575\\\":{\\\"url\\\":\\\"https://genius.com/2pac-holla-at-me-lyrics\\\",\\\"title\\\":\\\"Holla At Me\\\",\\\"path\\\":\\\"/2pac-holla-at-me-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6575,\\\"apiPath\\\":\\\"/songs/6575\\\",\\\"type\\\":\\\"song\\\"},\\\"6576\\\":{\\\"url\\\":\\\"https://genius.com/2pac-all-eyez-on-me-lyrics\\\",\\\"title\\\":\\\"All Eyez On Me\\\",\\\"path\\\":\\\"/2pac-all-eyez-on-me-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6576,\\\"apiPath\\\":\\\"/songs/6576\\\",\\\"type\\\":\\\"song\\\",\\\"writerArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Big-syke\\\",\\\"slug\\\":\\\"Big-syke\\\",\\\"name\\\":\\\"Big Syke\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"b\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/d45c72b156862a65719161af952e83a1.300x300x1.png\\\",\\\"id\\\":2571,\\\"headerImageUrl\\\":\\\"https://images.genius.com/c48b237b70dd1767f94bd3b13c0f4178.400x296x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2571\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/2pac\\\",\\\"slug\\\":\\\"2pac\\\",\\\"name\\\":\\\"2Pac\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"0\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/9118a64d842f1611d83f5090989a3475.788x788x1.jpg\\\",\\\"id\\\":59,\\\"headerImageUrl\\\":\\\"https://images.genius.com/66dab5428172d59b83ed49304aacfa05.932x718x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/59\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/Johnny-j\\\",\\\"slug\\\":\\\"Johnny-j\\\",\\\"name\\\":\\\"Johnny J\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"j\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/fb4f01398ed79ee7419123eec97a24c6.246x246x1.jpg\\\",\\\"id\\\":8976,\\\"headerImageUrl\\\":\\\"https://images.genius.com/e13e8ccdd6b42736a128f0339f9bac53.300x300x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/8976\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/Jp-pennington\\\",\\\"slug\\\":\\\"Jp-pennington\\\",\\\"name\\\":\\\"J.P. Pennington\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"j\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/757740f0aa86a2ca753acc9757e9ee0d.428x428x1.png\\\",\\\"id\\\":609422,\\\"headerImageUrl\\\":\\\"https://images.genius.com/757740f0aa86a2ca753acc9757e9ee0d.428x428x1.png\\\",\\\"apiPath\\\":\\\"/artists/609422\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/Thor-baldursson\\\",\\\"slug\\\":\\\"Thor-baldursson\\\",\\\"name\\\":\\\"Thor Baldursson\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"t\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/e8327b08b0487408b4c959e8f239a314.200x200x1.jpg\\\",\\\"id\\\":1496546,\\\"headerImageUrl\\\":\\\"https://images.genius.com/9780f03a8f88761af3a3e1baf1652394.200x272x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/1496546\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/Jurgen-koppers\\\",\\\"slug\\\":\\\"Jurgen-koppers\\\",\\\"name\\\":\\\"Jürgen Koppers\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"j\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/da26eb7bece27237e7f0acfb72afbb11.299x299x1.jpg\\\",\\\"id\\\":1184397,\\\"headerImageUrl\\\":\\\"https://images.genius.com/d41da8cf746c763d5a4e42899d06e4fd.176x176x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/1184397\\\",\\\"type\\\":\\\"artist\\\"}],\\\"verifiedLyricsBy\\\":[],\\\"verifiedContributors\\\":[],\\\"verifiedAnnotationsBy\\\":[],\\\"translationSongs\\\":[{\\\"url\\\":\\\"https://genius.com/Genius-brasil-traducoes-2pac-all-eyez-on-me-ft-big-syke-traducao-em-portugues-lyrics\\\",\\\"title\\\":\\\"2Pac - All Eyez on Me ft. Big Syke (Tradução em Português)\\\",\\\"path\\\":\\\"/Genius-brasil-traducoes-2pac-all-eyez-on-me-ft-big-syke-traducao-em-portugues-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"language\\\":\\\"pt\\\",\\\"id\\\":6314513,\\\"apiPath\\\":\\\"/songs/6314513\\\",\\\"type\\\":\\\"song\\\"}],\\\"topScholar\\\":{\\\"user\\\":{\\\"currentUserMetadata\\\":{\\\"interactions\\\":{\\\"following\\\":false},\\\"excludedPermissions\\\":[\\\"follow\\\"],\\\"permissions\\\":[]},\\\"url\\\":\\\"https://genius.com/SmashBeezy\\\",\\\"roleForDisplay\\\":\\\"editor\\\",\\\"name\\\":\\\"SmashBeezy\\\",\\\"login\\\":\\\"SmashBeezy\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"iq\\\":3686915,\\\"id\\\":3492882,\\\"humanReadableRoleForDisplay\\\":\\\"Editor\\\",\\\"headerImageUrl\\\":\\\"https://images.genius.com/avatars/medium/981605b56878ef16a812df453300d0fc\\\",\\\"avatar\\\":{\\\"medium\\\":{\\\"boundingBox\\\":{\\\"height\\\":400,\\\"width\\\":300},\\\"url\\\":\\\"https://images.genius.com/avatars/medium/981605b56878ef16a812df453300d0fc\\\"},\\\"small\\\":{\\\"boundingBox\\\":{\\\"height\\\":100,\\\"width\\\":100},\\\"url\\\":\\\"https://images.genius.com/avatars/small/981605b56878ef16a812df453300d0fc\\\"},\\\"thumb\\\":{\\\"boundingBox\\\":{\\\"height\\\":32,\\\"width\\\":32},\\\"url\\\":\\\"https://images.genius.com/avatars/thumb/981605b56878ef16a812df453300d0fc\\\"},\\\"tiny\\\":{\\\"boundingBox\\\":{\\\"height\\\":16,\\\"width\\\":16},\\\"url\\\":\\\"https://images.genius.com/avatars/tiny/981605b56878ef16a812df453300d0fc\\\"}},\\\"apiPath\\\":\\\"/users/3492882\\\",\\\"aboutMeSummary\\\":\\\"\\\",\\\"type\\\":\\\"user\\\"},\\\"pinnedRole\\\":null,\\\"attributionValue\\\":367.3,\\\"type\\\":\\\"user_attribution\\\"},\\\"tags\\\":[{\\\"url\\\":\\\"https://genius.com/tags/hip-hop\\\",\\\"primary\\\":false,\\\"name\\\":\\\"Hip-Hop\\\",\\\"id\\\":3783,\\\"type\\\":\\\"tag\\\"},{\\\"url\\\":\\\"https://genius.com/tags/hardcore-rap\\\",\\\"primary\\\":false,\\\"name\\\":\\\"Hardcore Rap\\\",\\\"id\\\":3171,\\\"type\\\":\\\"tag\\\"},{\\\"url\\\":\\\"https://genius.com/tags/west-coast-rap\\\",\\\"primary\\\":false,\\\"name\\\":\\\"West Coast Rap\\\",\\\"id\\\":798,\\\"type\\\":\\\"tag\\\"},{\\\"url\\\":\\\"https://genius.com/tags/gangsta-rap\\\",\\\"primary\\\":false,\\\"name\\\":\\\"Gangsta Rap\\\",\\\"id\\\":2741,\\\"type\\\":\\\"tag\\\"},{\\\"url\\\":\\\"https://genius.com/tags/rap\\\",\\\"primary\\\":true,\\\"name\\\":\\\"Rap\\\",\\\"id\\\":1434,\\\"type\\\":\\\"tag\\\"}],\\\"songRelationships\\\":[{\\\"songs\\\":[{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Linda-clifford\\\",\\\"slug\\\":\\\"Linda-clifford\\\",\\\"name\\\":\\\"Linda Clifford\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"l\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/1cb8607f8a54dc8f0440b27a05c15c71.566x566x1.jpg\\\",\\\"id\\\":369098,\\\"headerImageUrl\\\":\\\"https://images.genius.com/28e254f9073046ed5048863ab3b50af8.594x566x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/369098\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Linda-clifford\\\",\\\"slug\\\":\\\"Linda-clifford\\\",\\\"name\\\":\\\"Linda Clifford\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"l\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/1cb8607f8a54dc8f0440b27a05c15c71.566x566x1.jpg\\\",\\\"id\\\":369098,\\\"headerImageUrl\\\":\\\"https://images.genius.com/28e254f9073046ed5048863ab3b50af8.594x566x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/369098\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Linda-clifford-never-gonna-stop-lyrics\\\",\\\"updatedByHumanAt\\\":1690103304,\\\"titleWithFeatured\\\":\\\"Never Gonna Stop\\\",\\\"title\\\":\\\"Never Gonna Stop\\\",\\\"stats\\\":{\\\"pageviews\\\":5959,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/94dba2ab7e8af5b598690707f2f26aba.999x999x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/94dba2ab7e8af5b598690707f2f26aba.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Aug. 24, 1979\\\",\\\"releaseDateForDisplay\\\":\\\"August 24, 1979\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":24,\\\"month\\\":8,\\\"year\\\":1979},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Linda-clifford-never-gonna-stop-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Linda Clifford\\\",\\\"path\\\":\\\"/Linda-clifford-never-gonna-stop-lyrics\\\",\\\"lyricsUpdatedAt\\\":1661099794,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":923739,\\\"instrumental\\\":false,\\\"id\\\":4609454,\\\"headerImageUrl\\\":\\\"https://images.genius.com/94dba2ab7e8af5b598690707f2f26aba.999x999x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/94dba2ab7e8af5b598690707f2f26aba.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"Never Gonna Stop by Linda Clifford\\\",\\\"artistNames\\\":\\\"Linda Clifford\\\",\\\"apiPath\\\":\\\"/songs/4609454\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"}],\\\"url\\\":\\\"https://genius.com/2pac-all-eyez-on-me-sample/samples\\\",\\\"type\\\":\\\"song_relationship\\\",\\\"relationshipType\\\":\\\"samples\\\"},{\\\"songs\\\":[{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Tupac-thug-theory\\\",\\\"slug\\\":\\\"Tupac-thug-theory\\\",\\\"name\\\":\\\"Tupac Thug Theory\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"t\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/7e3f73b8e11fed26c795e9bc744213cf.225x225x1.jpg\\\",\\\"id\\\":1097753,\\\"headerImageUrl\\\":\\\"https://images.genius.com/7e3f73b8e11fed26c795e9bc744213cf.225x225x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/1097753\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Tupac-thug-theory\\\",\\\"slug\\\":\\\"Tupac-thug-theory\\\",\\\"name\\\":\\\"Tupac Thug Theory\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"t\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/7e3f73b8e11fed26c795e9bc744213cf.225x225x1.jpg\\\",\\\"id\\\":1097753,\\\"headerImageUrl\\\":\\\"https://images.genius.com/7e3f73b8e11fed26c795e9bc744213cf.225x225x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/1097753\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[{\\\"iq\\\":2680,\\\"url\\\":\\\"https://genius.com/artists/Ice-cube\\\",\\\"slug\\\":\\\"Ice-cube\\\",\\\"name\\\":\\\"Ice Cube\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"i\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/ad69cae78eaa818e57280d0a9ed8ec67.770x770x1.jpg\\\",\\\"id\\\":186,\\\"headerImageUrl\\\":\\\"https://images.genius.com/705e8afd565c80f7780066d589fc2d0a.1000x333x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/186\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/2pac\\\",\\\"slug\\\":\\\"2pac\\\",\\\"name\\\":\\\"2Pac\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"0\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/9118a64d842f1611d83f5090989a3475.788x788x1.jpg\\\",\\\"id\\\":59,\\\"headerImageUrl\\\":\\\"https://images.genius.com/66dab5428172d59b83ed49304aacfa05.932x718x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/59\\\",\\\"type\\\":\\\"artist\\\"},{\\\"iq\\\":272507,\\\"url\\\":\\\"https://genius.com/artists/Eminem\\\",\\\"slug\\\":\\\"Eminem\\\",\\\"name\\\":\\\"Eminem\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"e\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/37f958ab28031af87d13b5d224749878.533x533x1.jpg\\\",\\\"id\\\":45,\\\"headerImageUrl\\\":\\\"https://images.genius.com/7879bf220b8614a7fe4617006f132a92.1000x391x1.png\\\",\\\"apiPath\\\":\\\"/artists/45\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/Eazy-e\\\",\\\"slug\\\":\\\"Eazy-e\\\",\\\"name\\\":\\\"Eazy-E\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"e\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/eea688032f52b16d798baa54f94cc16e.858x858x1.jpg\\\",\\\"id\\\":496,\\\"headerImageUrl\\\":\\\"https://images.genius.com/908c348f9d3a11cb477771500880a427.577x433x1.png\\\",\\\"apiPath\\\":\\\"/artists/496\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/Mc-ren\\\",\\\"slug\\\":\\\"Mc-ren\\\",\\\"name\\\":\\\"MC Ren\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"m\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/0775f581c14a33e3c5ed6bec9a85de49.353x353x1.jpg\\\",\\\"id\\\":1436,\\\"headerImageUrl\\\":\\\"https://images.genius.com/a722d2901f7522aa22febf9036d8ef9e.1000x563x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/1436\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/The-notorious-big\\\",\\\"slug\\\":\\\"The-notorious-big\\\",\\\"name\\\":\\\"The Notorious B.I.G.\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"n\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/cf57b0c402cf48c03a8170d6b7d946d4.999x999x1.jpg\\\",\\\"id\\\":22,\\\"headerImageUrl\\\":\\\"https://images.genius.com/1b66a66f14427bb384ff3c8ccecc05f7.1000x333x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/22\\\",\\\"type\\\":\\\"artist\\\"},{\\\"iq\\\":5223,\\\"url\\\":\\\"https://genius.com/artists/Snoop-dogg\\\",\\\"slug\\\":\\\"Snoop-dogg\\\",\\\"name\\\":\\\"Snoop Dogg\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"s\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/91bd22f5e53a3ea3cb1436de8f4a3722.1000x1000x1.jpg\\\",\\\"id\\\":46,\\\"headerImageUrl\\\":\\\"https://images.genius.com/4a8a11f406cfaa80085daaee24b78863.1000x563x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/46\\\",\\\"type\\\":\\\"artist\\\"}],\\\"url\\\":\\\"https://genius.com/Tupac-thug-theory-2pac-ft-ice-cube-gangsta-rap-made-me-do-it-ft-eminem-eazy-e-biggie-snoop-dogg-lyrics\\\",\\\"updatedByHumanAt\\\":1712594111,\\\"titleWithFeatured\\\":\\\"2Pac ft. Ice Cube - Gangsta Rap Made Me Do It (ft. Eminem, Eazy E, Biggie, Snoop Dogg) (Ft. 2Pac, Eazy-E, Eminem, Ice Cube, MC Ren, The Notorious B.I.G. & Snoop Dogg)\\\",\\\"title\\\":\\\"2Pac ft. Ice Cube - Gangsta Rap Made Me Do It (ft. Eminem, Eazy E, Biggie, Snoop Dogg)\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/f2f6842d782c19baf18a2004e6726d7a.320x180x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/f2f6842d782c19baf18a2004e6726d7a.300x169x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Jun. 18, 2017\\\",\\\"releaseDateForDisplay\\\":\\\"June 18, 2017\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":18,\\\"month\\\":6,\\\"year\\\":2017},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Tupac-thug-theory-2pac-ft-ice-cube-gangsta-rap-made-me-do-it-ft-eminem-eazy-e-biggie-snoop-dogg-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Tupac Thug Theory\\\",\\\"path\\\":\\\"/Tupac-thug-theory-2pac-ft-ice-cube-gangsta-rap-made-me-do-it-ft-eminem-eazy-e-biggie-snoop-dogg-lyrics\\\",\\\"lyricsUpdatedAt\\\":1712166102,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":19017107,\\\"instrumental\\\":false,\\\"id\\\":10238644,\\\"headerImageUrl\\\":\\\"https://images.genius.com/f2f6842d782c19baf18a2004e6726d7a.320x180x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/f2f6842d782c19baf18a2004e6726d7a.300x169x1.jpg\\\",\\\"fullTitle\\\":\\\"2Pac ft. Ice Cube - Gangsta Rap Made Me Do It (ft. Eminem, Eazy E, Biggie, Snoop Dogg) by Tupac Thug Theory (Ft. 2Pac, Eazy-E, Eminem, Ice Cube, MC Ren, The Notorious B.I.G. & Snoop Dogg)\\\",\\\"artistNames\\\":\\\"Tupac Thug Theory (Ft. 2Pac, Eazy-E, Eminem, Ice Cube, MC Ren, The Notorious B.I.G. & Snoop Dogg)\\\",\\\"apiPath\\\":\\\"/songs/10238644\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Young-dre-the-truth\\\",\\\"slug\\\":\\\"Young-dre-the-truth\\\",\\\"name\\\":\\\"Young Dre The Truth\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"y\\\",\\\"imageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"id\\\":479577,\\\"headerImageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"apiPath\\\":\\\"/artists/479577\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Young-dre-the-truth\\\",\\\"slug\\\":\\\"Young-dre-the-truth\\\",\\\"name\\\":\\\"Young Dre The Truth\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"y\\\",\\\"imageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"id\\\":479577,\\\"headerImageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"apiPath\\\":\\\"/artists/479577\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[{\\\"iq\\\":6821,\\\"url\\\":\\\"https://genius.com/artists/Bj-the-chicago-kid\\\",\\\"slug\\\":\\\"Bj-the-chicago-kid\\\",\\\"name\\\":\\\"BJ the Chicago Kid\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"b\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/972dde8548fb25ce53485284c4437932.1000x1000x1.jpg\\\",\\\"id\\\":1955,\\\"headerImageUrl\\\":\\\"https://images.genius.com/c3046ba14e8e6f69b27924083632cb54.1000x562x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/1955\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/2pac\\\",\\\"slug\\\":\\\"2pac\\\",\\\"name\\\":\\\"2Pac\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"0\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/9118a64d842f1611d83f5090989a3475.788x788x1.jpg\\\",\\\"id\\\":59,\\\"headerImageUrl\\\":\\\"https://images.genius.com/66dab5428172d59b83ed49304aacfa05.932x718x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/59\\\",\\\"type\\\":\\\"artist\\\"}],\\\"url\\\":\\\"https://genius.com/Young-dre-the-truth-all-eyez-on-me-the-truth-lyrics\\\",\\\"updatedByHumanAt\\\":1704603103,\\\"titleWithFeatured\\\":\\\"All Eyez On Me (The Truth) (Ft. 2Pac & BJ the Chicago Kid)\\\",\\\"title\\\":\\\"All Eyez On Me (The Truth)\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/1b1c406048e9ae8c8a001aa5796dddc2.730x730x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/1b1c406048e9ae8c8a001aa5796dddc2.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"2009\\\",\\\"releaseDateForDisplay\\\":\\\"2009\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":null,\\\"month\\\":null,\\\"year\\\":2009},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Young-dre-the-truth-all-eyez-on-me-the-truth-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Young Dre The Truth\\\",\\\"path\\\":\\\"/Young-dre-the-truth-all-eyez-on-me-the-truth-lyrics\\\",\\\"lyricsUpdatedAt\\\":1670311836,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":9517279,\\\"instrumental\\\":false,\\\"id\\\":5456449,\\\"headerImageUrl\\\":\\\"https://images.genius.com/1b1c406048e9ae8c8a001aa5796dddc2.730x730x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/1b1c406048e9ae8c8a001aa5796dddc2.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"All Eyez On Me (The Truth) by Young Dre The Truth (Ft. 2Pac & BJ the Chicago Kid)\\\",\\\"artistNames\\\":\\\"Young Dre The Truth (Ft. 2Pac & BJ the Chicago Kid)\\\",\\\"apiPath\\\":\\\"/songs/5456449\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Bonez-mc-and-gzuz\\\",\\\"slug\\\":\\\"Bonez-mc-and-gzuz\\\",\\\"name\\\":\\\"Bonez MC & Gzuz\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"b\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/d426b95454953b85e3ac95f462e9db3f.1000x1000x1.jpg\\\",\\\"id\\\":2084456,\\\"headerImageUrl\\\":\\\"https://images.genius.com/2015c544da63b0b9b01f82aa79ace49e.563x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2084456\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Bonez-mc-and-gzuz\\\",\\\"slug\\\":\\\"Bonez-mc-and-gzuz\\\",\\\"name\\\":\\\"Bonez MC & Gzuz\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"b\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/d426b95454953b85e3ac95f462e9db3f.1000x1000x1.jpg\\\",\\\"id\\\":2084456,\\\"headerImageUrl\\\":\\\"https://images.genius.com/2015c544da63b0b9b01f82aa79ace49e.563x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2084456\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Bonez-mc-and-gzuz-intro-high-and-hungrig-3-lyrics\\\",\\\"updatedByHumanAt\\\":1714508165,\\\"titleWithFeatured\\\":\\\"Intro (High & Hungrig 3)\\\",\\\"title\\\":\\\"Intro (High & Hungrig 3)\\\",\\\"stats\\\":{\\\"pageviews\\\":8192,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":1},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/ccbf04e569d372740900cce4b99a4b3b.1000x1000x1.png\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/ccbf04e569d372740900cce4b99a4b3b.300x300x1.png\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Apr. 28, 2023\\\",\\\"releaseDateForDisplay\\\":\\\"April 28, 2023\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":28,\\\"month\\\":4,\\\"year\\\":2023},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Bonez-mc-and-gzuz-intro-high-and-hungrig-3-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Bonez MC & Gzuz\\\",\\\"path\\\":\\\"/Bonez-mc-and-gzuz-intro-high-and-hungrig-3-lyrics\\\",\\\"lyricsUpdatedAt\\\":1714508165,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":10976632,\\\"instrumental\\\":false,\\\"id\\\":6735432,\\\"headerImageUrl\\\":\\\"https://images.genius.com/ccbf04e569d372740900cce4b99a4b3b.1000x1000x1.png\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/ccbf04e569d372740900cce4b99a4b3b.300x300x1.png\\\",\\\"fullTitle\\\":\\\"Intro (High & Hungrig 3) by Bonez MC & Gzuz\\\",\\\"artistNames\\\":\\\"Bonez MC & Gzuz\\\",\\\"apiPath\\\":\\\"/songs/6735432\\\",\\\"annotationCount\\\":1,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Bonez-mc-gzuz-and-maxwell\\\",\\\"slug\\\":\\\"Bonez-mc-gzuz-and-maxwell\\\",\\\"name\\\":\\\"Bonez MC, Gzuz & Maxwell\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"b\\\",\\\"imageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"id\\\":3085452,\\\"headerImageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"apiPath\\\":\\\"/artists/3085452\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Bonez-mc-gzuz-and-maxwell\\\",\\\"slug\\\":\\\"Bonez-mc-gzuz-and-maxwell\\\",\\\"name\\\":\\\"Bonez MC, Gzuz & Maxwell\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"b\\\",\\\"imageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"id\\\":3085452,\\\"headerImageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"apiPath\\\":\\\"/artists/3085452\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Bonez-mc-gzuz-and-maxwell-das-ist-gang-lyrics\\\",\\\"updatedByHumanAt\\\":1710370039,\\\"titleWithFeatured\\\":\\\"Das ist Gang\\\",\\\"title\\\":\\\"Das ist Gang\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/ccbf04e569d372740900cce4b99a4b3b.1000x1000x1.png\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/ccbf04e569d372740900cce4b99a4b3b.300x300x1.png\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Apr. 28, 2023\\\",\\\"releaseDateForDisplay\\\":\\\"April 28, 2023\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":28,\\\"month\\\":4,\\\"year\\\":2023},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Bonez-mc-gzuz-and-maxwell-das-ist-gang-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Bonez MC, Gzuz & Maxwell\\\",\\\"path\\\":\\\"/Bonez-mc-gzuz-and-maxwell-das-ist-gang-lyrics\\\",\\\"lyricsUpdatedAt\\\":1682847466,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":4624615,\\\"instrumental\\\":false,\\\"id\\\":8956442,\\\"headerImageUrl\\\":\\\"https://images.genius.com/ccbf04e569d372740900cce4b99a4b3b.1000x1000x1.png\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/ccbf04e569d372740900cce4b99a4b3b.300x300x1.png\\\",\\\"fullTitle\\\":\\\"Das ist Gang by Bonez MC, Gzuz & Maxwell\\\",\\\"artistNames\\\":\\\"Bonez MC, Gzuz & Maxwell\\\",\\\"apiPath\\\":\\\"/songs/8956442\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/101barz\\\",\\\"slug\\\":\\\"101barz\\\",\\\"name\\\":\\\"101Barz\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"0\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/9e636705a50635b42c3f24d6431a8d12.288x288x1.png\\\",\\\"id\\\":134675,\\\"headerImageUrl\\\":\\\"https://images.genius.com/70dc982cdefaf4d98513d2f70b9060e1.1000x525x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/134675\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/101barz\\\",\\\"slug\\\":\\\"101barz\\\",\\\"name\\\":\\\"101Barz\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"0\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/9e636705a50635b42c3f24d6431a8d12.288x288x1.png\\\",\\\"id\\\":134675,\\\"headerImageUrl\\\":\\\"https://images.genius.com/70dc982cdefaf4d98513d2f70b9060e1.1000x525x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/134675\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Sepa\\\",\\\"slug\\\":\\\"Sepa\\\",\\\"name\\\":\\\"Sepa\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"s\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/a2f85a23982b35e793b6f559404d1c61.371x371x1.jpg\\\",\\\"id\\\":481019,\\\"headerImageUrl\\\":\\\"https://images.genius.com/a2f85a23982b35e793b6f559404d1c61.371x371x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/481019\\\",\\\"type\\\":\\\"artist\\\"}],\\\"url\\\":\\\"https://genius.com/101barz-sepa-wintersessie-2018-lyrics\\\",\\\"updatedByHumanAt\\\":1676728232,\\\"titleWithFeatured\\\":\\\"Sepa - Wintersessie 2018 (Ft. Sepa)\\\",\\\"title\\\":\\\"Sepa - Wintersessie 2018\\\",\\\"stats\\\":{\\\"pageviews\\\":9389,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":1},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/c71f41eebfe7444b684fa3a8a0de3936.1000x563x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/c71f41eebfe7444b684fa3a8a0de3936.300x169x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Mar. 9, 2018\\\",\\\"releaseDateForDisplay\\\":\\\"March 9, 2018\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":9,\\\"month\\\":3,\\\"year\\\":2018},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/101barz-sepa-wintersessie-2018-sample\\\",\\\"pyongsCount\\\":2,\\\"primaryArtistNames\\\":\\\"101Barz\\\",\\\"path\\\":\\\"/101barz-sepa-wintersessie-2018-lyrics\\\",\\\"lyricsUpdatedAt\\\":1676728232,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":3948561,\\\"instrumental\\\":false,\\\"id\\\":3582525,\\\"headerImageUrl\\\":\\\"https://images.genius.com/c71f41eebfe7444b684fa3a8a0de3936.1000x563x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/c71f41eebfe7444b684fa3a8a0de3936.300x169x1.jpg\\\",\\\"fullTitle\\\":\\\"Sepa - Wintersessie 2018 by 101Barz (Ft. Sepa)\\\",\\\"artistNames\\\":\\\"101Barz (Ft. Sepa)\\\",\\\"apiPath\\\":\\\"/songs/3582525\\\",\\\"annotationCount\\\":1,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Jari\\\",\\\"slug\\\":\\\"Jari\\\",\\\"name\\\":\\\"Jari\\$\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"j\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/20d46d860f3a1c7a6240a4b6c85f1667.1000x1000x1.jpg\\\",\\\"id\\\":2660235,\\\"headerImageUrl\\\":\\\"https://images.genius.com/04618915f7cc9931726adae5a3737211.750x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2660235\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Jari\\\",\\\"slug\\\":\\\"Jari\\\",\\\"name\\\":\\\"Jari\\$\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"j\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/20d46d860f3a1c7a6240a4b6c85f1667.1000x1000x1.jpg\\\",\\\"id\\\":2660235,\\\"headerImageUrl\\\":\\\"https://images.genius.com/04618915f7cc9931726adae5a3737211.750x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2660235\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Jari-frisco-shit-freestyle-lyrics\\\",\\\"updatedByHumanAt\\\":1649147546,\\\"titleWithFeatured\\\":\\\"Frisco Shit Freestyle\\\",\\\"title\\\":\\\"Frisco Shit Freestyle\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/dd5edc23e6105776082bfab483d4bbf9.1000x1000x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/dd5edc23e6105776082bfab483d4bbf9.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Apr. 4, 2022\\\",\\\"releaseDateForDisplay\\\":\\\"April 4, 2022\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":4,\\\"month\\\":4,\\\"year\\\":2022},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Jari-frisco-shit-freestyle-sample\\\",\\\"pyongsCount\\\":1,\\\"primaryArtistNames\\\":\\\"Jari\\$\\\",\\\"path\\\":\\\"/Jari-frisco-shit-freestyle-lyrics\\\",\\\"lyricsUpdatedAt\\\":1649146868,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":13008022,\\\"instrumental\\\":false,\\\"id\\\":7872240,\\\"headerImageUrl\\\":\\\"https://images.genius.com/dd5edc23e6105776082bfab483d4bbf9.1000x1000x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/dd5edc23e6105776082bfab483d4bbf9.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"Frisco Shit Freestyle by Jari\\$\\\",\\\"artistNames\\\":\\\"Jari\\$\\\",\\\"apiPath\\\":\\\"/songs/7872240\\\",\\\"annotationCount\\\":1,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":10068,\\\"url\\\":\\\"https://genius.com/artists/Tory-lanez\\\",\\\"slug\\\":\\\"Tory-lanez\\\",\\\"name\\\":\\\"Tory Lanez\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"t\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/83554e3a9e4e12f26685ec7c5247f1f5.467x467x1.jpg\\\",\\\"id\\\":1632,\\\"headerImageUrl\\\":\\\"https://images.genius.com/b25b80bed02917634a3387773e2e3e40.700x467x14.gif\\\",\\\"apiPath\\\":\\\"/artists/1632\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":10068,\\\"url\\\":\\\"https://genius.com/artists/Tory-lanez\\\",\\\"slug\\\":\\\"Tory-lanez\\\",\\\"name\\\":\\\"Tory Lanez\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"t\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/83554e3a9e4e12f26685ec7c5247f1f5.467x467x1.jpg\\\",\\\"id\\\":1632,\\\"headerImageUrl\\\":\\\"https://images.genius.com/b25b80bed02917634a3387773e2e3e40.700x467x14.gif\\\",\\\"apiPath\\\":\\\"/artists/1632\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Tory-lanez-florida-shit-lyrics\\\",\\\"updatedByHumanAt\\\":1717863553,\\\"titleWithFeatured\\\":\\\"Florida Shit\\\",\\\"title\\\":\\\"Florida Shit\\\",\\\"stats\\\":{\\\"pageviews\\\":6363,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":2},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/74d13534bf3ddceda2e7ce40941ae92f.1000x1000x1.png\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/74d13534bf3ddceda2e7ce40941ae92f.300x300x1.png\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Apr. 1, 2022\\\",\\\"releaseDateForDisplay\\\":\\\"April 1, 2022\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":1,\\\"month\\\":4,\\\"year\\\":2022},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Tory-lanez-florida-shit-sample\\\",\\\"pyongsCount\\\":1,\\\"primaryArtistNames\\\":\\\"Tory Lanez\\\",\\\"path\\\":\\\"/Tory-lanez-florida-shit-lyrics\\\",\\\"lyricsUpdatedAt\\\":1673626541,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":10316240,\\\"instrumental\\\":false,\\\"id\\\":7854427,\\\"headerImageUrl\\\":\\\"https://images.genius.com/74d13534bf3ddceda2e7ce40941ae92f.1000x1000x1.png\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/74d13534bf3ddceda2e7ce40941ae92f.300x300x1.png\\\",\\\"fullTitle\\\":\\\"Florida Shit by Tory Lanez\\\",\\\"artistNames\\\":\\\"Tory Lanez\\\",\\\"apiPath\\\":\\\"/songs/7854427\\\",\\\"annotationCount\\\":3,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/King-keil\\\",\\\"slug\\\":\\\"King-keil\\\",\\\"name\\\":\\\"King Keil\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"k\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/cd35ef14a1d851c7935fb2ef378a4206.570x300x1.jpg\\\",\\\"id\\\":34243,\\\"headerImageUrl\\\":\\\"https://images.genius.com/cd35ef14a1d851c7935fb2ef378a4206.570x300x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/34243\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/King-keil\\\",\\\"slug\\\":\\\"King-keil\\\",\\\"name\\\":\\\"King Keil\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"k\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/cd35ef14a1d851c7935fb2ef378a4206.570x300x1.jpg\\\",\\\"id\\\":34243,\\\"headerImageUrl\\\":\\\"https://images.genius.com/cd35ef14a1d851c7935fb2ef378a4206.570x300x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/34243\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/King-keil-leben-im-beton-lyrics\\\",\\\"updatedByHumanAt\\\":1641314660,\\\"titleWithFeatured\\\":\\\"Leben im Beton\\\",\\\"title\\\":\\\"Leben im Beton\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/3d79a851e2b5fa594141fb5bb665f4ba.500x500x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/3d79a851e2b5fa594141fb5bb665f4ba.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Sep. 3, 2021\\\",\\\"releaseDateForDisplay\\\":\\\"September 3, 2021\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":3,\\\"month\\\":9,\\\"year\\\":2021},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/King-keil-leben-im-beton-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"King Keil\\\",\\\"path\\\":\\\"/King-keil-leben-im-beton-lyrics\\\",\\\"lyricsUpdatedAt\\\":1641314660,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":15622079,\\\"instrumental\\\":false,\\\"id\\\":7557895,\\\"headerImageUrl\\\":\\\"https://images.genius.com/40bd6dff3ab8056f2848eba7b7039708.1000x429x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/40bd6dff3ab8056f2848eba7b7039708.300x129x1.jpg\\\",\\\"fullTitle\\\":\\\"Leben im Beton by King Keil\\\",\\\"artistNames\\\":\\\"King Keil\\\",\\\"apiPath\\\":\\\"/songs/7557895\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/18-karat\\\",\\\"slug\\\":\\\"18-karat\\\",\\\"name\\\":\\\"18 Karat\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"0\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/5ab9c97867c8659d1ab0603cb041b677.555x555x1.jpg\\\",\\\"id\\\":547530,\\\"headerImageUrl\\\":\\\"https://images.genius.com/2cafbe0c8915ec59da669ffab7547cce.1000x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/547530\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/18-karat\\\",\\\"slug\\\":\\\"18-karat\\\",\\\"name\\\":\\\"18 Karat\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"0\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/5ab9c97867c8659d1ab0603cb041b677.555x555x1.jpg\\\",\\\"id\\\":547530,\\\"headerImageUrl\\\":\\\"https://images.genius.com/2cafbe0c8915ec59da669ffab7547cce.1000x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/547530\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/18-karat-all-eyez-on-me-lyrics\\\",\\\"updatedByHumanAt\\\":1717858859,\\\"titleWithFeatured\\\":\\\"ALL EYEZ ON ME\\\",\\\"title\\\":\\\"ALL EYEZ ON ME\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/27db1a00ee6a0de6273c22414b398ec0.1000x1000x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/27db1a00ee6a0de6273c22414b398ec0.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Nov. 19, 2021\\\",\\\"releaseDateForDisplay\\\":\\\"November 19, 2021\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":19,\\\"month\\\":11,\\\"year\\\":2021},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/18-karat-all-eyez-on-me-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"18 Karat\\\",\\\"path\\\":\\\"/18-karat-all-eyez-on-me-lyrics\\\",\\\"lyricsUpdatedAt\\\":1638796104,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":14633888,\\\"instrumental\\\":false,\\\"id\\\":7364547,\\\"headerImageUrl\\\":\\\"https://images.genius.com/3d5ee4baed9220afe2a81d54a0bdbdea.1000x563x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/3d5ee4baed9220afe2a81d54a0bdbdea.300x169x1.jpg\\\",\\\"fullTitle\\\":\\\"ALL EYEZ ON ME by 18 Karat\\\",\\\"artistNames\\\":\\\"18 Karat\\\",\\\"apiPath\\\":\\\"/songs/7364547\\\",\\\"annotationCount\\\":1,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Lil-shrimp\\\",\\\"slug\\\":\\\"Lil-shrimp\\\",\\\"name\\\":\\\"Lil Shrimp\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"l\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/eeaf15d46fd27ee64b32ecc968c5d0a1.463x463x1.jpg\\\",\\\"id\\\":1775472,\\\"headerImageUrl\\\":\\\"https://images.genius.com/eeaf15d46fd27ee64b32ecc968c5d0a1.463x463x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/1775472\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Lil-shrimp\\\",\\\"slug\\\":\\\"Lil-shrimp\\\",\\\"name\\\":\\\"Lil Shrimp\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"l\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/eeaf15d46fd27ee64b32ecc968c5d0a1.463x463x1.jpg\\\",\\\"id\\\":1775472,\\\"headerImageUrl\\\":\\\"https://images.genius.com/eeaf15d46fd27ee64b32ecc968c5d0a1.463x463x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/1775472\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Lil-shrimp-all-eyez-on-me-lyrics\\\",\\\"updatedByHumanAt\\\":1690093545,\\\"titleWithFeatured\\\":\\\"​all eyez on me\\\",\\\"title\\\":\\\"​all eyez on me\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/b28dcbfd8576851f70b9bdacae0fa8b8.1000x1000x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/b28dcbfd8576851f70b9bdacae0fa8b8.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Jul. 3, 2020\\\",\\\"releaseDateForDisplay\\\":\\\"July 3, 2020\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":3,\\\"month\\\":7,\\\"year\\\":2020},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Lil-shrimp-all-eyez-on-me-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Lil Shrimp\\\",\\\"path\\\":\\\"/Lil-shrimp-all-eyez-on-me-lyrics\\\",\\\"lyricsUpdatedAt\\\":1606866914,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":6479473,\\\"instrumental\\\":false,\\\"id\\\":5730314,\\\"headerImageUrl\\\":\\\"https://images.genius.com/b28dcbfd8576851f70b9bdacae0fa8b8.1000x1000x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/b28dcbfd8576851f70b9bdacae0fa8b8.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"​all eyez on me by Lil Shrimp\\\",\\\"artistNames\\\":\\\"Lil Shrimp\\\",\\\"apiPath\\\":\\\"/songs/5730314\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":2463,\\\"url\\\":\\\"https://genius.com/artists/Kage-dq\\\",\\\"slug\\\":\\\"Kage-dq\\\",\\\"name\\\":\\\"Kage (dq)\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"k\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/bab7d26e1aacdf52b905966cf8bdded4.1000x1000x1.jpg\\\",\\\"id\\\":2322384,\\\"headerImageUrl\\\":\\\"https://images.genius.com/cfcf9dd63d41e753e53fd0cf10f8bbd9.1000x667x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2322384\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":2463,\\\"url\\\":\\\"https://genius.com/artists/Kage-dq\\\",\\\"slug\\\":\\\"Kage-dq\\\",\\\"name\\\":\\\"Kage (dq)\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"k\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/bab7d26e1aacdf52b905966cf8bdded4.1000x1000x1.jpg\\\",\\\"id\\\":2322384,\\\"headerImageUrl\\\":\\\"https://images.genius.com/cfcf9dd63d41e753e53fd0cf10f8bbd9.1000x667x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2322384\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Kage-dq-na-mnie-20-lyrics\\\",\\\"updatedByHumanAt\\\":1610125791,\\\"titleWithFeatured\\\":\\\"Na mnie 2.0\\\",\\\"title\\\":\\\"Na mnie 2.0\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/0ad2f61f56b98fd22e71d4c5253bb6fe.1000x1000x1.png\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/0ad2f61f56b98fd22e71d4c5253bb6fe.300x300x1.png\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Jan. 8, 2021\\\",\\\"releaseDateForDisplay\\\":\\\"January 8, 2021\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":8,\\\"month\\\":1,\\\"year\\\":2021},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Kage-dq-na-mnie-20-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Kage (dq)\\\",\\\"path\\\":\\\"/Kage-dq-na-mnie-20-lyrics\\\",\\\"lyricsUpdatedAt\\\":1610125791,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":10309818,\\\"instrumental\\\":false,\\\"id\\\":6354385,\\\"headerImageUrl\\\":\\\"https://images.genius.com/0ad2f61f56b98fd22e71d4c5253bb6fe.1000x1000x1.png\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/0ad2f61f56b98fd22e71d4c5253bb6fe.300x300x1.png\\\",\\\"fullTitle\\\":\\\"Na mnie 2.0 by Kage (dq)\\\",\\\"artistNames\\\":\\\"Kage (dq)\\\",\\\"apiPath\\\":\\\"/songs/6354385\\\",\\\"annotationCount\\\":9,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":6792,\\\"url\\\":\\\"https://genius.com/artists/Valkirin\\\",\\\"slug\\\":\\\"Valkirin\\\",\\\"name\\\":\\\"Валькирин (Valkirin)\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"v\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/33b3d3ffa1d7a951e9489eccdf3df318.398x398x1.png\\\",\\\"id\\\":2466753,\\\"headerImageUrl\\\":\\\"https://images.genius.com/66c7855d40a66670d5dbd42cf27d92b9.960x704x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2466753\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":6792,\\\"url\\\":\\\"https://genius.com/artists/Valkirin\\\",\\\"slug\\\":\\\"Valkirin\\\",\\\"name\\\":\\\"Валькирин (Valkirin)\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"v\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/33b3d3ffa1d7a951e9489eccdf3df318.398x398x1.png\\\",\\\"id\\\":2466753,\\\"headerImageUrl\\\":\\\"https://images.genius.com/66c7855d40a66670d5dbd42cf27d92b9.960x704x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2466753\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Valkirin-fuck-traitors-lyrics\\\",\\\"updatedByHumanAt\\\":1608659027,\\\"titleWithFeatured\\\":\\\"Нахуй предателей (Fuck traitors)\\\",\\\"title\\\":\\\"Нахуй предателей (Fuck traitors)\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/443af1a74e8bf937ab8e162fcf8473e6.1000x1000x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/443af1a74e8bf937ab8e162fcf8473e6.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Dec. 22, 2020\\\",\\\"releaseDateForDisplay\\\":\\\"December 22, 2020\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":22,\\\"month\\\":12,\\\"year\\\":2020},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Valkirin-fuck-traitors-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Валькирин (Valkirin)\\\",\\\"path\\\":\\\"/Valkirin-fuck-traitors-lyrics\\\",\\\"lyricsUpdatedAt\\\":1608658477,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":12168930,\\\"instrumental\\\":false,\\\"id\\\":6301739,\\\"headerImageUrl\\\":\\\"https://images.genius.com/443af1a74e8bf937ab8e162fcf8473e6.1000x1000x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/443af1a74e8bf937ab8e162fcf8473e6.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"Нахуй предателей (Fuck traitors) by Валькирин (Valkirin)\\\",\\\"artistNames\\\":\\\"Валькирин (Valkirin)\\\",\\\"apiPath\\\":\\\"/songs/6301739\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Bonez-mc-and-gzuz\\\",\\\"slug\\\":\\\"Bonez-mc-and-gzuz\\\",\\\"name\\\":\\\"Bonez MC & Gzuz\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"b\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/d426b95454953b85e3ac95f462e9db3f.1000x1000x1.jpg\\\",\\\"id\\\":2084456,\\\"headerImageUrl\\\":\\\"https://images.genius.com/2015c544da63b0b9b01f82aa79ace49e.563x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2084456\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Bonez-mc-and-gzuz\\\",\\\"slug\\\":\\\"Bonez-mc-and-gzuz\\\",\\\"name\\\":\\\"Bonez MC & Gzuz\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"b\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/d426b95454953b85e3ac95f462e9db3f.1000x1000x1.jpg\\\",\\\"id\\\":2084456,\\\"headerImageUrl\\\":\\\"https://images.genius.com/2015c544da63b0b9b01f82aa79ace49e.563x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2084456\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Bonez-mc-and-gzuz-grabstein-lyrics\\\",\\\"updatedByHumanAt\\\":1710370598,\\\"titleWithFeatured\\\":\\\"Grabstein\\\",\\\"title\\\":\\\"Grabstein\\\",\\\"stats\\\":{\\\"pageviews\\\":24377,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/fd655262d9191749bb47cc048b4916ca.1000x1000x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/fd655262d9191749bb47cc048b4916ca.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Sep. 11, 2020\\\",\\\"releaseDateForDisplay\\\":\\\"September 11, 2020\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":11,\\\"month\\\":9,\\\"year\\\":2020},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Bonez-mc-and-gzuz-grabstein-sample\\\",\\\"pyongsCount\\\":1,\\\"primaryArtistNames\\\":\\\"Bonez MC & Gzuz\\\",\\\"path\\\":\\\"/Bonez-mc-and-gzuz-grabstein-lyrics\\\",\\\"lyricsUpdatedAt\\\":1705219818,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":8032109,\\\"instrumental\\\":false,\\\"id\\\":5857450,\\\"headerImageUrl\\\":\\\"https://images.genius.com/fd655262d9191749bb47cc048b4916ca.1000x1000x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/fd655262d9191749bb47cc048b4916ca.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"Grabstein by Bonez MC & Gzuz\\\",\\\"artistNames\\\":\\\"Bonez MC & Gzuz\\\",\\\"apiPath\\\":\\\"/songs/5857450\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":290,\\\"url\\\":\\\"https://genius.com/artists/Lexpair\\\",\\\"slug\\\":\\\"Lexpair\\\",\\\"name\\\":\\\"Lexpair\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"l\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/c8683b969b442c29e74175f655c99bf4.1000x1000x1.jpg\\\",\\\"id\\\":315738,\\\"headerImageUrl\\\":\\\"https://images.genius.com/8c5bc03055fe366329f9f3198713d88f.720x720x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/315738\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":290,\\\"url\\\":\\\"https://genius.com/artists/Lexpair\\\",\\\"slug\\\":\\\"Lexpair\\\",\\\"name\\\":\\\"Lexpair\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"l\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/c8683b969b442c29e74175f655c99bf4.1000x1000x1.jpg\\\",\\\"id\\\":315738,\\\"headerImageUrl\\\":\\\"https://images.genius.com/8c5bc03055fe366329f9f3198713d88f.720x720x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/315738\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Lexpair-zermi-lyrics\\\",\\\"updatedByHumanAt\\\":1667024109,\\\"titleWithFeatured\\\":\\\"Zermi\\\",\\\"title\\\":\\\"Zermi\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/bd0832ff82ace9d4fab718ae2055995b.500x500x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/bd0832ff82ace9d4fab718ae2055995b.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":null,\\\"releaseDateForDisplay\\\":null,\\\"releaseDateComponents\\\":null,\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Lexpair-zermi-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Lexpair\\\",\\\"path\\\":\\\"/Lexpair-zermi-lyrics\\\",\\\"lyricsUpdatedAt\\\":1591816258,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":694892,\\\"instrumental\\\":false,\\\"id\\\":5656358,\\\"headerImageUrl\\\":\\\"https://images.genius.com/bd0832ff82ace9d4fab718ae2055995b.500x500x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/bd0832ff82ace9d4fab718ae2055995b.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"Zermi by Lexpair\\\",\\\"artistNames\\\":\\\"Lexpair\\\",\\\"apiPath\\\":\\\"/songs/5656358\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":290,\\\"url\\\":\\\"https://genius.com/artists/Lexpair\\\",\\\"slug\\\":\\\"Lexpair\\\",\\\"name\\\":\\\"Lexpair\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"l\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/c8683b969b442c29e74175f655c99bf4.1000x1000x1.jpg\\\",\\\"id\\\":315738,\\\"headerImageUrl\\\":\\\"https://images.genius.com/8c5bc03055fe366329f9f3198713d88f.720x720x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/315738\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":290,\\\"url\\\":\\\"https://genius.com/artists/Lexpair\\\",\\\"slug\\\":\\\"Lexpair\\\",\\\"name\\\":\\\"Lexpair\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"l\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/c8683b969b442c29e74175f655c99bf4.1000x1000x1.jpg\\\",\\\"id\\\":315738,\\\"headerImageUrl\\\":\\\"https://images.genius.com/8c5bc03055fe366329f9f3198713d88f.720x720x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/315738\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Lexpair-interlude-silence-radio-lyrics\\\",\\\"updatedByHumanAt\\\":1667024109,\\\"titleWithFeatured\\\":\\\"Interlude (Silence Radio)\\\",\\\"title\\\":\\\"Interlude (Silence Radio)\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/bd0832ff82ace9d4fab718ae2055995b.500x500x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/bd0832ff82ace9d4fab718ae2055995b.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":null,\\\"releaseDateForDisplay\\\":null,\\\"releaseDateComponents\\\":null,\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Lexpair-interlude-silence-radio-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Lexpair\\\",\\\"path\\\":\\\"/Lexpair-interlude-silence-radio-lyrics\\\",\\\"lyricsUpdatedAt\\\":1591816241,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":694892,\\\"instrumental\\\":false,\\\"id\\\":5656357,\\\"headerImageUrl\\\":\\\"https://images.genius.com/bd0832ff82ace9d4fab718ae2055995b.500x500x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/bd0832ff82ace9d4fab718ae2055995b.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"Interlude (Silence Radio) by Lexpair\\\",\\\"artistNames\\\":\\\"Lexpair\\\",\\\"apiPath\\\":\\\"/songs/5656357\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Dellafuente\\\",\\\"slug\\\":\\\"Dellafuente\\\",\\\"name\\\":\\\"DELLAFUENTE\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"d\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/85c10d2948fd7dd06e2f16dbed157f96.1000x1000x1.png\\\",\\\"id\\\":463998,\\\"headerImageUrl\\\":\\\"https://images.genius.com/7416f7f9b284abeddd5d8b3e879c2e1c.800x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/463998\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Dellafuente\\\",\\\"slug\\\":\\\"Dellafuente\\\",\\\"name\\\":\\\"DELLAFUENTE\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"d\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/85c10d2948fd7dd06e2f16dbed157f96.1000x1000x1.png\\\",\\\"id\\\":463998,\\\"headerImageUrl\\\":\\\"https://images.genius.com/7416f7f9b284abeddd5d8b3e879c2e1c.800x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/463998\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Dellafuente-corazon-mio-lyrics\\\",\\\"updatedByHumanAt\\\":1696807681,\\\"titleWithFeatured\\\":\\\"Corazón Mío\\\",\\\"title\\\":\\\"Corazón Mío\\\",\\\"stats\\\":{\\\"pageviews\\\":8957,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/6563d4c2071a60cdbf8561583344dec1.1000x1000x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/6563d4c2071a60cdbf8561583344dec1.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Jun. 20, 2016\\\",\\\"releaseDateForDisplay\\\":\\\"June 20, 2016\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":20,\\\"month\\\":6,\\\"year\\\":2016},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Dellafuente-corazon-mio-sample\\\",\\\"pyongsCount\\\":5,\\\"primaryArtistNames\\\":\\\"DELLAFUENTE\\\",\\\"path\\\":\\\"/Dellafuente-corazon-mio-lyrics\\\",\\\"lyricsUpdatedAt\\\":1696817720,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":2746400,\\\"instrumental\\\":false,\\\"id\\\":2624191,\\\"headerImageUrl\\\":\\\"https://images.genius.com/e041334da727e2f55e5058c652372943.640x480x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/e041334da727e2f55e5058c652372943.300x225x1.jpg\\\",\\\"fullTitle\\\":\\\"Corazón Mío by DELLAFUENTE\\\",\\\"artistNames\\\":\\\"DELLAFUENTE\\\",\\\"apiPath\\\":\\\"/songs/2624191\\\",\\\"annotationCount\\\":8,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":6539,\\\"url\\\":\\\"https://genius.com/artists/Kxng-crooked\\\",\\\"slug\\\":\\\"Kxng-crooked\\\",\\\"name\\\":\\\"KXNG Crooked\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"k\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/95ef03a3fbbbe4a5ad5015fafb6689dc.1000x1000x1.jpg\\\",\\\"id\\\":484,\\\"headerImageUrl\\\":\\\"https://images.genius.com/a4c8df721cc1eb833f54a1c9514553ba.618x184x1.png\\\",\\\"apiPath\\\":\\\"/artists/484\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":6539,\\\"url\\\":\\\"https://genius.com/artists/Kxng-crooked\\\",\\\"slug\\\":\\\"Kxng-crooked\\\",\\\"name\\\":\\\"KXNG Crooked\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"k\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/95ef03a3fbbbe4a5ad5015fafb6689dc.1000x1000x1.jpg\\\",\\\"id\\\":484,\\\"headerImageUrl\\\":\\\"https://images.genius.com/a4c8df721cc1eb833f54a1c9514553ba.618x184x1.png\\\",\\\"apiPath\\\":\\\"/artists/484\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Kxng-crooked-i-think-im-big-syke-lyrics\\\",\\\"updatedByHumanAt\\\":1714339598,\\\"titleWithFeatured\\\":\\\"I Think I\\'m Big Syke\\\",\\\"title\\\":\\\"I Think I’m Big Syke\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":4},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/23d55398dc759f5c6ec33cb7d80ca66d.1000x1000x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/23d55398dc759f5c6ec33cb7d80ca66d.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Oct. 25, 2019\\\",\\\"releaseDateForDisplay\\\":\\\"October 25, 2019\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":25,\\\"month\\\":10,\\\"year\\\":2019},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Kxng-crooked-i-think-im-big-syke-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"KXNG Crooked & KXNG Crooked\\\",\\\"path\\\":\\\"/Kxng-crooked-i-think-im-big-syke-lyrics\\\",\\\"lyricsUpdatedAt\\\":1582723440,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":3950515,\\\"instrumental\\\":false,\\\"id\\\":4969594,\\\"headerImageUrl\\\":\\\"https://images.genius.com/5f65a79a0150da499f0c4ab06d1b48f3.700x700x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/5f65a79a0150da499f0c4ab06d1b48f3.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"I Think I\\'m Big Syke by KXNG Crooked & KXNG Crooked\\\",\\\"artistNames\\\":\\\"KXNG Crooked & KXNG Crooked\\\",\\\"apiPath\\\":\\\"/songs/4969594\\\",\\\"annotationCount\\\":4,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Tantrum-ta\\\",\\\"slug\\\":\\\"Tantrum-ta\\\",\\\"name\\\":\\\"Tantrum T.A.\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"t\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/ad434dbe7aa50e7f91e8d09e22996941.709x709x1.png\\\",\\\"id\\\":62300,\\\"headerImageUrl\\\":\\\"https://images.genius.com/cb3a341946b7583b0118951da85f881c.1000x429x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/62300\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Tantrum-ta\\\",\\\"slug\\\":\\\"Tantrum-ta\\\",\\\"name\\\":\\\"Tantrum T.A.\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"t\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/ad434dbe7aa50e7f91e8d09e22996941.709x709x1.png\\\",\\\"id\\\":62300,\\\"headerImageUrl\\\":\\\"https://images.genius.com/cb3a341946b7583b0118951da85f881c.1000x429x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/62300\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Sleepy-west\\\",\\\"slug\\\":\\\"Sleepy-west\\\",\\\"name\\\":\\\"Sleepy West\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"s\\\",\\\"imageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"id\\\":1895352,\\\"headerImageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"apiPath\\\":\\\"/artists/1895352\\\",\\\"type\\\":\\\"artist\\\"}],\\\"url\\\":\\\"https://genius.com/Tantrum-ta-outlawz-lyrics\\\",\\\"updatedByHumanAt\\\":1565150988,\\\"titleWithFeatured\\\":\\\"Outlawz (Ft. Sleepy West)\\\",\\\"title\\\":\\\"Outlawz\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/951fc0f9ed1f2b88b34ab098ae6a4d61.1000x1000x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/951fc0f9ed1f2b88b34ab098ae6a4d61.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Jul. 25, 2019\\\",\\\"releaseDateForDisplay\\\":\\\"July 25, 2019\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":25,\\\"month\\\":7,\\\"year\\\":2019},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Tantrum-ta-outlawz-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Tantrum T.A.\\\",\\\"path\\\":\\\"/Tantrum-ta-outlawz-lyrics\\\",\\\"lyricsUpdatedAt\\\":1563999283,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":4276381,\\\"instrumental\\\":false,\\\"id\\\":4724923,\\\"headerImageUrl\\\":\\\"https://images.genius.com/951fc0f9ed1f2b88b34ab098ae6a4d61.1000x1000x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/951fc0f9ed1f2b88b34ab098ae6a4d61.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"Outlawz by Tantrum T.A. (Ft. Sleepy West)\\\",\\\"artistNames\\\":\\\"Tantrum T.A. (Ft. Sleepy West)\\\",\\\"apiPath\\\":\\\"/songs/4724923\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":100,\\\"url\\\":\\\"https://genius.com/artists/Dima-bamberg\\\",\\\"slug\\\":\\\"Dima-bamberg\\\",\\\"name\\\":\\\"дима бамберг (dima bamberg)\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"d\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/69f08444a8ffcedfae80f85779fbb02e.403x403x1.jpg\\\",\\\"id\\\":24799,\\\"headerImageUrl\\\":\\\"https://images.genius.com/a4d6b77a82792fd2871daefb77e68409.960x640x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/24799\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":100,\\\"url\\\":\\\"https://genius.com/artists/Dima-bamberg\\\",\\\"slug\\\":\\\"Dima-bamberg\\\",\\\"name\\\":\\\"дима бамберг (dima bamberg)\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"d\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/69f08444a8ffcedfae80f85779fbb02e.403x403x1.jpg\\\",\\\"id\\\":24799,\\\"headerImageUrl\\\":\\\"https://images.genius.com/a4d6b77a82792fd2871daefb77e68409.960x640x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/24799\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Dima-bamberg-on-your-knees-mortes-lyrics\\\",\\\"updatedByHumanAt\\\":1711030114,\\\"titleWithFeatured\\\":\\\"На колени, мортес (On your knees, mortes)\\\",\\\"title\\\":\\\"На колени, мортес (On your knees, mortes)\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/806f991dd553767deb91c00569a00483.999x999x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/806f991dd553767deb91c00569a00483.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Dec. 11, 2007\\\",\\\"releaseDateForDisplay\\\":\\\"December 11, 2007\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":11,\\\"month\\\":12,\\\"year\\\":2007},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Dima-bamberg-on-your-knees-mortes-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"дима бамберг (dima bamberg)\\\",\\\"path\\\":\\\"/Dima-bamberg-on-your-knees-mortes-lyrics\\\",\\\"lyricsUpdatedAt\\\":1709739862,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":1529326,\\\"instrumental\\\":false,\\\"id\\\":3683561,\\\"headerImageUrl\\\":\\\"https://images.genius.com/806f991dd553767deb91c00569a00483.999x999x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/806f991dd553767deb91c00569a00483.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"На колени, мортес (On your knees, mortes) by дима бамберг (dima bamberg)\\\",\\\"artistNames\\\":\\\"дима бамберг (dima bamberg)\\\",\\\"apiPath\\\":\\\"/songs/3683561\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Jul\\\",\\\"slug\\\":\\\"Jul\\\",\\\"name\\\":\\\"JuL\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"j\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/6f7a11d70aa5ec81f5fdd3433c056b81.1000x1000x1.jpg\\\",\\\"id\\\":74283,\\\"headerImageUrl\\\":\\\"https://images.genius.com/f3f6f91406d8b491a8f4264c84ae0dc9.600x400x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/74283\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Jul\\\",\\\"slug\\\":\\\"Jul\\\",\\\"name\\\":\\\"JuL\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"j\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/6f7a11d70aa5ec81f5fdd3433c056b81.1000x1000x1.jpg\\\",\\\"id\\\":74283,\\\"headerImageUrl\\\":\\\"https://images.genius.com/f3f6f91406d8b491a8f4264c84ae0dc9.600x400x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/74283\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Dj-kayz\\\",\\\"slug\\\":\\\"Dj-kayz\\\",\\\"name\\\":\\\"DJ Kayz\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"d\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/2f194224fee5cd97aff9d21b1b28e273.400x400x1.jpg\\\",\\\"id\\\":59247,\\\"headerImageUrl\\\":\\\"https://images.genius.com/a4bbe9ad335ac167d3dea33f16d2299a.1000x333x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/59247\\\",\\\"type\\\":\\\"artist\\\"}],\\\"url\\\":\\\"https://genius.com/Jul-all-eyez-on-me-lyrics\\\",\\\"updatedByHumanAt\\\":1657903083,\\\"titleWithFeatured\\\":\\\"All Eyez on Me (Ft. DJ Kayz)\\\",\\\"title\\\":\\\"All Eyez on Me\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/592e22ae76c9768e1491a886e770241e.1000x563x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/592e22ae76c9768e1491a886e770241e.300x169x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Sep. 29, 2014\\\",\\\"releaseDateForDisplay\\\":\\\"September 29, 2014\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":29,\\\"month\\\":9,\\\"year\\\":2014},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Jul-all-eyez-on-me-sample\\\",\\\"pyongsCount\\\":1,\\\"primaryArtistNames\\\":\\\"JuL\\\",\\\"path\\\":\\\"/Jul-all-eyez-on-me-lyrics\\\",\\\"lyricsUpdatedAt\\\":1477750805,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":1415602,\\\"instrumental\\\":false,\\\"id\\\":661176,\\\"headerImageUrl\\\":\\\"https://images.genius.com/592e22ae76c9768e1491a886e770241e.1000x563x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/592e22ae76c9768e1491a886e770241e.300x169x1.jpg\\\",\\\"fullTitle\\\":\\\"All Eyez on Me by JuL (Ft. DJ Kayz)\\\",\\\"artistNames\\\":\\\"JuL (Ft. DJ Kayz)\\\",\\\"apiPath\\\":\\\"/songs/661176\\\",\\\"annotationCount\\\":3,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Crooked-i\\\",\\\"slug\\\":\\\"Crooked-i\\\",\\\"name\\\":\\\"Crooked I\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"c\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/b24d173adc2f62d02ea2756f1c6a8a76.400x400x1.png\\\",\\\"id\\\":263426,\\\"headerImageUrl\\\":\\\"https://images.genius.com/2d7c87189fa656197398e74192215028.398x125x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/263426\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Crooked-i\\\",\\\"slug\\\":\\\"Crooked-i\\\",\\\"name\\\":\\\"Crooked I\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"c\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/b24d173adc2f62d02ea2756f1c6a8a76.400x400x1.png\\\",\\\"id\\\":263426,\\\"headerImageUrl\\\":\\\"https://images.genius.com/2d7c87189fa656197398e74192215028.398x125x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/263426\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Crooked-i-all-eyez-on-me-lyrics\\\",\\\"updatedByHumanAt\\\":1441142464,\\\"titleWithFeatured\\\":\\\"All Eyez On Me\\\",\\\"title\\\":\\\"All Eyez On Me\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/e0501cb9717ca2634488b8966fc8202c.1000x1000x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/e0501cb9717ca2634488b8966fc8202c.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":null,\\\"releaseDateForDisplay\\\":null,\\\"releaseDateComponents\\\":null,\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Crooked-i-all-eyez-on-me-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Crooked I\\\",\\\"path\\\":\\\"/Crooked-i-all-eyez-on-me-lyrics\\\",\\\"lyricsUpdatedAt\\\":1441142464,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":20658,\\\"instrumental\\\":false,\\\"id\\\":2282299,\\\"headerImageUrl\\\":\\\"https://images.genius.com/e0501cb9717ca2634488b8966fc8202c.1000x1000x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/e0501cb9717ca2634488b8966fc8202c.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"All Eyez On Me by Crooked I\\\",\\\"artistNames\\\":\\\"Crooked I\\\",\\\"apiPath\\\":\\\"/songs/2282299\\\",\\\"annotationCount\\\":3,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":7711,\\\"url\\\":\\\"https://genius.com/artists/Ti\\\",\\\"slug\\\":\\\"Ti\\\",\\\"name\\\":\\\"T.I.\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"t\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/6f1e66b5da6bb21cceee3cee5fb5dab5.272x272x1.jpg\\\",\\\"id\\\":85,\\\"headerImageUrl\\\":\\\"https://images.genius.com/9a7118eb0b1e83879cdb4b7855158d53.320x180x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/85\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":7711,\\\"url\\\":\\\"https://genius.com/artists/Ti\\\",\\\"slug\\\":\\\"Ti\\\",\\\"name\\\":\\\"T.I.\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"t\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/6f1e66b5da6bb21cceee3cee5fb5dab5.272x272x1.jpg\\\",\\\"id\\\":85,\\\"headerImageUrl\\\":\\\"https://images.genius.com/9a7118eb0b1e83879cdb4b7855158d53.320x180x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/85\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Daz-dillinger\\\",\\\"slug\\\":\\\"Daz-dillinger\\\",\\\"name\\\":\\\"Daz Dillinger\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"d\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/215adaad704cbb1409277e852d327a37.371x371x1.jpg\\\",\\\"id\\\":3431,\\\"headerImageUrl\\\":\\\"https://images.genius.com/215adaad704cbb1409277e852d327a37.371x371x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/3431\\\",\\\"type\\\":\\\"artist\\\"}],\\\"url\\\":\\\"https://genius.com/Ti-my-life-lyrics\\\",\\\"updatedByHumanAt\\\":1710395781,\\\"titleWithFeatured\\\":\\\"My Life (Ft. Daz Dillinger)\\\",\\\"title\\\":\\\"My Life\\\",\\\"stats\\\":{\\\"pageviews\\\":9818,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":1},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/639bf5b825c66e0f59d87574014bc4cf.600x600x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/639bf5b825c66e0f59d87574014bc4cf.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Nov. 30, 2004\\\",\\\"releaseDateForDisplay\\\":\\\"November 30, 2004\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":30,\\\"month\\\":11,\\\"year\\\":2004},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Ti-my-life-sample\\\",\\\"pyongsCount\\\":1,\\\"primaryArtistNames\\\":\\\"T.I.\\\",\\\"path\\\":\\\"/Ti-my-life-lyrics\\\",\\\"lyricsUpdatedAt\\\":1710395781,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":50,\\\"instrumental\\\":false,\\\"id\\\":45521,\\\"headerImageUrl\\\":\\\"https://images.genius.com/639bf5b825c66e0f59d87574014bc4cf.600x600x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/639bf5b825c66e0f59d87574014bc4cf.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"My Life by T.I. (Ft. Daz Dillinger)\\\",\\\"artistNames\\\":\\\"T.I. (Ft. Daz Dillinger)\\\",\\\"apiPath\\\":\\\"/songs/45521\\\",\\\"annotationCount\\\":2,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Joe\\\",\\\"slug\\\":\\\"Joe\\\",\\\"name\\\":\\\"Joe\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"j\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/792ad532a8ba3cb84ac8abe6ccaab643.478x478x1.jpg\\\",\\\"id\\\":220229,\\\"headerImageUrl\\\":\\\"https://images.genius.com/7e6cef388b4735f4978f8cee460d3c05.920x520x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/220229\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Joe\\\",\\\"slug\\\":\\\"Joe\\\",\\\"name\\\":\\\"Joe\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"j\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/792ad532a8ba3cb84ac8abe6ccaab643.478x478x1.jpg\\\",\\\"id\\\":220229,\\\"headerImageUrl\\\":\\\"https://images.genius.com/7e6cef388b4735f4978f8cee460d3c05.920x520x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/220229\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Joe-street-dreams-lyrics\\\",\\\"updatedByHumanAt\\\":1690725813,\\\"titleWithFeatured\\\":\\\"Street Dreams\\\",\\\"title\\\":\\\"Street Dreams\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":1},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/69a8660df9f9301797174a3166e79f58.300x300x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/69a8660df9f9301797174a3166e79f58.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Dec. 2, 2003\\\",\\\"releaseDateForDisplay\\\":\\\"December 2, 2003\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":2,\\\"month\\\":12,\\\"year\\\":2003},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Joe-street-dreams-sample\\\",\\\"pyongsCount\\\":1,\\\"primaryArtistNames\\\":\\\"Joe\\\",\\\"path\\\":\\\"/Joe-street-dreams-lyrics\\\",\\\"lyricsUpdatedAt\\\":1690725813,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":160617,\\\"instrumental\\\":false,\\\"id\\\":214134,\\\"headerImageUrl\\\":\\\"https://images.genius.com/69a8660df9f9301797174a3166e79f58.300x300x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/69a8660df9f9301797174a3166e79f58.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"Street Dreams by Joe\\\",\\\"artistNames\\\":\\\"Joe\\\",\\\"apiPath\\\":\\\"/songs/214134\\\",\\\"annotationCount\\\":2,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":4672,\\\"url\\\":\\\"https://genius.com/artists/Meek-mill\\\",\\\"slug\\\":\\\"Meek-mill\\\",\\\"name\\\":\\\"Meek Mill\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"m\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/52a29948e810a6d545ce9dac8d37357f.1000x1000x1.jpg\\\",\\\"id\\\":1319,\\\"headerImageUrl\\\":\\\"https://images.genius.com/af2451de1bca93f1e1c9ee489647cb2e.942x384x1.png\\\",\\\"apiPath\\\":\\\"/artists/1319\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":4672,\\\"url\\\":\\\"https://genius.com/artists/Meek-mill\\\",\\\"slug\\\":\\\"Meek-mill\\\",\\\"name\\\":\\\"Meek Mill\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"m\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/52a29948e810a6d545ce9dac8d37357f.1000x1000x1.jpg\\\",\\\"id\\\":1319,\\\"headerImageUrl\\\":\\\"https://images.genius.com/af2451de1bca93f1e1c9ee489647cb2e.942x384x1.png\\\",\\\"apiPath\\\":\\\"/artists/1319\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Chris-brown\\\",\\\"slug\\\":\\\"Chris-brown\\\",\\\"name\\\":\\\"Chris Brown\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"c\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/34b5eb355ff9bc8e966bd8aaf92fe73e.1000x1000x1.jpg\\\",\\\"id\\\":438,\\\"headerImageUrl\\\":\\\"https://images.genius.com/38218a75fcbd4c078a1b72912b77a138.1000x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/438\\\",\\\"type\\\":\\\"artist\\\"},{\\\"iq\\\":3889,\\\"url\\\":\\\"https://genius.com/artists/Nicki-minaj\\\",\\\"slug\\\":\\\"Nicki-minaj\\\",\\\"name\\\":\\\"Nicki Minaj\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"n\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/3545da2eb22383eb325aa4d211db145a.307x307x1.png\\\",\\\"id\\\":92,\\\"headerImageUrl\\\":\\\"https://images.genius.com/910506f8d056c52f9a3c43bb35158e14.1000x333x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/92\\\",\\\"type\\\":\\\"artist\\\"}],\\\"url\\\":\\\"https://genius.com/Meek-mill-all-eyes-on-you-lyrics\\\",\\\"updatedByHumanAt\\\":1709554210,\\\"titleWithFeatured\\\":\\\"All Eyes On You (Ft. Chris Brown & Nicki Minaj)\\\",\\\"title\\\":\\\"All Eyes On You\\\",\\\"stats\\\":{\\\"pageviews\\\":2277275,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/4cc9eed162b5221069436678d4be1dae.1000x1000x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/4cc9eed162b5221069436678d4be1dae.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Jun. 26, 2015\\\",\\\"releaseDateForDisplay\\\":\\\"June 26, 2015\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":26,\\\"month\\\":6,\\\"year\\\":2015},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Meek-mill-all-eyes-on-you-sample\\\",\\\"pyongsCount\\\":905,\\\"primaryArtistNames\\\":\\\"Meek Mill\\\",\\\"path\\\":\\\"/Meek-mill-all-eyes-on-you-lyrics\\\",\\\"lyricsUpdatedAt\\\":1683241524,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":104344,\\\"instrumental\\\":false,\\\"id\\\":2153759,\\\"headerImageUrl\\\":\\\"https://images.genius.com/e9299f0d951c2903fa00c485675816bc.1000x1000x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/e9299f0d951c2903fa00c485675816bc.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"All Eyes On You by Meek Mill (Ft. Chris Brown & Nicki Minaj)\\\",\\\"artistNames\\\":\\\"Meek Mill (Ft. Chris Brown & Nicki Minaj)\\\",\\\"apiPath\\\":\\\"/songs/2153759\\\",\\\"annotationCount\\\":18,\\\"type\\\":\\\"song\\\"}],\\\"url\\\":\\\"https://genius.com/2pac-all-eyez-on-me-sample/samples\\\",\\\"type\\\":\\\"song_relationship\\\",\\\"relationshipType\\\":\\\"sampled_in\\\"},{\\\"songs\\\":[],\\\"url\\\":null,\\\"type\\\":\\\"song_relationship\\\",\\\"relationshipType\\\":\\\"interpolates\\\"},{\\\"songs\\\":[{\\\"primaryArtists\\\":[{\\\"iq\\\":100,\\\"url\\\":\\\"https://genius.com/artists/Pharaoh\\\",\\\"slug\\\":\\\"Pharaoh\\\",\\\"name\\\":\\\"PHARAOH\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"p\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/fb36f4330fd8aeb9f7a1d97416c691ed.1000x1000x1.jpg\\\",\\\"id\\\":101583,\\\"headerImageUrl\\\":\\\"https://images.genius.com/5d6d2d0b18ba3cd7e641dc678eb57af5.1000x660x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/101583\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":100,\\\"url\\\":\\\"https://genius.com/artists/Pharaoh\\\",\\\"slug\\\":\\\"Pharaoh\\\",\\\"name\\\":\\\"PHARAOH\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"p\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/fb36f4330fd8aeb9f7a1d97416c691ed.1000x1000x1.jpg\\\",\\\"id\\\":101583,\\\"headerImageUrl\\\":\\\"https://images.genius.com/5d6d2d0b18ba3cd7e641dc678eb57af5.1000x660x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/101583\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Pharaoh-full-clip-lyrics\\\",\\\"updatedByHumanAt\\\":1717604267,\\\"titleWithFeatured\\\":\\\"Фул клип (Full Clip)\\\",\\\"title\\\":\\\"Фул клип (Full Clip)\\\",\\\"stats\\\":{\\\"pageviews\\\":81747,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/f3a238db218926c438951f17560bfb50.1000x1000x1.png\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/f3a238db218926c438951f17560bfb50.300x300x1.png\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Apr. 14, 2018\\\",\\\"releaseDateForDisplay\\\":\\\"April 14, 2018\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":14,\\\"month\\\":4,\\\"year\\\":2018},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Pharaoh-full-clip-sample\\\",\\\"pyongsCount\\\":15,\\\"primaryArtistNames\\\":\\\"PHARAOH\\\",\\\"path\\\":\\\"/Pharaoh-full-clip-lyrics\\\",\\\"lyricsUpdatedAt\\\":1717604239,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":1529326,\\\"instrumental\\\":false,\\\"id\\\":3649267,\\\"headerImageUrl\\\":\\\"https://images.genius.com/f3a238db218926c438951f17560bfb50.1000x1000x1.png\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/f3a238db218926c438951f17560bfb50.300x300x1.png\\\",\\\"fullTitle\\\":\\\"Фул клип (Full Clip) by PHARAOH\\\",\\\"artistNames\\\":\\\"PHARAOH\\\",\\\"apiPath\\\":\\\"/songs/3649267\\\",\\\"annotationCount\\\":4,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":6539,\\\"url\\\":\\\"https://genius.com/artists/Kxng-crooked\\\",\\\"slug\\\":\\\"Kxng-crooked\\\",\\\"name\\\":\\\"KXNG Crooked\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"k\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/95ef03a3fbbbe4a5ad5015fafb6689dc.1000x1000x1.jpg\\\",\\\"id\\\":484,\\\"headerImageUrl\\\":\\\"https://images.genius.com/a4c8df721cc1eb833f54a1c9514553ba.618x184x1.png\\\",\\\"apiPath\\\":\\\"/artists/484\\\",\\\"type\\\":\\\"artist\\\"},{\\\"iq\\\":6539,\\\"url\\\":\\\"https://genius.com/artists/Kxng-crooked\\\",\\\"slug\\\":\\\"Kxng-crooked\\\",\\\"name\\\":\\\"KXNG Crooked\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"k\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/95ef03a3fbbbe4a5ad5015fafb6689dc.1000x1000x1.jpg\\\",\\\"id\\\":484,\\\"headerImageUrl\\\":\\\"https://images.genius.com/a4c8df721cc1eb833f54a1c9514553ba.618x184x1.png\\\",\\\"apiPath\\\":\\\"/artists/484\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":6539,\\\"url\\\":\\\"https://genius.com/artists/Kxng-crooked\\\",\\\"slug\\\":\\\"Kxng-crooked\\\",\\\"name\\\":\\\"KXNG Crooked\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"k\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/95ef03a3fbbbe4a5ad5015fafb6689dc.1000x1000x1.jpg\\\",\\\"id\\\":484,\\\"headerImageUrl\\\":\\\"https://images.genius.com/a4c8df721cc1eb833f54a1c9514553ba.618x184x1.png\\\",\\\"apiPath\\\":\\\"/artists/484\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Kxng-crooked-i-think-im-big-syke-lyrics\\\",\\\"updatedByHumanAt\\\":1714339598,\\\"titleWithFeatured\\\":\\\"I Think I\\'m Big Syke\\\",\\\"title\\\":\\\"I Think I’m Big Syke\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":4},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/23d55398dc759f5c6ec33cb7d80ca66d.1000x1000x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/23d55398dc759f5c6ec33cb7d80ca66d.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Oct. 25, 2019\\\",\\\"releaseDateForDisplay\\\":\\\"October 25, 2019\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":25,\\\"month\\\":10,\\\"year\\\":2019},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Kxng-crooked-i-think-im-big-syke-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"KXNG Crooked & KXNG Crooked\\\",\\\"path\\\":\\\"/Kxng-crooked-i-think-im-big-syke-lyrics\\\",\\\"lyricsUpdatedAt\\\":1582723440,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":3950515,\\\"instrumental\\\":false,\\\"id\\\":4969594,\\\"headerImageUrl\\\":\\\"https://images.genius.com/5f65a79a0150da499f0c4ab06d1b48f3.700x700x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/5f65a79a0150da499f0c4ab06d1b48f3.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"I Think I\\'m Big Syke by KXNG Crooked & KXNG Crooked\\\",\\\"artistNames\\\":\\\"KXNG Crooked & KXNG Crooked\\\",\\\"apiPath\\\":\\\"/songs/4969594\\\",\\\"annotationCount\\\":4,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Teuterekordz\\\",\\\"slug\\\":\\\"Teuterekordz\\\",\\\"name\\\":\\\"Teuterekordz\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"t\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/dd2e505bbb0d2ebf303f5579aed5df0b.1000x1000x1.jpg\\\",\\\"id\\\":1804642,\\\"headerImageUrl\\\":\\\"https://images.genius.com/859863b3ee280bdaaaeb84a4f9023ca5.1000x681x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/1804642\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Teuterekordz\\\",\\\"slug\\\":\\\"Teuterekordz\\\",\\\"name\\\":\\\"Teuterekordz\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"t\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/dd2e505bbb0d2ebf303f5579aed5df0b.1000x1000x1.jpg\\\",\\\"id\\\":1804642,\\\"headerImageUrl\\\":\\\"https://images.genius.com/859863b3ee280bdaaaeb84a4f9023ca5.1000x681x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/1804642\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Beko101\\\",\\\"slug\\\":\\\"Beko101\\\",\\\"name\\\":\\\"Beko101\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"b\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/59434177d50946d5eea23ca8811ec0be.720x720x1.jpg\\\",\\\"id\\\":2904227,\\\"headerImageUrl\\\":\\\"https://images.genius.com/a4e373343136489796729fbb13858dc6.1000x563x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2904227\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/Dispo-teuterekordz\\\",\\\"slug\\\":\\\"Dispo-teuterekordz\\\",\\\"name\\\":\\\"Dispo (Teuterekordz)\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"d\\\",\\\"imageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"id\\\":2904225,\\\"headerImageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"apiPath\\\":\\\"/artists/2904225\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/Lucky-teuterekordz\\\",\\\"slug\\\":\\\"Lucky-teuterekordz\\\",\\\"name\\\":\\\"Lucky (Teuterekordz)\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"l\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/9b70e7b7d333938387ff3afafa08ab66.180x180x1.jpg\\\",\\\"id\\\":2904226,\\\"headerImageUrl\\\":\\\"https://images.genius.com/20967baed1e046d14a2888f4296218f7.1000x563x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2904226\\\",\\\"type\\\":\\\"artist\\\"}],\\\"url\\\":\\\"https://genius.com/Teuterekordz-ipod-classic-lyrics\\\",\\\"updatedByHumanAt\\\":1707002800,\\\"titleWithFeatured\\\":\\\"​iPod Classic (Ft. Beko101, Dispo (Teuterekordz) & Lucky (Teuterekordz))\\\",\\\"title\\\":\\\"​iPod Classic\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/dfdceed21a1ae35223d4c291eee17032.1000x1000x1.png\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/dfdceed21a1ae35223d4c291eee17032.300x300x1.png\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Dec. 1, 2023\\\",\\\"releaseDateForDisplay\\\":\\\"December 1, 2023\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":1,\\\"month\\\":12,\\\"year\\\":2023},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Teuterekordz-ipod-classic-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Teuterekordz\\\",\\\"path\\\":\\\"/Teuterekordz-ipod-classic-lyrics\\\",\\\"lyricsUpdatedAt\\\":1707002800,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":10648820,\\\"instrumental\\\":false,\\\"id\\\":9805170,\\\"headerImageUrl\\\":\\\"https://images.genius.com/dfdceed21a1ae35223d4c291eee17032.1000x1000x1.png\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/dfdceed21a1ae35223d4c291eee17032.300x300x1.png\\\",\\\"fullTitle\\\":\\\"​iPod Classic by Teuterekordz (Ft. Beko101, Dispo (Teuterekordz) & Lucky (Teuterekordz))\\\",\\\"artistNames\\\":\\\"Teuterekordz (Ft. Beko101, Dispo (Teuterekordz) & Lucky (Teuterekordz))\\\",\\\"apiPath\\\":\\\"/songs/9805170\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":2714,\\\"url\\\":\\\"https://genius.com/artists/Chief-keef\\\",\\\"slug\\\":\\\"Chief-keef\\\",\\\"name\\\":\\\"Chief Keef\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"c\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/12592b0da715efe0916dd3645763c64a.999x999x1.jpg\\\",\\\"id\\\":16808,\\\"headerImageUrl\\\":\\\"https://images.genius.com/4021ba198d32951f29a747ea92c3d57f.1000x333x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/16808\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":2714,\\\"url\\\":\\\"https://genius.com/artists/Chief-keef\\\",\\\"slug\\\":\\\"Chief-keef\\\",\\\"name\\\":\\\"Chief Keef\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"c\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/12592b0da715efe0916dd3645763c64a.999x999x1.jpg\\\",\\\"id\\\":16808,\\\"headerImageUrl\\\":\\\"https://images.genius.com/4021ba198d32951f29a747ea92c3d57f.1000x333x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/16808\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Chief-keef-all-eyes-on-me-lyrics\\\",\\\"updatedByHumanAt\\\":1694964372,\\\"titleWithFeatured\\\":\\\"All Eyes On Me\\\",\\\"title\\\":\\\"All Eyes On Me\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/12592b0da715efe0916dd3645763c64a.999x999x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/12592b0da715efe0916dd3645763c64a.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":null,\\\"releaseDateForDisplay\\\":null,\\\"releaseDateComponents\\\":null,\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Chief-keef-all-eyes-on-me-sample\\\",\\\"pyongsCount\\\":1,\\\"primaryArtistNames\\\":\\\"Chief Keef\\\",\\\"path\\\":\\\"/Chief-keef-all-eyes-on-me-lyrics\\\",\\\"lyricsUpdatedAt\\\":1690139142,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":2323597,\\\"instrumental\\\":false,\\\"id\\\":9352639,\\\"headerImageUrl\\\":\\\"https://images.genius.com/12592b0da715efe0916dd3645763c64a.999x999x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/12592b0da715efe0916dd3645763c64a.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"All Eyes On Me by Chief Keef\\\",\\\"artistNames\\\":\\\"Chief Keef\\\",\\\"apiPath\\\":\\\"/songs/9352639\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":720,\\\"url\\\":\\\"https://genius.com/artists/Gucci-mane\\\",\\\"slug\\\":\\\"Gucci-mane\\\",\\\"name\\\":\\\"Gucci Mane\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"g\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/82117b940dc8deada6f07c7590ca3b5f.978x978x1.jpg\\\",\\\"id\\\":13,\\\"headerImageUrl\\\":\\\"https://images.genius.com/de090514e8c38dadeac4a335a55f1989.800x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/13\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":720,\\\"url\\\":\\\"https://genius.com/artists/Gucci-mane\\\",\\\"slug\\\":\\\"Gucci-mane\\\",\\\"name\\\":\\\"Gucci Mane\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"g\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/82117b940dc8deada6f07c7590ca3b5f.978x978x1.jpg\\\",\\\"id\\\":13,\\\"headerImageUrl\\\":\\\"https://images.genius.com/de090514e8c38dadeac4a335a55f1989.800x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/13\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Gucci-mane-pick-up-the-pieces-outro-lyrics\\\",\\\"updatedByHumanAt\\\":1711106351,\\\"titleWithFeatured\\\":\\\"Pick Up the Pieces (Outro)\\\",\\\"title\\\":\\\"Pick Up the Pieces (Outro)\\\",\\\"stats\\\":{\\\"pageviews\\\":18781,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/e6029a6cbca207718022a4a730d9f321.1000x1000x1.png\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/e6029a6cbca207718022a4a730d9f321.300x300x1.png\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Jul. 21, 2016\\\",\\\"releaseDateForDisplay\\\":\\\"July 21, 2016\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":21,\\\"month\\\":7,\\\"year\\\":2016},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Gucci-mane-pick-up-the-pieces-outro-sample\\\",\\\"pyongsCount\\\":3,\\\"primaryArtistNames\\\":\\\"Gucci Mane\\\",\\\"path\\\":\\\"/Gucci-mane-pick-up-the-pieces-outro-lyrics\\\",\\\"lyricsUpdatedAt\\\":1693561770,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":1766304,\\\"instrumental\\\":false,\\\"id\\\":2824446,\\\"headerImageUrl\\\":\\\"https://images.genius.com/e6029a6cbca207718022a4a730d9f321.1000x1000x1.png\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/e6029a6cbca207718022a4a730d9f321.300x300x1.png\\\",\\\"fullTitle\\\":\\\"Pick Up the Pieces (Outro) by Gucci Mane\\\",\\\"artistNames\\\":\\\"Gucci Mane\\\",\\\"apiPath\\\":\\\"/songs/2824446\\\",\\\"annotationCount\\\":8,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Pr-sad-r6-and-dopesmoke\\\",\\\"slug\\\":\\\"Pr-sad-r6-and-dopesmoke\\\",\\\"name\\\":\\\"PR SAD, R6 & dopesmoke\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"p\\\",\\\"imageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"id\\\":3522550,\\\"headerImageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"apiPath\\\":\\\"/artists/3522550\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Pr-sad-r6-and-dopesmoke\\\",\\\"slug\\\":\\\"Pr-sad-r6-and-dopesmoke\\\",\\\"name\\\":\\\"PR SAD, R6 & dopesmoke\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"p\\\",\\\"imageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"id\\\":3522550,\\\"headerImageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"apiPath\\\":\\\"/artists/3522550\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Pr-sad-r6-and-dopesmoke-punch-lyrics\\\",\\\"updatedByHumanAt\\\":1697223606,\\\"titleWithFeatured\\\":\\\"PUNCH\\\",\\\"title\\\":\\\"PUNCH\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":29},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/5bda54eac4a0d01156ad8cd7101c15bc.724x730x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/5bda54eac4a0d01156ad8cd7101c15bc.300x302x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Jun. 25, 2023\\\",\\\"releaseDateForDisplay\\\":\\\"June 25, 2023\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":25,\\\"month\\\":6,\\\"year\\\":2023},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Pr-sad-r6-and-dopesmoke-punch-sample\\\",\\\"pyongsCount\\\":2,\\\"primaryArtistNames\\\":\\\"PR SAD, R6 & dopesmoke\\\",\\\"path\\\":\\\"/Pr-sad-r6-and-dopesmoke-punch-lyrics\\\",\\\"lyricsUpdatedAt\\\":1697223606,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":16604797,\\\"instrumental\\\":false,\\\"id\\\":9076995,\\\"headerImageUrl\\\":\\\"https://images.genius.com/5bda54eac4a0d01156ad8cd7101c15bc.724x730x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/5bda54eac4a0d01156ad8cd7101c15bc.300x302x1.jpg\\\",\\\"fullTitle\\\":\\\"PUNCH by PR SAD, R6 & dopesmoke\\\",\\\"artistNames\\\":\\\"PR SAD, R6 & dopesmoke\\\",\\\"apiPath\\\":\\\"/songs/9076995\\\",\\\"annotationCount\\\":29,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":5223,\\\"url\\\":\\\"https://genius.com/artists/Snoop-dogg\\\",\\\"slug\\\":\\\"Snoop-dogg\\\",\\\"name\\\":\\\"Snoop Dogg\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"s\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/91bd22f5e53a3ea3cb1436de8f4a3722.1000x1000x1.jpg\\\",\\\"id\\\":46,\\\"headerImageUrl\\\":\\\"https://images.genius.com/4a8a11f406cfaa80085daaee24b78863.1000x563x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/46\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":5223,\\\"url\\\":\\\"https://genius.com/artists/Snoop-dogg\\\",\\\"slug\\\":\\\"Snoop-dogg\\\",\\\"name\\\":\\\"Snoop Dogg\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"s\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/91bd22f5e53a3ea3cb1436de8f4a3722.1000x1000x1.jpg\\\",\\\"id\\\":46,\\\"headerImageUrl\\\":\\\"https://images.genius.com/4a8a11f406cfaa80085daaee24b78863.1000x563x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/46\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Snoop-dogg-straight-ballin-lyrics\\\",\\\"updatedByHumanAt\\\":1715781220,\\\"titleWithFeatured\\\":\\\"Straight Ballin\\'\\\",\\\"title\\\":\\\"Straight Ballin’\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/25bc30a6b9ef9bf94d76fe870a178c85.600x600x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/25bc30a6b9ef9bf94d76fe870a178c85.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Sep. 30, 2023\\\",\\\"releaseDateForDisplay\\\":\\\"September 30, 2023\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":30,\\\"month\\\":9,\\\"year\\\":2023},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Snoop-dogg-straight-ballin-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Snoop Dogg\\\",\\\"path\\\":\\\"/Snoop-dogg-straight-ballin-lyrics\\\",\\\"lyricsUpdatedAt\\\":1679753166,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":14240235,\\\"instrumental\\\":false,\\\"id\\\":8888370,\\\"headerImageUrl\\\":\\\"https://images.genius.com/25bc30a6b9ef9bf94d76fe870a178c85.600x600x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/25bc30a6b9ef9bf94d76fe870a178c85.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"Straight Ballin\\' by Snoop Dogg\\\",\\\"artistNames\\\":\\\"Snoop Dogg\\\",\\\"apiPath\\\":\\\"/songs/8888370\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Kayspinit\\\",\\\"slug\\\":\\\"Kayspinit\\\",\\\"name\\\":\\\"KaySpinIt\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"k\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/8c07179babc209d9bd7cd6fd9b1e45f6.900x900x1.jpg\\\",\\\"id\\\":2783182,\\\"headerImageUrl\\\":\\\"https://images.genius.com/dca9314e0004cdf466fd8a08831f86ea.1000x562x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2783182\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Kayspinit\\\",\\\"slug\\\":\\\"Kayspinit\\\",\\\"name\\\":\\\"KaySpinIt\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"k\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/8c07179babc209d9bd7cd6fd9b1e45f6.900x900x1.jpg\\\",\\\"id\\\":2783182,\\\"headerImageUrl\\\":\\\"https://images.genius.com/dca9314e0004cdf466fd8a08831f86ea.1000x562x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2783182\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Kayspinit-task-force-x-intro-lyrics\\\",\\\"updatedByHumanAt\\\":1666586197,\\\"titleWithFeatured\\\":\\\"Task Force X (Intro)\\\",\\\"title\\\":\\\"Task Force X (Intro)\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":4},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/d78c0ec317da43da7c28e9e0b1259703.1000x1000x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/d78c0ec317da43da7c28e9e0b1259703.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Sep. 29, 2022\\\",\\\"releaseDateForDisplay\\\":\\\"September 29, 2022\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":29,\\\"month\\\":9,\\\"year\\\":2022},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Kayspinit-task-force-x-intro-sample\\\",\\\"pyongsCount\\\":3,\\\"primaryArtistNames\\\":\\\"KaySpinIt\\\",\\\"path\\\":\\\"/Kayspinit-task-force-x-intro-lyrics\\\",\\\"lyricsUpdatedAt\\\":1666636629,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":12800475,\\\"instrumental\\\":false,\\\"id\\\":8426456,\\\"headerImageUrl\\\":\\\"https://images.genius.com/d78c0ec317da43da7c28e9e0b1259703.1000x1000x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/d78c0ec317da43da7c28e9e0b1259703.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"Task Force X (Intro) by KaySpinIt\\\",\\\"artistNames\\\":\\\"KaySpinIt\\\",\\\"apiPath\\\":\\\"/songs/8426456\\\",\\\"annotationCount\\\":4,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Yhung-to\\\",\\\"slug\\\":\\\"Yhung-to\\\",\\\"name\\\":\\\"Yhung T.O.\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"y\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/b863cc7298abe71ddf48548c3f3e1bd3.1000x1000x1.png\\\",\\\"id\\\":1048962,\\\"headerImageUrl\\\":\\\"https://images.genius.com/58a5d2217ec72d414e104b7c1e2659ff.1000x1000x1.png\\\",\\\"apiPath\\\":\\\"/artists/1048962\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Yhung-to\\\",\\\"slug\\\":\\\"Yhung-to\\\",\\\"name\\\":\\\"Yhung T.O.\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"y\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/b863cc7298abe71ddf48548c3f3e1bd3.1000x1000x1.png\\\",\\\"id\\\":1048962,\\\"headerImageUrl\\\":\\\"https://images.genius.com/58a5d2217ec72d414e104b7c1e2659ff.1000x1000x1.png\\\",\\\"apiPath\\\":\\\"/artists/1048962\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Yhung-to-paranoia-lyrics\\\",\\\"updatedByHumanAt\\\":1664229867,\\\"titleWithFeatured\\\":\\\"Paranoia\\\",\\\"title\\\":\\\"Paranoia\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/7d95c94ce9f492bc050f8446f06aaac3.1000x1000x1.png\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/7d95c94ce9f492bc050f8446f06aaac3.300x300x1.png\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Dec. 8, 2020\\\",\\\"releaseDateForDisplay\\\":\\\"December 8, 2020\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":8,\\\"month\\\":12,\\\"year\\\":2020},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Yhung-to-paranoia-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Yhung T.O.\\\",\\\"path\\\":\\\"/Yhung-to-paranoia-lyrics\\\",\\\"lyricsUpdatedAt\\\":1662670295,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":12490052,\\\"instrumental\\\":false,\\\"id\\\":6206185,\\\"headerImageUrl\\\":\\\"https://images.genius.com/7d95c94ce9f492bc050f8446f06aaac3.1000x1000x1.png\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/7d95c94ce9f492bc050f8446f06aaac3.300x300x1.png\\\",\\\"fullTitle\\\":\\\"Paranoia by Yhung T.O.\\\",\\\"artistNames\\\":\\\"Yhung T.O.\\\",\\\"apiPath\\\":\\\"/songs/6206185\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Verde-babii-and-armani-depaul\\\",\\\"slug\\\":\\\"Verde-babii-and-armani-depaul\\\",\\\"name\\\":\\\"Verde Babii & Armani Depaul\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"v\\\",\\\"imageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"id\\\":3254916,\\\"headerImageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"apiPath\\\":\\\"/artists/3254916\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Verde-babii-and-armani-depaul\\\",\\\"slug\\\":\\\"Verde-babii-and-armani-depaul\\\",\\\"name\\\":\\\"Verde Babii & Armani Depaul\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"v\\\",\\\"imageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"id\\\":3254916,\\\"headerImageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"apiPath\\\":\\\"/artists/3254916\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Verde-babii-and-armani-depaul-all-eyes-on-me-lyrics\\\",\\\"updatedByHumanAt\\\":1670908154,\\\"titleWithFeatured\\\":\\\"All Eyes On Me\\\",\\\"title\\\":\\\"All Eyes On Me\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/7e27a4f269e97c522d7148bbc5f811cc.1000x1000x1.png\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/7e27a4f269e97c522d7148bbc5f811cc.300x300x1.png\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"May. 14, 2022\\\",\\\"releaseDateForDisplay\\\":\\\"May 14, 2022\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":14,\\\"month\\\":5,\\\"year\\\":2022},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Verde-babii-and-armani-depaul-all-eyes-on-me-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Verde Babii & Armani Depaul\\\",\\\"path\\\":\\\"/Verde-babii-and-armani-depaul-all-eyes-on-me-lyrics\\\",\\\"lyricsUpdatedAt\\\":1670908113,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":15118685,\\\"instrumental\\\":false,\\\"id\\\":8266513,\\\"headerImageUrl\\\":\\\"https://images.genius.com/7e27a4f269e97c522d7148bbc5f811cc.1000x1000x1.png\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/7e27a4f269e97c522d7148bbc5f811cc.300x300x1.png\\\",\\\"fullTitle\\\":\\\"All Eyes On Me by Verde Babii & Armani Depaul\\\",\\\"artistNames\\\":\\\"Verde Babii & Armani Depaul\\\",\\\"apiPath\\\":\\\"/songs/8266513\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Sus\\\",\\\"slug\\\":\\\"Sus\\\",\\\"name\\\":\\\"Sus\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"s\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/c56a59218c64b40069816eaead7564b1.535x535x1.jpg\\\",\\\"id\\\":2165777,\\\"headerImageUrl\\\":\\\"https://images.genius.com/03b64eb9bdf92a041f7295c714f0f753.600x296x268.gif\\\",\\\"apiPath\\\":\\\"/artists/2165777\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Sus\\\",\\\"slug\\\":\\\"Sus\\\",\\\"name\\\":\\\"Sus\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"s\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/c56a59218c64b40069816eaead7564b1.535x535x1.jpg\\\",\\\"id\\\":2165777,\\\"headerImageUrl\\\":\\\"https://images.genius.com/03b64eb9bdf92a041f7295c714f0f753.600x296x268.gif\\\",\\\"apiPath\\\":\\\"/artists/2165777\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Malty-2bz\\\",\\\"slug\\\":\\\"Malty-2bz\\\",\\\"name\\\":\\\"Malty 2BZ\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"m\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/30990ba0e82c4b46d0c8999532cd3756.559x559x1.jpg\\\",\\\"id\\\":1953290,\\\"headerImageUrl\\\":\\\"https://images.genius.com/11efc49c8fa882437c5fcccaca4e11a6.1000x563x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/1953290\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/Strika\\\",\\\"slug\\\":\\\"Strika\\\",\\\"name\\\":\\\"Strika\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"s\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/19a8034fd7d1dcb1bc32f503bf7c77c4.391x391x1.png\\\",\\\"id\\\":2577618,\\\"headerImageUrl\\\":\\\"https://images.genius.com/19a8034fd7d1dcb1bc32f503bf7c77c4.391x391x1.png\\\",\\\"apiPath\\\":\\\"/artists/2577618\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/Workrate\\\",\\\"slug\\\":\\\"Workrate\\\",\\\"name\\\":\\\"Workrate\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"w\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/994d4684e82d6007348047873a6d2728.1000x1000x1.jpg\\\",\\\"id\\\":1492439,\\\"headerImageUrl\\\":\\\"https://images.genius.com/1bbb6990340133a3883e53fb6fed9562.1000x667x15.gif\\\",\\\"apiPath\\\":\\\"/artists/1492439\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/Rondomontana\\\",\\\"slug\\\":\\\"Rondomontana\\\",\\\"name\\\":\\\"RondoMontana\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"r\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/4a07710e9a8f3455b346e928a88171b5.535x535x1.jpg\\\",\\\"id\\\":2871653,\\\"headerImageUrl\\\":\\\"https://images.genius.com/a70053ee3351cb4c3f462cd3bcdee63e.1000x563x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2871653\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/Broadday\\\",\\\"slug\\\":\\\"Broadday\\\",\\\"name\\\":\\\"Broadday\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"b\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/8f323882278997baac1103df049d59b8.851x851x1.jpg\\\",\\\"id\\\":2448535,\\\"headerImageUrl\\\":\\\"https://images.genius.com/8f323882278997baac1103df049d59b8.851x851x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2448535\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/Lil-dotz\\\",\\\"slug\\\":\\\"Lil-dotz\\\",\\\"name\\\":\\\"Lil Dotz\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"l\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/e8897f5f0b08df90d3dbbe8d3f838bf9.367x367x1.png\\\",\\\"id\\\":1582445,\\\"headerImageUrl\\\":\\\"https://images.genius.com/e8897f5f0b08df90d3dbbe8d3f838bf9.367x367x1.png\\\",\\\"apiPath\\\":\\\"/artists/1582445\\\",\\\"type\\\":\\\"artist\\\"}],\\\"url\\\":\\\"https://genius.com/Sus-caught-inda-rain-2-lyrics\\\",\\\"updatedByHumanAt\\\":1698229785,\\\"titleWithFeatured\\\":\\\"Caught Inda Rain 2 (Ft. Broadday, Lil Dotz, Malty 2BZ, RondoMontana, Strika & Workrate)\\\",\\\"title\\\":\\\"Caught Inda Rain 2\\\",\\\"stats\\\":{\\\"pageviews\\\":13263,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":36},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/bbc132493ccfdba63b038a4bac772e70.1000x1000x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/bbc132493ccfdba63b038a4bac772e70.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Jul. 3, 2022\\\",\\\"releaseDateForDisplay\\\":\\\"July 3, 2022\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":3,\\\"month\\\":7,\\\"year\\\":2022},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Sus-caught-inda-rain-2-sample\\\",\\\"pyongsCount\\\":4,\\\"primaryArtistNames\\\":\\\"Sus\\\",\\\"path\\\":\\\"/Sus-caught-inda-rain-2-lyrics\\\",\\\"lyricsUpdatedAt\\\":1698229785,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":10246179,\\\"instrumental\\\":false,\\\"id\\\":8088127,\\\"headerImageUrl\\\":\\\"https://images.genius.com/bbc132493ccfdba63b038a4bac772e70.1000x1000x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/bbc132493ccfdba63b038a4bac772e70.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"Caught Inda Rain 2 by Sus (Ft. Broadday, Lil Dotz, Malty 2BZ, RondoMontana, Strika & Workrate)\\\",\\\"artistNames\\\":\\\"Sus (Ft. Broadday, Lil Dotz, Malty 2BZ, RondoMontana, Strika & Workrate)\\\",\\\"apiPath\\\":\\\"/songs/8088127\\\",\\\"annotationCount\\\":36,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Fumez-the-engineer-and-hazey\\\",\\\"slug\\\":\\\"Fumez-the-engineer-and-hazey\\\",\\\"name\\\":\\\"Fumez The Engineer & Hazey\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"f\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/0c3a459f29c450c54570ebb04c7ef000.1000x1000x1.jpg\\\",\\\"id\\\":3145330,\\\"headerImageUrl\\\":\\\"https://images.genius.com/0c3a459f29c450c54570ebb04c7ef000.1000x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/3145330\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Fumez-the-engineer-and-hazey\\\",\\\"slug\\\":\\\"Fumez-the-engineer-and-hazey\\\",\\\"name\\\":\\\"Fumez The Engineer & Hazey\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"f\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/0c3a459f29c450c54570ebb04c7ef000.1000x1000x1.jpg\\\",\\\"id\\\":3145330,\\\"headerImageUrl\\\":\\\"https://images.genius.com/0c3a459f29c450c54570ebb04c7ef000.1000x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/3145330\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Fumez-the-engineer-and-hazey-hazey-x-fumez-the-engineer-plugged-in-pt-1-lyrics\\\",\\\"updatedByHumanAt\\\":1666737725,\\\"titleWithFeatured\\\":\\\"Hazey x Fumez the Engineer - Plugged In, Pt. 1\\\",\\\"title\\\":\\\"Hazey x Fumez the Engineer - Plugged In, Pt. 1\\\",\\\"stats\\\":{\\\"pageviews\\\":7729,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":9},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/e10b664c99232705438893caaa2b9c11.1000x1000x1.png\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/e10b664c99232705438893caaa2b9c11.300x300x1.png\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Mar. 9, 2022\\\",\\\"releaseDateForDisplay\\\":\\\"March 9, 2022\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":9,\\\"month\\\":3,\\\"year\\\":2022},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Fumez-the-engineer-and-hazey-hazey-x-fumez-the-engineer-plugged-in-pt-1-sample\\\",\\\"pyongsCount\\\":2,\\\"primaryArtistNames\\\":\\\"Fumez The Engineer & Hazey\\\",\\\"path\\\":\\\"/Fumez-the-engineer-and-hazey-hazey-x-fumez-the-engineer-plugged-in-pt-1-lyrics\\\",\\\"lyricsUpdatedAt\\\":1666737676,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":6139655,\\\"instrumental\\\":false,\\\"id\\\":7672542,\\\"headerImageUrl\\\":\\\"https://images.genius.com/e10b664c99232705438893caaa2b9c11.1000x1000x1.png\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/e10b664c99232705438893caaa2b9c11.300x300x1.png\\\",\\\"fullTitle\\\":\\\"Hazey x Fumez the Engineer - Plugged In, Pt. 1 by Fumez The Engineer & Hazey\\\",\\\"artistNames\\\":\\\"Fumez The Engineer & Hazey\\\",\\\"apiPath\\\":\\\"/songs/7672542\\\",\\\"annotationCount\\\":9,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":2714,\\\"url\\\":\\\"https://genius.com/artists/Chief-keef\\\",\\\"slug\\\":\\\"Chief-keef\\\",\\\"name\\\":\\\"Chief Keef\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"c\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/12592b0da715efe0916dd3645763c64a.999x999x1.jpg\\\",\\\"id\\\":16808,\\\"headerImageUrl\\\":\\\"https://images.genius.com/4021ba198d32951f29a747ea92c3d57f.1000x333x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/16808\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":2714,\\\"url\\\":\\\"https://genius.com/artists/Chief-keef\\\",\\\"slug\\\":\\\"Chief-keef\\\",\\\"name\\\":\\\"Chief Keef\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"c\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/12592b0da715efe0916dd3645763c64a.999x999x1.jpg\\\",\\\"id\\\":16808,\\\"headerImageUrl\\\":\\\"https://images.genius.com/4021ba198d32951f29a747ea92c3d57f.1000x333x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/16808\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Tadoe\\\",\\\"slug\\\":\\\"Tadoe\\\",\\\"name\\\":\\\"Tadoe\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"t\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/034d9df1747988e5d5566c8ea612fae1.640x640x1.jpg\\\",\\\"id\\\":35512,\\\"headerImageUrl\\\":\\\"https://images.genius.com/349f5c5de7019e08fa47f0d1a271d93e.1000x834x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/35512\\\",\\\"type\\\":\\\"artist\\\"}],\\\"url\\\":\\\"https://genius.com/Chief-keef-me-lyrics\\\",\\\"updatedByHumanAt\\\":1716699014,\\\"titleWithFeatured\\\":\\\"Me (Ft. Tadoe)\\\",\\\"title\\\":\\\"Me\\\",\\\"stats\\\":{\\\"pageviews\\\":89971,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":2},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/5661a3c924647dd39fddc9ab59c61842.770x770x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/5661a3c924647dd39fddc9ab59c61842.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Oct. 12, 2013\\\",\\\"releaseDateForDisplay\\\":\\\"October 12, 2013\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":12,\\\"month\\\":10,\\\"year\\\":2013},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Chief-keef-me-sample\\\",\\\"pyongsCount\\\":8,\\\"primaryArtistNames\\\":\\\"Chief Keef\\\",\\\"path\\\":\\\"/Chief-keef-me-lyrics\\\",\\\"lyricsUpdatedAt\\\":1716699014,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":3514,\\\"instrumental\\\":false,\\\"id\\\":237813,\\\"headerImageUrl\\\":\\\"https://images.genius.com/5661a3c924647dd39fddc9ab59c61842.770x770x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/5661a3c924647dd39fddc9ab59c61842.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"Me by Chief Keef (Ft. Tadoe)\\\",\\\"artistNames\\\":\\\"Chief Keef (Ft. Tadoe)\\\",\\\"apiPath\\\":\\\"/songs/237813\\\",\\\"annotationCount\\\":2,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Francuz-mordo\\\",\\\"slug\\\":\\\"Francuz-mordo\\\",\\\"name\\\":\\\"Francuz Mordo\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"f\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/60e522fa615bc7c5779bebf32d329ad6.720x720x1.jpg\\\",\\\"id\\\":2728103,\\\"headerImageUrl\\\":\\\"https://images.genius.com/2302594f23b91366b899c0a912ef593d.1000x435x1.png\\\",\\\"apiPath\\\":\\\"/artists/2728103\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Francuz-mordo\\\",\\\"slug\\\":\\\"Francuz-mordo\\\",\\\"name\\\":\\\"Francuz Mordo\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"f\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/60e522fa615bc7c5779bebf32d329ad6.720x720x1.jpg\\\",\\\"id\\\":2728103,\\\"headerImageUrl\\\":\\\"https://images.genius.com/2302594f23b91366b899c0a912ef593d.1000x435x1.png\\\",\\\"apiPath\\\":\\\"/artists/2728103\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Francuz-mordo-all-eyez-on-me-lyrics\\\",\\\"updatedByHumanAt\\\":1676368914,\\\"titleWithFeatured\\\":\\\"All eyez on me\\\",\\\"title\\\":\\\"All eyez on me\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":6},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/1eecf50be81575302447c6fdab0cdd03.1000x1000x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/1eecf50be81575302447c6fdab0cdd03.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Oct. 19, 2021\\\",\\\"releaseDateForDisplay\\\":\\\"October 19, 2021\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":19,\\\"month\\\":10,\\\"year\\\":2021},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Francuz-mordo-all-eyez-on-me-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Francuz Mordo\\\",\\\"path\\\":\\\"/Francuz-mordo-all-eyez-on-me-lyrics\\\",\\\"lyricsUpdatedAt\\\":1676376940,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":7887315,\\\"instrumental\\\":false,\\\"id\\\":7295495,\\\"headerImageUrl\\\":\\\"https://images.genius.com/1eecf50be81575302447c6fdab0cdd03.1000x1000x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/1eecf50be81575302447c6fdab0cdd03.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"All eyez on me by Francuz Mordo\\\",\\\"artistNames\\\":\\\"Francuz Mordo\\\",\\\"apiPath\\\":\\\"/songs/7295495\\\",\\\"annotationCount\\\":6,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":639,\\\"url\\\":\\\"https://genius.com/artists/Al-doms\\\",\\\"slug\\\":\\\"Al-doms\\\",\\\"name\\\":\\\"Al-Doms\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"a\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/3bfeef76ccb4f04456a9d0985313c21c.1000x1000x1.jpg\\\",\\\"id\\\":1719329,\\\"headerImageUrl\\\":\\\"https://images.genius.com/73633ee8b2673e18210e7fb19ccb48e0.1000x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/1719329\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":639,\\\"url\\\":\\\"https://genius.com/artists/Al-doms\\\",\\\"slug\\\":\\\"Al-doms\\\",\\\"name\\\":\\\"Al-Doms\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"a\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/3bfeef76ccb4f04456a9d0985313c21c.1000x1000x1.jpg\\\",\\\"id\\\":1719329,\\\"headerImageUrl\\\":\\\"https://images.genius.com/73633ee8b2673e18210e7fb19ccb48e0.1000x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/1719329\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Al-doms-bff-lyrics\\\",\\\"updatedByHumanAt\\\":1646242897,\\\"titleWithFeatured\\\":\\\"BFF\\\",\\\"title\\\":\\\"BFF\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/4043a5dacbe52124b9b7a80af93ddbb8.1000x1000x1.png\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/4043a5dacbe52124b9b7a80af93ddbb8.300x300x1.png\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Aug. 25, 2021\\\",\\\"releaseDateForDisplay\\\":\\\"August 25, 2021\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":25,\\\"month\\\":8,\\\"year\\\":2021},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Al-doms-bff-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Al-Doms\\\",\\\"path\\\":\\\"/Al-doms-bff-lyrics\\\",\\\"lyricsUpdatedAt\\\":1639740186,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":548516,\\\"instrumental\\\":false,\\\"id\\\":7165704,\\\"headerImageUrl\\\":\\\"https://images.genius.com/4043a5dacbe52124b9b7a80af93ddbb8.1000x1000x1.png\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/4043a5dacbe52124b9b7a80af93ddbb8.300x300x1.png\\\",\\\"fullTitle\\\":\\\"BFF by Al-Doms\\\",\\\"artistNames\\\":\\\"Al-Doms\\\",\\\"apiPath\\\":\\\"/songs/7165704\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":6427,\\\"url\\\":\\\"https://genius.com/artists/Freddie-gibbs\\\",\\\"slug\\\":\\\"Freddie-gibbs\\\",\\\"name\\\":\\\"Freddie Gibbs\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"f\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/c9a0de64e8b42a8635ec2fe5ddd9c41c.899x899x1.jpg\\\",\\\"id\\\":939,\\\"headerImageUrl\\\":\\\"https://images.genius.com/eb0f9c196fe929a248bccba2428d22a8.510x504x1.png\\\",\\\"apiPath\\\":\\\"/artists/939\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":6427,\\\"url\\\":\\\"https://genius.com/artists/Freddie-gibbs\\\",\\\"slug\\\":\\\"Freddie-gibbs\\\",\\\"name\\\":\\\"Freddie Gibbs\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"f\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/c9a0de64e8b42a8635ec2fe5ddd9c41c.899x899x1.jpg\\\",\\\"id\\\":939,\\\"headerImageUrl\\\":\\\"https://images.genius.com/eb0f9c196fe929a248bccba2428d22a8.510x504x1.png\\\",\\\"apiPath\\\":\\\"/artists/939\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Freddie-gibbs-rearview-lyrics\\\",\\\"updatedByHumanAt\\\":1714031902,\\\"titleWithFeatured\\\":\\\"Rearview\\\",\\\"title\\\":\\\"Rearview\\\",\\\"stats\\\":{\\\"pageviews\\\":16622,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":2},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/6a6a9dbb83d9848cfceae302bd4ad523.600x600x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/6a6a9dbb83d9848cfceae302bd4ad523.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Nov. 17, 2015\\\",\\\"releaseDateForDisplay\\\":\\\"November 17, 2015\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":17,\\\"month\\\":11,\\\"year\\\":2015},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Freddie-gibbs-rearview-sample\\\",\\\"pyongsCount\\\":6,\\\"primaryArtistNames\\\":\\\"Freddie Gibbs\\\",\\\"path\\\":\\\"/Freddie-gibbs-rearview-lyrics\\\",\\\"lyricsUpdatedAt\\\":1622753521,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":1644620,\\\"instrumental\\\":false,\\\"id\\\":2357973,\\\"headerImageUrl\\\":\\\"https://images.genius.com/6a6a9dbb83d9848cfceae302bd4ad523.600x600x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/6a6a9dbb83d9848cfceae302bd4ad523.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"Rearview by Freddie Gibbs\\\",\\\"artistNames\\\":\\\"Freddie Gibbs\\\",\\\"apiPath\\\":\\\"/songs/2357973\\\",\\\"annotationCount\\\":6,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":132,\\\"url\\\":\\\"https://genius.com/artists/Alonestar\\\",\\\"slug\\\":\\\"Alonestar\\\",\\\"name\\\":\\\"ALONESTAR\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"a\\\",\\\"imageUrl\\\":\\\"https://s3.amazonaws.com/rapgenius/avatars/medium/1362941739_WYtfa3uRmC2WZPabyeug.jpg\\\",\\\"id\\\":41073,\\\"headerImageUrl\\\":\\\"https://s3.amazonaws.com/rapgenius/avatars/medium/1362941739_WYtfa3uRmC2WZPabyeug.jpg\\\",\\\"apiPath\\\":\\\"/artists/41073\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":132,\\\"url\\\":\\\"https://genius.com/artists/Alonestar\\\",\\\"slug\\\":\\\"Alonestar\\\",\\\"name\\\":\\\"ALONESTAR\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"a\\\",\\\"imageUrl\\\":\\\"https://s3.amazonaws.com/rapgenius/avatars/medium/1362941739_WYtfa3uRmC2WZPabyeug.jpg\\\",\\\"id\\\":41073,\\\"headerImageUrl\\\":\\\"https://s3.amazonaws.com/rapgenius/avatars/medium/1362941739_WYtfa3uRmC2WZPabyeug.jpg\\\",\\\"apiPath\\\":\\\"/artists/41073\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[{\\\"iq\\\":1334,\\\"url\\\":\\\"https://genius.com/artists/Ed-sheeran\\\",\\\"slug\\\":\\\"Ed-sheeran\\\",\\\"name\\\":\\\"Ed Sheeran\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"e\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/e61adb6350f274fda9c4b1fa73939e95.1000x1000x1.jpg\\\",\\\"id\\\":12418,\\\"headerImageUrl\\\":\\\"https://images.genius.com/3ab6a4f22f71e08d2b68987f2e725ca9.1000x750x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/12418\\\",\\\"type\\\":\\\"artist\\\"}],\\\"url\\\":\\\"https://genius.com/Alonestar-hands-high-lyrics\\\",\\\"updatedByHumanAt\\\":1621584146,\\\"titleWithFeatured\\\":\\\"Hands High (Ft. Ed Sheeran)\\\",\\\"title\\\":\\\"Hands High\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/fd6fcfd7a885ef3b3aee21379cf4ebef.1000x1000x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/fd6fcfd7a885ef3b3aee21379cf4ebef.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"May. 21, 2021\\\",\\\"releaseDateForDisplay\\\":\\\"May 21, 2021\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":21,\\\"month\\\":5,\\\"year\\\":2021},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Alonestar-hands-high-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"ALONESTAR\\\",\\\"path\\\":\\\"/Alonestar-hands-high-lyrics\\\",\\\"lyricsUpdatedAt\\\":1621582421,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":458792,\\\"instrumental\\\":false,\\\"id\\\":6833246,\\\"headerImageUrl\\\":\\\"https://images.genius.com/fd6fcfd7a885ef3b3aee21379cf4ebef.1000x1000x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/fd6fcfd7a885ef3b3aee21379cf4ebef.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"Hands High by ALONESTAR (Ft. Ed Sheeran)\\\",\\\"artistNames\\\":\\\"ALONESTAR (Ft. Ed Sheeran)\\\",\\\"apiPath\\\":\\\"/songs/6833246\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":4045,\\\"url\\\":\\\"https://genius.com/artists/50-cent\\\",\\\"slug\\\":\\\"50-cent\\\",\\\"name\\\":\\\"50 Cent\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"0\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/10f98dca7bcd1a31222e36374544cad5.1000x1000x1.png\\\",\\\"id\\\":108,\\\"headerImageUrl\\\":\\\"https://images.genius.com/27360d71a1b5342f0ff6b262f1b988cb.605x448x1.png\\\",\\\"apiPath\\\":\\\"/artists/108\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":4045,\\\"url\\\":\\\"https://genius.com/artists/50-cent\\\",\\\"slug\\\":\\\"50-cent\\\",\\\"name\\\":\\\"50 Cent\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"0\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/10f98dca7bcd1a31222e36374544cad5.1000x1000x1.png\\\",\\\"id\\\":108,\\\"headerImageUrl\\\":\\\"https://images.genius.com/27360d71a1b5342f0ff6b262f1b988cb.605x448x1.png\\\",\\\"apiPath\\\":\\\"/artists/108\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/50-cent-be-a-gentleman-lyrics\\\",\\\"updatedByHumanAt\\\":1709408073,\\\"titleWithFeatured\\\":\\\"Be A Gentleman\\\",\\\"title\\\":\\\"Be A Gentleman\\\",\\\"stats\\\":{\\\"pageviews\\\":32664,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":5},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/2eca458210218b7a1ae330e42b3fb03b.500x500x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/2eca458210218b7a1ae330e42b3fb03b.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Jun. 25, 2002\\\",\\\"releaseDateForDisplay\\\":\\\"June 25, 2002\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":25,\\\"month\\\":6,\\\"year\\\":2002},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/50-cent-be-a-gentleman-sample\\\",\\\"pyongsCount\\\":6,\\\"primaryArtistNames\\\":\\\"50 Cent\\\",\\\"path\\\":\\\"/50-cent-be-a-gentleman-lyrics\\\",\\\"lyricsUpdatedAt\\\":1708374199,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":50,\\\"instrumental\\\":false,\\\"id\\\":7031,\\\"headerImageUrl\\\":\\\"https://images.genius.com/2eca458210218b7a1ae330e42b3fb03b.500x500x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/2eca458210218b7a1ae330e42b3fb03b.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"Be A Gentleman by 50 Cent\\\",\\\"artistNames\\\":\\\"50 Cent\\\",\\\"apiPath\\\":\\\"/songs/7031\\\",\\\"annotationCount\\\":15,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Mc-ridin-rapper\\\",\\\"slug\\\":\\\"Mc-ridin-rapper\\\",\\\"name\\\":\\\"MC Ridin (Rapper)\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"m\\\",\\\"imageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"id\\\":2498583,\\\"headerImageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"apiPath\\\":\\\"/artists/2498583\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Mc-ridin-rapper\\\",\\\"slug\\\":\\\"Mc-ridin-rapper\\\",\\\"name\\\":\\\"MC Ridin (Rapper)\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"m\\\",\\\"imageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"id\\\":2498583,\\\"headerImageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"apiPath\\\":\\\"/artists/2498583\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Mc-ridin-rapper-destiny-lyrics\\\",\\\"updatedByHumanAt\\\":1606729147,\\\"titleWithFeatured\\\":\\\"Destiny\\\",\\\"title\\\":\\\"Destiny\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":6},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/3c8425c59f6491b5279643c98db8c43c.446x438x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/3c8425c59f6491b5279643c98db8c43c.300x295x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Nov. 28, 2020\\\",\\\"releaseDateForDisplay\\\":\\\"November 28, 2020\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":28,\\\"month\\\":11,\\\"year\\\":2020},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Mc-ridin-rapper-destiny-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"MC Ridin (Rapper)\\\",\\\"path\\\":\\\"/Mc-ridin-rapper-destiny-lyrics\\\",\\\"lyricsUpdatedAt\\\":1606672686,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":9153523,\\\"instrumental\\\":false,\\\"id\\\":6222569,\\\"headerImageUrl\\\":\\\"https://images.genius.com/3c8425c59f6491b5279643c98db8c43c.446x438x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/3c8425c59f6491b5279643c98db8c43c.300x295x1.jpg\\\",\\\"fullTitle\\\":\\\"Destiny by MC Ridin (Rapper)\\\",\\\"artistNames\\\":\\\"MC Ridin (Rapper)\\\",\\\"apiPath\\\":\\\"/songs/6222569\\\",\\\"annotationCount\\\":6,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":112143,\\\"url\\\":\\\"https://genius.com/artists/Bright-campa\\\",\\\"slug\\\":\\\"Bright-campa\\\",\\\"name\\\":\\\"Bright Campa\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"b\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/8ef7b5a3682baa523d2fb5d8f710f9ca.506x1000x129.gif\\\",\\\"id\\\":1285747,\\\"headerImageUrl\\\":\\\"https://images.genius.com/db9942bd45e835ec69f9f0cb822db38b.480x480x297.gif\\\",\\\"apiPath\\\":\\\"/artists/1285747\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":112143,\\\"url\\\":\\\"https://genius.com/artists/Bright-campa\\\",\\\"slug\\\":\\\"Bright-campa\\\",\\\"name\\\":\\\"Bright Campa\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"b\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/8ef7b5a3682baa523d2fb5d8f710f9ca.506x1000x129.gif\\\",\\\"id\\\":1285747,\\\"headerImageUrl\\\":\\\"https://images.genius.com/db9942bd45e835ec69f9f0cb822db38b.480x480x297.gif\\\",\\\"apiPath\\\":\\\"/artists/1285747\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Bright-campa-broke-bitches-lyrics\\\",\\\"updatedByHumanAt\\\":1667540762,\\\"titleWithFeatured\\\":\\\"Broke Bitches\\\",\\\"title\\\":\\\"Broke Bitches\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/2a252b55647effc06ef20d7850c964b9.1000x1000x1.png\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/2a252b55647effc06ef20d7850c964b9.300x300x1.png\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Jan. 3, 2021\\\",\\\"releaseDateForDisplay\\\":\\\"January 3, 2021\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":3,\\\"month\\\":1,\\\"year\\\":2021},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Bright-campa-broke-bitches-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Bright Campa\\\",\\\"path\\\":\\\"/Bright-campa-broke-bitches-lyrics\\\",\\\"lyricsUpdatedAt\\\":1619335813,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":5628993,\\\"instrumental\\\":false,\\\"id\\\":4606565,\\\"headerImageUrl\\\":\\\"https://images.genius.com/2a252b55647effc06ef20d7850c964b9.1000x1000x1.png\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/2a252b55647effc06ef20d7850c964b9.300x300x1.png\\\",\\\"fullTitle\\\":\\\"Broke Bitches by Bright Campa\\\",\\\"artistNames\\\":\\\"Bright Campa\\\",\\\"apiPath\\\":\\\"/songs/4606565\\\",\\\"annotationCount\\\":1,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Darkside-e\\\",\\\"slug\\\":\\\"Darkside-e\\\",\\\"name\\\":\\\"Darkside E\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"d\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/931478da78cf35b87dfe07351f0b19b5.1000x1000x1.jpg\\\",\\\"id\\\":2042052,\\\"headerImageUrl\\\":\\\"https://images.genius.com/931478da78cf35b87dfe07351f0b19b5.1000x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2042052\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Darkside-e\\\",\\\"slug\\\":\\\"Darkside-e\\\",\\\"name\\\":\\\"Darkside E\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"d\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/931478da78cf35b87dfe07351f0b19b5.1000x1000x1.jpg\\\",\\\"id\\\":2042052,\\\"headerImageUrl\\\":\\\"https://images.genius.com/931478da78cf35b87dfe07351f0b19b5.1000x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2042052\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Darkside-e-all-eyez-on-e-lyrics\\\",\\\"updatedByHumanAt\\\":1577929178,\\\"titleWithFeatured\\\":\\\"All Eyez On E\\\",\\\"title\\\":\\\"All Eyez On E\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":1},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/d53eabebfaab7766b94e5bb252afe84b.1000x1000x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/d53eabebfaab7766b94e5bb252afe84b.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Jun. 29, 2016\\\",\\\"releaseDateForDisplay\\\":\\\"June 29, 2016\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":29,\\\"month\\\":6,\\\"year\\\":2016},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Darkside-e-all-eyez-on-e-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Darkside E\\\",\\\"path\\\":\\\"/Darkside-e-all-eyez-on-e-lyrics\\\",\\\"lyricsUpdatedAt\\\":1577929177,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":9897124,\\\"instrumental\\\":false,\\\"id\\\":5105873,\\\"headerImageUrl\\\":\\\"https://images.genius.com/d53eabebfaab7766b94e5bb252afe84b.1000x1000x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/d53eabebfaab7766b94e5bb252afe84b.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"All Eyez On E by Darkside E\\\",\\\"artistNames\\\":\\\"Darkside E\\\",\\\"apiPath\\\":\\\"/songs/5105873\\\",\\\"annotationCount\\\":1,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":4806,\\\"url\\\":\\\"https://genius.com/artists/Lil-wayne\\\",\\\"slug\\\":\\\"Lil-wayne\\\",\\\"name\\\":\\\"Lil Wayne\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"l\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/aa8b9dce2492fe413c23f77b643788fd.914x914x1.jpg\\\",\\\"id\\\":4,\\\"headerImageUrl\\\":\\\"https://images.genius.com/3b3485a28f2eb47c5ff3d7691e71bcba.1000x333x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/4\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":4806,\\\"url\\\":\\\"https://genius.com/artists/Lil-wayne\\\",\\\"slug\\\":\\\"Lil-wayne\\\",\\\"name\\\":\\\"Lil Wayne\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"l\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/aa8b9dce2492fe413c23f77b643788fd.914x914x1.jpg\\\",\\\"id\\\":4,\\\"headerImageUrl\\\":\\\"https://images.genius.com/3b3485a28f2eb47c5ff3d7691e71bcba.1000x333x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/4\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Christina-milian\\\",\\\"slug\\\":\\\"Christina-milian\\\",\\\"name\\\":\\\"Christina Milian\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"c\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/d274d4eda54d0a5bb72d7c2e3ba48b18.400x400x1.jpg\\\",\\\"id\\\":6757,\\\"headerImageUrl\\\":\\\"https://images.genius.com/1900f7572564d4af261673d2ad1b975c.1000x333x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/6757\\\",\\\"type\\\":\\\"artist\\\"},{\\\"iq\\\":178,\\\"url\\\":\\\"https://genius.com/artists/Mannie-fresh\\\",\\\"slug\\\":\\\"Mannie-fresh\\\",\\\"name\\\":\\\"Mannie Fresh\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"m\\\",\\\"imageUrl\\\":\\\"https://s3.amazonaws.com/rapgenius/1302472115_Mannie%20Fresh.jpg\\\",\\\"id\\\":146,\\\"headerImageUrl\\\":\\\"https://s3.amazonaws.com/rapgenius/1302472115_Mannie%20Fresh.jpg\\\",\\\"apiPath\\\":\\\"/artists/146\\\",\\\"type\\\":\\\"artist\\\"}],\\\"url\\\":\\\"https://genius.com/Lil-wayne-start-this-shit-off-right-2014-version-lyrics\\\",\\\"updatedByHumanAt\\\":1719612620,\\\"titleWithFeatured\\\":\\\"Start This Shit Off Right (2014 Version) (Ft. Christina Milian & Mannie Fresh)\\\",\\\"title\\\":\\\"Start This Shit Off Right (2014 Version)\\\",\\\"stats\\\":{\\\"pageviews\\\":11006,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/9e2d71d269b2971975d5d9ca36deb943.1000x1000x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/9e2d71d269b2971975d5d9ca36deb943.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"2017\\\",\\\"releaseDateForDisplay\\\":\\\"2017\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":null,\\\"month\\\":null,\\\"year\\\":2017},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Lil-wayne-start-this-shit-off-right-2014-version-sample\\\",\\\"pyongsCount\\\":3,\\\"primaryArtistNames\\\":\\\"Lil Wayne\\\",\\\"path\\\":\\\"/Lil-wayne-start-this-shit-off-right-2014-version-lyrics\\\",\\\"lyricsUpdatedAt\\\":1602206276,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":124555,\\\"instrumental\\\":false,\\\"id\\\":2924214,\\\"headerImageUrl\\\":\\\"https://images.genius.com/9e2d71d269b2971975d5d9ca36deb943.1000x1000x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/9e2d71d269b2971975d5d9ca36deb943.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"Start This Shit Off Right (2014 Version) by Lil Wayne (Ft. Christina Milian & Mannie Fresh)\\\",\\\"artistNames\\\":\\\"Lil Wayne (Ft. Christina Milian & Mannie Fresh)\\\",\\\"apiPath\\\":\\\"/songs/2924214\\\",\\\"annotationCount\\\":2,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Arma-blanca\\\",\\\"slug\\\":\\\"Arma-blanca\\\",\\\"name\\\":\\\"Arma Blanca\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"a\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/8aa997291f95984ae1b9bd4119fa4c85.500x500x1.jpg\\\",\\\"id\\\":351840,\\\"headerImageUrl\\\":\\\"https://images.genius.com/b1cedc1645d6526fdd1f6d525eee174f.975x159x1.png\\\",\\\"apiPath\\\":\\\"/artists/351840\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Arma-blanca\\\",\\\"slug\\\":\\\"Arma-blanca\\\",\\\"name\\\":\\\"Arma Blanca\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"a\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/8aa997291f95984ae1b9bd4119fa4c85.500x500x1.jpg\\\",\\\"id\\\":351840,\\\"headerImageUrl\\\":\\\"https://images.genius.com/b1cedc1645d6526fdd1f6d525eee174f.975x159x1.png\\\",\\\"apiPath\\\":\\\"/artists/351840\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Nach\\\",\\\"slug\\\":\\\"Nach\\\",\\\"name\\\":\\\"Nach\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"n\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/99ebd1789e384e3b69f2db7189a48789.634x634x1.jpg\\\",\\\"id\\\":11871,\\\"headerImageUrl\\\":\\\"https://images.genius.com/e9cbd19c5f94217cbc2ad2030e9c19a2.1000x562x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/11871\\\",\\\"type\\\":\\\"artist\\\"}],\\\"url\\\":\\\"https://genius.com/Arma-blanca-el-musicologo-lyrics\\\",\\\"updatedByHumanAt\\\":1699145876,\\\"titleWithFeatured\\\":\\\"El Musicólogo (Ft. Nach)\\\",\\\"title\\\":\\\"El Musicólogo\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/058c3dba981f86c63123bcd3a0b402f8.994x1000x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/058c3dba981f86c63123bcd3a0b402f8.300x302x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"May. 1, 2007\\\",\\\"releaseDateForDisplay\\\":\\\"May 1, 2007\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":1,\\\"month\\\":5,\\\"year\\\":2007},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Arma-blanca-el-musicologo-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Arma Blanca\\\",\\\"path\\\":\\\"/Arma-blanca-el-musicologo-lyrics\\\",\\\"lyricsUpdatedAt\\\":1699145859,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":492802,\\\"instrumental\\\":false,\\\"id\\\":3009750,\\\"headerImageUrl\\\":\\\"https://images.genius.com/058c3dba981f86c63123bcd3a0b402f8.994x1000x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/058c3dba981f86c63123bcd3a0b402f8.300x302x1.jpg\\\",\\\"fullTitle\\\":\\\"El Musicólogo by Arma Blanca (Ft. Nach)\\\",\\\"artistNames\\\":\\\"Arma Blanca (Ft. Nach)\\\",\\\"apiPath\\\":\\\"/songs/3009750\\\",\\\"annotationCount\\\":2,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/2pac\\\",\\\"slug\\\":\\\"2pac\\\",\\\"name\\\":\\\"2Pac\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"0\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/9118a64d842f1611d83f5090989a3475.788x788x1.jpg\\\",\\\"id\\\":59,\\\"headerImageUrl\\\":\\\"https://images.genius.com/66dab5428172d59b83ed49304aacfa05.932x718x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/59\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/2pac\\\",\\\"slug\\\":\\\"2pac\\\",\\\"name\\\":\\\"2Pac\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"0\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/9118a64d842f1611d83f5090989a3475.788x788x1.jpg\\\",\\\"id\\\":59,\\\"headerImageUrl\\\":\\\"https://images.genius.com/66dab5428172d59b83ed49304aacfa05.932x718x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/59\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Big-syke\\\",\\\"slug\\\":\\\"Big-syke\\\",\\\"name\\\":\\\"Big Syke\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"b\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/d45c72b156862a65719161af952e83a1.300x300x1.png\\\",\\\"id\\\":2571,\\\"headerImageUrl\\\":\\\"https://images.genius.com/c48b237b70dd1767f94bd3b13c0f4178.400x296x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2571\\\",\\\"type\\\":\\\"artist\\\"}],\\\"url\\\":\\\"https://genius.com/2pac-syke-interlude-t2001-lyrics\\\",\\\"updatedByHumanAt\\\":1684432412,\\\"titleWithFeatured\\\":\\\"Syke Interlude T2001 (Ft. Big Syke)\\\",\\\"title\\\":\\\"Syke Interlude T2001\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":1},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/a455e738860b86109e5800e28474c43f.1000x1000x1.png\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/a455e738860b86109e5800e28474c43f.300x300x1.png\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Mar. 27, 2001\\\",\\\"releaseDateForDisplay\\\":\\\"March 27, 2001\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":27,\\\"month\\\":3,\\\"year\\\":2001},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/2pac-syke-interlude-t2001-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"2Pac\\\",\\\"path\\\":\\\"/2pac-syke-interlude-t2001-lyrics\\\",\\\"lyricsUpdatedAt\\\":1683478552,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":675102,\\\"instrumental\\\":false,\\\"id\\\":1640242,\\\"headerImageUrl\\\":\\\"https://images.genius.com/a455e738860b86109e5800e28474c43f.1000x1000x1.png\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/a455e738860b86109e5800e28474c43f.300x300x1.png\\\",\\\"fullTitle\\\":\\\"Syke Interlude T2001 by 2Pac (Ft. Big Syke)\\\",\\\"artistNames\\\":\\\"2Pac (Ft. Big Syke)\\\",\\\"apiPath\\\":\\\"/songs/1640242\\\",\\\"annotationCount\\\":1,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":4806,\\\"url\\\":\\\"https://genius.com/artists/Lil-wayne\\\",\\\"slug\\\":\\\"Lil-wayne\\\",\\\"name\\\":\\\"Lil Wayne\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"l\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/aa8b9dce2492fe413c23f77b643788fd.914x914x1.jpg\\\",\\\"id\\\":4,\\\"headerImageUrl\\\":\\\"https://images.genius.com/3b3485a28f2eb47c5ff3d7691e71bcba.1000x333x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/4\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":4806,\\\"url\\\":\\\"https://genius.com/artists/Lil-wayne\\\",\\\"slug\\\":\\\"Lil-wayne\\\",\\\"name\\\":\\\"Lil Wayne\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"l\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/aa8b9dce2492fe413c23f77b643788fd.914x914x1.jpg\\\",\\\"id\\\":4,\\\"headerImageUrl\\\":\\\"https://images.genius.com/3b3485a28f2eb47c5ff3d7691e71bcba.1000x333x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/4\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Mack-maine\\\",\\\"slug\\\":\\\"Mack-maine\\\",\\\"name\\\":\\\"Mack Maine\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"m\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/4d5be3b260ef6329f717748048465daa.276x276x1.jpg\\\",\\\"id\\\":160,\\\"headerImageUrl\\\":\\\"https://images.genius.com/2570ddd697cb4552f034c623fdc28008.1000x669x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/160\\\",\\\"type\\\":\\\"artist\\\"},{\\\"iq\\\":333,\\\"url\\\":\\\"https://genius.com/artists/Ashanti\\\",\\\"slug\\\":\\\"Ashanti\\\",\\\"name\\\":\\\"Ashanti\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"a\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/e9a4bf905f4391465727421d0094dc09.400x400x1.jpg\\\",\\\"id\\\":2675,\\\"headerImageUrl\\\":\\\"https://images.genius.com/e29e395ddc0e2a3d0154f9e9aca3c4ee.1000x333x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2675\\\",\\\"type\\\":\\\"artist\\\"}],\\\"url\\\":\\\"https://genius.com/Lil-wayne-start-this-shit-off-right-lyrics\\\",\\\"updatedByHumanAt\\\":1709472526,\\\"titleWithFeatured\\\":\\\"Start This Shit Off Right (Ft. Ashanti & Mack Maine)\\\",\\\"title\\\":\\\"Start This Shit Off Right\\\",\\\"stats\\\":{\\\"pageviews\\\":87575,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":5},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/0607dab9ef83679f9e3939ffb215a1a0.1000x1000x1.png\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/0607dab9ef83679f9e3939ffb215a1a0.300x300x1.png\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Sep. 28, 2018\\\",\\\"releaseDateForDisplay\\\":\\\"September 28, 2018\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":28,\\\"month\\\":9,\\\"year\\\":2018},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Lil-wayne-start-this-shit-off-right-sample\\\",\\\"pyongsCount\\\":4,\\\"primaryArtistNames\\\":\\\"Lil Wayne\\\",\\\"path\\\":\\\"/Lil-wayne-start-this-shit-off-right-lyrics\\\",\\\"lyricsUpdatedAt\\\":1631035143,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":3831017,\\\"instrumental\\\":false,\\\"id\\\":3985352,\\\"headerImageUrl\\\":\\\"https://images.genius.com/0607dab9ef83679f9e3939ffb215a1a0.1000x1000x1.png\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/0607dab9ef83679f9e3939ffb215a1a0.300x300x1.png\\\",\\\"fullTitle\\\":\\\"Start This Shit Off Right by Lil Wayne (Ft. Ashanti & Mack Maine)\\\",\\\"artistNames\\\":\\\"Lil Wayne (Ft. Ashanti & Mack Maine)\\\",\\\"apiPath\\\":\\\"/songs/3985352\\\",\\\"annotationCount\\\":23,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":16360,\\\"url\\\":\\\"https://genius.com/artists/Royce-da-59\\\",\\\"slug\\\":\\\"Royce-da-59\\\",\\\"name\\\":\\\"Royce Da 5\\'9\\\\\\\"\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"r\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/892233b049a8da052593a9b4030be708.988x988x1.png\\\",\\\"id\\\":471,\\\"headerImageUrl\\\":\\\"https://images.genius.com/fcc76a7f58081f4c6dc1b7f837843f49.988x988x1.png\\\",\\\"apiPath\\\":\\\"/artists/471\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":16360,\\\"url\\\":\\\"https://genius.com/artists/Royce-da-59\\\",\\\"slug\\\":\\\"Royce-da-59\\\",\\\"name\\\":\\\"Royce Da 5\\'9\\\\\\\"\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"r\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/892233b049a8da052593a9b4030be708.988x988x1.png\\\",\\\"id\\\":471,\\\"headerImageUrl\\\":\\\"https://images.genius.com/fcc76a7f58081f4c6dc1b7f837843f49.988x988x1.png\\\",\\\"apiPath\\\":\\\"/artists/471\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Royce-da-59-airplanes-freestyle-lyrics\\\",\\\"updatedByHumanAt\\\":1653490908,\\\"titleWithFeatured\\\":\\\"Airplanes (Freestyle)\\\",\\\"title\\\":\\\"Airplanes (Freestyle)\\\",\\\"stats\\\":{\\\"pageviews\\\":13227,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":3},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/f26afa0e15f73eb0b5cef561ec304c68.450x450x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/f26afa0e15f73eb0b5cef561ec304c68.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"May. 30, 2010\\\",\\\"releaseDateForDisplay\\\":\\\"May 30, 2010\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":30,\\\"month\\\":5,\\\"year\\\":2010},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Royce-da-59-airplanes-freestyle-sample\\\",\\\"pyongsCount\\\":4,\\\"primaryArtistNames\\\":\\\"Royce Da 5\\'9\\\\\\\"\\\",\\\"path\\\":\\\"/Royce-da-59-airplanes-freestyle-lyrics\\\",\\\"lyricsUpdatedAt\\\":1653490908,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":50,\\\"instrumental\\\":false,\\\"id\\\":40626,\\\"headerImageUrl\\\":\\\"https://images.genius.com/f26afa0e15f73eb0b5cef561ec304c68.450x450x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/f26afa0e15f73eb0b5cef561ec304c68.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"Airplanes (Freestyle) by Royce Da 5\\'9\\\\\\\"\\\",\\\"artistNames\\\":\\\"Royce Da 5\\'9\\\\\\\"\\\",\\\"apiPath\\\":\\\"/songs/40626\\\",\\\"annotationCount\\\":22,\\\"type\\\":\\\"song\\\"}],\\\"url\\\":\\\"https://genius.com/2pac-all-eyez-on-me-sample/interpolations\\\",\\\"type\\\":\\\"song_relationship\\\",\\\"relationshipType\\\":\\\"interpolated_by\\\"},{\\\"songs\\\":[],\\\"url\\\":null,\\\"type\\\":\\\"song_relationship\\\",\\\"relationshipType\\\":\\\"cover_of\\\"},{\\\"songs\\\":[{\\\"primaryArtists\\\":[{\\\"iq\\\":343,\\\"url\\\":\\\"https://genius.com/artists/Tsu-surf\\\",\\\"slug\\\":\\\"Tsu-surf\\\",\\\"name\\\":\\\"Tsu Surf\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"t\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/4d06cd6fc5a811b61a0e24bdba5e2527.1000x1000x1.jpg\\\",\\\"id\\\":29362,\\\"headerImageUrl\\\":\\\"https://images.genius.com/48ce7ea838343e4629c5b25df156416b.1000x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/29362\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":343,\\\"url\\\":\\\"https://genius.com/artists/Tsu-surf\\\",\\\"slug\\\":\\\"Tsu-surf\\\",\\\"name\\\":\\\"Tsu Surf\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"t\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/4d06cd6fc5a811b61a0e24bdba5e2527.1000x1000x1.jpg\\\",\\\"id\\\":29362,\\\"headerImageUrl\\\":\\\"https://images.genius.com/48ce7ea838343e4629c5b25df156416b.1000x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/29362\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Tsu-surf-tsu-surf-funk-flex-freestyle021-lyrics\\\",\\\"updatedByHumanAt\\\":1677205065,\\\"titleWithFeatured\\\":\\\"Tsu Surf | Funk Flex | #Freestyle021\\\",\\\"title\\\":\\\"Tsu Surf | Funk Flex | #Freestyle021\\\",\\\"stats\\\":{\\\"pageviews\\\":11017,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/8e54bf72dcf67fc93632171767ec36b2.1000x563x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/8e54bf72dcf67fc93632171767ec36b2.300x169x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Oct. 21, 2016\\\",\\\"releaseDateForDisplay\\\":\\\"October 21, 2016\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":21,\\\"month\\\":10,\\\"year\\\":2016},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Tsu-surf-tsu-surf-funk-flex-freestyle021-sample\\\",\\\"pyongsCount\\\":1,\\\"primaryArtistNames\\\":\\\"Tsu Surf\\\",\\\"path\\\":\\\"/Tsu-surf-tsu-surf-funk-flex-freestyle021-lyrics\\\",\\\"lyricsUpdatedAt\\\":1630582564,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":275726,\\\"instrumental\\\":false,\\\"id\\\":2899630,\\\"headerImageUrl\\\":\\\"https://images.genius.com/8e54bf72dcf67fc93632171767ec36b2.1000x563x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/8e54bf72dcf67fc93632171767ec36b2.300x169x1.jpg\\\",\\\"fullTitle\\\":\\\"Tsu Surf | Funk Flex | #Freestyle021 by Tsu Surf\\\",\\\"artistNames\\\":\\\"Tsu Surf\\\",\\\"apiPath\\\":\\\"/songs/2899630\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":711,\\\"url\\\":\\\"https://genius.com/artists/Hichkas\\\",\\\"slug\\\":\\\"Hichkas\\\",\\\"name\\\":\\\"Hichkas\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"h\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/4a8776786383eb39bfb2025dd5a464d4.640x640x1.jpg\\\",\\\"id\\\":20849,\\\"headerImageUrl\\\":\\\"https://images.genius.com/61cdbe73f8394f9e922bf969c4053515.500x500x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/20849\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":711,\\\"url\\\":\\\"https://genius.com/artists/Hichkas\\\",\\\"slug\\\":\\\"Hichkas\\\",\\\"name\\\":\\\"Hichkas\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"h\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/4a8776786383eb39bfb2025dd5a464d4.640x640x1.jpg\\\",\\\"id\\\":20849,\\\"headerImageUrl\\\":\\\"https://images.genius.com/61cdbe73f8394f9e922bf969c4053515.500x500x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/20849\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[{\\\"iq\\\":1799,\\\"url\\\":\\\"https://genius.com/artists/Salome-mc\\\",\\\"slug\\\":\\\"Salome-mc\\\",\\\"name\\\":\\\"Salome Mc\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"s\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/d290c7ed0a44b07b3b7afbaf6891cb6d.960x960x1.jpg\\\",\\\"id\\\":26655,\\\"headerImageUrl\\\":\\\"https://images.genius.com/3a17c9e4c3db542d5629c4da3e0ee12e.1000x459x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/26655\\\",\\\"type\\\":\\\"artist\\\"}],\\\"url\\\":\\\"https://genius.com/Hichkas-cheshma-roo-man-lyrics\\\",\\\"updatedByHumanAt\\\":1652636260,\\\"titleWithFeatured\\\":\\\"Cheshma Roo Man (Ft. Salome Mc)\\\",\\\"title\\\":\\\"Cheshma Roo Man\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/61aafb4d94976fa80121ae22c72da24a.600x600x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/61aafb4d94976fa80121ae22c72da24a.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"2004\\\",\\\"releaseDateForDisplay\\\":\\\"2004\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":null,\\\"month\\\":null,\\\"year\\\":2004},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Hichkas-cheshma-roo-man-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Hichkas\\\",\\\"path\\\":\\\"/Hichkas-cheshma-roo-man-lyrics\\\",\\\"lyricsUpdatedAt\\\":1652636260,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":12793925,\\\"instrumental\\\":false,\\\"id\\\":6110057,\\\"headerImageUrl\\\":\\\"https://images.genius.com/61aafb4d94976fa80121ae22c72da24a.600x600x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/61aafb4d94976fa80121ae22c72da24a.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"Cheshma Roo Man by Hichkas (Ft. Salome Mc)\\\",\\\"artistNames\\\":\\\"Hichkas (Ft. Salome Mc)\\\",\\\"apiPath\\\":\\\"/songs/6110057\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"}],\\\"url\\\":null,\\\"type\\\":\\\"song_relationship\\\",\\\"relationshipType\\\":\\\"covered_by\\\"},{\\\"songs\\\":[],\\\"url\\\":null,\\\"type\\\":\\\"song_relationship\\\",\\\"relationshipType\\\":\\\"remix_of\\\"},{\\\"songs\\\":[{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Imanbek\\\",\\\"slug\\\":\\\"Imanbek\\\",\\\"name\\\":\\\"Imanbek\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"i\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/ddc7541d1af2470a078026b586597c8a.578x578x1.png\\\",\\\"id\\\":1954794,\\\"headerImageUrl\\\":\\\"https://images.genius.com/33c81016fa31a51e7c87ebe9b6c32ff4.700x578x1.png\\\",\\\"apiPath\\\":\\\"/artists/1954794\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Imanbek\\\",\\\"slug\\\":\\\"Imanbek\\\",\\\"name\\\":\\\"Imanbek\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"i\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/ddc7541d1af2470a078026b586597c8a.578x578x1.png\\\",\\\"id\\\":1954794,\\\"headerImageUrl\\\":\\\"https://images.genius.com/33c81016fa31a51e7c87ebe9b6c32ff4.700x578x1.png\\\",\\\"apiPath\\\":\\\"/artists/1954794\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Imanbek-all-eyez-on-me-lyrics\\\",\\\"updatedByHumanAt\\\":1706362374,\\\"titleWithFeatured\\\":\\\"All Eyez On Me\\\",\\\"title\\\":\\\"All Eyez On Me\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/82a2ce2b0e80b23e5d51bb62ce6055e7.1000x1000x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/82a2ce2b0e80b23e5d51bb62ce6055e7.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Oct. 20, 2023\\\",\\\"releaseDateForDisplay\\\":\\\"October 20, 2023\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":20,\\\"month\\\":10,\\\"year\\\":2023},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Imanbek-all-eyez-on-me-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Imanbek\\\",\\\"path\\\":\\\"/Imanbek-all-eyez-on-me-lyrics\\\",\\\"lyricsUpdatedAt\\\":1706351177,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":14580745,\\\"instrumental\\\":false,\\\"id\\\":9673430,\\\"headerImageUrl\\\":\\\"https://images.genius.com/82a2ce2b0e80b23e5d51bb62ce6055e7.1000x1000x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/82a2ce2b0e80b23e5d51bb62ce6055e7.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"All Eyez On Me by Imanbek\\\",\\\"artistNames\\\":\\\"Imanbek\\\",\\\"apiPath\\\":\\\"/songs/9673430\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Dark-boy\\\",\\\"slug\\\":\\\"Dark-boy\\\",\\\"name\\\":\\\"Dark Boy\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"d\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/8f28cc35d502770061a084de6416bf6b.1000x1000x1.jpg\\\",\\\"id\\\":1037283,\\\"headerImageUrl\\\":\\\"https://images.genius.com/8f28cc35d502770061a084de6416bf6b.1000x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/1037283\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Dark-boy\\\",\\\"slug\\\":\\\"Dark-boy\\\",\\\"name\\\":\\\"Dark Boy\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"d\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/8f28cc35d502770061a084de6416bf6b.1000x1000x1.jpg\\\",\\\"id\\\":1037283,\\\"headerImageUrl\\\":\\\"https://images.genius.com/8f28cc35d502770061a084de6416bf6b.1000x1000x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/1037283\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Dark-boy-other-position-lyrics\\\",\\\"updatedByHumanAt\\\":1716664140,\\\"titleWithFeatured\\\":\\\"Other Position\\\",\\\"title\\\":\\\"Other Position\\\",\\\"stats\\\":{\\\"pageviews\\\":9328,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/94a49be5bb3a2742e483522c61aa66aa.500x500x1.png\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/94a49be5bb3a2742e483522c61aa66aa.300x300x1.png\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"May. 11, 2023\\\",\\\"releaseDateForDisplay\\\":\\\"May 11, 2023\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":11,\\\"month\\\":5,\\\"year\\\":2023},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Dark-boy-other-position-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Dark Boy\\\",\\\"path\\\":\\\"/Dark-boy-other-position-lyrics\\\",\\\"lyricsUpdatedAt\\\":1716664140,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":4942891,\\\"instrumental\\\":false,\\\"id\\\":9406903,\\\"headerImageUrl\\\":\\\"https://images.genius.com/94a49be5bb3a2742e483522c61aa66aa.500x500x1.png\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/94a49be5bb3a2742e483522c61aa66aa.300x300x1.png\\\",\\\"fullTitle\\\":\\\"Other Position by Dark Boy\\\",\\\"artistNames\\\":\\\"Dark Boy\\\",\\\"apiPath\\\":\\\"/songs/9406903\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Dj-belite\\\",\\\"slug\\\":\\\"Dj-belite\\\",\\\"name\\\":\\\"Dj Belite\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"d\\\",\\\"imageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"id\\\":3510997,\\\"headerImageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"apiPath\\\":\\\"/artists/3510997\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Dj-belite\\\",\\\"slug\\\":\\\"Dj-belite\\\",\\\"name\\\":\\\"Dj Belite\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"d\\\",\\\"imageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"id\\\":3510997,\\\"headerImageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"apiPath\\\":\\\"/artists/3510997\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Dj-belite-all-eyes-on-me-lyrics\\\",\\\"updatedByHumanAt\\\":1710443540,\\\"titleWithFeatured\\\":\\\"All Eyes on Me\\\",\\\"title\\\":\\\"All Eyes on Me\\\",\\\"stats\\\":{\\\"pageviews\\\":11727,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/b3c92bc9b35c8e8ebdd6b29e6349b533.1000x1000x1.png\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/b3c92bc9b35c8e8ebdd6b29e6349b533.300x300x1.png\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Dec. 20, 2022\\\",\\\"releaseDateForDisplay\\\":\\\"December 20, 2022\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":20,\\\"month\\\":12,\\\"year\\\":2022},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Dj-belite-all-eyes-on-me-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Dj Belite\\\",\\\"path\\\":\\\"/Dj-belite-all-eyes-on-me-lyrics\\\",\\\"lyricsUpdatedAt\\\":1710450953,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":8521495,\\\"instrumental\\\":false,\\\"id\\\":9039776,\\\"headerImageUrl\\\":\\\"https://images.genius.com/b3c92bc9b35c8e8ebdd6b29e6349b533.1000x1000x1.png\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/b3c92bc9b35c8e8ebdd6b29e6349b533.300x300x1.png\\\",\\\"fullTitle\\\":\\\"All Eyes on Me by Dj Belite\\\",\\\"artistNames\\\":\\\"Dj Belite\\\",\\\"apiPath\\\":\\\"/songs/9039776\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":327,\\\"url\\\":\\\"https://genius.com/artists/Mister-you\\\",\\\"slug\\\":\\\"Mister-you\\\",\\\"name\\\":\\\"Mister You\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"m\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/04a40db284f23a309135e3e1f5dace45.407x407x1.jpg\\\",\\\"id\\\":1375,\\\"headerImageUrl\\\":\\\"https://images.genius.com/04a40db284f23a309135e3e1f5dace45.407x407x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/1375\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":327,\\\"url\\\":\\\"https://genius.com/artists/Mister-you\\\",\\\"slug\\\":\\\"Mister-you\\\",\\\"name\\\":\\\"Mister You\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"m\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/04a40db284f23a309135e3e1f5dace45.407x407x1.jpg\\\",\\\"id\\\":1375,\\\"headerImageUrl\\\":\\\"https://images.genius.com/04a40db284f23a309135e3e1f5dace45.407x407x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/1375\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Mister-you-freestyle-you-2021-lyrics\\\",\\\"updatedByHumanAt\\\":1611699693,\\\"titleWithFeatured\\\":\\\"Freestyle You 2021\\\",\\\"title\\\":\\\"Freestyle You 2021\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/a99ae22df3320024b44c8bceb9cba051.1000x563x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/a99ae22df3320024b44c8bceb9cba051.300x169x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Jan. 26, 2021\\\",\\\"releaseDateForDisplay\\\":\\\"January 26, 2021\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":26,\\\"month\\\":1,\\\"year\\\":2021},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Mister-you-freestyle-you-2021-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Mister You\\\",\\\"path\\\":\\\"/Mister-you-freestyle-you-2021-lyrics\\\",\\\"lyricsUpdatedAt\\\":1611699540,\\\"lyricsState\\\":\\\"unreleased\\\",\\\"lyricsOwnerId\\\":4838104,\\\"instrumental\\\":false,\\\"id\\\":6424229,\\\"headerImageUrl\\\":\\\"https://images.genius.com/a99ae22df3320024b44c8bceb9cba051.1000x563x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/a99ae22df3320024b44c8bceb9cba051.300x169x1.jpg\\\",\\\"fullTitle\\\":\\\"Freestyle You 2021 by Mister You\\\",\\\"artistNames\\\":\\\"Mister You\\\",\\\"apiPath\\\":\\\"/songs/6424229\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":2463,\\\"url\\\":\\\"https://genius.com/artists/Kage-dq\\\",\\\"slug\\\":\\\"Kage-dq\\\",\\\"name\\\":\\\"Kage (dq)\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"k\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/bab7d26e1aacdf52b905966cf8bdded4.1000x1000x1.jpg\\\",\\\"id\\\":2322384,\\\"headerImageUrl\\\":\\\"https://images.genius.com/cfcf9dd63d41e753e53fd0cf10f8bbd9.1000x667x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2322384\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":2463,\\\"url\\\":\\\"https://genius.com/artists/Kage-dq\\\",\\\"slug\\\":\\\"Kage-dq\\\",\\\"name\\\":\\\"Kage (dq)\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"k\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/bab7d26e1aacdf52b905966cf8bdded4.1000x1000x1.jpg\\\",\\\"id\\\":2322384,\\\"headerImageUrl\\\":\\\"https://images.genius.com/cfcf9dd63d41e753e53fd0cf10f8bbd9.1000x667x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2322384\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Kage-dq-na-mnie-lyrics\\\",\\\"updatedByHumanAt\\\":1609277604,\\\"titleWithFeatured\\\":\\\"Na mnie\\\",\\\"title\\\":\\\"Na mnie\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/42e4f4cfcbbeda42122f7c7f915d1e74.1000x1000x1.png\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/42e4f4cfcbbeda42122f7c7f915d1e74.300x300x1.png\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Jun. 18, 2020\\\",\\\"releaseDateForDisplay\\\":\\\"June 18, 2020\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":18,\\\"month\\\":6,\\\"year\\\":2020},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Kage-dq-na-mnie-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Kage (dq)\\\",\\\"path\\\":\\\"/Kage-dq-na-mnie-lyrics\\\",\\\"lyricsUpdatedAt\\\":1609277673,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":10309818,\\\"instrumental\\\":false,\\\"id\\\":5770704,\\\"headerImageUrl\\\":\\\"https://images.genius.com/42e4f4cfcbbeda42122f7c7f915d1e74.1000x1000x1.png\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/42e4f4cfcbbeda42122f7c7f915d1e74.300x300x1.png\\\",\\\"fullTitle\\\":\\\"Na mnie by Kage (dq)\\\",\\\"artistNames\\\":\\\"Kage (dq)\\\",\\\"apiPath\\\":\\\"/songs/5770704\\\",\\\"annotationCount\\\":3,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Spider-loc\\\",\\\"slug\\\":\\\"Spider-loc\\\",\\\"name\\\":\\\"Spider Loc\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"s\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/20e84deab324c241a3badf8f615d5719.500x383x1.jpg\\\",\\\"id\\\":2843,\\\"headerImageUrl\\\":\\\"https://images.genius.com/20e84deab324c241a3badf8f615d5719.500x383x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2843\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Spider-loc\\\",\\\"slug\\\":\\\"Spider-loc\\\",\\\"name\\\":\\\"Spider Loc\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"s\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/20e84deab324c241a3badf8f615d5719.500x383x1.jpg\\\",\\\"id\\\":2843,\\\"headerImageUrl\\\":\\\"https://images.genius.com/20e84deab324c241a3badf8f615d5719.500x383x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2843\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Big-syke\\\",\\\"slug\\\":\\\"Big-syke\\\",\\\"name\\\":\\\"Big Syke\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"b\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/d45c72b156862a65719161af952e83a1.300x300x1.png\\\",\\\"id\\\":2571,\\\"headerImageUrl\\\":\\\"https://images.genius.com/c48b237b70dd1767f94bd3b13c0f4178.400x296x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2571\\\",\\\"type\\\":\\\"artist\\\"}],\\\"url\\\":\\\"https://genius.com/Spider-loc-all-eyez-on-us-lyrics\\\",\\\"updatedByHumanAt\\\":1713502406,\\\"titleWithFeatured\\\":\\\"All Eyez On Us (Ft. Big Syke)\\\",\\\"title\\\":\\\"All Eyez On Us\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":1},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/dc9fb2e2053715fc81e6e2249291646d.774x774x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/dc9fb2e2053715fc81e6e2249291646d.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Jan. 2006\\\",\\\"releaseDateForDisplay\\\":\\\"January 2006\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":null,\\\"month\\\":1,\\\"year\\\":2006},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Spider-loc-all-eyez-on-us-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Spider Loc\\\",\\\"path\\\":\\\"/Spider-loc-all-eyez-on-us-lyrics\\\",\\\"lyricsUpdatedAt\\\":1713502315,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":18727782,\\\"instrumental\\\":false,\\\"id\\\":6065615,\\\"headerImageUrl\\\":\\\"https://images.genius.com/dc9fb2e2053715fc81e6e2249291646d.774x774x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/dc9fb2e2053715fc81e6e2249291646d.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"All Eyez On Us by Spider Loc (Ft. Big Syke)\\\",\\\"artistNames\\\":\\\"Spider Loc (Ft. Big Syke)\\\",\\\"apiPath\\\":\\\"/songs/6065615\\\",\\\"annotationCount\\\":1,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":465,\\\"url\\\":\\\"https://genius.com/artists/Hugo-toxxx\\\",\\\"slug\\\":\\\"Hugo-toxxx\\\",\\\"name\\\":\\\"Hugo Toxxx\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"h\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/2915dd844aded0433de935c16705658e.1000x1000x1.jpg\\\",\\\"id\\\":14711,\\\"headerImageUrl\\\":\\\"https://images.genius.com/e8ce5da9decbb0f69e9591ee159bb0e6.1000x667x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/14711\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":465,\\\"url\\\":\\\"https://genius.com/artists/Hugo-toxxx\\\",\\\"slug\\\":\\\"Hugo-toxxx\\\",\\\"name\\\":\\\"Hugo Toxxx\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"h\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/2915dd844aded0433de935c16705658e.1000x1000x1.jpg\\\",\\\"id\\\":14711,\\\"headerImageUrl\\\":\\\"https://images.genius.com/e8ce5da9decbb0f69e9591ee159bb0e6.1000x667x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/14711\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Hugo-toxxx-vsechny-oci-na-mne-lyrics\\\",\\\"updatedByHumanAt\\\":1558225224,\\\"titleWithFeatured\\\":\\\"Všechny oči na mně\\\",\\\"title\\\":\\\"Všechny oči na mně\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":3},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/41cf06401917be3a3ee9c51dfc256974.816x816x1.png\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/41cf06401917be3a3ee9c51dfc256974.300x300x1.png\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Jul. 31, 2017\\\",\\\"releaseDateForDisplay\\\":\\\"July 31, 2017\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":31,\\\"month\\\":7,\\\"year\\\":2017},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Hugo-toxxx-vsechny-oci-na-mne-sample\\\",\\\"pyongsCount\\\":2,\\\"primaryArtistNames\\\":\\\"Hugo Toxxx\\\",\\\"path\\\":\\\"/Hugo-toxxx-vsechny-oci-na-mne-lyrics\\\",\\\"lyricsUpdatedAt\\\":1558224832,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":3720308,\\\"instrumental\\\":false,\\\"id\\\":3180925,\\\"headerImageUrl\\\":\\\"https://images.genius.com/41cf06401917be3a3ee9c51dfc256974.816x816x1.png\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/41cf06401917be3a3ee9c51dfc256974.300x300x1.png\\\",\\\"fullTitle\\\":\\\"Všechny oči na mně by Hugo Toxxx\\\",\\\"artistNames\\\":\\\"Hugo Toxxx\\\",\\\"apiPath\\\":\\\"/songs/3180925\\\",\\\"annotationCount\\\":3,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":207,\\\"url\\\":\\\"https://genius.com/artists/Muslim\\\",\\\"slug\\\":\\\"Muslim\\\",\\\"name\\\":\\\"Muslim - مسلم\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"m\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/3629be244b18ee5725a610779f26d70e.1000x1000x1.png\\\",\\\"id\\\":20732,\\\"headerImageUrl\\\":\\\"https://images.genius.com/5e1b4b1f004dd599af39666ea7a521bf.1000x367x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/20732\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":207,\\\"url\\\":\\\"https://genius.com/artists/Muslim\\\",\\\"slug\\\":\\\"Muslim\\\",\\\"name\\\":\\\"Muslim - مسلم\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"m\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/3629be244b18ee5725a610779f26d70e.1000x1000x1.png\\\",\\\"id\\\":20732,\\\"headerImageUrl\\\":\\\"https://images.genius.com/5e1b4b1f004dd599af39666ea7a521bf.1000x367x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/20732\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Muslim-all-eyes-on-me-lyrics\\\",\\\"updatedByHumanAt\\\":1636032594,\\\"titleWithFeatured\\\":\\\"All Eyes On Me\\\",\\\"title\\\":\\\"All Eyes On Me\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":1},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/7o1ijuwznks052a89t95b3wuf.340x340x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/7o1ijuwznks052a89t95b3wuf.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Jan. 1, 2005\\\",\\\"releaseDateForDisplay\\\":\\\"January 1, 2005\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":1,\\\"month\\\":1,\\\"year\\\":2005},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Muslim-all-eyes-on-me-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Muslim - مسلم\\\",\\\"path\\\":\\\"/Muslim-all-eyes-on-me-lyrics\\\",\\\"lyricsUpdatedAt\\\":1636032594,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":3758128,\\\"instrumental\\\":false,\\\"id\\\":3085597,\\\"headerImageUrl\\\":\\\"https://images.genius.com/7o1ijuwznks052a89t95b3wuf.340x340x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/7o1ijuwznks052a89t95b3wuf.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"All Eyes On Me by Muslim - مسلم\\\",\\\"artistNames\\\":\\\"Muslim - مسلم\\\",\\\"apiPath\\\":\\\"/songs/3085597\\\",\\\"annotationCount\\\":1,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/2pac\\\",\\\"slug\\\":\\\"2pac\\\",\\\"name\\\":\\\"2Pac\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"0\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/9118a64d842f1611d83f5090989a3475.788x788x1.jpg\\\",\\\"id\\\":59,\\\"headerImageUrl\\\":\\\"https://images.genius.com/66dab5428172d59b83ed49304aacfa05.932x718x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/59\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/2pac\\\",\\\"slug\\\":\\\"2pac\\\",\\\"name\\\":\\\"2Pac\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"0\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/9118a64d842f1611d83f5090989a3475.788x788x1.jpg\\\",\\\"id\\\":59,\\\"headerImageUrl\\\":\\\"https://images.genius.com/66dab5428172d59b83ed49304aacfa05.932x718x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/59\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Big-syke\\\",\\\"slug\\\":\\\"Big-syke\\\",\\\"name\\\":\\\"Big Syke\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"b\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/d45c72b156862a65719161af952e83a1.300x300x1.png\\\",\\\"id\\\":2571,\\\"headerImageUrl\\\":\\\"https://images.genius.com/c48b237b70dd1767f94bd3b13c0f4178.400x296x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2571\\\",\\\"type\\\":\\\"artist\\\"}],\\\"url\\\":\\\"https://genius.com/2pac-all-eyez-on-me-nu-mixx-lyrics\\\",\\\"updatedByHumanAt\\\":1668150122,\\\"titleWithFeatured\\\":\\\"All Eyez On Me (Nu-Mixx) (Ft. Big Syke)\\\",\\\"title\\\":\\\"All Eyez On Me (Nu-Mixx)\\\",\\\"stats\\\":{\\\"pageviews\\\":7273,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":1},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/9c7e095edc751d01f9efc1a0fc09f646.297x297x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/9c7e095edc751d01f9efc1a0fc09f646.297x297x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Oct. 7, 2003\\\",\\\"releaseDateForDisplay\\\":\\\"October 7, 2003\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":7,\\\"month\\\":10,\\\"year\\\":2003},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/2pac-all-eyez-on-me-nu-mixx-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"2Pac\\\",\\\"path\\\":\\\"/2pac-all-eyez-on-me-nu-mixx-lyrics\\\",\\\"lyricsUpdatedAt\\\":1668150122,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":3492882,\\\"instrumental\\\":false,\\\"id\\\":4032504,\\\"headerImageUrl\\\":\\\"https://images.genius.com/9c7e095edc751d01f9efc1a0fc09f646.297x297x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/9c7e095edc751d01f9efc1a0fc09f646.297x297x1.jpg\\\",\\\"fullTitle\\\":\\\"All Eyez On Me (Nu-Mixx) by 2Pac (Ft. Big Syke)\\\",\\\"artistNames\\\":\\\"2Pac (Ft. Big Syke)\\\",\\\"apiPath\\\":\\\"/songs/4032504\\\",\\\"annotationCount\\\":1,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":3395,\\\"url\\\":\\\"https://genius.com/artists/Dax\\\",\\\"slug\\\":\\\"Dax\\\",\\\"name\\\":\\\"Dax\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"d\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/ebaef6dee442778e7fe3a61e15a62464.555x555x1.jpg\\\",\\\"id\\\":16711,\\\"headerImageUrl\\\":\\\"https://images.genius.com/44e95e196b60371bc405f61d2ef225ee.620x350x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/16711\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":3395,\\\"url\\\":\\\"https://genius.com/artists/Dax\\\",\\\"slug\\\":\\\"Dax\\\",\\\"name\\\":\\\"Dax\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"d\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/ebaef6dee442778e7fe3a61e15a62464.555x555x1.jpg\\\",\\\"id\\\":16711,\\\"headerImageUrl\\\":\\\"https://images.genius.com/44e95e196b60371bc405f61d2ef225ee.620x350x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/16711\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Dax-all-eyez-on-me-remix-lyrics\\\",\\\"updatedByHumanAt\\\":1699235760,\\\"titleWithFeatured\\\":\\\"All Eyez On Me (Remix)\\\",\\\"title\\\":\\\"All Eyez On Me (Remix)\\\",\\\"stats\\\":{\\\"pageviews\\\":7788,\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":4},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/e62aafc0e5b6ff320b547396493a540d.500x500x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/e62aafc0e5b6ff320b547396493a540d.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Oct. 3, 2017\\\",\\\"releaseDateForDisplay\\\":\\\"October 3, 2017\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":3,\\\"month\\\":10,\\\"year\\\":2017},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Dax-all-eyez-on-me-remix-sample\\\",\\\"pyongsCount\\\":2,\\\"primaryArtistNames\\\":\\\"Dax\\\",\\\"path\\\":\\\"/Dax-all-eyez-on-me-remix-lyrics\\\",\\\"lyricsUpdatedAt\\\":1699235760,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":5177186,\\\"instrumental\\\":false,\\\"id\\\":3467332,\\\"headerImageUrl\\\":\\\"https://images.genius.com/e62aafc0e5b6ff320b547396493a540d.500x500x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/e62aafc0e5b6ff320b547396493a540d.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"All Eyez On Me (Remix) by Dax\\\",\\\"artistNames\\\":\\\"Dax\\\",\\\"apiPath\\\":\\\"/songs/3467332\\\",\\\"annotationCount\\\":4,\\\"type\\\":\\\"song\\\"},{\\\"primaryArtists\\\":[{\\\"iq\\\":540,\\\"url\\\":\\\"https://genius.com/artists/Still-fresh\\\",\\\"slug\\\":\\\"Still-fresh\\\",\\\"name\\\":\\\"Still Fresh\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"s\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/fc6908c7e66c01f7eeab2b7bfec921fb.563x563x1.jpg\\\",\\\"id\\\":16272,\\\"headerImageUrl\\\":\\\"https://images.genius.com/fe5c7968803cc83e6811cc09102bd3b3.596x317x1.png\\\",\\\"apiPath\\\":\\\"/artists/16272\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"iq\\\":540,\\\"url\\\":\\\"https://genius.com/artists/Still-fresh\\\",\\\"slug\\\":\\\"Still-fresh\\\",\\\"name\\\":\\\"Still Fresh\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"s\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/fc6908c7e66c01f7eeab2b7bfec921fb.563x563x1.jpg\\\",\\\"id\\\":16272,\\\"headerImageUrl\\\":\\\"https://images.genius.com/fe5c7968803cc83e6811cc09102bd3b3.596x317x1.png\\\",\\\"apiPath\\\":\\\"/artists/16272\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[{\\\"iq\\\":2066,\\\"url\\\":\\\"https://genius.com/artists/Sultan\\\",\\\"slug\\\":\\\"Sultan\\\",\\\"name\\\":\\\"Sultan\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"s\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/6a57cec06cb6f360894d870f28fdf153.427x427x1.jpg\\\",\\\"id\\\":6670,\\\"headerImageUrl\\\":\\\"https://images.genius.com/6a57cec06cb6f360894d870f28fdf153.427x427x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/6670\\\",\\\"type\\\":\\\"artist\\\"}],\\\"url\\\":\\\"https://genius.com/Still-fresh-cest-la-demer-lyrics\\\",\\\"updatedByHumanAt\\\":1666285570,\\\"titleWithFeatured\\\":\\\"C\\'est la demer (Ft. Sultan)\\\",\\\"title\\\":\\\"C’est la demer\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":3},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/7ae9f2c158c1efbf46deedbacd0ce939.600x600x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/7ae9f2c158c1efbf46deedbacd0ce939.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Sep. 18, 2010\\\",\\\"releaseDateForDisplay\\\":\\\"September 18, 2010\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":18,\\\"month\\\":9,\\\"year\\\":2010},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Still-fresh-cest-la-demer-sample\\\",\\\"pyongsCount\\\":null,\\\"primaryArtistNames\\\":\\\"Still Fresh\\\",\\\"path\\\":\\\"/Still-fresh-cest-la-demer-lyrics\\\",\\\"lyricsUpdatedAt\\\":1501249630,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":3290396,\\\"instrumental\\\":false,\\\"id\\\":2855737,\\\"headerImageUrl\\\":\\\"https://images.genius.com/7ae9f2c158c1efbf46deedbacd0ce939.600x600x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/7ae9f2c158c1efbf46deedbacd0ce939.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"C\\'est la demer by Still Fresh (Ft. Sultan)\\\",\\\"artistNames\\\":\\\"Still Fresh (Ft. Sultan)\\\",\\\"apiPath\\\":\\\"/songs/2855737\\\",\\\"annotationCount\\\":5,\\\"type\\\":\\\"song\\\"}],\\\"url\\\":\\\"https://genius.com/2pac-all-eyez-on-me-sample/remixes\\\",\\\"type\\\":\\\"song_relationship\\\",\\\"relationshipType\\\":\\\"remixed_by\\\"},{\\\"songs\\\":[],\\\"url\\\":null,\\\"type\\\":\\\"song_relationship\\\",\\\"relationshipType\\\":\\\"live_version_of\\\"},{\\\"songs\\\":[],\\\"url\\\":null,\\\"type\\\":\\\"song_relationship\\\",\\\"relationshipType\\\":\\\"performed_live_as\\\"},{\\\"songs\\\":[],\\\"url\\\":null,\\\"type\\\":\\\"song_relationship\\\",\\\"relationshipType\\\":\\\"translation_of\\\"},{\\\"songs\\\":[{\\\"primaryArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Genius-brasil-traducoes\\\",\\\"slug\\\":\\\"Genius-brasil-traducoes\\\",\\\"name\\\":\\\"Genius Brasil Traduções\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"g\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/3a0be3346223ddde1f4b5c1d315cb9db.750x750x1.jpg\\\",\\\"id\\\":210448,\\\"headerImageUrl\\\":\\\"https://images.genius.com/887be16f52b0dec817adc23304662df0.900x176x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/210448\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryArtist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Genius-brasil-traducoes\\\",\\\"slug\\\":\\\"Genius-brasil-traducoes\\\",\\\"name\\\":\\\"Genius Brasil Traduções\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"g\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/3a0be3346223ddde1f4b5c1d315cb9db.750x750x1.jpg\\\",\\\"id\\\":210448,\\\"headerImageUrl\\\":\\\"https://images.genius.com/887be16f52b0dec817adc23304662df0.900x176x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/210448\\\",\\\"type\\\":\\\"artist\\\"},\\\"featuredArtists\\\":[],\\\"url\\\":\\\"https://genius.com/Genius-brasil-traducoes-2pac-all-eyez-on-me-ft-big-syke-traducao-em-portugues-lyrics\\\",\\\"updatedByHumanAt\\\":1697055245,\\\"titleWithFeatured\\\":\\\"2Pac - All Eyez on Me ft. Big Syke (Tradução em Português)\\\",\\\"title\\\":\\\"2Pac - All Eyez on Me ft. Big Syke (Tradução em Português)\\\",\\\"stats\\\":{\\\"hot\\\":false,\\\"unreviewedAnnotations\\\":0},\\\"songArtImageUrl\\\":\\\"https://images.genius.com/f3682149fbd09a5b23eee09d548aa887.561x561x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/f3682149fbd09a5b23eee09d548aa887.300x300x1.jpg\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Feb. 13, 1996\\\",\\\"releaseDateForDisplay\\\":\\\"February 13, 1996\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":13,\\\"month\\\":2,\\\"year\\\":1996},\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/Genius-brasil-traducoes-2pac-all-eyez-on-me-ft-big-syke-traducao-em-portugues-sample\\\",\\\"pyongsCount\\\":1,\\\"primaryArtistNames\\\":\\\"Genius Brasil Traduções\\\",\\\"path\\\":\\\"/Genius-brasil-traducoes-2pac-all-eyez-on-me-ft-big-syke-traducao-em-portugues-lyrics\\\",\\\"lyricsUpdatedAt\\\":1622655466,\\\"lyricsState\\\":\\\"complete\\\",\\\"lyricsOwnerId\\\":9318193,\\\"instrumental\\\":false,\\\"id\\\":6314513,\\\"headerImageUrl\\\":\\\"https://images.genius.com/f3682149fbd09a5b23eee09d548aa887.561x561x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/f3682149fbd09a5b23eee09d548aa887.300x300x1.jpg\\\",\\\"fullTitle\\\":\\\"2Pac - All Eyez on Me ft. Big Syke (Tradução em Português) by Genius Brasil Traduções\\\",\\\"artistNames\\\":\\\"Genius Brasil Traduções\\\",\\\"apiPath\\\":\\\"/songs/6314513\\\",\\\"annotationCount\\\":0,\\\"type\\\":\\\"song\\\"}],\\\"url\\\":null,\\\"type\\\":\\\"song_relationship\\\",\\\"relationshipType\\\":\\\"translations\\\"}],\\\"producerArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Johnny-j\\\",\\\"slug\\\":\\\"Johnny-j\\\",\\\"name\\\":\\\"Johnny J\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"j\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/fb4f01398ed79ee7419123eec97a24c6.246x246x1.jpg\\\",\\\"id\\\":8976,\\\"headerImageUrl\\\":\\\"https://images.genius.com/e13e8ccdd6b42736a128f0339f9bac53.300x300x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/8976\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryTag\\\":{\\\"url\\\":\\\"https://genius.com/tags/rap\\\",\\\"primary\\\":true,\\\"name\\\":\\\"Rap\\\",\\\"id\\\":1434,\\\"type\\\":\\\"tag\\\"},\\\"primaryArtists\\\":[59],\\\"primaryArtist\\\":59,\\\"media\\\":[{\\\"url\\\":\\\"https://open.spotify.com/track/2xTft6GEZeTyWNpdX94rkf\\\",\\\"type\\\":\\\"audio\\\",\\\"provider\\\":\\\"spotify\\\",\\\"nativeUri\\\":\\\"spotify:track:2xTft6GEZeTyWNpdX94rkf\\\"},{\\\"url\\\":\\\"http://www.youtube.com/watch?v=H1HdZFgR-aA\\\",\\\"type\\\":\\\"video\\\",\\\"start\\\":0,\\\"provider\\\":\\\"youtube\\\"}],\\\"lyricsMarkedStaffApprovedBy\\\":null,\\\"lyricsMarkedCompleteBy\\\":null,\\\"featuredArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Big-syke\\\",\\\"slug\\\":\\\"Big-syke\\\",\\\"name\\\":\\\"Big Syke\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"b\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/d45c72b156862a65719161af952e83a1.300x300x1.png\\\",\\\"id\\\":2571,\\\"headerImageUrl\\\":\\\"https://images.genius.com/c48b237b70dd1767f94bd3b13c0f4178.400x296x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/2571\\\",\\\"type\\\":\\\"artist\\\"}],\\\"descriptionAnnotation\\\":3495386,\\\"customPerformances\\\":[{\\\"artists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Joshuas-dream\\\",\\\"slug\\\":\\\"Joshuas-dream\\\",\\\"name\\\":\\\"Joshua’s Dream\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"j\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/587db275ffb41be915a52068c6550080.425x425x1.jpg\\\",\\\"id\\\":1225046,\\\"headerImageUrl\\\":\\\"https://images.genius.com/57d71980974afa1ba54a79d6ede11923.564x426x1.webp\\\",\\\"apiPath\\\":\\\"/artists/1225046\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/Interscope-pearl-music\\\",\\\"slug\\\":\\\"Interscope-pearl-music\\\",\\\"name\\\":\\\"Interscope Pearl Music\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"i\\\",\\\"imageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"id\\\":2490871,\\\"headerImageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"apiPath\\\":\\\"/artists/2490871\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/Warner-tamerlane-publishing-corp\\\",\\\"slug\\\":\\\"Warner-tamerlane-publishing-corp\\\",\\\"name\\\":\\\"Warner-Tamerlane Publishing Corp.\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"w\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/8af7bba6ca0c481513ffe4b4ab87e347.314x314x1.jpg\\\",\\\"id\\\":992219,\\\"headerImageUrl\\\":\\\"https://images.genius.com/a43ed819378fccdad8a80cad050f2029.768x433x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/992219\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/Black-hispanic-music\\\",\\\"slug\\\":\\\"Black-hispanic-music\\\",\\\"name\\\":\\\"Black/Hispanic Music\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"b\\\",\\\"imageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"id\\\":2490894,\\\"headerImageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"apiPath\\\":\\\"/artists/2490894\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/Careers-bmg-music-publishing\\\",\\\"slug\\\":\\\"Careers-bmg-music-publishing\\\",\\\"name\\\":\\\"Careers-BMG Music Publishing\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"c\\\",\\\"imageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"id\\\":1013873,\\\"headerImageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"apiPath\\\":\\\"/artists/1013873\\\",\\\"type\\\":\\\"artist\\\"}],\\\"label\\\":\\\"Publisher\\\"},{\\\"artists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Death-row-records\\\",\\\"slug\\\":\\\"Death-row-records\\\",\\\"name\\\":\\\"Death Row Records\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"d\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/31137d732921a682957d11c75c9c4e40.500x500x1.jpg\\\",\\\"id\\\":149462,\\\"headerImageUrl\\\":\\\"https://images.genius.com/9e17772c3ab9fce6f4d9bd9603ed9d74.1000x247x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/149462\\\",\\\"type\\\":\\\"artist\\\"},{\\\"iq\\\":175,\\\"url\\\":\\\"https://genius.com/artists/Interscope-records\\\",\\\"slug\\\":\\\"Interscope-records\\\",\\\"name\\\":\\\"Interscope Records\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"i\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/f28d415a7f5f966f5814e7c07cf8dc23.271x271x1.jpg\\\",\\\"id\\\":143490,\\\"headerImageUrl\\\":\\\"https://images.genius.com/20ee0dc07918a750d532b2316361d3a2.1000x333x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/143490\\\",\\\"type\\\":\\\"artist\\\"}],\\\"label\\\":\\\"Copyright ©\\\"},{\\\"artists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Death-row-records\\\",\\\"slug\\\":\\\"Death-row-records\\\",\\\"name\\\":\\\"Death Row Records\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"d\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/31137d732921a682957d11c75c9c4e40.500x500x1.jpg\\\",\\\"id\\\":149462,\\\"headerImageUrl\\\":\\\"https://images.genius.com/9e17772c3ab9fce6f4d9bd9603ed9d74.1000x247x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/149462\\\",\\\"type\\\":\\\"artist\\\"},{\\\"iq\\\":175,\\\"url\\\":\\\"https://genius.com/artists/Interscope-records\\\",\\\"slug\\\":\\\"Interscope-records\\\",\\\"name\\\":\\\"Interscope Records\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"i\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/f28d415a7f5f966f5814e7c07cf8dc23.271x271x1.jpg\\\",\\\"id\\\":143490,\\\"headerImageUrl\\\":\\\"https://images.genius.com/20ee0dc07918a750d532b2316361d3a2.1000x333x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/143490\\\",\\\"type\\\":\\\"artist\\\"}],\\\"label\\\":\\\"Phonographic Copyright ℗\\\"},{\\\"artists\\\":[{\\\"iq\\\":175,\\\"url\\\":\\\"https://genius.com/artists/Interscope-records\\\",\\\"slug\\\":\\\"Interscope-records\\\",\\\"name\\\":\\\"Interscope Records\\\",\\\"isVerified\\\":true,\\\"isMemeVerified\\\":true,\\\"indexCharacter\\\":\\\"i\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/f28d415a7f5f966f5814e7c07cf8dc23.271x271x1.jpg\\\",\\\"id\\\":143490,\\\"headerImageUrl\\\":\\\"https://images.genius.com/20ee0dc07918a750d532b2316361d3a2.1000x333x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/143490\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/Death-row-records\\\",\\\"slug\\\":\\\"Death-row-records\\\",\\\"name\\\":\\\"Death Row Records\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"d\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/31137d732921a682957d11c75c9c4e40.500x500x1.jpg\\\",\\\"id\\\":149462,\\\"headerImageUrl\\\":\\\"https://images.genius.com/9e17772c3ab9fce6f4d9bd9603ed9d74.1000x247x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/149462\\\",\\\"type\\\":\\\"artist\\\"}],\\\"label\\\":\\\"Label\\\"},{\\\"artists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Dave-aron\\\",\\\"slug\\\":\\\"Dave-aron\\\",\\\"name\\\":\\\"Dave Aron\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"d\\\",\\\"imageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"id\\\":675249,\\\"headerImageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"apiPath\\\":\\\"/artists/675249\\\",\\\"type\\\":\\\"artist\\\"},{\\\"url\\\":\\\"https://genius.com/artists/Johnny-j\\\",\\\"slug\\\":\\\"Johnny-j\\\",\\\"name\\\":\\\"Johnny J\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"j\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/fb4f01398ed79ee7419123eec97a24c6.246x246x1.jpg\\\",\\\"id\\\":8976,\\\"headerImageUrl\\\":\\\"https://images.genius.com/e13e8ccdd6b42736a128f0339f9bac53.300x300x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/8976\\\",\\\"type\\\":\\\"artist\\\"}],\\\"label\\\":\\\"Mixing Engineer\\\"},{\\\"artists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Rick-clifford\\\",\\\"slug\\\":\\\"Rick-clifford\\\",\\\"name\\\":\\\"Rick Clifford\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"r\\\",\\\"imageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"id\\\":979967,\\\"headerImageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"apiPath\\\":\\\"/artists/979967\\\",\\\"type\\\":\\\"artist\\\"}],\\\"label\\\":\\\"Engineer\\\"},{\\\"artists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Alvin-mcgill\\\",\\\"slug\\\":\\\"Alvin-mcgill\\\",\\\"name\\\":\\\"Alvin McGill\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"a\\\",\\\"imageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"id\\\":979956,\\\"headerImageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1720540228\\\",\\\"apiPath\\\":\\\"/artists/979956\\\",\\\"type\\\":\\\"artist\\\"}],\\\"label\\\":\\\"Assistant Engineer\\\"},{\\\"artists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Brian-gardner\\\",\\\"slug\\\":\\\"Brian-gardner\\\",\\\"name\\\":\\\"Brian Gardner\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"b\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/83484709e5b3cacf10774eb0639fcc7d.508x508x1.jpg\\\",\\\"id\\\":637914,\\\"headerImageUrl\\\":\\\"https://images.genius.com/9ed2a5315fe0d92d7047de8f72fc66ff.508x800x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/637914\\\",\\\"type\\\":\\\"artist\\\"}],\\\"label\\\":\\\"Mastering Engineer\\\"}],\\\"albums\\\":[{\\\"tracklist\\\":[{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-ambitionz-az-a-ridah-lyrics\\\",\\\"title\\\":\\\"Ambitionz Az a Ridah\\\",\\\"path\\\":\\\"/2pac-ambitionz-az-a-ridah-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":780,\\\"apiPath\\\":\\\"/songs/780\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":1,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-all-about-u-lyrics\\\",\\\"title\\\":\\\"All About U\\\",\\\"path\\\":\\\"/2pac-all-about-u-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":5533,\\\"apiPath\\\":\\\"/songs/5533\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":2,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-skandalouz-lyrics\\\",\\\"title\\\":\\\"Skandalouz\\\",\\\"path\\\":\\\"/2pac-skandalouz-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6583,\\\"apiPath\\\":\\\"/songs/6583\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":3,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-got-my-mind-made-up-lyrics\\\",\\\"title\\\":\\\"Got My Mind Made Up\\\",\\\"path\\\":\\\"/2pac-got-my-mind-made-up-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":312,\\\"apiPath\\\":\\\"/songs/312\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":4,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-how-do-u-want-it-lyrics\\\",\\\"title\\\":\\\"How Do U Want It\\\",\\\"path\\\":\\\"/2pac-how-do-u-want-it-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":712,\\\"apiPath\\\":\\\"/songs/712\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":5,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-2-of-amerikaz-most-wanted-lyrics\\\",\\\"title\\\":\\\"2 of Amerikaz Most Wanted\\\",\\\"path\\\":\\\"/2pac-2-of-amerikaz-most-wanted-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":389,\\\"apiPath\\\":\\\"/songs/389\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":6,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-no-more-pain-lyrics\\\",\\\"title\\\":\\\"No More Pain\\\",\\\"path\\\":\\\"/2pac-no-more-pain-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":55423,\\\"apiPath\\\":\\\"/songs/55423\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":7,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-heartz-of-men-lyrics\\\",\\\"title\\\":\\\"Heartz of Men\\\",\\\"path\\\":\\\"/2pac-heartz-of-men-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":819,\\\"apiPath\\\":\\\"/songs/819\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":8,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-life-goes-on-lyrics\\\",\\\"title\\\":\\\"Life Goes On\\\",\\\"path\\\":\\\"/2pac-life-goes-on-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6573,\\\"apiPath\\\":\\\"/songs/6573\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":9,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-only-god-can-judge-me-lyrics\\\",\\\"title\\\":\\\"Only God Can Judge Me\\\",\\\"path\\\":\\\"/2pac-only-god-can-judge-me-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":2626,\\\"apiPath\\\":\\\"/songs/2626\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":10,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-tradin-war-stories-lyrics\\\",\\\"title\\\":\\\"Tradin’ War Stories\\\",\\\"path\\\":\\\"/2pac-tradin-war-stories-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":3428,\\\"apiPath\\\":\\\"/songs/3428\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":11,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-california-love-remix-lyrics\\\",\\\"title\\\":\\\"California Love (Remix)\\\",\\\"path\\\":\\\"/2pac-california-love-remix-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":3123951,\\\"apiPath\\\":\\\"/songs/3123951\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":12,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-i-aint-mad-at-cha-lyrics\\\",\\\"title\\\":\\\"I Ain’t Mad At Cha\\\",\\\"path\\\":\\\"/2pac-i-aint-mad-at-cha-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":5377,\\\"apiPath\\\":\\\"/songs/5377\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":13,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-whatz-ya-phone-lyrics\\\",\\\"title\\\":\\\"What’z Ya Phone #\\\",\\\"path\\\":\\\"/2pac-whatz-ya-phone-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":567,\\\"apiPath\\\":\\\"/songs/567\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":14,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-cant-c-me-lyrics\\\",\\\"title\\\":\\\"Can’t C Me\\\",\\\"path\\\":\\\"/2pac-cant-c-me-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":544,\\\"apiPath\\\":\\\"/songs/544\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":15,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-shorty-wanna-be-a-thug-lyrics\\\",\\\"title\\\":\\\"Shorty Wanna Be a Thug\\\",\\\"path\\\":\\\"/2pac-shorty-wanna-be-a-thug-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6581,\\\"apiPath\\\":\\\"/songs/6581\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":16,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-holla-at-me-lyrics\\\",\\\"title\\\":\\\"Holla At Me\\\",\\\"path\\\":\\\"/2pac-holla-at-me-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6575,\\\"apiPath\\\":\\\"/songs/6575\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":17,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-wonda-why-they-call-u-bitch-lyrics\\\",\\\"title\\\":\\\"Wonda Why They Call U Bitch\\\",\\\"path\\\":\\\"/2pac-wonda-why-they-call-u-bitch-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6584,\\\"apiPath\\\":\\\"/songs/6584\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":18,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-when-we-ride-lyrics\\\",\\\"title\\\":\\\"When We Ride\\\",\\\"path\\\":\\\"/2pac-when-we-ride-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6587,\\\"apiPath\\\":\\\"/songs/6587\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":19,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-thug-passion-lyrics\\\",\\\"title\\\":\\\"Thug Passion\\\",\\\"path\\\":\\\"/2pac-thug-passion-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":843,\\\"apiPath\\\":\\\"/songs/843\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":20,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-picture-me-rollin-lyrics\\\",\\\"title\\\":\\\"Picture Me Rollin’\\\",\\\"path\\\":\\\"/2pac-picture-me-rollin-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":698,\\\"apiPath\\\":\\\"/songs/698\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":21,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-check-out-time-lyrics\\\",\\\"title\\\":\\\"Check Out Time\\\",\\\"path\\\":\\\"/2pac-check-out-time-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6569,\\\"apiPath\\\":\\\"/songs/6569\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":22,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-ratha-be-ya-nigga-lyrics\\\",\\\"title\\\":\\\"Ratha Be Ya Nigga\\\",\\\"path\\\":\\\"/2pac-ratha-be-ya-nigga-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6580,\\\"apiPath\\\":\\\"/songs/6580\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":23,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-all-eyez-on-me-lyrics\\\",\\\"title\\\":\\\"All Eyez On Me\\\",\\\"path\\\":\\\"/2pac-all-eyez-on-me-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6576,\\\"apiPath\\\":\\\"/songs/6576\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":24,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-run-tha-streetz-lyrics\\\",\\\"title\\\":\\\"Run Tha Streetz\\\",\\\"path\\\":\\\"/2pac-run-tha-streetz-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6582,\\\"apiPath\\\":\\\"/songs/6582\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":25,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-aint-hard-2-find-lyrics\\\",\\\"title\\\":\\\"Ain’t Hard 2 Find\\\",\\\"path\\\":\\\"/2pac-aint-hard-2-find-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6568,\\\"apiPath\\\":\\\"/songs/6568\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":26,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-heaven-aint-hard-2-find-lyrics\\\",\\\"title\\\":\\\"Heaven Ain’t Hard 2 Find\\\",\\\"path\\\":\\\"/2pac-heaven-aint-hard-2-find-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6574,\\\"apiPath\\\":\\\"/songs/6574\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":27,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-california-love-lyrics\\\",\\\"title\\\":\\\"California Love\\\",\\\"path\\\":\\\"/2pac-california-love-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":244,\\\"apiPath\\\":\\\"/songs/244\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":28,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-how-do-u-want-it-original-version-lyrics\\\",\\\"title\\\":\\\"How Do U Want It (Original Version)\\\",\\\"path\\\":\\\"/2pac-how-do-u-want-it-original-version-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":4035997,\\\"apiPath\\\":\\\"/songs/4035997\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":null,\\\"type\\\":\\\"album_appearance\\\"}],\\\"artist\\\":{\\\"url\\\":\\\"https://genius.com/artists/2pac\\\",\\\"slug\\\":\\\"2pac\\\",\\\"name\\\":\\\"2Pac\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"0\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/9118a64d842f1611d83f5090989a3475.788x788x1.jpg\\\",\\\"id\\\":59,\\\"headerImageUrl\\\":\\\"https://images.genius.com/66dab5428172d59b83ed49304aacfa05.932x718x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/59\\\",\\\"type\\\":\\\"artist\\\"},\\\"url\\\":\\\"https://genius.com/albums/2pac/All-eyez-on-me\\\",\\\"releaseDateForDisplay\\\":\\\"February 13, 1996\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":13,\\\"month\\\":2,\\\"year\\\":1996},\\\"nameWithArtist\\\":\\\"All Eyez On Me (artist: 2Pac)\\\",\\\"name\\\":\\\"All Eyez On Me\\\",\\\"id\\\":38,\\\"fullTitle\\\":\\\"All Eyez On Me by 2Pac\\\",\\\"coverArtUrl\\\":\\\"https://images.genius.com/51289a294de3498eb6e1ff4e3a769c28.1000x1000x1.png\\\",\\\"coverArtThumbnailUrl\\\":\\\"https://images.genius.com/51289a294de3498eb6e1ff4e3a769c28.300x300x1.png\\\",\\\"apiPath\\\":\\\"/albums/38\\\",\\\"type\\\":\\\"album\\\"},{\\\"tracklist\\\":[{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-only-god-can-judge-me-lyrics\\\",\\\"title\\\":\\\"Only God Can Judge Me\\\",\\\"path\\\":\\\"/2pac-only-god-can-judge-me-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":2626,\\\"apiPath\\\":\\\"/songs/2626\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":1,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-just-like-daddy-lyrics\\\",\\\"title\\\":\\\"Just Like Daddy\\\",\\\"path\\\":\\\"/2pac-just-like-daddy-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":3160,\\\"apiPath\\\":\\\"/songs/3160\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":2,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-me-and-my-girlfriend-lyrics\\\",\\\"title\\\":\\\"Me and My Girlfriend\\\",\\\"path\\\":\\\"/2pac-me-and-my-girlfriend-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":375,\\\"apiPath\\\":\\\"/songs/375\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":3,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-against-all-odds-lyrics\\\",\\\"title\\\":\\\"Against All Odds\\\",\\\"path\\\":\\\"/2pac-against-all-odds-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":1085,\\\"apiPath\\\":\\\"/songs/1085\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":4,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-tradin-war-stories-lyrics\\\",\\\"title\\\":\\\"Tradin’ War Stories\\\",\\\"path\\\":\\\"/2pac-tradin-war-stories-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":3428,\\\"apiPath\\\":\\\"/songs/3428\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":5,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-skandalouz-lyrics\\\",\\\"title\\\":\\\"Skandalouz\\\",\\\"path\\\":\\\"/2pac-skandalouz-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6583,\\\"apiPath\\\":\\\"/songs/6583\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":6,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-2-of-amerikaz-most-wanted-lyrics\\\",\\\"title\\\":\\\"2 of Amerikaz Most Wanted\\\",\\\"path\\\":\\\"/2pac-2-of-amerikaz-most-wanted-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":389,\\\"apiPath\\\":\\\"/songs/389\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":7,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-to-live-and-die-in-la-lyrics\\\",\\\"title\\\":\\\"To Live and Die In L.A.\\\",\\\"path\\\":\\\"/2pac-to-live-and-die-in-la-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":96,\\\"apiPath\\\":\\\"/songs/96\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":8,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-california-love-lyrics\\\",\\\"title\\\":\\\"California Love\\\",\\\"path\\\":\\\"/2pac-california-love-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":244,\\\"apiPath\\\":\\\"/songs/244\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":9,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/Thug-life-pour-out-a-little-liquor-lyrics\\\",\\\"title\\\":\\\"Pour Out a Little Liquor\\\",\\\"path\\\":\\\"/Thug-life-pour-out-a-little-liquor-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":1214,\\\"apiPath\\\":\\\"/songs/1214\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":10,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-life-of-an-outlaw-lyrics\\\",\\\"title\\\":\\\"Life of an Outlaw\\\",\\\"path\\\":\\\"/2pac-life-of-an-outlaw-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":2817,\\\"apiPath\\\":\\\"/songs/2817\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":11,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-all-eyez-on-me-lyrics\\\",\\\"title\\\":\\\"All Eyez On Me\\\",\\\"path\\\":\\\"/2pac-all-eyez-on-me-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6576,\\\"apiPath\\\":\\\"/songs/6576\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":12,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-wanted-dead-or-alive-lyrics\\\",\\\"title\\\":\\\"Wanted Dead Or Alive\\\",\\\"path\\\":\\\"/2pac-wanted-dead-or-alive-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":31888,\\\"apiPath\\\":\\\"/songs/31888\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":13,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/2pac-staring-through-my-rear-view-lyrics\\\",\\\"title\\\":\\\"Staring Through My Rear View\\\",\\\"path\\\":\\\"/2pac-staring-through-my-rear-view-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":1280,\\\"apiPath\\\":\\\"/songs/1280\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":14,\\\"type\\\":\\\"album_appearance\\\"}],\\\"artist\\\":{\\\"url\\\":\\\"https://genius.com/artists/2pac\\\",\\\"slug\\\":\\\"2pac\\\",\\\"name\\\":\\\"2Pac\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"0\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/9118a64d842f1611d83f5090989a3475.788x788x1.jpg\\\",\\\"id\\\":59,\\\"headerImageUrl\\\":\\\"https://images.genius.com/66dab5428172d59b83ed49304aacfa05.932x718x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/59\\\",\\\"type\\\":\\\"artist\\\"},\\\"url\\\":\\\"https://genius.com/albums/2pac/The-prophet-the-best-of-the-works\\\",\\\"releaseDateForDisplay\\\":\\\"July 7, 2003\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":7,\\\"month\\\":7,\\\"year\\\":2003},\\\"nameWithArtist\\\":\\\"The Prophet: The Best of the Works (artist: 2Pac)\\\",\\\"name\\\":\\\"The Prophet: The Best of the Works\\\",\\\"id\\\":482084,\\\"fullTitle\\\":\\\"The Prophet: The Best of the Works by 2Pac\\\",\\\"coverArtUrl\\\":\\\"https://images.genius.com/8ec48b2cb57552179b13f1f160244d71.593x593x1.jpg\\\",\\\"coverArtThumbnailUrl\\\":\\\"https://images.genius.com/8ec48b2cb57552179b13f1f160244d71.300x300x1.jpg\\\",\\\"apiPath\\\":\\\"/albums/482084\\\",\\\"type\\\":\\\"album\\\"}],\\\"album\\\":38,\\\"stubhubDeal\\\":\\\"viagogo\\\",\\\"songArtTextColor\\\":\\\"#fff\\\",\\\"songArtSecondaryColor\\\":\\\"#601216\\\",\\\"songArtPrimaryColor\\\":\\\"#e02324\\\",\\\"currentUserMetadata\\\":{\\\"iqByAction\\\":{},\\\"relationships\\\":{},\\\"interactions\\\":{\\\"following\\\":false,\\\"pyong\\\":false},\\\"excludedPermissions\\\":[\\\"follow\\\",\\\"award_transcription_iq\\\",\\\"remove_transcription_iq\\\",\\\"pyong\\\",\\\"edit_lyrics\\\",\\\"view_annotation_engagement_data\\\",\\\"publish\\\",\\\"unpublish\\\",\\\"edit_spotify_details\\\",\\\"hide\\\",\\\"unhide\\\",\\\"toggle_featured_video\\\",\\\"add_pinned_annotation_to\\\",\\\"add_community_annotation_to\\\",\\\"destroy\\\",\\\"mark_as_not_spam\\\",\\\"edit_spotify_annotations_for\\\",\\\"verify_lyrics\\\",\\\"unverify_lyrics\\\",\\\"edit_anything\\\",\\\"edit_any_media\\\",\\\"edit\\\",\\\"rename\\\",\\\"edit_tags\\\",\\\"reindex\\\",\\\"view_lyrics_synchronization\\\",\\\"enable_media\\\",\\\"disable_media\\\",\\\"edit_lyrics_or_annotation_brackets\\\",\\\"see_editorial_indicators\\\",\\\"view_attribution_visualization\\\",\\\"edit_annotation_brackets\\\",\\\"preview_lyrics_for_export\\\",\\\"hide_apple_player\\\",\\\"unhide_apple_player\\\",\\\"trigger_apple_match\\\",\\\"mark_lyrics_evaluation_as_complete\\\",\\\"mark_lyrics_evaluation_as_staff_approved\\\",\\\"unmark_lyrics_evaluation_as_complete\\\",\\\"mark_lyrics_evaluation_as_un_staff_approved\\\",\\\"view_transcriber_media_player\\\",\\\"override_apple_match\\\",\\\"set_song_color_gradient\\\",\\\"mark_as_hot\\\",\\\"unmark_as_hot\\\",\\\"use_mark_complete_button\\\",\\\"edit_multiple_primary_artist\\\",\\\"edit_youtube_url\\\",\\\"edit_soundcloud_url\\\",\\\"edit_spotify_uuid\\\",\\\"edit_vevo_url\\\",\\\"create_comment\\\",\\\"moderate_annotations\\\",\\\"create_annotation\\\",\\\"see_short_id\\\",\\\"manage_chart_item\\\",\\\"create_tag\\\",\\\"propose_lyrics_edit\\\",\\\"view_lyrics_edit_proposals_on_song\\\",\\\"create_question\\\",\\\"answer_question_with_source\\\",\\\"create_additional_role\\\",\\\"add_qa\\\",\\\"pin_qa\\\"],\\\"permissions\\\":[\\\"see_pageviews\\\",\\\"view_apple_music_player\\\",\\\"view_recommendations\\\",\\\"view_relationships_page\\\",\\\"view_song_story_gallery\\\"]},\\\"youtubeUrl\\\":\\\"http://www.youtube.com/watch?v=H1HdZFgR-aA\\\",\\\"youtubeStart\\\":\\\"0\\\",\\\"vttpId\\\":null,\\\"viewableByRoles\\\":[],\\\"updatedByHumanAt\\\":1720296086,\\\"twitterShareMessageWithoutUrl\\\":\\\"2Pac – All Eyez On Me @2PAC\\\",\\\"twitterShareMessage\\\":\\\"2Pac – All Eyez On Me @2PAC https://genius.com/2pac-all-eyez-on-me-lyrics\\\",\\\"transcriptionPriority\\\":\\\"normal\\\",\\\"trackingPaths\\\":{\\\"concurrent\\\":\\\"/2pac-all-eyez-on-me-lyrics\\\",\\\"aggregate\\\":\\\"/2pac-all-eyez-on-me-lyrics\\\"},\\\"trackingData\\\":[{\\\"value\\\":6576,\\\"key\\\":\\\"Song ID\\\"},{\\\"value\\\":\\\"All Eyez On Me\\\",\\\"key\\\":\\\"Title\\\"},{\\\"value\\\":\\\"2Pac\\\",\\\"key\\\":\\\"Primary Artist\\\"},{\\\"value\\\":59,\\\"key\\\":\\\"Primary Artist ID\\\"},{\\\"value\\\":[\\\"2Pac\\\"],\\\"key\\\":\\\"Primary Artists\\\"},{\\\"value\\\":[59],\\\"key\\\":\\\"Primary Artists IDs\\\"},{\\\"value\\\":\\\"All Eyez On Me\\\",\\\"key\\\":\\\"Primary Album\\\"},{\\\"value\\\":38,\\\"key\\\":\\\"Primary Album ID\\\"},{\\\"value\\\":\\\"rap\\\",\\\"key\\\":\\\"Tag\\\"},{\\\"value\\\":\\\"rap\\\",\\\"key\\\":\\\"Primary Tag\\\"},{\\\"value\\\":1434,\\\"key\\\":\\\"Primary Tag ID\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"Music?\\\"},{\\\"value\\\":\\\"Song\\\",\\\"key\\\":\\\"Annotatable Type\\\"},{\\\"value\\\":6576,\\\"key\\\":\\\"Annotatable ID\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"featured_video\\\"},{\\\"value\\\":[],\\\"key\\\":\\\"cohort_ids\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"has_verified_callout\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"has_featured_annotation\\\"},{\\\"value\\\":\\\"2011-04-09T02:50:15Z\\\",\\\"key\\\":\\\"created_at\\\"},{\\\"value\\\":\\\"2011-04-01\\\",\\\"key\\\":\\\"created_month\\\"},{\\\"value\\\":2011,\\\"key\\\":\\\"created_year\\\"},{\\\"value\\\":\\\"C\\\",\\\"key\\\":\\\"song_tier\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"Has Recirculated Articles\\\"},{\\\"value\\\":\\\"en\\\",\\\"key\\\":\\\"Lyrics Language\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"Has Apple Match\\\"},{\\\"value\\\":\\\"1996-02-13\\\",\\\"key\\\":\\\"Release Date\\\"},{\\\"value\\\":null,\\\"key\\\":\\\"NRM Tier\\\"},{\\\"value\\\":null,\\\"key\\\":\\\"NRM Target Date\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"Has Description\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"Has Youtube URL\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"Has Translation Q&A\\\"},{\\\"value\\\":35,\\\"key\\\":\\\"Comment Count\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"hot\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"has_recommendations\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"has_stubhub_artist\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"has_stubhub_link\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"Translation\\\"},{\\\"value\\\":\\\"mixpanel\\\",\\\"key\\\":\\\"recommendation_strategy\\\"},{\\\"value\\\":\\\"control\\\",\\\"key\\\":\\\"web_interstitial_variant\\\"},{\\\"value\\\":\\\"desktop_react\\\",\\\"key\\\":\\\"platform_variant\\\"}],\\\"titleWithFeatured\\\":\\\"All Eyez On Me (Ft. Big Syke)\\\",\\\"stats\\\":{\\\"pageviews\\\":1212666,\\\"hot\\\":false,\\\"verifiedAnnotations\\\":0,\\\"unreviewedAnnotations\\\":7,\\\"transcribers\\\":0,\\\"iqEarners\\\":156,\\\"contributors\\\":156,\\\"acceptedAnnotations\\\":16},\\\"spotifyUuid\\\":\\\"2xTft6GEZeTyWNpdX94rkf\\\",\\\"soundcloudUrl\\\":null,\\\"songArtImageUrl\\\":\\\"https://images.genius.com/51289a294de3498eb6e1ff4e3a769c28.1000x1000x1.png\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/51289a294de3498eb6e1ff4e3a769c28.300x300x1.png\\\",\\\"shareUrl\\\":\\\"https://genius.com/2pac-all-eyez-on-me-lyrics\\\",\\\"releaseDateWithAbbreviatedMonthForDisplay\\\":\\\"Feb. 13, 1996\\\",\\\"releaseDateForDisplay\\\":\\\"February 13, 1996\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":13,\\\"month\\\":2,\\\"year\\\":1996},\\\"releaseDate\\\":\\\"1996-02-13\\\",\\\"relationshipsIndexUrl\\\":\\\"https://genius.com/2pac-all-eyez-on-me-sample\\\",\\\"recordingLocation\\\":\\\"Can-Am Studios (Tarzana, CA)\\\",\\\"pyongsCount\\\":111,\\\"pusherChannel\\\":\\\"song-6576\\\",\\\"published\\\":false,\\\"primaryArtistNames\\\":\\\"2Pac\\\",\\\"pendingLyricsEditsCount\\\":2,\\\"metadataFieldsNa\\\":{\\\"songMeaning\\\":false,\\\"albums\\\":false},\\\"lyricsVerified\\\":false,\\\"lyricsUpdatedAt\\\":1720296086,\\\"lyricsPlaceholderReason\\\":null,\\\"lyricsOwnerId\\\":50,\\\"language\\\":\\\"en\\\",\\\"isMusic\\\":true,\\\"instrumental\\\":false,\\\"hidden\\\":false,\\\"headerImageUrl\\\":\\\"https://images.genius.com/51289a294de3498eb6e1ff4e3a769c28.1000x1000x1.png\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/51289a294de3498eb6e1ff4e3a769c28.300x300x1.png\\\",\\\"hasInstagramReelAnnotations\\\":null,\\\"fullTitle\\\":\\\"All Eyez On Me by 2Pac (Ft. Big Syke)\\\",\\\"featuredVideo\\\":false,\\\"facebookShareMessageWithoutUrl\\\":\\\"2Pac – All Eyez On Me\\\",\\\"explicit\\\":true,\\\"embedContent\\\":\\\"<div id=\\'rg_embed_link_6576\\' class=\\'rg_embed_link\\' data-song-id=\\'6576\\'>Read <a href=\\'https://genius.com/2pac-all-eyez-on-me-lyrics\\'>“All Eyez On Me” by 2Pac<\\/a> on Genius<\\/div> <script crossorigin src=\\'//genius.com/songs/6576/embed.js\\'><\\/script>\\\",\\\"descriptionPreview\\\":\\\"The title track off of 2Pac’s album All Eyez on Me samples Linda Clifford’s “Never Gonna Stop” (also used for Nas\\' “Street Dreams,” which was released a few months later in July 1996). Producer Johnny J recalls connecting with 2Pac for this track:\\\\n\\\\nThat was the very first track I laid when we got together at Death Row. When he just got out of jail, just got released, two days later he’s like, “‘J’, get to the studio, I’m with Death Row now.” I assumed it was a joke, somebody perpetrating Tupac. I’m like “Hell no – ‘Pac is locked up!” He’s like “J, I’m out” I walk in, 15 minutes into the session, the first beat I put in the drum machine is “All Eyez On Me.” I wasn’t going to show him the track, honestly. I was like, “This track? Nah, it’s not finished. It’s incomplete.” My wife says, “Hey, it’s a dope beat!” So I just pop it in, titles just come right off his fuckin’ head.\\\\n\\\\nThis classic gives us an idea of what the media was doing with Pac’s life. At this moment, all eyes in the music world were on him due to the intrigue around his release from prison, signing with the notorious Death Row Records, as well as the 2Pac/Death Row/West Coast vs. Biggie/Bad Boy/East Coast beef.\\\",\\\"description\\\":{\\\"markdown\\\":\\\"The title track off of 2Pac\\'s album *All Eyez on Me* samples Linda Clifford\\'s \\\\\\\"Never Gonna Stop\\\\\\\" (also used for Nas\\' [\\\\\\\"Street Dreams,\\\\\\\"](/Nas-street-dreams-lyrics) which was released a few months later in July 1996). Producer Johnny J [recalls connecting with 2Pac for this track](https://2paclegacy.net/the-making-of-tupacs-all-eyez-on-me-xxl/):\\\\n\\\\n>That was the very first track I laid when we got together at Death Row. When he just got out of jail, just got released, two days later he’s like, “‘J’, get to the studio, I’m with Death Row now.” I assumed it was a joke, somebody perpetrating Tupac. I’m like “Hell no – ‘Pac is locked up!” He’s like “J, I’m out” I walk in, 15 minutes into the session, the first beat I put in the drum machine is “All Eyez On Me.” I wasn’t going to show him the track, honestly. I was like, “This track? Nah, it’s not finished. It’s incomplete.” My wife says, “Hey, it’s a dope beat!” So I just pop it in, titles just come right off his fuckin’ head.\\\\n\\\\nThis classic gives us an idea of what the media was doing with Pac\\'s life. At this moment, all eyes in the music world were on him due to the intrigue around his release from prison, signing with the notorious Death Row Records, as well as the 2Pac/Death Row/West Coast vs. Biggie/Bad Boy/East Coast beef.\\\",\\\"html\\\":\\\"<p>The title track off of 2Pac’s album <em>All Eyez on Me<\\/em> samples Linda Clifford’s “Never Gonna Stop” (also used for Nas\\' <a href=\\\\\\\"https://genius.com/Nas-street-dreams-lyrics\\\\\\\" rel=\\\\\\\"noopener\\\\\\\" data-api_path=\\\\\\\"/songs/979\\\\\\\">“Street Dreams,”<\\/a> which was released a few months later in July 1996). Producer Johnny J <a href=\\\\\\\"https://2paclegacy.net/the-making-of-tupacs-all-eyez-on-me-xxl/\\\\\\\" rel=\\\\\\\"noopener nofollow\\\\\\\">recalls connecting with 2Pac for this track<\\/a>:<\\/p>\\\\n\\\\n<blockquote><p>That was the very first track I laid when we got together at Death Row. When he just got out of jail, just got released, two days later he’s like, “‘J’, get to the studio, I’m with Death Row now.” I assumed it was a joke, somebody perpetrating Tupac. I’m like “Hell no – ‘Pac is locked up!” He’s like “J, I’m out” I walk in, 15 minutes into the session, the first beat I put in the drum machine is “All Eyez On Me.” I wasn’t going to show him the track, honestly. I was like, “This track? Nah, it’s not finished. It’s incomplete.” My wife says, “Hey, it’s a dope beat!” So I just pop it in, titles just come right off his fuckin’ head.<\\/p><\\/blockquote>\\\\n\\\\n<p>This classic gives us an idea of what the media was doing with Pac’s life. At this moment, all eyes in the music world were on him due to the intrigue around his release from prison, signing with the notorious Death Row Records, as well as the 2Pac/Death Row/West Coast vs. Biggie/Bad Boy/East Coast beef.<\\/p>\\\"},\\\"customSongArtImageUrl\\\":\\\"\\\",\\\"customHeaderImageUrl\\\":\\\"\\\",\\\"commentCount\\\":35,\\\"artistNames\\\":\\\"2Pac (Ft. Big Syke)\\\",\\\"appleMusicPlayerUrl\\\":\\\"https://genius.com/songs/6576/apple_music_player\\\",\\\"appleMusicId\\\":\\\"6917191\\\",\\\"annotationCount\\\":24},\\\"6580\\\":{\\\"url\\\":\\\"https://genius.com/2pac-ratha-be-ya-nigga-lyrics\\\",\\\"title\\\":\\\"Ratha Be Ya Nigga\\\",\\\"path\\\":\\\"/2pac-ratha-be-ya-nigga-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6580,\\\"apiPath\\\":\\\"/songs/6580\\\",\\\"type\\\":\\\"song\\\"},\\\"6581\\\":{\\\"url\\\":\\\"https://genius.com/2pac-shorty-wanna-be-a-thug-lyrics\\\",\\\"title\\\":\\\"Shorty Wanna Be a Thug\\\",\\\"path\\\":\\\"/2pac-shorty-wanna-be-a-thug-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6581,\\\"apiPath\\\":\\\"/songs/6581\\\",\\\"type\\\":\\\"song\\\"},\\\"6582\\\":{\\\"url\\\":\\\"https://genius.com/2pac-run-tha-streetz-lyrics\\\",\\\"title\\\":\\\"Run Tha Streetz\\\",\\\"path\\\":\\\"/2pac-run-tha-streetz-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6582,\\\"apiPath\\\":\\\"/songs/6582\\\",\\\"type\\\":\\\"song\\\"},\\\"6583\\\":{\\\"url\\\":\\\"https://genius.com/2pac-skandalouz-lyrics\\\",\\\"title\\\":\\\"Skandalouz\\\",\\\"path\\\":\\\"/2pac-skandalouz-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6583,\\\"apiPath\\\":\\\"/songs/6583\\\",\\\"type\\\":\\\"song\\\"},\\\"6584\\\":{\\\"url\\\":\\\"https://genius.com/2pac-wonda-why-they-call-u-bitch-lyrics\\\",\\\"title\\\":\\\"Wonda Why They Call U Bitch\\\",\\\"path\\\":\\\"/2pac-wonda-why-they-call-u-bitch-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6584,\\\"apiPath\\\":\\\"/songs/6584\\\",\\\"type\\\":\\\"song\\\"},\\\"6587\\\":{\\\"url\\\":\\\"https://genius.com/2pac-when-we-ride-lyrics\\\",\\\"title\\\":\\\"When We Ride\\\",\\\"path\\\":\\\"/2pac-when-we-ride-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":6587,\\\"apiPath\\\":\\\"/songs/6587\\\",\\\"type\\\":\\\"song\\\"},\\\"55423\\\":{\\\"url\\\":\\\"https://genius.com/2pac-no-more-pain-lyrics\\\",\\\"title\\\":\\\"No More Pain\\\",\\\"path\\\":\\\"/2pac-no-more-pain-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":55423,\\\"apiPath\\\":\\\"/songs/55423\\\",\\\"type\\\":\\\"song\\\"},\\\"3123951\\\":{\\\"url\\\":\\\"https://genius.com/2pac-california-love-remix-lyrics\\\",\\\"title\\\":\\\"California Love (Remix)\\\",\\\"path\\\":\\\"/2pac-california-love-remix-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":3123951,\\\"apiPath\\\":\\\"/songs/3123951\\\",\\\"type\\\":\\\"song\\\"},\\\"4035997\\\":{\\\"url\\\":\\\"https://genius.com/2pac-how-do-u-want-it-original-version-lyrics\\\",\\\"title\\\":\\\"How Do U Want It (Original Version)\\\",\\\"path\\\":\\\"/2pac-how-do-u-want-it-original-version-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":4035997,\\\"apiPath\\\":\\\"/songs/4035997\\\",\\\"type\\\":\\\"song\\\"}},\\\"albumAppearances\\\":{\\\"244\\\":{\\\"song\\\":244,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":28,\\\"type\\\":\\\"album_appearance\\\"},\\\"312\\\":{\\\"song\\\":312,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":4,\\\"type\\\":\\\"album_appearance\\\"},\\\"389\\\":{\\\"song\\\":389,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":6,\\\"type\\\":\\\"album_appearance\\\"},\\\"544\\\":{\\\"song\\\":544,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":15,\\\"type\\\":\\\"album_appearance\\\"},\\\"567\\\":{\\\"song\\\":567,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":14,\\\"type\\\":\\\"album_appearance\\\"},\\\"698\\\":{\\\"song\\\":698,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":21,\\\"type\\\":\\\"album_appearance\\\"},\\\"712\\\":{\\\"song\\\":712,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":5,\\\"type\\\":\\\"album_appearance\\\"},\\\"780\\\":{\\\"song\\\":780,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":1,\\\"type\\\":\\\"album_appearance\\\"},\\\"819\\\":{\\\"song\\\":819,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":8,\\\"type\\\":\\\"album_appearance\\\"},\\\"843\\\":{\\\"song\\\":843,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":20,\\\"type\\\":\\\"album_appearance\\\"},\\\"2626\\\":{\\\"song\\\":2626,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":10,\\\"type\\\":\\\"album_appearance\\\"},\\\"3428\\\":{\\\"song\\\":3428,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":11,\\\"type\\\":\\\"album_appearance\\\"},\\\"5377\\\":{\\\"song\\\":5377,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":13,\\\"type\\\":\\\"album_appearance\\\"},\\\"5533\\\":{\\\"song\\\":5533,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":2,\\\"type\\\":\\\"album_appearance\\\"},\\\"6568\\\":{\\\"song\\\":6568,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":26,\\\"type\\\":\\\"album_appearance\\\"},\\\"6569\\\":{\\\"song\\\":6569,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":22,\\\"type\\\":\\\"album_appearance\\\"},\\\"6573\\\":{\\\"song\\\":6573,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":9,\\\"type\\\":\\\"album_appearance\\\"},\\\"6574\\\":{\\\"song\\\":6574,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":27,\\\"type\\\":\\\"album_appearance\\\"},\\\"6575\\\":{\\\"song\\\":6575,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":17,\\\"type\\\":\\\"album_appearance\\\"},\\\"6576\\\":{\\\"song\\\":6576,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":24,\\\"type\\\":\\\"album_appearance\\\"},\\\"6580\\\":{\\\"song\\\":6580,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":23,\\\"type\\\":\\\"album_appearance\\\"},\\\"6581\\\":{\\\"song\\\":6581,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":16,\\\"type\\\":\\\"album_appearance\\\"},\\\"6582\\\":{\\\"song\\\":6582,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":25,\\\"type\\\":\\\"album_appearance\\\"},\\\"6583\\\":{\\\"song\\\":6583,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":3,\\\"type\\\":\\\"album_appearance\\\"},\\\"6584\\\":{\\\"song\\\":6584,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":18,\\\"type\\\":\\\"album_appearance\\\"},\\\"6587\\\":{\\\"song\\\":6587,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":19,\\\"type\\\":\\\"album_appearance\\\"},\\\"55423\\\":{\\\"song\\\":55423,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":7,\\\"type\\\":\\\"album_appearance\\\"},\\\"3123951\\\":{\\\"song\\\":3123951,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":12,\\\"type\\\":\\\"album_appearance\\\"},\\\"4035997\\\":{\\\"song\\\":4035997,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":null,\\\"type\\\":\\\"album_appearance\\\"}},\\\"albums\\\":{\\\"38\\\":{\\\"tracklist\\\":[780,5533,6583,312,712,389,55423,819,6573,2626,3428,3123951,5377,567,544,6581,6575,6584,6587,843,698,6569,6580,6576,6582,6568,6574,244,4035997],\\\"artist\\\":59,\\\"url\\\":\\\"https://genius.com/albums/2pac/All-eyez-on-me\\\",\\\"releaseDateForDisplay\\\":\\\"February 13, 1996\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":13,\\\"month\\\":2,\\\"year\\\":1996},\\\"nameWithArtist\\\":\\\"All Eyez On Me (artist: 2Pac)\\\",\\\"name\\\":\\\"All Eyez On Me\\\",\\\"id\\\":38,\\\"fullTitle\\\":\\\"All Eyez On Me by 2Pac\\\",\\\"coverArtUrl\\\":\\\"https://images.genius.com/51289a294de3498eb6e1ff4e3a769c28.1000x1000x1.png\\\",\\\"coverArtThumbnailUrl\\\":\\\"https://images.genius.com/51289a294de3498eb6e1ff4e3a769c28.300x300x1.png\\\",\\\"apiPath\\\":\\\"/albums/38\\\",\\\"type\\\":\\\"album\\\"}},\\\"users\\\":{\\\"50\\\":{\\\"currentUserMetadata\\\":{\\\"interactions\\\":{\\\"following\\\":false},\\\"excludedPermissions\\\":[\\\"follow\\\"],\\\"permissions\\\":[]},\\\"url\\\":\\\"https://genius.com/AlysonWnderland\\\",\\\"roleForDisplay\\\":\\\"contributor\\\",\\\"name\\\":\\\"AlysonWnderland\\\",\\\"login\\\":\\\"AlysonWnderland\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"iq\\\":3979,\\\"id\\\":50,\\\"humanReadableRoleForDisplay\\\":\\\"Contributor\\\",\\\"headerImageUrl\\\":\\\"https://images.rapgenius.com/avatars/medium/f815fe8f1b9c915e5b77e6089431c771\\\",\\\"avatar\\\":{\\\"medium\\\":{\\\"boundingBox\\\":{\\\"height\\\":400,\\\"width\\\":300},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/medium/f815fe8f1b9c915e5b77e6089431c771\\\"},\\\"small\\\":{\\\"boundingBox\\\":{\\\"height\\\":100,\\\"width\\\":100},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/small/f815fe8f1b9c915e5b77e6089431c771\\\"},\\\"thumb\\\":{\\\"boundingBox\\\":{\\\"height\\\":32,\\\"width\\\":32},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/thumb/f815fe8f1b9c915e5b77e6089431c771\\\"},\\\"tiny\\\":{\\\"boundingBox\\\":{\\\"height\\\":16,\\\"width\\\":16},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/tiny/f815fe8f1b9c915e5b77e6089431c771\\\"}},\\\"apiPath\\\":\\\"/users/50\\\",\\\"aboutMeSummary\\\":\\\"\\\",\\\"type\\\":\\\"user\\\"},\\\"11899\\\":{\\\"currentUserMetadata\\\":{\\\"interactions\\\":{\\\"following\\\":false},\\\"excludedPermissions\\\":[\\\"follow\\\"],\\\"permissions\\\":[]},\\\"url\\\":\\\"https://genius.com/RimmerGawd\\\",\\\"roleForDisplay\\\":\\\"contributor\\\",\\\"name\\\":\\\"RimmerGawd\\\",\\\"login\\\":\\\"RimmerGawd\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"iq\\\":37066,\\\"id\\\":11899,\\\"humanReadableRoleForDisplay\\\":\\\"Contributor\\\",\\\"headerImageUrl\\\":\\\"https://s3.amazonaws.com/rapgenius/avatars/medium/1358288334_11899_11899_Public%20Enemy_RG.jpg\\\",\\\"avatar\\\":{\\\"medium\\\":{\\\"boundingBox\\\":{\\\"height\\\":400,\\\"width\\\":300},\\\"url\\\":\\\"https://s3.amazonaws.com/rapgenius/avatars/medium/1358288334_11899_11899_Public%20Enemy_RG.jpg\\\"},\\\"small\\\":{\\\"boundingBox\\\":{\\\"height\\\":100,\\\"width\\\":100},\\\"url\\\":\\\"https://s3.amazonaws.com/rapgenius/avatars/small/1358288335_11899_11899_Public%20Enemy_RG.jpg\\\"},\\\"thumb\\\":{\\\"boundingBox\\\":{\\\"height\\\":32,\\\"width\\\":32},\\\"url\\\":\\\"https://s3.amazonaws.com/rapgenius/avatars/thumb/1358288334_11899_11899_Public%20Enemy_RG.jpg\\\"},\\\"tiny\\\":{\\\"boundingBox\\\":{\\\"height\\\":16,\\\"width\\\":16},\\\"url\\\":\\\"https://s3.amazonaws.com/rapgenius/avatars/tiny/11899_11899_Public%20Enemy_RG.jpg\\\"}},\\\"apiPath\\\":\\\"/users/11899\\\",\\\"aboutMeSummary\\\":\\\"Twitter OG\\\",\\\"type\\\":\\\"user\\\"},\\\"15733\\\":{\\\"currentUserMetadata\\\":{\\\"interactions\\\":{\\\"following\\\":false},\\\"excludedPermissions\\\":[\\\"follow\\\"],\\\"permissions\\\":[]},\\\"url\\\":\\\"https://genius.com/moshitup\\\",\\\"roleForDisplay\\\":\\\"editor\\\",\\\"name\\\":\\\"moshitup\\\",\\\"login\\\":\\\"moshitup\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"iq\\\":82849,\\\"id\\\":15733,\\\"humanReadableRoleForDisplay\\\":\\\"Editor\\\",\\\"headerImageUrl\\\":\\\"https://s3.amazonaws.com/rapgenius/avatars/medium/1358288296_15733_justice.png\\\",\\\"avatar\\\":{\\\"medium\\\":{\\\"boundingBox\\\":{\\\"height\\\":400,\\\"width\\\":300},\\\"url\\\":\\\"https://s3.amazonaws.com/rapgenius/avatars/medium/1358288296_15733_justice.png\\\"},\\\"small\\\":{\\\"boundingBox\\\":{\\\"height\\\":100,\\\"width\\\":100},\\\"url\\\":\\\"https://s3.amazonaws.com/rapgenius/avatars/small/1358288297_15733_justice.png\\\"},\\\"thumb\\\":{\\\"boundingBox\\\":{\\\"height\\\":32,\\\"width\\\":32},\\\"url\\\":\\\"https://s3.amazonaws.com/rapgenius/avatars/thumb/1358288297_15733_justice.png\\\"},\\\"tiny\\\":{\\\"boundingBox\\\":{\\\"height\\\":16,\\\"width\\\":16},\\\"url\\\":\\\"https://s3.amazonaws.com/rapgenius/avatars/tiny/15733_justice.png\\\"}},\\\"apiPath\\\":\\\"/users/15733\\\",\\\"aboutMeSummary\\\":\\\"\\\",\\\"type\\\":\\\"user\\\"},\\\"1441140\\\":{\\\"currentUserMetadata\\\":{\\\"interactions\\\":{\\\"following\\\":false},\\\"excludedPermissions\\\":[\\\"follow\\\"],\\\"permissions\\\":[]},\\\"url\\\":\\\"https://genius.com/sixteenbars\\\",\\\"roleForDisplay\\\":\\\"contributor\\\",\\\"name\\\":\\\"Reservoir\\\",\\\"login\\\":\\\"sixteenbars\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"iq\\\":33286,\\\"id\\\":1441140,\\\"humanReadableRoleForDisplay\\\":\\\"Contributor\\\",\\\"headerImageUrl\\\":\\\"https://images.rapgenius.com/avatars/medium/ecf613b934b065a6d1cfba5dd972dfb9\\\",\\\"avatar\\\":{\\\"medium\\\":{\\\"boundingBox\\\":{\\\"height\\\":400,\\\"width\\\":300},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/medium/ecf613b934b065a6d1cfba5dd972dfb9\\\"},\\\"small\\\":{\\\"boundingBox\\\":{\\\"height\\\":100,\\\"width\\\":100},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/small/ecf613b934b065a6d1cfba5dd972dfb9\\\"},\\\"thumb\\\":{\\\"boundingBox\\\":{\\\"height\\\":32,\\\"width\\\":32},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/thumb/ecf613b934b065a6d1cfba5dd972dfb9\\\"},\\\"tiny\\\":{\\\"boundingBox\\\":{\\\"height\\\":16,\\\"width\\\":16},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/tiny/ecf613b934b065a6d1cfba5dd972dfb9\\\"}},\\\"apiPath\\\":\\\"/users/1441140\\\",\\\"aboutMeSummary\\\":\\\"são paulo state of mind.\\\\n\\\\n07/17/2015: editorized by RaulMarquesRJ.\\\\n\\\\n\\\\n\\\\n08/14/2015: coached GabrielYuji to editorship.\\\",\\\"type\\\":\\\"user\\\"},\\\"1662078\\\":{\\\"currentUserMetadata\\\":{\\\"interactions\\\":{\\\"following\\\":false},\\\"excludedPermissions\\\":[\\\"follow\\\"],\\\"permissions\\\":[]},\\\"url\\\":\\\"https://genius.com/MarouaneDermoumi\\\",\\\"roleForDisplay\\\":\\\"contributor\\\",\\\"name\\\":\\\"MarouaneDermoumi\\\",\\\"login\\\":\\\"MarouaneDermoumi\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"iq\\\":1924,\\\"id\\\":1662078,\\\"humanReadableRoleForDisplay\\\":\\\"Contributor\\\",\\\"headerImageUrl\\\":\\\"https://images.rapgenius.com/avatars/medium/1141119ef9a8d19755be50ea65d75da9\\\",\\\"avatar\\\":{\\\"medium\\\":{\\\"boundingBox\\\":{\\\"height\\\":400,\\\"width\\\":300},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/medium/1141119ef9a8d19755be50ea65d75da9\\\"},\\\"small\\\":{\\\"boundingBox\\\":{\\\"height\\\":100,\\\"width\\\":100},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/small/1141119ef9a8d19755be50ea65d75da9\\\"},\\\"thumb\\\":{\\\"boundingBox\\\":{\\\"height\\\":32,\\\"width\\\":32},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/thumb/1141119ef9a8d19755be50ea65d75da9\\\"},\\\"tiny\\\":{\\\"boundingBox\\\":{\\\"height\\\":16,\\\"width\\\":16},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/tiny/1141119ef9a8d19755be50ea65d75da9\\\"}},\\\"apiPath\\\":\\\"/users/1662078\\\",\\\"aboutMeSummary\\\":\\\"\\\",\\\"type\\\":\\\"user\\\"},\\\"2556988\\\":{\\\"currentUserMetadata\\\":{\\\"interactions\\\":{\\\"following\\\":false},\\\"excludedPermissions\\\":[\\\"follow\\\"],\\\"permissions\\\":[]},\\\"url\\\":\\\"https://genius.com/papos124\\\",\\\"roleForDisplay\\\":null,\\\"name\\\":\\\"papos124\\\",\\\"login\\\":\\\"papos124\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"iq\\\":190,\\\"id\\\":2556988,\\\"humanReadableRoleForDisplay\\\":null,\\\"headerImageUrl\\\":\\\"https://images.rapgenius.com/avatars/medium/3f51be29d90fc161ca7d6f3f159c6a63\\\",\\\"avatar\\\":{\\\"medium\\\":{\\\"boundingBox\\\":{\\\"height\\\":400,\\\"width\\\":300},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/medium/3f51be29d90fc161ca7d6f3f159c6a63\\\"},\\\"small\\\":{\\\"boundingBox\\\":{\\\"height\\\":100,\\\"width\\\":100},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/small/3f51be29d90fc161ca7d6f3f159c6a63\\\"},\\\"thumb\\\":{\\\"boundingBox\\\":{\\\"height\\\":32,\\\"width\\\":32},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/thumb/3f51be29d90fc161ca7d6f3f159c6a63\\\"},\\\"tiny\\\":{\\\"boundingBox\\\":{\\\"height\\\":16,\\\"width\\\":16},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/tiny/3f51be29d90fc161ca7d6f3f159c6a63\\\"}},\\\"apiPath\\\":\\\"/users/2556988\\\",\\\"aboutMeSummary\\\":\\\"\\\",\\\"type\\\":\\\"user\\\"},\\\"2954567\\\":{\\\"currentUserMetadata\\\":{\\\"interactions\\\":{\\\"following\\\":false},\\\"excludedPermissions\\\":[\\\"follow\\\"],\\\"permissions\\\":[]},\\\"url\\\":\\\"https://genius.com/Makavelis123\\\",\\\"roleForDisplay\\\":\\\"contributor\\\",\\\"name\\\":\\\"Makavelis123\\\",\\\"login\\\":\\\"Makavelis123\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"iq\\\":2360,\\\"id\\\":2954567,\\\"humanReadableRoleForDisplay\\\":\\\"Contributor\\\",\\\"headerImageUrl\\\":\\\"https://images.rapgenius.com/avatars/medium/4753c9296c3b6831a5351d4ded45fbaf\\\",\\\"avatar\\\":{\\\"medium\\\":{\\\"boundingBox\\\":{\\\"height\\\":400,\\\"width\\\":300},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/medium/4753c9296c3b6831a5351d4ded45fbaf\\\"},\\\"small\\\":{\\\"boundingBox\\\":{\\\"height\\\":100,\\\"width\\\":100},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/small/4753c9296c3b6831a5351d4ded45fbaf\\\"},\\\"thumb\\\":{\\\"boundingBox\\\":{\\\"height\\\":32,\\\"width\\\":32},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/thumb/4753c9296c3b6831a5351d4ded45fbaf\\\"},\\\"tiny\\\":{\\\"boundingBox\\\":{\\\"height\\\":16,\\\"width\\\":16},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/tiny/4753c9296c3b6831a5351d4ded45fbaf\\\"}},\\\"apiPath\\\":\\\"/users/2954567\\\",\\\"aboutMeSummary\\\":\\\"I’m here to get Tupac great work annotated and looking for help to do that. The Michael Jordan of the rap era should have a better record when it comes to his works being annotated. I mean he is top 3 all time if not the GOAT.\\\",\\\"type\\\":\\\"user\\\"},\\\"3492882\\\":{\\\"currentUserMetadata\\\":{\\\"interactions\\\":{\\\"following\\\":false},\\\"excludedPermissions\\\":[\\\"follow\\\"],\\\"permissions\\\":[]},\\\"url\\\":\\\"https://genius.com/SmashBeezy\\\",\\\"roleForDisplay\\\":\\\"editor\\\",\\\"name\\\":\\\"SmashBeezy\\\",\\\"login\\\":\\\"SmashBeezy\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"iq\\\":3686915,\\\"id\\\":3492882,\\\"humanReadableRoleForDisplay\\\":\\\"Editor\\\",\\\"headerImageUrl\\\":\\\"https://images.genius.com/avatars/medium/981605b56878ef16a812df453300d0fc\\\",\\\"avatar\\\":{\\\"medium\\\":{\\\"boundingBox\\\":{\\\"height\\\":400,\\\"width\\\":300},\\\"url\\\":\\\"https://images.genius.com/avatars/medium/981605b56878ef16a812df453300d0fc\\\"},\\\"small\\\":{\\\"boundingBox\\\":{\\\"height\\\":100,\\\"width\\\":100},\\\"url\\\":\\\"https://images.genius.com/avatars/small/981605b56878ef16a812df453300d0fc\\\"},\\\"thumb\\\":{\\\"boundingBox\\\":{\\\"height\\\":32,\\\"width\\\":32},\\\"url\\\":\\\"https://images.genius.com/avatars/thumb/981605b56878ef16a812df453300d0fc\\\"},\\\"tiny\\\":{\\\"boundingBox\\\":{\\\"height\\\":16,\\\"width\\\":16},\\\"url\\\":\\\"https://images.genius.com/avatars/tiny/981605b56878ef16a812df453300d0fc\\\"}},\\\"apiPath\\\":\\\"/users/3492882\\\",\\\"aboutMeSummary\\\":\\\"\\\",\\\"type\\\":\\\"user\\\"},\\\"6136918\\\":{\\\"currentUserMetadata\\\":{\\\"interactions\\\":{\\\"following\\\":false},\\\"excludedPermissions\\\":[\\\"follow\\\"],\\\"permissions\\\":[]},\\\"url\\\":\\\"https://genius.com/PJTHEGAWD\\\",\\\"roleForDisplay\\\":null,\\\"name\\\":\\\"PJTHEGAWD\\\",\\\"login\\\":\\\"PJTHEGAWD\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"iq\\\":150,\\\"id\\\":6136918,\\\"humanReadableRoleForDisplay\\\":null,\\\"headerImageUrl\\\":\\\"https://filepicker-images.genius.com/8p5i1e0fj0w\\\",\\\"avatar\\\":{\\\"medium\\\":{\\\"boundingBox\\\":{\\\"height\\\":400,\\\"width\\\":300},\\\"url\\\":\\\"https://images.genius.com/avatars/medium/2641d37cd8806a00d6ac77d1f0236797\\\"},\\\"small\\\":{\\\"boundingBox\\\":{\\\"height\\\":100,\\\"width\\\":100},\\\"url\\\":\\\"https://images.genius.com/avatars/small/2641d37cd8806a00d6ac77d1f0236797\\\"},\\\"thumb\\\":{\\\"boundingBox\\\":{\\\"height\\\":32,\\\"width\\\":32},\\\"url\\\":\\\"https://images.genius.com/avatars/thumb/2641d37cd8806a00d6ac77d1f0236797\\\"},\\\"tiny\\\":{\\\"boundingBox\\\":{\\\"height\\\":16,\\\"width\\\":16},\\\"url\\\":\\\"https://images.genius.com/avatars/tiny/2641d37cd8806a00d6ac77d1f0236797\\\"}},\\\"apiPath\\\":\\\"/users/6136918\\\",\\\"aboutMeSummary\\\":\\\"\\\",\\\"type\\\":\\\"user\\\"}},\\\"comments\\\":{\\\"5000933\\\":{\\\"reason\\\":null,\\\"author\\\":2556988,\\\"anonymousAuthor\\\":null,\\\"currentUserMetadata\\\":{\\\"interactions\\\":{\\\"vote\\\":null},\\\"excludedPermissions\\\":[\\\"vote\\\",\\\"accept\\\",\\\"reject\\\",\\\"mark_spam\\\",\\\"integrate\\\",\\\"archive\\\",\\\"destroy\\\"],\\\"permissions\\\":[]},\\\"votesTotal\\\":32,\\\"pinnedRole\\\":null,\\\"id\\\":5000933,\\\"hasVoters\\\":true,\\\"createdAt\\\":1496456046,\\\"commentableType\\\":\\\"Annotation\\\",\\\"commentableId\\\":3495386,\\\"body\\\":{\\\"markdown\\\":\\\"shout out to Johnny J for putting a sick beat for pac rip to both \\\\n\\\",\\\"html\\\":\\\"<p>shout out to Johnny J for putting a sick beat for pac rip to both<\\/p>\\\"},\\\"apiPath\\\":\\\"/comments/5000933\\\",\\\"type\\\":\\\"comment\\\"}},\\\"annotations\\\":{\\\"3495386\\\":{\\\"verifiedBy\\\":null,\\\"topComment\\\":5000933,\\\"rejectionComment\\\":null,\\\"createdBy\\\":11899,\\\"cosignedBy\\\":[],\\\"authors\\\":[{\\\"user\\\":3492882,\\\"pinnedRole\\\":null,\\\"attribution\\\":0.6929824561403508,\\\"type\\\":\\\"user_attribution\\\"},{\\\"user\\\":1441140,\\\"pinnedRole\\\":null,\\\"attribution\\\":0.1754385964912281,\\\"type\\\":\\\"user_attribution\\\"},{\\\"user\\\":11899,\\\"pinnedRole\\\":null,\\\"attribution\\\":0.07017543859649122,\\\"type\\\":\\\"user_attribution\\\"},{\\\"user\\\":15733,\\\"pinnedRole\\\":null,\\\"attribution\\\":0.052631578947368425,\\\"type\\\":\\\"user_attribution\\\"},{\\\"user\\\":6136918,\\\"pinnedRole\\\":null,\\\"attribution\\\":0.008771929824561403,\\\"type\\\":\\\"user_attribution\\\"}],\\\"acceptedBy\\\":50,\\\"currentUserMetadata\\\":{\\\"iqByAction\\\":{},\\\"interactions\\\":{\\\"vote\\\":null,\\\"pyong\\\":false,\\\"cosign\\\":false},\\\"excludedPermissions\\\":[\\\"vote\\\",\\\"edit\\\",\\\"cosign\\\",\\\"uncosign\\\",\\\"destroy\\\",\\\"accept\\\",\\\"reject\\\",\\\"see_unreviewed\\\",\\\"clear_votes\\\",\\\"propose_edit_to\\\",\\\"pin_to_profile\\\",\\\"unpin_from_profile\\\",\\\"update_source\\\",\\\"edit_custom_preview\\\",\\\"create_comment\\\"],\\\"permissions\\\":[]},\\\"votesTotal\\\":184,\\\"verified\\\":false,\\\"url\\\":\\\"https://genius.com/3495386/2pac-all-eyez-on-me/All-eyez-on-me\\\",\\\"twitterShareMessage\\\":\\\"“The title track off of 2Pac’s album All Eyez on Me samples Linda Clifford’s “Never Gonna Stop” (also use…” —@Genius\\\",\\\"state\\\":\\\"accepted\\\",\\\"source\\\":null,\\\"shareUrl\\\":\\\"https://genius.com/3495386\\\",\\\"referentId\\\":3495386,\\\"pyongsCount\\\":null,\\\"proposedEditCount\\\":0,\\\"pinned\\\":false,\\\"needsExegesis\\\":false,\\\"id\\\":3495386,\\\"hasVoters\\\":true,\\\"embedContent\\\":\\\"<blockquote class=\\'rg_standalone_container\\' data-src=\\'//genius.com/annotations/3495386/standalone_embed\\'><a href=\\'https://genius.com/3495386/2pac-all-eyez-on-me/All-eyez-on-me\\'>All Eyez On Me<\\/a><br><a href=\\'https://genius.com/2pac-all-eyez-on-me-lyrics\\'>&#8213; 2Pac (Ft. Big Syke) – All Eyez On Me<\\/a><\\/blockquote><script async crossorigin src=\\'//genius.com/annotations/load_standalone_embeds.js\\'><\\/script>\\\",\\\"deleted\\\":false,\\\"customPreview\\\":null,\\\"createdAt\\\":1318056473,\\\"community\\\":true,\\\"commentCount\\\":2,\\\"body\\\":{\\\"markdown\\\":\\\"The title track off of 2Pac\\'s album *All Eyez on Me* samples Linda Clifford\\'s \\\\\\\"Never Gonna Stop\\\\\\\" (also used for Nas\\' [\\\\\\\"Street Dreams,\\\\\\\"](/Nas-street-dreams-lyrics) which was released a few months later in July 1996). Producer Johnny J [recalls connecting with 2Pac for this track](https://2paclegacy.net/the-making-of-tupacs-all-eyez-on-me-xxl/):\\\\n\\\\n>That was the very first track I laid when we got together at Death Row. When he just got out of jail, just got released, two days later he’s like, “‘J’, get to the studio, I’m with Death Row now.” I assumed it was a joke, somebody perpetrating Tupac. I’m like “Hell no – ‘Pac is locked up!” He’s like “J, I’m out” I walk in, 15 minutes into the session, the first beat I put in the drum machine is “All Eyez On Me.” I wasn’t going to show him the track, honestly. I was like, “This track? Nah, it’s not finished. It’s incomplete.” My wife says, “Hey, it’s a dope beat!” So I just pop it in, titles just come right off his fuckin’ head.\\\\n\\\\nThis classic gives us an idea of what the media was doing with Pac\\'s life. At this moment, all eyes in the music world were on him due to the intrigue around his release from prison, signing with the notorious Death Row Records, as well as the 2Pac/Death Row/West Coast vs. Biggie/Bad Boy/East Coast beef.\\\",\\\"html\\\":\\\"<p>The title track off of 2Pac’s album <em>All Eyez on Me<\\/em> samples Linda Clifford’s “Never Gonna Stop” (also used for Nas\\' <a href=\\\\\\\"https://genius.com/Nas-street-dreams-lyrics\\\\\\\" rel=\\\\\\\"noopener\\\\\\\" data-api_path=\\\\\\\"/songs/979\\\\\\\">“Street Dreams,”<\\/a> which was released a few months later in July 1996). Producer Johnny J <a href=\\\\\\\"https://2paclegacy.net/the-making-of-tupacs-all-eyez-on-me-xxl/\\\\\\\" rel=\\\\\\\"noopener nofollow\\\\\\\">recalls connecting with 2Pac for this track<\\/a>:<\\/p>\\\\n\\\\n<blockquote><p>That was the very first track I laid when we got together at Death Row. When he just got out of jail, just got released, two days later he’s like, “‘J’, get to the studio, I’m with Death Row now.” I assumed it was a joke, somebody perpetrating Tupac. I’m like “Hell no – ‘Pac is locked up!” He’s like “J, I’m out” I walk in, 15 minutes into the session, the first beat I put in the drum machine is “All Eyez On Me.” I wasn’t going to show him the track, honestly. I was like, “This track? Nah, it’s not finished. It’s incomplete.” My wife says, “Hey, it’s a dope beat!” So I just pop it in, titles just come right off his fuckin’ head.<\\/p><\\/blockquote>\\\\n\\\\n<p>This classic gives us an idea of what the media was doing with Pac’s life. At this moment, all eyes in the music world were on him due to the intrigue around his release from prison, signing with the notorious Death Row Records, as well as the 2Pac/Death Row/West Coast vs. Biggie/Bad Boy/East Coast beef.<\\/p>\\\"},\\\"beingCreated\\\":false,\\\"apiPath\\\":\\\"/annotations/3495386\\\",\\\"type\\\":\\\"annotation\\\"}},\\\"referents\\\":{\\\"255696\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":255696},\\\"293434\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":293434},\\\"2269422\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":2269422},\\\"2432384\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":2432384},\\\"2432432\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":2432432},\\\"2689751\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":2689751},\\\"2966483\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":2966483},\\\"3082776\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":3082776},\\\"3495386\\\":{\\\"annotations\\\":[3495386],\\\"annotatable\\\":{\\\"url\\\":\\\"https://genius.com/2pac-all-eyez-on-me-lyrics\\\",\\\"type\\\":\\\"song\\\",\\\"title\\\":\\\"All Eyez On Me\\\",\\\"linkTitle\\\":\\\"All Eyez On Me by 2Pac (Ft. Big Syke)\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/51289a294de3498eb6e1ff4e3a769c28.1000x1000x1.png\\\",\\\"id\\\":6576,\\\"context\\\":\\\"2Pac\\\",\\\"clientTimestamps\\\":{\\\"lyricsUpdatedAt\\\":1720296086,\\\"updatedByHumanAt\\\":1720296086},\\\"apiPath\\\":\\\"/songs/6576\\\"},\\\"twitterShareMessage\\\":\\\"“The title track off of 2Pac’s album All Eyez on Me samples Linda Clifford’s “Never Gonna Stop” (…” —@Genius\\\",\\\"trackingPaths\\\":{\\\"concurrent\\\":\\\"/2pac-all-eyez-on-me-lyrics\\\",\\\"aggregate\\\":\\\"/3495386/2pac-all-eyez-on-me/All-eyez-on-me\\\"},\\\"currentUserMetadata\\\":{\\\"relationships\\\":{},\\\"excludedPermissions\\\":[\\\"add_pinned_annotation_to\\\",\\\"add_community_annotation_to\\\"],\\\"permissions\\\":[]},\\\"verifiedAnnotatorIds\\\":[],\\\"url\\\":\\\"https://genius.com/3495386/2pac-all-eyez-on-me/All-eyez-on-me\\\",\\\"songId\\\":6576,\\\"range\\\":{\\\"content\\\":\\\"All Eyez On Me\\\"},\\\"path\\\":\\\"/3495386/2pac-all-eyez-on-me/All-eyez-on-me\\\",\\\"isImage\\\":false,\\\"isDescription\\\":true,\\\"iosAppUrl\\\":\\\"genius://referents/3495386\\\",\\\"id\\\":3495386,\\\"fragment\\\":\\\"All Eyez On Me\\\",\\\"classification\\\":\\\"accepted\\\",\\\"apiPath\\\":\\\"/referents/3495386\\\",\\\"annotatorLogin\\\":\\\"RimmerGawd\\\",\\\"annotatorId\\\":11899,\\\"type\\\":\\\"referent\\\"},\\\"5049714\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":5049714},\\\"9137652\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":9137652},\\\"11537155\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":11537155},\\\"12128274\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":12128274},\\\"12859897\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":12859897},\\\"13839497\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":13839497},\\\"15698084\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":15698084},\\\"18437859\\\":{\\\"classification\\\":\\\"accepted\\\",\\\"editorialState\\\":\\\"accepted\\\",\\\"id\\\":18437859},\\\"18907858\\\":{\\\"classification\\\":\\\"unreviewed\\\",\\\"editorialState\\\":\\\"pending\\\",\\\"id\\\":18907858},\\\"19617841\\\":{\\\"classification\\\":\\\"unreviewed\\\",\\\"editorialState\\\":\\\"pending\\\",\\\"id\\\":19617841},\\\"20748281\\\":{\\\"classification\\\":\\\"unreviewed\\\",\\\"editorialState\\\":\\\"pending\\\",\\\"id\\\":20748281},\\\"20748314\\\":{\\\"classification\\\":\\\"unreviewed\\\",\\\"editorialState\\\":\\\"pending\\\",\\\"id\\\":20748314},\\\"20901107\\\":{\\\"classification\\\":\\\"unreviewed\\\",\\\"editorialState\\\":\\\"pending\\\",\\\"id\\\":20901107},\\\"22058141\\\":{\\\"classification\\\":\\\"unreviewed\\\",\\\"editorialState\\\":\\\"pending\\\",\\\"id\\\":22058141},\\\"24858294\\\":{\\\"classification\\\":\\\"unreviewed\\\",\\\"editorialState\\\":\\\"pending\\\",\\\"id\\\":24858294}},\\\"answers\\\":{\\\"151786\\\":{\\\"authors\\\":[{\\\"user\\\":2954567,\\\"pinnedRole\\\":null,\\\"attribution\\\":1,\\\"type\\\":\\\"user_attribution\\\"}],\\\"answerSource\\\":null,\\\"currentUserMetadata\\\":{\\\"interactions\\\":{\\\"vote\\\":null},\\\"excludedPermissions\\\":[\\\"vote\\\",\\\"edit\\\",\\\"destroy\\\"],\\\"permissions\\\":[]},\\\"votesTotal\\\":20,\\\"id\\\":151786,\\\"hasVoters\\\":true,\\\"editorialState\\\":\\\"normal\\\",\\\"createdAt\\\":1541437537,\\\"bodyForEdit\\\":\\\"Kendrick on the Impact of Tupacs All Eyez on Me and California Love Video Shoot\\\\n\\\\n \\\\\\\"I was 8 yrs old when I first saw you. I couldn’t describe how I felt at that moment. So many emotions. Full of excitement. Full of joy and eagerness. 20 yrs later I understand exactly what that feeling was. INSPIRED,” Lamar wrote (via Pitchfork). “The people that you touched on that small intersection changed lives forever. I told myself I wanted to be a voice for man one day. Whoever knew I was speaking out loud for u to listen. Thank you, K.L.”\\\\n\\\\nAs Lamar told Rolling Stone, he was eight years old when he sat atop his father’s shoulder and witnessed Tupac and Dr. Dre film the video for [“California Love”](https://genius.com/2pac-california-love-lyrics) at the Compton Swap Meet.  “I want to say they were in a white Bentley,” Lamar said. “These motorcycle cops trying to conduct traffic but one almost scraped the car, and Pac stood up on the passenger seat, like, ‘Yo, what the fuck!’ Yelling at the police, just like on his motherfucking songs. He gave us what we wanted.” Lamar would later shoot a scene at that same swap meet for his [“King Kunta”](https://genius.com/Kendrick-lamar-king-kunta-lyrics) music video, a nod to 2Pac.\\\",\\\"body\\\":{\\\"markdown\\\":\\\"Kendrick on the Impact of Tupacs All Eyez on Me and California Love Video Shoot\\\\n\\\\n \\\\\\\"I was 8 yrs old when I first saw you. I couldn’t describe how I felt at that moment. So many emotions. Full of excitement. Full of joy and eagerness. 20 yrs later I understand exactly what that feeling was. INSPIRED,” Lamar wrote (via Pitchfork). “The people that you touched on that small intersection changed lives forever. I told myself I wanted to be a voice for man one day. Whoever knew I was speaking out loud for u to listen. Thank you, K.L.”\\\\n\\\\nAs Lamar told Rolling Stone, he was eight years old when he sat atop his father’s shoulder and witnessed Tupac and Dr. Dre film the video for [“California Love”](https://genius.com/2pac-california-love-lyrics) at the Compton Swap Meet.  “I want to say they were in a white Bentley,” Lamar said. “These motorcycle cops trying to conduct traffic but one almost scraped the car, and Pac stood up on the passenger seat, like, ‘Yo, what the fuck!’ Yelling at the police, just like on his motherfucking songs. He gave us what we wanted.” Lamar would later shoot a scene at that same swap meet for his [“King Kunta”](https://genius.com/Kendrick-lamar-king-kunta-lyrics) music video, a nod to 2Pac.\\\",\\\"html\\\":\\\"<p>Kendrick on the Impact of Tupacs All Eyez on Me and California Love Video Shoot<\\/p>\\\\n\\\\n<p> “I was 8 yrs old when I first saw you. I couldn’t describe how I felt at that moment. So many emotions. Full of excitement. Full of joy and eagerness. 20 yrs later I understand exactly what that feeling was. INSPIRED,” Lamar wrote (via Pitchfork). “The people that you touched on that small intersection changed lives forever. I told myself I wanted to be a voice for man one day. Whoever knew I was speaking out loud for u to listen. Thank you, K.L.”<\\/p>\\\\n\\\\n<p>As Lamar told Rolling Stone, he was eight years old when he sat atop his father’s shoulder and witnessed Tupac and Dr. Dre film the video for <a href=\\\\\\\"https://genius.com/2pac-california-love-lyrics\\\\\\\" rel=\\\\\\\"noopener\\\\\\\" data-api_path=\\\\\\\"/songs/244\\\\\\\">“California Love”<\\/a> at the Compton Swap Meet.  “I want to say they were in a white Bentley,” Lamar said. “These motorcycle cops trying to conduct traffic but one almost scraped the car, and Pac stood up on the passenger seat, like, ‘Yo, what the fuck!’ Yelling at the police, just like on his motherfucking songs. He gave us what we wanted.” Lamar would later shoot a scene at that same swap meet for his <a href=\\\\\\\"https://genius.com/Kendrick-lamar-king-kunta-lyrics\\\\\\\" rel=\\\\\\\"noopener\\\\\\\" data-api_path=\\\\\\\"/songs/721659\\\\\\\">“King Kunta”<\\/a> music video, a nod to 2Pac.<\\/p>\\\"},\\\"type\\\":\\\"answer\\\"}},\\\"questions\\\":{\\\"99350\\\":{\\\"defaultQuestion\\\":{\\\"id\\\":\\\"artist_comment:song:6576\\\",\\\"answerPrefix\\\":null},\\\"author\\\":1662078,\\\"answer\\\":151786,\\\"currentUserMetadata\\\":{\\\"interactions\\\":{\\\"vote\\\":null},\\\"iqByAction\\\":{},\\\"excludedPermissions\\\":[\\\"vote\\\",\\\"add_answer\\\",\\\"update_answer_source\\\",\\\"update\\\",\\\"archive\\\",\\\"pin\\\",\\\"unpin\\\",\\\"move\\\",\\\"moderate\\\",\\\"add_verified_answer\\\",\\\"view_qa_page\\\"],\\\"permissions\\\":[]},\\\"votesTotal\\\":0,\\\"url\\\":\\\"https://genius.com/2pac-all-eyez-on-me-lyrics/questions/99350\\\",\\\"state\\\":\\\"pinned\\\",\\\"pinOrder\\\":65536,\\\"id\\\":99350,\\\"hasVoters\\\":false,\\\"defaultKey\\\":\\\"artist_comment\\\",\\\"createdAt\\\":1501561315,\\\"contributorsCount\\\":2,\\\"body\\\":\\\"What did 2Pac say about \\\\\\\"All Eyez On Me\\\\\\\"?\\\",\\\"bagonUrl\\\":\\\"https://genius.com/2pac-all-eyez-on-me-lyrics/questions/99350\\\",\\\"type\\\":\\\"question\\\"}},\\\"questionAnswers\\\":{\\\"producer:song:6576\\\":{\\\"question\\\":\\\"Who produced “All Eyez On Me” by 2Pac?\\\",\\\"path\\\":\\\"/2pac-all-eyez-on-me-lyrics/q/producer\\\",\\\"id\\\":\\\"producer:song:6576\\\",\\\"answer\\\":\\\"“<a href=\\\\\\\"/2pac-all-eyez-on-me-lyrics\\\\\\\">All Eyez On Me<\\/a>” by <a href=\\\\\\\"/artists/2pac\\\\\\\">2Pac<\\/a> was produced by <a href=\\\\\\\"/artists/Johnny-j\\\\\\\">Johnny J<\\/a>.\\\",\\\"type\\\":\\\"song_producer\\\"},\\\"release-date:song:6576\\\":{\\\"question\\\":\\\"When did 2Pac release “All Eyez On Me”?\\\",\\\"path\\\":\\\"/2pac-all-eyez-on-me-lyrics/q/release-date\\\",\\\"id\\\":\\\"release-date:song:6576\\\",\\\"answer\\\":\\\"<a href=\\\\\\\"/artists/2pac\\\\\\\">2Pac<\\/a> released “<a href=\\\\\\\"/2pac-all-eyez-on-me-lyrics\\\\\\\">All Eyez On Me<\\/a>” on February 13, 1996.\\\",\\\"type\\\":\\\"song_release_date\\\"},\\\"writer:song:6576\\\":{\\\"question\\\":\\\"Who wrote “All Eyez On Me” by 2Pac?\\\",\\\"path\\\":\\\"/2pac-all-eyez-on-me-lyrics/q/writer\\\",\\\"id\\\":\\\"writer:song:6576\\\",\\\"answer\\\":\\\"“<a href=\\\\\\\"/2pac-all-eyez-on-me-lyrics\\\\\\\">All Eyez On Me<\\/a>” by <a href=\\\\\\\"/artists/2pac\\\\\\\">2Pac<\\/a> was written by <a href=\\\\\\\"/artists/Big-syke\\\\\\\">Big Syke<\\/a>, <a href=\\\\\\\"/artists/2pac\\\\\\\">2Pac<\\/a>, <a href=\\\\\\\"/artists/Johnny-j\\\\\\\">Johnny J<\\/a>, <a href=\\\\\\\"/artists/Jp-pennington\\\\\\\">J.P. Pennington<\\/a>, <a href=\\\\\\\"/artists/Thor-baldursson\\\\\\\">Thor Baldursson<\\/a> & <a href=\\\\\\\"/artists/Jurgen-koppers\\\\\\\">Jürgen Koppers<\\/a>.\\\",\\\"type\\\":\\\"song_writer\\\"}}}}');\n      window.__APP_CONFIG__ = {\"env\":\"production\",\"api_root_url\":\"/api\",\"microsite_label\":\"\",\"microsite_url\":\"\",\"transform_domain\":\"transform.genius.com\",\"facebook_app_id\":\"265539304824\",\"facebook_opengraph_api_version\":\"8.0\",\"pusher_app_key\":\"6d893fcc6a0c695853ac\",\"embedly_key\":\"fc778e44915911e088ae4040f9f86dcd\",\"a9_pub_id\":\"3459\",\"app_store_url\":\"https://itunes.apple.com/us/app/genius-by-rap-genius-search/id709482991?ls=1&mt=8\",\"play_store_url\":\"https://play.google.com/store/apps/details?id=com.genius.android\",\"soundcloud_client_id\":\"632c544d1c382f82526f369877aab5c0\",\"annotator_context_length\":32,\"comment_reasons\":[{\"_type\":\"comment_reason\",\"context_url\":\"https://genius.com/8846441/Genius-how-genius-works/More-on-annotations\",\"display_character\":\"R\",\"handle\":\"Restates the line\",\"id\":1,\"name\":\"restates-the-line\",\"raw_name\":\"restates the line\",\"requires_body\":false,\"slug\":\"restates_the_line\"},{\"_type\":\"comment_reason\",\"context_url\":\"https://genius.com/8846441/Genius-how-genius-works/More-on-annotations\",\"display_character\":\"S\",\"handle\":\"It’s a stretch\",\"id\":2,\"name\":\"its-a-stretch\",\"raw_name\":\"it’s a stretch\",\"requires_body\":false,\"slug\":\"its_a_stretch\"},{\"_type\":\"comment_reason\",\"context_url\":\"https://genius.com/8846441/Genius-how-genius-works/More-on-annotations\",\"display_character\":\"M\",\"handle\":\"Missing something\",\"id\":3,\"name\":\"missing-something\",\"raw_name\":\"missing something\",\"requires_body\":false,\"slug\":\"missing_something\"},{\"_type\":\"comment_reason\",\"context_url\":\"https://genius.com/8846441/Genius-how-genius-works/More-on-annotations\",\"display_character\":\"…\",\"handle\":\"Other\",\"id\":4,\"name\":\"other\",\"raw_name\":\"other\",\"requires_body\":true,\"slug\":\"other\"}],\"comment_reasons_help_url\":\"https://genius.com/8846441/Genius-how-genius-works/More-on-annotations\",\"filepicker_api_key\":\"Ar03MDs73TQm241ZgLwfjz\",\"filepicker_policy\":\"eyJleHBpcnkiOjIzNTEwOTE1NTgsImNhbGwiOlsicGljayIsInJlYWQiLCJzdG9yZSIsInN0YXQiLCJjb252ZXJ0Il19\",\"filepicker_signature\":\"68597b455e6c09bce0bfd73f758e299c95d49a5d5c8e808aaf4877da7801c4da\",\"filepicker_s3_image_bucket\":\"filepicker-images-rapgenius\",\"filepicker_cdn_domain\":\"filepicker-images.genius.com\",\"available_roles\":[\"moderator\",\"mega_boss\",\"in_house_staff\",\"verified_artist\",\"meme_artist\",\"engineer\",\"editor\",\"educator\",\"staff\",\"whitehat\",\"tech_liaison\",\"mediator\",\"transcriber\"],\"canonical_domain\":\"genius.com\",\"enable_angular_debug\":false,\"fact_track_launch_article_url\":\"https://genius.com/a/genius-and-spotify-together\",\"user_authority_roles\":[\"moderator\",\"editor\",\"mediator\",\"transcriber\"],\"user_verification_roles\":[\"community_artist\",\"verified_artist\",\"meme_artist\"],\"user_vote_types_for_delete\":[\"votes\",\"upvotes\",\"downvotes\"],\"brightcove_account_id\":\"4863540648001\",\"mixpanel_delayed_events_timeout\":\"86400\",\"unreviewed_annotation_tooltip_info_url\":\"https://genius.com/8846524/Genius-how-genius-works/More-on-editorial-review\",\"community_policy_and_moderation_guidelines\":\"https://genius.com/Genius-community-policy-and-moderation-guidelines-annotated\",\"video_placements\":{\"desktop_song_page\":[{\"name\":\"sidebar\",\"min_relevance\":\"high\",\"fringe_min_relevance\":\"low\",\"max_videos\":0},{\"name\":\"sidebar_thumb\",\"min_relevance\":\"medium\",\"fringe_min_relevance\":\"low\",\"max_videos\":0},{\"name\":\"recirculated\",\"min_relevance\":\"low\",\"max_videos\":3}],\"mobile_song_page\":[{\"name\":\"footer\",\"min_relevance\":\"medium\",\"max_videos\":1},{\"name\":\"recirculated\",\"min_relevance\":\"low\",\"max_videos\":3}],\"desktop_artist_page\":[{\"name\":\"sidebar\",\"min_relevance\":\"medium\",\"fringe_min_relevance\":\"low\",\"max_videos\":2}],\"mobile_artist_page\":[{\"name\":\"carousel\",\"min_relevance\":\"medium\",\"fringe_min_relevance\":\"low\",\"max_videos\":5}],\"amp_song_page\":[{\"name\":\"footer\",\"min_relevance\":\"medium\",\"max_videos\":1}],\"amp_video_page\":[{\"name\":\"related\",\"min_relevance\":\"low\",\"max_videos\":8}],\"desktop_video_page\":[{\"name\":\"series_related\",\"min_relevance\":\"low\",\"max_videos\":8,\"series\":true},{\"name\":\"related\",\"min_relevance\":\"low\",\"max_videos\":8}],\"desktop_article_page\":[{\"name\":\"carousel\",\"min_relevance\":\"low\",\"max_videos\":5}],\"mobile_article_page\":[{\"name\":\"carousel\",\"min_relevance\":\"low\",\"max_videos\":5}],\"desktop_album_page\":[{\"name\":\"sidebar\",\"min_relevance\":\"medium\",\"fringe_min_relevance\":\"low\",\"max_videos\":1}],\"amp_album_page\":[{\"name\":\"carousel\",\"min_relevance\":\"low\",\"max_videos\":5}]},\"app_name\":\"rapgenius-cedar\",\"vttp_parner_id\":\"719c82b0-266e-11e7-827d-7f7dc47f6bc0\",\"default_cover_art_url\":\"https://assets.genius.com/images/default_cover_art.png?1720540228\",\"sizies_base_url\":\"https://t2.genius.com/unsafe\",\"max_line_item_event_count\":10,\"dmp_match_threshold\":0.05,\"ab_tests_version\":\"e36f65f4354837405d11261b86f084e8be42d16036a0b7061f863f47135e4f0c\",\"external_song_match_purposes\":[\"streaming_service_lyrics\",\"streaming_service_player\"],\"release_version\":\"Production f6629f67\",\"react_bugsnag_api_key\":\"a3ab84a89baa4ee509c9e3f71b9296e0\",\"mixpanel_token\":\"77967c52dc38186cc1aadebdd19e2a82\",\"mixpanel_enabled\":true,\"get_involved_page_url\":\"https://genius.com/Genius-getting-involved-with-genius-projects-annotated\",\"solidarity_text\":\"\",\"solidarity_url\":\"http://so.genius.com/aroYTdx\",\"track_gdpr_banner_shown_event\":true,\"show_cmp_modal\":true,\"recaptcha_v3_site_key\":\"6LewuscaAAAAABNevDiTNPHrKCs8viRjfPnm6xc6\",\"ga_web_vitals_sampling_percentage\":\"10\",\"mixpanel_web_vitals_sampling_percentage\":{\"mobile\":\"5\",\"desktop\":\"10\"},\"top_level_block_containers\":[\"address\",\"article\",\"aside\",\"blockquote\",\"div\",\"dl\",\"fieldset\",\"footer\",\"form\",\"h1\",\"h2\",\"h3\",\"h4\",\"h5\",\"h6\",\"header\",\"menu\",\"nav\",\"ol\",\"p\",\"pre\",\"section\",\"table\",\"ul\"],\"top_level_standalone_blocks\":[\"img\",\"hr\"],\"zendesk_ccpa_link\":\"https://support.genius.com/hc/en-us/requests/new\",\"artist_promo_portal_launched\":true,\"transcriber_guide_url\":\"https://genius.com/25045402/Genius-what-is-a-transcriber/Mark-lyrics-complete\",\"recommended_content_unit_enabled\":true,\"tmg_livestreams_index_public\":false,\"fingerprint_api_key\":\"dEe6R1Hyzg87t38keh0R\",\"song_derivation_types\":{\"sample\":{\"upstream\":\"samples\",\"downstream\":\"sampled_in\",\"route_name\":\"samples\"},\"interpolation\":{\"upstream\":\"interpolates\",\"downstream\":\"interpolated_by\",\"route_name\":\"interpolations\"},\"cover\":{\"upstream\":\"cover_of\",\"downstream\":\"covered_by\",\"route_name\":\"covers\"},\"remix\":{\"upstream\":\"remix_of\",\"downstream\":\"remixed_by\",\"route_name\":\"remixes\"},\"live\":{\"upstream\":\"live_version_of\",\"downstream\":\"performed_live_as\",\"route_name\":\"live-versions\"},\"translation\":{\"upstream\":\"translation_of\",\"downstream\":\"translations\",\"route_name\":\"translations\"}},\"primis_demand_test_fraction\":0.0,\"outbrain_rollout_percentage\":100,\"stubhub_url_proxy\":\"https://stubhub-proxy.genius.com/stubhub\",\"viagogo_url_proxy\":\"https://stubhub-proxy.genius.com/viagogo\",\"stubhub_partnerize_url\":\"https://stubhub.prf.hn/click/camref:1100lqTK8\",\"viagogo_partnerize_url\":\"https://viagogo.prf.hn/click/camref:1101l3DfTB\",\"ga4_measurement_ids\":[\"G-BJ6QSCFYD0\",\"G-JRDWPGGXWW\"],\"tonefuse_player_on_windows_threshold\":100,\"tonefuse_player_on_windows_enabled_countries\":[\"US\",\"AT\",\"AU\",\"BR\",\"CA\",\"DE\",\"ES\",\"FR\",\"IT\",\"JP\",\"NZ\",\"GB\"],\"tonefuse_interstitial_threshold\":0,\"brightcove_mobile_thumbnail_web_player_id\":\"SyGQSOxol\",\"brightcove_modal_web_player_id\":\"S1LI5bh0\",\"brightcove_song_story_web_player_id\":\"SkfSovRVf\",\"brightcove_standard_web_player_id\":\"S1ZcmcOC1x\",\"brightcove_standard_no_autoplay_web_player_id\":\"ByRtIUBvx\",\"brightcove_sitemap_player_id\":\"BJfoOE1ol\"};\n      window.__IQ_BY_EVENT_TYPE__ = {\"accepted_a_lyrics_edit\":3.0,\"annotation_downvote_by_contrib\":-1.0,\"annotation_downvote_by_default\":-1.0,\"annotation_downvote_by_editor\":-1.0,\"annotation_downvote_by_high_iq_user\":-1.0,\"annotation_downvote_by_moderator\":-1.0,\"annotation_upvote_by_contrib\":4.0,\"annotation_upvote_by_default\":2.0,\"annotation_upvote_by_editor\":6.0,\"annotation_upvote_by_high_iq_user\":4.0,\"annotation_upvote_by_moderator\":10.0,\"answer_downvote_by_contrib\":-1.0,\"answer_downvote_by_default\":-1.0,\"answer_downvote_by_editor\":-1.0,\"answer_downvote_by_high_iq_user\":-1.0,\"answer_downvote_by_moderator\":-1.0,\"answered_a_question\":5.0,\"answered_a_question_meme\":25.0,\"answered_a_question_verified\":10.0,\"answer_upvote_by_contrib\":4.0,\"answer_upvote_by_default\":2.0,\"answer_upvote_by_editor\":6.0,\"answer_upvote_by_high_iq_user\":4.0,\"answer_upvote_by_moderator\":10.0,\"archived_a_question\":1.0,\"article_downvote\":-1.0,\"article_upvote\":1.0,\"asked_a_question\":1.0,\"auto_accepted_explanation\":15.0,\"comment_downvote\":-0.5,\"comment_upvote\":0.5,\"created_a_lyrics_edit\":2.0,\"created_a_real_high_priority_song\":60.0,\"created_a_real_song\":30.0,\"created_a_song\":5.0,\"forum_post_downvote\":-0.5,\"forum_post_upvote\":0.5,\"historical_you_published_a_song\":60.0,\"metadata_update_or_addition\":2.0,\"pending_explanation\":5.0,\"pinned_a_question_not_your_own\":2.0,\"question_downvote\":-2.0,\"question_upvote\":2.0,\"rejected_a_lyrics_edit\":2.0,\"song_metadata_update_or_addition\":2.0,\"song_pageviews_1000\":25.0,\"song_pageviews_10000\":50.0,\"song_pageviews_100000\":125.0,\"song_pageviews_1000000\":500.0,\"song_pageviews_2500\":30.0,\"song_pageviews_25000\":75.0,\"song_pageviews_250000\":150.0,\"song_pageviews_2500000\":1000.0,\"song_pageviews_500\":20.0,\"song_pageviews_5000\":35.0,\"song_pageviews_50000\":100.0,\"song_pageviews_500000\":200.0,\"song_pageviews_5000000\":2000.0,\"suggestion_downvote_by_contrib\":-1.0,\"suggestion_downvote_by_default\":-0.5,\"suggestion_downvote_by_editor\":-1.0,\"suggestion_downvote_by_high_iq_user\":-1.0,\"suggestion_downvote_by_moderator\":-1.0,\"suggestion_upvote_by_contrib\":2.0,\"suggestion_upvote_by_default\":1.0,\"suggestion_upvote_by_editor\":3.0,\"suggestion_upvote_by_high_iq_user\":2.0,\"suggestion_upvote_by_moderator\":4.0,\"verified_explanation_by_meme\":100.0,\"verified_explanation_by_non_meme\":15.0,\"verified_lyrics_by_meme_featured\":50.0,\"verified_lyrics_by_meme_primary\":75.0,\"verified_lyrics_by_meme_writer\":75.0,\"verified_lyrics_by_non_meme_featured\":10.0,\"verified_lyrics_by_non_meme_primary\":15.0,\"verified_lyrics_by_non_meme_writer\":15.0,\"you_accepted_a_comment\":6.0,\"you_accepted_an_annotation\":10.0,\"you_added_a_photo\":100.0,\"you_archived_a_comment\":2.0,\"you_contributed_to_a_marked_complete_song\":10.0,\"you_contributed_to_a_recent_marked_complete_song\":20.0,\"you_deleted_an_annotation\":5.0,\"you_incorporated_an_annotation\":5.0,\"you_integrated_a_comment\":2.0,\"you_linked_an_identity\":100.0,\"you_marked_a_song_complete\":10.0,\"you_merged_an_annotation_edit\":4.0,\"you_published_a_song\":5.0,\"your_annotation_accepted\":10.0,\"your_annotation_edit_merged\":5.0,\"your_annotation_edit_rejected\":-0.5,\"your_annotation_incorporated\":15.0,\"your_annotation_rejected\":0.0,\"your_annotation_was_cosigned_by_community_verified\":2.0,\"your_annotation_was_cosigned_by_meme\":50.0,\"your_annotation_was_cosigned_by_verified_verified\":20.0,\"your_answer_cleared\":-5.0,\"your_answer_pinned\":5.0,\"your_comment_accepted\":2.0,\"your_comment_archived\":0.0,\"your_comment_integrated\":2.0,\"your_comment_rejected\":-0.5,\"you_rejected_a_comment\":2.0,\"you_rejected_an_annotation\":2.0,\"you_rejected_an_annotation_edit\":2.0,\"your_lyrics_edit_accepted\":3.0,\"your_lyrics_edit_rejected\":-2.0,\"your_question_answered\":4.0,\"your_question_archived\":-1.0,\"your_question_pinned\":5.0};\n    </script>\n    \n  <script type=\"text/javascript\">_qevents.push({ qacct: \"p-f3CPQ6vHckedE\"});</script>\n<noscript>\n  <div style=\"display: none;\">\n    <img src=\"http://pixel.quantserve.com/pixel/p-f3CPQ6vHckedE.gif\" height=\"1\" width=\"1\" alt=\"Quantcast\"/>\n  </div>\n</noscript>\n\n  \n\n<script type=\"text/javascript\">\n\n  var _sf_async_config={};\n\n  _sf_async_config.uid = 3877;\n  _sf_async_config.domain = 'genius.com';\n  _sf_async_config.title = '2Pac – All Eyez On Me Lyrics | Genius Lyrics';\n  _sf_async_config.sections = 'songs,tag:rap';\n  _sf_async_config.authors = '2Pac,Big Syke';\n\n  var _cbq = window._cbq || [];\n\n  (function(){\n    function loadChartbeat() {\n      window._sf_endpt=(new Date()).getTime();\n      var e = document.createElement('script');\n      e.setAttribute('language', 'javascript');\n      e.setAttribute('type', 'text/javascript');\n      e.setAttribute('src', 'https://static.chartbeat.com/js/chartbeat.js');\n      document.body.appendChild(e);\n    }\n    var oldonload = window.onload;\n    window.onload = (typeof window.onload != 'function') ?\n       loadChartbeat : function() { oldonload(); loadChartbeat(); };\n  })();\n</script>\n\n  <!-- Begin comScore Tag -->\n<script>\n  var _comscore = _comscore || [];\n  _comscore.push({\n    c1: \"2\", c2: \"22489583\",\n    options: {\n      enableFirstPartyCookie: true,\n    }\n  });\n  (function() {\n    var s = document.createElement(\"script\"), el = document.getElementsByTagName(\"script\")[0]; s.async = true;\n    s.src = \"https://sb.scorecardresearch.com/cs/22489583/beacon.js\"\n    el.parentNode.insertBefore(s, el);\n  })();\n</script>\n<noscript>\n  <img src=\"http://b.scorecardresearch.com/p?c1=2&c2=22489583&cv=2.0&cj=1\" />\n</noscript>\n<!-- End comScore Tag -->\n\n  <script>\n  !function(f,b,e,v,n,t,s)\n  {if(f.fbq)return;n=f.fbq=function(){n.callMethod?\n  n.callMethod.apply(n,arguments):n.queue.push(arguments)};\n  if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';\n  n.queue=[];t=b.createElement(e);t.async=!0;\n  t.src=v;s=b.getElementsByTagName(e)[0];\n  s.parentNode.insertBefore(t,s)}(window, document,'script',\n  'https://connect.facebook.net/en_US/fbevents.js');\n  fbq('init', '201983886890479');\n  fbq('track', 'PageView');\n</script>\n<noscript><img height=\"1\" width=\"1\" style=\"display:none\"\n  src=\"https://www.facebook.com/tr?id=201983886890479&ev=PageView&noscript=1\"\n/></noscript>\n\n  \n    <script>\n  !function(a,l,b,c,k,s,t,g,A){a.CustomerConnectAnalytics=k,a[k]=a[k]||function(){\n  (a[k].q=a[k].q||[]).push(arguments)},g=l.createElement(b),A=l.getElementsByTagName(b)[0],\n  g.type=\"text/javascript\",g.async=!0,g.src=c+\"?id=\"+s+\"&parentId=\"+t,A.parentNode.insertBefore(g,A)\n  }(window,document,\"script\",\"//carbon-cdn.ccgateway.net/script\",\"cca\",window.location.hostname, \"e8a16a4090\");\n\n  window.googletag = window.googletag || {};\n  window.googletag.cmd = window.googletag.cmd || [];\n  window.googletag.cmd.push(function () {\n    if (googletag.pubads().getTargeting('carbon_segment').length === 0) {\n      const carbon = JSON.parse(window.localStorage.getItem('ccRealtimeData'));\n      googletag.pubads().setTargeting('carbon_segment', carbon ? carbon.audiences.map(function (i) { return i.id; }) : []);\n    }\n    if (googletag.pubads().getTargeting('cc-iab-class-id').length === 0) {\n      const iabIds = JSON.parse(window.localStorage.getItem('ccContextualData'));\n      if (iabIds) {\n        googletag.pubads().setTargeting('cc-iab-class-id', iabIds);\n      }\n    }\n  });\n</script>\n\n  \n\n\n  </body>\n</html>\n"
  },
  {
    "path": "test/rsrc/lyrics/geniuscom/Ttngchinchillalyrics.txt",
    "content": "<!doctype html>\n<html>\n  <head>\n    <title>TTNG – Chinchilla Lyrics | Genius Lyrics</title>\n\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n<meta content='width=device-width,initial-scale=1' name='viewport'>\n\n  <meta name=\"apple-itunes-app\" content=\"app-id=709482991\">\n\n<link href=\"https://assets.genius.com/images/apple-touch-icon.png?1649950983\" rel=\"apple-touch-icon\" />\n\n\n  \n\n  <link href=\"https://assets.genius.com/images/apple-touch-icon.png?1649950983\" rel=\"apple-touch-icon\" />\n\n  \n\n  <!-- Mobile IE allows us to activate ClearType technology for smoothing fonts for easy reading -->\n  <meta http-equiv=\"cleartype\" content=\"on\">\n\n\n\n\n<META name=\"y_key\" content=\"f63347d284f184b0\">\n\n<meta property=\"og:site_name\" content=\"Genius\"/>\n<meta property=\"fb:app_id\" content=\"265539304824\" />\n<meta property=\"fb:pages\" content=\"308252472676410\" />\n\n<link title=\"Genius\" type=\"application/opensearchdescription+xml\" rel=\"search\" href=\"https://genius.com/opensearch.xml\">\n\n<script>\n!function(){if('PerformanceLongTaskTiming' in window){var g=window.__tti={e:[]};\ng.o=new PerformanceObserver(function(l){g.e=g.e.concat(l.getEntries())});\ng.o.observe({entryTypes:['longtask']})}}();\n</script>\n\n\n    <!--sse-->\n\n\n\n<!--/sse-->\n\n    \n      <link as=\"script\" href=\"https://assets.genius.com/javascripts/compiled/reactSongClient.desktop-7698f7b44bc9e1f4ee60.js\" rel=\"preload\" /><script defer=\"true\" src=\"https://assets.genius.com/javascripts/compiled/reactSongClient.desktop-7698f7b44bc9e1f4ee60.js\" type=\"text/javascript\"></script>\n    \n      <link as=\"script\" href=\"https://assets.genius.com/javascripts/compiled/reactVendors.desktop-2bf820d170c192c56e46.js\" rel=\"preload\" /><script defer=\"true\" src=\"https://assets.genius.com/javascripts/compiled/reactVendors.desktop-2bf820d170c192c56e46.js\" type=\"text/javascript\"></script>\n    \n      <link as=\"script\" href=\"https://assets.genius.com/javascripts/compiled/reactPageVendors.desktop-e0becb1b07a96c17bc5d.js\" rel=\"preload\" /><script defer=\"true\" src=\"https://assets.genius.com/javascripts/compiled/reactPageVendors.desktop-e0becb1b07a96c17bc5d.js\" type=\"text/javascript\"></script>\n    \n      <link as=\"script\" href=\"https://assets.genius.com/javascripts/compiled/reactPage.desktop-202f836f1fe655679354.js\" rel=\"preload\" /><script defer=\"true\" src=\"https://assets.genius.com/javascripts/compiled/reactPage.desktop-202f836f1fe655679354.js\" type=\"text/javascript\"></script>\n    \n    <style>\n  @font-face {\n    font-family: 'Programme';\n    src: url(https://assets.genius.com/fonts/programme_bold.woff2?1649950983) format('woff2'),\n      url(https://assets.genius.com/fonts/programme_bold.woff?1649950983) format('woff');\n    font-style: normal;\n    font-weight: bold;\n  }\n\n  @font-face {\n    font-family: 'Programme';\n    src: url(https://assets.genius.com/fonts/programme_normal.woff2?1649950983) format('woff2'),\n      url(https://assets.genius.com/fonts/programme_normal.woff?1649950983) format('woff');\n    font-style: normal;\n    font-weight: normal;\n  }\n\n  @font-face {\n    font-family: 'Programme';\n    src: url(https://assets.genius.com/fonts/programme_normal_italic.woff2?1649950983) format('woff2'),\n      url(https://assets.genius.com/fonts/programme_normal_italic.woff?1649950983) format('woff');\n    font-style: italic;\n    font-weight: normal;\n  }\n\n  @font-face {\n    font-family: 'Programme';\n    src: url(data:font/woff2;base64,d09GMgABAAAAAGIkAA8AAAABbawAAGHBAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP0ZGVE0cGnIbgd5IHMseBmAAhy4RCAqC3EiCg2cLiQYAATYCJAOSCAQgBYkwB6QbW707cQI97QZ47rYBBC/8WhYzVqhu6VDubhWCgGNbwI1xGpxHACuKn87+//+0ZGMMA7Q7FGymW1v/AkWAQqGGO11AOFuXiBxxvhDzmnG9luyI2O+AdIlxgrLWwDx1C1mhPUzgk4bXywJCAMxb0qXBK26YQFgo6OZKRSh+u3XTImklaZFuUbZ9yf5d6mqK+qyWMJvAosriZz3qT/nh4lELatt+nGGPaRHFgHnDTJQvn1/pZb/DfvS/hn4bqJSCAq8Ydh5tGRi7DEpEGyvOenkC1uL3Znb3RL85eKNpVOtiEiJeCZVWxUogqtwRbdru3MEhekE8xyEaPARLAkSldSqmqaWvpjWxSEXT+vA0t3/EgaIoUe3GRuSCxS1vGwu2wahJjlGjDUCFVkRi+NlEUUpFjPwGNipmYCUqVvP/H/e8rn3uJwhHNJyZJFOQAlWttQLom37Or9ALYBumEn/Kz4fOarTdPUAaf3Kn7iTLFNyXdydcjWonC9w/3epXE8IDLxqKak3Kc6tvAgSNDYQvKIMDxJySTNYL4J+wbbfB52I0GpoYtb3M+l/mwUggxlZ2T682CGCu3fbqgpnWQiiJ7veXEd8A/wIBJ8Yl0cLybnV5SfEtzsL/6nz/tMOydNn+NBDgA6S82GEDieBKil04m3MMCghd2aqmQm3Kd0C6cCXlRpfZl//Q7b7twPqKbVZgUTDU/SWUbkAnp2ETpKLdSf1kEdref02tqm5poKrbnt01DbEhwOwPLX1FbVm25UTD0Nq9ScmNyRDiCQAIhuHdlxjsiG37h2RtPMMytTez5YPhsYRhu4VmAwIYCfx7Va0WME2HrE3yZXluuCkWjZ0vxQA+EBCBTwgkIBIMyrY1lulASc6bBAKgICgsSIq084aYFDYFh435cg6VVhdDtbPlVrfbhSu6FOrmuuqK/qpQFPXB//+v9WZrB6vDXCvqy14RBq34fRpvvU8drv9CVAl3h/qHpipAakih0p0eQFAgUY0wI9zIkXLcGD3Kxn9aq7Tbt9d37gKko+wBSrPzquvPNtR0CGE+VPN2AEGxjo6MVbEGUAb+01P78oEuDwtYALvMLgMq4EjG87ET5J7dhwpQwwKkCQ8nhPu/ae29U/tufu7oCoxCGOQkVk1+CbtTEkpyvUhaH/qsy67i4ZClSWzzBo1tI2pHSYFr4UNCloheRNrtq5cYVnvt3tNbXkUkL0gQGUIY7Pkp02svpP4306eFPfr27tRZZ9WpUTVGREREjKh5//9f77FvfYMLupE4UWi/is+8G9e0YPeww0mRrYLFaoeP70U0N41p1bLXcjt+z3YbIFgqICAoICbzEG3OzbXmJiEjTeJxmW848W8mBOL77/UCxBd/bweIr98DWxGGbCCvQEqKwIcJCAChJbB2tXQ9HBWc2SkBmZMqOAuzCGRRFsNZnCUgS9JBJ6+mi25ebwTcRJOgqWZgJKSQQFZatYE77nr+Hklee9rIkQEJcUghxeLc+shNAtiP298JYD9vPCeA/bnxkgD21+2nBLDQIYUBQqLfZ3EBsAij7T4WKTMbRABVI0smQqpwxi7zXW7fweFj8sybxsH/fxjBID5ggAVhICBUmShJglRiUQ5xA04jBMEm4mDcSId1J1eINwWifSuCp6JCUKoMUYUqJIutVadXENgJE9BOmYIxYQbWWXNwJi3Au2AJwSUriK5YQ3LNBrIbtlDcsoPqjj009xygu+8Iw0MnmB47w/LUBbbnrnC8dIPrtTs8Dx7wPXki8OKF0Js3Ih8+iH35IiH9P/1U3x9/NPQv2oEONdEz1gwy1cLQUqvZtlaa42iVee7amKikZ0bMwIKckRU1Ext6ZnbMLBzYWTlxs3HhZ+cmzMFDnJOXNBcf9dz8NPMI0M4rSDcflH5+wYYFYI0LCndUSKSTwqLlReCcF0NAKC4OqQQKSikwuDQ6ehlMzLJEpLVIklSnZMl1SZVat1z17dOoqeuptHYzvY5uZ3OkqRZtKvRzp8rfOxfGpBzgYE0poLC2fOBjXYUgxPpKQIoNlZeOqYoqwlRVVczVVBO2uppiqaXeOOtvaUIqbObEzYiE/N7XPBBqap741p7olX4f7AZmGDiUD0uGhmHlsGXYM7wc/cb0qBxTq8bKsXpcOR7ElFfGh1PwlJ6Eq9wkn0ancT0xW2Y+sw2z3/PQeft8w0KfZ1pYF+5FYBFdpMC0DibFIZFhXZiYMUngjcWoNPHL0ogvW+qWzGB7ZF+mln2XWWO1j1u2Rws9qpZ9yx3LEysDXnTFrBKr6lXX6trCA5tZ/V17rH1j+xrLE15YP1y/noJxPtfXtge2t3Z2u/IdfcHb8Xdpu4JJ+KxlV7pr2HvtY7xT7GMX0WnZnkR2zaHTRu2H7Uv3hSm/Hh3lK/kJ/k8DEu6juqqw7MsQJ1z78n8a7EqOeuXZMA3lsux7NKFa27D3qNrh2jNyrLkvP0cL6KtpNx2ja8UUcYnY1u5pJ+kz9E2p8pV8zLtAzJuCj9tBzJamvovQ4yle68Lp+IecQXKII2LZfjyDL1B7iZ0F7uNtL30tOd7WHa46xfETeZ0Kg8KO5+EaoJCyEnlgtjIPNqEwnRWhawv14QqEFUUp9oDsEdYeAWHnAhaQNZULzFp3yRvL2bgwJcRozGrASXFzbhjU+lvVKXSstKxnNp1tp1TzwIlHhvYm09EMgwBkG+RSeaKIyXNNLkgnwlB82LTxRoeSB5ZcK7gt2o6vlNoL9sp/LgdJTdFy7Jp0LlOTow7VEwnwxoZ4cq8xQqyIQt1K27pVGnhg4pGOPXwGGoiykTwcgQ/L4qa1N0JHeWe0KwOW5SCylKsSvCMoDzf4cNVI8lUbGrpxutDk1tti8WXUKPuYTlyWdTAaSvHarCsIYRVkTkVTuTGj/7RWqTAlA7wz92Pmt8bVNNlhe6w71xNNwwB8WpboqJHyAbpyGHyCHGZkG2nPM6Zds4ksKpLQVPrZ3gcPQfOdBdS8tzG2YkqYDJZv8VRRzfOUlnawrTtenASWcH2Snl21R0EPONzzaTIQk+adGRuo29zvgwJtsYpXTOvMxrb29+yQSk0PaB7hPSsCD70ioVJeAlAIrSQWoedqeQKhdyAC1ZiAbAh18DjI4xIuXiKtm7r9PdRdzu6o7kyt3mJwvcfBOtXWGIfSazSn1rTfozH//F3B16OKHx5IqVjh4Zce6Hgk9exi195yNYNafi9xmeQfm/zR8JdE6kAJphm5iBJn1GcVcxLctKTcbOdu6+PUF6l55/dMwbMSW781+m9/h1Zqe6Dtkdizo2qqeqJ0OMM4kwlcZVHZ4lwiz6RX9Euq7agx6AQM6/dWwbES2eGYaRJSIq24hEslHig7QRO/5dTHwRdBQbNiWadKlgcSj9iDqHncdizRPPh96p9/K0QWtZW6fxf+/7dNsA1Kdf9B657dr2ZYz3WS0UpipMeASRcWLCseo0odj/pn8em4DCXNqGFVuiE1Ky5IiifWsR2RC4Y/fNNeczqZoQzwbGWevFfiiOZzNS8gE1k4lb5IyIqO05BYdTDX9DleEnyJfnoL8E4l9yp1PWB7RJYeNLIjtnYnsEAVJoP3DyJHZbXyEtPCGwVLeq03KWhf2lbKGlIKCRDfgDFPFFz3SiCj862oG3w08OidZkoTXn2zvXg6yICy0TxNRPxc8YU6xR/p63HS2N13R6lV6ynAKFf/MRl04FjhO+C0KJ122ZU8PS5CSKJYLPa19tCyLtyQmB1Lqq0nKbrfVu01RsLn/mLojsU7Z+yaIvY9Gsa7Gr+SCn5zttE8vJY3ME3c3RlBRoVkzVelQB0N9bhThSQcP3dlUT8waDQ3NqvfqZZLianAG+wpOftUhRI4YolfGCQEBWsJrwMIG93Q+eVMXEe7gfaGvk+QfqF5gZ7SYJ+SfWNG0E/76G8SCPhmhLw4IutKHnJpo0lttrhrb8Stn9R+ydipqi7ZjkFTtQ8tQv9n2WXpdKY64HIUoWoiisKzYJUl1aTVCHykP5CPZqeHEGhiqocbbDCPzuy4WJQqIlUkppIKsKqUMU7WGFWqNKjSqF6Tes2qtCg3nMcIw0zgMkXAYmYf07FClR7MiGJNkt9U+bWZVjdqr/TutfK+cK8Hc8aofoEs87EsqGrBLAsrXoQFSUrNii9LnDYqoEmkHuFAE1F6mJhOSqqlxwU+vADeAgGWF8oL14oQRHFiGTgtCgMm0Uh0EoPHJOKThKQEhpSTKEgSyIBkXj5ruWCFoE5QL2gQNAqaeM28Dk4Xr5un5vXw1vJ6eesZI4xdpBOCU4IJwVneJO8C7xLvGuch7zHvKe8Z0XN+anznMJNmZoYFVsXWZE7aXHDDI/IS8in2S9Bmio5itOEMXzeCGykzyjLYjL7DtNRKWsrWeVoXbGi5EceW6N6SvcttxvmYfib7gvQr4QdW/WSQc6h5Q2N2avRAfA4a82vhzZu3g9kzDjjiZIEUBP0JGZKwsdLJMMkyGUFbG10dJGQoCiIKEilWQRlRWSGNKCMUWEyqh2Ha+OrA1omuh2HFiGqYiABFSERIIswqX8ipywjWC2xoEz6sbha3UDgb9cIPJ2enEeixeDVshPcjFem/+Q9vOb2gnvpIKr9U7Z1wDmtzruSQzLKgj0Ykg5JFGPFY5SEQNyYHVsveTn+l9tLbHsVIxEiiHKnuGSWykqqINRiNNNHsWnSUQieo0/1YK2KM34O+NtFeZ5wO7jrpi5oVG8HXClPg6Em0Uc76uTyRD1JAMcVvplgwlWnFMyyaZ1ujCZh0FLrYsyZqiyH2QePW+f73qr0KjVrqrGakRuuF6kxH/ObqsknjApdUuz0kHttTCp6fxdpCsKfZRnLQvdSeMmc2ymx6GGAwGnJjJDpru3NM2tub7p8t9AXz1WLfz86ffqsMBihZlIiTbBTzcJ2RzVGmzjnuYngH7CLvxTy+nJh8Pjo+DRLnrJDSpMsoaaE5vvfGVR97+SSw0n/Aw7Z6krCK77gEoObYPcxs5phHynPcP/MQx28wlKavr2SUsLo5Ndxq9yRFBOK1RSoK2RHtzCJvoCgqKjTSzN6fgptJMHAc4FVwg+nQRtn0s9OOwKgRIYPsmgGt7uzCZfs09UbTzDKwcQCl9MhXVny2g6mkeCzyY+qqemsmGWNRHQQCzBZwuwxwhF5AiZsQOrwhvlEjvBkmTic3o3IAu2tII1vOYMQUQQ32BnLAnhzX4K2dPmMd7n2v2l8MNmcTjBP0D48alucDrYCHBASv9gVjADvIKu5EaXzT9fwSKcZxGgzgFYEoxsmNMyYSZlv42dK6x8LuwrjgeCkhB0l+s4lt656zsR3+l+ORQaQJ3yVhtB4npiWiKew58DrSHuAe/QoqRDKFGiQcnFzcPLx8/AKCwmLiEpJS0jKyikqqauoamlo6unr6BoYIKKhgNHQMLGwILp54fAIiCaQSJZFJliLVQjnyFVKpstQyImISUjJyBiYWVjZ2Dk4ufgFBCa3atOvQb8yEKTNmrVizbsOmA4dOnLlw5Y6efPTs1UeffPPdDz/98Q+Dyewhkik0OoPJ5nD5QrFUXUPTxKEjJ07lzl0igdExSSVJlipbjlwKSvlKlSnXoFOXjTYZMGjYZltsNWrMTrsdMW7UmHETJk2ZNuPAsTsPHj178cFHn33xzXc//JydJKTSxnlVEplCpTOYLDaHKxDq6Bo4curCRUTQPXOgD1rarFOgpMB2aYWM1ihNCBPl7/9NFkL79SPYjXeMXFn2VdymAIJ7HbXLiRV6Kjkn+L+5DuY6TfM7x+rXLqFtFj+x601qaLQRZ5em4SiTjoXHqCipM2ngtV9Xrtlno8Br6+HCBg8WyP80iZpagAkl5Pka44GrNyiID15h8/mO9T2cl1KOBQksB6UgAVWfDNlnJdi8Ms2A8FLPQVgzlNNOgtnxM7wURy8a9OOONcxfXPI6Nen3BoN/C74mQc4WLIsGU7+DQ0YSvyAzBDpGXQn8HD+8xJrMTUOms1Kzyg6LrMWpRrO+ogkpHO9nJZqL3gNN8y+XvQRxGwxB734Edab5QnNGfe7FitKc5pn0JNZeArepCKj0q1vK497WUzHJrUfF2TboaHyO7OXXpfgWHjdbcHPSsbIjwNzbLZt8O8eks8qoXci6imljRye+H7GfRpoBHUBXnrQ9nOl2BAq/8MNKXktoh3pWJi5hNIuLv2FFPs2SRqkmsE4g28bgtcBodliwR/GVJaY4aiMbczQ46CEIlFLTMBXAWM5YGKs75Rtc9Vwjk4lRaHC+bS5leAIbSITbt3kSMpDfZ53xAeqIr9Ul93uhVExJbIjOVWTBMdU+kPzkVesaNMw3aAVJCpTVJgg5E4kVMyTMum7OImZaYLz6Qq7ggQuLZYQ2oCKTrq7ttmjKvqlapP8rA2I3tUoS+6Lb/NhemTstrdmAmZm3c5rdDjekF46gWaK09CJ0zAagzXjz608Gkn7DehnwDKKDK+9dYw922/NYQ5pse7Xq5iHNDmvRDk7W8eg67t7T736PH5HbJNnSjx+Tp6PXOlw2LO0Q3M26y2n5tX/gtblcSCYODKannd+24G5SN5+xM/Wy7fnNPFdcPw3b/6Rd9S4NG3s4cLntaKP1HVTBkkOD+WlFDLEfqN4LfyeYivU2D119HtH+mXNbvu8vG/9Pyg0lkc/87VsTYZXSAsDzZiLJzXwM/kzU5fqeln927vvPtU9pS5+xPhNQnK/ha9IHynuh+x0ry5VzrHMUzogt5fscn0V47dPxMAfxRy/Rz/1hx77c2CDK+1XXfqXlUbbc+sEXdvW/ft7h0cd2XSo8SrnZbfH4S1fW7bi6n7EgrNFMiy6IkOEj4O8epxzo2XV1mEPqyR3UFutf/5vnXTlOXSyiM5l6+E6ZNwWWksB0JmggUT7wG8TMntPIIttFLTXuUI+sFCjlMq7PafZUVxbrMu2Zq0aA/qTZJ2d/5ViEOM5o7t41BanoVUNbLcTSihGZwzldiXQPLzg2LNRXYjHrNgPWaWf9S4qom1+XnDZVC4VuujhaMH6L8pcaVUWKHsOrS1AwLnE9dOlNT2h8Eg2POI9cKdI5HXRRCmElJSvnuSdyx2nr7o3umQBWnIDQSPVUjf+QcaOZKGe+c3vKZAD1CaAo+URrTPmhUmo6y9eoRPahEc8XDxoHsXOg+FBfZCSBWwd1VpN93hKFI1HK3ySAOP/iE66rPH2pvVaQJedZfwEd8H3raNp/jW3bgnrIxYgzkKI7NxwCf7qTwK/+GjP0PVTyCLDkyMSZBbvuXdi0bceu7ZAOVabIHTNLoSsvMIBsrZEHlVuTkuj20K41QYXq0OpkQmKNzkjKmTlNkTIyPyRzYxzpXkOp64T6NHoIy3oiPyrjG8l6GEWpkV5SdVrtoEwc9UarnkFYo19kbzGgvM2hYMI0iOIlMqpjqOyxB63CzKm8WjHfZUhUa5SYq/3cIEcdynervCymvZ695m3MBX2uvalHyyw7NSe3iKQL5ZORn/5EW5KiC0N21FeJn+DxQD95zC9Y5LoQo3i0yhG5UsQoTneeWHM+rlFpB9KMaypbtD7WrMuMMxlXMG6G0EhMVcE9RwckFxIVjUs5u0+zQrUOjCJgrfF0VFTwgrz1eb/7XycFX9kKXCo1bUEUlPuzgJEI1fNenq7ZuO0lCKJT+ILC3GcBb66T+tZgLtAGEK5qlpPrf3quhlk3kN5C8YooMLCr3kaLCIdN/1c+/ZHML7+9jPApbpdPLfGkBiiff+7oodZodMlmucBdlxrlm4KfF1ZIvl5siUb3E3ny35wnQpda1Eh9bDrOm92RBx1+S5B++IbKBqkvqBYF+TdlPT5Wqv1J4U4rnA9kk7/UNoTq2M/8Quk0WjiZLNJpqCv2FYFD4PO5AO7ynj+V+mzKsKHtnOxfGXV/npfwHSFeUg70n1koaqPJGAC9oHjvi/xetYtPi1mJL9KNmV1Af/o4A4em+tYRpolmoer6MZO4EB2Ou/+F5Ow2ShzvNDg132vZfpfqPr+f2HC/3irETTg03NXsbv6fiV7gN/W/wcyKF1EDQKyadPnl16meA3FrSBy6MgxKHdoqj2J6ljWMIeeaIDSuF9NlAYB2IZI+6/ofgP4zPLpJ9+27+Cf80wYyLTmjmDsJFYhHr68mHIYhokrxL6Ylv5y7cFHRzqNxTVEq2CpkDXzD9IQ4bF3oe21DEOr53Fdz0sgphLO12qKMO8rndQtaAtrM+y8HrW9cgnM/qdlsc0sYSKI9q9M50UkerA9ZZVAlRbUjTzjqGRML7buS0BH+DwMQIkVjyLVeXXbwy/1DXH5OUN+9gsmWjfYdVMLjfghXD77sWKQazmR4go1ML/l/ho5J40uIRh7Q7IsOy+uPkzW/6SwzaJx5Htnc9vtxIFXbP0TfxDoPxqvdW1tgyJhFaN2mTlWQ0+jQc22ogC41UKk1iNbczNrqOuX5+yPadnztpY02z0oi69j4CFrTGyyoXvXT8zIxLhcQTaIFxvLhpkuTPFowlK8iJCwiOi+LEYcVOnSrjimiaDORFoQ+4UU0YRjuaeo5PKLKJK1Ce1LyZ5wdQnyAcyl4uACwh4OLS0iIJ5VEOp4kIgkSiTlQggAwuXSRuTYKM+tA2+2Mdp3XthkisjUUdeXTYIOejXtMECdDvE2GT2YF6ARosAH28QKJhJoZZeeGifiLR+NGz8humIq3ZKitgWNdfMBLBAANm2RMkmiHQCiMsQ9Qq5AhFW6YRZIZ4ebGI3jLxTlAkJhJIUagZeAGpHlUXoAB7jSTDBlFG0uhwYtMCdMNh7fRJh5OjyFBJfTzAT8eGFzoiXMHlpNXUFSypQmFS323DzgIm9mUzbfcZTFWsdGFnXYu4JpcsouuIgYIeLRMLGRKu/Nsb5UPKhR6hYMB76yO4+fkAlBVIwAtJunQpij74Qm3JRbEdcSlmDlEQB4URO0zt4IBADAXSHTIqKHhkdrTMbw5+VUCnYlMQC4EPQMA4NXk88vVMjS1+jVVtDkfvDXx05mwkfKo+ABFvLiXxfEDHYtpNILMKaZevCJSpAJ0NtrAyHnP5Q41eTv7rC2UkE6DDXxqAZUjzUTYN2zkG9zGDRm5QAYFg27hasgxCF9xPJ/4NiBCNBny7MxVfswUcDdeV1uuDAZHZYaCiJYXV7JpHpUXYIA7zSRDGB5jDVDpQjryyKXXOu1D7j6kxTrCdQ+hjLUAg9QCGr0Hj806lJiSDcGsDhBF4sCxtm+SpjbY5cBmJMSsGGnshuoqwbVjW9r06pRflVQEGkGaxaix5tnbC73hKhsFeoVDeBNfMFraxi2gzM1DZE72bcJ/ygkwBgchEABVQXKJEStzMIxlLKE2pYp0ZyGmfPlYChViq1IFsdRSHMs41OuVBbdLH7aD1ohs2OC05YHLkyftXrzo8OZNpw8fbJw9vvpqr+++2+enX/b7kx76OxAWoRXNw1kbIoXlwXJJy2w8PgUxzZyUxTavpTv2+SRb4pjvMhqv/JCxeGdZJuOTPXmRkHTma6j53rm4d+eBFI+vab2Z2KD68njRDeaJhjeWJxvd4jxVfAlpoQNZNTQ9tYde5/745aEAQwQoEn5E51FS09IzyGdmYePg4ublExQSERWTlFIqvf+eZZzlhw43u+mkzTpP1MCJRePgkZKjKaho6BiZDL1mKzsnj4KRGg4rVKS9cLESGYYBr/hhEIfrv8DNn9DNH9/1nmia7G/KzXjlswvWdaWejbX6jf9ZszWMt7xWl3S8Y4UHXpODGI+v8NRrk/HxBcs1yx3LQ+H5m3k0Dcv/+GZRLsc/AxMw7RAns4A12X21+8RqhU7vfrlCg/UnwWswhEiIgTTVYqGR3obAIEYZChOYYRjMo91weIznjIBX1jt7jYKP+Eo0/MBvBsN/rBIDm9ghFg5wwhA4xwVD4QZ3DSNIBIYTNBojCGY59wZF+M7Iu1/rfhp9kt6Tg5jS/5x9Kd9+83VDEamgDKWKelSIjI4chgPhhgwRgKaTBxFcAeGxRMSkQR19dBoYY9AkPyYZ5sfwHwvpj6CNZWUjjdo5YtfJFadunrj1KohXH//43hCgE4SEDAknciMKsaKKSDtidOIuCROSSZlSnJL+Abix0jKkkSydMigePmlAQnP6U6lqLyW2tW1opB0d6Jg5Dz78Tt/pwxZrGzt1DRNfIjkFFbU+Q8bsM27Gih0nbrz4zuUFsABZNWXqbKCxpguX04SmNKsFLW91G9pWdfs61LHu63gneqm3+rAvO9PvA0CBpHMQkIwf0IFk+gP8Y5kd4xTuq910bmYzS88KZurZ+Ozl3GQemw+bN8zH5rcWrHfBBQqsiErjPOUzK+pUllyskvkP/awSWj+ZgAvZr7at/q4D1iPW6vWNjcWmdlO9Obw5wG/jYOMs4wHjY5s9bIvgtlx+rG5AbHzwLLJSnz0m3fGyvsZW3yxz+YFJKCzR5nBNx06XDS9YsWBMHGlU2o3V/djP5SZ8ZQB9HXbtd7lHh68JoOubYlhgO398axy73Qbtsdc+r8J468lznGnji/7JqG0pffbFV91e+jZpLs7x6f+cDmN22O7BqNh8kUPZ2OD81Yh82cgmG8sv9a2lznoazO38e3I3f/I3d3MkN3M7N3KrAAIABJDAAS5QwAM+CEAIInwgLzpvrvnB5FSe5X6eZ2rCeY46rCB1dUj9ybflZL1zon453oAcq0eO1iuHisqdOpVpdPI6MyKPqJ0Ny3/Zn/9zsIH15Y1iWtxi2FNmExKTkKF/bRey8w7pjIgoHgunZVVOs+fqgwMq9zPAQIOn2GeMMJmZpmUzzLZIA+1q+GngPxpa6/TpN2KLrUZts9v/DjnsiHF77XfQYUcdc9wJJ51y2hlndehyw8z6g2kWFmARFmPppHpeHywWAmeFJ94UxBd/AgkmlHAiKUw0RSkLNfSwIgHyW2C2Yd/cSlmZVWlzQXVDvo39BwB2hatqjfobZpwpZmnTrk6LdmpaGwwYMWqHPQ447Jg1Nthihz0OOOKEM7pBNuco0qLhms40wpwuZBxhbhdzDGRbjoNszwmQHTkJsjOnQHblNMjuTICm0gVSW0eQhjrD1DAlTAPTwnQwPcwAM8JMubqr82GgWsmCZ6amKJkg14w+grB9CeMQiEaAQLyfsEDVAqt6P27eolOz8dNbTyBBpjaMMMOCkWFjiT2OpJzah4COBRUDKzckkDvgRsYCRfMiciFLJx4o6LB+kHcqc5JFvbOcjmBRQKO7CtWXXBPVdUQb3zTX0Fg5NNSDt6wBNNKuB1mRhnTD9b6ZoaHutwwQoUX7medhxF4e6hKVJUhTLL6/tGMku4hKvZNbrSOg3CEkrALAHDiap6X4Rcc6m4vLllzMpVzOlVzNtVwHsB1jk994aspqGcsysfgtxccOi7O8+EC+U4espvb8LMrlD16h0WPWMUodnhBCNpBdOTKwaC9GvR0b5Y8oZBtI5NCh/A7NtsTb/t/aaQ/kdkAAtQm/7eUBXKK8RldLnXQ1wrC63LEfgotm4BOTVQ8jsxiwlOMLWXoxZXya/pCZP1Qp7Z5P8c+B129u/AH98dLbKDjach3unsBggFhXUscWQcPmFYJuGOYdmOI3C8MZR8g3YLfRhokdyXS1EtWLbfixLQ6LsYM7dr0c69TD9Rzn+oCjjkKmqxc5LLUQ94MwmkSbbvfv7TD773P4no6/DyMMvsOPWsZM51XexCv0SMUq5JVuExTbUOPBieKoMiqpEpMohCNdauzRfEIIIYQQQqqMQoviD3JEJc0D6p2QKiImIw96pKIO1BRwjJ1shkVhhtKoNr7Iji6UAguctTr22JdAQokkGlKwLZEEjNB0f285qgC1XFPKnYEwgVpViv1miOyC14vSS4ASdcUVoH71fZEMJzdqADwQgAhhHPWOO+m0sz9ATLOCvJ47eYEawl/VK1fvqTHI7pImPX5AwGhpq9n63COgQGVD7sbtewCj9PJr0BybDYDGCm/Wsupqd71+2UYf4yq6ZVSIATSA+ro3wDgDYTaeKBybPMnjPMrDPMg9mNBAkfo0BcafYMJXt1ZPBhCVc/7iVRJ477Wy6n+gSGAveAcStpnfDd9XXlMCkIs1hYGA2y9AmeWvGtEnhdIKL9eiqmqCRWkusVW9CmSW1p8rbkGsCQr5YiJsmNoNuQjVoavuJeM4ymko4RUygr4zGUJ7sNsxuQwMqDzJ0yv+Bx0Wd/qxsuvysQadkA+3eHBC9yOPM/qwfqWRytyJjD0YG2AdlzUb2TkQK7+mZF49oncJkh2zabWEAFFy3TO5y1qeo/oqIYDy/YvdZ0ihRG8TtBtxDUmApsNautdctaVerf37XK32Qe2Bgw+trPtQIBBo0T2zbVorS1Q/ZtqSAsG0JQSC3IKmtSzMeHW/jWif1rJpGQ9bSUdtqoxRVDQXKbPT3VcU/XmfZ9fFyyTk0wbHEkdMVB8vDtRPmwC5f2k5TwjlgromCJgNyY74Kz789f3yJ/2f6hUeGqy7Y+68QZ8B5MdqAZcjIQNkgFyAIfI5PRhyAZknAE9I4zBBtha3JO3591Tff3q6U0PHMDwcHMYvPXr95E/MijnB6BgTwzJ2JsAkmHJ/y+lmtptfPntuddkZu2ePsTrWxHrY6jAlXBietG24AYF2//cKbDE+SHt+1qb/9FRv7X147dePfpklc5zRMAaGuXWOL/nC2ZV5lNV2luP7F8fDwVd22H/V3v17yaP/gRdnbo7vn2FLTZaw+HhxY0cu5njk2OaTxheYPr47FuAqU6pAnlRkQUc9Rzbtz5RdWnTOOuhIV5hVgO5EoMGVA1wXIz6d8ojqPakpuEidkWsd86ao/zxEqbt95pDw5epGiQn6XmYuV3HNOlc/6Zsn1Hea1QA01HzhehgQsJzr1U9Oc611tra+NrWxgYYabLjNbW2LD/pdtL2xdrSz3e1tT/s60P4Od6gjjYePFCRoSMUxClYUWmW0UFbWVK/mSqzSVnZ0y9WrI9GSMo6EEh1sWQvDaRKn1Yry41IFSlR9Paxo3pdDKV8uRf3aO16N71QAnFV/u1wUU54CJUla2YpW1dLq2ltTW92pddSVtnVp+hJCEBEXT3wsQnwc4JAcXilEb4MY3wfGO6HrIXf8B4AH2ID6+4VTDCpCF5YEagK4NLsnEIhII/rrQWKf43wEccQEDWEZKSSkCsBTSAQKs2sgKYzu3vH8EE52cdgkKdKpcKoHKldeSMeAYwWWHVxCjcMQ+ZQXyDCAW4KXqyQQw0J0nhoi8AoSpwKTJ+lu+VTuCruMRy9nEShVQ7wWP1Jpl/g0DSh2hfK+1HgNuZ3ksgRLWqO0rJYUMwbFSk59tDh/AGBrwPKaUgNO8TXh1nMGo1boDckD+t8Cx57hKAIDdnxYsiTXKiHn2gGDF66X9P8oJpamYpYMFVAd9hP0rqrmim6bo8nysJklLYX1E0vaSyJBBdLi9eS1Dotfe5BN4LF389rr/DJ3GoU8ce6wy/3GBfhKZPJxMVKQPBM0x+VA35jQjEykXCiJhxxGxw1TbPCTTSuHMotDGN1ZXFpTkOHbYIbA6Acsbu+bp2C0qoVMiOo5LThgig32f5pbjC9JJ4EOwNRNNgefvWWt0Q8OaEeGmwfMoO4OI6hd14FAE/xj/VmJiUUTMflLwjxBJVI5c5r1nbaieo3wSbRbh19J6OB1Ii6P6qrcRbeia2xRubYPN/bBBnu0XN9jx/BqYOKPgKxAEGjU7NaGy2UEssSzF/acP/tWhOD4tVez+pcTZm4IO7LIzYFhshtT1sgAh6RZ8YQXfdIXeZIXQ9CJpMxCnVGOMnRDQYcSXRRjZyxYt4bjTncNTUpzIAxsGTUVj8ZBo0pGF7wk1RgcgD7IXzZDNZKoz1jE0S/YgxlQkIyiAoTsbYb3Rpova8Yx9p3DkRxPG7kujrLfue/7TcXKRcBlzxomofv0iNEEVP2IW3fOVjjPARh8sHfMI6XvzXDdyF4K/0kW1CzVBGkuofyX2MDFSxBmTBgCEHZs68zPXB0hm+TGLN8Ztcutwhh4SsoZ0A5ThlEGMxZ2IzxohHvTMn9s0Xq4IQNwA/daFiGiEpmvN6aWBQcHIvx3pke7YmXkR5YoYZjxMwSJnCuCxGCokEpPBpd7seLIOU/WGj+qNDA54ZbEXeQS8RNHmC9+zcN+pnTskissG+TzRHHKL4LOXHemK6tjTuX6RabYLVDBZ4vymVPcE8FxwigWpLNcTYK6I3dn0R2IiYqzLEFYnIXcWeCWMHenqSKmpJGhMBKoq0jkwYmjz0uxbmHyQqYYChYK2SQebqjW1nGLcWgnTL8KNC9PI17HtpyfIlfjYAj6CHWZgFAtRTMnieNAfnZK5lACSIIihnw+Na3EUilTVMhZf8QIXyKDlkLOUGKWVoaRgYGAkKRmYVxwruV/HDvNP6THTqEvs3kZIZ92chfNPZajOURhXQ7NqXOYy0hB2J3KVo6PMKgddEGonUBOKYlEGlOrU2yJ3aKT8lhHry1fQ7YLDlCB7I+oT4vDHlIVfsNehRgUkkbyctBt/0u6YMSLq1j8MnHugKMUIBee4ce6ETz0AS88MgnvmVTJOB+9Sie/GrXsaRY6AdSMSQZFMWAPWCebkRPQRKa7e3/adbUKONgdQ45XrIAIM+c+Z5H/alalhXDGz0W2glyThVmoGWAOky1U2DU/SSVevICUI3xaUrNkT4Vw1boib26ucGZvTvGD7j2nTd5NQNPrAe2ac4OkcYIKOV/FJfYphOb9/GzBgb4z1l13ZDoSutaMSlGg3HayfJgt12YeFj5tBRtqjxOD8CC3oRv1bksrUFTA5ixKseuEOeSmlu9/1HCI7MyjmWqq6eNRdNmqVVWPUFAZrAVazUSW7n2Vl+GPumygbVev+ujhrXLlTzkC0cIPi9YVFhdAkuiTWXG3WUMpht4XyAENC4UsSRyaFGcbxayTHIkARckiNEpXXNp06SpO5X5ql8KE5UClUEhsmSa4C+46zhG5vPSkYT+ysrC2crU2HCn03YwLEaNo8Ykl+RqP4j5HDIOnl7V4hKmoiWLoLLLIR7fdBSedALqfgUIuLWSJHCbk+1JKkOGIfFRCinUaW7zDbcRjKjuD8Jb8pSO8XUhFQzGTwrxauGMEXpXLDOhpHXAvmZvNcD3Z8JlAN/WCUidg6OyqBhPNDxEqMWjcomJ0kdpKqHHqBGCxLIOZzQ2nXrXnYRvx3BaIgSyyCVltQFvBCG3OMLob0zmyUId+pbffRcjcrQvgiRyozda3me5cZ9CR/c4NxsJcxjrswJauuXp7CoQEUmNWl8GwschmRsGRSx8+oRsU8bO+2pU37noCuTiTztQ8nK5LiAiXrKKV50BnQWbn6c6RBKKFBiTyAKBMg1RYyW0DJV1gy7KERXbqAFcVievg6EB55Q591wtamOgWZpq6OiwxVvB8lg4AthD1XSjCurpy0maLsOM5uFRzBF1Hy4zwoVeG4GbnRTHIUjOICJAShFqSjl2IJDgpB90WU1F5BQp2TgQq04zMrhdw+27kW7zD+R2qJUn3s8bM9OqqqzDPJHyTgBx4HrYo8NWSFY8QvLRbE37G3bp1aWflFW5vhg5i1rKuZdu2EZJsLgo8UJPZCyPNYBTFOAXxLE8sdxl5ImUZC6ql6RJmnMmMHzV65K3czHbUiDNgDGWNuk7Myht9tiC9SwODPHvyGUitwZCVGbLZwxzZx4udnKX4wRSPTo7HFU7MlDc4wR1lJpgpD58KlMpTHFe6OD9McotGySQygjmg6OYCJ4qjyBPyByiiABF2+Oguiv95zOAomT0SHIWSBtymmXakZpAXNrm/xFoKRk0lqxJAioBsUqT1Bc59xpIidP5VDFUW5fLBiQB7yjAoSBIyzkKQcMExDIR6effy1cuXY2b4cg3hKSJj5oi0uND2kmHIJ3oKHtnnYInrHlQTsk+a2W/EMKDwkzdhz9+IE+g+ut9WUZpByYCE4TiLbPHVvKqQVcGd3L25FryttVDOF6QuUlIbJWvXtKJGMGbeQHXPx+McQ/ur9Y8CpkAmleVt3UMxpBG6o2NPT+c1MHBb5ew/1d3w2W79Szz9lWkAJ8XleBNoUpw2aI0rRtYrRm+RXUQHfP09bl0n98mJa24TQEZFBg2lgyH5ZZQ+hrmn+Y4AJAqa68p2BJ25pE2e0LryBb9OaCZQk0iBcLD4yD4DbcjvHW2SRUb/byCIaebZTwoui/cjTQ0ERFFbVGqQbX0l1zY8TposajWFWuODp2g8bIKFpDhtsDQ0sdYPSQ/qa8XiRn+27likhJyjxDytGqVOZNWcUY1jEGocOUevCQYyNRZsK5lZqpaC4bExPimOD1JDs6NGUcWczCChJoaHoectzZL8z22UHe4h1KiDy70CZhoQ3vILuFVTKZVGEw1zWDS6GB+pUxiNNPBjj8XDn1sMRHF3ToYCH3765aw6ns8NhDYodaxOG+5KG2K6DkfcCg6d6hlseRqmtuR5JjGjiwxjhgN1Xj6fbDVSxi4ON77mbEVu3/Af3FP3FXLnmc2fWzpvVaKf9r6YHQd88DYib8LpU6lrdpDVptSD1Kj1aLqCg2jRU4a+6mxdUpv1l69Q2qUWAaeOHjki03x+vaIOxppjRD6RGecd5ZZMww+OkuKkle73wUiOTmLo1zqCZsJ7JmX5anvKR9YqndWayvm11opJZIwQK6Arx7fsoMs7gFGLbpfSlz3DK82et/4ob5iDDBjEtq+fNjqvf5Fhi6E21Ii9Y7BAO+BgHGtHlOz6yaJQLXybm/KQnhUNFqqiBWiJNRWsv8vTCZiW//M+YEvRFKq2iufIFoWqOc26pvxwSdK8rOepuqX5ugbflbg+86GDbpNRspBmgdb7a8wZEk80F1Y1mvS7GbwiMvW8gJOA3qrYT/6K8QeiuFnl4cNBz/4zV00MZXTmeJEwj1yx8+GaGMPKYeYRuGlgrfWlXm/oXCTcnUZsX09akPYw+eHsvm8Hox7UXBmloePKpeXSgr+PLV10/M8abEJOJ1YXmVE/4bqYzRY0JOTWkNqvCfP+3u9998Etwq8X2nKaGp3bRUyhnmameeDKBl3HH7PcHFSJekpEHJRAu+OCPXH7PdUXtp8i9iWlG20G7XCEqZ5tMSiWt1RhtlypKfwQovl7ZbfY1vSLSxVqc5kGMe8utPUn2HesWxDUXDdRFjErCpqn3ufzN9b7aqEYUKOJ0eRc6ed/GTGtKqvgExES7MlI8ExI5LH/rHgMWhDsUlw8zPM1Glf5tnWaXY9DTvddadpBiVD/jK37suP+dQtj98vV+g1iVUL6WpzLoDDBKYjKLYDYdRDuwdqWe5pwHlUqARJqhohbG9w4C1gqaeY2hBeQTUTkcGjglwds5FcTTv5+s2JtptHkHShjNTLZc0svh+D8OJnOldKRSnz0qqMwlz7A7hoEL67v9cVC3RyoMCImmFhasdyERDC+1GjwvS/zZER5rawVHv4WwODwmKRXQ4lcgj9HHFd5V2RGF8GAphj9HI/XBaY27fz47F7CYU+rbG5vQYzWWB2TOva/jVho8hmhQJjv5CezKYh0WrZDJM+bjPSLvAMhToOlZ7W5N2/lnnrWHMtnRco5tI2+/PGnKhjsi1i/hPwUDP81KYVe+8duzVTrJFrXlqME5Ubb1VBG9IobTNOEFHF6SiOyAyKeZAKQ8DzFPxUmPWMUL6L4IkpjrvDQ82teg4zvlw7EN5ekF5TxDXGK/du+dDs4yNjYcnF4wnnA/poFqT6Cm1JWUNfwS3t1bSLnG/yGy+FdUswrfGcL9kHzriq2kRxYwE3eTNroJ06SI9b5R2coUo61nPfnJvhW1lYOZb4gbUYL3jn+zFEcvhUmKLyeJRYcefszY1kuZenS54ISavvpNaZBJbUuSKa9T1zj7gtC/8cIg1xaEeE3wf5UvARU9mzhwWEbmFYR8jJMtZnCt1ku0OtkP0t9BKl+9LRFNsu+/TXTifdVp+pi4Upbk3WxWa866IJuDVn29Ye7ZJusmGe7cytUa6plXkpUNbrWnCrTqqrnWs68atmmU3NbdOUcx26s9JFdhjWEu5Bm4tEvU9gJjcXovwElW4RLINWGPYxllT2sLEPIkTEco6Tic8cA6K6bguNk54kr7CM2ERqZ5lRMhup8+QmiBtXuDbplwe68BoYjhIjrzbEj+jgSqzcd0yQZmhJAzR9YE4SyIUw2o3nbmomwiVLEo6gRCftY6V+lxYV/5hf56fWtWzWVAI69dSzMuBK8q0KMecGind/dleWnQqL0itLyw13fBcauyRSYDY/2eU0+W3yCV8qqG6mAJ0VILWVPZh4Vvpg7T9D0EM+5i+RHFfcVh0ya+bAUhV5Q2EXcjnncbjl86mgg8/CwIKxpJrhInXxxV9fIDJk3uQlGs1mf72AsDS8x9h7xUzt2XD+Sf67jORdKDIdTGeUwt5kzhH6VdCxMCAA7MjMPr9yf3BuN28xiuwx2x8KWO7ZIzYUFJOXmYLk/C0Fy5K3xncdcpHfRyV9H8s/QHCOPw1J06N6TIIZYN7SBw43CnSFCDps6BO1Ir1el6J3UIG4cESh6id7VRvHuhKbqUHqIn/L4kspnG2uBLyDGbWHLmfUL4uh6DI5JIUMwRgKNG4fHaOUCta5vmk33Gx89n//QYg9+eBCHPH4MVRZyoez+1wMFz/MxPBNhewfhXOMeyPp8t20cni4NQ0srJLLK2VnPLj5QtHxcaCTlOYtD3Xa6VPONYEcrwfC8sbC6RZf3mKVzs/cV1XVTNhbYnrawlxsXzWcy0VHQne6u8xlokXkvS3TtrFubVbxrtzZgV1Gm1xyQmTMrGP0p06Z6ZlVwcR2cXB/qvppK16sdJGwtjG2brnKq5plhjERd1FzK+Yp9ILZc/tsIYsRPQ75KJj/vEvJU1FsAqyYc1m5nXLzunTh4Zwfv0YW8I3GoTgw5WktbaxZDrdRfbu+hHQK5+f+BiZF6AQ+Qk7paMK/dvytlnWMf4PDms57cyMV9PCNHvQ+nqjvOAlqdG9kLM0V1QjPHIijqqAUctTBB1J67u2h/tTxw+/Y648zVLoyIkmaLwTcn7bwbJbNL/jdVGa6/uNTzLkceH34PAsskfgf4e81t6HzWXlS7AVr/rB93CrEMku/DtGjDGyDgW6kTTAh65/xICeKhsdAeKcqdmiAsImxTI6RiLcOH2QWpnvg9BwuV0Q8EEBan/bwmfR1cNPL/OkaYphCFXY89MvRhqCy84IDm47MsVJ62HLs1Z4CDE7MTSvz85drOmyGkxzpUFn75bmv/thFLv7hi4KV43DO+lXWllX7zmt9CFn81HxxD6Ho+cv5lRyRdiwXAxbNe/bCleyebf7/FaxverG5zjUG44Y0o8r4PZbFB7yJr1MT9brI2pHlnE0IpNmx2E0PnM4gm485k9LvY0eXE1Qt27ZvnYOqAdsA4uLQlWIsKDYUUvdQVXkLRiTg6DfXJKQTpGuQ5YtEf6mpnG4wNtrUDDyZnX4JnXzp3GgVYXifLGaM0tZLS66zppPS7baU0qPze30zpcf3tlL6EO2sYneV85gCHTEZC+mw1F5IrPNFMMSEquGsNYv9faCUuSUY3FjLiJbOPqjNc7BiAp0UGOcxC/0pz6w5p4ihzRLPBnnJf37gcPxIngIwN9SMqIuq4C1kIR06KO2xihOVXfC5XhYe61WR4qgC5+hDCxnOq8upqJE3dZFIMOVLAj46JZ+P9I7lV6yfro3hUOXOMpz/+fksMJQSXKKTJfZ+5Jrh4KJxcY4h78wDc+hMEXFsh4fqxVyVJDzk8AcRF0h8+T74ELNohGmdwPheE54YEUSLjLnlVMQWIDVKj11PWt25LIi5gYm19Yk1ydaNsaTca+HkePMk4eQg+ZF65g8Atb8FdmfQuwHkawAZgTZ//L24hR0/DAYPG36tOdnXWTJRyC1Mwa9O6jgwr2Vy3clqwWaInJw4VEMkWOJWQaa7lwOctFN+ZiNciOPAfFQv/F/PDPjfGLZJFJMlpLIg3kukXKi9VClxdKI4QqOWHXjrrAJfD20YS9CpLxPK6mpxsAooD7RE5+yPiBAI+MZ4R8hk4XJ1/RnFWDstbkJbAgP23Cm8D8mJoKMVnASuNz4aFApo/2Wsav1AF+4/rkpwC5EtrMnJba5XYVJdTYIm6/ZaXZ1JDjTyrte5Sa10WDQVP+Ikk9rBM2iSTtclkZFicAGiOAjJ8BK8BwvRyCt4PRSZUkgkov9r7JBEjLErIaBIwosL59P7Mg4UKKgbNdD4VK44SVKsyM2pL+JGEpH4XFBtFBcehioz0TvynwQ25Ddu/YegiV8CrjtkHyMiFJydjyoFvDkMIpP2iz1NDCCBV74X3KskPItixKt8fcWBG8epAu1MwMToGHR9YCfEaEr1RSXk5NH8P5sxcbstNlDliGz4Pi6ny70IwZcr0psbiE/m1JYqU6moBq8+P54+ELsDk8BuOjXeuO3thXc/E6U4lPUiXy/XOOfj50+7/P/89l7yeklUc2QpXpq662XoohODsHscTxZLj+Xh3khswmqfhmGj4uH4arv8pTkd5KCI3iXvJOP7SUWcekGpnxQjd1DZOmHA+a4rMiXGJOiN2w5nHj90bIxNJgSh8xFfPpxO0aMICNJlAAi3hJbQwCOFgVOqJ0z3q0xPqfG4wAp2Uu0SUrtW++08LJJpjWzfv6+ka2b3j8P/xCqOCuuX1lbOARLObvnCSS+d+zqEfABLNIF00SaQTYzmgJVzUfOoY3TgsHtfQcxrjVEUsNAcih6pwQcrKezQgm4NX4jlajoXSggM2jyceoDXvIv7VGgmsZW6LDlYNFp8EEWoDG6tKSPqZwWb/yUfGYKMl4HMPzDefBtoME9cWdkWptJw/HsBits7mjB5cPFegFq+w6NG5kZUxrD7DVd1R9MPARakzy0o34gRHK/I7VCuP1bEymj3P+AhHG4+eCCRfBzbhzzUbVMWa9c9Vk8WqTW/9zp3t7pg859NEdnSDhL+j/6reCX9zFQVJ6WV5xvw5/Wvql/eCW3OhJAN+aE7n94NUZy282QpJmGIarQrTRwrNhlKBHIyIO5jk6JilLWvP4E/MhRKPzi804Wg/yAuUEk5yDhhFi8SdI2nIy8rjYiXpCaQtykhMIQYAFof/iu5Kfwm+hqvjeewTnBPFS8sfMlM8nQFsUJT5RZOgHces8nGVvOkdF4EszdK+civ2gzCyd2NFHwmesu2S5r9xSF3cx7uhQSp67flVltbJTVW9/Nsa4KBJPKC7XzcJeh6qmwgl3vC75pcEgcRnoeBfocmCBP97HvIUQ++yUBzgwafdTOMn0Z6k84EVi3ww7iC5cD8olmUel/aFKfRHgVh28r/bX/a+io1vZQNc3AbCBmDoQS8vV4JfH6PYwUHeMyh2Q+Y+5rgsGmLtTxbniPg8ut8LEawQ0JFKEZ92KygD+O8KZnsZ/hoVszNyuAkKOQnO7FmtKuzoUEjx4f50LXLtKgiKkvhCrD1iVZs6L6N1XaWghAmJtAq5KD47CwTuoivkCdyMHLZ49Jch4hUCi69eQ7R0f3y4VNHRUajqWQ0q7P31iFQBEScVj2blddRXlTa1p+WWdJbN0l0EkeV8evokxLwNlO9NYvrddfHe3ygCJxdjp0jbZkrzCDKSRAELdrKyxfGcXAlDsniKwvQcizory4+id2Yx3NnQeJqVb5EsNzToqlTRvqywRF2TEZTgke4PejQ34hmuflKZXEBCwvB5Md4Mn68liBYwZpAZWQrqb1P6BwViX8+s+Xxd5FvsBotHZDcv8hMHv+kgcOY9dP31y59QyuNPnxEtor7yCmj0OQF6ioqRGQbaGvbyow9OXoulSen7g5JgJDb6dZgn3sPBw+rZ7f6lrIuqZEfpQRYcr6BGHCLw/lbY0ZGv7OjKL1qzJr/AfqiyC5EshTozl8NbmN2bkQ38o+gKuYiXkcNOOPLLkO0ZShNfPYd0DmsTgVHBbC+DX0cS2CU7yhMp5CTa6PXpRM49B3S+JwciknlF3J2OEi92IjIDkm+xoRmajaevjq2DI4vtpKeJJqZxeCi+nZIF4cUOnqgVHgv2MhxIcbioMBmL4sewApODduFzz6Ccn1CvB9Du9yB+SlhZi9RWw6ClsnP/7q5AXP3FyWIhG4krKvan+YDolOPvBdpBzWA8QBfODL+XFS0vXJ625T27ta+qj1/a19QHXF5D4rp9K/aJoJGnJwAk0VzVXhVCR/+xG/lw/9UZ4cDqvtUiMEymrLEaNqSstBgGinkAqNkNIU9No+azcHjChItro1Wjq8sEAYdnzY9yv4ptygKb2hlxDFCm35BtBYCW1YS96g75QWP6FFDf6dHnhBovG7nxkbDELeSTHms6FPsR/WhXNsYoEAxuRKaQ9DzF80ahZjKSZaYFK1MSFJ1pZskMKUXPZS6wCJ88MrmwHsDgCUI/BI0UJomZBAxPP4cbRgkNCEaxhgkrulABWH6grhVtE74nIC/EFNCxsZiGdw2kRaSuXA8xFkdUrwKxeA8/EtXEKuXyVQyPk4JkFKH6sJegiDzutZQwghs5BrZztqupcWA4j0lkMv7h0yxAIqB4mr13UJWDC3N5SoQ4LD6MQB5mYBlEGZdBz+BAU5QFIa4ua13iQ/BOqNQcV3oUg5zI5dJo3p6DmH4BNoPqHkbw5qFE8wBJEjQV8DZoOmCbQ3SAIBjNXHfnduzd2+u0j8uN0tTv2VXfkFW3Z2dD/Reh/T/1XaPxgvhaAY8QrYKIHFLi4pJ9VaWJOA7gSfTDfKPCCCHBpyJsLoYF7QgIomJC3bNNwwnBcQrXtUGWdt5ZMTCo/LHkIMSoPlp3kg5hxqO1AG35IQljZJbGp7Um51ZLDATnbojrJ3SpiTQSRia1rtuxlBWodI/hg5J0dCQaEONL2ey/FrBZTr/g4TVKP+LpAWKKaBv3Uvaup63ffZH6e3kPeQ9oS/wP+9a3stXdPPMoA5nl3ObmEHVMH1eCiyLjMEHR4QRgo5ImekfHBcbwKNGRikh9PNZrjqv7d2/ncF0PEok0PI1pw94DKz3o/HIlWDdzbLF7DiL9bzRgtfGpfcqiBIGoXCwXYDnzCfOz5gN8EBN+cvdHENR9kcnAyz9CHB5YEomiBICAiQaJhBgnEZNJkoQ6215/b36YkO7r5wNmopccXDYO0etOVh9l7NX/cDrAQEnXX/oPNBYNR8KDwzM6F34zq7NqTfwdSZaL94da1Tg7NyAA03IJO1GgM7v2y8Tndr4iY4h86RoC0ulmI/EGBEHi5lwNkHIVqxxlhJhgZz/15pzgv2GZVJ+NzR8SfgSygZJUns5zQaKz8RzuhQnT+VjLM7PcPf3fya2caK/qwRP33ZpxE/TcDt47ZxVoRPysNjXK47YRnijRIaQ3mke7V5ZPg1eDD1j/VhGwJdeDhvQLmJo5WmU9N2qnubQiypJro6LFjeGSx7HnA+WbVt8YEHnirPUxxi0CExV9JwmvS2bR+VJhOAbyc+JjSrQtGKEI0gATeq3o/90Uj7cTtr/QpDXpp0lvN06Heq1VCHaspfpsjDokfNOIY6dma2Ou14BGrhg9qSeb+Ff8APm/jo1e3dvcTJ9tY27kKyFbAs2mhaKopNeRGRnFPlJ3LE6i5b1eIkqr7z53dlNqIYOT28uIkBZr1ps0Fms2qORRtGx1nL9JLDyj94hyMjY6OHxJQO8B5byE+tWtiK4lu7B093VVW3laXtW2zOJSkbSsSD4qMltH5caR49hkGnAMiaOxwvFMGjGA4hb9KhSDjwqt2n6Ac0KRvjRTosiCc5K9deYxxbQEVYHMf4KSbTanM6xA03amFA89vrSvoH9iVSBpeS81xQo54eyBQY8VYqTJHRjl6YFqSz08oUwZtGdJxkuJIpokkATlqiCwk2u8RTiRYiNgngSVSfk+qwf1G5Fc7xKBhehboNSratYcyE8FN4AvoHTBYzxS14qgjRdpTqm/c0VCQy2oTNFwZjhgWfNQ6rvOp3zBu/gL81+b+FjOdH22tl8NyW4muGXSHXKab1o4s91zVzbfX9WcK29emeRREsNpCBFVBBUq2enpDGZqOpslEr3DMDPkW5WkuSdYStsGRnhypw3aQLATQ8o35dzhh6TOnNqkYBlz0oY/sY1JP5oeeVi8+E1vYydSjsPHcU0rwAf5dPgS4Sxyh8g7Hbnbh0pnK2k739lNhbObl6YsLonwxqYBy8zM3SgJC8aHSFUOJsX5ujPTnrz8O5Aq5MTga4eQE0XNZISJ22R9GA/7UdTZqQgRuj2a5MRxrn8QuIXkdHdw8/kZCnYYy1ffQJ/lG8bOyAOlLG3Oe4xFvV4jtD/St2MkUAT/bbCqiTWN3aBCcAk4H6GPV5n3GDOyBgWFHBW8xr+EW/dYmnnrkv2cFjxHRxNLnBG834l1FtxPNvDRSjErxusjgtZla24M+61NMPWyTmS9ZIjegTemm8U0Kcn0JRYJWOhpKfnvIZfznA0a6auDWaO5+VKv525NobHpbFJSeXoTJV4qjBorqrTiV65m+9SutNs62r9eZkq+Kj5/xFdl6wnrQ5AQMUHsg5wgFya/Z8DhuBU6El2w3ZbLjm4yGwCrZOIrbtBknLIcSzzikZG6J4xPboT3MtrpA48nLr11riRmwXtqRfGbqBD4GaPtQGcg1dFaDTk+5GjnOd/yom0STE84kyet0Lynnj2reToUP6RoOm01192SqWmfe8TOaLBlrFLMn6rOIH0E075srEqfRf9zih57ysv5NMlpd2wsad+0uxzVLpO2Y4x+iB17wdLupfdbZztyM5nEe+1n6HxSG+NVb9mdyvQPgZvMSVba4SKC6C/qa/AvUqxUKUGPJlIdrU4pEvZJi06L3GImR8tNnNr5hkbYO2QGmUHxFJT9PIEZRkx4MRk3sFErBlsm25MqL9wpYESj3aZrz0EO5/TMKy3skrwea1zUJNkc6VqYhCO/Grfu88it3I2ccDzpjvRqYu96HAnppsCSR/6J9CZh1gMkhiyXFYy+kvU898TR1GVNDbL6B2cOLOvO8xecGc7UxXvFPighuUWlkVZ2VP7qSMoNsEdy6dGgR3I5RyULB8/JbhJayKp/o87eW3d7iEz2IWWfmoLuM2rQRI77WTaDD/k5jd5jCiymGNXoOMRzOYbRzFUQYuI4EPqS6qXOUBaq1VmaMIjm9HaoO4t6SXmazhL5hmxUh8dka5o/ODu925aUtTGWRsFnyuBoNTAYZpv8NksWD8kJGiAQglu8MpxDk+PWbgtternAtJ1VBifl7PT6PUUs/u0dMnMIbMKehDGn8UydWb5KMefeATzy1Zoxesokk878vJC+OyO1bKN3c0c6NqZWlP+urGVw2bgDV0gqbC1Ys6aI+F4mKaINbqZHxai3+JNQVdJw4JKoZVdU6D3YHAWPkFCATdOCuHPev4lsl8nbn0TC0UQtZ8knhAIV0E0OWzOQgW6c7iDH/Vv0fE3TwCe9W7rd8i/do59K1a3QINK8am3Zrq+2/HTvPVOu61ix9PYWlBn7PXITfnKSdJFmWZpPMcqmd9tFfGvjVe4w5i5/lEkD6/Ya9yoRRMM8RRBeGkeouXh2X6yZIkkcErg0mJOJr856wMtzcgopuyOTdpYu/6rW20kLpyGVsQrhjUJNDQkh4F+GIkr8HbrkUoUcjx4HUcaT5Huhy04VqSRm/nDKGE0cugBUhcm8Yzw/G8MOZ2WoEufQPE2fimmJckNlqQKnA0/0rXZ0vCagns1pqqgP09BxuTh55TKftX5KVzL6cnBDd6rgTDrHbe0hUV3XtRnuhCBzoWfK/Hn4A/2RZaZg0e63zziuiGlyLUzOLs3edONbZDLUW0NJQzXPMm+6RBXlzi94Nsy/a1292H+ICnnZhURvZQylUpRDXseqg8XKmqEeCui5hR408kF1WjWQg4KjRZN9hb+oI4lwJcMIc9bep2VtsqVG5W6W4BdMpM04mUWzM8Riv0ciZGuKxdYtKAwvQom9OA6Z9AuUb8zDOnIf7NP5aLGaNzvd64urzMhujFWFaM0DIUfD/HRN7G4CJGWQAMzb7FQ5RDA78KVHfmzUIl2KnjNh4x3/giubLsp3TguWIo2JZgVnzRZfN4fmH7jLp3t7KxJKerjWZAxjl25Oamty++mJ0MbdnjxH5OjuYmDIYSLVuIQK3/vJujxsEY6/6+/8BL7uR32kM4ax3zCCjJRQ/Clgtg80PYzt0kRV6IvwB4slZiFkwpvwtqoihZkPRb4x2qOqimxqp3P24rAUvFwkefxXM96lXIbzfAm/7CYYhAfBYQ3DcKGMI3qkXeg016Nvcc+Ofrqmq2nNWLZTcNtI28Yp410IoNu/o6gY/fxKg0SnSBZLxWZFRrDZKhY7LFia63ruz7jxYG+o9FCwa3SbAkR+mCUOtARg5IADDl/P5p1PYNoV9LRmiwgBbP1kGTMnzYpjSDAzsrI1MiMYWLJzkxhJG/XZRLQ4s7fBVskiEOfYWBmBv4YxyU1hvLDmGFnMLGedSrg/mt6fozN/VkwXR/B/gavgAHdOTKWderJ1CEq4fHUnQNOkT8A3tydDW78Q4H8C3LJ6+HsAfEuQVX7NP5j2FfD1PmCmfH2nMB/0+AIwvETAN+ULFkd8ViD+cuY1dy0i/VdJtj7zXCW8Nr1jFZX7I6Y2cr5v/m4PoYixgFEEGn/rCgsZFxnGoyugkkJA8ghClgXSnOEdwyWUzcvrdEexzAuaBHD/MyuNLiWFEYhoVBwhPK56DmsrW5yfy+HmKwT4xMhNqSKa6TonWh4BFUwkRAQIgVRy8zgW7rcE4C9p3mrUnO6tQpsAtiiWFzp4GqJUzRjE936vw/K0SBBfPY2poXATK+AN/nkX4NeY2lXf2nq4fyEvgmjyEcqRpP19EJxYmbgIhvavB4Svbv0/5CQr+8W5yEwLg7CXlLR0CvXvjNRUOALkIc1mOriqZhOd66ajbJbd5AHSZSUyGv2/1/nzD3M6ed7nWGZc+3v3klog8en7oEnP4FDvHiXcYf6GwOh+ZjiTYcQMY4KVsXS2TAl0ACsCFdHkWyube6i7Nlaj9qh7nf9t0LQsAWHrT0v98J0JU/cFr7AQZIH8dP/IyG+amAXEMDk3syAWemPn8AaQpPWjrKetbe7eVXmhoarzMmWVtihNlZFp4fRwMhjQvGfQw2llpDQD2KADyUUA3DtijamJDUtr1NeaI1Zy+DgQCzNb4Mm7LuLaO4p8DmbHgX0+ooPAqRbTDkSmuhh3FFLYzAubFEiZsyPp909hI3E+mVdfxA/Fhfhbj90woK1tZPIx6t0o6gHa+gC7xJQmdr42SFNu18EgwHTals2lRj7J8Od+lSWYmsXLFcTi8K7e+BAMj4mHuHBEGqKQkm2cUo1nU7Bzv0VAMAhquK/uKihSdz5UdxYVwBA3W4GwFUqtQtmbowQjgwMb+FCJSnRTiAgF3jrylgEsqLaDaUMyWLYJ2RQYsPoW7TbYe2sInjRM9hVTNtoy6foZ3bGVVzdCLRefbvmYq45ZLFyBIASEmbZvnhbqhK/2gHHfM3RL4No0AfV1O/Xbc6vO0OgdhM/Q4Q6FRdbS6x9IGOJj8nCBHP9eRkz9zNch+Gxc5cn3eXxJRy4mNj8XTrb/olssyGu3NNDNPa0TrLRdZ6mRgw52qPMUXAaBVRsHX/yiYvozej5fVYsk2asEui/zW44Bfd3hM3ZIZId3YHkHArUbNdmb/vaZCg3yzf7SDNfW7ZCPW/GlO2TUIzWNbIvDl8QiO00ofGDkXphCoDaXNl8y61Pp/b96q+g6/1weuofT8DmzkEJDTi+OXZzfVJvOTSmh2qpfltHfz+3ukM2lpvPjkg+P6bAZfBDxseN0Kl9MTvSm4vGS26BeCd8VDsyCmr51f2HWgjmfrnPCeVjcWyVzAXxDiwhBS7jM/pQrXREmxtV68d65aAf6JJ8DXsUkJnG0nFtEynsnKWTJ8KJCoNn0Fp1XMTdnWOzXuQ1Krvkvq5duz/5jMzJZVmsek0IjRzDweM0YY0oHSkJnZHK0v9Jyi3jiIiUQJULLWaIsiR6jW5jaXKcs6lydF8i3cWRAIiFFKdSjFHDlrStzVOrWPMnqs++pRF9RHBF2nhITgVTL4QfxOVpQYW+5KBpHSeeLGpMNaTkcUvIbiKnOU3avmu5elR2Z4isASs0pSlmWKfI4SQLyjXBS/05p3prlBSVrazJQQneSP73zoZiVLYnn5EoZgSz37AcUpifo2XgKLmMhsoLRczDr8fgBgwvvqynVaYvgcyNKirIWCQapUO0jJokrSqzxsibOJYE5Y4rLA0K8Sp/6twkSguEJttXaL+nYnAEk6WVwvlbIhPxekaIVQVySsXZkd/JoShE1kAmAbI6nhIWhtZaf86zJM4Db17O3ZKrDiY4UXRvEtBU5FPAmryeplbhEfCDiohxT0Dgb9ffu0+Buy7wKRxM0VmGQej8PLK4aVSM/jqCtjNhwoDd7E8YqIQhtBLMYrIsviog2u7+Y8bTPPbdZIwy3hLKnn+MfT6cFhCkf+9V1r5HJzl+WnNn+3J0Td4pRJAw8+nNDINzkBu0sLCU/0pBUtDJ/zZqi/I4uJTkv2sBQERVHS0fed5KXkd27MJvHycxVZynwRaynUe4KrSdnPS3MUJ31UWMB8cnjVtFnMbbio8azZBtL83Qxk/mXr8jP8j19GB8XAt1ZPpouBycIVJcqMomRlrj+7Mf9qm363ARXHNW1Bi69r4arUxbB54bstaLp5vpETZo99IQEKpZJd87Yp7/KMqsp1Tuj4V9KijIzRwTMiSqw2slRv6YvN3Q0ybE/Kbl58fUNvh8vMunl23jnQbIgcNx5YECy0cOk+za5BfOKvVLQEqejNPcTiEGlrCuw1rNOTWxU7HHxhb2PkvT80Se9P7aeo5oOBiFOj2gPqN7WYcQdvxdbirJBaC5m8GpYNm/G8MWRqqwTmCUxhEL9ABr4aC8sUaYvhTOdth6Fl5otXAP3ZTOPwKCEdzeeox3uNp64v0q4m3wKoNy0Wo32p+VNblMaLbP3sgcPNF3X93pBMXoBsSneqT4zSV5DTlGR3GxmAMbHA9P3h8KmANL4dXv+gxrtQZBGYYkwDT5RV3fqYN81mZ6WHLmxQMnl5efF622g5HFtENLzDS5vSqminB3YMIAMlND96aBR2vjjKG3pYfwRGuDfNYi7NCd0/brmpeLnGuCSg6kviSSC/24f8140tppyq2Hq5XtzmhhsePYa5CBd0MAU7/8S2MQRH6+c0M2tSwY+g1esVdgAy+D6eaDm/6MoEtbDSesrfQPeuIgZZLV7PA7/KmBT0XamL67cX6zX/llf//rRv5FziHP/dZujeLCcixw6HIMsAMHx+154XrUJ9noC+Dlf6N6WezQQefBLf/v9C+SMppKU7ks+o1DkvNhNEbNGaUXcFeHnBt/peFxk6TF4FyR6hev6izSQdQQqHEaxmRlnVLkYvo5hVyi0Yk634ZuHCrrPZRkV/RXZ/zxuJc8eZtT5clvMs09JR5VbkNzW8d8quQxQrnpWa5SbEcqds9zOyZ0Nz1rkzopnObkZoPBqKPaY+C2mvP9YbTxoC/WU8orJ1cG5GHP/Qo+VjiXjrHffQHVNc2wg53RcDURvEPfs6pyW1PHEe6ZOl8bDqntdMhEnBcofI/VR17DgRpHNoXLWNBehH0bCMlRtpU7NGUa5rDlGoQq9cW8+qHwAXTcOioylk5rNHA8N6RZXy1Ucl69x/HXxLa4CyFfm7/WYLuJfWEkTlTZYcQ0qxjJgi+W7g5CrCtd596rAukR2GLtr3I5CwX7jgbnS+/DhucMDGio/saoKizJ4EEJUW3hgTjUfM4eCXNg/t9AiGrJJ0NpHbh7qML9hCq9BSRRk0iJnniqUKDz5qfgsLsiH0aTUIhoySbB/w9QNqlNH5SbVraNvLomfPomzdnFF2Ki8W1nEZfHpppJ43jGvw8lfR2PNjhk8IE4/J7PPPCNMkPdBdqepaErq1sqjFyTlUarGPYmIp81+xauMUK2ukpObSI5rLCRs9T/cXhUlhcIxP+3GUzxFpfDMHcLX/PCWS36SQDVN5Mc+SUoWN5Uk17JkaulkC00TJSy6bkeS1CU3gaAu+QdsXQ739eE3qsV18a1WS25J+1wSujvyezASulooCt/gQGHm7XvZ3iAB6e2u4VhGZsmUdV7XgOEHHeZvPvgl3Q/1JenzltYt6KQiV3n2NnQvCesjnp1hnklxX2IeIvF6iHthvsIibYT6Auc9LXy4eLf0wtUOCcEAm8i6AdJRya3TmWfn0mRpxs++BMx9JrIOwmZVLFnvxSpeiWzHxbrOM4uK8xJaKXoV6NnVZWZkVBXvHOcAu4PKLa7jJc2r4xi0hSvacujnvr4LsSiytMIoUTlfRFdpeOTKyn+sIDEaXmbPBYpMxTUZ9vtrmahdmWGjqp5QFX3x5qEVr0LFcEo6IrQZ/N2EmYxyEpSyqLn7GUYrhGsop8C7rPFJJBtopnXQteCliUjs2JZJkm8tP7PXDbbU3guH2++vw2nJrsFuitDdlRqdQhfO6xmWq7e6INdnDtx7ZLcJp0VJ8mX7aS1FX5blXT/dEtySHvVsg+ReIq2Zpa4MMm33/nulPpHjAqXlvat4teZThN7iU7pmoLm1KDLWPjn8fYK6yzR6fmqXBeMo2WLGjhOni95F5u0i7Ar4CsCweJHaqOVFiVhqYYQhTptRR4SPamLKt+3yHqOTv2W4bCFVayvKBNtA5wtmzK0nbRNxpLYZaYptc5ZetS1wsxZfWi/Y1mRjmA2svWxbmr2G76zHbUfwB8mRH8UZrAh45CPkHJRsmInbBiQG2ggRMxgS1pUM5856FeP2zGijVMay8YjyJ8OHdYtNiOm+TYRZ0CYW2ChGwtbvbTL0bp8FcoJB40vBAoLCqpagDQVUsuXJF4Q/xS16a6GPkLz8INhUJF+mIqny5FnIxapsmbKoADU4cpWVRw7AtoUylVBIVQQ4fFcGoCSveL6kOjZw8LFENR4gkC3tPwrQRXxUF2uglmfRa5n9NKNI8RbzKbkMw7EOMLJXbJsqx9Pak2Iurrj4LTFyQXlxMLk0yw8ZoctilG9sivQGBEtIehbyehconB0ALEhVKpuCwkIqLuKCtpUopgIWrgduKwtVLvHgNhWrwB9eVJUCkQoKPkYYeyML8OFyfXz4y3cj7gcWeMWDk24hpWILuQWiU23lHaVJVQ29BpgxNVlRdNy5f6Fq/5QWFwABEsAEQmEYpP+qDNDvRMQGWdyQ0FByiwsFoF/tETJ6807NSmbckVceCZ8+fAlImzUtAyBdG5BzwCbMWbZg0ZJbEFetWJUFNUNvy4ZNMG88kkPIliuPglIfrEIFihQroVKqzGs0lSpUWWyRffy4lqjG9+CJku9jYLHa7I7++Ntcl9urf9J5VIlgbzKFShBstR8/cuYLhIRiJlGu5v5BUYo3rj83OcWVjxw7kZlGrfrMebXOXXRD7IrMUMVFQ/8eYC1tx8KG4CCUivAJ9Dzt329PXdYggZiEVKIkMslSpEqTTm6hDJmynEGG1y+PxXT4BV25FlGoSDGVkgxAprVXqmrHqX0LkZO66ziWxZbktFS1GrWWWW6FOvUaJKW6aCahT/ieU3dCR7s7SdAx0jZ3ylBHOqi1ltZhXhdktXZrdOhM3VBr6+m9PmI31V5vg62qo07d7afW07jRHP7TS0NrnT7rbdAvJMIMKT+SmW269u122GmX3Vjp0H4HELKzP+Swn0jZrDnzZLhIxD1ZtmzbsWvPvgOHjhw7cerMOWcufOXajRYVfq8O6zjST79I03X9B3NcchbG1iZmxlbcRX5mImYSbq2mrqGppZ1a7+kbGBoZm2DTuCPSGjpFO7Ln0LGKLl35TlBnre4yYGJc+ycmWa9WF6vhyGssif+5Qg9c5yMp7a9nEdXDQ9JkrLIw2KBlZxKUfumo+v+1B+39ZlvJlaFZV0Q07XAHqRml57pzKQzuU4M1bhmCprBna1Tm0dSLDIAGmJnDcx8MiXSuGfN8wTE6bUM8Ou+DyUITN87MlHYz0T6KLFPznKF+0mErF1kPlu/VllnQ/3RiPU76Bmz22jDhOCkaHz1kPzD5dyRsZcil+13D0HSecNwk6TQwwMpR040zN1dmNrUt8klhyfm5BXy1DKEUbRdmpvt0xaaaLKewXuIg49kKgVr/yQ2qQZa0GwSng3FXX4ti8XDk4twBsT/OPiFQZx8SFWonlej19WHjrm1RiukvHdi0EGcvAKAMMgJQdG0OLVt2bU6nSRfylKMOQ2EHqiZ6nj53vdS+FlW5U3/Prgx3/r9mSylgaFPf1rB0456u2e0Q9Jz21UJbSSPrm/XJyc7ZRe/SLeJVofKYte1555y5OH94eYXynkNbY+gZ+jzF6vmY5HnNepVqgTybFpVI7husYpTtX1ySEs63stlIl7HfejY4i4aOtPMxX9HCaXS1M4vPLpG6a+3f9jFzGKwO2h6T1cwYgpYzQ4n3N+vzwcmO2UmdUQ2m0BobqqVgfDi/+fZ0e4vE4m8NeK/N0VZS2nXjmbMyu7MYMitPeKkhtxiBRoXAkxUusSQhaJyiLC8Wr8phzDcjTEiltYTELR1IJnU28d8ClHkfqrsmLhL/dcXkSlQOjfoVoYyeKDgf0yJC4Q5UubRyIuC9PlK5DHe+N4L9KEg7mrWMTwgDMTEuEgbIG4HaUm2w+03b9Zvatd2y++8bklHiLLHRGiNSNzLMBMfSbUz0zqJMtPVARNN6uNGDEGJFoeBbcK6TKmfP9/1KBPVghzaW+q/XX7XQqi/Iz7qrs82aI7BEy8DUc3jPOut/SciObnxzqAVCurDU0iRsWfpW1mg5n98iUrOzRG0bXuBnR9cRnrhy1hznOKfHOEdAc7rSnBBwjqAfDOaGTgtk3lQBGtpwNL0CBKYGFYlbKgCB4LwRaYm5kYdk4gTGozcpBZ1d43IiqokrNvxCSWTFAWlFKRkLv2WctvgTRsCS7zlzafrqWLvWeCN13+TVQrOu7ejIu4pr4w6tlGfF4Y0z2OBABDEkAEICDDgMcIRLDKMF541OgxTu/YDpmiRt8LlwMiwyoSANLQG0JJD4DSxGkrUAWrIg/Y4CRWUPDJp94PTnDfYX8CRsjFWrwnIN94172nqcGjymRG+IvPCps7Qq1OFVwjRWqnZMDz+lKew8NEyELL6nSeFoXwKJkIYu3MbymnOqcvksVx5quDwX6y+xtcuSnxiOSUDceNQ4EZhUDmQkPyjiqQwCJS38ScB0PDSGuITlwdm/RqzTjtWu4h7aJ4/vxh+pIin8x1knqMxT3vujTriY0eOWctB4rWUt8xfAW7wJTdwefmHRdfgaYyOAM2iWmrUZH/MtHbGTHoGRs0fb31UVnb8n4mIEPTy3MOLprPU9HdwLwwAAAAA=) format('woff2'),\n      url(https://assets.genius.com/fonts/programme_light.woff?1649950983) format('woff');\n    font-style: normal;\n    font-weight: 100;\n  }\n\n  @font-face {\n    font-family: 'Programme';\n    src: url(https://assets.genius.com/fonts/programme_light_italic.woff2?1649950983) format('woff2'),\n      url(https://assets.genius.com/fonts/programme_light_italic.woff?1649950983) format('woff');\n    font-style: italic;\n    font-weight: 100;\n  }\n</style>\n\n    \n    <script>\n  window['Genius.cmp'] = window['Genius.cmp'] || [];\n</script>\n\n\n\n    <style data-styled=\"true\" data-styled-version=\"5.1.0\">.jxHdAP{word-wrap:break-word;word-break:break-word;font:100 1rem/1.5 'Programme',Arial,sans-serif;}/*!sc*/\n.jxHdAP h1{font-size:1.5rem;}/*!sc*/\n.jxHdAP h2{font-size:1.25rem;}/*!sc*/\n.jxHdAP h3{font-size:1.125rem;}/*!sc*/\n.jxHdAP h4,.jxHdAP h5,.jxHdAP h6{font-size:1rem;}/*!sc*/\n.jxHdAP h1,.jxHdAP h2,.jxHdAP h3,.jxHdAP h4,.jxHdAP h5,.jxHdAP h6{font-family:'Programme',Arial,sans-serif;font-weight:600;margin:1rem 0 0;}/*!sc*/\n.jxHdAP a{color:#fff;-webkit-text-decoration:underline;text-decoration:underline;}/*!sc*/\n.jxHdAP p{margin:1rem 0;}/*!sc*/\n.jxHdAP p:empty{display:none;}/*!sc*/\n.jxHdAP small{font-size:.75rem;}/*!sc*/\n.jxHdAP img{display:block;height:auto;margin-left:auto;margin-right:auto;max-height:325px;width:auto;}/*!sc*/\n.jxHdAP blockquote{margin:1rem 0 0 .5rem;padding-left:1rem;position:relative;display:block;}/*!sc*/\n.jxHdAP blockquote:before{content:'\\201C';position:absolute;top:-.1em;left:-0.3em;font:bold 2.25rem/1 \"Times New Roman\";color:inherit;opacity:.75;}/*!sc*/\n.jxHdAP pre{font-family:inherit;margin:1rem 0;}/*!sc*/\n.jxHdAP code{font-family:'Consolas','Monaco','Lucida Console','Liberation Mono','DejaVu Sans Mono','Bitstream Vera Sans Mono','Courier New',monospace;font-size:.75rem;}/*!sc*/\n.jxHdAP table{margin:1rem 0;width:100%;}/*!sc*/\n.jxHdAP th,.jxHdAP td{padding:.5rem .5rem 0;}/*!sc*/\n.jxHdAP th{border-bottom:1px solid #a58735;font-weight:bold;text-align:left;}/*!sc*/\n.jxHdAP ul,.jxHdAP ol{list-style-type:none;margin:1rem 0;padding:0;}/*!sc*/\n.jxHdAP ul ul,.jxHdAP ol ul,.jxHdAP ul ol,.jxHdAP ol ol{margin:0;padding:0 0 0 1rem;}/*!sc*/\n.jxHdAP ul li,.jxHdAP ol li{padding-left:1.6em;position:relative;}/*!sc*/\n.jxHdAP ul li:before{content:'\\2022';left:calc(1.6em / 2.5);position:absolute;}/*!sc*/\n.jxHdAP ol{counter-reset:ordered_list_counter;}/*!sc*/\n.jxHdAP ol li:before{font-feature-settings:'tnum';content:counter(ordered_list_counter) '. ';counter-increment:ordered_list_counter;left:-.5rem;position:absolute;text-align:right;width:1.6em;}/*!sc*/\n.jxHdAP dd{margin:0 0 0 1rem;}/*!sc*/\n.jxHdAP hr{border:1px solid #a58735;border-width:1px 0 0;}/*!sc*/\n.jxHdAP iframe{max-width:100%;}/*!sc*/\n.jxHdAP ins,.jxHdAP ins > *{-webkit-text-decoration:none;text-decoration:none;background:inherit !important;color:#24c609;}/*!sc*/\n.jxHdAP ins img,.jxHdAP ins > * img{border:2px solid #24c609;}/*!sc*/\n.jxHdAP del,.jxHdAP del > *{background:inherit !important;color:#ff1414;}/*!sc*/\n.jxHdAP del img,.jxHdAP del > * img{border:2px solid #ff1414;}/*!sc*/\n.jxHdAP .embedly_preview{font-size:1rem;margin-top:1rem;margin-bottom:1rem;}/*!sc*/\n.jxHdAP .embedly_preview a{font-weight:bold;}/*!sc*/\n.jxHdAP .embedly_preview iframe{display:block;margin:auto;width:100%;}/*!sc*/\n.jxHdAP .embedly_preview--video,.jxHdAP .embedly_preview--vertical-video{position:relative;width:100%;height:0;}/*!sc*/\n.jxHdAP .embedly_preview--video iframe,.jxHdAP .embedly_preview--vertical-video iframe{position:absolute;top:0;left:0;width:100%;height:100%;}/*!sc*/\n.jxHdAP .embedly_preview--vertical-video{padding-bottom:177.77777777777777%;}/*!sc*/\n.jxHdAP .embedly_preview--video{padding-bottom:56.25%;}/*!sc*/\n.jxHdAP .embedly_preview-thumb{position:relative;height:0;width:100%;padding-bottom:50%;overflow:hidden;border-bottom:1px solid #fff;}/*!sc*/\n.jxHdAP .embedly_preview-thumb img{position:absolute;top:0;left:0;min-width:100%;min-height:100%;}/*!sc*/\n.jxHdAP .embedly_preview .gray_container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;border:1px solid #fff;font-weight:400;color:inherit;word-wrap:break-word;word-break:break-word;}/*!sc*/\n.jxHdAP .embedly_preview .gray_container:hover{background-color:#fff;color:#b54624;}/*!sc*/\n.jxHdAP .embedly_preview > a{-webkit-text-decoration:none;text-decoration:none;}/*!sc*/\n.jxHdAP .embedly_preview .gray_container .embedly_preview-text{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:.75rem;font-weight:100;}/*!sc*/\n.jxHdAP .embedly_preview .gray_container .embedly_preview-text .embedly_preview-provider{-webkit-order:1;-ms-flex-order:1;order:1;font-size:.75rem;margin-bottom:.25rem;}/*!sc*/\n.jxHdAP .embedly_preview .gray_container .embedly_preview-text .embedly_preview-dash{display:none;}/*!sc*/\n.jxHdAP .embedly_preview .gray_container .embedly_preview-text .embedly_preview-title{-webkit-order:2;-ms-flex-order:2;order:2;margin-bottom:.25rem;line-height:1.33;}/*!sc*/\n.jxHdAP .embedly_preview .gray_container .embedly_preview-text .embedly_preview-description{-webkit-order:3;-ms-flex-order:3;order:3;font-size:.75rem;line-height:1.5;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;max-height:4.5em;overflow:hidden;}/*!sc*/\ndata-styled.g1[id=\"RichText__Container-oz284w-0\"]{content:\"jxHdAP,\"}/*!sc*/\n.eNxXUK{overflow:hidden;border-radius:100%;border:1px solid #fff;}/*!sc*/\ndata-styled.g4[id=\"UserAvatar__Container-ln6u1h-0\"]{content:\"eNxXUK,\"}/*!sc*/\n.bcJdHU{position:relative;background-position:center;width:24px;height:24px;background-color:#a58735;background-size:cover;}/*!sc*/\n.jtGbpX{position:relative;background-position:center;padding-bottom:100%;background-color:#a58735;background-size:cover;}/*!sc*/\ndata-styled.g13[id=\"SizedImage__Container-sc-1hyeaua-0\"]{content:\"bcJdHU,jtGbpX,\"}/*!sc*/\n.iMdmgx:not([src]){visibility:hidden;}/*!sc*/\ndata-styled.g14[id=\"SizedImage__Image-sc-1hyeaua-1\"]{content:\"iMdmgx,\"}/*!sc*/\n.UJCmI{position:absolute;width:100%;height:100%;object-fit:cover;}/*!sc*/\ndata-styled.g15[id=\"SizedImage__NoScript-sc-1hyeaua-2\"]{content:\"UJCmI,\"}/*!sc*/\n.kNjZBr{font-size:.75rem;font-weight:100;line-height:1;-webkit-letter-spacing:1px;-moz-letter-spacing:1px;-ms-letter-spacing:1px;letter-spacing:1px;text-transform:capitalize;color:#fff;}/*!sc*/\n.hEVcyj{font-size:.625rem;font-weight:400;line-height:1;-webkit-letter-spacing:1px;-moz-letter-spacing:1px;-ms-letter-spacing:1px;letter-spacing:1px;text-transform:uppercase;color:#fff;}/*!sc*/\n.kokouQ{font-size:.75rem;font-weight:400;line-height:1;-webkit-letter-spacing:1px;-moz-letter-spacing:1px;-ms-letter-spacing:1px;letter-spacing:1px;text-transform:uppercase;color:#fff;}/*!sc*/\ndata-styled.g18[id=\"TextLabel-sc-8kw9oj-0\"]{content:\"kNjZBr,hEVcyj,kokouQ,\"}/*!sc*/\n.dluqjZ svg{display:block;fill:none;height:2.25rem;margin:auto;stroke:#9a9a9a;stroke-width:.05rem;width:2.25rem;height:36px;width:36px;}/*!sc*/\n.dluqjZ svg circle{-webkit-animation:hFBEL 2s ease-out infinite;animation:hFBEL 2s ease-out infinite;background-color:inherit;}/*!sc*/\ndata-styled.g22[id=\"PlaceholderSpinner__Container-r4gz6r-0\"]{content:\"dluqjZ,\"}/*!sc*/\n.duhTAK{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;cursor:pointer;position:relative;height:41px;}/*!sc*/\n.duhTAK svg{fill:#fff;height:18px;width:18px;}/*!sc*/\ndata-styled.g42[id=\"PageHeaderInboxdesktop__Container-sc-18cqfy1-0\"]{content:\"duhTAK,\"}/*!sc*/\n.cZffjt{margin-left:.375rem;}/*!sc*/\n@media (max-width:1593px){.cZffjt{display:none;}}/*!sc*/\ndata-styled.g43[id=\"PageHeaderInboxdesktop__Label-sc-18cqfy1-1\"]{content:\"cZffjt,\"}/*!sc*/\n.emAXHD{cursor:pointer;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;height:41px;}/*!sc*/\ndata-styled.g46[id=\"PageHeaderMenu__Container-sc-13myo8l-0\"]{content:\"emAXHD,\"}/*!sc*/\n.jLiNjI{margin-left:.5rem;}/*!sc*/\n.jLiNjI span{display:block;}/*!sc*/\n@media (max-width:1593px){.jLiNjI{display:none;}}/*!sc*/\ndata-styled.g47[id=\"PageHeaderMenu__Iq-sc-13myo8l-1\"]{content:\"jLiNjI,\"}/*!sc*/\n.cziiuX svg{display:block;height:22px;fill:#fff;}/*!sc*/\n.fRTMWj svg{display:block;height:19px;fill:#fff;}/*!sc*/\ndata-styled.g55[id=\"SocialLinks__Link-jwyj6b-1\"]{content:\"cziiuX,fRTMWj,\"}/*!sc*/\n.iuNSEV{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}/*!sc*/\n.iuNSEV .SocialLinks__Link-jwyj6b-1 + .SocialLinks__Link-jwyj6b-1{margin-left:1.5rem;}/*!sc*/\ndata-styled.g56[id=\"SocialLinks__Container-jwyj6b-2\"]{content:\"iuNSEV,\"}/*!sc*/\n.fmeUAz{position:absolute;background:#ff1464;border-radius:18px;padding:0 .2rem;line-height:1;height:18px;min-width:18px;left:18px;top:4px;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%);text-align:center;}/*!sc*/\ndata-styled.g57[id=\"PageHeadershared__InboxUnreadCount-sc-1cjax99-0\"]{content:\"fmeUAz,\"}/*!sc*/\n.boDKcJ{background-color:#121212;}/*!sc*/\ndata-styled.g63[id=\"PageFooterdesktop__Container-hz1fx1-0\"]{content:\"boDKcJ,\"}/*!sc*/\n.gwrcCS{display:block;color:inherit;font-weight:100;cursor:pointer;}/*!sc*/\ndata-styled.g64[id=\"PageFooterdesktop__Link-hz1fx1-1\"]{content:\"gwrcCS,\"}/*!sc*/\n.gUdeqB{display:grid;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;grid-template-columns:[grid-start] repeat(3,minmax(14px,44px)) [center-start] repeat(6,minmax(14px,44px)) [center-end] repeat(3,minmax(14px,44px)) [grid-end];grid-column-gap:60px;grid-row-gap:40px;color:#fff;padding:3rem 60px 2.25rem;}/*!sc*/\n.gUdeqB .PageFooterdesktop__Link-hz1fx1-1 + .PageFooterdesktop__Link-hz1fx1-1{margin-top:1rem;}/*!sc*/\ndata-styled.g65[id=\"PageFooterdesktop__Section-hz1fx1-2\"]{content:\"gUdeqB,\"}/*!sc*/\n.mJdfj{grid-column:span 6;}/*!sc*/\ndata-styled.g66[id=\"PageFooterdesktop__Half-hz1fx1-3\"]{content:\"mJdfj,\"}/*!sc*/\n.hMUvCn{grid-column:span 3;}/*!sc*/\ndata-styled.g67[id=\"PageFooterdesktop__Quarter-hz1fx1-4\"]{content:\"hMUvCn,\"}/*!sc*/\n.diwZPD{grid-column:span 3;grid-column:7 / span 3;}/*!sc*/\ndata-styled.g68[id=\"PageFooterdesktop__OffsetQuarter-hz1fx1-5\"]{content:\"diwZPD,\"}/*!sc*/\n.iDkyVM{display:block;color:#9a9a9a;font-weight:100;font-size:.625rem;}/*!sc*/\ndata-styled.g69[id=\"PageFooterdesktop__FinePrint-hz1fx1-6\"]{content:\"iDkyVM,\"}/*!sc*/\n.kNXBDG{display:grid;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;grid-template-columns:[grid-start] repeat(3,minmax(14px,44px)) [center-start] repeat(6,minmax(14px,44px)) [center-end] repeat(3,minmax(14px,44px)) [grid-end];grid-column-gap:60px;grid-row-gap:40px;color:#fff;padding:3rem 60px 2.25rem;border-top:.15rem solid #2a2a2a;padding-top:2.25rem;padding-bottom:2.25rem;}/*!sc*/\n.kNXBDG .PageFooterdesktop__Link-hz1fx1-1 + .PageFooterdesktop__Link-hz1fx1-1{margin-top:1rem;}/*!sc*/\ndata-styled.g70[id=\"PageFooterdesktop__Bottom-hz1fx1-7\"]{content:\"kNXBDG,\"}/*!sc*/\n.eIiYRJ{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;grid-column:1 / -1;-webkit-align-items:baseline;-webkit-box-align:baseline;-ms-flex-align:baseline;align-items:baseline;}/*!sc*/\ndata-styled.g71[id=\"PageFooterdesktop__Row-hz1fx1-8\"]{content:\"eIiYRJ,\"}/*!sc*/\n.hNrwqx{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-flex:1;-ms-flex:1;flex:1;}/*!sc*/\ndata-styled.g72[id=\"PageFooterdesktop__FlexWrap-hz1fx1-9\"]{content:\"hNrwqx,\"}/*!sc*/\n.bMBKQI{color:inherit;}/*!sc*/\n.bMBKQI:after{content:'•';margin:0 1rem;}/*!sc*/\ndata-styled.g73[id=\"PageFooterdesktop__VerifiedArtists-hz1fx1-10\"]{content:\"bMBKQI,\"}/*!sc*/\n.dcpJwP{margin-right:1rem;}/*!sc*/\ndata-styled.g74[id=\"PageFooterdesktop__Label-hz1fx1-11\"]{content:\"dcpJwP,\"}/*!sc*/\n.uGviF{font-size:2.25rem;font-weight:100;line-height:1.125;margin-bottom:2.25rem;}/*!sc*/\ndata-styled.g75[id=\"PageFooterSocial__Slogan-sc-14u22mq-0\"]{content:\"uGviF,\"}/*!sc*/\n.kTXFZQ{display:block;color:inherit;margin:0 .25rem;}/*!sc*/\ndata-styled.g76[id=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0\"]{content:\"kTXFZQ,\"}/*!sc*/\n.hAxKUd{display:block;color:inherit;}/*!sc*/\n.hAxKUd + .PageFooterHotSongLinks__Link-sc-1adazwo-0:before{content:'•';margin:0 .75rem;}/*!sc*/\ndata-styled.g77[id=\"PageFooterHotSongLinks__Link-sc-1adazwo-0\"]{content:\"hAxKUd,\"}/*!sc*/\n.dIgauN{width:100%;}/*!sc*/\ndata-styled.g94[id=\"LeaderboardOrMarquee__Sticky-yjd3i4-0\"]{content:\"dIgauN,\"}/*!sc*/\n.ljczSm{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;border-radius:1.25rem;padding:.25rem .75rem;border:1px solid #000;font-family:'HelveticaNeue',Arial,sans-serif;font-size:.75rem;color:#000;}/*!sc*/\n.ljczSm:disabled{opacity:.5;}/*!sc*/\n.ljczSm:hover:not(:disabled){background-color:#000;color:#fff;}/*!sc*/\n.ljczSm:hover:not(:disabled) span{color:#fff;}/*!sc*/\n.ljczSm:hover:not(:disabled) svg{fill:#fff;}/*!sc*/\n.krYtET{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;border-radius:1.25rem;padding:.25rem .75rem;border:1px solid #fff;font-family:'HelveticaNeue',Arial,sans-serif;font-size:.75rem;color:#fff;}/*!sc*/\n.krYtET:disabled{opacity:.5;}/*!sc*/\n.krYtET:hover:not(:disabled){background-color:#fff;color:#000;mix-blend-mode:screen;}/*!sc*/\n.krYtET:hover:not(:disabled) span{color:#000;}/*!sc*/\n.krYtET:hover:not(:disabled) svg{fill:#000;}/*!sc*/\ndata-styled.g96[id=\"SmallButton__Container-mg33hl-0\"]{content:\"ljczSm,krYtET,\"}/*!sc*/\n.ixMmYX{font-family:inherit;font-size:inherit;font-weight:inherit;color:inherit;line-height:1;-webkit-text-decoration:underline;text-decoration:underline;}/*!sc*/\ndata-styled.g98[id=\"TextButton-sc-192nsqv-0\"]{content:\"ixMmYX,\"}/*!sc*/\n.hmntnn{color:#fff;-webkit-text-decoration:underline;text-decoration:underline;}/*!sc*/\ndata-styled.g106[id=\"SongPage__FeedbackLink-sc-19xhmoi-1\"]{content:\"hmntnn,\"}/*!sc*/\n.gNhDlQ{background-color:#ff1464;text-align:center;color:#fff;font-weight:100;font-size:.75rem;padding:1rem;}/*!sc*/\ndata-styled.g107[id=\"SongPage__FeedbackBanner-sc-19xhmoi-2\"]{content:\"gNhDlQ,\"}/*!sc*/\n.cXvCRB{position:relative;}/*!sc*/\ndata-styled.g108[id=\"SongPage__Section-sc-19xhmoi-3\"]{content:\"cXvCRB,\"}/*!sc*/\n.gcjDma{position:absolute;top:calc(-1 * (0px + 0));}/*!sc*/\ndata-styled.g109[id=\"SongPage__SectionAnchor-sc-19xhmoi-4\"]{content:\"gcjDma,\"}/*!sc*/\n.UKjRP{position:relative;}/*!sc*/\ndata-styled.g110[id=\"SongPage__LyricsWrapper-sc-19xhmoi-5\"]{content:\"UKjRP,\"}/*!sc*/\n.iTrjDY{padding-bottom:3rem;}/*!sc*/\ndata-styled.g111[id=\"SongPage__PageFooter-sc-19xhmoi-6\"]{content:\"iTrjDY,\"}/*!sc*/\n.hfRKjb{display:grid;grid-template-columns: [page-start] 2.25rem [grid-start] 1fr [left-start] repeat(6,1fr) [left-end right-start] repeat(4,1fr) [right-end] 1fr [grid-end] 2.25rem [page-end];grid-gap:0.75rem;}/*!sc*/\n@media screen and (min-width:1164px){.hfRKjb{grid-template-columns: [page-start] 1fr [grid-start] 5rem [left-start] repeat(6,5rem) [left-end right-start] repeat(4,5rem) [right-end] 5rem [grid-end] 1fr [page-end];}}/*!sc*/\n@media screen and (min-width:1526px){.hfRKjb{grid-template-columns: [page-start] 1fr [grid-start] 6rem [left-start] repeat(6,6rem) [left-end right-start] repeat(4,6rem) [right-end] 6rem [grid-end] 1fr [page-end];}}/*!sc*/\ndata-styled.g113[id=\"SongPageGriddesktop__TwoColumn-sc-1px5b71-1\"]{content:\"hfRKjb,\"}/*!sc*/\n.hrzyda{z-index:1;background-image:linear-gradient(#b54624,#a58735);padding-top:1.5rem;padding-bottom:2.25rem;position:relative;grid-template-columns: 2.25rem [header-left-start] repeat(3,1fr) [header-left-end header-center-start] repeat(4,1fr) [header-center-end header-right-start] repeat(4,1fr) [header-right-end header-end] 2.25rem;}/*!sc*/\n@media screen and (min-width:1164px){.hrzyda{min-height:calc( (3 * 5rem) + (2 * 0.75rem) - (1.5rem + 2.25rem) );grid-template-columns: 1fr [header-left-start] repeat(3,5rem) [header-left-end header-center-start] repeat(4,5rem) [header-center-end header-right-start] repeat(4,5rem) [header-right-end header-end] 1fr;}}/*!sc*/\n@media screen and (min-width:1526px){.hrzyda{min-height:calc( (3 * 6rem) + (2 * 0.75rem) - (1.5rem + 2.25rem) );grid-template-columns: 1fr [header-left-start] repeat(3,6rem) [header-left-end header-center-start] repeat(4,6rem) [header-center-end header-right-start] repeat(4,6rem) [header-right-end header-end] 1fr;}}/*!sc*/\ndata-styled.g125[id=\"SongHeaderVariantdesktop__Container-sc-12tszai-0\"]{content:\"hrzyda,\"}/*!sc*/\n.fEcDuq{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;width:100%;height:100%;position:relative;grid-column:header-left-start / header-left-end;z-index:1;}/*!sc*/\n@media screen and (min-width:1164px){.fEcDuq{position:absolute;padding-top:1.5rem;}}/*!sc*/\ndata-styled.g127[id=\"SongHeaderVariantdesktop__Left-sc-12tszai-2\"]{content:\"fEcDuq,\"}/*!sc*/\n.bpBizA{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;grid-column:header-center-start / header-center-end;color:#fff;}/*!sc*/\ndata-styled.g128[id=\"SongHeaderVariantdesktop__Center-sc-12tszai-3\"]{content:\"bpBizA,\"}/*!sc*/\n.jryaLr{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;grid-column:header-right-start / header-right-end;}/*!sc*/\ndata-styled.g129[id=\"SongHeaderVariantdesktop__Right-sc-12tszai-4\"]{content:\"jryaLr,\"}/*!sc*/\n.lbUWUz{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end;margin-right:calc(2 * 0.75rem);width:100%;}/*!sc*/\n@media screen and (min-width:1164px){.lbUWUz{width:calc(100% + 1.5rem);max-height:calc(100% + 1.5rem);}}/*!sc*/\n@media screen and (min-width:1526px){.lbUWUz{width:calc(100% - 1.313rem);}}/*!sc*/\ndata-styled.g130[id=\"SongHeaderVariantdesktop__CoverArtContainer-sc-12tszai-5\"]{content:\"lbUWUz,\"}/*!sc*/\n.cicAJN img{display:inline-block;box-shadow:0 0 .75rem 0 rgba(0,0,0,0.05);}/*!sc*/\n@media screen and (min-width:1164px){.cicAJN{height:100%;text-align:right;}.cicAJN img{max-height:100%;}}/*!sc*/\ndata-styled.g131[id=\"SongHeaderVariantdesktop__CoverArt-sc-12tszai-6\"]{content:\"cicAJN,\"}/*!sc*/\n.iWUdKG{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;font-size:2rem;font-weight:400;font-feature-settings:'ss07','ss08','ss11','ss12','ss14','ss15','ss16','ss18','ss19','ss20','ss21';color:#fff;line-height:1.125;max-width:calc(3.5*5rem + 3*0.75rem);word-wrap:break-word;}/*!sc*/\ndata-styled.g132[id=\"SongHeaderVariantdesktop__Title-sc-12tszai-7\"]{content:\"iWUdKG,\"}/*!sc*/\n.bFjDxc{opacity:1;}/*!sc*/\ndata-styled.g135[id=\"SongHeaderVariantdesktop__HiddenMask-sc-12tszai-10\"]{content:\"bFjDxc,\"}/*!sc*/\n.ayFeg{line-height:1.33;}/*!sc*/\ndata-styled.g136[id=\"SongHeaderVariantdesktop__Artist-sc-12tszai-11\"]{content:\"ayFeg,\"}/*!sc*/\n.evqGUV{position:absolute;width:100%;bottom:calc(0px + 3rem);z-index:-2;}/*!sc*/\ndata-styled.g137[id=\"SongHeaderVariantdesktop__Sentinel-sc-12tszai-12\"]{content:\"evqGUV,\"}/*!sc*/\n.kZmmHP{font:100 1.125rem/1.5 'Programme',Arial,sans-serif;padding:0;grid-auto-rows:max-content;min-height:min(var(--annotation-height),100vh);word-wrap:break-word;word-break:break-word;padding-top:2.25rem;position:relative;width:100%;}/*!sc*/\ndata-styled.g142[id=\"Lyrics__Root-sc-1ynbvzw-1\"]{content:\"kZmmHP,\"}/*!sc*/\n.lYpBt{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;grid-column:left-start / left-end;}/*!sc*/\ndata-styled.g143[id=\"Lyrics__Footer-sc-1ynbvzw-2\"]{content:\"lYpBt,\"}/*!sc*/\n.kuxJDq.kuxJDq{margin:0.75rem 0 calc(0.75rem + 1.313rem);}/*!sc*/\ndata-styled.g144[id=\"Lyrics__FooterShareButtons-sc-1ynbvzw-3\"]{content:\"kuxJDq,\"}/*!sc*/\n.jYfhrf{grid-column:left-start / left-end;padding-top:.5rem;padding-bottom:1.5rem;padding-right:1.5rem;}/*!sc*/\ndata-styled.g147[id=\"Lyrics__Container-sc-1ynbvzw-6\"]{content:\"jYfhrf,\"}/*!sc*/\n.eIbATv{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;margin-bottom:.25rem;font-weight:100;font-size:1rem;}/*!sc*/\ndata-styled.g187[id=\"Fieldshared__FieldLabel-dxskot-2\"]{content:\"eIbATv,\"}/*!sc*/\n.fxOdHk{position:relative;}/*!sc*/\ndata-styled.g190[id=\"Dropdown__Container-aitjcr-0\"]{content:\"fxOdHk,\"}/*!sc*/\n.cjTarM{display:block;border-bottom:1px solid transparent;}/*!sc*/\ndata-styled.g191[id=\"Dropdown__Toggle-aitjcr-1\"]{content:\"cjTarM,\"}/*!sc*/\n.dsaqcS{-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:0;margin:0;background:inherit;border:1px solid #fff;border-radius:0;padding:.75rem;color:#fff;font-size:1rem;font-family:'HelveticaNeue',Arial,sans-serif;font-weight:100;overflow-y:auto;resize:vertical;display:block;width:100%;overflow-y:hidden;resize:none;}/*!sc*/\n.dsaqcS:disabled{opacity:.3;background-image:url(\"data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2011%2015%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20fill%3D%22%23fff%22%20clip-rule%3D%22evenodd%22%20d%3D%22M1.642%206.864H.369V14.5h10.182V6.864H9.278V4.318a3.818%203.818%200%200%200-7.636%200v2.546zm1.272%200h5.091V4.318a2.546%202.546%200%200%200-5.09%200v2.546z%22%2F%3E%3C%2Fsvg%3E\");background-size:.625rem auto;background-position:calc(100% - .5rem + 1px) center;background-repeat:no-repeat;padding-right:calc(.75rem * 2 + .625rem);}/*!sc*/\n.dsaqcS::-webkit-input-placeholder{color:rgba(255,255,255,0.5);}/*!sc*/\n.dsaqcS::-moz-placeholder{color:rgba(255,255,255,0.5);}/*!sc*/\n.dsaqcS:-ms-input-placeholder{color:rgba(255,255,255,0.5);}/*!sc*/\n.dsaqcS::placeholder{color:rgba(255,255,255,0.5);}/*!sc*/\ndata-styled.g237[id=\"Textarea__Input-u7wct2-0\"]{content:\"dsaqcS,\"}/*!sc*/\n.cabqMy .Fieldshared__FieldControlWithLabel-dxskot-1{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column-reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse;}/*!sc*/\n.cabqMy .Fieldshared__FieldLabel-dxskot-2{-webkit-transition:all .2s;transition:all .2s;pointer-events:none;}/*!sc*/\n.cabqMy input:placeholder-shown + .Fieldshared__FieldLabel-dxskot-2{cursor:text;-webkit-transform-origin:left bottom;-ms-transform-origin:left bottom;transform-origin:left bottom;-webkit-transform:translate(.75rem,2.5rem);-ms-transform:translate(.75rem,2.5rem);transform:translate(.75rem,2.5rem);}/*!sc*/\n.cabqMy input:focus + .Fieldshared__FieldLabel-dxskot-2,.cabqMy input:not(:placeholder-shown) + .Fieldshared__FieldLabel-dxskot-2{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);transform:translate(0,0);}/*!sc*/\ndata-styled.g246[id=\"FloatingField-sc-187lo5b-0\"]{content:\"cabqMy,\"}/*!sc*/\n.iJYdoP{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;font-weight:100;font-size:.75rem;height:3rem;background-color:#b54624;color:#fff;position:-webkit-sticky;position:sticky;top:calc(0px);z-index:6;}/*!sc*/\ndata-styled.g267[id=\"StickyNavdesktop__Container-sc-60o25r-0\"]{content:\"iJYdoP,\"}/*!sc*/\n.cpvLYi{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-left:1rem;}/*!sc*/\ndata-styled.g268[id=\"StickyNavdesktop__Left-sc-60o25r-1\"]{content:\"cpvLYi,\"}/*!sc*/\n.kMDkxm{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-right:1rem;}/*!sc*/\ndata-styled.g269[id=\"StickyNavdesktop__Right-sc-60o25r-2\"]{content:\"kMDkxm,\"}/*!sc*/\n.iMBkpA{margin-right:1.5rem;}/*!sc*/\ndata-styled.g270[id=\"StickyNavdesktop__Inboxes-sc-60o25r-3\"]{content:\"iMBkpA,\"}/*!sc*/\n.kphUUq{margin-right:1.5rem;line-height:1.33;color:#fff;}/*!sc*/\n.kphUUq span{white-space:nowrap;color:#fff;}/*!sc*/\n.kphUUq:hover{-webkit-text-decoration:underline;text-decoration:underline;}/*!sc*/\ndata-styled.g271[id=\"StickyNavdesktop__SiteLink-sc-60o25r-4\"]{content:\"kphUUq,\"}/*!sc*/\n.bGGxIJ{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-left:2.25rem;}/*!sc*/\ndata-styled.g272[id=\"StickyNavdesktop__Subnavigation-sc-60o25r-5\"]{content:\"bGGxIJ,\"}/*!sc*/\n.fCsyNi{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;}/*!sc*/\ndata-styled.g276[id=\"HeaderMetadata__Container-sc-1p42fnf-0\"]{content:\"fCsyNi,\"}/*!sc*/\n.ePueMf{display:grid;grid-template-columns:repeat(2,1fr);-webkit-column-gap:0.75rem;column-gap:0.75rem;}/*!sc*/\ndata-styled.g277[id=\"HeaderMetadata__Grid-sc-1p42fnf-1\"]{content:\"ePueMf,\"}/*!sc*/\n.gcxzH{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap-reverse;-ms-flex-wrap:wrap-reverse;flex-wrap:wrap-reverse;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;-webkit-align-items:flex-start;-webkit-box-align:flex-start;-ms-flex-align:flex-start;align-items:flex-start;gap:0.75rem;}/*!sc*/\ndata-styled.g278[id=\"HeaderMetadata__Lower-sc-1p42fnf-2\"]{content:\"gcxzH,\"}/*!sc*/\n.jROWVH{font-weight:100;font-size:.75rem;line-height:1.33;margin-bottom:1.5rem;color:#fff;}/*!sc*/\n.cKIVtz{font-weight:100;font-size:.75rem;line-height:1.33;margin-bottom:1.5rem;color:#fff;-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end;}/*!sc*/\ndata-styled.g279[id=\"HeaderMetadata__Section-sc-1p42fnf-3\"]{content:\"jROWVH,cKIVtz,\"}/*!sc*/\n.dIyyeJ{display:block;line-height:1.2;font-size:.625rem;color:#fff;}/*!sc*/\ndata-styled.g280[id=\"HeaderMetadata__Label-sc-1p42fnf-4\"]{content:\"dIyyeJ,\"}/*!sc*/\n.hppQiC{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:end;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end;-webkit-align-items:flex-end;-webkit-box-align:flex-end;-ms-flex-align:flex-end;align-items:flex-end;margin-left:auto;gap:.75rem;}/*!sc*/\ndata-styled.g281[id=\"HeaderMetadata__Controls-sc-1p42fnf-5\"]{content:\"hppQiC,\"}/*!sc*/\n.fJDnoB{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}/*!sc*/\n.fJDnoB > *:not(:last-child){margin-right:1.5rem;}/*!sc*/\n.fJDnoB svg{fill:#fff;}/*!sc*/\ndata-styled.g282[id=\"HeaderMetadata__Stats-sc-1p42fnf-6\"]{content:\"fJDnoB,\"}/*!sc*/\n.dwjgaZ svg{fill:#fff;}/*!sc*/\ndata-styled.g283[id=\"HeaderMetadata__ViewCredits-sc-1p42fnf-7\"]{content:\"dwjgaZ,\"}/*!sc*/\n.gkAIfz{margin-top:3rem;font-size:.75rem;font-weight:100;line-height:1.33;-webkit-text-decoration:none;text-decoration:none;color:#fff;}/*!sc*/\n@media screen and (min-width:1164px){.gkAIfz{width:calc(3 * 5rem + 2 * 0.75rem);}}/*!sc*/\n.gkAIfz:hover span{-webkit-text-decoration:none;text-decoration:none;}/*!sc*/\ndata-styled.g284[id=\"HeaderBio__Wrapper-oaxemt-0\"]{content:\"gkAIfz,\"}/*!sc*/\n.hTbbsa{-webkit-text-decoration:underline;text-decoration:underline;white-space:nowrap;}/*!sc*/\n.hTbbsa svg{fill:#fff;}/*!sc*/\ndata-styled.g285[id=\"HeaderBio__ViewBio-oaxemt-1\"]{content:\"hTbbsa,\"}/*!sc*/\n.fUGBbC{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-align-items:flex-end;-webkit-box-align:flex-end;-ms-flex-align:flex-end;align-items:flex-end;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;font-size:.75rem;line-height:1.33;font-weight:100;color:#fff;}/*!sc*/\ndata-styled.g291[id=\"HeaderTracklistVariant__Container-doi9xi-0\"]{content:\"fUGBbC,\"}/*!sc*/\n.iinkOX{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;line-height:1.33;margin-right:.25rem;}/*!sc*/\ndata-styled.g292[id=\"HeaderTracklistVariant__AlbumWrapper-doi9xi-1\"]{content:\"iinkOX,\"}/*!sc*/\n.gyOWcZ{word-break:break-word;}/*!sc*/\n.gyOWcZ svg{fill:#fff;}/*!sc*/\ndata-styled.g293[id=\"HeaderTracklistVariant__Album-doi9xi-2\"]{content:\"gyOWcZ,\"}/*!sc*/\n.xQwqG svg{height:1.33em;margin-left:calc(-1em * 0.548625);vertical-align:top;}/*!sc*/\n.xQwqG::before{content:'\\2003';font-size:0.548625em;}/*!sc*/\ndata-styled.g294[id=\"InlineSvg__Wrapper-b788hd-0\"]{content:\"xQwqG,\"}/*!sc*/\n.boJhnK{font-weight:100;color:inherit;}/*!sc*/\n.hwdSYP{font-size:.75rem;font-weight:100;-webkit-text-decoration:underline;text-decoration:underline;color:inherit;}/*!sc*/\n.hwdSYP:hover{-webkit-text-decoration:none;text-decoration:none;}/*!sc*/\n.gHBbjJ{font-weight:100;-webkit-text-decoration:underline;text-decoration:underline;color:inherit;}/*!sc*/\n.gHBbjJ:hover{-webkit-text-decoration:none;text-decoration:none;}/*!sc*/\n.bLjCRC{font-weight:100;color:#fff;}/*!sc*/\ndata-styled.g295[id=\"Link-h3isu4-0\"]{content:\"boJhnK,hwdSYP,gHBbjJ,bLjCRC,\"}/*!sc*/\n.cLBJdA{-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:auto;background-color:transparent;border-radius:1.25rem;padding:.5rem 1.313rem;border:1px solid #000;font-family:'HelveticaNeue',Arial,sans-serif;font-size:1rem;font-weight:400;line-height:1.1;color:#000;cursor:pointer;}/*!sc*/\n.cLBJdA svg{fill:currentColor;}/*!sc*/\n.cLBJdA:hover{background-color:#000;color:#fff;}/*!sc*/\n.cLBJdA:hover span{color:#fff;}/*!sc*/\n.cLBJdA:hover svg{fill:#fff;}/*!sc*/\n.kPiSeV{-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:auto;background-color:transparent;border-radius:1.25rem;padding:.5rem 1.313rem;border:1px solid #fff;font-family:'HelveticaNeue',Arial,sans-serif;font-size:1rem;font-weight:400;line-height:1.1;color:#fff;cursor:pointer;}/*!sc*/\n.kPiSeV svg{fill:currentColor;}/*!sc*/\n.kPiSeV:hover{background-color:#fff;color:#000;mix-blend-mode:screen;}/*!sc*/\n.kPiSeV:hover span{color:#000;}/*!sc*/\n.kPiSeV:hover svg{fill:#000;}/*!sc*/\ndata-styled.g297[id=\"Button__Container-sc-1874dbw-0\"]{content:\"cLBJdA,kPiSeV,\"}/*!sc*/\n.dbwAaA{font-weight:100;background-image:linear-gradient(#b54624,#a58735);padding-bottom:2.25rem;}/*!sc*/\ndata-styled.g305[id=\"About__Grid-ut4i9m-0\"]{content:\"dbwAaA,\"}/*!sc*/\n.jCTumN{grid-column:left-start / left-end;text-align:center;padding-top:calc(2.25rem + .375rem);}/*!sc*/\ndata-styled.g306[id=\"About__Container-ut4i9m-1\"]{content:\"jCTumN,\"}/*!sc*/\n.kcXwIY{font-size:5rem;font-weight:normal;color:#fff;}/*!sc*/\ndata-styled.g307[id=\"About__Title-ut4i9m-2\"]{content:\"kcXwIY,\"}/*!sc*/\n.kJtCmu{position:relative;font-weight:100;padding:0 1.313rem 0 1.313rem;color:#fff;}/*!sc*/\ndata-styled.g308[id=\"SongInfo__Container-nekw6x-0\"]{content:\"kJtCmu,\"}/*!sc*/\n.iRKrFW{font-size:2.25rem;font-weight:400;margin-bottom:2.25rem;line-height:1.1;}/*!sc*/\ndata-styled.g309[id=\"SongInfo__Title-nekw6x-1\"]{content:\"iRKrFW,\"}/*!sc*/\n.lgBflw{display:grid;text-align:left;grid-template-columns:1fr 1fr;grid-gap:0.75rem;}/*!sc*/\ndata-styled.g310[id=\"SongInfo__Columns-nekw6x-2\"]{content:\"lgBflw,\"}/*!sc*/\n.fognin{font-size:.75rem;}/*!sc*/\n.fognin:not(:last-child){margin-bottom:1.5rem;}/*!sc*/\ndata-styled.g311[id=\"SongInfo__Credit-nekw6x-3\"]{content:\"fognin,\"}/*!sc*/\n.ipOVSb{font-size:.625rem;}/*!sc*/\ndata-styled.g312[id=\"SongInfo__Label-nekw6x-4\"]{content:\"ipOVSb,\"}/*!sc*/\n.hPWhcr{position:absolute;top:calc(-1.5 * (0px + 3rem));}/*!sc*/\ndata-styled.g313[id=\"SongInfo__SectionAnchor-nekw6x-5\"]{content:\"hPWhcr,\"}/*!sc*/\n.cYtdTH{position:relative;margin:1.313rem 0 1rem 0;padding-bottom:56.25%;width:100%;}/*!sc*/\n.cYtdTH iframe{position:absolute;top:0;left:0;width:100%;height:100%;}/*!sc*/\ndata-styled.g314[id=\"MusicVideo__Container-sc-1980jex-0\"]{content:\"cYtdTH,\"}/*!sc*/\n.jzPNvv{text-align:center;color:#fff;}/*!sc*/\n.jzPNvv .ExpandableContent__TextButton-sc-1165iv-2{margin-top:.75rem;}/*!sc*/\n.jzPNvv .ExpandableContent__TextButton-sc-1165iv-2,.jzPNvv .ExpandableContent__Button-sc-1165iv-1{line-height:1.25;}/*!sc*/\n.jzPNvv svg{fill:#fff;}/*!sc*/\ndata-styled.g318[id=\"ExpandableContent__ButtonContainer-sc-1165iv-3\"]{content:\"jzPNvv,\"}/*!sc*/\n.huhsMa{overflow:hidden;max-height:250px;-webkit-mask:linear-gradient(rgba(0,0,0,1) 0%,transparent);mask:linear-gradient(rgba(0,0,0,1) 0%,transparent);}/*!sc*/\ndata-styled.g319[id=\"ExpandableContent__Content-sc-1165iv-4\"]{content:\"huhsMa,\"}/*!sc*/\n.kqeVkf{position:relative;display:grid;grid-template-columns:[grid-start] repeat(2,1fr) [grid-end];text-align:start;}/*!sc*/\ndata-styled.g320[id=\"PrimaryAlbum__Container-cuci8p-0\"]{content:\"kqeVkf,\"}/*!sc*/\n.lkEvgY{position:absolute;top:calc(-1.5 * 2.25rem);}/*!sc*/\ndata-styled.g321[id=\"PrimaryAlbum__SectionAnchor-cuci8p-1\"]{content:\"lkEvgY,\"}/*!sc*/\n.fMIxkO{margin:0 3rem 2.5rem 3rem;}/*!sc*/\ndata-styled.g322[id=\"PrimaryAlbum__CoverArt-cuci8p-2\"]{content:\"fMIxkO,\"}/*!sc*/\n.hGGOcK{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;margin-bottom:2.5rem;}/*!sc*/\ndata-styled.g323[id=\"PrimaryAlbum__AlbumDetails-cuci8p-3\"]{content:\"hGGOcK,\"}/*!sc*/\n.NcWGs{font-size:1rem;color:#fff;}/*!sc*/\ndata-styled.g324[id=\"PrimaryAlbum__Title-cuci8p-4\"]{content:\"NcWGs,\"}/*!sc*/\n.hyzSGh{font-size:.75rem;color:#fff;}/*!sc*/\ndata-styled.g325[id=\"PrimaryAlbum__Artist-cuci8p-5\"]{content:\"hyzSGh,\"}/*!sc*/\n.lbmvpT{color:#fff;-webkit-scroll-margin-top:calc(0px + 3rem);-moz-scroll-margin-top:calc(0px + 3rem);-ms-scroll-margin-top:calc(0px + 3rem);scroll-margin-top:calc(0px + 3rem);}/*!sc*/\ndata-styled.g326[id=\"SongDescription__Container-sc-615rvk-0\"]{content:\"lbmvpT,\"}/*!sc*/\n.kRzyD{text-align:left;}/*!sc*/\ndata-styled.g328[id=\"SongDescription__Content-sc-615rvk-2\"]{content:\"kRzyD,\"}/*!sc*/\n.iyrTpw{border-bottom:1px solid #fff;margin-bottom:3rem;padding-bottom:3rem;}/*!sc*/\ndata-styled.g332[id=\"InnerSectionDivider-sc-1x4onqw-0\"]{content:\"iyrTpw,\"}/*!sc*/\n.bIlJhm{padding-bottom:1.313rem;}/*!sc*/\ndata-styled.g340[id=\"SongComments__Grid-sc-131p4fy-0\"]{content:\"bIlJhm,\"}/*!sc*/\n.hVwpyI{grid-column:left-start / left-end;padding-top:calc(2.25rem + .375rem);}/*!sc*/\ndata-styled.g341[id=\"SongComments__Container-sc-131p4fy-1\"]{content:\"hVwpyI,\"}/*!sc*/\n.kojbqH{font-size:5rem;margin-bottom:1rem;text-align:center;}/*!sc*/\ndata-styled.g342[id=\"SongComments__Title-sc-131p4fy-2\"]{content:\"kojbqH,\"}/*!sc*/\n.ldOStC{margin-top:1.313rem;padding:.5rem 0 .5rem .75rem;border:1px solid #000;font-size:1.125rem;font-weight:100;width:100%;cursor:pointer;display:block;color:inherit;}/*!sc*/\ndata-styled.g344[id=\"SongComments__Add-sc-131p4fy-4\"]{content:\"ldOStC,\"}/*!sc*/\n.euQZer{margin-top:2.25rem;text-align:center;font-weight:100;color:#000;}/*!sc*/\ndata-styled.g346[id=\"SongComments__CTA-sc-131p4fy-6\"]{content:\"euQZer,\"}/*!sc*/\n.jcROPx{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;font-size:.75rem;font-family:'Programme',Arial,sans-serif;color:#fff;}/*!sc*/\n.jcROPx svg{display:block;height:.65em;fill:#fff;}/*!sc*/\n.jcROPx:disabled{color:#9a9a9a;}/*!sc*/\n.dCKKNS{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;font-size:.75rem;font-family:'Programme',Arial,sans-serif;color:#000;}/*!sc*/\n.dCKKNS svg{display:block;height:.75rem;fill:#000;}/*!sc*/\n.dCKKNS:disabled{color:#9a9a9a;}/*!sc*/\ndata-styled.g374[id=\"LabelWithIcon__Container-sc-1ri57wg-0\"]{content:\"jcROPx,dCKKNS,\"}/*!sc*/\n.kMItKF{-webkit-text-decoration:none;text-decoration:none;font-weight:100;font-size:.75rem;margin-left:.25rem;color:inherit;}/*!sc*/\n.gjSNHg{-webkit-text-decoration:underline;text-decoration:underline;font-weight:100;font-size:.75rem;margin-left:.25rem;color:inherit;}/*!sc*/\n.gjSNHg:hover{-webkit-text-decoration:none;text-decoration:none;}/*!sc*/\n.IVJjV{-webkit-text-decoration:underline;text-decoration:underline;font-weight:100;font-size:.75rem;margin-left:.25rem;color:inherit;}/*!sc*/\ndata-styled.g375[id=\"LabelWithIcon__Label-sc-1ri57wg-1\"]{content:\"kMItKF,gjSNHg,IVJjV,\"}/*!sc*/\n.kwgaZp{grid-column:auto / right-end;justify-self:right;}/*!sc*/\ndata-styled.g376[id=\"RightSidebar__Container-pajcl2-0\"]{content:\"kwgaZp,\"}/*!sc*/\n.crbHJx{height:100%;min-height:calc(px + 0.75rem);max-height:calc(100% - (0px + 3rem + 0.75rem));padding-top:0.75rem;}/*!sc*/\ndata-styled.g377[id=\"SidebarAd__Container-sc-1cw85h6-0\"]{content:\"crbHJx,\"}/*!sc*/\n.kAKdir{position:-webkit-sticky;position:sticky;top:calc(0px + 3rem + 0.75rem);}/*!sc*/\ndata-styled.g378[id=\"SidebarAd__Ad-sc-1cw85h6-1\"]{content:\"kAKdir,\"}/*!sc*/\n.cSKAwQ{display:grid;grid-template-columns:1fr [center-start] 1fr [center-end] 1fr;background-color:black;}/*!sc*/\ndata-styled.g379[id=\"SectionLeaderboard__Container-sc-1pjk0bw-0\"]{content:\"cSKAwQ,\"}/*!sc*/\n.fPpEQG{padding:.75rem 0;grid-column:center-start / center-end;}/*!sc*/\ndata-styled.g380[id=\"SectionLeaderboard__Center-sc-1pjk0bw-1\"]{content:\"fPpEQG,\"}/*!sc*/\n.cJGzHp{background-color:black;min-height:calc(3rem + 90px);display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;}/*!sc*/\ndata-styled.g381[id=\"Leaderboard-da326u-0\"]{content:\"cJGzHp,\"}/*!sc*/\n.eJULet{z-index:2;width:100%;-webkit-transition:bottom 0.2s;transition:bottom 0.2s;position:fixed;bottom:0;visibility:hidden;}/*!sc*/\ndata-styled.g382[id=\"AppleMusicPlayer__PositioningContainer-uavgzr-0\"]{content:\"eJULet,\"}/*!sc*/\n.gnMmAT{width:100%;position:absolute;bottom:0;}/*!sc*/\ndata-styled.g383[id=\"AppleMusicPlayer__IframeWrapper-uavgzr-1\"]{content:\"gnMmAT,\"}/*!sc*/\n.hHaNaR{grid-column:left-start / right-end;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;position:absolute;width:100%;height:calc(100% - 16px);bottom:16px;left:16px;border:2px solid #000;background:#fff;margin-left:-16px;}/*!sc*/\ndata-styled.g384[id=\"AppleMusicPlayer__IframeWrapperLoader-uavgzr-2\"]{content:\"hHaNaR,\"}/*!sc*/\n.gnVZvh{grid-column:left-start / right-end;justify-self:end;-webkit-transition:height .2s,width .2s;transition:height .2s,width .2s;width:calc(100% + 2 * 16px);height:64px;margin-right:-16px;}/*!sc*/\ndata-styled.g385[id=\"AppleMusicPlayer__Iframe-uavgzr-3\"]{content:\"gnVZvh,\"}/*!sc*/\n.lbozyU{line-height:13px;}/*!sc*/\n.lbozyU svg{position:relative;fill:#fff;top:1px;height:13px;}/*!sc*/\ndata-styled.g387[id=\"PageHeaderLogo__Link-sc-13m4mcv-0\"]{content:\"lbozyU,\"}/*!sc*/\n.dawSPu{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;background:#fff;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:calc(10 * 1rem + .75rem);height:1.5rem;}/*!sc*/\n.dawSPu .PageHeadershared__Dropdown-sc-1cjax99-1{right:0;}/*!sc*/\ndata-styled.g388[id=\"PageHeaderSearchdesktop__Form-sc-1o6zub0-0\"]{content:\"dawSPu,\"}/*!sc*/\n.cmIqeW{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;position:relative;cursor:pointer;padding:0 .75rem;}/*!sc*/\n.cmIqeW svg{fill:#000;width:.625rem;}/*!sc*/\ndata-styled.g389[id=\"PageHeaderSearchdesktop__Icon-sc-1o6zub0-1\"]{content:\"cmIqeW,\"}/*!sc*/\n.hTPksM{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;position:relative;border:none;border-radius:0;background:#fff;color:#000;font-family:'Programme',Arial,sans-serif;font-feature-settings:'ss07','ss08','ss11','ss12','ss14','ss15','ss16','ss18','ss19','ss20','ss21';font-size:inherit;font-weight:inherit;line-height:inherit;-webkit-appearance:none;overflow:hidden;resize:none;outline:none;padding-left:.75rem;}/*!sc*/\n.hTPksM::-webkit-input-placeholder,.hTPksM::-webkit-input-placeholder{color:#000;font-size:.75rem;}/*!sc*/\n.hTPksM::-moz-placeholder,.hTPksM::-webkit-input-placeholder{color:#000;font-size:.75rem;}/*!sc*/\n.hTPksM:-ms-input-placeholder,.hTPksM::-webkit-input-placeholder{color:#000;font-size:.75rem;}/*!sc*/\n.hTPksM::placeholder,.hTPksM::-webkit-input-placeholder{color:#000;font-size:.75rem;}/*!sc*/\n.hTPksM::-ms-input-placeholder{color:#000;font-size:.75rem;}/*!sc*/\n.hTPksM:focus::-webkit-input-placeholder{color:#9a9a9a;font-size:.75rem;}/*!sc*/\n.hTPksM:focus::-moz-placeholder{color:#9a9a9a;font-size:.75rem;}/*!sc*/\n.hTPksM:focus:-ms-input-placeholder{color:#9a9a9a;font-size:.75rem;}/*!sc*/\n.hTPksM:focus::placeholder{color:#9a9a9a;font-size:.75rem;}/*!sc*/\ndata-styled.g390[id=\"PageHeaderSearchdesktop__Input-sc-1o6zub0-2\"]{content:\"hTPksM,\"}/*!sc*/\n.loHKiK{position:absolute;top:calc(100vh - (0px + 3rem));bottom:0;width:100%;z-index:-2;}/*!sc*/\ndata-styled.g391[id=\"SectionScrollSentinel__Element-sc-1c8cvz5-0\"]{content:\"loHKiK,\"}/*!sc*/\n.kGJQLs{grid-column:grid-start / grid-end;text-align:start;-webkit-column-count:2;column-count:2;font-size:.75rem;}/*!sc*/\ndata-styled.g392[id=\"AlbumTracklist__Container-sc-123giuo-0\"]{content:\"kGJQLs,\"}/*!sc*/\n.iLxPGk{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;margin-bottom:.75rem;-webkit-break-inside:avoid;break-inside:avoid;color:#fff;padding:0 0.75rem;}/*!sc*/\ndata-styled.g393[id=\"AlbumTracklist__Track-sc-123giuo-1\"]{content:\"iLxPGk,\"}/*!sc*/\n.guEaas{display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;position:relative;padding:.25rem 0;}/*!sc*/\n.guEaas:after{content:'';position:absolute;top:0;right:-0.75rem;bottom:0;left:-0.75rem;border:1px solid;}/*!sc*/\n.bPLsDz{display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;position:relative;padding:.25rem 0;}/*!sc*/\ndata-styled.g394[id=\"AlbumTracklist__TrackName-sc-123giuo-2\"]{content:\"guEaas,bPLsDz,\"}/*!sc*/\n.epTVob{padding-right:.5rem;}/*!sc*/\ndata-styled.g395[id=\"AlbumTracklist__TrackNumber-sc-123giuo-3\"]{content:\"epTVob,\"}/*!sc*/\n.esoPOn{font-size:2.25rem;font-weight:400;margin-bottom:1.313rem;line-height:1.1;}/*!sc*/\ndata-styled.g396[id=\"QuestionList__Title-sc-1a58vti-0\"]{content:\"esoPOn,\"}/*!sc*/\n.gatDgA{position:relative;font-weight:100;color:#fff;}/*!sc*/\ndata-styled.g397[id=\"QuestionList__Container-sc-1a58vti-1\"]{content:\"gatDgA,\"}/*!sc*/\n.niBCl{position:absolute;top:calc(-1.5 * (0px + 3rem));}/*!sc*/\ndata-styled.g398[id=\"QuestionList__SectionAnchor-sc-1a58vti-2\"]{content:\"niBCl,\"}/*!sc*/\n.fyTOee{margin-top:2.25rem;}/*!sc*/\ndata-styled.g399[id=\"QuestionList__Empty-sc-1a58vti-3\"]{content:\"fyTOee,\"}/*!sc*/\n.dcUlUV{display:grid;gap:.5rem;}/*!sc*/\ndata-styled.g415[id=\"QuestionForm__Container-wsr2cj-0\"]{content:\"dcUlUV,\"}/*!sc*/\n.dhIhSa{font-size:.75rem;color:#fff;}/*!sc*/\ndata-styled.g429[id=\"Attribution__Container-sc-1nmry9o-0\"]{content:\"dhIhSa,\"}/*!sc*/\n.iPNsXE{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;margin-bottom:1rem;}/*!sc*/\ndata-styled.g430[id=\"Attribution__Header-sc-1nmry9o-1\"]{content:\"iPNsXE,\"}/*!sc*/\n.drLwGn{margin-bottom:2.25rem;font-size:.75rem;}/*!sc*/\ndata-styled.g435[id=\"UnreviewedAnnotation__Container-sc-1fjei54-0\"]{content:\"drLwGn,\"}/*!sc*/\n.hCfSzi{padding:0.8rem 0;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;border:1px solid #ff1464;color:#ff1464;background:repeating-linear-gradient( 45deg,#ffffff,#ffffff 7px,#ffecf3 7px,#ffecf3 14px );}/*!sc*/\ndata-styled.g436[id=\"UnreviewedAnnotation__Unreviewed-sc-1fjei54-1\"]{content:\"hCfSzi,\"}/*!sc*/\n.ggQBPB{-webkit-text-decoration:underline;text-decoration:underline;cursor:pointer;}/*!sc*/\ndata-styled.g437[id=\"UnreviewedAnnotation__ExplainerToggle-sc-1fjei54-2\"]{content:\"ggQBPB,\"}/*!sc*/\n.jEcpKK{fill:#ff1464;height:.75rem;margin-right:.25rem;}/*!sc*/\ndata-styled.g438[id=\"UnreviewedAnnotation__Alert-sc-1fjei54-3\"]{content:\"jEcpKK,\"}/*!sc*/\n.dyewdM{margin-bottom:1.5rem;width:100%;font-weight:100;font-size:.75rem;}/*!sc*/\n@media screen and (min-width:1164px){.dyewdM{font-size:1rem;}}/*!sc*/\ndata-styled.g486[id=\"ShareButtons__Root-jws18q-0\"]{content:\"dyewdM,\"}/*!sc*/\n.ePvBqA{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;gap:.75rem;}/*!sc*/\ndata-styled.g487[id=\"ShareButtons__Container-jws18q-1\"]{content:\"ePvBqA,\"}/*!sc*/\n.kJxpEi{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;font-size:inherit;line-height:1;font-family:'Programme',Arial,sans-serif;font-weight:100;white-space:pre;}/*!sc*/\n.kJxpEi svg{display:block;height:1rem;}/*!sc*/\ndata-styled.g488[id=\"ShareButtons__Button-jws18q-2\"]{content:\"kJxpEi,\"}/*!sc*/\n.ueUKD{-webkit-flex:1;-ms-flex:1;flex:1;text-align:center;font-size:inherit;}/*!sc*/\n.ueUKD .react-share__ShareButton{width:100%;}/*!sc*/\ndata-styled.g489[id=\"ShareButtons__ButtonWrapper-jws18q-3\"]{content:\"ueUKD,\"}/*!sc*/\n.bnFCuj{grid-column:left-start / left-end;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:start;-webkit-box-align:start;-ms-flex-align:start;align-items:start;gap:1rem;}/*!sc*/\n.bnFCuj:not(:empty){margin:1rem 0 0;}/*!sc*/\ndata-styled.g498[id=\"LyricsControls__Container-sghmdv-0\"]{content:\"bnFCuj,\"}/*!sc*/\n.jYdMbk{margin-left:.375rem;}/*!sc*/\n.jYdMbk svg{width:.625rem;}/*!sc*/\ndata-styled.g500[id=\"LyricsControls__SmallButtonIcon-sghmdv-2\"]{content:\"jYdMbk,\"}/*!sc*/\n.kkpCaw{display:none;grid-column:page-start / page-end;font-weight:100;margin-bottom:3rem;}/*!sc*/\ndata-styled.g502[id=\"LyricsEditdesktop__Container-sc-19lxrhp-0\"]{content:\"kkpCaw,\"}/*!sc*/\n.ewKWbT{grid-column:page-start / page-end;border-bottom:1px solid #000;margin-bottom:1.313rem;background-color:#e9e9e9;padding-top:3rem;}/*!sc*/\ndata-styled.g503[id=\"LyricsEditdesktop__Header-sc-19lxrhp-1\"]{content:\"ewKWbT,\"}/*!sc*/\n.bcLwQh{grid-column:left-start / left-end;padding-bottom:.75rem;}/*!sc*/\ndata-styled.g504[id=\"LyricsEditdesktop__HeaderLeft-sc-19lxrhp-2\"]{content:\"bcLwQh,\"}/*!sc*/\n.hFPGxa{grid-column:left-start / left-end;position:relative;}/*!sc*/\ndata-styled.g505[id=\"LyricsEditdesktop__Editor-sc-19lxrhp-3\"]{content:\"hFPGxa,\"}/*!sc*/\n.lbdVJq{grid-column:right-start / right-end;margin-left:1.313rem;}/*!sc*/\ndata-styled.g506[id=\"LyricsEditdesktop__ControlsContainer-sc-19lxrhp-4\"]{content:\"lbdVJq,\"}/*!sc*/\n.bwjuqY{position:-webkit-sticky;position:sticky;top:calc(0px + 3rem + 2.25rem);}/*!sc*/\ndata-styled.g507[id=\"LyricsEditdesktop__Controls-sc-19lxrhp-5\"]{content:\"bwjuqY,\"}/*!sc*/\n.bXbziL{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;padding:.5rem 1rem;margin:0;margin-bottom:.75rem;}/*!sc*/\ndata-styled.g508[id=\"LyricsEditdesktop__Button-sc-19lxrhp-6\"]{content:\"bXbziL,\"}/*!sc*/\n.ldjaSd{margin-top:1.313rem;}/*!sc*/\ndata-styled.g510[id=\"LyricsEditdesktop__Explainer-sc-19lxrhp-8\"]{content:\"ldjaSd,\"}/*!sc*/\n.pDArA{font-size:.75rem;}/*!sc*/\n.pDArA ul{padding-left:1rem;}/*!sc*/\n.pDArA li{list-style-type:disc;}/*!sc*/\ndata-styled.g517[id=\"LyricsEditExplainer__Container-sc-1aeph76-0\"]{content:\"pDArA,\"}/*!sc*/\n.eDBQeK{font-weight:700;}/*!sc*/\ndata-styled.g518[id=\"LyricsEditExplainer__Bold-sc-1aeph76-1\"]{content:\"eDBQeK,\"}/*!sc*/\n.rncXA{font-style:italic;}/*!sc*/\ndata-styled.g519[id=\"LyricsEditExplainer__Italic-sc-1aeph76-2\"]{content:\"rncXA,\"}/*!sc*/\n.cRrFdP{display:block;position:relative;}/*!sc*/\ndata-styled.g571[id=\"Tooltip__Container-sc-1uvy5c2-0\"]{content:\"cRrFdP,\"}/*!sc*/\n.dvOJud{cursor:pointer;}/*!sc*/\ndata-styled.g573[id=\"Tooltip__Children-sc-1uvy5c2-2\"]{content:\"dvOJud,\"}/*!sc*/\n.jIHZck{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;}/*!sc*/\n.jIHZck > *{margin-right:0.75rem;margin-bottom:0.75rem;white-space:pre;}/*!sc*/\ndata-styled.g605[id=\"AnnotationEditActions__Container-sc-10x4ilq-0\"]{content:\"jIHZck,\"}/*!sc*/\n.ceKRFE{font-size:.625rem;margin:.75rem 0;text-align:left;}/*!sc*/\ndata-styled.g646[id=\"SongTags__Title-xixwg3-0\"]{content:\"ceKRFE,\"}/*!sc*/\n.bZsZHM{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;grid-gap:.75rem;}/*!sc*/\ndata-styled.g647[id=\"SongTags__Container-xixwg3-1\"]{content:\"bZsZHM,\"}/*!sc*/\n.evrydK{color:#fff;border:1px solid #fff;padding:.5rem;font-size:.75rem;font-weight:700;}/*!sc*/\n.evrydK:visited{color:#fff;}/*!sc*/\n.evrydK:hover{background-color:#fff;color:#000;mix-blend-mode:screen;}/*!sc*/\n.evrydK:hover span{color:#000;}/*!sc*/\n.evrydK:hover svg{fill:#000;}/*!sc*/\n.kykqAa{color:#fff;border:1px solid #fff;padding:.5rem;font-size:.75rem;}/*!sc*/\n.kykqAa:visited{color:#fff;}/*!sc*/\n.kykqAa:hover{background-color:#fff;color:#000;mix-blend-mode:screen;}/*!sc*/\n.kykqAa:hover span{color:#000;}/*!sc*/\n.kykqAa:hover svg{fill:#000;}/*!sc*/\ndata-styled.g648[id=\"SongTags__Tag-xixwg3-2\"]{content:\"evrydK,kykqAa,\"}/*!sc*/\n.eMjKRh{position:relative;}/*!sc*/\ndata-styled.g649[id=\"Pyong__Container-yq95kq-0\"]{content:\"eMjKRh,\"}/*!sc*/\n.goeTDT svg{width:16px;height:16px;}/*!sc*/\n.goeTDT .PageHeadershared__InboxUnreadCount-sc-1cjax99-0{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;border:1px solid #fff;height:16px;min-width:16px;top:6px;left:16px;}/*!sc*/\n.goeTDT .PageHeadershared__Dropdown-sc-1cjax99-1{width:100%;left:0;top:calc(3rem + 1rem);max-height:calc(100vh - 3rem - 3rem);}/*!sc*/\ndata-styled.g675[id=\"PageHeaderLoggedIn__Item-sc-1i45pz8-0\"]{content:\"goeTDT,\"}/*!sc*/\n.kgoOvC{position:relative;}/*!sc*/\n.kgoOvC .PageHeadershared__Dropdown-sc-1cjax99-1{left:0;top:calc(3rem + 1rem);max-height:calc(100vh - 3rem - 3rem);}/*!sc*/\ndata-styled.g676[id=\"PageHeaderLoggedIn__AvatarMenu-sc-1i45pz8-1\"]{content:\"kgoOvC,\"}/*!sc*/\n.iHELGn{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;}/*!sc*/\n.iHELGn .PageHeaderLoggedIn__Item-sc-1i45pz8-0 + .PageHeaderLoggedIn__Item-sc-1i45pz8-0,.iHELGn .PageHeaderLoggedIn__Item-sc-1i45pz8-0 + .PageHeaderLoggedIn__AvatarMenu-sc-1i45pz8-1{margin-left:1rem;}/*!sc*/\ndata-styled.g677[id=\"PageHeaderLoggedIn__Container-sc-1i45pz8-2\"]{content:\"iHELGn,\"}/*!sc*/\n.EJZPZ{line-height:normal;}/*!sc*/\ndata-styled.g681[id=\"OptOutButton__Container-e48zu0-0\"]{content:\"EJZPZ,\"}/*!sc*/\nhtml,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,input,button,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;}/*!sc*/\nh1,h2,h3,h4,h5,h6{font-size:100%;font-weight:inherit;}/*!sc*/\nhtml{line-height:1;}/*!sc*/\nol,ul{list-style:none;}/*!sc*/\ntable{border-collapse:collapse;border-spacing:0;}/*!sc*/\ncaption,th,td{text-align:left;font-weight:normal;vertical-align:middle;}/*!sc*/\nq,blockquote{quotes:none;}/*!sc*/\nq:before,blockquote:before,q:after,blockquote:after{content:\"\";content:none;}/*!sc*/\na img{border:none;}/*!sc*/\narticle,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block;}/*!sc*/\nbutton{background:unset;box-shadow:unset;border:unset;text-shadow:unset;cursor:pointer;}/*!sc*/\nbody{overflow-x:hidden;background-color:#fff;color:#000;font-family:'Programme',Arial,sans-serif;line-height:1.45;-webkit-text-size-adjust:100%;}/*!sc*/\nimg{max-width:100%;}/*!sc*/\nli{list-style:none;}/*!sc*/\na{color:#7d8fe8;-webkit-text-decoration:none;text-decoration:none;-webkit-tap-highlight-color:rgba(0,0,0,0);}/*!sc*/\n*,*:before,*:after{box-sizing:border-box;}/*!sc*/\nhr{border:1px solid #e9e9e9;border-width:1px 0 0;margin:1rem 0;}/*!sc*/\npre{white-space:pre-wrap;}/*!sc*/\n::selection{background-color:#b2d7fe;}/*!sc*/\n.noscroll{overflow:hidden;position:absolute;height:100vh;width:100vw;}/*!sc*/\n.noscroll-fixed{overflow:hidden;position:fixed;width:100%;}/*!sc*/\n.grecaptcha-badge{visibility:hidden;}/*!sc*/\ndata-styled.g756[id=\"sc-global-ixFuEb1\"]{content:\"sc-global-ixFuEb1,\"}/*!sc*/\n#cf_alert_div{display:none !important;}/*!sc*/\ndata-styled.g757[id=\"sc-global-gdfBvm1\"]{content:\"sc-global-gdfBvm1,\"}/*!sc*/\nhtml{font-size:16px;}/*!sc*/\n@media screen and (min-width:1526px){html{font-size:18px;}}/*!sc*/\ndata-styled.g758[id=\"sc-global-PeqFD1\"]{content:\"sc-global-PeqFD1,\"}/*!sc*/\n@-webkit-keyframes hFBEL{0%{stroke-dasharray:150%;stroke-dashoffset:0%;}50%{stroke-dasharray:300%;}100%{stroke-dasharray:150%;stroke-dashoffset:600%;}}/*!sc*/\n@keyframes hFBEL{0%{stroke-dasharray:150%;stroke-dashoffset:0%;}50%{stroke-dasharray:300%;}100%{stroke-dasharray:150%;stroke-dashoffset:600%;}}/*!sc*/\ndata-styled.g759[id=\"sc-keyframes-hFBEL\"]{content:\"hFBEL,\"}/*!sc*/\n</style>\n\n    \n\n\n  \n    <meta content=\"https://genius.com/Ttng-chinchilla-lyrics\" property=\"og:url\" />\n  \n\n  \n    <meta content=\"music.song\" property=\"og:type\" />\n  \n\n  \n    <meta content=\"TTNG – Chinchilla\" property=\"og:title\" />\n  \n\n  \n    <meta content=\"So far we&#39;ve lost focus / Let&#39;s just concentrate on words that could mean everything / On nights like this / We drink ourselves dry / And make promises / Without intention / So\" property=\"og:description\" />\n  \n\n  \n    <meta content=\"http://images.genius.com/7b7890cf624378fcedd2e8ce44ea234b.640x640x1.jpg\" property=\"og:image\" />\n  \n\n\n\n\n\n  <meta content=\"https://genius.com/Ttng-chinchilla-lyrics\" property=\"twitter:url\" />\n\n  <meta content=\"music.song\" property=\"twitter:type\" />\n\n  <meta content=\"TTNG – Chinchilla\" property=\"twitter:title\" />\n\n  <meta content=\"So far we&#39;ve lost focus / Let&#39;s just concentrate on words that could mean everything / On nights like this / We drink ourselves dry / And make promises / Without intention / So\" property=\"twitter:description\" />\n\n  <meta content=\"http://images.genius.com/7b7890cf624378fcedd2e8ce44ea234b.640x640x1.jpg\" property=\"twitter:image\" />\n\n  <meta content=\"@Genius\" property=\"twitter:site\" />\n\n  <meta content=\"summary_large_image\" property=\"twitter:card\" />\n\n\n  <meta content=\"Genius\" property=\"twitter:app:name:iphone\" />\n  <meta content=\"709482991\" property=\"twitter:app:id:iphone\" />\n  <meta content=\"genius://songs/719774\" property=\"twitter:app:url:iphone\" />\n\n<meta name=\"description\" content=\"Chinchilla Lyrics: So far we&#39;ve lost focus / Let&#39;s just concentrate on words that could mean everything / On nights like this / We drink ourselves dry / And make promises / Without intention / So\" /><meta name=\"theme-color\" content=\"#b54624\" />\n  <link href=\"ios-app://709482991/genius/songs/719774\" rel=\"alternate\" />\n  \n  <link href=\"https://genius.com/Ttng-chinchilla-lyrics\" rel=\"canonical\" />\n  \n\n\n    <script type=\"text/javascript\">\n  window.ga = window.ga || function() {\n    (window.ga.q = window.ga.q || []).push(arguments);\n  };\n\n  \n    (function(g, e, n, i, u, s) {\n      g['GoogleAnalyticsObject'] = 'ga';\n      g.ga.l = Date.now();\n      u = e.createElement(n);\n      s = e.getElementsByTagName(n)[0];\n      u.async = true;\n      u.src = i;\n      s.parentNode.insertBefore(u, s);\n    })(window, document, 'script', 'https://www.google-analytics.com/analytics.js');\n\n    const cookies = document.cookie.split(/;\\s*/).map(str => str.split('=', 2));\n    const primisMobileCohortCookie = cookies.find(([key]) => key === '_genius_ab_test_primis_mobile');\n    const primisMobileCohort = primisMobileCohortCookie ? primisMobileCohortCookie[1] : 'control';\n\n    ga('create', \"UA-10346621-1\", 'auto', {'useAmpClientId': true});\n    ga('set', 'dimension1', \"true\");\n    ga('set', 'dimension2', \"songs#show\");\n    ga('set', 'dimension3', \"rock\");\n    ga('set', 'dimension4', \"true\");\n    ga('set', 'dimension5', 'false');\n    ga('set', 'dimension6', \"react\");\n    ga('set', 'dimension8', \"desktop_react_2_column\");\n    ga('set', 'dimension9', \"control\");\n    ga('set', 'dimension10', primisMobileCohort);\n    ga('set', 'dimension11', \"false\");\n    ga('send', 'pageview');\n  \n</script>\n\n    <script type=\"text/javascript\">\n  let fullstory_segment;\n  try { fullstory_segment = localStorage.getItem('genius_fullstory_segment'); } catch (e) {}\n\n  if (typeof(fullstory_segment) === 'string') {\n    fullstory_segment = Number(fullstory_segment);\n  } else {\n    fullstory_segment = Math.random();\n    try { localStorage.setItem('genius_fullstory_segment', fullstory_segment); } catch (e) {}\n  }\n\n  if (fullstory_segment < 0.001) {\n    window['_fs_debug'] = false;\nwindow['_fs_host'] = 'fullstory.com';\nwindow['_fs_script'] = 'edge.fullstory.com/s/fs.js';\nwindow['_fs_org'] = 'AGFQ9';\nwindow['_fs_namespace'] = 'FS';\n(function(m,n,e,t,l,o,g,y){\n  if (e in m) {if(m.console && m.console.log) { m.console.log('FullStory namespace conflict. Please set window[\"_fs_namespace\"].');} return;}\n  g=m[e]=function(a,b,s){g.q?g.q.push([a,b,s]):g._api(a,b,s);};g.q=[];\n  o=n.createElement(t);o.async=1;o.crossOrigin='anonymous';o.src='https://'+_fs_script;\n  y=n.getElementsByTagName(t)[0];y.parentNode.insertBefore(o,y);\n  g.identify=function(i,v,s){g(l,{uid:i},s);if(v)g(l,v,s)};g.setUserVars=function(v,s){g(l,v,s)};g.event=function(i,v,s){g('event',{n:i,p:v},s)};\n  g.shutdown=function(){g(\"rec\",!1)};g.restart=function(){g(\"rec\",!0)};\n  g.log = function(a,b) { g(\"log\", [a,b]) };\n  g.consent=function(a){g(\"consent\",!arguments.length||a)};\n  g.identifyAccount=function(i,v){o='account';v=v||{};v.acctId=i;g(o,v)};\n  g.clearUserCookie=function(){};\n})(window,document,window['_fs_namespace'],'script','user');\n\n\n    let current_user = {\"id\":8250538,\"login\":\"pibbyG\",\"avatar_url\":\"https://images.genius.com/avatars/small/8a3f146f096d2e36f4a12d72ff2f2bf8\",\"iq\":{\"total\":\"100\"},\"profile_path\":\"/pibbyG\",\"pusher_channel\":\"private-user-8250538\",\"is_editor\":false,\"is_moderator\":false,\"is_contrib\":false,\"current_user_metadata\":{\"permissions\":[\"create_offsite_annotation\"]}};\n\n    if (current_user) {\n      FS.identify('user:' + current_user.id, {\n        displayName: current_user.login,\n        contrib_bool: current_user.is_contrib,\n        editor_bool: current_user.is_editor,\n      });\n    } else {\n      FS.identify(false);\n    }\n  }\n</script>\n\n    \n  <script type=\"text/javascript\">\n  var _qevents = _qevents || [];\n  (function() {\n    var elem = document.createElement('script');\n    elem.src = (document.location.protocol == 'https:' ? 'https://secure' : 'http://edge') + '.quantserve.com/quant.js';\n    elem.async = true;\n    elem.type = 'text/javascript';\n    var scpt = document.getElementsByTagName('script')[0];\n    scpt.parentNode.insertBefore(elem, scpt);\n  })();\n</script>\n\n\n\n\n    <meta itemprop=\"s3-response\" content=\"false\">\n    <meta itemprop=\"cf-cache-status\" content=\"XX\">\n    <meta itemprop=\"rendered-with-cache\" content=\"XX\">\n  </head>\n  <body>\n    <div id=\"application\"><p id=\"top\" class=\"SongPage__FeedbackBanner-sc-19xhmoi-2 gNhDlQ\"><span role=\"img\" aria-label=\"Construction sign emoji\">🚧</span>  The new song page is now the default experience! We need your help to continue improving contributor features.<!-- --> <a href=\"https://forms.gle/rSPEFZEHvZmXakvD7\" class=\"SongPage__FeedbackLink-sc-19xhmoi-1 hmntnn\">Take our new survey</a> <span role=\"img\" aria-label=\"Construction sign emoji\">🚧</span></p><div class=\"LeaderboardOrMarquee__Sticky-yjd3i4-0 dIgauN Leaderboard-da326u-0 cJGzHp\"><div class=\"LeaderboardOrMarquee__Container-yjd3i4-1 cLZLgM\"></div></div><div id=\"sticky-nav\" class=\"StickyNavdesktop__Container-sc-60o25r-0 iJYdoP\"><div class=\"StickyNavdesktop__Left-sc-60o25r-1 cpvLYi\"><a href=\"https://genius.com\" height=\"13\" class=\"PageHeaderLogo__Link-sc-13m4mcv-0 lbozyU\"><svg viewBox=\"0 0 100 15\"><path d=\"M11.7 2.9s0-.1 0 0c-.8-.8-1.7-1.2-2.8-1.2-1.1 0-2.1.4-2.8 1.1-.2.2-.3.4-.5.6v.1c0 .1.1.1.1.1.4-.2.9-.3 1.4-.3 1.1 0 2.2.5 2.9 1.2h1.6c.1 0 .1-.1.1-.1V2.9c.1 0 0 0 0 0zm-.1 4.6h-1.5c-.8 0-1.4-.6-1.5-1.4.1 0 0-.1 0-.1-.3 0-.6.2-.8.4v.2c-.6 1.8.1 2.4.9 2.4h1.1c.1 0 .1.1.1.1v.4c0 .1.1.1.1.1.6-.1 1.2-.4 1.7-.8V7.6c.1 0 0-.1-.1-.1z\"></path><path d=\"M11.6 11.9s-.1 0 0 0c-.1 0-.1 0 0 0-.1 0-.1 0 0 0-.8.3-1.6.5-2.5.5-3.7 0-6.8-3-6.8-6.8 0-.9.2-1.7.5-2.5 0-.1-.1-.1-.2-.1h-.1C1.4 4.2.8 5.7.8 7.5c0 3.6 2.9 6.4 6.4 6.4 1.7 0 3.3-.7 4.4-1.8V12c.1 0 0-.1 0-.1zm13.7-3.1h3.5c.8 0 1.4-.5 1.4-1.3v-.2c0-.1-.1-.1-.1-.1h-4.8c-.1 0-.1.1-.1.1v1.4c-.1 0 0 .1.1.1zm5.1-6.7h-5.2c-.1 0-.1.1-.1.1v1.4c0 .1.1.1.1.1H29c.8 0 1.4-.5 1.4-1.3v-.2c.1-.1.1-.1 0-.1z\"></path><path d=\"M30.4 12.3h-6.1c-1 0-1.6-.6-1.6-1.6V1c0-.1-.1-.1-.1-.1-1.1 0-1.8.7-1.8 1.8V12c0 1.1.7 1.8 1.8 1.8H29c.8 0 1.4-.6 1.4-1.3v-.1c.1 0 .1-.1 0-.1zm12 0c-.6-.1-.9-.6-.9-1.3V1.1s0-.1-.1-.1H41c-.9 0-1.5.6-1.5 1.5v9.9c0 .9.6 1.5 1.5 1.5.8 0 1.4-.6 1.5-1.5 0-.1 0-.1-.1-.1zm8.2 0h-.2c-.9 0-1.4-.4-1.8-1.1l-4.5-7.4-.1-.1c-.1 0-.1.1-.1.1V8l2.8 4.7c.4.6.9 1.2 2 1.2 1 0 1.7-.5 2-1.4 0-.2-.1-.2-.1-.2zm-.9-3.8c.1 0 .1-.1.1-.1V1.1c0-.1 0-.1-.1-.1h-.4c-.9 0-1.5.6-1.5 1.5v3.1l1.7 2.8c.1 0 .1.1.2.1zm13 3.8c-.6-.1-.9-.6-.9-1.2v-10c0-.1 0-.1-.1-.1h-.3c-.9 0-1.5.6-1.5 1.5v9.9c0 .9.6 1.5 1.5 1.5.8 0 1.4-.6 1.5-1.5l-.2-.1zm18.4-.5H81c-.7.3-1.5.5-2.5.5-1.6 0-2.9-.5-3.7-1.4-.9-1-1.4-2.4-1.4-4.2V1c0-.1 0-.1-.1-.1H73c-.9 0-1.5.6-1.5 1.5V8c0 3.7 2 5.9 5.4 5.9 1.9 0 3.4-.7 4.3-1.9v-.1c0-.1 0-.1-.1-.1z\"></path><path d=\"M81.2.9h-.3c-.9 0-1.5.6-1.5 1.5v5.7c0 .7-.1 1.3-.3 1.8 0 .1.1.1.1.1 1.4-.3 2.1-1.4 2.1-3.3V1c0-.1-.1-.1-.1-.1zm12.7 7.6l1.4.3c1.5.3 1.6.8 1.6 1.2 0 .1.1.1.1.1 1.1-.1 1.8-.7 1.8-1.5s-.6-1.2-1.9-1.5l-1.4-.3c-3.2-.6-3.8-2.3-3.8-3.6 0-.7.2-1.3.6-1.9v-.2c0-.1-.1-.1-.1-.1-1.5.7-2.3 1.9-2.3 3.4-.1 2.3 1.3 3.7 4 4.1zm5.2 3.2c-.1.1-.1.1 0 0-.9.4-1.8.6-2.8.6-1.6 0-3-.5-4.3-1.4-.3-.3-.5-.6-.5-1 0-.1 0-.1-.1-.1s-.3-.1-.4-.1c-.4 0-.8.2-1.1.6-.2.3-.4.7-.3 1.1.1.4.3.7.6 1 1.4 1 2.8 1.5 4.5 1.5 2 0 3.7-.7 4.5-1.9v-.1c0-.1 0-.2-.1-.2z\"></path><path d=\"M94.1 3.2c0 .1.1.1.1.1h.2c1.1 0 1.7.3 2.4.8.3.2.6.3 1 .3s.8-.2 1.1-.6c.2-.3.3-.6.3-.9 0-.1 0-.1-.1-.1-.2 0-.3-.1-.5-.2-.8-.6-1.4-.9-2.6-.9-1.2 0-2 .6-2 1.4.1 0 .1 0 .1.1z\"></path></svg></a><div class=\"StickyNavdesktop__Subnavigation-sc-60o25r-5 bGGxIJ\"><a href=\"/#featured-stories\" rel=\"noopener\" class=\"Link-h3isu4-0 boJhnK StickyNavdesktop__SiteLink-sc-60o25r-4 kphUUq\" font-weight=\"light\"><span font-weight=\"light\" class=\"TextLabel-sc-8kw9oj-0 kNjZBr\">Featured</span></a><a href=\"/#top-songs\" rel=\"noopener\" class=\"Link-h3isu4-0 boJhnK StickyNavdesktop__SiteLink-sc-60o25r-4 kphUUq\" font-weight=\"light\"><span font-weight=\"light\" class=\"TextLabel-sc-8kw9oj-0 kNjZBr\">Charts</span></a><a href=\"/#videos\" rel=\"noopener\" class=\"Link-h3isu4-0 boJhnK StickyNavdesktop__SiteLink-sc-60o25r-4 kphUUq\" font-weight=\"light\"><span font-weight=\"light\" class=\"TextLabel-sc-8kw9oj-0 kNjZBr\">Videos</span></a><a href=\"/forums\" rel=\"noopener\" class=\"Link-h3isu4-0 boJhnK StickyNavdesktop__SiteLink-sc-60o25r-4 kphUUq\" font-weight=\"light\"><span font-weight=\"light\" class=\"TextLabel-sc-8kw9oj-0 kNjZBr\">Forums</span></a><a href=\"/new\" rel=\"noopener\" class=\"Link-h3isu4-0 boJhnK StickyNavdesktop__SiteLink-sc-60o25r-4 kphUUq\" font-weight=\"light\"><span font-weight=\"light\" class=\"TextLabel-sc-8kw9oj-0 kNjZBr\">Add A Song</span></a><a href=\"https://promote.genius.com\" target=\"_blank\" rel=\"noopener\" class=\"Link-h3isu4-0 boJhnK StickyNavdesktop__SiteLink-sc-60o25r-4 kphUUq\" font-weight=\"light\"><span font-weight=\"light\" class=\"TextLabel-sc-8kw9oj-0 kNjZBr\">Promote Your Music</span></a></div></div><div class=\"StickyNavdesktop__Right-sc-60o25r-2 kMDkxm\"><div class=\"StickyNavdesktop__Inboxes-sc-60o25r-3 iMBkpA\"><div class=\"PageHeaderLoggedIn__Container-sc-1i45pz8-2 iHELGn\"><div class=\"PageHeaderLoggedIn__Item-sc-1i45pz8-0 goeTDT\"><div><div class=\"PageHeaderInboxdesktop__Container-sc-18cqfy1-0 duhTAK\"><svg viewBox=\"0 0 21.82 22\"><path d=\"M21.82,20.62,17,15.83l3.59-3.59L17.55,9.17l-3.36.12L10.09,5.19v-3L7.91,0,0,7.91l2.16,2.16L5,10.25,9.1,14.37,9,17.73l3.08,3.08,3.59-3.59L20.43,22ZM11,16.94l.12-3.36L5.85,8.34,3,8.16l-.25-.25L7.91,2.77,8.13,3V6l5.27,5.27,3.36-.12,1.09,1.09L12.06,18Z\"></path></svg><div class=\"PageHeaderInboxdesktop__Label-sc-18cqfy1-1 cZffjt\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 hEVcyj\">Forums</span></div></div></div></div><div class=\"PageHeaderLoggedIn__Item-sc-1i45pz8-0 goeTDT\"><div><div class=\"PageHeaderInboxdesktop__Container-sc-18cqfy1-0 duhTAK\"><svg viewBox=\"0 0 22 22\"><path d=\"M20.07,1.93V20.07H1.93V1.93H20.07M22,0H0V22H22V0Z\"></path><path d=\"M7.24,8.38l4.07-4.66L13.5,8.38H11.8s-.92,3.35-.92,3.4h3.88l-3.49,6.5s.44-4.61.44-4.66H7.82L9.74,8.38Z\"></path></svg><div class=\"PageHeaderInboxdesktop__Label-sc-18cqfy1-1 cZffjt\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 hEVcyj\">Feed</span></div></div></div></div><div class=\"PageHeaderLoggedIn__Item-sc-1i45pz8-0 goeTDT\"><div><div class=\"PageHeaderInboxdesktop__Container-sc-18cqfy1-0 duhTAK\"><svg viewBox=\"0 0 17.87 22\"><path d=\"M16,13.05v-6a7.05,7.05,0,0,0-14.11,0v6H0v6.66H6.65a2.29,2.29,0,0,0,4.57,0h6.65V13.05Zm-12.2-6a5.15,5.15,0,1,1,10.3,0v6H3.79ZM1.9,17.81V15.23H16v2.58Z\"></path></svg><div class=\"PageHeaderInboxdesktop__Label-sc-18cqfy1-1 cZffjt\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 hEVcyj\">Me</span></div><div class=\"PageHeadershared__InboxUnreadCount-sc-1cjax99-0 fmeUAz\"><span color=\"accent.on\" font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 hEVcyj\">1</span></div></div></div></div><div class=\"PageHeaderLoggedIn__Item-sc-1i45pz8-0 goeTDT\"><div><div class=\"PageHeaderInboxdesktop__Container-sc-18cqfy1-0 duhTAK\"><svg viewBox=\"0 0 22 15.34\"><path d=\"M0,0V15.34H22V0ZM12.32,8.2,11,9.47,9.68,8.2,8.3,6.88l-5.18-5H18.88l-5.18,5ZM6.82,8.1,1.9,12.17V3.37ZM8.21,9.42,11,12.1l2.79-2.68,4.86,4H3.35Zm7-1.33L20.1,3.37v8.8Z\"></path></svg><div class=\"PageHeaderInboxdesktop__Label-sc-18cqfy1-1 cZffjt\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 hEVcyj\">Messages</span></div><div class=\"PageHeadershared__InboxUnreadCount-sc-1cjax99-0 fmeUAz\"><span color=\"accent.on\" font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 hEVcyj\">1</span></div></div></div></div><div class=\"PageHeaderLoggedIn__Item-sc-1i45pz8-0 goeTDT\"><div><div class=\"PageHeaderInboxdesktop__Container-sc-18cqfy1-0 duhTAK\"><svg viewBox=\"0 0 22 22\"><path d=\"M20.07,1.93V20.07H1.93V1.93H20.07M22,0H0V22H22V0Z\"></path><path d=\"M3.83,16.29V5.71h2.1V16.29Z\"></path><path d=\"M16.35,16.57l-.65-.71a5.23,5.23,0,0,1-2.83.71A5.43,5.43,0,0,1,7.26,11a5.45,5.45,0,0,1,5.62-5.57A5.45,5.45,0,0,1,18.5,11,5.23,5.23,0,0,1,17,14.82l1.47,1.75ZM12.88,7.29A3.55,3.55,0,0,0,9.36,11a3.56,3.56,0,0,0,3.57,3.69,3.27,3.27,0,0,0,1.48-.28l-1.93-2.12,2.13-.16,1.09,1.22A3.74,3.74,0,0,0,16.4,11,3.55,3.55,0,0,0,12.88,7.29Z\"></path></svg><div class=\"PageHeaderInboxdesktop__Label-sc-18cqfy1-1 cZffjt\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 hEVcyj\">Earn IQ</span></div></div></div></div><div class=\"PageHeaderLoggedIn__AvatarMenu-sc-1i45pz8-1 kgoOvC\"><div><div class=\"PageHeaderMenu__Container-sc-13myo8l-0 emAXHD\"><div class=\"UserAvatar__Container-ln6u1h-0 eNxXUK\"><div role=\"img\" class=\"SizedImage__Container-sc-1hyeaua-0 bcJdHU\"><noscript><img src=\"https://images.genius.com/avatars/medium/8a3f146f096d2e36f4a12d72ff2f2bf8\" class=\"SizedImage__NoScript-sc-1hyeaua-2 UJCmI\"/></noscript></div></div><div class=\"PageHeaderMenu__Iq-sc-13myo8l-1 jLiNjI\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 hEVcyj\">100<!-- --> IQ</span></div></div></div></div></div></div><form action=\"/search\" method=\"get\" class=\"PageHeaderSearchdesktop__Form-sc-1o6zub0-0 dawSPu\"><input name=\"q\" placeholder=\"Search lyrics &amp; more\" autoComplete=\"off\" required=\"\" class=\"PageHeaderSearchdesktop__Input-sc-1o6zub0-2 hTPksM\"/><div class=\"PageHeaderSearchdesktop__Icon-sc-1o6zub0-1 cmIqeW\"><svg viewBox=\"0 0 21.48 21.59\"><path d=\"M21.48 20.18L14.8 13.5a8.38 8.38 0 1 0-1.43 1.4l6.69 6.69zM2 8.31a6.32 6.32 0 1 1 6.32 6.32A6.32 6.32 0 0 1 2 8.31z\"></path></svg></div></form></div></div><main class=\"SongPage__Container-sc-19xhmoi-0 buKnHw\"><div class=\"SongPageGriddesktop__TwoColumn-sc-1px5b71-1 hfRKjb SongHeaderVariantdesktop__Container-sc-12tszai-0 hrzyda\"><div class=\"SongHeaderVariantdesktop__Sentinel-sc-12tszai-12 evqGUV\"></div><div class=\"SongHeaderVariantdesktop__Column-sc-12tszai-1 SongHeaderVariantdesktop__Left-sc-12tszai-2 fEcDuq\"><div class=\"SongHeaderVariantdesktop__CoverArtContainer-sc-12tszai-5 lbUWUz\"><div class=\"SongHeaderVariantdesktop__CoverArt-sc-12tszai-6 cicAJN\"><img alt=\"Cover art for Chinchilla by TTNG\" class=\"SizedImage__Image-sc-1hyeaua-1 iMdmgx\"/></div></div></div><div class=\"SongHeaderVariantdesktop__Column-sc-12tszai-1 SongHeaderVariantdesktop__Center-sc-12tszai-3 bpBizA\"><div><h1 class=\"SongHeaderVariantdesktop__Title-sc-12tszai-7 iWUdKG\"><span class=\"SongHeaderVariantdesktop__HiddenMask-sc-12tszai-10 bFjDxc\">Chinchilla</span></h1><a href=\"https://genius.com/artists/Ttng\" font-size=\"smallReading\" class=\"Link-h3isu4-0 hwdSYP SongHeaderVariantdesktop__Artist-sc-12tszai-11 ayFeg\" font-weight=\"light\">TTNG</a><div class=\"HeaderTracklistVariant__Container-doi9xi-0 fUGBbC\"><div class=\"HeaderTracklistVariant__AlbumWrapper-doi9xi-1 iinkOX\">Track 1 on<!-- --> <div class=\"HeaderTracklistVariant__Album-doi9xi-2 gyOWcZ\"><a href=\"#primary-album\" font-weight=\"light\" class=\"Link-h3isu4-0 gHBbjJ\">Animals<!-- --> <span class=\"InlineSvg__Wrapper-b788hd-0 xQwqG\"><svg viewBox=\"0 0 6.6 16\"><path d=\"M1.6 8.8l.6-.6 1 1 .5.7V6H0v-.8h4.5v4.6l.5-.6 1-1 .6.5L4 11.3 1.6 8.8z\"></path></svg></span></a></div></div></div></div><a href=\"#about\" class=\"Link-h3isu4-0 bLjCRC HeaderBio__Wrapper-oaxemt-0 gkAIfz\" font-weight=\"light\">This song is about those relationships with a lot of fights and reconciliations. The singer and his couple are aruging/reconciliating, telling themselves everything is going to… <span class=\"HeaderBio__ViewBio-oaxemt-1 hTbbsa\">Read More<!-- --> <span class=\"InlineSvg__Wrapper-b788hd-0 xQwqG\"><svg viewBox=\"0 0 6.6 16\"><path d=\"M1.6 8.8l.6-.6 1 1 .5.7V6H0v-.8h4.5v4.6l.5-.6 1-1 .6.5L4 11.3 1.6 8.8z\"></path></svg></span></span></a></div><div class=\"SongHeaderVariantdesktop__Column-sc-12tszai-1 SongHeaderVariantdesktop__Right-sc-12tszai-4 jryaLr\"><div class=\"HeaderMetadata__Container-sc-1p42fnf-0 fCsyNi\"><div class=\"HeaderMetadata__Grid-sc-1p42fnf-1 ePueMf\"><div class=\"HeaderMetadata__Section-sc-1p42fnf-3 jROWVH\"><p class=\"HeaderMetadata__Label-sc-1p42fnf-4 dIyyeJ\">Produced by</p><a href=\"https://genius.com/artists/Max-read\" font-weight=\"light\" class=\"Link-h3isu4-0 gHBbjJ\">Max Read</a></div><div class=\"HeaderMetadata__Section-sc-1p42fnf-3 jROWVH\"><p class=\"HeaderMetadata__Label-sc-1p42fnf-4 dIyyeJ\">Release Date</p>October 13, 2008</div><div class=\"HeaderMetadata__Section-sc-1p42fnf-3 cKIVtz\"><div class=\"HeaderMetadata__ViewCredits-sc-1p42fnf-7 dwjgaZ\"><a href=\"#song-info\" font-weight=\"light\" class=\"Link-h3isu4-0 gHBbjJ\">View All Credits<!-- --> <span class=\"InlineSvg__Wrapper-b788hd-0 xQwqG\"><svg viewBox=\"0 0 6.6 16\"><path d=\"M1.6 8.8l.6-.6 1 1 .5.7V6H0v-.8h4.5v4.6l.5-.6 1-1 .6.5L4 11.3 1.6 8.8z\"></path></svg></span></a></div></div></div><div class=\"HeaderMetadata__Lower-sc-1p42fnf-2 gcxzH\"><div class=\"HeaderMetadata__Stats-sc-1p42fnf-6 fJDnoB\"><span class=\"LabelWithIcon__Container-sc-1ri57wg-0 jcROPx\" height=\".65em\"><svg viewBox=\"0 0 22 22\"><path d=\"M16.27 13.45L12 10.58V4.46H9.76v7.25L15 15.25z\"></path><path d=\"M11 2a9 9 0 1 1-9 9 9 9 0 0 1 9-9m0-2a11 11 0 1 0 11 11A11 11 0 0 0 11 0z\"></path></svg><span class=\"LabelWithIcon__Label-sc-1ri57wg-1 kMItKF\">1</span></span><span class=\"LabelWithIcon__Container-sc-1ri57wg-0 jcROPx\" height=\".65em\"><svg viewBox=\"0 0 22 15.45\"><path d=\"M11 2c4 0 7.26 3.85 8.6 5.72-1.34 1.87-4.6 5.73-8.6 5.73S3.74 9.61 2.4 7.73C3.74 5.86 7 2 11 2m0-2C4.45 0 0 7.73 0 7.73s4.45 7.73 11 7.73 11-7.73 11-7.73S17.55 0 11 0z\"></path><path d=\"M11 5a2.73 2.73 0 1 1-2.73 2.73A2.73 2.73 0 0 1 11 5m0-2a4.73 4.73 0 1 0 4.73 4.73A4.73 4.73 0 0 0 11 3z\"></path></svg><span class=\"LabelWithIcon__Label-sc-1ri57wg-1 kMItKF\">15K</span></span><button class=\"LabelWithIcon__Container-sc-1ri57wg-0 jcROPx\" height=\".65em\"><svg viewBox=\"0 0 22 16.47\"><path d=\"M12.55 6.76a4 4 0 1 0 0-4.59 4.41 4.41 0 0 1 0 4.59zm3.07 2.91v5.17H22V9.66l-6.38.01M7 9a4.43 4.43 0 0 0 3.87-2.23 4.41 4.41 0 0 0 0-4.59 4.47 4.47 0 0 0-8.38 2.3A4.48 4.48 0 0 0 7 9zm-7 1.35v6.12h13.89v-6.14l-6.04.01-7.85.01\"></path></svg><span class=\"LabelWithIcon__Label-sc-1ri57wg-1 gjSNHg\">9</span></button><div class=\"Pyong__Container-yq95kq-0 eMjKRh\"><div class=\"Tooltip__Container-sc-1uvy5c2-0 cRrFdP\"><div class=\"Tooltip__Children-sc-1uvy5c2-2 dvOJud\"><button class=\"LabelWithIcon__Container-sc-1ri57wg-0 jcROPx\" height=\".65em\"><svg viewBox=\"0 0 11.37 22\"><path d=\"M0 7l6.16-7 3.3 7H6.89S5.5 12.1 5.5 12.17h5.87L6.09 22l.66-7H.88l2.89-8z\"></path></svg><span class=\"LabelWithIcon__Label-sc-1ri57wg-1 IVJjV\">2</span></button></div></div></div></div><div class=\"HeaderMetadata__Controls-sc-1p42fnf-5 hppQiC\"></div></div></div></div></div><div class=\"SongPage__Section-sc-19xhmoi-3 cXvCRB\"><div class=\"SectionScrollSentinel__Element-sc-1c8cvz5-0 loHKiK\"></div><div id=\"annotation-portal-target\" class=\"SongPage__LyricsWrapper-sc-19xhmoi-5 UKjRP\"><div id=\"lyrics\" class=\"SongPage__SectionAnchor-sc-19xhmoi-4 gcjDma\"></div><div id=\"lyrics-root-pin-spacer\"><div font-size=\"largeReading\" id=\"lyrics-root\" class=\"SongPageGriddesktop__TwoColumn-sc-1px5b71-1 hfRKjb Lyrics__Root-sc-1ynbvzw-1 kZmmHP\">\n        <div class=\"LyricsControls__Container-sghmdv-0 bnFCuj\"><div class=\"Dropdown__Container-aitjcr-0 fxOdHk\"><button class=\"Dropdown__Toggle-aitjcr-1 cjTarM\"><span type=\"button\" class=\"SmallButton__Container-mg33hl-0 ljczSm\">Manage Lyrics<span class=\"LyricsControls__SmallButtonIcon-sghmdv-2 jYdMbk\"><svg viewBox=\"0 0 21.32 10.91\"><path d=\"M10.66 10.91L0 1.5 1.32 0l9.34 8.24L20 0l1.32 1.5-10.66 9.41\"></path></svg></span></span></button></div><a class=\"SmallButton__Container-mg33hl-0 ljczSm OptOutButton__Container-e48zu0-0 EJZPZ\" type=\"button\">Click here to go to the old song page</a>\n        \n    </div><div data-lyrics-container=\"true\" class=\"Lyrics__Container-sc-1ynbvzw-6 jYfhrf\">So far we&#x27;ve lost focus<br/>Let&#x27;s just concentrate on words that could mean everything<br/><br/>On nights like this<br/>We drink ourselves dry<br/>And make promises<br/>Without intention<br/><br/>So fortunate that this was brought up<br/>The last time. As I recall<br/>I can’t hold up your every expectation<br/><br/>On nights like this<br/>We drink ourselves dry<br/>And make promises<br/>Without intention<br/><br/>My God, is this what we’ve become?<br/>Living parodies of love and loss<br/>Can we really be all that lost?<br/><br/>So fortunate that this was brought up<br/>The last time. As I recall<br/>I can’t hold up your every expectation<br/><br/>One moment to another I am restless<br/>Seems making love forever can often risk your heart<br/>And I cannot remember when I was this messed up<br/>In service of another I am beautiful</div><div class=\"RightSidebar__Container-pajcl2-0 kwgaZp\"><div class=\"SidebarAd__Container-sc-1cw85h6-0 crbHJx\"><div class=\"SidebarAd__Ad-sc-1cw85h6-1 kAKdir\"></div></div></div><div class=\"Lyrics__Footer-sc-1ynbvzw-2 lYpBt\"><div class=\"ShareButtons__Root-jws18q-0 dyewdM Lyrics__FooterShareButtons-sc-1ynbvzw-3 kuxJDq\"><div class=\"ShareButtons__Container-jws18q-1 ePvBqA\"><div class=\"ShareButtons__ButtonWrapper-jws18q-3 ueUKD\"><button aria-label=\"facebook\" class=\"react-share__ShareButton\" style=\"background-color:transparent;border:none;padding:0;font:inherit;color:inherit;cursor:pointer\"><div class=\"Button__Container-sc-1874dbw-0 cLBJdA ShareButtons__Button-jws18q-2 kJxpEi\" type=\"button\"><svg viewBox=\"0 0 9.95 20\"><path d=\"M8.09 3.81c-1.4 0-1.58.84-1.58 1.67v1.3h3.35L9.49 11h-3v9H2.33v-9H0V6.88h2.42V3.81C2.42 1.3 3.81 0 6.6 0H10v3.81z\"></path></svg></div></button></div><div class=\"ShareButtons__ButtonWrapper-jws18q-3 ueUKD\"><button aria-label=\"twitter\" class=\"react-share__ShareButton\" style=\"background-color:transparent;border:none;padding:0;font:inherit;color:inherit;cursor:pointer\"><div class=\"Button__Container-sc-1874dbw-0 cLBJdA ShareButtons__Button-jws18q-2 kJxpEi\" type=\"button\"><svg viewBox=\"0 0 20 16.43\"><path d=\"M20 1.89l-2.3 2.16v.68a12.28 12.28 0 0 1-3.65 8.92c-5 5.13-13.1 1.76-14.05.81 0 0 3.78.14 5.81-1.76A4.15 4.15 0 0 1 2.3 9.86h2S.81 9.05.81 5.81A11 11 0 0 0 3 6.35S-.14 4.05 1.49.95a11.73 11.73 0 0 0 8.37 4.19A3.69 3.69 0 0 1 13.51 0a3.19 3.19 0 0 1 2.57 1.08 12.53 12.53 0 0 0 3.24-.81l-1.75 1.89A10.46 10.46 0 0 0 20 1.89z\"></path></svg></div></button></div><div class=\"ShareButtons__ButtonWrapper-jws18q-3 ueUKD\"><div class=\"Button__Container-sc-1874dbw-0 cLBJdA ShareButtons__Button-jws18q-2 kJxpEi\" type=\"button\"><div class=\"Pyong__Container-yq95kq-0 eMjKRh\"><div class=\"Tooltip__Container-sc-1uvy5c2-0 cRrFdP\"><div class=\"Tooltip__Children-sc-1uvy5c2-2 dvOJud\"><button class=\"LabelWithIcon__Container-sc-1ri57wg-0 dCKKNS\" height=\".75rem\"><svg viewBox=\"0 0 11.37 22\"><path d=\"M0 7l6.16-7 3.3 7H6.89S5.5 12.1 5.5 12.17h5.87L6.09 22l.66-7H.88l2.89-8z\"></path></svg><span class=\"LabelWithIcon__Label-sc-1ri57wg-1 IVJjV\">2</span></button></div></div></div></div></div><button class=\"ShareButtons__ButtonWrapper-jws18q-3 ueUKD\"><div class=\"Button__Container-sc-1874dbw-0 cLBJdA ShareButtons__Button-jws18q-2 kJxpEi\" type=\"button\">Embed</div></button></div></div></div></div></div><form class=\"SongPageGriddesktop__TwoColumn-sc-1px5b71-1 hfRKjb LyricsEditdesktop__Container-sc-19lxrhp-0 kkpCaw\"><div class=\"SongPageGriddesktop__TwoColumn-sc-1px5b71-1 hfRKjb LyricsEditdesktop__Header-sc-19lxrhp-1 ewKWbT\"><div class=\"LyricsEditdesktop__HeaderLeft-sc-19lxrhp-2 bcLwQh\"></div></div><div class=\"LyricsEditdesktop__Editor-sc-19lxrhp-3 hFPGxa\"></div><div class=\"LyricsEditdesktop__ControlsContainer-sc-19lxrhp-4 lbdVJq\"><div class=\"LyricsEditdesktop__Controls-sc-19lxrhp-5 bwjuqY\"><button class=\"Button__Container-sc-1874dbw-0 cLBJdA LyricsEditdesktop__Button-sc-19lxrhp-6 bXbziL\" type=\"button\">Cancel</button><div class=\"LyricsEditExplainer__Container-sc-1aeph76-0 pDArA LyricsEditdesktop__Explainer-sc-19lxrhp-8 ldjaSd\">How to Format Lyrics:<ul><li>Type out all lyrics, even if it’s a chorus that’s repeated throughout the song</li><li>The Section Header button breaks up song sections. Highlight the text then click the link</li><li>Use Bold and Italics only to distinguish between different singers in the same verse.<ul><li>E.g. “Verse 1: Kanye West, <span class=\"LyricsEditExplainer__Italic-sc-1aeph76-2 rncXA\">Jay-Z</span>, <span class=\"LyricsEditExplainer__Bold-sc-1aeph76-1 eDBQeK\">Both</span>”</li></ul></li><li>Capitalize each line</li><li>To move an annotation to different lyrics in the song, use the [...] menu to switch to referent editing mode</li></ul></div></div></div></form></div><div class=\"SectionLeaderboard__Container-sc-1pjk0bw-0 cSKAwQ\"><div class=\"SectionLeaderboard__Center-sc-1pjk0bw-1 fPpEQG\"></div></div></div><div class=\"SongPage__Section-sc-19xhmoi-3 cXvCRB\"><div class=\"SectionScrollSentinel__Element-sc-1c8cvz5-0 loHKiK\"></div><div id=\"about\" class=\"SongPage__SectionAnchor-sc-19xhmoi-4 gcjDma\"></div><div class=\"SongPageGriddesktop__TwoColumn-sc-1px5b71-1 hfRKjb About__Grid-ut4i9m-0 dbwAaA\"><div class=\"About__Container-ut4i9m-1 jCTumN\"><h1 font-size=\"xxLargeHeadline\" class=\"About__Title-ut4i9m-2 kcXwIY\">About</h1><div class=\"SongDescription__Container-sc-615rvk-0 lbmvpT\"><div class=\"UnreviewedAnnotation__Container-sc-1fjei54-0 drLwGn\"><div class=\"UnreviewedAnnotation__Unreviewed-sc-1fjei54-1 hCfSzi\"><svg viewBox=\"0 0 22 19.8\" class=\"UnreviewedAnnotation__Alert-sc-1fjei54-3 jEcpKK\"><path d=\"m11 4.12 7.6 13.68H3.4L11 4.12M11 0 0 19.8h22L11 0z\"></path><path d=\"M10 8.64h2v4.51h-2zm1 5.45a1.13 1.13 0 0 1 1.13 1.15A1.13 1.13 0 1 1 11 14.09z\"></path></svg><div>This song bio is<!-- --> <span class=\"UnreviewedAnnotation__ExplainerToggle-sc-1fjei54-2 ggQBPB\">unreviewed</span></div></div></div><div class=\"Attribution__Container-sc-1nmry9o-0 dhIhSa\"><div class=\"Attribution__Header-sc-1nmry9o-1 iPNsXE\"><span>Genius Annotation</span><button text-decoration=\"underline\" type=\"button\" class=\"TextButton-sc-192nsqv-0 ixMmYX\">1 contributor</button></div></div><div class=\"ExpandableContent__Container-sc-1165iv-0 ikywhQ\"><div class=\"ExpandableContent__Content-sc-1165iv-4 huhsMa\"><div class=\"SongDescription__Content-sc-615rvk-2 kRzyD\"><div class=\"RichText__Container-oz284w-0 jxHdAP\"><p>This song is about those relationships with a lot of fights and reconciliations. The singer and his couple are aruging/reconciliating, telling themselves everything is going to be better and things will change for good, specially when they get drunk, just to fight and reconciliate over and over again.</p></div><div class=\"AnnotationEditActions__Container-sc-10x4ilq-0 jIHZck\"><button type=\"button\" class=\"SmallButton__Container-mg33hl-0 krYtET\">Edit</button></div></div></div><div class=\"ExpandableContent__ButtonContainer-sc-1165iv-3 jzPNvv\"><button class=\"Button__Container-sc-1874dbw-0 kPiSeV ExpandableContent__Button-sc-1165iv-1 gBcitk\" type=\"button\">Expand<!-- --> <span class=\"InlineSvg__Wrapper-b788hd-0 xQwqG\"><svg viewBox=\"0 0 6.6 16\"><path d=\"M1.6 8.8l.6-.6 1 1 .5.7V6H0v-.8h4.5v4.6l.5-.6 1-1 .6.5L4 11.3 1.6 8.8z\"></path></svg></span></button></div></div></div><div class=\"InnerSectionDivider-sc-1x4onqw-0 iyrTpw\"></div><div class=\"QuestionList__Container-sc-1a58vti-1 gatDgA\"><div id=\"questions\" class=\"QuestionList__SectionAnchor-sc-1a58vti-2 niBCl\"></div><div class=\"QuestionList__Title-sc-1a58vti-0 esoPOn\">Ask us a question about this song</div><form class=\"QuestionForm__Container-wsr2cj-0 dcUlUV\"><label class=\"Fieldshared__FieldContainer-dxskot-0 hNAtee FloatingField-sc-187lo5b-0 cabqMy\"><div class=\"Fieldshared__FieldControlWithLabel-dxskot-1 cpCLCX\"><textarea rows=\"1\" placeholder=\" \" name=\"body\" aria-required=\"true\" type=\"text\" class=\"TextInput-sc-2wssth-0 Textarea__Input-u7wct2-0 dsaqcS\"></textarea><span class=\"Fieldshared__FieldLabel-dxskot-2 eIbATv\">Ask a question<!-- --> <!-- -->*</span></div></label></form><div class=\"QuestionList__Empty-sc-1a58vti-3 fyTOee\">No questions asked yet</div></div><div class=\"InnerSectionDivider-sc-1x4onqw-0 iyrTpw\"></div><div class=\"PrimaryAlbum__Container-cuci8p-0 kqeVkf\"><div id=\"primary-album\" class=\"PrimaryAlbum__SectionAnchor-cuci8p-1 lkEvgY\"></div><div class=\"PrimaryAlbum__CoverArt-cuci8p-2 fMIxkO\"><div role=\"img\" class=\"SizedImage__Container-sc-1hyeaua-0 jtGbpX\"><noscript><img src=\"https://images.genius.com/7b7890cf624378fcedd2e8ce44ea234b.640x640x1.jpg\" class=\"SizedImage__NoScript-sc-1hyeaua-2 UJCmI\"/></noscript></div></div><div class=\"PrimaryAlbum__AlbumDetails-cuci8p-3 hGGOcK\"><a href=\"https://genius.com/albums/Ttng/Animals\" class=\"PrimaryAlbum__Title-cuci8p-4 NcWGs\">Animals<!-- --> <!-- -->(2008)</a><div class=\"PrimaryAlbum__Artist-cuci8p-5 hyzSGh\"><a href=\"https://genius.com/artists/Ttng\" font-weight=\"light\" class=\"Link-h3isu4-0 gHBbjJ\">TTNG</a></div></div><ol class=\"AlbumTracklist__Container-sc-123giuo-0 kGJQLs\"><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 guEaas\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">1. </div>Chinchilla</div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">2. </div><a href=\"https://genius.com/Ttng-baboon-lyrics\" font-weight=\"light\" class=\"Link-h3isu4-0 gHBbjJ\">Baboon</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">3. </div><a href=\"https://genius.com/Ttng-lemur-lyrics\" font-weight=\"light\" class=\"Link-h3isu4-0 gHBbjJ\">Lemur</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">4. </div><a href=\"https://genius.com/Ttng-badger-lyrics\" font-weight=\"light\" class=\"Link-h3isu4-0 gHBbjJ\">Badger</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">5. </div><a href=\"https://genius.com/Ttng-quetzal-lyrics\" font-weight=\"light\" class=\"Link-h3isu4-0 gHBbjJ\">Quetzal</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">6. </div><a href=\"https://genius.com/Ttng-panda-lyrics\" font-weight=\"light\" class=\"Link-h3isu4-0 gHBbjJ\">Panda</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">7. </div><a href=\"https://genius.com/Ttng-elk-lyrics\" font-weight=\"light\" class=\"Link-h3isu4-0 gHBbjJ\">Elk</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">8. </div><a href=\"https://genius.com/Ttng-pig-lyrics\" font-weight=\"light\" class=\"Link-h3isu4-0 gHBbjJ\">Pig</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">9. </div><a href=\"https://genius.com/Ttng-gibbon-lyrics\" font-weight=\"light\" class=\"Link-h3isu4-0 gHBbjJ\">Gibbon</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">10. </div><a href=\"https://genius.com/Ttng-dog-lyrics\" font-weight=\"light\" class=\"Link-h3isu4-0 gHBbjJ\">Dog</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">11. </div><a href=\"https://genius.com/Ttng-crocodile-lyrics\" font-weight=\"light\" class=\"Link-h3isu4-0 gHBbjJ\">Crocodile</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">12. </div><a href=\"https://genius.com/Ttng-rabbit-lyrics\" font-weight=\"light\" class=\"Link-h3isu4-0 gHBbjJ\">Rabbit</a></div></li><li class=\"AlbumTracklist__Track-sc-123giuo-1 iLxPGk\"><div class=\"AlbumTracklist__TrackName-sc-123giuo-2 bPLsDz\"><div class=\"AlbumTracklist__TrackNumber-sc-123giuo-3 epTVob\">13. </div><a href=\"https://genius.com/Ttng-zebra-lyrics\" font-weight=\"light\" class=\"Link-h3isu4-0 gHBbjJ\">Zebra</a></div></li></ol></div><div class=\"InnerSectionDivider-sc-1x4onqw-0 iyrTpw\"></div><div class=\"ExpandableContent__Container-sc-1165iv-0 ikywhQ\"><div class=\"ExpandableContent__Content-sc-1165iv-4 huhsMa\"><div class=\"SongInfo__Container-nekw6x-0 kJtCmu\"><div id=\"song-info\" class=\"SongInfo__SectionAnchor-nekw6x-5 hPWhcr\"></div><div class=\"SongInfo__Title-nekw6x-1 iRKrFW\">Credits</div><div class=\"SongInfo__Columns-nekw6x-2 lgBflw\"><div class=\"SongInfo__Credit-nekw6x-3 fognin\"><div class=\"SongInfo__Label-nekw6x-4 ipOVSb\">Written By</div><a href=\"https://genius.com/artists/Stuart-smith\" font-weight=\"light\" class=\"Link-h3isu4-0 gHBbjJ\">Stuart Smith</a></div><div class=\"SongInfo__Credit-nekw6x-3 fognin\"><div class=\"SongInfo__Label-nekw6x-4 ipOVSb\">Release Date</div><div>October 13, 2008</div></div></div><div class=\"SongTags__Title-xixwg3-0 ceKRFE\">Tags</div><div class=\"SongTags__Container-xixwg3-1 bZsZHM\"><a href=\"https://genius.com/tags/rock\" class=\"SongTags__Tag-xixwg3-2 evrydK\">Rock</a><a href=\"https://genius.com/tags/indie-rock\" class=\"SongTags__Tag-xixwg3-2 kykqAa\">Indie Rock</a><a href=\"https://genius.com/tags/math-rock\" class=\"SongTags__Tag-xixwg3-2 kykqAa\">Math Rock</a></div></div></div><div class=\"ExpandableContent__ButtonContainer-sc-1165iv-3 jzPNvv\"><button class=\"Button__Container-sc-1874dbw-0 kPiSeV ExpandableContent__Button-sc-1165iv-1 gBcitk\" type=\"button\">Expand<!-- --> <span class=\"InlineSvg__Wrapper-b788hd-0 xQwqG\"><svg viewBox=\"0 0 6.6 16\"><path d=\"M1.6 8.8l.6-.6 1 1 .5.7V6H0v-.8h4.5v4.6l.5-.6 1-1 .6.5L4 11.3 1.6 8.8z\"></path></svg></span></button></div></div><div class=\"InnerSectionDivider-sc-1x4onqw-0 iyrTpw\"></div><div class=\"MusicVideo__Container-sc-1980jex-0 cYtdTH\"></div></div><div class=\"RightSidebar__Container-pajcl2-0 kwgaZp\"><div class=\"SidebarAd__Container-sc-1cw85h6-0 crbHJx\"><div class=\"SidebarAd__Ad-sc-1cw85h6-1 kAKdir\"></div></div></div></div><div class=\"SectionLeaderboard__Container-sc-1pjk0bw-0 cSKAwQ\"><div class=\"SectionLeaderboard__Center-sc-1pjk0bw-1 fPpEQG\"></div></div></div><div class=\"SongPage__Section-sc-19xhmoi-3 cXvCRB\"><div id=\"comments\" class=\"SongPage__SectionAnchor-sc-19xhmoi-4 gcjDma\"></div><div class=\"SectionScrollSentinel__Element-sc-1c8cvz5-0 loHKiK\"></div><div class=\"SongPageGriddesktop__TwoColumn-sc-1px5b71-1 hfRKjb SongComments__Grid-sc-131p4fy-0 bIlJhm\"><div class=\"SongComments__Container-sc-131p4fy-1 hVwpyI\"><div font-size=\"xxLargeHeadline\" class=\"SongComments__Title-sc-131p4fy-2 kojbqH\">Comments</div><a class=\"SongComments__Add-sc-131p4fy-4 ldOStC\">Add a comment</a><div class=\"SongComments__CTA-sc-131p4fy-6 euQZer\">Get the conversation started<br/>Be the first to comment</div></div><div class=\"RightSidebar__Container-pajcl2-0 kwgaZp\"><div class=\"SidebarAd__Container-sc-1cw85h6-0 crbHJx\"><div class=\"SidebarAd__Ad-sc-1cw85h6-1 kAKdir\"></div></div></div></div></div><div class=\"AppleMusicPlayer__PositioningContainer-uavgzr-0 eJULet\"><div class=\"SongPageGriddesktop__TwoColumn-sc-1px5b71-1 hfRKjb AppleMusicPlayer__IframeWrapper-uavgzr-1 gnMmAT\"><div class=\"AppleMusicPlayer__IframeWrapperLoader-uavgzr-2 hHaNaR\"><div class=\"PlaceholderSpinner__Container-r4gz6r-0 dluqjZ\"><svg viewBox=\"64 0 20 20\"><circle cx=\"74\" cy=\"10\" r=\"9\"></circle></svg></div></div><iframe sandbox=\"allow-scripts allow-same-origin allow-popups allow-forms allow-popups-to-escape-sandbox\" allow=\"encrypted-media\" src=\"https://genius.com/songs/719774/apple_music_player?react=1\" height=\"64px\" scrolling=\"no\" class=\"AppleMusicPlayer__Iframe-uavgzr-3 gnVZvh\"></iframe></div></div></main><div class=\"PageFooterdesktop__Container-hz1fx1-0 boDKcJ SongPage__PageFooter-sc-19xhmoi-6 iTrjDY\"><div class=\"PageGriddesktop-a6v82w-0 PageFooterdesktop__Section-hz1fx1-2 gUdeqB\"><div class=\"PageFooterdesktop__Half-hz1fx1-3 mJdfj\"><h1 class=\"PageFooterSocial__Slogan-sc-14u22mq-0 uGviF\">Genius is the world’s biggest collection of song lyrics and musical knowledge</h1><div class=\"SocialLinks__Container-jwyj6b-2 iuNSEV\"><a href=\"https://www.facebook.com/Genius/\" height=\"22\" target=\"_blank\" rel=\"noopener\" class=\"SocialLinks__Link-jwyj6b-1 cziiuX\"><svg viewBox=\"0 0 9.95 20\"><path d=\"M8.09 3.81c-1.4 0-1.58.84-1.58 1.67v1.3h3.35L9.49 11h-3v9H2.33v-9H0V6.88h2.42V3.81C2.42 1.3 3.81 0 6.6 0H10v3.81z\"></path></svg></a><a href=\"https://twitter.com/Genius\" height=\"22\" target=\"_blank\" rel=\"noopener\" class=\"SocialLinks__Link-jwyj6b-1 cziiuX\"><svg viewBox=\"0 0 20 16.43\"><path d=\"M20 1.89l-2.3 2.16v.68a12.28 12.28 0 0 1-3.65 8.92c-5 5.13-13.1 1.76-14.05.81 0 0 3.78.14 5.81-1.76A4.15 4.15 0 0 1 2.3 9.86h2S.81 9.05.81 5.81A11 11 0 0 0 3 6.35S-.14 4.05 1.49.95a11.73 11.73 0 0 0 8.37 4.19A3.69 3.69 0 0 1 13.51 0a3.19 3.19 0 0 1 2.57 1.08 12.53 12.53 0 0 0 3.24-.81l-1.75 1.89A10.46 10.46 0 0 0 20 1.89z\"></path></svg></a><a href=\"https://www.instagram.com/genius/\" height=\"22\" target=\"_blank\" rel=\"noopener\" class=\"SocialLinks__Link-jwyj6b-1 cziiuX\"><svg viewBox=\"0 0 20 20\"><path d=\"M10 0c2.724 0 3.062 0 4.125.06.83.017 1.65.175 2.426.467.668.254 1.272.65 1.77 1.162.508.498.902 1.1 1.153 1.768.292.775.45 1.595.467 2.424.06 1.063.06 1.41.06 4.123 0 2.712-.06 3.06-.06 4.123-.017.83-.175 1.648-.467 2.424-.52 1.34-1.58 2.402-2.922 2.92-.776.293-1.596.45-2.425.468-1.063.06-1.41.06-4.125.06-2.714 0-3.062-.06-4.125-.06-.83-.017-1.65-.175-2.426-.467-.668-.254-1.272-.65-1.77-1.162-.508-.498-.902-1.1-1.153-1.768-.292-.775-.45-1.595-.467-2.424C0 13.055 0 12.708 0 9.995c0-2.712 0-3.04.06-4.123.017-.83.175-1.648.467-2.424.25-.667.645-1.27 1.153-1.77.5-.507 1.103-.9 1.77-1.15C4.225.234 5.045.077 5.874.06 6.958 0 7.285 0 10 0zm0 1.798h.01c-2.674 0-2.992.06-4.046.06-.626.02-1.245.15-1.83.377-.434.16-.828.414-1.152.746-.337.31-.602.69-.775 1.113-.222.595-.34 1.224-.348 1.858-.06 1.064-.06 1.372-.06 4.045s.06 2.99.06 4.044c.007.633.125 1.262.347 1.857.17.434.434.824.775 1.142.31.33.692.587 1.113.754.596.222 1.224.34 1.86.348 1.063.06 1.37.06 4.045.06 2.674 0 2.992-.06 4.046-.06.635-.008 1.263-.126 1.86-.348.87-.336 1.56-1.025 1.897-1.897.217-.593.332-1.218.338-1.848.06-1.064.06-1.372.06-4.045s-.06-2.99-.06-4.044c-.01-.623-.128-1.24-.347-1.827-.16-.435-.414-.83-.745-1.152-.318-.34-.71-.605-1.143-.774-.596-.222-1.224-.34-1.86-.348-1.063-.06-1.37-.06-4.045-.06zm0 3.1c1.355 0 2.655.538 3.613 1.496.958.958 1.496 2.257 1.496 3.61 0 2.82-2.288 5.108-5.11 5.108-2.822 0-5.11-2.287-5.11-5.107 0-2.82 2.288-5.107 5.11-5.107zm0 8.415c.878 0 1.72-.348 2.34-.97.62-.62.97-1.46.97-2.338 0-1.827-1.482-3.31-3.31-3.31s-3.31 1.483-3.31 3.31 1.482 3.308 3.31 3.308zm6.51-8.633c0 .658-.533 1.192-1.192 1.192-.66 0-1.193-.534-1.193-1.192 0-.66.534-1.193 1.193-1.193.316 0 .62.126.844.35.223.223.35.526.35.843z\"></path></svg></a><a href=\"https://www.youtube.com/genius\" height=\"19\" target=\"_blank\" rel=\"noopener\" class=\"SocialLinks__Link-jwyj6b-1 fRTMWj\"><svg viewBox=\"0 0 20.01 14.07\"><path d=\"M19.81 3A4.32 4.32 0 0 0 19 1a2.86 2.86 0 0 0-2-.8C14.21 0 10 0 10 0S5.8 0 3 .2A2.87 2.87 0 0 0 1 1a4.32 4.32 0 0 0-.8 2S0 4.51 0 6.06V8a30 30 0 0 0 .2 3 4.33 4.33 0 0 0 .8 2 3.39 3.39 0 0 0 2.2.85c1.46.14 5.9.19 6.68.2h.4c1 0 4.35 0 6.72-.21a2.87 2.87 0 0 0 2-.84 4.32 4.32 0 0 0 .8-2 30.31 30.31 0 0 0 .2-3.21V6.28A30.31 30.31 0 0 0 19.81 3zM7.94 9.63V4l5.41 2.82z\"></path></svg></a></div></div><div class=\"PageFooterdesktop__Quarter-hz1fx1-4 hMUvCn\"><a href=\"/about\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">About Genius</a><a href=\"/contributor_guidelines\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Contributor Guidelines</a><a href=\"/press\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Press</a><a href=\"https://shop.genius.com\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Shop</a><a href=\"mailto:inquiry@genius.com\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Advertise</a><a href=\"https://eventspace.genius.com\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Event Space</a><a href=\"/static/privacy_policy\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Privacy Policy</a><a href=\"/static/privacy_policy#delete-account\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Delete Account</a></div><div class=\"PageFooterdesktop__Quarter-hz1fx1-4 hMUvCn\"><a href=\"/static/licensing\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Licensing</a><a href=\"/jobs\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Jobs</a><a href=\"/developers\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Developers</a><a href=\"/static/copyright\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Copyright Policy</a><a href=\"/feedback/new\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Contact Us</a><div class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Sign Out</div><a href=\"/static/ccpa\" rel=\"noopener\" class=\"PageFooterdesktop__Link-hz1fx1-1 gwrcCS\">Do Not Sell My Personal Information</a></div><div class=\"PageFooterdesktop__Quarter-hz1fx1-4 PageFooterdesktop__OffsetQuarter-hz1fx1-5 diwZPD\"><div class=\"PageFooterdesktop__FinePrint-hz1fx1-6 iDkyVM\">© 2022 Genius Media Group Inc.</div></div><div class=\"PageFooterdesktop__Quarter-hz1fx1-4 hMUvCn\"><a href=\"/static/terms\" class=\"PageFooterdesktop__FinePrint-hz1fx1-6 iDkyVM\">Terms of Use</a></div></div><div class=\"PageGriddesktop-a6v82w-0 PageFooterdesktop__Section-hz1fx1-2 PageFooterdesktop__Bottom-hz1fx1-7 kNXBDG\"><div class=\"PageFooterdesktop__Row-hz1fx1-8 eIiYRJ\"><a href=\"/verified-artists\" class=\"PageFooterdesktop__VerifiedArtists-hz1fx1-10 bMBKQI\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">Verified Artists</span></a><div class=\"PageFooterdesktop__FlexWrap-hz1fx1-9 hNrwqx\"><div class=\"PageFooterdesktop__Label-hz1fx1-11 dcpJwP\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">All Artists:</span></div><a href=\"/artists-index/a\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">a</span></a><a href=\"/artists-index/b\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">b</span></a><a href=\"/artists-index/c\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">c</span></a><a href=\"/artists-index/d\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">d</span></a><a href=\"/artists-index/e\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">e</span></a><a href=\"/artists-index/f\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">f</span></a><a href=\"/artists-index/g\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">g</span></a><a href=\"/artists-index/h\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">h</span></a><a href=\"/artists-index/i\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">i</span></a><a href=\"/artists-index/j\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">j</span></a><a href=\"/artists-index/k\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">k</span></a><a href=\"/artists-index/l\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">l</span></a><a href=\"/artists-index/m\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">m</span></a><a href=\"/artists-index/n\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">n</span></a><a href=\"/artists-index/o\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">o</span></a><a href=\"/artists-index/p\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">p</span></a><a href=\"/artists-index/q\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">q</span></a><a href=\"/artists-index/r\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">r</span></a><a href=\"/artists-index/s\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">s</span></a><a href=\"/artists-index/t\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">t</span></a><a href=\"/artists-index/u\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">u</span></a><a href=\"/artists-index/v\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">v</span></a><a href=\"/artists-index/w\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">w</span></a><a href=\"/artists-index/x\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">x</span></a><a href=\"/artists-index/y\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">y</span></a><a href=\"/artists-index/z\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">z</span></a><a href=\"/artists-index/0\" class=\"PageFooterArtistLinks__Link-sc-1ng9ih0-0 kTXFZQ\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">#</span></a></div></div><div class=\"PageFooterdesktop__Row-hz1fx1-8 eIiYRJ\"><div class=\"PageFooterdesktop__Label-hz1fx1-11 dcpJwP\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">Hot Songs:</span></div><div class=\"PageFooterdesktop__FlexWrap-hz1fx1-9 hNrwqx\"><a href=\"https://genius.com/Harry-styles-as-it-was-lyrics\" class=\"PageFooterHotSongLinks__Link-sc-1adazwo-0 hAxKUd\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">As It Was</span></a><a href=\"https://genius.com/Lauren-spencer-smith-flowers-lyrics\" class=\"PageFooterHotSongLinks__Link-sc-1adazwo-0 hAxKUd\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">Flowers</span></a><a href=\"https://genius.com/Jack-harlow-first-class-lyrics\" class=\"PageFooterHotSongLinks__Link-sc-1adazwo-0 hAxKUd\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">First Class</span></a><a href=\"https://genius.com/Conan-gray-memories-lyrics\" class=\"PageFooterHotSongLinks__Link-sc-1adazwo-0 hAxKUd\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">Memories</span></a><a href=\"https://genius.com/Anees-sun-and-moon-lyrics\" class=\"PageFooterHotSongLinks__Link-sc-1adazwo-0 hAxKUd\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">Sun and Moon</span></a><a href=\"/hot-songs\" class=\"PageFooterHotSongLinks__Link-sc-1adazwo-0 hAxKUd\"><span font-weight=\"normal\" class=\"TextLabel-sc-8kw9oj-0 kokouQ\">View All</span></a></div></div></div></div></div>\n    <script>\n      window.__PRELOADED_STATE__ = JSON.parse('{\\\"currentPage\\\":\\\"songPage\\\",\\\"deviceType\\\":\\\"desktop\\\",\\\"session\\\":{\\\"cmpEnabled\\\":false,\\\"showAds\\\":false,\\\"logClientMetrics\\\":false,\\\"fringeEnabled\\\":false,\\\"features\\\":[\\\"song_stories_public_launch\\\",\\\"ios_spotify_auth\\\"],\\\"currentUser\\\":8250538},\\\"songPage\\\":{\\\"s3CacheExperiment\\\":null,\\\"variant\\\":true,\\\"song\\\":719774,\\\"pinnedQuestions\\\":[],\\\"lyricsData\\\":{\\\"body\\\":{\\\"html\\\":\\\"<p>So far we\\'ve lost focus<br>\\\\nLet\\'s just concentrate on words that could mean everything<br>\\\\n<br>\\\\nOn nights like this<br>\\\\nWe drink ourselves dry<br>\\\\nAnd make promises<br>\\\\nWithout intention<br>\\\\n<br>\\\\nSo fortunate that this was brought up<br>\\\\nThe last time. As I recall<br>\\\\nI can’t hold up your every expectation<br>\\\\n<br>\\\\nOn nights like this<br>\\\\nWe drink ourselves dry<br>\\\\nAnd make promises<br>\\\\nWithout intention<br>\\\\n<br>\\\\nMy God, is this what we’ve become?<br>\\\\nLiving parodies of love and loss<br>\\\\nCan we really be all that lost?<br>\\\\n<br>\\\\nSo fortunate that this was brought up<br>\\\\nThe last time. As I recall<br>\\\\nI can’t hold up your every expectation<br>\\\\n<br>\\\\nOne moment to another I am restless<br>\\\\nSeems making love forever can often risk your heart<br>\\\\nAnd I cannot remember when I was this messed up<br>\\\\nIn service of another I am beautiful<\\/p>\\\\n\\\\n\\\",\\\"children\\\":[{\\\"children\\\":[\\\"So far we\\'ve lost focus\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Let\\'s just concentrate on words that could mean everything\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"tag\\\":\\\"br\\\"},\\\"On nights like this\\\",{\\\"tag\\\":\\\"br\\\"},\\\"We drink ourselves dry\\\",{\\\"tag\\\":\\\"br\\\"},\\\"And make promises\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Without intention\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"tag\\\":\\\"br\\\"},\\\"So fortunate that this was brought up\\\",{\\\"tag\\\":\\\"br\\\"},\\\"The last time. As I recall\\\",{\\\"tag\\\":\\\"br\\\"},\\\"I can’t hold up your every expectation\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"tag\\\":\\\"br\\\"},\\\"On nights like this\\\",{\\\"tag\\\":\\\"br\\\"},\\\"We drink ourselves dry\\\",{\\\"tag\\\":\\\"br\\\"},\\\"And make promises\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Without intention\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"tag\\\":\\\"br\\\"},\\\"My God, is this what we’ve become?\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Living parodies of love and loss\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Can we really be all that lost?\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"tag\\\":\\\"br\\\"},\\\"So fortunate that this was brought up\\\",{\\\"tag\\\":\\\"br\\\"},\\\"The last time. As I recall\\\",{\\\"tag\\\":\\\"br\\\"},\\\"I can’t hold up your every expectation\\\",{\\\"tag\\\":\\\"br\\\"},\\\"\\\",{\\\"tag\\\":\\\"br\\\"},\\\"One moment to another I am restless\\\",{\\\"tag\\\":\\\"br\\\"},\\\"Seems making love forever can often risk your heart\\\",{\\\"tag\\\":\\\"br\\\"},\\\"And I cannot remember when I was this messed up\\\",{\\\"tag\\\":\\\"br\\\"},\\\"In service of another I am beautiful\\\"],\\\"tag\\\":\\\"p\\\"},\\\"\\\"],\\\"tag\\\":\\\"root\\\"},\\\"lyricsPlaceholderReason\\\":null,\\\"clientTimestamps\\\":{\\\"updatedByHumanAt\\\":1594615896,\\\"lyricsUpdatedAt\\\":1594615896}},\\\"hotSongsPreview\\\":[{\\\"url\\\":\\\"https://genius.com/Harry-styles-as-it-was-lyrics\\\",\\\"title\\\":\\\"As It Was\\\",\\\"id\\\":7844309},{\\\"url\\\":\\\"https://genius.com/Lauren-spencer-smith-flowers-lyrics\\\",\\\"title\\\":\\\"Flowers\\\",\\\"id\\\":7765941},{\\\"url\\\":\\\"https://genius.com/Jack-harlow-first-class-lyrics\\\",\\\"title\\\":\\\"First Class\\\",\\\"id\\\":7857574},{\\\"url\\\":\\\"https://genius.com/Conan-gray-memories-lyrics\\\",\\\"title\\\":\\\"Memories\\\",\\\"id\\\":7894064},{\\\"url\\\":\\\"https://genius.com/Anees-sun-and-moon-lyrics\\\",\\\"title\\\":\\\"Sun and Moon\\\",\\\"id\\\":7660481}],\\\"featuredQuestion\\\":null,\\\"showFeaturedQuestion\\\":false,\\\"pendingQuestionCount\\\":0,\\\"dfpKv\\\":[{\\\"values\\\":[\\\"719774\\\"],\\\"name\\\":\\\"song_id\\\"},{\\\"values\\\":[\\\"Chinchilla\\\"],\\\"name\\\":\\\"song_title\\\"},{\\\"values\\\":[\\\"49919\\\"],\\\"name\\\":\\\"artist_id\\\"},{\\\"values\\\":[\\\"TTNG\\\"],\\\"name\\\":\\\"artist_name\\\"},{\\\"values\\\":[\\\"false\\\"],\\\"name\\\":\\\"is_explicit\\\"},{\\\"values\\\":[\\\"14958\\\"],\\\"name\\\":\\\"pageviews\\\"},{\\\"values\\\":[\\\"567\\\"],\\\"name\\\":\\\"primary_tag_id\\\"},{\\\"values\\\":[\\\"rock\\\"],\\\"name\\\":\\\"primary_tag\\\"},{\\\"values\\\":[\\\"1506\\\",\\\"3066\\\",\\\"567\\\"],\\\"name\\\":\\\"tag_id\\\"},{\\\"values\\\":[\\\"E\\\"],\\\"name\\\":\\\"song_tier\\\"},{\\\"values\\\":[],\\\"name\\\":\\\"topic\\\"},{\\\"values\\\":[\\\"false\\\"],\\\"name\\\":\\\"in_top_10\\\"},{\\\"values\\\":[\\\"false\\\"],\\\"name\\\":\\\"artist_in_top_10\\\"},{\\\"values\\\":[\\\"false\\\"],\\\"name\\\":\\\"album_in_top_10\\\"},{\\\"values\\\":[\\\"false\\\"],\\\"name\\\":\\\"new_release\\\"},{\\\"values\\\":[\\\"200810\\\"],\\\"name\\\":\\\"release_month\\\"},{\\\"values\\\":[\\\"2008\\\"],\\\"name\\\":\\\"release_year\\\"},{\\\"values\\\":[\\\"2000\\\"],\\\"name\\\":\\\"release_decade\\\"},{\\\"values\\\":[\\\"false\\\"],\\\"name\\\":\\\"in_top_10_rap\\\"},{\\\"values\\\":[\\\"false\\\"],\\\"name\\\":\\\"in_top_10_rock\\\"},{\\\"values\\\":[\\\"false\\\"],\\\"name\\\":\\\"in_top_10_country\\\"},{\\\"values\\\":[\\\"false\\\"],\\\"name\\\":\\\"in_top_10_r_and_b\\\"},{\\\"values\\\":[\\\"false\\\"],\\\"name\\\":\\\"in_top_10_pop\\\"},{\\\"values\\\":[\\\"production\\\"],\\\"name\\\":\\\"environment\\\"},{\\\"values\\\":[\\\"web\\\"],\\\"name\\\":\\\"platform\\\"},{\\\"values\\\":[\\\"desktop_react_2_column\\\"],\\\"name\\\":\\\"platform_variant\\\"},{\\\"values\\\":[\\\"control\\\"],\\\"name\\\":\\\"interstitial_variant\\\"},{\\\"values\\\":[\\\"song\\\"],\\\"name\\\":\\\"ad_page_type\\\"}],\\\"trackingData\\\":[{\\\"value\\\":719774,\\\"key\\\":\\\"Song ID\\\"},{\\\"value\\\":\\\"Chinchilla\\\",\\\"key\\\":\\\"Title\\\"},{\\\"value\\\":\\\"TTNG\\\",\\\"key\\\":\\\"Primary Artist\\\"},{\\\"value\\\":49919,\\\"key\\\":\\\"Primary Artist ID\\\"},{\\\"value\\\":\\\"Animals\\\",\\\"key\\\":\\\"Primary Album\\\"},{\\\"value\\\":33612,\\\"key\\\":\\\"Primary Album ID\\\"},{\\\"value\\\":\\\"rock\\\",\\\"key\\\":\\\"Tag\\\"},{\\\"value\\\":\\\"rock\\\",\\\"key\\\":\\\"Primary Tag\\\"},{\\\"value\\\":567,\\\"key\\\":\\\"Primary Tag ID\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"Music?\\\"},{\\\"value\\\":\\\"Song\\\",\\\"key\\\":\\\"Annotatable Type\\\"},{\\\"value\\\":719774,\\\"key\\\":\\\"Annotatable ID\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"featured_video\\\"},{\\\"value\\\":[],\\\"key\\\":\\\"cohort_ids\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"has_verified_callout\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"has_featured_annotation\\\"},{\\\"value\\\":\\\"2015-03-08T18:28:49Z\\\",\\\"key\\\":\\\"created_at\\\"},{\\\"value\\\":\\\"2015-03-01\\\",\\\"key\\\":\\\"created_month\\\"},{\\\"value\\\":2015,\\\"key\\\":\\\"created_year\\\"},{\\\"value\\\":\\\"E\\\",\\\"key\\\":\\\"song_tier\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"Has Recirculated Articles\\\"},{\\\"value\\\":\\\"en\\\",\\\"key\\\":\\\"Lyrics Language\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"Has Apple Match\\\"},{\\\"value\\\":\\\"2008-10-13\\\",\\\"key\\\":\\\"Release Date\\\"},{\\\"value\\\":null,\\\"key\\\":\\\"NRM Tier\\\"},{\\\"value\\\":null,\\\"key\\\":\\\"NRM Target Date\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"Has Description\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"Has Youtube URL\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"Has Translation Q&A\\\"},{\\\"value\\\":0,\\\"key\\\":\\\"Comment Count\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"hot\\\"},{\\\"value\\\":8250538,\\\"key\\\":\\\"User ID\\\"},{\\\"value\\\":\\\"control\\\",\\\"key\\\":\\\"web_interstitial_variant\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"two_column_song_page\\\"},{\\\"value\\\":\\\"desktop_react_2_column\\\",\\\"key\\\":\\\"platform_variant\\\"}],\\\"title\\\":\\\"TTNG – Chinchilla Lyrics | Genius Lyrics\\\",\\\"path\\\":\\\"/Ttng-chinchilla-lyrics\\\",\\\"pageType\\\":\\\"song\\\",\\\"initialAdUnits\\\":[\\\"desktop_song_inread\\\",\\\"desktop_song_inread2\\\",\\\"desktop_song_inread3\\\",\\\"desktop_song_leaderboard\\\",\\\"desktop_song_lyrics_footer\\\",\\\"desktop_song_marquee\\\",\\\"desktop_song_medium1\\\",\\\"desktop_song_sidebar_top\\\"],\\\"headerBidPlacements\\\":[],\\\"dmpDataLayer\\\":{\\\"page\\\":{\\\"type\\\":\\\"song\\\"}},\\\"controllerAndAction\\\":\\\"songs#show\\\",\\\"chartbeat\\\":{\\\"title\\\":\\\"TTNG – Chinchilla Lyrics | Genius Lyrics\\\",\\\"sections\\\":\\\"songs,tag:rock\\\",\\\"authors\\\":\\\"TTNG\\\"}},\\\"entities\\\":{\\\"artists\\\":{\\\"49919\\\":{\\\"url\\\":\\\"https://genius.com/artists/Ttng\\\",\\\"slug\\\":\\\"Ttng\\\",\\\"name\\\":\\\"TTNG\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"t\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/f9b83cfbdcc85089e0d322e8a14a901c.532x532x1.png\\\",\\\"id\\\":49919,\\\"headerImageUrl\\\":\\\"https://images.genius.com/e7bc4bc0ad72f28752e27630299d2442.700x700x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/49919\\\",\\\"type\\\":\\\"artist\\\"}},\\\"songs\\\":{\\\"151045\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-zebra-lyrics\\\",\\\"title\\\":\\\"Zebra\\\",\\\"path\\\":\\\"/Ttng-zebra-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":151045,\\\"apiPath\\\":\\\"/songs/151045\\\",\\\"type\\\":\\\"song\\\"},\\\"487024\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-crocodile-lyrics\\\",\\\"title\\\":\\\"Crocodile\\\",\\\"path\\\":\\\"/Ttng-crocodile-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":487024,\\\"apiPath\\\":\\\"/songs/487024\\\",\\\"type\\\":\\\"song\\\"},\\\"719774\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-chinchilla-lyrics\\\",\\\"title\\\":\\\"Chinchilla\\\",\\\"path\\\":\\\"/Ttng-chinchilla-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":719774,\\\"apiPath\\\":\\\"/songs/719774\\\",\\\"type\\\":\\\"song\\\",\\\"writerArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Stuart-smith\\\",\\\"slug\\\":\\\"Stuart-smith\\\",\\\"name\\\":\\\"Stuart Smith\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"s\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/56b0191bf920e9064fbeabfbda56296f.410x410x1.jpg\\\",\\\"id\\\":1290276,\\\"headerImageUrl\\\":\\\"https://images.genius.com/6de79b3173ffc9130dc90784db766336.768x512x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/1290276\\\",\\\"type\\\":\\\"artist\\\"}],\\\"verifiedLyricsBy\\\":[],\\\"verifiedContributors\\\":[],\\\"verifiedAnnotationsBy\\\":[],\\\"topScholar\\\":{\\\"user\\\":{\\\"currentUserMetadata\\\":{\\\"interactions\\\":{\\\"following\\\":false},\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"follow\\\"]},\\\"url\\\":\\\"https://genius.com/Sciabbala\\\",\\\"roleForDisplay\\\":\\\"contributor\\\",\\\"name\\\":\\\"Sciabbala\\\",\\\"login\\\":\\\"Sciabbala\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"iq\\\":2374,\\\"id\\\":592416,\\\"humanReadableRoleForDisplay\\\":\\\"Contributor\\\",\\\"headerImageUrl\\\":\\\"https://images.rapgenius.com/avatars/medium/4c0de329306004791131065edd4d7fac\\\",\\\"avatar\\\":{\\\"medium\\\":{\\\"boundingBox\\\":{\\\"height\\\":400,\\\"width\\\":300},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/medium/4c0de329306004791131065edd4d7fac\\\"},\\\"small\\\":{\\\"boundingBox\\\":{\\\"height\\\":100,\\\"width\\\":100},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/small/4c0de329306004791131065edd4d7fac\\\"},\\\"thumb\\\":{\\\"boundingBox\\\":{\\\"height\\\":32,\\\"width\\\":32},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/thumb/4c0de329306004791131065edd4d7fac\\\"},\\\"tiny\\\":{\\\"boundingBox\\\":{\\\"height\\\":16,\\\"width\\\":16},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/tiny/4c0de329306004791131065edd4d7fac\\\"}},\\\"apiPath\\\":\\\"/users/592416\\\",\\\"aboutMeSummary\\\":\\\"\\\",\\\"type\\\":\\\"user\\\"},\\\"pinnedRole\\\":null,\\\"attributionValue\\\":40,\\\"type\\\":\\\"user_attribution\\\"},\\\"tags\\\":[{\\\"url\\\":\\\"https://genius.com/tags/indie-rock\\\",\\\"primary\\\":false,\\\"name\\\":\\\"Indie Rock\\\",\\\"id\\\":1506,\\\"type\\\":\\\"tag\\\"},{\\\"url\\\":\\\"https://genius.com/tags/math-rock\\\",\\\"primary\\\":false,\\\"name\\\":\\\"Math Rock\\\",\\\"id\\\":3066,\\\"type\\\":\\\"tag\\\"},{\\\"url\\\":\\\"https://genius.com/tags/rock\\\",\\\"primary\\\":true,\\\"name\\\":\\\"Rock\\\",\\\"id\\\":567,\\\"type\\\":\\\"tag\\\"}],\\\"songRelationships\\\":[{\\\"songs\\\":[],\\\"type\\\":\\\"song_relationship\\\",\\\"relationshipType\\\":\\\"samples\\\"},{\\\"songs\\\":[],\\\"type\\\":\\\"song_relationship\\\",\\\"relationshipType\\\":\\\"sampled_in\\\"},{\\\"songs\\\":[],\\\"type\\\":\\\"song_relationship\\\",\\\"relationshipType\\\":\\\"interpolates\\\"},{\\\"songs\\\":[],\\\"type\\\":\\\"song_relationship\\\",\\\"relationshipType\\\":\\\"interpolated_by\\\"},{\\\"songs\\\":[],\\\"type\\\":\\\"song_relationship\\\",\\\"relationshipType\\\":\\\"cover_of\\\"},{\\\"songs\\\":[],\\\"type\\\":\\\"song_relationship\\\",\\\"relationshipType\\\":\\\"covered_by\\\"},{\\\"songs\\\":[],\\\"type\\\":\\\"song_relationship\\\",\\\"relationshipType\\\":\\\"remix_of\\\"},{\\\"songs\\\":[],\\\"type\\\":\\\"song_relationship\\\",\\\"relationshipType\\\":\\\"remixed_by\\\"},{\\\"songs\\\":[],\\\"type\\\":\\\"song_relationship\\\",\\\"relationshipType\\\":\\\"live_version_of\\\"},{\\\"songs\\\":[],\\\"type\\\":\\\"song_relationship\\\",\\\"relationshipType\\\":\\\"performed_live_as\\\"}],\\\"producerArtists\\\":[{\\\"url\\\":\\\"https://genius.com/artists/Max-read\\\",\\\"slug\\\":\\\"Max-read\\\",\\\"name\\\":\\\"Max Read\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"m\\\",\\\"imageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1649950983\\\",\\\"id\\\":49920,\\\"headerImageUrl\\\":\\\"https://assets.genius.com/images/default_avatar_300.png?1649950983\\\",\\\"apiPath\\\":\\\"/artists/49920\\\",\\\"type\\\":\\\"artist\\\"}],\\\"primaryTag\\\":{\\\"url\\\":\\\"https://genius.com/tags/rock\\\",\\\"primary\\\":true,\\\"name\\\":\\\"Rock\\\",\\\"id\\\":567,\\\"type\\\":\\\"tag\\\"},\\\"primaryArtist\\\":49919,\\\"media\\\":[{\\\"url\\\":\\\"https://soundcloud.com/this-town-needs-guns/chinchilla\\\",\\\"type\\\":\\\"audio\\\",\\\"provider\\\":\\\"soundcloud\\\",\\\"attribution\\\":\\\"this town needs guns\\\"},{\\\"url\\\":\\\"http://www.youtube.com/watch?v=oMznqfz9KQo\\\",\\\"type\\\":\\\"video\\\",\\\"start\\\":0,\\\"provider\\\":\\\"youtube\\\"}],\\\"lyricsMarkedStaffApprovedBy\\\":null,\\\"lyricsMarkedCompleteBy\\\":null,\\\"featuredArtists\\\":[],\\\"descriptionAnnotation\\\":5012584,\\\"customPerformances\\\":[],\\\"albums\\\":[{\\\"tracklist\\\":[{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-chinchilla-lyrics\\\",\\\"title\\\":\\\"Chinchilla\\\",\\\"path\\\":\\\"/Ttng-chinchilla-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":719774,\\\"apiPath\\\":\\\"/songs/719774\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":1,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-baboon-lyrics\\\",\\\"title\\\":\\\"Baboon\\\",\\\"path\\\":\\\"/Ttng-baboon-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":719777,\\\"apiPath\\\":\\\"/songs/719777\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":2,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-lemur-lyrics\\\",\\\"title\\\":\\\"Lemur\\\",\\\"path\\\":\\\"/Ttng-lemur-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":719778,\\\"apiPath\\\":\\\"/songs/719778\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":3,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-badger-lyrics\\\",\\\"title\\\":\\\"Badger\\\",\\\"path\\\":\\\"/Ttng-badger-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":719780,\\\"apiPath\\\":\\\"/songs/719780\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":4,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-quetzal-lyrics\\\",\\\"title\\\":\\\"Quetzal\\\",\\\"path\\\":\\\"/Ttng-quetzal-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":719802,\\\"apiPath\\\":\\\"/songs/719802\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":5,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-panda-lyrics\\\",\\\"title\\\":\\\"Panda\\\",\\\"path\\\":\\\"/Ttng-panda-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":719782,\\\"apiPath\\\":\\\"/songs/719782\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":6,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-elk-lyrics\\\",\\\"title\\\":\\\"Elk\\\",\\\"path\\\":\\\"/Ttng-elk-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":719801,\\\"apiPath\\\":\\\"/songs/719801\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":7,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-pig-lyrics\\\",\\\"title\\\":\\\"Pig\\\",\\\"path\\\":\\\"/Ttng-pig-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":719785,\\\"apiPath\\\":\\\"/songs/719785\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":8,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-gibbon-lyrics\\\",\\\"title\\\":\\\"Gibbon\\\",\\\"path\\\":\\\"/Ttng-gibbon-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":719786,\\\"apiPath\\\":\\\"/songs/719786\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":9,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-dog-lyrics\\\",\\\"title\\\":\\\"Dog\\\",\\\"path\\\":\\\"/Ttng-dog-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":719789,\\\"apiPath\\\":\\\"/songs/719789\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":10,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-crocodile-lyrics\\\",\\\"title\\\":\\\"Crocodile\\\",\\\"path\\\":\\\"/Ttng-crocodile-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":487024,\\\"apiPath\\\":\\\"/songs/487024\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":11,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-rabbit-lyrics\\\",\\\"title\\\":\\\"Rabbit\\\",\\\"path\\\":\\\"/Ttng-rabbit-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":719791,\\\"apiPath\\\":\\\"/songs/719791\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":12,\\\"type\\\":\\\"album_appearance\\\"},{\\\"song\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-zebra-lyrics\\\",\\\"title\\\":\\\"Zebra\\\",\\\"path\\\":\\\"/Ttng-zebra-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":151045,\\\"apiPath\\\":\\\"/songs/151045\\\",\\\"type\\\":\\\"song\\\"},\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":13,\\\"type\\\":\\\"album_appearance\\\"}],\\\"artist\\\":{\\\"url\\\":\\\"https://genius.com/artists/Ttng\\\",\\\"slug\\\":\\\"Ttng\\\",\\\"name\\\":\\\"TTNG\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"indexCharacter\\\":\\\"t\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/f9b83cfbdcc85089e0d322e8a14a901c.532x532x1.png\\\",\\\"id\\\":49919,\\\"headerImageUrl\\\":\\\"https://images.genius.com/e7bc4bc0ad72f28752e27630299d2442.700x700x1.jpg\\\",\\\"apiPath\\\":\\\"/artists/49919\\\",\\\"type\\\":\\\"artist\\\"},\\\"url\\\":\\\"https://genius.com/albums/Ttng/Animals\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":13,\\\"month\\\":10,\\\"year\\\":2008},\\\"nameWithArtist\\\":\\\"Animals (artist: TTNG)\\\",\\\"name\\\":\\\"Animals\\\",\\\"id\\\":33612,\\\"fullTitle\\\":\\\"Animals by TTNG\\\",\\\"coverArtUrl\\\":\\\"https://images.genius.com/7b7890cf624378fcedd2e8ce44ea234b.640x640x1.jpg\\\",\\\"coverArtThumbnailUrl\\\":\\\"https://images.genius.com/7b7890cf624378fcedd2e8ce44ea234b.300x300x1.jpg\\\",\\\"apiPath\\\":\\\"/albums/33612\\\",\\\"type\\\":\\\"album\\\"}],\\\"album\\\":33612,\\\"songArtTextColor\\\":\\\"#fff\\\",\\\"songArtSecondaryColor\\\":\\\"#a58735\\\",\\\"songArtPrimaryColor\\\":\\\"#b54624\\\",\\\"currentUserMetadata\\\":{\\\"iqByAction\\\":{\\\"editMetadata\\\":{\\\"primary\\\":{\\\"applicable\\\":true,\\\"base\\\":2,\\\"multiplier\\\":1}}},\\\"relationships\\\":{\\\"pinnedRole\\\":null},\\\"interactions\\\":{\\\"following\\\":false,\\\"pyong\\\":false},\\\"excludedPermissions\\\":[\\\"award_transcription_iq\\\",\\\"remove_transcription_iq\\\",\\\"edit_lyrics\\\",\\\"view_annotation_engagement_data\\\",\\\"publish\\\",\\\"unpublish\\\",\\\"edit_spotify_details\\\",\\\"hide\\\",\\\"unhide\\\",\\\"toggle_featured_video\\\",\\\"add_pinned_annotation_to\\\",\\\"destroy\\\",\\\"mark_as_not_spam\\\",\\\"edit_spotify_annotations_for\\\",\\\"verify_lyrics\\\",\\\"unverify_lyrics\\\",\\\"edit_anything\\\",\\\"edit_any_media\\\",\\\"edit\\\",\\\"rename\\\",\\\"edit_tags\\\",\\\"reindex\\\",\\\"view_lyrics_synchronization\\\",\\\"enable_media\\\",\\\"disable_media\\\",\\\"edit_lyrics_or_annotation_brackets\\\",\\\"see_editorial_indicators\\\",\\\"view_attribution_visualization\\\",\\\"edit_annotation_brackets\\\",\\\"preview_lyrics_for_export\\\",\\\"hide_apple_player\\\",\\\"unhide_apple_player\\\",\\\"trigger_apple_match\\\",\\\"mark_lyrics_evaluation_as_complete\\\",\\\"mark_lyrics_evaluation_as_staff_approved\\\",\\\"unmark_lyrics_evaluation_as_complete\\\",\\\"mark_lyrics_evaluation_as_un_staff_approved\\\",\\\"view_transcriber_media_player\\\",\\\"override_apple_match\\\",\\\"set_song_color_gradient\\\",\\\"mark_as_hot\\\",\\\"unmark_as_hot\\\",\\\"edit_youtube_url\\\",\\\"edit_soundcloud_url\\\",\\\"edit_spotify_uuid\\\",\\\"edit_vevo_url\\\",\\\"moderate_annotations\\\",\\\"see_short_id\\\",\\\"manage_chart_item\\\",\\\"create_tag\\\",\\\"view_lyrics_edit_proposals_on_song\\\"],\\\"permissions\\\":[\\\"follow\\\",\\\"see_pageviews\\\",\\\"pyong\\\",\\\"add_community_annotation_to\\\",\\\"view_apple_music_player\\\",\\\"create_comment\\\",\\\"create_annotation\\\",\\\"view_song_story_gallery\\\",\\\"propose_lyrics_edit\\\"]},\\\"youtubeUrl\\\":\\\"http://www.youtube.com/watch?v=oMznqfz9KQo\\\",\\\"youtubeStart\\\":\\\"\\\",\\\"vttpId\\\":null,\\\"viewableByRoles\\\":[],\\\"updatedByHumanAt\\\":1594615896,\\\"twitterShareMessageWithoutUrl\\\":\\\"TTNG – Chinchilla @TTNGuk\\\",\\\"twitterShareMessage\\\":\\\"TTNG – Chinchilla @TTNGuk https://genius.com/Ttng-chinchilla-lyrics\\\",\\\"trackingPaths\\\":{\\\"concurrent\\\":\\\"/Ttng-chinchilla-lyrics\\\",\\\"aggregate\\\":\\\"/Ttng-chinchilla-lyrics\\\"},\\\"trackingData\\\":[{\\\"value\\\":719774,\\\"key\\\":\\\"Song ID\\\"},{\\\"value\\\":\\\"Chinchilla\\\",\\\"key\\\":\\\"Title\\\"},{\\\"value\\\":\\\"TTNG\\\",\\\"key\\\":\\\"Primary Artist\\\"},{\\\"value\\\":49919,\\\"key\\\":\\\"Primary Artist ID\\\"},{\\\"value\\\":\\\"Animals\\\",\\\"key\\\":\\\"Primary Album\\\"},{\\\"value\\\":33612,\\\"key\\\":\\\"Primary Album ID\\\"},{\\\"value\\\":\\\"rock\\\",\\\"key\\\":\\\"Tag\\\"},{\\\"value\\\":\\\"rock\\\",\\\"key\\\":\\\"Primary Tag\\\"},{\\\"value\\\":567,\\\"key\\\":\\\"Primary Tag ID\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"Music?\\\"},{\\\"value\\\":\\\"Song\\\",\\\"key\\\":\\\"Annotatable Type\\\"},{\\\"value\\\":719774,\\\"key\\\":\\\"Annotatable ID\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"featured_video\\\"},{\\\"value\\\":[],\\\"key\\\":\\\"cohort_ids\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"has_verified_callout\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"has_featured_annotation\\\"},{\\\"value\\\":\\\"2015-03-08T18:28:49Z\\\",\\\"key\\\":\\\"created_at\\\"},{\\\"value\\\":\\\"2015-03-01\\\",\\\"key\\\":\\\"created_month\\\"},{\\\"value\\\":2015,\\\"key\\\":\\\"created_year\\\"},{\\\"value\\\":\\\"E\\\",\\\"key\\\":\\\"song_tier\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"Has Recirculated Articles\\\"},{\\\"value\\\":\\\"en\\\",\\\"key\\\":\\\"Lyrics Language\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"Has Apple Match\\\"},{\\\"value\\\":\\\"2008-10-13\\\",\\\"key\\\":\\\"Release Date\\\"},{\\\"value\\\":null,\\\"key\\\":\\\"NRM Tier\\\"},{\\\"value\\\":null,\\\"key\\\":\\\"NRM Target Date\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"Has Description\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"Has Youtube URL\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"Has Translation Q&A\\\"},{\\\"value\\\":0,\\\"key\\\":\\\"Comment Count\\\"},{\\\"value\\\":false,\\\"key\\\":\\\"hot\\\"},{\\\"value\\\":8250538,\\\"key\\\":\\\"User ID\\\"},{\\\"value\\\":\\\"control\\\",\\\"key\\\":\\\"web_interstitial_variant\\\"},{\\\"value\\\":true,\\\"key\\\":\\\"two_column_song_page\\\"},{\\\"value\\\":\\\"desktop_react_2_column\\\",\\\"key\\\":\\\"platform_variant\\\"}],\\\"titleWithFeatured\\\":\\\"Chinchilla\\\",\\\"stats\\\":{\\\"pageviews\\\":14959,\\\"hot\\\":false,\\\"verifiedAnnotations\\\":0,\\\"unreviewedAnnotations\\\":1,\\\"transcribers\\\":2,\\\"iqEarners\\\":9,\\\"contributors\\\":9,\\\"acceptedAnnotations\\\":0},\\\"spotifyUuid\\\":null,\\\"soundcloudUrl\\\":\\\"https://soundcloud.com/this-town-needs-guns/chinchilla\\\",\\\"songArtImageUrl\\\":\\\"https://images.genius.com/7b7890cf624378fcedd2e8ce44ea234b.640x640x1.jpg\\\",\\\"songArtImageThumbnailUrl\\\":\\\"https://images.genius.com/7b7890cf624378fcedd2e8ce44ea234b.300x300x1.jpg\\\",\\\"shareUrl\\\":\\\"https://genius.com/Ttng-chinchilla-lyrics\\\",\\\"releaseDateForDisplay\\\":\\\"October 13, 2008\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":13,\\\"month\\\":10,\\\"year\\\":2008},\\\"releaseDate\\\":\\\"2008-10-13\\\",\\\"recordingLocation\\\":null,\\\"pyongsCount\\\":2,\\\"pusherChannel\\\":\\\"song-719774\\\",\\\"published\\\":false,\\\"pendingLyricsEditsCount\\\":0,\\\"lyricsUpdatedAt\\\":1594615896,\\\"lyricsPlaceholderReason\\\":null,\\\"lyricsOwnerId\\\":592416,\\\"isMusic\\\":true,\\\"instrumental\\\":false,\\\"hidden\\\":false,\\\"headerImageUrl\\\":\\\"https://images.genius.com/7b7890cf624378fcedd2e8ce44ea234b.640x640x1.jpg\\\",\\\"headerImageThumbnailUrl\\\":\\\"https://images.genius.com/7b7890cf624378fcedd2e8ce44ea234b.300x300x1.jpg\\\",\\\"hasInstagramReelAnnotations\\\":null,\\\"fullTitle\\\":\\\"Chinchilla by TTNG\\\",\\\"featuredVideo\\\":false,\\\"facebookShareMessageWithoutUrl\\\":\\\"TTNG – Chinchilla\\\",\\\"explicit\\\":false,\\\"embedContent\\\":\\\"<div id=\\'rg_embed_link_719774\\' class=\\'rg_embed_link\\' data-song-id=\\'719774\\'>Read <a href=\\'https://genius.com/Ttng-chinchilla-lyrics\\'>“Chinchilla” by TTNG<\\/a> on Genius<\\/div> <script crossorigin src=\\'//genius.com/songs/719774/embed.js\\'><\\/script>\\\",\\\"descriptionPreview\\\":\\\"This song is about those relationships with a lot of fights and reconciliations. The singer and his couple are aruging/reconciliating, telling themselves everything is going to be better and things will change for good, specially when they get drunk, just to fight and reconciliate over and over again.\\\",\\\"description\\\":{\\\"markdown\\\":\\\"This song is about those relationships with a lot of fights and reconciliations. The singer and his couple are aruging/reconciliating, telling themselves everything is going to be better and things will change for good, specially when they get drunk, just to fight and reconciliate over and over again.\\\",\\\"html\\\":\\\"<p>This song is about those relationships with a lot of fights and reconciliations. The singer and his couple are aruging/reconciliating, telling themselves everything is going to be better and things will change for good, specially when they get drunk, just to fight and reconciliate over and over again.<\\/p>\\\"},\\\"customSongArtImageUrl\\\":null,\\\"customHeaderImageUrl\\\":\\\"\\\",\\\"commentCount\\\":0,\\\"artistNames\\\":\\\"TTNG\\\",\\\"appleMusicPlayerUrl\\\":\\\"https://genius.com/songs/719774/apple_music_player\\\",\\\"appleMusicId\\\":\\\"1416751416\\\",\\\"annotationCount\\\":1},\\\"719777\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-baboon-lyrics\\\",\\\"title\\\":\\\"Baboon\\\",\\\"path\\\":\\\"/Ttng-baboon-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":719777,\\\"apiPath\\\":\\\"/songs/719777\\\",\\\"type\\\":\\\"song\\\"},\\\"719778\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-lemur-lyrics\\\",\\\"title\\\":\\\"Lemur\\\",\\\"path\\\":\\\"/Ttng-lemur-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":719778,\\\"apiPath\\\":\\\"/songs/719778\\\",\\\"type\\\":\\\"song\\\"},\\\"719780\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-badger-lyrics\\\",\\\"title\\\":\\\"Badger\\\",\\\"path\\\":\\\"/Ttng-badger-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":719780,\\\"apiPath\\\":\\\"/songs/719780\\\",\\\"type\\\":\\\"song\\\"},\\\"719782\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-panda-lyrics\\\",\\\"title\\\":\\\"Panda\\\",\\\"path\\\":\\\"/Ttng-panda-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":719782,\\\"apiPath\\\":\\\"/songs/719782\\\",\\\"type\\\":\\\"song\\\"},\\\"719785\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-pig-lyrics\\\",\\\"title\\\":\\\"Pig\\\",\\\"path\\\":\\\"/Ttng-pig-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":719785,\\\"apiPath\\\":\\\"/songs/719785\\\",\\\"type\\\":\\\"song\\\"},\\\"719786\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-gibbon-lyrics\\\",\\\"title\\\":\\\"Gibbon\\\",\\\"path\\\":\\\"/Ttng-gibbon-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":719786,\\\"apiPath\\\":\\\"/songs/719786\\\",\\\"type\\\":\\\"song\\\"},\\\"719789\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-dog-lyrics\\\",\\\"title\\\":\\\"Dog\\\",\\\"path\\\":\\\"/Ttng-dog-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":719789,\\\"apiPath\\\":\\\"/songs/719789\\\",\\\"type\\\":\\\"song\\\"},\\\"719791\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-rabbit-lyrics\\\",\\\"title\\\":\\\"Rabbit\\\",\\\"path\\\":\\\"/Ttng-rabbit-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":719791,\\\"apiPath\\\":\\\"/songs/719791\\\",\\\"type\\\":\\\"song\\\"},\\\"719801\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-elk-lyrics\\\",\\\"title\\\":\\\"Elk\\\",\\\"path\\\":\\\"/Ttng-elk-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":719801,\\\"apiPath\\\":\\\"/songs/719801\\\",\\\"type\\\":\\\"song\\\"},\\\"719802\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-quetzal-lyrics\\\",\\\"title\\\":\\\"Quetzal\\\",\\\"path\\\":\\\"/Ttng-quetzal-lyrics\\\",\\\"lyricsState\\\":\\\"complete\\\",\\\"id\\\":719802,\\\"apiPath\\\":\\\"/songs/719802\\\",\\\"type\\\":\\\"song\\\"}},\\\"albumAppearances\\\":{\\\"151045\\\":{\\\"song\\\":151045,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":13,\\\"type\\\":\\\"album_appearance\\\"},\\\"487024\\\":{\\\"song\\\":487024,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":11,\\\"type\\\":\\\"album_appearance\\\"},\\\"719774\\\":{\\\"song\\\":719774,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":1,\\\"type\\\":\\\"album_appearance\\\"},\\\"719777\\\":{\\\"song\\\":719777,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":2,\\\"type\\\":\\\"album_appearance\\\"},\\\"719778\\\":{\\\"song\\\":719778,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":3,\\\"type\\\":\\\"album_appearance\\\"},\\\"719780\\\":{\\\"song\\\":719780,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":4,\\\"type\\\":\\\"album_appearance\\\"},\\\"719782\\\":{\\\"song\\\":719782,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":6,\\\"type\\\":\\\"album_appearance\\\"},\\\"719785\\\":{\\\"song\\\":719785,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":8,\\\"type\\\":\\\"album_appearance\\\"},\\\"719786\\\":{\\\"song\\\":719786,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":9,\\\"type\\\":\\\"album_appearance\\\"},\\\"719789\\\":{\\\"song\\\":719789,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":10,\\\"type\\\":\\\"album_appearance\\\"},\\\"719791\\\":{\\\"song\\\":719791,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":12,\\\"type\\\":\\\"album_appearance\\\"},\\\"719801\\\":{\\\"song\\\":719801,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":7,\\\"type\\\":\\\"album_appearance\\\"},\\\"719802\\\":{\\\"song\\\":719802,\\\"currentUserMetadata\\\":{\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"view_song\\\"]},\\\"number\\\":5,\\\"type\\\":\\\"album_appearance\\\"}},\\\"albums\\\":{\\\"33612\\\":{\\\"tracklist\\\":[719774,719777,719778,719780,719802,719782,719801,719785,719786,719789,487024,719791,151045],\\\"artist\\\":49919,\\\"url\\\":\\\"https://genius.com/albums/Ttng/Animals\\\",\\\"releaseDateComponents\\\":{\\\"day\\\":13,\\\"month\\\":10,\\\"year\\\":2008},\\\"nameWithArtist\\\":\\\"Animals (artist: TTNG)\\\",\\\"name\\\":\\\"Animals\\\",\\\"id\\\":33612,\\\"fullTitle\\\":\\\"Animals by TTNG\\\",\\\"coverArtUrl\\\":\\\"https://images.genius.com/7b7890cf624378fcedd2e8ce44ea234b.640x640x1.jpg\\\",\\\"coverArtThumbnailUrl\\\":\\\"https://images.genius.com/7b7890cf624378fcedd2e8ce44ea234b.300x300x1.jpg\\\",\\\"apiPath\\\":\\\"/albums/33612\\\",\\\"type\\\":\\\"album\\\"}},\\\"users\\\":{\\\"2039425\\\":{\\\"currentUserMetadata\\\":{\\\"interactions\\\":{\\\"following\\\":false},\\\"excludedPermissions\\\":[],\\\"permissions\\\":[\\\"follow\\\"]},\\\"url\\\":\\\"https://genius.com/JoyDivision95\\\",\\\"roleForDisplay\\\":null,\\\"name\\\":\\\"JoyDivision95\\\",\\\"login\\\":\\\"JoyDivision95\\\",\\\"isVerified\\\":false,\\\"isMemeVerified\\\":false,\\\"iq\\\":224,\\\"id\\\":2039425,\\\"humanReadableRoleForDisplay\\\":null,\\\"headerImageUrl\\\":\\\"https://images.rapgenius.com/avatars/medium/f5219809b9ecdade440f4b533ab6d0f7\\\",\\\"avatar\\\":{\\\"medium\\\":{\\\"boundingBox\\\":{\\\"height\\\":400,\\\"width\\\":300},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/medium/f5219809b9ecdade440f4b533ab6d0f7\\\"},\\\"small\\\":{\\\"boundingBox\\\":{\\\"height\\\":100,\\\"width\\\":100},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/small/f5219809b9ecdade440f4b533ab6d0f7\\\"},\\\"thumb\\\":{\\\"boundingBox\\\":{\\\"height\\\":32,\\\"width\\\":32},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/thumb/f5219809b9ecdade440f4b533ab6d0f7\\\"},\\\"tiny\\\":{\\\"boundingBox\\\":{\\\"height\\\":16,\\\"width\\\":16},\\\"url\\\":\\\"https://images.rapgenius.com/avatars/tiny/f5219809b9ecdade440f4b533ab6d0f7\\\"}},\\\"apiPath\\\":\\\"/users/2039425\\\",\\\"aboutMeSummary\\\":\\\"\\\",\\\"type\\\":\\\"user\\\"},\\\"8250538\\\":{\\\"currentUserMetadata\\\":{\\\"features\\\":[],\\\"interactions\\\":{\\\"following\\\":false},\\\"excludedPermissions\\\":[\\\"follow\\\",\\\"see_unreviewed_referents\\\",\\\"view_user_report\\\",\\\"manage_messages\\\",\\\"impersonate\\\",\\\"remove_asset\\\",\\\"view_rate_limits\\\",\\\"view_penalties\\\",\\\"search_user_aliases\\\",\\\"manage_incoming_emails\\\",\\\"view_probably_spam_songs\\\",\\\"view_chartbeat\\\",\\\"create_article\\\",\\\"manage_articles\\\"],\\\"permissions\\\":[\\\"view_contribution_opportunity_assignments\\\",\\\"view_activity_stream_firehose\\\"]},\\\"virtualIqEventTypeMap\\\":{\\\"createdCommunityAnnotation\\\":\\\"pending_explanation\\\",\\\"createdPinnedAnnotation\\\":\\\"verified_explanation_by_non_meme\\\"},\\\"url\\\":\\\"https://genius.com/pibbyG\\\",\\\"unreadNewsfeedInboxCount\\\":0,\\\"unreadMessagesInboxCount\\\":1,\\\"unreadMainActivityInboxCount\\\":1,\\\"unreadIqInboxCount\\\":0,\\\"unreadGroupsInboxCount\\\":0,\\\"roleForDisplay\\\":null,\\\"pusherChannel\\\":\\\"private-user-8250538\\\",\\\"name\\\":\\\"pibbyG\\\",\\\"login\\\":\\\"pibbyG\\\",\\\"isVerified\\\":false,\\\"isModerator\\\":false,\\\"isMemeVerified\\\":false,\\\"isEditor\\\":false,\\\"iq\\\":100,\\\"id\\\":8250538,\\\"humanReadableRoleForDisplay\\\":null,\\\"headerImageUrl\\\":\\\"https://images.genius.com/avatars/medium/8a3f146f096d2e36f4a12d72ff2f2bf8\\\",\\\"dismissedContributionOpportunitiesExplanation\\\":false,\\\"avatar\\\":{\\\"medium\\\":{\\\"boundingBox\\\":{\\\"height\\\":400,\\\"width\\\":300},\\\"url\\\":\\\"https://images.genius.com/avatars/medium/8a3f146f096d2e36f4a12d72ff2f2bf8\\\"},\\\"small\\\":{\\\"boundingBox\\\":{\\\"height\\\":100,\\\"width\\\":100},\\\"url\\\":\\\"https://images.genius.com/avatars/small/8a3f146f096d2e36f4a12d72ff2f2bf8\\\"},\\\"thumb\\\":{\\\"boundingBox\\\":{\\\"height\\\":32,\\\"width\\\":32},\\\"url\\\":\\\"https://images.genius.com/avatars/thumb/8a3f146f096d2e36f4a12d72ff2f2bf8\\\"},\\\"tiny\\\":{\\\"boundingBox\\\":{\\\"height\\\":16,\\\"width\\\":16},\\\"url\\\":\\\"https://images.genius.com/avatars/tiny/8a3f146f096d2e36f4a12d72ff2f2bf8\\\"}},\\\"apiPath\\\":\\\"/users/8250538\\\",\\\"aboutMeSummary\\\":\\\"\\\",\\\"type\\\":\\\"user\\\"}},\\\"annotations\\\":{\\\"5012584\\\":{\\\"verifiedBy\\\":null,\\\"topComment\\\":null,\\\"rejectionComment\\\":null,\\\"createdBy\\\":2039425,\\\"cosignedBy\\\":[],\\\"authors\\\":[{\\\"user\\\":2039425,\\\"pinnedRole\\\":null,\\\"attribution\\\":1,\\\"type\\\":\\\"user_attribution\\\"}],\\\"acceptedBy\\\":null,\\\"currentUserMetadata\\\":{\\\"iqByAction\\\":{\\\"delete\\\":{\\\"primary\\\":{\\\"applicable\\\":true,\\\"base\\\":5,\\\"multiplier\\\":1}},\\\"reject\\\":{\\\"primary\\\":{\\\"applicable\\\":true,\\\"base\\\":2,\\\"multiplier\\\":1}},\\\"accept\\\":{\\\"primary\\\":{\\\"applicable\\\":true,\\\"base\\\":10,\\\"multiplier\\\":1}}},\\\"interactions\\\":{\\\"vote\\\":null,\\\"pyong\\\":false,\\\"cosign\\\":false},\\\"excludedPermissions\\\":[\\\"edit\\\",\\\"cosign\\\",\\\"uncosign\\\",\\\"destroy\\\",\\\"accept\\\",\\\"reject\\\",\\\"see_unreviewed\\\",\\\"clear_votes\\\",\\\"pin_to_profile\\\",\\\"unpin_from_profile\\\",\\\"update_source\\\",\\\"edit_custom_preview\\\"],\\\"permissions\\\":[\\\"vote\\\",\\\"propose_edit_to\\\",\\\"create_comment\\\"]},\\\"votesTotal\\\":2,\\\"verified\\\":false,\\\"url\\\":\\\"https://genius.com/5012584/Ttng-chinchilla/Chinchilla\\\",\\\"twitterShareMessage\\\":\\\"“This song is about those relationships with a lot of fights and reconciliations. The singer and his coup…” —@Genius\\\",\\\"state\\\":\\\"pending\\\",\\\"source\\\":null,\\\"shareUrl\\\":\\\"https://genius.com/5012584\\\",\\\"referentId\\\":5012584,\\\"pyongsCount\\\":null,\\\"proposedEditCount\\\":0,\\\"pinned\\\":false,\\\"needsExegesis\\\":false,\\\"id\\\":5012584,\\\"hasVoters\\\":true,\\\"embedContent\\\":\\\"<blockquote class=\\'rg_standalone_container\\' data-src=\\'//genius.com/annotations/5012584/standalone_embed\\'><a href=\\'https://genius.com/5012584/Ttng-chinchilla/Chinchilla\\'>Chinchilla<\\/a><br><a href=\\'https://genius.com/Ttng-chinchilla-lyrics\\'>&#8213; TTNG – Chinchilla<\\/a><\\/blockquote><script async crossorigin src=\\'//genius.com/annotations/load_standalone_embeds.js\\'><\\/script>\\\",\\\"deleted\\\":false,\\\"customPreview\\\":null,\\\"createdAt\\\":1540614220,\\\"community\\\":true,\\\"commentCount\\\":0,\\\"body\\\":{\\\"markdown\\\":\\\"This song is about those relationships with a lot of fights and reconciliations. The singer and his couple are aruging/reconciliating, telling themselves everything is going to be better and things will change for good, specially when they get drunk, just to fight and reconciliate over and over again.\\\",\\\"html\\\":\\\"<p>This song is about those relationships with a lot of fights and reconciliations. The singer and his couple are aruging/reconciliating, telling themselves everything is going to be better and things will change for good, specially when they get drunk, just to fight and reconciliate over and over again.<\\/p>\\\"},\\\"beingCreated\\\":false,\\\"apiPath\\\":\\\"/annotations/5012584\\\",\\\"type\\\":\\\"annotation\\\"}},\\\"referents\\\":{\\\"5012584\\\":{\\\"annotations\\\":[5012584],\\\"annotatable\\\":{\\\"url\\\":\\\"https://genius.com/Ttng-chinchilla-lyrics\\\",\\\"type\\\":\\\"song\\\",\\\"title\\\":\\\"Chinchilla\\\",\\\"linkTitle\\\":\\\"Chinchilla by TTNG\\\",\\\"imageUrl\\\":\\\"https://images.genius.com/7b7890cf624378fcedd2e8ce44ea234b.640x640x1.jpg\\\",\\\"id\\\":719774,\\\"context\\\":\\\"TTNG\\\",\\\"clientTimestamps\\\":{\\\"lyricsUpdatedAt\\\":1594615896,\\\"updatedByHumanAt\\\":1594615896},\\\"apiPath\\\":\\\"/songs/719774\\\"},\\\"twitterShareMessage\\\":\\\"“This song is about those relationships with a lot of fights and reconciliations. The singer and …” —@Genius\\\",\\\"trackingPaths\\\":{\\\"concurrent\\\":\\\"/Ttng-chinchilla-lyrics\\\",\\\"aggregate\\\":\\\"/5012584/Ttng-chinchilla/Chinchilla\\\"},\\\"currentUserMetadata\\\":{\\\"relationships\\\":{\\\"pinnedRole\\\":null},\\\"excludedPermissions\\\":[\\\"add_pinned_annotation_to\\\",\\\"add_community_annotation_to\\\"],\\\"permissions\\\":[]},\\\"verifiedAnnotatorIds\\\":[],\\\"url\\\":\\\"https://genius.com/5012584/Ttng-chinchilla/Chinchilla\\\",\\\"songId\\\":719774,\\\"range\\\":{\\\"content\\\":\\\"Chinchilla\\\"},\\\"path\\\":\\\"/5012584/Ttng-chinchilla/Chinchilla\\\",\\\"isImage\\\":false,\\\"isDescription\\\":true,\\\"iosAppUrl\\\":\\\"genius://referents/5012584\\\",\\\"id\\\":5012584,\\\"fragment\\\":\\\"Chinchilla\\\",\\\"classification\\\":\\\"unreviewed\\\",\\\"apiPath\\\":\\\"/referents/5012584\\\",\\\"annotatorLogin\\\":\\\"JoyDivision95\\\",\\\"annotatorId\\\":2039425,\\\"type\\\":\\\"referent\\\"}}}}');\n      window.__APP_CONFIG__ = {\"env\":\"production\",\"api_root_url\":\"/api\",\"genius_live_launched\":false,\"genius_live_launch_link\":\"https://live.genius.com\",\"microsite_label\":\"\",\"microsite_url\":\"\",\"transform_domain\":\"transform.genius.com\",\"facebook_app_id\":\"265539304824\",\"facebook_opengraph_api_version\":\"8.0\",\"pusher_app_key\":\"6d893fcc6a0c695853ac\",\"embedly_key\":\"fc778e44915911e088ae4040f9f86dcd\",\"a9_pub_id\":\"3459\",\"app_store_url\":\"https://itunes.apple.com/us/app/genius-by-rap-genius-search/id709482991?ls=1&mt=8\",\"play_store_url\":\"https://play.google.com/store/apps/details?id=com.genius.android\",\"soundcloud_client_id\":\"632c544d1c382f82526f369877aab5c0\",\"annotator_context_length\":32,\"comment_reasons\":[{\"_type\":\"comment_reason\",\"context_url\":\"https://genius.com/8846441/Genius-how-genius-works/More-on-annotations\",\"display_character\":\"R\",\"handle\":\"Restates the line\",\"id\":1,\"name\":\"restates-the-line\",\"raw_name\":\"restates the line\",\"requires_body\":false,\"slug\":\"restates_the_line\"},{\"_type\":\"comment_reason\",\"context_url\":\"https://genius.com/8846441/Genius-how-genius-works/More-on-annotations\",\"display_character\":\"S\",\"handle\":\"It’s a stretch\",\"id\":2,\"name\":\"its-a-stretch\",\"raw_name\":\"it’s a stretch\",\"requires_body\":false,\"slug\":\"its_a_stretch\"},{\"_type\":\"comment_reason\",\"context_url\":\"https://genius.com/8846441/Genius-how-genius-works/More-on-annotations\",\"display_character\":\"M\",\"handle\":\"Missing something\",\"id\":3,\"name\":\"missing-something\",\"raw_name\":\"missing something\",\"requires_body\":false,\"slug\":\"missing_something\"},{\"_type\":\"comment_reason\",\"context_url\":\"https://genius.com/8846441/Genius-how-genius-works/More-on-annotations\",\"display_character\":\"…\",\"handle\":\"Other\",\"id\":4,\"name\":\"other\",\"raw_name\":\"other\",\"requires_body\":true,\"slug\":\"other\"}],\"comment_reasons_help_url\":\"https://genius.com/8846441/Genius-how-genius-works/More-on-annotations\",\"filepicker_api_key\":\"Ar03MDs73TQm241ZgLwfjz\",\"filepicker_policy\":\"eyJleHBpcnkiOjIzNTEwOTE1NTgsImNhbGwiOlsicGljayIsInJlYWQiLCJzdG9yZSIsInN0YXQiLCJjb252ZXJ0Il19\",\"filepicker_signature\":\"68597b455e6c09bce0bfd73f758e299c95d49a5d5c8e808aaf4877da7801c4da\",\"filepicker_s3_image_bucket\":\"filepicker-images-rapgenius\",\"available_roles\":[\"moderator\",\"mega_boss\",\"in_house_staff\",\"verified_artist\",\"meme_artist\",\"engineer\",\"editor\",\"educator\",\"staff\",\"whitehat\",\"tech_liaison\",\"mediator\",\"transcriber\"],\"canonical_domain\":\"genius.com\",\"enable_angular_debug\":false,\"fact_track_launch_article_url\":\"https://genius.com/a/genius-and-spotify-together\",\"user_authority_roles\":[\"moderator\",\"editor\",\"mediator\",\"transcriber\"],\"user_verification_roles\":[\"community_artist\",\"verified_artist\",\"meme_artist\"],\"user_vote_types_for_delete\":[\"votes\",\"upvotes\",\"downvotes\"],\"brightcove_account_id\":\"4863540648001\",\"mixpanel_delayed_events_timeout\":\"86400\",\"unreviewed_annotation_tooltip_info_url\":\"https://genius.com/8846524/Genius-how-genius-works/More-on-editorial-review\",\"community_policy_and_moderation_guidelines\":\"https://genius.com/Genius-community-policy-and-moderation-guidelines-annotated\",\"video_placements\":{\"desktop_song_page\":[{\"name\":\"sidebar\",\"min_relevance\":\"high\",\"fringe_min_relevance\":\"low\",\"max_videos\":0},{\"name\":\"sidebar_thumb\",\"min_relevance\":\"medium\",\"fringe_min_relevance\":\"low\",\"max_videos\":0},{\"name\":\"recirculated\",\"min_relevance\":\"low\",\"max_videos\":3}],\"mobile_song_page\":[{\"name\":\"footer\",\"min_relevance\":\"medium\",\"max_videos\":1},{\"name\":\"recirculated\",\"min_relevance\":\"low\",\"max_videos\":3}],\"desktop_artist_page\":[{\"name\":\"sidebar\",\"min_relevance\":\"medium\",\"fringe_min_relevance\":\"low\",\"max_videos\":2}],\"mobile_artist_page\":[{\"name\":\"carousel\",\"min_relevance\":\"medium\",\"fringe_min_relevance\":\"low\",\"max_videos\":5}],\"amp_song_page\":[{\"name\":\"footer\",\"min_relevance\":\"medium\",\"max_videos\":1}],\"amp_video_page\":[{\"name\":\"related\",\"min_relevance\":\"low\",\"max_videos\":8}],\"desktop_video_page\":[{\"name\":\"series_related\",\"min_relevance\":\"low\",\"max_videos\":8,\"series\":true},{\"name\":\"related\",\"min_relevance\":\"low\",\"max_videos\":8}],\"desktop_article_page\":[{\"name\":\"carousel\",\"min_relevance\":\"low\",\"max_videos\":5}],\"mobile_article_page\":[{\"name\":\"carousel\",\"min_relevance\":\"low\",\"max_videos\":5}],\"desktop_album_page\":[{\"name\":\"sidebar\",\"min_relevance\":\"medium\",\"fringe_min_relevance\":\"low\",\"max_videos\":1}],\"amp_album_page\":[{\"name\":\"carousel\",\"min_relevance\":\"low\",\"max_videos\":5}]},\"app_name\":\"rapgenius-cedar\",\"vttp_parner_id\":\"719c82b0-266e-11e7-827d-7f7dc47f6bc0\",\"default_cover_art_url\":\"https://assets.genius.com/images/default_cover_art.png?1649950983\",\"sizies_base_url\":\"https://t2.genius.com/unsafe\",\"max_line_item_event_count\":10,\"dmp_match_threshold\":0.05,\"ab_tests\":[\"apple_desktop_static_cta\"],\"external_song_match_purposes\":[\"streaming_service_lyrics\",\"streaming_service_player\"],\"release_version\":\"Production 187cde40\",\"react_bugsnag_api_key\":\"a3ab84a89baa4ee509c9e3f71b9296e0\",\"mixpanel_token\":\"77967c52dc38186cc1aadebdd19e2a82\",\"mixpanel_enabled\":true,\"get_involved_page_url\":\"https://genius.com/Genius-getting-involved-with-genius-projects-annotated\",\"solidarity_text\":\"\",\"solidarity_url\":\"http://so.genius.com/aroYTdx\",\"track_gdpr_banner_shown_event\":true,\"show_cmp_modal\":true,\"react_song_page_survey_url\":\"https://forms.gle/rSPEFZEHvZmXakvD7\",\"recaptcha_v3_site_key\":\"6LewuscaAAAAABNevDiTNPHrKCs8viRjfPnm6xc6\",\"ga_web_vitals_sampling_percentage\":\"10\",\"mixpanel_web_vitals_sampling_percentage\":{\"mobile\":\"5\",\"desktop\":\"10\"},\"top_level_block_containers\":[\"address\",\"article\",\"aside\",\"blockquote\",\"div\",\"dl\",\"fieldset\",\"footer\",\"form\",\"h1\",\"h2\",\"h3\",\"h4\",\"h5\",\"h6\",\"header\",\"menu\",\"nav\",\"ol\",\"p\",\"pre\",\"section\",\"table\",\"ul\"],\"top_level_standalone_blocks\":[\"img\",\"hr\"],\"primis_player_api_id\":\"geniusPrimis\",\"zendesk_ccpa_link\":\"https://support.genius.com/hc/en-us/requests/new\",\"artist_promo_portal_launched\":true,\"transcriber_guide_url\":\"https://genius.com/25045402/Genius-what-is-a-transcriber/Mark-lyrics-complete-coming-soon\",\"brightcove_mobile_thumbnail_web_player_id\":\"SyGQSOxol\",\"brightcove_modal_web_player_id\":\"S1LI5bh0\",\"brightcove_song_story_web_player_id\":\"SkfSovRVf\",\"brightcove_standard_web_player_id\":\"S1ZcmcOC1x\",\"brightcove_standard_no_autoplay_web_player_id\":\"ByRtIUBvx\",\"brightcove_sitemap_player_id\":\"BJfoOE1ol\"};\n      window.__IQ_BY_EVENT_TYPE__ = {\"accepted_a_lyrics_edit\":3.0,\"annotation_downvote_by_contrib\":-1.0,\"annotation_downvote_by_default\":-1.0,\"annotation_downvote_by_editor\":-1.0,\"annotation_downvote_by_high_iq_user\":-1.0,\"annotation_downvote_by_moderator\":-1.0,\"annotation_upvote_by_contrib\":4.0,\"annotation_upvote_by_default\":2.0,\"annotation_upvote_by_editor\":6.0,\"annotation_upvote_by_high_iq_user\":4.0,\"annotation_upvote_by_moderator\":10.0,\"answer_downvote_by_contrib\":-1.0,\"answer_downvote_by_default\":-1.0,\"answer_downvote_by_editor\":-1.0,\"answer_downvote_by_high_iq_user\":-1.0,\"answer_downvote_by_moderator\":-1.0,\"answered_a_question\":5.0,\"answered_a_question_meme\":25.0,\"answered_a_question_verified\":10.0,\"answer_upvote_by_contrib\":4.0,\"answer_upvote_by_default\":2.0,\"answer_upvote_by_editor\":6.0,\"answer_upvote_by_high_iq_user\":4.0,\"answer_upvote_by_moderator\":10.0,\"archived_a_question\":1.0,\"article_downvote\":-1.0,\"article_upvote\":1.0,\"asked_a_question\":1.0,\"auto_accepted_explanation\":15.0,\"comment_downvote\":-0.5,\"comment_upvote\":0.5,\"created_a_lyrics_edit\":2.0,\"created_a_real_high_priority_song\":60.0,\"created_a_real_song\":30.0,\"created_a_song\":5.0,\"forum_post_downvote\":-0.5,\"forum_post_upvote\":0.5,\"historical_you_published_a_song\":60.0,\"metadata_update_or_addition\":2.0,\"pending_explanation\":5.0,\"pinned_a_question_not_your_own\":2.0,\"question_downvote\":-2.0,\"question_upvote\":2.0,\"rejected_a_lyrics_edit\":2.0,\"song_metadata_update_or_addition\":2.0,\"song_pageviews_1000\":25.0,\"song_pageviews_10000\":50.0,\"song_pageviews_100000\":125.0,\"song_pageviews_1000000\":500.0,\"song_pageviews_2500\":30.0,\"song_pageviews_25000\":75.0,\"song_pageviews_250000\":150.0,\"song_pageviews_2500000\":1000.0,\"song_pageviews_500\":20.0,\"song_pageviews_5000\":35.0,\"song_pageviews_50000\":100.0,\"song_pageviews_500000\":200.0,\"song_pageviews_5000000\":2000.0,\"suggestion_downvote_by_contrib\":-1.0,\"suggestion_downvote_by_default\":-0.5,\"suggestion_downvote_by_editor\":-1.0,\"suggestion_downvote_by_high_iq_user\":-1.0,\"suggestion_downvote_by_moderator\":-1.0,\"suggestion_upvote_by_contrib\":2.0,\"suggestion_upvote_by_default\":1.0,\"suggestion_upvote_by_editor\":3.0,\"suggestion_upvote_by_high_iq_user\":2.0,\"suggestion_upvote_by_moderator\":4.0,\"verified_explanation_by_meme\":100.0,\"verified_explanation_by_non_meme\":15.0,\"verified_lyrics_by_meme_featured\":50.0,\"verified_lyrics_by_meme_primary\":75.0,\"verified_lyrics_by_meme_writer\":75.0,\"verified_lyrics_by_non_meme_featured\":10.0,\"verified_lyrics_by_non_meme_primary\":15.0,\"verified_lyrics_by_non_meme_writer\":15.0,\"you_accepted_a_comment\":6.0,\"you_accepted_an_annotation\":10.0,\"you_added_a_photo\":100.0,\"you_archived_a_comment\":2.0,\"you_contributed_to_a_marked_complete_song\":10.0,\"you_contributed_to_a_recent_marked_complete_song\":20.0,\"you_deleted_an_annotation\":5.0,\"you_incorporated_an_annotation\":5.0,\"you_integrated_a_comment\":2.0,\"you_linked_an_identity\":100.0,\"you_marked_a_song_complete\":10.0,\"you_merged_an_annotation_edit\":4.0,\"you_published_a_song\":5.0,\"your_annotation_accepted\":10.0,\"your_annotation_edit_merged\":5.0,\"your_annotation_edit_rejected\":-0.5,\"your_annotation_incorporated\":15.0,\"your_annotation_rejected\":0.0,\"your_annotation_was_cosigned_by_community_verified\":2.0,\"your_annotation_was_cosigned_by_meme\":50.0,\"your_annotation_was_cosigned_by_verified_verified\":20.0,\"your_answer_cleared\":-5.0,\"your_answer_pinned\":5.0,\"your_comment_accepted\":2.0,\"your_comment_archived\":0.0,\"your_comment_integrated\":2.0,\"your_comment_rejected\":-0.5,\"you_rejected_a_comment\":2.0,\"you_rejected_an_annotation\":2.0,\"you_rejected_an_annotation_edit\":2.0,\"your_lyrics_edit_accepted\":3.0,\"your_lyrics_edit_rejected\":-2.0,\"your_question_answered\":4.0,\"your_question_archived\":-1.0,\"your_question_pinned\":5.0};\n    </script>\n    \n  <script type=\"text/javascript\">_qevents.push({ qacct: \"p-f3CPQ6vHckedE\"});</script>\n<noscript>\n  <div style=\"display: none;\">\n    <img src=\"http://pixel.quantserve.com/pixel/p-f3CPQ6vHckedE.gif\" height=\"1\" width=\"1\" alt=\"Quantcast\"/>\n  </div>\n</noscript>\n\n  \n\n<script type=\"text/javascript\">\n\n  var _sf_async_config={};\n\n  _sf_async_config.uid = 3877;\n  _sf_async_config.domain = 'genius.com';\n  _sf_async_config.title = 'TTNG – Chinchilla Lyrics | Genius Lyrics';\n  _sf_async_config.sections = 'songs,tag:rock';\n  _sf_async_config.authors = 'TTNG';\n\n  var _cbq = window._cbq || [];\n\n  (function(){\n    function loadChartbeat() {\n      window._sf_endpt=(new Date()).getTime();\n      var e = document.createElement('script');\n      e.setAttribute('language', 'javascript');\n      e.setAttribute('type', 'text/javascript');\n      e.setAttribute('src', 'https://static.chartbeat.com/js/chartbeat.js');\n      document.body.appendChild(e);\n    }\n    var oldonload = window.onload;\n    window.onload = (typeof window.onload != 'function') ?\n       loadChartbeat : function() { oldonload(); loadChartbeat(); };\n  })();\n</script>\n\n  <!-- Begin comScore Tag -->\n<script>\n  var _comscore = _comscore || [];\n  _comscore.push({ c1: \"2\", c2: \"22489583\" });\n  (function() {\n    var s = document.createElement(\"script\"), el = document.getElementsByTagName(\"script\")[0]; s.async = true;\n    s.src = (document.location.protocol == \"https:\" ? \"https://sb\" : \"http://b\") + \".scorecardresearch.com/beacon.js\";\n    el.parentNode.insertBefore(s, el);\n  })();\n</script>\n<noscript>\n  <img src=\"http://b.scorecardresearch.com/p?c1=2&c2=22489583&cv=2.0&cj=1\" />\n</noscript>\n<!-- End comScore Tag -->\n\n  <script>\n  !function(f,b,e,v,n,t,s)\n  {if(f.fbq)return;n=f.fbq=function(){n.callMethod?\n  n.callMethod.apply(n,arguments):n.queue.push(arguments)};\n  if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';\n  n.queue=[];t=b.createElement(e);t.async=!0;\n  t.src=v;s=b.getElementsByTagName(e)[0];\n  s.parentNode.insertBefore(t,s)}(window, document,'script',\n  'https://connect.facebook.net/en_US/fbevents.js');\n  fbq('init', '201983886890479');\n  fbq('track', 'PageView');\n</script>\n<noscript><img height=\"1\" width=\"1\" style=\"display:none\"\n  src=\"https://www.facebook.com/tr?id=201983886890479&ev=PageView&noscript=1\"\n/></noscript>\n\n  \n\n\n  </body>\n</html>\n\n"
  },
  {
    "path": "test/rsrc/lyrics/geniuscom/sample.txt",
    "content": "<!DOCTYPE html>\n<html class=\"snarly apple_music_player--enabled bagon_song_page--enabled song_stories_public_launch--enabled react_forums--disabled\" xmlns=\"http://www.w3.org/1999/xhtml\" xmlns:fb=\"http://www.facebook.com/2008/fbml\" lang=\"en\" xml:lang=\"en\">\n  <head>\n    <base target='_top' href=\"//g-example.com/\">\n\n    <script type=\"text/javascript\">\n    //<![CDATA[\n\n      var _sf_startpt=(new Date()).getTime();\n      if (window.performance && performance.mark) {\n        window.performance.mark('parse_start');\n      }\n\n    //]]>\n    </script>\n\n    <title>SAMPLE – SONG Lyrics | g-example Lyrics</title>\n\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n    <meta content='width=device-width,initial-scale=1' name='viewport'>\n\n    <meta property=\"og:site_name\" content=\"g-example\"/>\n\n    <link title=\"g-example\" type=\"application/opensearchdescription+xml\" rel=\"search\" href=\"https://g-example.com/opensearch.xml\">\n\n    <script async src=\"https://www.youtube.com/iframe_api\"></script>\n    <script defer src=\"https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js\"></script>\n\n      <meta content=\"https://g-example.com/SAMPLE-SONG-lyrics\" property=\"og:url\" />\n      \n      <link href=\"ios-app://#/g-example/songs/#\" rel=\"alternate\" />\n      <meta content=\"/songs/3113595\" name=\"newrelic-resource-path\" />\n      <link href=\"https://g-example.com/SAMPLE-SONG-lyrics\" rel=\"canonical\" />\n      <link href=\"https://g-example.com/amp/SAMPLE-SONG-lyrics\" rel=\"amphtml\" />\n      \n    <script type=\"text/javascript\">\n      var _qevents = _qevents || [];\n      (function() {\n        var elem = document.createElement('script');\n        elem.src = (document.location.protocol == 'https:' ? 'https://secure' : 'http://edge') + '.quantserve.com/quant.js';\n        elem.async = true;\n        elem.type = 'text/javascript';\n        var scpt = document.getElementsByTagName('script')[0];\n        scpt.parentNode.insertBefore(elem, scpt);\n      })();\n    </script>\n\n    <script type=\"text/javascript\">\n      window.ga = window.ga || function() {\n        (window.ga.q = window.ga.q || []).push(arguments);\n      };\n\n      \n        (function(g, e, n, i, u, s) {\n          g['GoogleAnalyticsObject'] = 'ga';\n          g.ga.l = Date.now();\n          u = e.createElement(n);\n          s = e.getElementsByTagName(n)[0];\n          u.async = true;\n          u.src = i;\n          s.parentNode.insertBefore(u, s);\n        })(window, document, 'script', 'https://www.google-analytics.com/analytics.js');\n\n        ga('create', \"UA-10346621-1\", 'auto', {'useAmpClientId': true});\n        ga('set', 'dimension1', \"false\");\n        ga('set', 'dimension2', \"songs#show\");\n        ga('set', 'dimension3', \"r-b\");\n        ga('set', 'dimension4', \"true\");\n        ga('set', 'dimension5', 'false');\n        ga('set', 'dimension6', \"none\");\n        ga('send', 'pageview');\n      \n    </script>\n  </head>\n\n  <body>    \n\n    <div class=\"header\" ng-controller=\"HeaderALBUM as header_ALBUM\" click-outside=\"close_mobile_subnav_menu()\">\n      <div class=\"header-primary active\">\n        <div class=\"header-expand_nav_menu\" ng-click=\"toggle_mobile_subnav_menu()\"><div class=\"header-expand_nav_menu-contents\"></div></div>\n\n        \n          <div class=\"logo_container\">\n            <a href=\"https://g-example.com/\" class=\"logo_link\">g-example</a>\n          </div>\n        \n\n        <header-actions></header-actions>\n\n      <search-form search-style=\"header\"></search-form>\n      </div>\n    </div>\n\n    <routable-page>\n      <ng-non-bindable>\n\n        <div class=\"header_with_cover_art\">\n          <div class=\"header_with_cover_art-inner column_layout\">\n            <div class=\"column_layout-column_span column_layout-column_span--primary\">\n              <div class=\"header_with_cover_art-cover_art \">\n                <div class=\"cover_art\">\n                  <img alt=\"#\" class=\"cover_art-image\" src=\"#\" srcset=\"#\" />\n                </div>\n              </div>\n\n              <div class=\"header_with_cover_art-primary_info_container\">\n                <div class=\"header_with_cover_art-primary_info\">\n                  <h1 class=\"header_with_cover_art-primary_info-title \">SONG</h1>\n                  <h2>\n                    <a href=\"https://g-example.com/artists/SAMPLE\" class=\"header_with_cover_art-primary_info-primary_artist\">\n                      SAMPLE\n                    </a>\n                  </h2>\n                  <h3>\n                    <div class=\"metadata_unit \">\n                      <span class=\"metadata_unit-label\">Produced by</span>\n                      <span class=\"metadata_unit-info\">\n                        <a href=\"https://g-example.com/artists/Person1\">Person 1</a> & <a href=\"https://g-example.com/artists/Person 2\">Person 2</a>\n                      </span>\n                    </div>\n                  </h3>\n                  <h3>\n                    <div class=\"metadata_unit \">\n                      <span class=\"metadata_unit-label\">Album</span>\n                      <span class=\"metadata_unit-info\"><a href=\"https://g-example.com/albums/SAMPLE/ALBUM\">ALBUM</a></span>\n                    </div>\n                  </h3>\n                  </div>\n              </div>\n\n            </div>\n          </div>\n        </div>\n\n        <div class=\"song_body column_layout\" initial-content-for=\"song_body\">\n          <div class=\"column_layout-column_span column_layout-column_span--primary\">\n            <div class=\"song_body-lyrics\">\n                <h2 class=\"text_label text_label--gray text_label--x_small_text_size u-top_margin\">SONG Lyrics</h2>\n              <div initial-content-for=\"lyrics\">\n                <div class=\"totally-not-the-lyrics-div\">\n                  !!!! MISSING LYRICS HERE !!!\n                </div>\n              </div>\n              <div initial-content-for=\"recirculated_content\">\n                  <div class=\"u-xx_large_vertical_margins\">\n                    <div class=\"text_label text_label--gray\">More on g-example</div>\n                  </div>\n              </div>\n            </div>\n          </div>\n        </div>         \n        \n        <div class=\"metadata_unit metadata_unit--table_row\">\n          <span class=\"metadata_unit-label\">Released by</span>\n\n          <span class=\"metadata_unit-info\">\n            <a href=\"https://g-example.com/artists/records\">Records</a> & <a href=\"https://g-example.com/artists/Top\">Top</a>\n          </span>\n        </div>\n        \n        <div class=\"metadata_unit metadata_unit--table_row\">\n          <span class=\"metadata_unit-label\">Mixing</span>\n          <span class=\"metadata_unit-info\">\n            <a href=\"https://g-example.com/artists/Mixed-by-person\">Mixed by Person</a>\n          </span>\n        </div>\n\n        <div class=\"metadata_unit metadata_unit--table_row\">\n          <span class=\"metadata_unit-label\">Recorded At</span>\n          <span class=\"metadata_unit-info metadata_unit-info--text_only\">City, Place</span>\n        </div>\n\n        <div class=\"metadata_unit metadata_unit--table_row\">\n          <span class=\"metadata_unit-label\">Release Date</span>\n          <span class=\"metadata_unit-info metadata_unit-info--text_only\">Feb 30, 1290</span>\n        </div>\n            \n        <div class=\"metadata_unit metadata_unit--table_row\">\n          <span class=\"metadata_unit-label\">Interpolated By</span>\n          <span class=\"metadata_unit-info\">\n            \n            <div class=\"u-x_small_bottom_margin\">\n              <a href=\"#\"> # </a>\n            </div>\n            \n          </span>\n        </div>\n\n        <div initial-content-for=\"album\">\n          <div class=\"u-xx_large_vertical_margins\">\n            <div class=\"song_album u-bottom_margin\">\n              <a href=\"https://g-example.com/albums/SAMPLE/ALBUM\" class=\"song_album-album_art\" title=\"ALBUM\">\n                <img alt=\"#\" src=\"#\" srcset=\"#\"/>\n              </a>\n            <div class=\"song_album-info\">\n              <a href=\"https://g-example.com/albums/SAMPLE/ALBUM\" title=\"ALBUM\" class=\"song_album-info-title\">\n                ALBUM\n              </a>\n              <a href=\"https://g-example.com/artists/SAMPLE\" class=\"song_album-info-artist\" title=\"ALBUM\">SAMPLE</a>\n            </div>\n          </div>\n\n      </ng-non-bindable>\n    </routable-page>\n\n    <div class=\"page_footer page_footer--padding-for-sticky-player\">\n      <div class=\"footer\">\n        <div>\n          <a href=\"/about\">About g-example</a>\n          <a href=\"/contributor_guidelines\">Contributor Guidelines</a> \n        </div>\n\n        <div>\n            <span>g-example</span>\n        </div>\n      </div>\n    </div>\n\n    <script type=\"text/javascript\">_qevents.push({ qacct: \"################\"});</script>\n    <noscript>\n      <div style=\"display: none;\">\n        <img src=\"#\" height=\"1\" width=\"1\" alt=\"#\"/>\n      </div>\n    </noscript>\n\n    <script type=\"text/javascript\">\n      var _sf_async_config={};\n\n      _sf_async_config.uid = 3877;\n      _sf_async_config.domain = 'g-example.com';\n      _sf_async_config.title = 'SAMPLE – SONG Lyrics | g-example Lyrics';\n      _sf_async_config.sections = 'songs,tag:r-b';\n      _sf_async_config.authors = 'SAMPLE';\n\n      var _cbq = window._cbq || [];\n\n      (function(){\n        function loadChartbeat() {\n          window._sf_endpt=(new Date()).getTime();\n          var e = document.createElement('script');\n          e.setAttribute('language', 'javascript');\n          e.setAttribute('type', 'text/javascript');\n          e.setAttribute('src', '#');\n          document.body.appendChild(e);\n        }\n        var oldonload = window.onload;\n        window.onload = (typeof window.onload != 'function') ?\n          loadChartbeat : function() { oldonload(); loadChartbeat(); };\n      })();\n    </script>\n\n      <!-- Begin comScore Tag -->\n    <script>\n      var _comscore = _comscore || [];\n      _comscore.push({ c1: \"2\", c2: \"17151659\" });\n      (function() {\n        var s = document.createElement(\"script\"), el = document.getElementsByTagName(\"script\")[0]; s.async = true;\n        s.src = (document.location.protocol == \"https:\" ? \"https://sb\" : \"http://b\") + \".scorecardresearch.com/beacon.js\";\n        el.parentNode.insertBefore(s, el);\n      })();\n    </script>\n    <noscript>\n      <img src=\"#\"/>\n    </noscript>\n    <!-- End comScore Tag -->\n    <noscript>\n      <img height=\"1\" width=\"1\" style=\"display:none\" src=\"#\"/> \n    </noscript>\n  </body>\n</html>\n"
  },
  {
    "path": "test/rsrc/lyrics/tekstowopl/piosenka24kgoldncityofangels1.txt",
    "content": "<!DOCTYPE html>\r\n<html lang=\"pl\" prefix=\"og: http://ogp.me/ns#\" itemscope=\"\" itemtype=\"http://schema.org/Article\" slick-uniqueid=\"3\"><head>\r\n\t<!-- Required meta tags -->\r\n\t<meta charset=\"utf-8\">\r\n\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\r\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\r\n\r\n\t                \t<title>24kGoldn - City Of Angels - tekst i tłumaczenie piosenki na Tekstowo.pl</title>\r\n\t<meta name=\"Description\" content=\"24kGoldn - City Of Angels - tekst piosenki, tłumaczenie piosenki i teledysk. Zobacz słowa utworu City Of Angels wraz z teledyskiem i tłumaczeniem.\">\r\n\t<meta name=\"Keywords\" content=\"City Of Angels, 24kGoldn, tekst, słowa, tłumaczenie, tekst piosenki, słowa piosenki, tłumaczenie piosenki, teledysk\">\r\n\t<meta name=\"revisit-after\" content=\"12 hours\">\r\n\t<meta http-equiv=\"Content-language\" content=\"pl\">\r\n\t<meta name=\"robots\" content=\"INDEX, FOLLOW\">\r\n\t<link rel=\"manifest\" href=\"/manifest.webmanifest\">\r\n        <link rel=\"search\" type=\"application/opensearchdescription+xml\" title=\"Tekstowo: Po tytule piosenki\" href=\"https://www.tekstowo.pl/piosenki_osd.xml\">\r\n        <link rel=\"search\" type=\"application/opensearchdescription+xml\" title=\"Tekstowo: Po tytule soundtracka\" href=\"https://www.tekstowo.pl/soundtracki_osd.xml\">\r\n\r\n\t\t<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin=\"\">\t\r\n\t<link rel=\"preload\" as=\"style\" href=\"https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700;900&amp;display=swap\">\r\n\t<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700;900&amp;display=swap\" media=\"all\" onload=\"this.media = 'all'\">\r\n\r\n\t\t<link rel=\"preconnect\" href=\"https://cdn.jsdelivr.net\" crossorigin=\"\">\r\n\t<link rel=\"preconnect\" href=\"https://ssl.google-analytics.com\" crossorigin=\"\">\r\n\t\t    <link rel=\"preconnect\" href=\"https://ls.hit.gemius.pl\" crossorigin=\"\">\r\n\t    <link rel=\"preconnect\" href=\"https://get.optad360.io\" crossorigin=\"\">\r\n\t    <link rel=\"preconnect\" href=\"https://www.google.com\" crossorigin=\"\">\r\n\t    <link rel=\"preconnect\" href=\"https://adservice.google.com\" crossorigin=\"\">\r\n\t    <link rel=\"preconnect\" href=\"https://tpc.googlesyndication.com\" crossorigin=\"\">\r\n\t    <link rel=\"preconnect\" href=\"https://connect.facebook.net\" crossorigin=\"\">\r\n\t\r\n\r\n\t\t<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/v2/css/glyphs.css?v=201216\" media=\"all\" onload=\"this.media = 'all'\">\r\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/v2/css/main.css?v=220210\">\r\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/v2/css/media/main-print.css?v=220210\" media=\"print\">\r\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/v2/css/media/main-md.css?v=220210\" media=\"screen and (min-width: 768px)\">\r\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/v2/css/media/main-lg.css?v=220210\" media=\"screen and (min-width: 992px)\">\r\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/v2/css/media/main-xl.css?v=220210\" media=\"screen and (min-width: 1200px)\">\r\n\t<!-- generics -->\r\n\t<link rel=\"icon\" href=\"https://www.tekstowo.pl/static/v2/images/favicons/icon-32.png\" sizes=\"32x32\">\r\n\t<link rel=\"icon\" href=\"https://www.tekstowo.pl/static/v2/images/favicons/icon-128.png\" sizes=\"128x128\">\r\n\t<link rel=\"icon\" href=\"https://www.tekstowo.pl/static/v2/images/favicons/icon-192.png\" sizes=\"192x192\">\r\n\r\n\t<!-- Android -->\r\n\t<link rel=\"shortcut icon\" href=\"https://www.tekstowo.pl/static/v2/images/favicons/icon-196.png\" sizes=\"196x196\">\r\n\r\n\t<!-- iOS -->\r\n\t<link rel=\"apple-touch-icon\" href=\"https://www.tekstowo.pl/static/v2/images/favicons/icon-152.png\" sizes=\"152x152\">\r\n\t<link rel=\"apple-touch-icon\" href=\"https://www.tekstowo.pl/static/v2/images/favicons/icon-167.png\" sizes=\"167x167\">\r\n\t<link rel=\"apple-touch-icon\" href=\"https://www.tekstowo.pl/static/v2/images/favicons/icon-180.png\" sizes=\"180x180\">\r\n\r\n\t<meta name=\"msapplication-config\" content=\"/browserconfig.xml\">\r\n\r\n\t\t    <link rel=\"canonical\" href=\"https://www.tekstowo.pl/piosenka,24kgoldn,city_of_angels_1.html\">\r\n\t    <meta property=\"og:title\" content=\"24kGoldn - City Of Angels - tekst piosenki na Tekstowo.pl\">\r\n\t    \t\t<meta property=\"og:image\" content=\"https://www.tekstowo.pl/miniatura_teledysku,yHwGIA4VeOc.jpg\">\r\n\t    \t    <meta property=\"og:url\" content=\"https://www.tekstowo.pl/piosenka,24kgoldn,city_of_angels_1.html\">\r\n\t    <meta property=\"og:type\" content=\"website\">\r\n\t    <meta name=\"twitter:card\" content=\"summary_large_image\">\r\n\t    <meta name=\"twitter:title\" content=\"24kGoldn - City Of Angels\">\r\n\t    \t\t<meta name=\"twitter:image\" content=\"https://www.tekstowo.pl/miniatura_teledysku,yHwGIA4VeOc.jpg\">\r\n\t    \t    \r\n\t    \t\t\t\t    \t\t\t\t    \t\t\t    \r\n\t    <meta name=\"twitter:description\" content=\"24kGoldn - City Of Angels - tekst piosenki, tłumaczenie piosenki i teledysk. Zobacz słowa utworu City Of Angels wraz z teledyskiem i tłumaczeniem.\">\r\n\t    <meta property=\"og:description\" content=\"24kGoldn - City Of Angels - tekst piosenki, tłumaczenie piosenki i teledysk. Zobacz słowa utworu City Of Angels wraz z teledyskiem i tłumaczeniem.\">\r\n\t    \t\t<meta itemprop=\"name\" content=\"24kGoldn - City Of Angels - tekst piosenki na Tekstowo.pl\">\r\n\t\t<meta itemprop=\"description\" content=\"24kGoldn - City Of Angels - tekst piosenki, tłumaczenie piosenki i teledysk. Zobacz słowa utworu City Of Angels wraz z teledyskiem i tłumaczeniem.\">\r\n\t\t\t\t    <meta itemprop=\"image\" content=\"https://www.tekstowo.pl/miniatura_teledysku,yHwGIA4VeOc.jpg\">\r\n\t\t\t    \t\r\n    <meta property=\"og:site_name\" content=\"Tekstowo.pl\">\r\n    <meta property=\"fb:app_id\" content=\"131858753537922\">\r\n\r\n\r\n    <script src=\"https://connect.facebook.net/pl_PL/sdk.js?hash=e8ef6558bd8d6379e75570b9a385366f\" async=\"\" crossorigin=\"anonymous\"></script><script type=\"text/javascript\" async=\"\" src=\"https://ssl.google-analytics.com/ga.js\"></script><script type=\"text/javascript\">\r\n\t\t\t    var ytProxy = '//filmiki4.maxart.pl/ytp.php';\r\n\t    </script>\r\n    <script>\r\n\t\t\r\n\t    window.addEvent = function (event, fn) {\r\n\t\tif (window.asyncEventHoler == undefined)\r\n\t\t{\r\n\t\t    window.asyncEventHoler = [];\r\n\t\t}\r\n\r\n\t\twindow.asyncEventHoler.push({\r\n\t\t    'event': event,\r\n\t\t    'fn': fn\r\n\t\t});\r\n\t    }\r\n\t\r\n    </script>\r\n\t<script async=\"\" src=\"//cmp.optad360.io/items/6a750fad-191a-4309-bcd6-81e330cb392d.min.js\"></script>  \r\n\t\r\n<style type=\"text/css\" data-fbcssmodules=\"css:fb.css.base css:fb.css.dialog css:fb.css.iframewidget css:fb.css.customer_chat_plugin_iframe\">.fb_hidden{position:absolute;top:-10000px;z-index:10001}.fb_reposition{overflow:hidden;position:relative}.fb_invisible{display:none}.fb_reset{background:none;border:0;border-spacing:0;color:#000;cursor:auto;direction:ltr;font-family:'lucida grande', tahoma, verdana, arial, sans-serif;font-size:11px;font-style:normal;font-variant:normal;font-weight:normal;letter-spacing:normal;line-height:1;margin:0;overflow:visible;padding:0;text-align:left;text-decoration:none;text-indent:0;text-shadow:none;text-transform:none;visibility:visible;white-space:normal;word-spacing:normal}.fb_reset>div{overflow:hidden}@keyframes fb_transform{from{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.fb_animate{animation:fb_transform .3s forwards}\r\n.fb_hidden{position:absolute;top:-10000px;z-index:10001}.fb_reposition{overflow:hidden;position:relative}.fb_invisible{display:none}.fb_reset{background:none;border:0;border-spacing:0;color:#000;cursor:auto;direction:ltr;font-family:'lucida grande', tahoma, verdana, arial, sans-serif;font-size:11px;font-style:normal;font-variant:normal;font-weight:normal;letter-spacing:normal;line-height:1;margin:0;overflow:visible;padding:0;text-align:left;text-decoration:none;text-indent:0;text-shadow:none;text-transform:none;visibility:visible;white-space:normal;word-spacing:normal}.fb_reset>div{overflow:hidden}@keyframes fb_transform{from{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.fb_animate{animation:fb_transform .3s forwards}\r\n.fb_dialog{background:rgba(82, 82, 82, .7);position:absolute;top:-10000px;z-index:10001}.fb_dialog_advanced{border-radius:8px;padding:10px}.fb_dialog_content{background:#fff;color:#373737}.fb_dialog_close_icon{background:url(https://connect.facebook.net/rsrc.php/v3/yq/r/IE9JII6Z1Ys.png) no-repeat scroll 0 0 transparent;cursor:pointer;display:block;height:15px;position:absolute;right:18px;top:17px;width:15px}.fb_dialog_mobile .fb_dialog_close_icon{left:5px;right:auto;top:5px}.fb_dialog_padding{background-color:transparent;position:absolute;width:1px;z-index:-1}.fb_dialog_close_icon:hover{background:url(https://connect.facebook.net/rsrc.php/v3/yq/r/IE9JII6Z1Ys.png) no-repeat scroll 0 -15px transparent}.fb_dialog_close_icon:active{background:url(https://connect.facebook.net/rsrc.php/v3/yq/r/IE9JII6Z1Ys.png) no-repeat scroll 0 -30px transparent}.fb_dialog_iframe{line-height:0}.fb_dialog_content .dialog_title{background:#6d84b4;border:1px solid #365899;color:#fff;font-size:14px;font-weight:bold;margin:0}.fb_dialog_content .dialog_title>span{background:url(https://connect.facebook.net/rsrc.php/v3/yd/r/Cou7n-nqK52.gif) no-repeat 5px 50%;float:left;padding:5px 0 7px 26px}body.fb_hidden{height:100%;left:0;margin:0;overflow:visible;position:absolute;top:-10000px;transform:none;width:100%}.fb_dialog.fb_dialog_mobile.loading{background:url(https://connect.facebook.net/rsrc.php/v3/ya/r/3rhSv5V8j3o.gif) white no-repeat 50% 50%;min-height:100%;min-width:100%;overflow:hidden;position:absolute;top:0;z-index:10001}.fb_dialog.fb_dialog_mobile.loading.centered{background:none;height:auto;min-height:initial;min-width:initial;width:auto}.fb_dialog.fb_dialog_mobile.loading.centered #fb_dialog_loader_spinner{width:100%}.fb_dialog.fb_dialog_mobile.loading.centered .fb_dialog_content{background:none}.loading.centered #fb_dialog_loader_close{clear:both;color:#fff;display:block;font-size:18px;padding-top:20px}#fb-root #fb_dialog_ipad_overlay{background:rgba(0, 0, 0, .4);bottom:0;left:0;min-height:100%;position:absolute;right:0;top:0;width:100%;z-index:10000}#fb-root #fb_dialog_ipad_overlay.hidden{display:none}.fb_dialog.fb_dialog_mobile.loading iframe{visibility:hidden}.fb_dialog_mobile .fb_dialog_iframe{position:sticky;top:0}.fb_dialog_content .dialog_header{background:linear-gradient(from(#738aba), to(#2c4987));border-bottom:1px solid;border-color:#043b87;box-shadow:white 0 1px 1px -1px inset;color:#fff;font:bold 14px Helvetica, sans-serif;text-overflow:ellipsis;text-shadow:rgba(0, 30, 84, .296875) 0 -1px 0;vertical-align:middle;white-space:nowrap}.fb_dialog_content .dialog_header table{height:43px;width:100%}.fb_dialog_content .dialog_header td.header_left{font-size:12px;padding-left:5px;vertical-align:middle;width:60px}.fb_dialog_content .dialog_header td.header_right{font-size:12px;padding-right:5px;vertical-align:middle;width:60px}.fb_dialog_content .touchable_button{background:linear-gradient(from(#4267B2), to(#2a4887));background-clip:padding-box;border:1px solid #29487d;border-radius:3px;display:inline-block;line-height:18px;margin-top:3px;max-width:85px;padding:4px 12px;position:relative}.fb_dialog_content .dialog_header .touchable_button input{background:none;border:none;color:#fff;font:bold 12px Helvetica, sans-serif;margin:2px -12px;padding:2px 6px 3px 6px;text-shadow:rgba(0, 30, 84, .296875) 0 -1px 0}.fb_dialog_content .dialog_header .header_center{color:#fff;font-size:16px;font-weight:bold;line-height:18px;text-align:center;vertical-align:middle}.fb_dialog_content .dialog_content{background:url(https://connect.facebook.net/rsrc.php/v3/y9/r/jKEcVPZFk-2.gif) no-repeat 50% 50%;border:1px solid #4a4a4a;border-bottom:0;border-top:0;height:150px}.fb_dialog_content .dialog_footer{background:#f5f6f7;border:1px solid #4a4a4a;border-top-color:#ccc;height:40px}#fb_dialog_loader_close{float:left}.fb_dialog.fb_dialog_mobile .fb_dialog_close_icon{visibility:hidden}#fb_dialog_loader_spinner{animation:rotateSpinner 1.2s linear infinite;background-color:transparent;background-image:url(https://connect.facebook.net/rsrc.php/v3/yD/r/t-wz8gw1xG1.png);background-position:50% 50%;background-repeat:no-repeat;height:24px;width:24px}@keyframes rotateSpinner{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}\r\n.fb_iframe_widget{display:inline-block;position:relative}.fb_iframe_widget span{display:inline-block;position:relative;text-align:justify}.fb_iframe_widget iframe{position:absolute}.fb_iframe_widget_fluid_desktop,.fb_iframe_widget_fluid_desktop span,.fb_iframe_widget_fluid_desktop iframe{max-width:100%}.fb_iframe_widget_fluid_desktop iframe{min-width:220px;position:relative}.fb_iframe_widget_lift{z-index:1}.fb_iframe_widget_fluid{display:inline}.fb_iframe_widget_fluid span{width:100%}\r\n.fb_mpn_mobile_landing_page_slide_out{animation-duration:200ms;animation-name:fb_mpn_landing_page_slide_out;transition-timing-function:ease-in}.fb_mpn_mobile_landing_page_slide_out_from_left{animation-duration:200ms;animation-name:fb_mpn_landing_page_slide_out_from_left;transition-timing-function:ease-in}.fb_mpn_mobile_landing_page_slide_up{animation-duration:500ms;animation-name:fb_mpn_landing_page_slide_up;transition-timing-function:ease-in}.fb_mpn_mobile_bounce_in{animation-duration:300ms;animation-name:fb_mpn_bounce_in;transition-timing-function:ease-in}.fb_mpn_mobile_bounce_out{animation-duration:300ms;animation-name:fb_mpn_bounce_out;transition-timing-function:ease-in}.fb_mpn_mobile_bounce_out_v2{animation-duration:300ms;animation-name:fb_mpn_fade_out;transition-timing-function:ease-in}.fb_customer_chat_bounce_in_v2{animation-duration:300ms;animation-name:fb_bounce_in_v2;transition-timing-function:ease-in}.fb_customer_chat_bounce_in_from_left{animation-duration:300ms;animation-name:fb_bounce_in_from_left;transition-timing-function:ease-in}.fb_customer_chat_bounce_out_v2{animation-duration:300ms;animation-name:fb_bounce_out_v2;transition-timing-function:ease-in}.fb_customer_chat_bounce_out_from_left{animation-duration:300ms;animation-name:fb_bounce_out_from_left;transition-timing-function:ease-in}.fb_invisible_flow{display:inherit;height:0;overflow-x:hidden;width:0}@keyframes fb_mpn_landing_page_slide_out{0%{margin:0 12px;width:100% - 24px}60%{border-radius:18px}100%{border-radius:50%;margin:0 24px;width:60px}}@keyframes fb_mpn_landing_page_slide_out_from_left{0%{left:12px;width:100% - 24px}60%{border-radius:18px}100%{border-radius:50%;left:12px;width:60px}}@keyframes fb_mpn_landing_page_slide_up{0%{bottom:0;opacity:0}100%{bottom:24px;opacity:1}}@keyframes fb_mpn_bounce_in{0%{opacity:.5;top:100%}100%{opacity:1;top:0}}@keyframes fb_mpn_fade_out{0%{bottom:30px;opacity:1}100%{bottom:0;opacity:0}}@keyframes fb_mpn_bounce_out{0%{opacity:1;top:0}100%{opacity:.5;top:100%}}@keyframes fb_bounce_in_v2{0%{opacity:0;transform:scale(0, 0);transform-origin:bottom right}50%{transform:scale(1.03, 1.03);transform-origin:bottom right}100%{opacity:1;transform:scale(1, 1);transform-origin:bottom right}}@keyframes fb_bounce_in_from_left{0%{opacity:0;transform:scale(0, 0);transform-origin:bottom left}50%{transform:scale(1.03, 1.03);transform-origin:bottom left}100%{opacity:1;transform:scale(1, 1);transform-origin:bottom left}}@keyframes fb_bounce_out_v2{0%{opacity:1;transform:scale(1, 1);transform-origin:bottom right}100%{opacity:0;transform:scale(0, 0);transform-origin:bottom right}}@keyframes fb_bounce_out_from_left{0%{opacity:1;transform:scale(1, 1);transform-origin:bottom left}100%{opacity:0;transform:scale(0, 0);transform-origin:bottom left}}@keyframes slideInFromBottom{0%{opacity:.1;transform:translateY(100%)}100%{opacity:1;transform:translateY(0)}}@keyframes slideInFromBottomDelay{0%{opacity:0;transform:translateY(100%)}97%{opacity:0;transform:translateY(100%)}100%{opacity:1;transform:translateY(0)}}</style></head>\r\n<body class=\"\">\r\n    \r\n\r\n    <nav class=\"navbar navbar-expand-lg navbar-dark sticky-top d-md-none\" id=\"navbar\">\r\n\t<div class=\"container\" id=\"navbarMobile\">\r\n\t    <a href=\"/\" class=\"navbar-brand logo\">\r\n\t\t\t\t<img src=\"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20441.8%20136%22%3E%3Cpath%20fill%3D%22%23FFF%22%20d%3D%22M130.7%2046v9h6.5v6.2h-6.5v20.6c0%201.4.3%202.4.8%203.1.6.6%201.6.9%203%20.9%201%200%201.9-.1%202.9-.3v6.4c-1.9.5-3.7.8-5.5.8-6.4%200-9.6-3.5-9.6-10.6v-21h-6.1V55h6.1v-9h8.4zM156.3%2092.7c-5.3%200-9.5-1.7-12.8-5s-4.9-7.7-4.9-13.2v-1c0-3.7.7-7%202.1-9.9%201.4-2.9%203.4-5.2%206-6.8s5.4-2.4%208.6-2.4c5%200%208.9%201.6%2011.7%204.8%202.7%203.2%204.1%207.8%204.1%2013.6v3.3H147c.3%203.1%201.3%205.5%203.1%207.2%201.8%201.8%204%202.7%206.8%202.7%203.8%200%206.9-1.5%209.3-4.6l4.5%204.3c-1.5%202.2-3.5%203.9-5.9%205.1-2.6%201.3-5.4%201.9-8.5%201.9zm-1-31.7c-2.3%200-4.1.8-5.5%202.4-1.4%201.6-2.3%203.8-2.7%206.7h15.8v-.6c-.2-2.8-.9-4.9-2.2-6.3-1.3-1.5-3.1-2.2-5.4-2.2zM186.1%2076.1l-3.7%203.8V92h-8.3V39.5h8.3v30.3l2.6-3.2L195.2%2055h10l-13.7%2015.4L206.7%2092h-9.6l-11-15.9z%22%2F%3E%3Cpath%20fill%3D%22%23FFF%22%20d%3D%22M227.9%2082c0-1.5-.6-2.6-1.8-3.4s-3.2-1.5-6.1-2.1c-2.8-.6-5.2-1.3-7.1-2.3-4.1-2-6.2-4.9-6.2-8.7%200-3.2%201.3-5.9%204-8s6.1-3.2%2010.3-3.2c4.4%200%208%201.1%2010.7%203.3s4.1%205%204.1%208.5h-8.3c0-1.6-.6-2.9-1.8-4s-2.8-1.6-4.7-1.6c-1.8%200-3.3.4-4.5%201.3-1.2.8-1.7%202-1.7%203.4%200%201.3.5%202.3%201.6%203s3.2%201.4%206.5%202.1%205.8%201.6%207.7%202.6%203.2%202.2%204.1%203.6c.9%201.4%201.3%203.1%201.3%205.1%200%203.3-1.4%206-4.1%208.1-2.8%202.1-6.4%203.1-10.8%203.1-3%200-5.7-.5-8.1-1.6-2.4-1.1-4.2-2.6-5.5-4.5s-2-4-2-6.2h8.1c.1%202%20.9%203.5%202.2%204.5%201.4%201.1%203.2%201.6%205.4%201.6s3.9-.4%205-1.2c1.1-1%201.7-2.1%201.7-3.4zM250.2%2046v9h6.5v6.2h-6.5v20.6c0%201.4.3%202.4.8%203.1.6.6%201.6.9%203%20.9%201%200%201.9-.1%202.9-.3v6.4c-1.9.5-3.7.8-5.5.8-6.4%200-9.6-3.5-9.6-10.6v-21h-6.1V55h6.1v-9h8.4zM257%2073.2c0-3.6.7-6.9%202.2-9.8s3.5-5.1%206-6.7c2.6-1.6%205.6-2.3%209-2.3%205%200%209%201.6%2012.2%204.8%203.1%203.2%204.8%207.5%205%2012.8v1.9c0%203.6-.7%206.9-2.1%209.8s-3.4%205.1-6%206.7-5.6%202.4-9%202.4c-5.2%200-9.4-1.7-12.5-5.2s-4.7-8.1-4.7-13.9v-.5h-.1zm8.3.7c0%203.8.8%206.8%202.4%208.9%201.6%202.2%203.8%203.2%206.6%203.2s5-1.1%206.5-3.3c1.6-2.2%202.3-5.4%202.3-9.6%200-3.7-.8-6.7-2.4-8.9-1.6-2.2-3.8-3.3-6.5-3.3s-4.9%201.1-6.5%203.2c-1.6%202.3-2.4%205.5-2.4%209.8zM327.4%2080.5l5.9-25.5h8.1l-10.1%2037h-6.8l-7.9-25.4-7.9%2025.4h-6.8l-10.1-37h8.1l6%2025.3%207.6-25.3h6.3l7.6%2025.5zM341.8%2073.2c0-3.6.7-6.9%202.2-9.8s3.5-5.1%206-6.7c2.6-1.6%205.6-2.3%209-2.3%205%200%209%201.6%2012.2%204.8%203.1%203.2%204.8%207.5%205%2012.8v1.9c0%203.6-.7%206.9-2.1%209.8s-3.4%205.1-6%206.7-5.6%202.4-9%202.4c-5.2%200-9.4-1.7-12.5-5.2s-4.7-8.1-4.7-13.9v-.5h-.1zm8.3.7c0%203.8.8%206.8%202.4%208.9%201.6%202.2%203.8%203.2%206.6%203.2s5-1.1%206.5-3.3c1.6-2.2%202.3-5.4%202.3-9.6%200-3.7-.8-6.7-2.4-8.9-1.6-2.2-3.8-3.3-6.5-3.3s-4.9%201.1-6.5%203.2c-1.6%202.3-2.4%205.5-2.4%209.8zM384.9%2083.3c1.5%200%202.7.4%203.6%201.3.8.9%201.3%202%201.3%203.3s-.4%202.4-1.3%203.2c-.8.9-2%201.3-3.6%201.3-1.5%200-2.7-.4-3.5-1.3-.9-.8-1.3-1.9-1.3-3.2%200-1.3.4-2.4%201.3-3.3.8-.9%202-1.3%203.5-1.3zM428.2%2073.9c0%205.7-1.3%2010.3-3.9%2013.7s-6.1%205.1-10.5%205.1c-4.1%200-7.3-1.3-9.7-4v17.5h-8.3V55h7.7l.3%203.8c2.4-3%205.8-4.4%209.9-4.4%204.5%200%208%201.7%2010.6%205%202.6%203.4%203.8%208%203.8%2014v.5h.1zm-8.3-.7c0-3.7-.7-6.6-2.2-8.8-1.5-2.2-3.6-3.2-6.3-3.2-3.4%200-5.8%201.4-7.3%204.2v16.4c1.5%202.9%204%204.3%207.4%204.3%202.6%200%204.7-1.1%206.2-3.2%201.5-2.2%202.2-5.4%202.2-9.7zM440.5%2092h-8.3V39.5h8.3V92zM123.5%20107.6l-.5%202.8h2.1l-.3%201.5h-2.1l-1.2%207.2v.5c0%20.6.4.8.9.8.2%200%20.6%200%20.9-.1l-.2%201.6c-.5.1-.9.2-1.4.2-.8%200-1.4-.3-1.7-.9-.4-.6-.5-1.3-.5-2.2l1.2-7.2h-2.1l.3-1.5h2.1l.5-2.8%202%20.1zM129.3%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2.7.8%201.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.6.8-2.6.8zm1.3-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1.1-.8-1.8-.8zM141.8%20112.1l-.9-.1c-1.2%200-2.2.6-2.9%201.7l-1.4%208.3h-1.9l2-11.6h1.9l-.3%201.4c.8-1.1%201.8-1.6%202.9-1.6.2%200%20.5.1.9.2l-.3%201.7zM147.5%20122c-.1-.2-.1-.4-.1-.6v-.6c-1.1%201-2.2%201.5-3.4%201.4-1%200-1.9-.3-2.5-1-.6-.6-.9-1.4-.9-2.4.1-1.2.6-2.2%201.6-2.9%201-.7%202.3-1%204-1h2l.1-.9c.1-.7-.1-1.2-.4-1.6-.3-.4-.9-.6-1.5-.6-.7%200-1.3.1-1.8.5-.5.3-.8.8-.9%201.3h-2c.1-.7.3-1.3.8-1.8s1.1-.9%201.8-1.2c.7-.3%201.5-.4%202.3-.4%201.2%200%202.2.4%202.8%201.1.7.7%201%201.6.8%202.8l-.9%205.8-.1.8c0%20.4%200%20.8.1%201.2v.2h-1.8v-.1zm-3-1.5c.6%200%201.2-.1%201.8-.4s1-.7%201.4-1.3l.4-2.4h-1.5c-1.2%200-2.2.2-2.8.6-.7.4-1%20.9-1.1%201.6-.1.5.1%201%20.4%201.4.3.3.8.5%201.4.5zM153.2%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM168.8%20110.4l-.2%201.3c1-1%202.2-1.5%203.5-1.5.7%200%201.4.2%201.9.5s.9.8%201.1%201.4c1.1-1.3%202.4-1.9%203.9-1.9%201.2%200%202%20.4%202.6%201.2.6.8.8%201.8.6%203l-1.3%207.6H179l1.3-7.7v-1c-.1-1-.8-1.5-1.9-1.5-.7%200-1.4.2-1.9.7-.6.5-.9%201.1-1.1%201.8l-1.3%207.7h-2l1.3-7.6c.1-.8%200-1.4-.4-1.8s-.9-.7-1.6-.7c-1.2%200-2.2.5-2.9%201.7l-1.5%208.5h-1.9l2-11.6%201.7-.1zM189.1%20110.2c1%200%201.8.3%202.5.8s1.2%201.2%201.5%202.1.4%201.9.3%203v.2c-.1%201.1-.5%202.2-1%203.1s-1.2%201.6-2.1%202.1c-.9.5-1.8.7-2.8.7s-1.8-.3-2.5-.8-1.2-1.2-1.5-2.1-.4-1.9-.3-2.9c.1-1.2.4-2.3%201-3.2.5-1%201.2-1.7%202.1-2.2.8-.6%201.8-.9%202.8-.8zm-4%206.2c-.1.5-.1.9%200%201.4.1.8.3%201.5.8%202%20.4.5%201%20.8%201.7.8.6%200%201.2-.1%201.8-.5.5-.3%201-.9%201.4-1.5.4-.7.6-1.5.7-2.3.1-.7.1-1.2%200-1.7-.1-.9-.3-1.6-.8-2.1-.4-.5-1-.8-1.7-.8-1%200-1.9.4-2.6%201.2-.7.8-1.1%201.9-1.3%203.2v.3zM195.8%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zm3.2-13c0-.3.1-.6.3-.8.2-.2.5-.3.8-.4.3%200%20.6.1.8.3.2.2.3.5.3.8s-.1.6-.3.8c-.2.2-.5.3-.8.3s-.6-.1-.8-.3c-.2-.1-.3-.4-.3-.7zM208.1%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2.7.8%201.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1-.8-1.8-.8zM220.6%20118.9c.1-.8-.3-1.4-1.3-1.7l-2-.6c-1.7-.6-2.5-1.6-2.5-2.9%200-1%20.5-1.9%201.4-2.5.9-.7%201.9-1%203.1-1%201.2%200%202.1.4%202.9%201%20.7.7%201.1%201.5%201.1%202.6h-1.9c0-.6-.2-1.1-.5-1.4s-.9-.6-1.5-.6c-.7%200-1.3.2-1.7.5-.5.3-.7.7-.8%201.3-.1.7.3%201.2%201.2%201.5l1%20.3c1.3.3%202.3.8%202.8%201.3s.8%201.2.8%202.1c0%20.7-.3%201.4-.7%201.9s-1%20.9-1.7%201.2c-.7.3-1.5.4-2.3.4-1.2%200-2.2-.4-3.1-1.1-.8-.7-1.2-1.6-1.2-2.7h1.9c0%20.7.2%201.2.6%201.6.4.4%201%20.6%201.7.6s1.3-.1%201.8-.4c.5-.5.8-.9.9-1.4zM225.5%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM241.3%20110.4l-2.2%2013c-.1%201.1-.5%201.9-1.1%202.5s-1.4.9-2.3.8c-.4%200-.8-.1-1.3-.2l.2-1.6c.3.1.6.1.9.1.9%200%201.5-.6%201.7-1.7l2.2-13%201.9.1zm-1.6-3.1c0-.3.1-.6.3-.8.2-.2.5-.3.8-.4.3%200%20.6.1.8.3.2.2.3.5.3.8s-.1.6-.3.8-.5.3-.8.3-.6-.1-.8-.3-.3-.4-.3-.7zM246.4%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2s1.1%201.8%201.1%203.1v1.4l-.1.9H244c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1-.8-1.8-.8zM266.5%20116.2c-.1%201.2-.5%202.2-1%203.2s-1.1%201.7-1.8%202.1c-.7.5-1.5.7-2.4.7-1.3%200-2.4-.5-3.1-1.4l-1%205.6h-1.9l2.8-16.1h1.8l-.2%201.3c1-1%202.1-1.5%203.4-1.5%201.1%200%202%20.4%202.6%201.2s1%201.9%201%203.3c0%20.5%200%20.9-.1%201.3l-.1.3zm-1.9-.2l.1-.9c0-1-.2-1.8-.6-2.4s-1-.8-1.7-.9c-1.1%200-2.1.5-2.9%201.6l-1%205.6c.4%201%201.2%201.6%202.4%201.6%201%200%201.8-.4%202.5-1.1.5-.8.9-2%201.2-3.5zM274.2%20112.1l-.9-.1c-1.2%200-2.2.6-2.9%201.7L269%20122h-1.9l2-11.6h1.9l-.3%201.4c.8-1.1%201.8-1.6%202.9-1.6.2%200%20.5.1.9.2l-.3%201.7zM275.3%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM287.6%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2s1.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.4-.5-1-.8-1.8-.8zM297.8%20120.6c.7%200%201.3-.2%201.9-.6s.9-1%201.1-1.6h1.8c-.1.7-.4%201.4-.9%202s-1.1%201.1-1.8%201.4-1.5.5-2.2.5c-1%200-1.8-.3-2.5-.8s-1.2-1.2-1.5-2.1-.4-1.9-.3-3l.1-.5c.1-1.1.5-2.1%201-3s1.2-1.6%202-2.1%201.7-.7%202.8-.7c1.2%200%202.2.4%202.9%201.2s1.1%201.8%201.1%203h-1.8c0-.8-.2-1.4-.6-1.8-.4-.5-1-.7-1.7-.7-1%200-1.8.4-2.5%201.1-.7.8-1.1%201.9-1.3%203.2v1.7c0%20.8.3%201.5.7%202s1%20.8%201.7.8zM305.2%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM316.8%20119.1l4.1-8.7h2.1l-6.9%2013.6c-1%201.9-2.2%202.8-3.7%202.8-.3%200-.7-.1-1.2-.2l.2-1.6.5.1c.6%200%201.1-.1%201.6-.4.4-.3.8-.8%201.2-1.5l.7-1.3-2-11.4h2l1.4%208.6z%22%2F%3E%3Cpath%20fill%3D%22%23FFF%22%20d%3D%22M326.9%20107.6l-.5%202.8h2.1l-.3%201.5h-2.1l-1.2%207.2v.5c0%20.6.4.8.9.8.2%200%20.6%200%20.9-.1l-.2%201.6c-.5.1-.9.2-1.4.2-.8%200-1.4-.3-1.7-.9-.4-.6-.5-1.3-.5-2.2l1.2-7.2H322l.3-1.5h2.1l.5-2.8%202%20.1zM334.8%20122c0-.2-.1-.4-.1-.6v-.6c-1.1%201-2.2%201.5-3.4%201.4-1%200-1.9-.3-2.5-1-.6-.6-.9-1.4-.9-2.4.1-1.2.6-2.2%201.6-2.9%201-.7%202.3-1%204-1h2l.1-.9c.1-.7-.1-1.2-.4-1.6s-.9-.6-1.5-.6c-.7%200-1.3.1-1.8.5-.5.3-.8.8-.9%201.3h-2c.1-.7.3-1.3.8-1.8s1.1-.9%201.8-1.2c.7-.3%201.5-.4%202.3-.4%201.2%200%202.2.4%202.8%201.1.7.7%201%201.6.8%202.8l-.9%205.8-.1.8c0%20.4%200%20.8.1%201.2v.2h-1.8v-.1zm-3-1.5c.6%200%201.2-.1%201.8-.4.6-.3%201-.7%201.4-1.3l.4-2.4h-1.5c-1.2%200-2.2.2-2.8.6s-1%20.9-1.1%201.6c-.1.5.1%201%20.4%201.4.3.3.8.5%201.4.5zM343.3%20120.6c.7%200%201.3-.2%201.9-.6s.9-1%201.1-1.6h1.8c-.1.7-.4%201.4-.9%202s-1.1%201.1-1.8%201.4-1.5.5-2.2.5c-1%200-1.8-.3-2.5-.8s-1.2-1.2-1.5-2.1-.4-1.9-.3-3l.1-.5c.1-1.1.5-2.1%201-3s1.2-1.6%202-2.1%201.7-.7%202.8-.7c1.2%200%202.2.4%202.9%201.2s1.1%201.8%201.1%203H347c0-.8-.2-1.4-.6-1.8-.4-.5-1-.7-1.7-.7-1%200-1.8.4-2.5%201.1-.7.8-1.1%201.9-1.3%203.2v1.7c0%20.8.3%201.5.7%202s.9.8%201.7.8zm2.7-15.1h2.5l-3.3%203.2h-1.7l2.5-3.2z%22%2F%3E%3Cpath%20fill%3D%22%23F60%22%20d%3D%22M92.9%20124H7.1c-3.9%200-7.1-3.2-7.1-7.1V31.1C0%2027.2%203.2%2024%207.1%2024h85.8c3.9%200%207.1%203.2%207.1%207.1v85.8c0%203.9-3.2%207.1-7.1%207.1z%22%2F%3E%3Cpath%20opacity%3D%22.2%22%20d%3D%22M68%2044.1c-6.8-7-15.9-12.2-24.6-14.4C28.8%2026%2025%2023.9%2017.8%2019c-4.1-2.8-7.2-8.4-10.6-9.8-1.6-.6-2.6.1-3.1.7-.9%201-1.6%202.9-.4%205.2%2010.1%2019.8%2048.4%2076.8%2048.4%2076.8-5.9%201.1-12.4%205.7-17%2012.9-6.9%2010.7-7%2022.9-.1%2027.2%206.9%204.3%2018.1-.9%2025-11.6%205.9-9%206.8-19.1%202.8-24.6L29.2%2042.7s-3.9-4.7%202.3-6.1c4.5-1%2013.9.1%2019%202.8C56%2042.3%2069%2051.7%2072.3%2065.1c1.5%206.2.6%209.6%201.4%2015.1.4%202.6%203.2%204.8%205.5%201.2%201.2-1.9%201.4-8.4%201.1-13.5-.5-6.7-6-17.3-12.3-23.8zm-10.4%2048c.3.1.7.2%201%20.3-.3-.1-.6-.2-1-.3zm-2.5-.4h.4-.4zm1.1.1c.3%200%20.6.1.8.1-.2%200-.5-.1-.8-.1zm-3%200c.1%200%20.1%200%200%200%20.1%200%20.1%200%200%200zm5.8.8l1.2.6c-.4-.3-.8-.5-1.2-.6zm2.9%202zm-.7-.7c-.3-.3-.6-.5-1-.7.3.2.6.5%201%20.7z%22%2F%3E%3ClinearGradient%20id%3D%22a%22%20gradientUnits%3D%22userSpaceOnUse%22%20x1%3D%2239.902%22%20y1%3D%2245.741%22%20x2%3D%2252.109%22%20y2%3D%2212.201%22%20gradientTransform%3D%22matrix%281%200%200%20-1%200%20138%29%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%238fe132%22%2F%3E%3Cstop%20offset%3D%22.362%22%20stop-color%3D%22%2365bd23%22%2F%3E%3Cstop%20offset%3D%22.668%22%20stop-color%3D%22%2349a519%22%2F%3E%3Cstop%20offset%3D%22.844%22%20stop-color%3D%22%233e9c15%22%2F%3E%3C%2FlinearGradient%3E%3Cpath%20fill%3D%22url%28%23a%29%22%20d%3D%22M57.9%20117.8c-7.6%2010.4-19.2%2014.9-25.7%2010.1s-5.7-17.2%201.9-27.6%2019.2-14.9%2025.7-10.1c6.6%204.9%205.7%2017.2-1.9%2027.6z%22%2F%3E%3ClinearGradient%20id%3D%22b%22%20gradientUnits%3D%22userSpaceOnUse%22%20x1%3D%22-3.575%22%20y1%3D%22113.687%22%20x2%3D%2282.758%22%20y2%3D%2263.843%22%20gradientTransform%3D%22matrix%281%200%200%20-1%200%20138%29%22%3E%3Cstop%20offset%3D%22.184%22%20stop-color%3D%22%238fe132%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%233e9c15%22%2F%3E%3C%2FlinearGradient%3E%3Cpath%20fill%3D%22url%28%23b%29%22%20d%3D%22M70.9%2041c-6.3-7.6-15-13.4-23.6-16.2-14.4-4.7-18-7.1-24.8-12.5-3.9-3.1-6.6-9-9.9-10.7-1.6-.8-2.6-.1-3.2.5-.9.9-1.8%202.8-.7%205.2C17.4%2028%2051.9%2088.4%2051.9%2088.4c3-.3%205.7.2%207.9%201.8%201%20.8%201.8%201.7%202.5%202.8l-30-56s-3.6-5%202.7-6c4.5-.7%2013.9%201.1%2018.8%204.1%205.3%203.3%2017.6%2013.7%2020%2027.5%201.1%206.4%200%209.7.4%2015.4.2%202.6%202.9%205.1%205.4%201.6%201.3-1.8%202-8.4%202-13.6%200-6.8-4.9-17.9-10.7-25z%22%2F%3E%3CradialGradient%20id%3D%22c%22%20cx%3D%22426.647%22%20cy%3D%22271.379%22%20r%3D%2214.948%22%20gradientTransform%3D%22matrix%28.2966%20.4025%20.805%20-.5933%20-299.03%2088.664%29%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%23f4ff72%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%2373c928%22%20stop-opacity%3D%220%22%2F%3E%3C%2FradialGradient%3E%3Cpath%20fill%3D%22url%28%23c%29%22%20d%3D%22M50.4%20105.4c-6.6%204.9-14%206.2-16.5%202.8-2.4-3.3%201-10%207.6-14.9s14-6.2%2016.5-2.8c2.5%203.3-.9%2010-7.6%2014.9z%22%2F%3E%3C%2Fsvg%3E\" class=\"img-fluid\" alt=\"tekstowo.pl\" title=\"Teksty piosenek, tłumaczenia, teledyski\" style=\"width: 140px\">\t    </a>\r\n\r\n\t    <div class=\"btn-group\" role=\"group\">\r\n\t\t<button class=\"navbar-toggler pr-2 pr-lg-0\" type=\"button\" data-toggle=\"collapse\" data-target=\"#navbarUser\" aria-controls=\"navbarUser\" aria-expanded=\"false\" aria-label=\"Konto\">\r\n\t\t    <span class=\"navbar-toggler-icon user \"></span>\r\n\t\t</button>\r\n\r\n\t\t<button class=\"navbar-toggler pr-2 pr-lg-0\" type=\"button\" data-toggle=\"collapse\" data-target=\"#navbarSearch\" aria-controls=\"navbarSearch\" aria-expanded=\"false\" aria-label=\"Wyszukiwarka\">\r\n\t\t    <span class=\"navbar-toggler-icon search\"></span>\r\n\t\t</button>\r\n\r\n\t\t<button class=\"navbar-toggler pr-2 pr-lg-0\" type=\"button\" data-toggle=\"collapse\" data-target=\"#navbarSupportedContent\" aria-controls=\"navbarSupportedContent\" aria-expanded=\"false\" aria-label=\"Menu główne\">\r\n\t\t    <span class=\"navbar-toggler-icon\"></span>\r\n\t\t</button>\r\n\t    </div>\r\n\r\n\t    \t</div>\r\n    </nav>\r\n\r\n    <div class=\"container\">\r\n\t<div class=\"row top-row d-none d-md-flex\">\r\n\t    <div class=\"col-md-4 col-lg-3 align-self-center\">\r\n\t\t<a href=\"/\" class=\"logo\">\r\n\t\t    <img src=\"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20441.8%20136%22%3E%3Cstyle%3E.st0%7Bfill%3A%2362ae25%7D.st1%7Bfill%3A%23999%7D%3C%2Fstyle%3E%3Cg%20id%3D%22logo_1_%22%3E%3Cpath%20class%3D%22st0%22%20d%3D%22M130.7%2046v9h6.5v6.2h-6.5v20.6c0%201.4.3%202.4.8%203.1.6.6%201.6.9%203%20.9%201%200%201.9-.1%202.9-.3v6.4c-1.9.5-3.7.8-5.5.8-6.4%200-9.6-3.5-9.6-10.6v-21h-6.1V55h6.1v-9h8.4zM156.3%2092.7c-5.3%200-9.5-1.7-12.8-5s-4.9-7.7-4.9-13.2v-1c0-3.7.7-7%202.1-9.9%201.4-2.9%203.4-5.2%206-6.8s5.4-2.4%208.6-2.4c5%200%208.9%201.6%2011.7%204.8%202.7%203.2%204.1%207.8%204.1%2013.6v3.3H147c.3%203.1%201.3%205.5%203.1%207.2%201.8%201.8%204%202.7%206.8%202.7%203.8%200%206.9-1.5%209.3-4.6l4.5%204.3c-1.5%202.2-3.5%203.9-5.9%205.1-2.6%201.3-5.4%201.9-8.5%201.9zm-1-31.7c-2.3%200-4.1.8-5.5%202.4-1.4%201.6-2.3%203.8-2.7%206.7h15.8v-.6c-.2-2.8-.9-4.9-2.2-6.3-1.3-1.5-3.1-2.2-5.4-2.2zM186.1%2076.1l-3.7%203.8V92h-8.3V39.5h8.3v30.3l2.6-3.2L195.2%2055h10l-13.7%2015.4L206.7%2092h-9.6l-11-15.9z%22%2F%3E%3Cpath%20class%3D%22st0%22%20d%3D%22M227.9%2082c0-1.5-.6-2.6-1.8-3.4s-3.2-1.5-6.1-2.1c-2.8-.6-5.2-1.3-7.1-2.3-4.1-2-6.2-4.9-6.2-8.7%200-3.2%201.3-5.9%204-8s6.1-3.2%2010.3-3.2c4.4%200%208%201.1%2010.7%203.3s4.1%205%204.1%208.5h-8.3c0-1.6-.6-2.9-1.8-4s-2.8-1.6-4.7-1.6c-1.8%200-3.3.4-4.5%201.3-1.2.8-1.7%202-1.7%203.4%200%201.3.5%202.3%201.6%203s3.2%201.4%206.5%202.1%205.8%201.6%207.7%202.6%203.2%202.2%204.1%203.6c.9%201.4%201.3%203.1%201.3%205.1%200%203.3-1.4%206-4.1%208.1-2.8%202.1-6.4%203.1-10.8%203.1-3%200-5.7-.5-8.1-1.6-2.4-1.1-4.2-2.6-5.5-4.5s-2-4-2-6.2h8.1c.1%202%20.9%203.5%202.2%204.5%201.4%201.1%203.2%201.6%205.4%201.6s3.9-.4%205-1.2c1.1-1%201.7-2.1%201.7-3.4zM250.2%2046v9h6.5v6.2h-6.5v20.6c0%201.4.3%202.4.8%203.1.6.6%201.6.9%203%20.9%201%200%201.9-.1%202.9-.3v6.4c-1.9.5-3.7.8-5.5.8-6.4%200-9.6-3.5-9.6-10.6v-21h-6.1V55h6.1v-9h8.4zM257%2073.2c0-3.6.7-6.9%202.2-9.8s3.5-5.1%206-6.7c2.6-1.6%205.6-2.3%209-2.3%205%200%209%201.6%2012.2%204.8%203.1%203.2%204.8%207.5%205%2012.8v1.9c0%203.6-.7%206.9-2.1%209.8s-3.4%205.1-6%206.7-5.6%202.4-9%202.4c-5.2%200-9.4-1.7-12.5-5.2s-4.7-8.1-4.7-13.9v-.5h-.1zm8.3.7c0%203.8.8%206.8%202.4%208.9%201.6%202.2%203.8%203.2%206.6%203.2s5-1.1%206.5-3.3c1.6-2.2%202.3-5.4%202.3-9.6%200-3.7-.8-6.7-2.4-8.9-1.6-2.2-3.8-3.3-6.5-3.3s-4.9%201.1-6.5%203.2c-1.6%202.3-2.4%205.5-2.4%209.8zM327.4%2080.5l5.9-25.5h8.1l-10.1%2037h-6.8l-7.9-25.4-7.9%2025.4h-6.8l-10.1-37h8.1l6%2025.3%207.6-25.3h6.3l7.6%2025.5zM341.8%2073.2c0-3.6.7-6.9%202.2-9.8s3.5-5.1%206-6.7c2.6-1.6%205.6-2.3%209-2.3%205%200%209%201.6%2012.2%204.8%203.1%203.2%204.8%207.5%205%2012.8v1.9c0%203.6-.7%206.9-2.1%209.8s-3.4%205.1-6%206.7-5.6%202.4-9%202.4c-5.2%200-9.4-1.7-12.5-5.2s-4.7-8.1-4.7-13.9v-.5h-.1zm8.3.7c0%203.8.8%206.8%202.4%208.9%201.6%202.2%203.8%203.2%206.6%203.2s5-1.1%206.5-3.3c1.6-2.2%202.3-5.4%202.3-9.6%200-3.7-.8-6.7-2.4-8.9-1.6-2.2-3.8-3.3-6.5-3.3s-4.9%201.1-6.5%203.2c-1.6%202.3-2.4%205.5-2.4%209.8zM384.9%2083.3c1.5%200%202.7.4%203.6%201.3.8.9%201.3%202%201.3%203.3s-.4%202.4-1.3%203.2c-.8.9-2%201.3-3.6%201.3-1.5%200-2.7-.4-3.5-1.3-.9-.8-1.3-1.9-1.3-3.2%200-1.3.4-2.4%201.3-3.3.8-.9%202-1.3%203.5-1.3zM428.2%2073.9c0%205.7-1.3%2010.3-3.9%2013.7s-6.1%205.1-10.5%205.1c-4.1%200-7.3-1.3-9.7-4v17.5h-8.3V55h7.7l.3%203.8c2.4-3%205.8-4.4%209.9-4.4%204.5%200%208%201.7%2010.6%205%202.6%203.4%203.8%208%203.8%2014v.5h.1zm-8.3-.7c0-3.7-.7-6.6-2.2-8.8-1.5-2.2-3.6-3.2-6.3-3.2-3.4%200-5.8%201.4-7.3%204.2v16.4c1.5%202.9%204%204.3%207.4%204.3%202.6%200%204.7-1.1%206.2-3.2%201.5-2.2%202.2-5.4%202.2-9.7zM440.5%2092h-8.3V39.5h8.3V92z%22%2F%3E%3Cpath%20class%3D%22st1%22%20d%3D%22M123.5%20107.6l-.5%202.8h2.1l-.3%201.5h-2.1l-1.2%207.2v.5c0%20.6.4.8.9.8.2%200%20.6%200%20.9-.1l-.2%201.6c-.5.1-.9.2-1.4.2-.8%200-1.4-.3-1.7-.9-.4-.6-.5-1.3-.5-2.2l1.2-7.2h-2.1l.3-1.5h2.1l.5-2.8%202%20.1zM129.3%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2.7.8%201.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.6.8-2.6.8zm1.3-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1.1-.8-1.8-.8zM141.8%20112.1l-.9-.1c-1.2%200-2.2.6-2.9%201.7l-1.4%208.3h-1.9l2-11.6h1.9l-.3%201.4c.8-1.1%201.8-1.6%202.9-1.6.2%200%20.5.1.9.2l-.3%201.7zM147.5%20122c-.1-.2-.1-.4-.1-.6v-.6c-1.1%201-2.2%201.5-3.4%201.4-1%200-1.9-.3-2.5-1-.6-.6-.9-1.4-.9-2.4.1-1.2.6-2.2%201.6-2.9%201-.7%202.3-1%204-1h2l.1-.9c.1-.7-.1-1.2-.4-1.6-.3-.4-.9-.6-1.5-.6-.7%200-1.3.1-1.8.5-.5.3-.8.8-.9%201.3h-2c.1-.7.3-1.3.8-1.8s1.1-.9%201.8-1.2c.7-.3%201.5-.4%202.3-.4%201.2%200%202.2.4%202.8%201.1.7.7%201%201.6.8%202.8l-.9%205.8-.1.8c0%20.4%200%20.8.1%201.2v.2h-1.8v-.1zm-3-1.5c.6%200%201.2-.1%201.8-.4s1-.7%201.4-1.3l.4-2.4h-1.5c-1.2%200-2.2.2-2.8.6-.7.4-1%20.9-1.1%201.6-.1.5.1%201%20.4%201.4.3.3.8.5%201.4.5zM153.2%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM168.8%20110.4l-.2%201.3c1-1%202.2-1.5%203.5-1.5.7%200%201.4.2%201.9.5s.9.8%201.1%201.4c1.1-1.3%202.4-1.9%203.9-1.9%201.2%200%202%20.4%202.6%201.2.6.8.8%201.8.6%203l-1.3%207.6H179l1.3-7.7v-1c-.1-1-.8-1.5-1.9-1.5-.7%200-1.4.2-1.9.7-.6.5-.9%201.1-1.1%201.8l-1.3%207.7h-2l1.3-7.6c.1-.8%200-1.4-.4-1.8s-.9-.7-1.6-.7c-1.2%200-2.2.5-2.9%201.7l-1.5%208.5h-1.9l2-11.6%201.7-.1zM189.1%20110.2c1%200%201.8.3%202.5.8s1.2%201.2%201.5%202.1.4%201.9.3%203v.2c-.1%201.1-.5%202.2-1%203.1s-1.2%201.6-2.1%202.1c-.9.5-1.8.7-2.8.7s-1.8-.3-2.5-.8-1.2-1.2-1.5-2.1-.4-1.9-.3-2.9c.1-1.2.4-2.3%201-3.2.5-1%201.2-1.7%202.1-2.2.8-.6%201.8-.9%202.8-.8zm-4%206.2c-.1.5-.1.9%200%201.4.1.8.3%201.5.8%202%20.4.5%201%20.8%201.7.8.6%200%201.2-.1%201.8-.5.5-.3%201-.9%201.4-1.5.4-.7.6-1.5.7-2.3.1-.7.1-1.2%200-1.7-.1-.9-.3-1.6-.8-2.1-.4-.5-1-.8-1.7-.8-1%200-1.9.4-2.6%201.2-.7.8-1.1%201.9-1.3%203.2v.3zM195.8%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zm3.2-13c0-.3.1-.6.3-.8.2-.2.5-.3.8-.4.3%200%20.6.1.8.3.2.2.3.5.3.8s-.1.6-.3.8c-.2.2-.5.3-.8.3s-.6-.1-.8-.3c-.2-.1-.3-.4-.3-.7zM208.1%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2.7.8%201.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1-.8-1.8-.8zM220.6%20118.9c.1-.8-.3-1.4-1.3-1.7l-2-.6c-1.7-.6-2.5-1.6-2.5-2.9%200-1%20.5-1.9%201.4-2.5.9-.7%201.9-1%203.1-1%201.2%200%202.1.4%202.9%201%20.7.7%201.1%201.5%201.1%202.6h-1.9c0-.6-.2-1.1-.5-1.4s-.9-.6-1.5-.6c-.7%200-1.3.2-1.7.5-.5.3-.7.7-.8%201.3-.1.7.3%201.2%201.2%201.5l1%20.3c1.3.3%202.3.8%202.8%201.3s.8%201.2.8%202.1c0%20.7-.3%201.4-.7%201.9s-1%20.9-1.7%201.2c-.7.3-1.5.4-2.3.4-1.2%200-2.2-.4-3.1-1.1-.8-.7-1.2-1.6-1.2-2.7h1.9c0%20.7.2%201.2.6%201.6.4.4%201%20.6%201.7.6s1.3-.1%201.8-.4c.5-.5.8-.9.9-1.4zM225.5%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM241.3%20110.4l-2.2%2013c-.1%201.1-.5%201.9-1.1%202.5s-1.4.9-2.3.8c-.4%200-.8-.1-1.3-.2l.2-1.6c.3.1.6.1.9.1.9%200%201.5-.6%201.7-1.7l2.2-13%201.9.1zm-1.6-3.1c0-.3.1-.6.3-.8.2-.2.5-.3.8-.4.3%200%20.6.1.8.3.2.2.3.5.3.8s-.1.6-.3.8-.5.3-.8.3-.6-.1-.8-.3-.3-.4-.3-.7zM246.4%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2s1.1%201.8%201.1%203.1v1.4l-.1.9H244c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1-.8-1.8-.8zM266.5%20116.2c-.1%201.2-.5%202.2-1%203.2s-1.1%201.7-1.8%202.1c-.7.5-1.5.7-2.4.7-1.3%200-2.4-.5-3.1-1.4l-1%205.6h-1.9l2.8-16.1h1.8l-.2%201.3c1-1%202.1-1.5%203.4-1.5%201.1%200%202%20.4%202.6%201.2s1%201.9%201%203.3c0%20.5%200%20.9-.1%201.3l-.1.3zm-1.9-.2l.1-.9c0-1-.2-1.8-.6-2.4s-1-.8-1.7-.9c-1.1%200-2.1.5-2.9%201.6l-1%205.6c.4%201%201.2%201.6%202.4%201.6%201%200%201.8-.4%202.5-1.1.5-.8.9-2%201.2-3.5zM274.2%20112.1l-.9-.1c-1.2%200-2.2.6-2.9%201.7L269%20122h-1.9l2-11.6h1.9l-.3%201.4c.8-1.1%201.8-1.6%202.9-1.6.2%200%20.5.1.9.2l-.3%201.7zM275.3%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM287.6%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2s1.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.4-.5-1-.8-1.8-.8zM297.8%20120.6c.7%200%201.3-.2%201.9-.6s.9-1%201.1-1.6h1.8c-.1.7-.4%201.4-.9%202s-1.1%201.1-1.8%201.4-1.5.5-2.2.5c-1%200-1.8-.3-2.5-.8s-1.2-1.2-1.5-2.1-.4-1.9-.3-3l.1-.5c.1-1.1.5-2.1%201-3s1.2-1.6%202-2.1%201.7-.7%202.8-.7c1.2%200%202.2.4%202.9%201.2s1.1%201.8%201.1%203h-1.8c0-.8-.2-1.4-.6-1.8-.4-.5-1-.7-1.7-.7-1%200-1.8.4-2.5%201.1-.7.8-1.1%201.9-1.3%203.2v1.7c0%20.8.3%201.5.7%202s1%20.8%201.7.8zM305.2%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM316.8%20119.1l4.1-8.7h2.1l-6.9%2013.6c-1%201.9-2.2%202.8-3.7%202.8-.3%200-.7-.1-1.2-.2l.2-1.6.5.1c.6%200%201.1-.1%201.6-.4.4-.3.8-.8%201.2-1.5l.7-1.3-2-11.4h2l1.4%208.6z%22%2F%3E%3Cpath%20class%3D%22st1%22%20d%3D%22M326.9%20107.6l-.5%202.8h2.1l-.3%201.5h-2.1l-1.2%207.2v.5c0%20.6.4.8.9.8.2%200%20.6%200%20.9-.1l-.2%201.6c-.5.1-.9.2-1.4.2-.8%200-1.4-.3-1.7-.9-.4-.6-.5-1.3-.5-2.2l1.2-7.2H322l.3-1.5h2.1l.5-2.8%202%20.1zM334.8%20122c0-.2-.1-.4-.1-.6v-.6c-1.1%201-2.2%201.5-3.4%201.4-1%200-1.9-.3-2.5-1-.6-.6-.9-1.4-.9-2.4.1-1.2.6-2.2%201.6-2.9%201-.7%202.3-1%204-1h2l.1-.9c.1-.7-.1-1.2-.4-1.6s-.9-.6-1.5-.6c-.7%200-1.3.1-1.8.5-.5.3-.8.8-.9%201.3h-2c.1-.7.3-1.3.8-1.8s1.1-.9%201.8-1.2c.7-.3%201.5-.4%202.3-.4%201.2%200%202.2.4%202.8%201.1.7.7%201%201.6.8%202.8l-.9%205.8-.1.8c0%20.4%200%20.8.1%201.2v.2h-1.8v-.1zm-3-1.5c.6%200%201.2-.1%201.8-.4.6-.3%201-.7%201.4-1.3l.4-2.4h-1.5c-1.2%200-2.2.2-2.8.6s-1%20.9-1.1%201.6c-.1.5.1%201%20.4%201.4.3.3.8.5%201.4.5zM343.3%20120.6c.7%200%201.3-.2%201.9-.6s.9-1%201.1-1.6h1.8c-.1.7-.4%201.4-.9%202s-1.1%201.1-1.8%201.4-1.5.5-2.2.5c-1%200-1.8-.3-2.5-.8s-1.2-1.2-1.5-2.1-.4-1.9-.3-3l.1-.5c.1-1.1.5-2.1%201-3s1.2-1.6%202-2.1%201.7-.7%202.8-.7c1.2%200%202.2.4%202.9%201.2s1.1%201.8%201.1%203H347c0-.8-.2-1.4-.6-1.8-.4-.5-1-.7-1.7-.7-1%200-1.8.4-2.5%201.1-.7.8-1.1%201.9-1.3%203.2v1.7c0%20.8.3%201.5.7%202s.9.8%201.7.8zm2.7-15.1h2.5l-3.3%203.2h-1.7l2.5-3.2z%22%2F%3E%3Cpath%20id%3D%22box%22%20d%3D%22M92.9%20124H7.1c-3.9%200-7.1-3.2-7.1-7.1V31.1C0%2027.2%203.2%2024%207.1%2024h85.8c3.9%200%207.1%203.2%207.1%207.1v85.8c0%203.9-3.2%207.1-7.1%207.1z%22%20fill%3D%22%23f60%22%2F%3E%3Cpath%20d%3D%22M68%2044.1c-6.8-7-15.9-12.2-24.6-14.4C28.8%2026%2025%2023.9%2017.8%2019c-4.1-2.8-7.2-8.4-10.6-9.8-1.6-.6-2.6.1-3.1.7-.9%201-1.6%202.9-.4%205.2%2010.1%2019.8%2048.4%2076.8%2048.4%2076.8-5.9%201.1-12.4%205.7-17%2012.9-6.9%2010.7-7%2022.9-.1%2027.2%206.9%204.3%2018.1-.9%2025-11.6%205.9-9%206.8-19.1%202.8-24.6L29.2%2042.7s-3.9-4.7%202.3-6.1c4.5-1%2013.9.1%2019%202.8C56%2042.3%2069%2051.7%2072.3%2065.1c1.5%206.2.6%209.6%201.4%2015.1.4%202.6%203.2%204.8%205.5%201.2%201.2-1.9%201.4-8.4%201.1-13.5-.5-6.7-6-17.3-12.3-23.8zm-10.4%2048c.3.1.7.2%201%20.3-.3-.1-.6-.2-1-.3zm-2.5-.4h.4-.4zm1.1.1c.3%200%20.6.1.8.1-.2%200-.5-.1-.8-.1zm-3%200c.1%200%20.1%200%200%200%20.1%200%20.1%200%200%200zm5.8.8l1.2.6c-.4-.3-.8-.5-1.2-.6zm2.9%202zm-.7-.7c-.3-.3-.6-.5-1-.7.3.2.6.5%201%20.7z%22%20opacity%3D%22.2%22%2F%3E%3ClinearGradient%20id%3D%22SVGID_1_%22%20gradientUnits%3D%22userSpaceOnUse%22%20x1%3D%2239.888%22%20y1%3D%2292.284%22%20x2%3D%2252.096%22%20y2%3D%22125.824%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%238fe132%22%2F%3E%3Cstop%20offset%3D%22.362%22%20stop-color%3D%22%2365bd23%22%2F%3E%3Cstop%20offset%3D%22.668%22%20stop-color%3D%22%2349a519%22%2F%3E%3Cstop%20offset%3D%22.844%22%20stop-color%3D%22%233e9c15%22%2F%3E%3C%2FlinearGradient%3E%3Cpath%20d%3D%22M57.9%20117.8c-7.6%2010.4-19.2%2014.9-25.7%2010.1s-5.7-17.2%201.9-27.6%2019.2-14.9%2025.7-10.1c6.6%204.9%205.7%2017.2-1.9%2027.6z%22%20fill%3D%22url%28%23SVGID_1_%29%22%2F%3E%3ClinearGradient%20id%3D%22SVGID_2_%22%20gradientUnits%3D%22userSpaceOnUse%22%20x1%3D%22-3.574%22%20y1%3D%2224.316%22%20x2%3D%2282.773%22%20y2%3D%2274.169%22%3E%3Cstop%20offset%3D%22.184%22%20stop-color%3D%22%238fe132%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%233e9c15%22%2F%3E%3C%2FlinearGradient%3E%3Cpath%20d%3D%22M70.9%2041c-6.3-7.6-15-13.4-23.6-16.2-14.4-4.7-18-7.1-24.8-12.5-3.9-3.1-6.6-9-9.9-10.7-1.6-.8-2.6-.1-3.2.5-.9.9-1.8%202.8-.7%205.2C17.4%2028%2051.9%2088.4%2051.9%2088.4c3-.3%205.7.2%207.9%201.8%201%20.8%201.8%201.7%202.5%202.8l-30-56s-3.6-5%202.7-6c4.5-.7%2013.9%201.1%2018.8%204.1%205.3%203.3%2017.6%2013.7%2020%2027.5%201.1%206.4%200%209.7.4%2015.4.2%202.6%202.9%205.1%205.4%201.6%201.3-1.8%202-8.4%202-13.6%200-6.8-4.9-17.9-10.7-25z%22%20fill%3D%22url%28%23SVGID_2_%29%22%2F%3E%3CradialGradient%20id%3D%22SVGID_3_%22%20cx%3D%2221.505%22%20cy%3D%22103.861%22%20r%3D%2214.934%22%20gradientTransform%3D%22matrix%28.2966%20.4025%20-.805%20.5933%20123.22%2029.092%29%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%23f4ff72%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%2373c928%22%20stop-opacity%3D%220%22%2F%3E%3C%2FradialGradient%3E%3Cpath%20d%3D%22M50.4%20105.4c-6.6%204.9-14%206.2-16.5%202.8-2.4-3.3%201-10%207.6-14.9s14-6.2%2016.5-2.8c2.5%203.3-.9%2010-7.6%2014.9z%22%20fill%3D%22url%28%23SVGID_3_%29%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E\" class=\"img-fluid\" alt=\"tekstowo.pl\" title=\"Teksty piosenek, tłumaczenia, teledyski\">\t\t</a>\r\n\t    </div>\r\n\r\n\t    <div class=\"col\">\r\n\t\t<div class=\"top-links\">\r\n\t\t    \t\t\t<a href=\"/logowanie.html\" title=\"Logowanie\">Logowanie</a> |\r\n\t\t\t<a href=\"/rejestracja.html\" title=\"Rejestracja\">Rejestracja</a> |\r\n\t\t    \t\t    <a href=\"/faq.html\" title=\"FAQ\">FAQ</a> |\r\n\t\t    <a href=\"/regulamin.html\" title=\"Regulamin\">Regulamin</a> |\r\n\t\t    <a href=\"/kontakt.html\" title=\"Kontakt\">Kontakt</a>\r\n\t\t</div>\r\n\r\n\t\t<div class=\"topmenu\">\r\n\t\t    <ul class=\"\">\r\n    <li class=\"topmenu-first\"><a href=\"/\" title=\"Główna\">Główna</a></li>\r\n    <li><a href=\"/przegladaj_teksty.html\" title=\"Teksty\">Teksty</a></li>\r\n    <li><a href=\"/szukane_utwory,6-miesiecy.html\" title=\"Poszukiwane teksty\">Poszukiwane teksty</a></li>\r\n    <li><a href=\"/soundtracki_najnowsze.html\" title=\"Soundtracki\">Soundtracki</a></li>\r\n    <li><a href=\"/rankingi\" title=\"Rankingi\">Rankingi</a></li>\r\n    <li class=\"topmenu-last\"><a href=\"/uzytkownicy.html\" title=\"Użytkownicy\" class=\"no-bg \">Użytkownicy</a></li>\r\n\t</ul>\t\t</div>\r\n\t    </div>\r\n\t</div>\r\n\t<div id=\"t170319\">\r\n\t    <div class=\"adv-top\" style=\"min-height: 300px\"> <!-- reklama bill -->\r\n\t\t<div>\r\n\t\t    <a title=\"Ukryj reklamy\" href=\"javascript:;\" rel=\"loginbox\" id=\"hide-ads\"></a>\t\t    \r\n    \t\t    \t    \r\n    \r\n    \t    \r\n    \r\n   \r\n\r\n<!-- kod reklamy desktop -->\r\n<center>\r\n<script data-adfscript=\"adx.adform.net/adx/?mid=668393&amp;rnd=<random_number>\"></script>\r\n</center> \r\n \r\n\r\n\t\t</div>\r\n\t    </div> <!-- end reklama bill -->\r\n\t</div>\r\n\t\t<div class=\"row topbar\">\r\n\t    <div class=\"col-auto\">\r\n\t\t<a href=\"/\" class=\"green\" title=\"Teksty piosenek\">Teksty piosenek</a> &gt; <a href=\"/artysci_na,pozostale.html\" class=\"green\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców w kategorii &quot;Pozostałe&quot;\">Pozostali</a>\r\n\t\t &gt; <a href=\"/piosenki_artysty,24kgoldn.html\" class=\"green\" title=\"24kGoldn\">24kGoldn</a>\r\n\t\t &gt; <a href=\"/piosenka,24kgoldn,city_of_angels_1.html\" class=\"green\" title=\"City Of Angels\">City Of Angels</a>\r\n\t\t\t\t</div>\r\n\t\t<div class=\"col d-none text-right d-md-block\">\r\n\t\t    2 170 744 tekstów, 20 217 poszukiwanych i 501 oczekujących\r\n\t\t</div>\r\n\t    </div>\r\n\r\n\t    <div class=\"row\">\r\n\r\n\t\t<div class=\"col-sm-4 col-lg-3 order-2 order-sm-1\">\r\n    <div class=\"big-buttons row mr-0\" role=\"group\">\r\n    <a href=\"/dodaj_tekst.html\" rel=\"loginbox\" class=\"dodaj-tekst\" title=\"Dodaj tekst\">\r\n\t<i class=\"icon\"></i>\r\n\tDodaj tekst\r\n    </a>\r\n    <a href=\"/zaproponuj_utwor.html\" rel=\"loginbox\" class=\"zaproponuj-utwor\" title=\"Szukasz utworu?\">\r\n\t<i class=\"icon\"></i>\r\n\tBrak tekstu?</a>\r\n    <a href=\"/dodaj_soundtrack.html\" rel=\"loginbox\" class=\"dodaj-soundtrack\" title=\"Dodaj soundtrack\">\r\n\t<i class=\"icon\"></i>\r\n\tDodaj soundtrack</a>\r\n</div>\r\n        \t<div class=\"left-box account-box mt-3\">\r\n\t    <h4>Zaloguj się</h4>\r\n\t    i wykorzystaj wszystkie możliwości serwisu!\r\n\r\n\t    <fieldset class=\"login mt-2 text-center\">\r\n\t\t<a class=\"login-send btn btn-block btn-primary mb-2\" href=\"https://www.tekstowo.pl/logowanie.html\">Zaloguj się</a>\r\n\t\t<a href=\"javascript:;\" class=\"fb-button my-fb-login-button btn btn-block btn-fb\" title=\"Zaloguj się z Facebookiem\"><span class=\"icon\"></span>Zaloguj się przez Facebooka</a>\r\n\t\t\t    </fieldset>\r\n\r\n\t    <ul class=\"arrows mt-2\">\r\n\t\t<li><a href=\"/przypomnij.html\" class=\"green bold\" title=\"Zapomniałem hasła\">Przypomnienie hasła</a></li>\r\n\t\t<li><a href=\"/rejestracja.html\" class=\"green bold\" title=\"Nie mam jeszcze konta\">Nie mam jeszcze konta</a></li>\r\n\t    </ul>\r\n\r\n\t</div>\r\n    \r\n    \t\t\t\t\t\t\t<div class=\"left-box mt-3\">\r\n\t\t\t\t<h4>Inne teksty piosenek</h4>\r\n\t\t\t\t<h4>24kGoldn</h4>\r\n\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t\t<b>1.</b> <a href=\"piosenka,24kgoldn,mistakes.html\" class=\"title\" title=\"24kGoldn - Mistakes\">24kGoldn - Mistakes </a>\r\n\t\t\t\t\t<b title=\"teledysk\" class=\"icon_kamera\"></b>\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t\t<b>2.</b> <a href=\"piosenka,24kgoldn,scar_ft__sokodomo_1.html\" class=\"title\" title=\"24kGoldn - Scar ft. Sokodomo\">24kGoldn - Scar ft. Sokodomo </a>\r\n\t\t\t\t\t<b title=\"teledysk\" class=\"icon_kamera\"></b>\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t\t<b>3.</b> <a href=\"piosenka,24kgoldn,scar.html\" class=\"title\" title=\"24kGoldn - Scar\">24kGoldn - Scar </a>\r\n\t\t\t\t\t<b title=\"teledysk\" class=\"icon_kamera\"></b>\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t\t<b>4.</b> <a href=\"piosenka,24kgoldn,more_than_friends.html\" class=\"title\" title=\"24kGoldn - More Than Friends\">24kGoldn - More Than Friends </a>\r\n\t\t\t\t\t<b title=\"teledysk\" class=\"icon_kamera\"></b><b title=\"tłumaczenie\" class=\"icon_pl\"></b>\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t<div class=\"box-przeboje no-bg\">\r\n\t\t\t\t\t<b>5.</b> <a href=\"piosenka,24kgoldn,prada_feat__lil_tecca_.html\" class=\"title\" title=\"24kGoldn - Prada feat. Lil Tecca \">24kGoldn - Prada feat. Lil Tecca  </a>\r\n\t\t\t\t\t<b title=\"teledysk\" class=\"icon_kamera\"></b>\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t</div> <!-- end inne utwory -->\r\n\t\t\t<a href=\"/piosenki_artysty,24kgoldn.html\" class=\"block text-right\" title=\"Zobacz więcej >>\">Zobacz więcej &gt;&gt;</a>\r\n\t\t\t\t        \r\n    \t<div class=\"left-box mt-3\">\r\n\t    <h4>Poszukiwane teksty</h4>\r\n\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>1.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,Henning+Wehland,tytul,Weil+wir+Champions+sind,poszukiwany,354712.html\" class=\"title\">Henning Wehland - Weil wir Champions sind</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>2.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,Demis+Roussos,tytul,For+Love,poszukiwany,355966.html\" class=\"title\">Demis Roussos - For Love</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>3.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,David+Bowie,tytul,Move+On+%28Moonage+Daydream+A+Cappella+Mix+Edit%29,poszukiwany,357748.html\" class=\"title\">David Bowie - Move On (Moonage Daydream A Cappella Mix Edit)</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>4.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,Inside,tytul,Wizzard+King,poszukiwany,355937.html\" class=\"title\">Inside - Wizzard King</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>5.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,Rob+Zombie,tytul,El+Vampiro,poszukiwany,358402.html\" class=\"title\">Rob Zombie - El Vampiro</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>6.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,Soul+Superiors,tytul,A+Great+Day,poszukiwany,355739.html\" class=\"title\">Soul Superiors - A Great Day</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>7.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,Jefferson+State,tytul,White+Out,poszukiwany,357438.html\" class=\"title\">Jefferson State - White Out</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>8.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,Jennifer+McNutt,tytul,After+Everyone,poszukiwany,355721.html\" class=\"title\">Jennifer McNutt - After Everyone</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>9.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,H%C3%A9loise+Janiaud,tytul,Be+a+Good+Girl,poszukiwany,356157.html\" class=\"title\">Héloise Janiaud - Be a Good Girl</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje no-bg\">\r\n\t\t    <b>10.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,The+Merced+Blue+Notes,tytul,Your+tender+lips,poszukiwany,355933.html\" class=\"title\">The Merced Blue Notes - Your tender lips</a>\r\n\t\t</div>\r\n\t    \t</div>\r\n\t<a href=\"/szukane_utwory,6-miesiecy.html\" class=\"block text-right\" title=\"Zobacz więcej >>\">Zobacz więcej &gt;&gt;</a>\r\n\r\n\t\t    <div class=\"left-box mt-3\">\r\n\t\t<h4>Poszukiwane tłumaczenia</h4>\r\n\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>1.</b> <a href=\"/piosenka,ub40,wear_you_to_the_ball.html\" class=\"title\">UB40 - Wear You To The Ball</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>2.</b> <a href=\"/piosenka,tony_christie,love_hurts.html\" class=\"title\">Tony Christie - Love Hurts</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>3.</b> <a href=\"/piosenka,rosa_chemical,polka_____feat__ernia__gu__pequeno_.html\" class=\"title\">Rosa Chemical - Polka :-/ (feat. Ernia, Guè Pequeno)</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>4.</b> <a href=\"/piosenka,marco,maybach_dyrektora.html\" class=\"title\">Marco - Maybach dyrektora</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>5.</b> <a href=\"/piosenka,ub40,so_destructive.html\" class=\"title\">UB40 - So Destructive</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>6.</b> <a href=\"/piosenka,varius_manx__kasia_stankiewicz,przed_epoka_wstydu.html\" class=\"title\">Varius Manx &amp; Kasia Stankiewicz - Przed epoką wstydu</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>7.</b> <a href=\"/piosenka,ub40,hurry_come_on_up.html\" class=\"title\">UB40 - Hurry Come On Up</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>8.</b> <a href=\"/piosenka,demis_roussos,o_my_friends_you_ve_been_untrue_to_me.html\" class=\"title\">Demis Roussos - O My Friends You've Been Untrue To Me</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>9.</b> <a href=\"/piosenka,ub40,the_earth_dies_screaming.html\" class=\"title\">UB40 - The Earth Dies Screaming</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje no-bg\">\r\n\t\t\t\t<b>10.</b> <a href=\"/piosenka,tony_christie,on_broadway.html\" class=\"title\">Tony Christie - On Broadway</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t    </div>\r\n\t    <a href=\"/szukane_tlumaczenia,6-miesiecy.html\" class=\"block text-right\" title=\"Zobacz więcej >>\">Zobacz więcej &gt;&gt;</a>\r\n\t    \r\n    <!-- polecamy -->\r\n    <div class=\"left-box mt-3\">\r\n\t<h4>Polecamy</h4>\r\n\t<ul class=\"arrows\">\r\n    <li><a href=\"http://www.giercownia.pl\" title=\"Gry online\" class=\"blank\">Gry online</a></li>\r\n    <li><a href=\"http://www.maxior.pl\" title=\"Filmy\" class=\"blank\">Śmieszne filmy</a></li>\r\n    <li><a href=\"http://www.bajer.pl\" title=\"Dziewczyny\" class=\"blank\">Polskie dziewczyny</a></li>\r\n    <li><a href=\"http://m.giercownia.pl\" title=\"Gry na telefon i tablet\" class=\"blank\">Gry na telefon i tablet</a></li>\r\n</ul>    </div>\r\n    <!-- end polecamy -->\r\n\r\n</div> <!-- end left-column -->\r\n\t\t<div class=\"col-sm-8 col-lg-9 order-1 order-sm-2\">\r\n\t\t    \t\t\t<div class=\"row search-box\">\r\n    <div class=\"inner col\">\r\n\r\n\t<div class=\"search-form\">\r\n\t    <form action=\"/wyszukaj.html\" method=\"get\" class=\"form-inline\">\r\n\t\t<label for=\"s-autor\" class=\"big-text my-1 mr-2\">Szukaj tekstu piosenki</label>\r\n\t\t<input type=\"text\" id=\"s-autor\" name=\"search-artist\" class=\"form-control form-control input-artist mb-2 mr-sm-2\" placeholder=\"Podaj wykonawcę\" value=\"\" autocomplete=\"off\">\r\n\t\t<label class=\"my-1 mr-2\" for=\"s-title\"> i/lub </label>\r\n\t\t<input type=\"text\" id=\"s-title\" name=\"search-title\" class=\"form-control form-control input-title mb-2 mr-sm-2\" placeholder=\"Podaj tytuł\" value=\"\" autocomplete=\"off\">\r\n\t\t<input type=\"submit\" class=\"submit\" value=\"Szukaj\">\r\n\t    </form>\r\n\r\n\t    <div class=\"search-adv\">\r\n\t\t<a href=\"/wyszukiwanie-zaawansowane.html\">wyszukiwanie zaawansowane &gt;</a>\r\n\t    </div>\r\n\r\n\t    <hr class=\"d-none d-md-block\">\r\n\t</div>\r\n\t<div class=\"search-browse\">\r\n\t    <div class=\"przegladaj big-text\">Przeglądaj wykonawców na literę</div>\r\n\t    <ul class=\"alfabet\">\r\n\t\t\t\t    <li><a href=\"/artysci_na,A.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę A\">A</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,B.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę B\">B</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,C.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę C\">C</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,D.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę D\">D</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,E.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę E\">E</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,F.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę F\">F</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,G.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę G\">G</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,H.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę H\">H</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,I.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę I\">I</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,J.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę J\">J</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,K.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę K\">K</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,L.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę L\">L</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,M.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę M\">M</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,N.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę N\">N</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,O.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę O\">O</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,P.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę P\">P</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,Q.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę Q\">Q</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,R.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę R\">R</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,S.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę S\">S</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,T.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę T\">T</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,U.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę U\">U</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,V.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę V\">V</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,W.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę W\">W</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,X.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę X\">X</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,Y.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę Y\">Y</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,Z.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę Z\">Z</a></li>\r\n\t\t    \t\t<li><a href=\"/artysci_na,pozostale.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców w kategorii &quot;Pozostałe&quot;\">0-9</a></li>\r\n\t    </ul>\r\n\t</div>\r\n\r\n    </div> <!-- end green-box -->\r\n</div>\t\t    \r\n<div class=\"row right-column\"> <!-- right column -->\r\n    <div class=\"col\">\r\n\r\n\t \r\n\r\n\t<div class=\"belka row mx-0 px-3\">\r\n\t    <div class=\"col-lg-7 px-0\">\r\n\t\t<h1 class=\"strong\">24kGoldn - City Of Angels</h1>\r\n\t    </div>\r\n\t    <div class=\"col-lg-5\">\r\n\t\t<div class=\"row belka-right\">\r\n\t\t    <div class=\"col\">\r\n\t\t\t<div class=\"odslon\">Odsłon: 9846</div>\r\n\t\t    </div>\r\n\t\t    <div class=\"col-auto\">\r\n\t\t\t<a href=\"/wykonawca,24kgoldn.html\" class=\"link-wykonawca\" title=\"Przejdź na stronę wykonawcy >\">Przejdź na stronę wykonawcy &gt;</a>\r\n\t\t    </div>\r\n\t\t</div>  \r\n\t    </div>\r\n\t</div>\r\n\r\n\r\n\t\r\n\t\r\n\t\r\n\t<div class=\"row mx-0\">\r\n\t    \t\t<div id=\"advSong\" class=\"adv-home col-lg-6 mt-2 order-2 order-lg-1\"> <!-- reklama środek -->\r\n\t\t        \t    \r\n    \r\n    \t    \r\n    \r\n\r\n   \r\n\r\n<!-- kod reklamy desktop -->\r\n<center>\r\n<script async=\"\" src=\"https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js\"></script>\r\n<!-- tekstowo desktop 336x280 -->\r\n<ins class=\"adsbygoogle\" style=\"display:inline-block;width:336px;height:280px\" data-ad-client=\"ca-pub-3653916656187385\" data-ad-slot=\"1803845785\"><iframe id=\"aswift_0\" style=\"height: 1px !important; max-height: 1px !important; max-width: 1px !important; width: 1px !important;\"><iframe id=\"google_ads_frame0\"></iframe></iframe></ins>\r\n<script>\r\n     (adsbygoogle = window.adsbygoogle || []).push({});\r\n</script>\r\n</center>\r\n   \r\n\t\t</div> <!-- end reklama środek -->\r\n\t    \r\n\t    <div class=\"teledysk zdjecie col-lg-6 order-1 order-lg-2 px-0\">\r\n\t\t\t\t    <div class=\"movieDivWrap\">\r\n\t\t\t<div id=\"movieDiv\">\r\n\t\t\t    \t\t\t\t\t\t\t\t\t\t\t\t\t\t    \t\t\t\t\t\t\t\t\t    <iframe style=\"border: 0; margin: 0; padding: 0; overflow: hidden\" width=\"365\" height=\"280\" src=\"//filmiki4.maxart.pl/tplayer3n/#adText=1&amp;autoplay=1&amp;videoId=yHwGIA4VeOc&amp;loadVideoTimeout=500000&amp;volume=0&amp;aoID=DVJAPyL5hymQ7Q0.2FRYVWxk34YuoC7ExS6BHR_5ALr.c7\" frameborder=\"0\" allowfullscreen=\"1\" scrolling=\"no\"></iframe>\r\n\t\t\t\t    \t\t\t\t\t\t\t    </div>\r\n\t\t\t</div>\r\n\t\t    \t\t</div>\r\n\t    </div>\r\n\r\n\t    <div class=\"row mt-3 mx-0\">\r\n\t\t<div class=\"col-lg px-md-0 pr-lg-3\">\r\n\t\t    \t\t\t<div class=\"teledysk-left\">\r\n\t\t\t    <div class=\"box-przeboje\">\r\n\t\t\t\t<span>\r\n\t\t\t\t    Tekst dodał(a): <a title=\"Tekst dodany 13.03.2020 przez asdfghjklmnop\" href=\"profil,asdfghjklmnop.html\">asdfghjklmnop</a>\r\n\t\t\t\t</span>\r\n\t\t\t\t<a rel=\"loginbox\" href=\"/edytuj_tekst,24kgoldn,city_of_angels_1.html\" class=\"new\" title=\"Edytuj tekst\">Edytuj tekst</a>\r\n\t\t\t    </div>\r\n\t\t\t    <div class=\"box-przeboje\">\r\n\t\t\t\t<span>\r\n\t\t\t\t    Tłumaczenie dodał(a): <a title=\"Tłumaczenie dodane 22.03.2020 przez tapcapslock\" href=\"profil,tapcapslock.html\">tapcapslock</a>\t\t\t\t</span>\r\n\t\t\t\t<a rel=\"loginbox\" href=\"/edytuj_tlumaczenie,24kgoldn,city_of_angels_1.html\" class=\"new\" title=\"Edytuj tłumaczenie\">Edytuj tłumaczenie</a>\r\n\t\t\t    </div>\r\n\t\t\t    <div class=\"box-przeboje no-bg\">\r\n\t\t\t\t<span>\r\n\t\t\t\t    Teledysk dodał(a): <a title=\"Teledysk dodany 02.04.2020 przez olcia_197\" href=\"profil,olcia_197.html\">olcia_197</a>\t\t\t\t</span>\r\n\t\t\t\t<a rel=\"loginbox\" href=\"javascript:editTeledisc('24kgoldn','city_of_angels_1', '');\" class=\"new\" title=\"Edytuj teledysk\">Edytuj teledysk</a>\r\n\t\t\t    </div>\r\n\t\t\t</div>\r\n\t\t    \t\t</div>\r\n\r\n\t\t<div class=\"col-lg px-md-0 mt-3 mt-lg-0\">\r\n\t\t    <div class=\"teledysk-right\">\r\n\t\t\t<div class=\"social\">\r\n\t\t\t    <label for=\"emb\">Skopiuj link:</label>\r\n\t\t\t    \t\t\t    \t\t\t</div>\r\n\t\t\t<fieldset class=\"mt-0 mb-2 mt-2 mt-lg-0\">\r\n\t\t\t    <input type=\"text\" id=\"emb\" name=\"emb\" readonly=\"\" value=\"https://www.tekstowo.pl/piosenka,24kgoldn,city_of_angels_1.html\" class=\"emb form-control\" onfocus=\"this.select();\" onclick=\"this.select();\r\n\t\t\t\t\t   document.execCommand('copy');\r\n\t\t\t\t\t   modalAlert('Udostępnianie tekstu', 'Link skopiowano do schowka!')\">\r\n\t\t\t</fieldset>\r\n\t\t\t\t\t\t    <div class=\"social mt-1 social-even\">\r\n\t\t\t\t<a class=\"btn btn-sm btn-fb share-pop\" href=\"https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fwww.tekstowo.pl%2Fpiosenka%2C24kgoldn%2Ccity_of_angels_1.html\"><i class=\"icon\"></i>&nbsp;Udostępnij</a>\r\n\t\t\t\t<a class=\"btn btn-sm btn-tweeter share-pop\" href=\"https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.tekstowo.pl%2Fpiosenka%2C24kgoldn%2Ccity_of_angels_1.html&amp;text=24kGoldn - City Of Angels\"><i class=\"icon\"></i>&nbsp;Tweetnij</a>\r\n\t\t\t\t<a class=\"btn btn-sm btn-messenger share-pop\" href=\"https://www.facebook.com/dialog/send?app_id=131858753537922&amp;link=https%3A%2F%2Fwww.tekstowo.pl%2Fpiosenka%2C24kgoldn%2Ccity_of_angels_1.html&amp;redirect_uri=https%3A%2F%2Fwww.tekstowo.pl%2Fpiosenka%2C24kgoldn%2Ccity_of_angels_1.html\"><i class=\"icon\"></i>&nbsp;Messenger</a>\r\n\t\t\t    </div>\r\n\t\t\t\t\t    </div>\r\n\t\t</div>\r\n\t    </div>\r\n\r\n\t    <div class=\"mt-3\">\r\n\t\t<!-- glosowanie -->\r\n\t\t<div class=\"glosowanie\">            \r\n\t\t    <div class=\"vote-group\">        \r\n\t\t\t<span>Głosuj na ten utwór</span>\r\n\t\t\t<div class=\"flex-group song-rank\" data-id=\"1817532\">\r\n    <a rel=\"loginbox\" title=\"ranking +\" href=\"javascript:ajxRankSong('Up',1817532);\" class=\"na-plus\"></a>\r\n    <a rel=\"loginbox\" title=\"ranking -\" href=\"javascript:ajxRankSong('Down', 1817532);\" class=\"na-minus\"></a>\r\n    <span class=\"rank\">(0)</span>\r\n</div>\t\t    </div>\r\n\r\n\t\t    <div class=\"action-group\">\r\n<a href=\"javascript:ajxAddFav('1817532')\" rel=\"loginbox\" class=\"glo-links glo-ulubione\" title=\"Dodaj do ulubionych\">Dodaj do ulubionych</a>\r\n<a href=\"#dodaj_komentarz\" rel=\"loginbox\" class=\"glo-links glo-komentuj\" title=\"Komentuj\">Komentuj</a>\r\n<a href=\"javascript:sendSongBox();\" rel=\"loginbox\" class=\"glo-links glo-polec\" title=\"Poleć znajomemu\">Poleć znajomemu</a>\r\n<a href=\"javascript:errorBox();\" rel=\"loginbox\" class=\"glo-links glo-zglos\" title=\"Zgłoś błąd\">Zgłoś błąd</a>\r\n</div>\t\t</div>\r\n\t\t\t    </div>\r\n\r\n\t    \t\t<!-- tekst -->\r\n\t\t<div class=\"row mx-0 mt-3\"> \r\n\t\t\r\n\t\r\n\t\t    <div class=\"col-12 d-lg-none\">\r\n\t\t\t<div class=\"text-right mb-2\">\r\n\t\t\t    <div class=\"custom-control custom-switch\">\r\n\t\t\t\t<input type=\"checkbox\" class=\"custom-control-input loginbox\" id=\"toggleColumnsSwitch\">\r\n\t\t\t\t<label class=\"custom-control-label\" for=\"toggleColumnsSwitch\">Układ 2-kolumnowy</label>\r\n\t\t\t    </div>\r\n\t\t\t</div>\r\n\t\t    </div>\r\n\t\t    <div class=\"col-lg\">\t\t\t\r\n\t\t\t<div class=\"song-text\" id=\"songText\" data-id=\"1817532\">                          \r\n\t\t\t    <h2 class=\"mb-2\">Tekst piosenki:</h2>\r\n\r\n\t\t\t    <div class=\"inner-text\">[Chorus]<br>\r\nI sold my soul to the devil for designer<br>\r\nThey said, \"Go to hell,\" but I told 'em I don’t wanna<br>\r\nIf you know me well, then you know that I ain't goin'<br>\r\n’Cause I don't wanna, I don't wanna<br>\r\nI don't wanna die young<br>\r\nThe city of angels where I have my fun<br>\r\nDon't wanna die young<br>\r\nWhen I'm gone, remember all I've done-one<br>\r\n<br>\r\n[Verse]<br>\r\nWe've had our fun-un<br>\r\nBut now I’m done-one<br>\r\n’Cause you crazy (Yeah), I can't take it (No)<br>\r\nJust wanted to see you naked<br>\r\nHeard time like money, can’t waste it<br>\r\nWhat's the price of fame? 'Cause I can taste it<br>\r\nSo I'm chasin’ (Yeah), and I'm facin'<br>\r\nA little Hennessy, it might be good for me<br>\r\n<br>\r\n[Chorus]<br>\r\nI sold my soul to the devil for designer<br>\r\nThey said, \"Go to hell,\" but I told 'em I don't wanna<br>\r\nIf you know me well, then you know that I ain't goin'<br>\r\n'Cause I don't wanna, I don't wanna<br>\r\nI don't wanna die young<br>\r\nThe city of angels where I have my fun<br>\r\nDon't wanna die young<br>\r\nWhen I'm gone, remember all I've done-one</div>\r\n\t\t\t    <a rel=\"loginbox\" class=\"btn-secondary btn-block btn-sm my-2 py-1 add-annotation\" href=\"javascript:;\">Dodaj interpretację do tego tekstu »</a>\r\n\r\n\r\n\t\t\t    \r\n\t\t\t    \t\t\t\t<div class=\"adv-home\"> <!-- reklama środek -->\r\n\t\t\t\t        \t    \r\n\r\n\r\n\r\n<script async=\"\" src=\"//www.statsforads.com/tag/d5e49d0e-64d6-4751-ae6c-eb53cd6568f6.min.js\"></script>\r\n\r\n\r\n\r\n<ins class=\"staticpubads89354\" data-sizes-desktop=\"300x250\" data-sizes-mobile=\"300x250\" data-slot=\"4\">\r\n</ins>\r\n\t\t\t\t</div>\r\n\t\t\t    \t\t\t    <p>&nbsp;</p>\r\n\t\t\t    \t\t\t    <a href=\"javascript:;\" id=\"song_revisions_link\" class=\"pokaz-rev\" song_id=\"1817532\">Historia edycji tekstu <span class=\"icon\"></span></a>\r\n\t\t\t    <div style=\"margin: 0px; position: static; overflow: hidden; height: 0px;\"><div id=\"song_revisions\" class=\"revisions\" style=\"margin: -14px 0px 0px; overflow: hidden;\"></div></div>\r\n\t\t\t    \t\t\t</div>\r\n\t\t    </div>\r\n\r\n\t\t    <div class=\"col-lg mt-3 mt-md-0\"> \t\t\t    \r\n\t\t\t<div class=\"tlumaczenie\" id=\"songTranslation\" data-id=\"1817532\">\r\n\t\t\t    <h2 class=\"mb-2\">Tłumaczenie:</h2>\r\n\t\t\t    \t\t\t\t<a href=\"javascript:void(0);\" class=\"pokaz-tlumaczenie\" id=\"a-show-tr\" title=\"Pokaż tłumaczenie\">Pokaż tłumaczenie</a>\t\t\t    \r\n\t\t\t    <div style=\"margin: 0px; position: static; overflow: hidden; height: 0px;\"><div id=\"translation\" class=\"id-707431\" data-id=\"707431\" style=\"margin: -710px 0px 0px; overflow: hidden;\">\r\n\t\t\t\t<div class=\"inner-text\">[Chór]<br>\r\nSprzedałem duszę diabłu za projektanta<br>\r\nPowiedzieli „Idź do piekła”, ale powiedziałem im, że nie chcę<br>\r\nJeśli dobrze mnie znasz, to wiesz, że nie idę<br>\r\nBo nie chcę, nie chcę<br>\r\nNie chcę umrzeć młodo<br>\r\nMiasto aniołów, w którym dobrze się bawię<br>\r\nNie chcę umrzeć młodo<br>\r\nKiedy odejdę, pamiętaj wszystko, co zrobiłem <br>\r\n<br>\r\n[Werset]<br>\r\nMieliśmy naszą zabawę<br>\r\nAle teraz skończyłem<br>\r\nBo jesteś szalony (Tak), nie mogę tego znieść (Nie)<br>\r\nChciałem tylko zobaczyć cię nago<br>\r\nSłyszałem czas jak pieniądze, nie można go marnować<br>\r\nJaka jest cena sławy? Bo mogę to posmakować<br>\r\nWięc chasin '(Yeah) i patrzę<br>\r\nTrochę Hennessy, może być dla mnie dobre<br>\r\n<br>\r\n[Chór]<br>\r\nSprzedałem duszę diabłu za projektanta<br>\r\nPowiedzieli „Idź do piekła”, ale powiedziałem im, że nie chcę<br>\r\nJeśli dobrze mnie znasz, to wiesz, że nie idę<br>\r\nBo nie chcę, nie chcę<br>\r\nNie chcę umrzeć młodo<br>\r\nMiasto aniołów, w którym dobrze się bawię<br>\r\nNie chcę umrzeć młodo<br>\r\nKiedy odejdę, pamiętaj wszystko, co zrobiłem</div>\r\n\t\t\t\t\r\n\t\t\t\t\t\t\t\t<p>&nbsp;</p>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t    <a href=\"javascript:;\" class=\"pokaz-rev\" id=\"trans_revisions_link\" song_id=\"1817532\">\r\n\t\t\t\t\tHistoria edycji tłumaczenia <span class=\"icon\"></span>\r\n\t\t\t\t    </a>\r\n\t\t\t\t    <div style=\"margin: 0px; position: static; overflow: hidden; height: 0px;\"><div id=\"trans_revisions\" class=\"revisions\" style=\"margin: -14px 0px 0px; overflow: hidden;\"></div></div>\r\n\t\t\t\t\t\t\t\t\t\t\t    </div></div>\r\n\r\n\t\t\t    \t\t\t\t<div class=\"adv-home\"> <!-- reklama środek -->\r\n\t\t\t\t        \t    \r\n\r\n    \r\n\r\n<script async=\"\" src=\"https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js\"></script>\r\n<!-- tekstowo kwadrat tłumaczenie elastyczna -->\r\n<ins class=\"adsbygoogle\" style=\"display:block\" data-ad-client=\"ca-pub-3653916656187385\" data-ad-slot=\"9521483524\" data-ad-format=\"auto\" data-full-width-responsive=\"true\"><iframe id=\"aswift_1\" style=\"height: 1px !important; max-height: 1px !important; max-width: 1px !important; width: 1px !important;\"><iframe id=\"google_ads_frame1\"></iframe></iframe></ins>\r\n<script>\r\n     (adsbygoogle = window.adsbygoogle || []).push({});\r\n</script>\r\n\t\t\t\t</div>  \r\n\t\t\t    \r\n\t\t\t</div>\r\n\t\t    </div>\r\n\t\t    <div class=\"col-12\">\r\n\t\t\t<a href=\"/drukuj,24kgoldn,city_of_angels_1.html\" class=\"drukuj blank btn btn-light2 my-3\" rel=\"nofollow\" title=\"Drukuj tekst\">\r\n\t\t\t    Drukuj tekst <span class=\"icon\"></span>\r\n\t\t\t</a>\r\n\t\t    </div>\r\n\t\t</div>\r\n\t\t<!-- end tekst -->\r\n\r\n\t\t<div class=\"row mx-0\">\r\n\t\t    <div class=\"col-lg px-md-0\">\r\n\t\t\t<div class=\"metric\">\r\n\t\t\t    <table>\r\n\t\t\t\t\t\t\t\t    <tbody><tr><th>Rok wydania:</th><td><p>2019 </p><a rel=\"loginbox\" href=\"/edytuj_tekst,24kgoldn,city_of_angels_1.html?metric=1\" class=\"edit btn btn-sm btn-primary\" title=\"Edytuj metrykę\">Edytuj metrykę</a></td></tr>\r\n\t\t\t\t\t\t\t\t\t    \t\t\t\t    <tr><th>Płyty:</th><td><p>Dropped Outta College </p></td></tr>\r\n\t\t\t\t\t\t\t\t\t    \t\t\t\t    \t\t\t</tbody></table>\r\n\t\t    </div>\r\n\t\t</div>\r\n\t    </div>\r\n\r\n\r\n\t    <!-- glosowanie -->\r\n\t    <div class=\"glosowanie mt-3\">            \r\n\t\t<div class=\"vote-group\">\r\n\t\t    <span>Głosuj na ten utwór</span>\r\n\t\t    <div class=\"flex-group song-rank\" data-id=\"1817532\">\r\n    <a rel=\"loginbox\" title=\"ranking +\" href=\"javascript:ajxRankSong('Up',1817532);\" class=\"na-plus\"></a>\r\n    <a rel=\"loginbox\" title=\"ranking -\" href=\"javascript:ajxRankSong('Down', 1817532);\" class=\"na-minus\"></a>\r\n    <span class=\"rank\">(0)</span>\r\n</div>\t\t</div>\r\n\r\n\t\t<div class=\"action-group\">\r\n<a href=\"javascript:ajxAddFav('1817532')\" rel=\"loginbox\" class=\"glo-links glo-ulubione\" title=\"Dodaj do ulubionych\">Dodaj do ulubionych</a>\r\n<a href=\"#dodaj_komentarz\" rel=\"loginbox\" class=\"glo-links glo-komentuj\" title=\"Komentuj\">Komentuj</a>\r\n<a href=\"javascript:sendSongBox();\" rel=\"loginbox\" class=\"glo-links glo-polec\" title=\"Poleć znajomemu\">Poleć znajomemu</a>\r\n<a href=\"javascript:errorBox();\" rel=\"loginbox\" class=\"glo-links glo-zglos\" title=\"Zgłoś błąd\">Zgłoś błąd</a>\r\n</div>\t    </div>\r\n\t    \t\t<!-- komentarze -->\r\n\t<div class=\"row mt-4 mx-0\">\r\n\t    <div class=\"col-12 px-md-0\">\r\n\t\t<a name=\"komentarze\"></a>\r\n\r\n\t\t<div id=\"comments_content\" class=\"d-none\">\r\n\t\t    <h2 class=\"margint10\">Komentarze (0):</h2>\r\n\t\t    \t\t    \t\t    \r\n\t\t</div>\r\n\r\n\t\t<div id=\"comm_show_more\" class=\"comm_show_more d-none\"></div>\r\n\t    </div>\r\n\r\n\t    <div class=\"col-12\">\r\n\t\t\t\t<noscript>\r\n</noscript>\r\n\t    </div>\r\n\t</div>\r\n\r\n\t<!-- end komentarze -->\r\n\r\n    </div>\r\n</div> <!-- end right column -->\r\n\r\n    <script type=\"text/javascript\">\r\n\t\r\n\t    function addLink() {\r\n\t\tvar body_element = document.getElementsByTagName('body')[0];\r\n\t\tvar selection;\r\n\t\tselection = window.getSelection();\r\n\t\tvar pagelink = \"<br /><br />Tekst pochodzi z <a href='\" + document.location.href + \"'>\" + document.location.href + \"</a>\";\r\n\t\tvar copytext = selection + pagelink;\r\n\t\tvar newdiv = document.createElement('div');\r\n\t\tnewdiv.style.position = 'absolute';\r\n\t\tnewdiv.style.left = '-99999px';\r\n\t\tbody_element.appendChild(newdiv);\r\n\t\tnewdiv.innerHTML = copytext;\r\n\t\tselection.selectAllChildren(newdiv);\r\n\t\twindow.setTimeout(function () {\r\n\t\t    body_element.removeChild(newdiv);\r\n\t\t}, 0);\r\n\t    }\r\n\t    document.getElementById('songText').oncopy = addLink;\r\n\t    document.getElementById('songTranslation').oncopy = addLink;\r\n\r\n\t\r\n    </script>\r\n\r\n    <script type=\"text/javascript\">\r\n\t\r\n\t    var i18nAnn = {\r\n\t\tadd: 'Dodaj',\r\n\t\tadd_title: 'Dodawanie interpretacji',\r\n\t\tadd_desc: 'Dodawanie interpretacji do zaznaczonego tekstu:',\r\n\t\tfunction_unavailble: 'Funkcja niedostępna na urządzeniach dotykowych. Skorzystaj z komputera klasy PC.',\r\n\t\tenabled: 'Uruchomiony został tryb dodawania interpretacji. Zaznacz fragment tekstu, który chcesz zinterpretować',\r\n\t\texit: 'Kliknij tutaj aby wyjść',\r\n\t\texists: 'Istnieje już interpretacja do tego fragmentu, zaznacz inny fragment tekstu',\r\n\t\ttoo_short: 'Treść jest za krótka, napisz coś więcej.',\r\n\t\ttextarea_placehodler: 'Tutaj pisz interpretację',\r\n\t\tsuccess_msg: 'Interpretacja została dodana i oczekuje akceptacji moderatora. Dziękujemy.',\r\n\t\tadded_msg: 'Dodałeś/aś już jedną interpretacje do tego utworu, która oczekuje akceptacji moderatora. <br>Poczekaj na akceptację.'\r\n\t    }\r\n\t\r\n    </script>\r\n</div>\r\n\r\n</div> <!-- end center -->\r\n</div>\r\n\r\n\r\n    <div class=\"generic_dialog outer\" id=\"fb-modal\" style=\"opacity: 0; display: none; visibility: hidden;\">\r\n\t<div class=\"generic_dialog_popup middle\">\r\n\t\t<table class=\"pop_dialog_table inner\" id=\"pop_dialog_table\">\r\n\t\t\t<tbody>\r\n\t\t\t\t<tr>\r\n\t\t\t\t\t<td id=\"pop_content\" class=\"pop_content\">\r\n\t\t\t\t\t\t<h2 class=\"dialog_title\"><span></span></h2>\r\n\t\t\t\t\t\t<div class=\"dialog_content\">\r\n\t\t\t\t\t\t\t<p id=\"modal-p\"></p>\r\n\t\t\t\t\t\t\t<div class=\"dialog_buttons\">\r\n\t\t\t\t\t\t\t\t<input type=\"button\" value=\"Zamknij\" name=\"close\" class=\"inputsubmit\" id=\"fb-close\">\r\n\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t</div>\r\n\t\t\t\t\t</td>\r\n\t\t\t\t</tr>\r\n\t\t\t</tbody>\r\n\t\t</table>\r\n\t</div>\r\n</div>    <div id=\"login-modal\">\r\n    <div style=\"position: relative\">\r\n\t<a id=\"yt-close\" href=\"javascript:modalFadeOut();\" title=\"Zamknij\"></a>\r\n    </div>\r\n    <div class=\"login_box\">\r\n\t<div class=\"row justify-content-md-center\">\r\n\t    <div class=\"col col-10\">\r\n\t\t<img src=\"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20441.8%20136%22%3E%3Cstyle%3E.st0%7Bfill%3A%2362ae25%7D.st1%7Bfill%3A%23999%7D%3C%2Fstyle%3E%3Cg%20id%3D%22logo_1_%22%3E%3Cpath%20class%3D%22st0%22%20d%3D%22M130.7%2046v9h6.5v6.2h-6.5v20.6c0%201.4.3%202.4.8%203.1.6.6%201.6.9%203%20.9%201%200%201.9-.1%202.9-.3v6.4c-1.9.5-3.7.8-5.5.8-6.4%200-9.6-3.5-9.6-10.6v-21h-6.1V55h6.1v-9h8.4zM156.3%2092.7c-5.3%200-9.5-1.7-12.8-5s-4.9-7.7-4.9-13.2v-1c0-3.7.7-7%202.1-9.9%201.4-2.9%203.4-5.2%206-6.8s5.4-2.4%208.6-2.4c5%200%208.9%201.6%2011.7%204.8%202.7%203.2%204.1%207.8%204.1%2013.6v3.3H147c.3%203.1%201.3%205.5%203.1%207.2%201.8%201.8%204%202.7%206.8%202.7%203.8%200%206.9-1.5%209.3-4.6l4.5%204.3c-1.5%202.2-3.5%203.9-5.9%205.1-2.6%201.3-5.4%201.9-8.5%201.9zm-1-31.7c-2.3%200-4.1.8-5.5%202.4-1.4%201.6-2.3%203.8-2.7%206.7h15.8v-.6c-.2-2.8-.9-4.9-2.2-6.3-1.3-1.5-3.1-2.2-5.4-2.2zM186.1%2076.1l-3.7%203.8V92h-8.3V39.5h8.3v30.3l2.6-3.2L195.2%2055h10l-13.7%2015.4L206.7%2092h-9.6l-11-15.9z%22%2F%3E%3Cpath%20class%3D%22st0%22%20d%3D%22M227.9%2082c0-1.5-.6-2.6-1.8-3.4s-3.2-1.5-6.1-2.1c-2.8-.6-5.2-1.3-7.1-2.3-4.1-2-6.2-4.9-6.2-8.7%200-3.2%201.3-5.9%204-8s6.1-3.2%2010.3-3.2c4.4%200%208%201.1%2010.7%203.3s4.1%205%204.1%208.5h-8.3c0-1.6-.6-2.9-1.8-4s-2.8-1.6-4.7-1.6c-1.8%200-3.3.4-4.5%201.3-1.2.8-1.7%202-1.7%203.4%200%201.3.5%202.3%201.6%203s3.2%201.4%206.5%202.1%205.8%201.6%207.7%202.6%203.2%202.2%204.1%203.6c.9%201.4%201.3%203.1%201.3%205.1%200%203.3-1.4%206-4.1%208.1-2.8%202.1-6.4%203.1-10.8%203.1-3%200-5.7-.5-8.1-1.6-2.4-1.1-4.2-2.6-5.5-4.5s-2-4-2-6.2h8.1c.1%202%20.9%203.5%202.2%204.5%201.4%201.1%203.2%201.6%205.4%201.6s3.9-.4%205-1.2c1.1-1%201.7-2.1%201.7-3.4zM250.2%2046v9h6.5v6.2h-6.5v20.6c0%201.4.3%202.4.8%203.1.6.6%201.6.9%203%20.9%201%200%201.9-.1%202.9-.3v6.4c-1.9.5-3.7.8-5.5.8-6.4%200-9.6-3.5-9.6-10.6v-21h-6.1V55h6.1v-9h8.4zM257%2073.2c0-3.6.7-6.9%202.2-9.8s3.5-5.1%206-6.7c2.6-1.6%205.6-2.3%209-2.3%205%200%209%201.6%2012.2%204.8%203.1%203.2%204.8%207.5%205%2012.8v1.9c0%203.6-.7%206.9-2.1%209.8s-3.4%205.1-6%206.7-5.6%202.4-9%202.4c-5.2%200-9.4-1.7-12.5-5.2s-4.7-8.1-4.7-13.9v-.5h-.1zm8.3.7c0%203.8.8%206.8%202.4%208.9%201.6%202.2%203.8%203.2%206.6%203.2s5-1.1%206.5-3.3c1.6-2.2%202.3-5.4%202.3-9.6%200-3.7-.8-6.7-2.4-8.9-1.6-2.2-3.8-3.3-6.5-3.3s-4.9%201.1-6.5%203.2c-1.6%202.3-2.4%205.5-2.4%209.8zM327.4%2080.5l5.9-25.5h8.1l-10.1%2037h-6.8l-7.9-25.4-7.9%2025.4h-6.8l-10.1-37h8.1l6%2025.3%207.6-25.3h6.3l7.6%2025.5zM341.8%2073.2c0-3.6.7-6.9%202.2-9.8s3.5-5.1%206-6.7c2.6-1.6%205.6-2.3%209-2.3%205%200%209%201.6%2012.2%204.8%203.1%203.2%204.8%207.5%205%2012.8v1.9c0%203.6-.7%206.9-2.1%209.8s-3.4%205.1-6%206.7-5.6%202.4-9%202.4c-5.2%200-9.4-1.7-12.5-5.2s-4.7-8.1-4.7-13.9v-.5h-.1zm8.3.7c0%203.8.8%206.8%202.4%208.9%201.6%202.2%203.8%203.2%206.6%203.2s5-1.1%206.5-3.3c1.6-2.2%202.3-5.4%202.3-9.6%200-3.7-.8-6.7-2.4-8.9-1.6-2.2-3.8-3.3-6.5-3.3s-4.9%201.1-6.5%203.2c-1.6%202.3-2.4%205.5-2.4%209.8zM384.9%2083.3c1.5%200%202.7.4%203.6%201.3.8.9%201.3%202%201.3%203.3s-.4%202.4-1.3%203.2c-.8.9-2%201.3-3.6%201.3-1.5%200-2.7-.4-3.5-1.3-.9-.8-1.3-1.9-1.3-3.2%200-1.3.4-2.4%201.3-3.3.8-.9%202-1.3%203.5-1.3zM428.2%2073.9c0%205.7-1.3%2010.3-3.9%2013.7s-6.1%205.1-10.5%205.1c-4.1%200-7.3-1.3-9.7-4v17.5h-8.3V55h7.7l.3%203.8c2.4-3%205.8-4.4%209.9-4.4%204.5%200%208%201.7%2010.6%205%202.6%203.4%203.8%208%203.8%2014v.5h.1zm-8.3-.7c0-3.7-.7-6.6-2.2-8.8-1.5-2.2-3.6-3.2-6.3-3.2-3.4%200-5.8%201.4-7.3%204.2v16.4c1.5%202.9%204%204.3%207.4%204.3%202.6%200%204.7-1.1%206.2-3.2%201.5-2.2%202.2-5.4%202.2-9.7zM440.5%2092h-8.3V39.5h8.3V92z%22%2F%3E%3Cpath%20class%3D%22st1%22%20d%3D%22M123.5%20107.6l-.5%202.8h2.1l-.3%201.5h-2.1l-1.2%207.2v.5c0%20.6.4.8.9.8.2%200%20.6%200%20.9-.1l-.2%201.6c-.5.1-.9.2-1.4.2-.8%200-1.4-.3-1.7-.9-.4-.6-.5-1.3-.5-2.2l1.2-7.2h-2.1l.3-1.5h2.1l.5-2.8%202%20.1zM129.3%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2.7.8%201.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.6.8-2.6.8zm1.3-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1.1-.8-1.8-.8zM141.8%20112.1l-.9-.1c-1.2%200-2.2.6-2.9%201.7l-1.4%208.3h-1.9l2-11.6h1.9l-.3%201.4c.8-1.1%201.8-1.6%202.9-1.6.2%200%20.5.1.9.2l-.3%201.7zM147.5%20122c-.1-.2-.1-.4-.1-.6v-.6c-1.1%201-2.2%201.5-3.4%201.4-1%200-1.9-.3-2.5-1-.6-.6-.9-1.4-.9-2.4.1-1.2.6-2.2%201.6-2.9%201-.7%202.3-1%204-1h2l.1-.9c.1-.7-.1-1.2-.4-1.6-.3-.4-.9-.6-1.5-.6-.7%200-1.3.1-1.8.5-.5.3-.8.8-.9%201.3h-2c.1-.7.3-1.3.8-1.8s1.1-.9%201.8-1.2c.7-.3%201.5-.4%202.3-.4%201.2%200%202.2.4%202.8%201.1.7.7%201%201.6.8%202.8l-.9%205.8-.1.8c0%20.4%200%20.8.1%201.2v.2h-1.8v-.1zm-3-1.5c.6%200%201.2-.1%201.8-.4s1-.7%201.4-1.3l.4-2.4h-1.5c-1.2%200-2.2.2-2.8.6-.7.4-1%20.9-1.1%201.6-.1.5.1%201%20.4%201.4.3.3.8.5%201.4.5zM153.2%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM168.8%20110.4l-.2%201.3c1-1%202.2-1.5%203.5-1.5.7%200%201.4.2%201.9.5s.9.8%201.1%201.4c1.1-1.3%202.4-1.9%203.9-1.9%201.2%200%202%20.4%202.6%201.2.6.8.8%201.8.6%203l-1.3%207.6H179l1.3-7.7v-1c-.1-1-.8-1.5-1.9-1.5-.7%200-1.4.2-1.9.7-.6.5-.9%201.1-1.1%201.8l-1.3%207.7h-2l1.3-7.6c.1-.8%200-1.4-.4-1.8s-.9-.7-1.6-.7c-1.2%200-2.2.5-2.9%201.7l-1.5%208.5h-1.9l2-11.6%201.7-.1zM189.1%20110.2c1%200%201.8.3%202.5.8s1.2%201.2%201.5%202.1.4%201.9.3%203v.2c-.1%201.1-.5%202.2-1%203.1s-1.2%201.6-2.1%202.1c-.9.5-1.8.7-2.8.7s-1.8-.3-2.5-.8-1.2-1.2-1.5-2.1-.4-1.9-.3-2.9c.1-1.2.4-2.3%201-3.2.5-1%201.2-1.7%202.1-2.2.8-.6%201.8-.9%202.8-.8zm-4%206.2c-.1.5-.1.9%200%201.4.1.8.3%201.5.8%202%20.4.5%201%20.8%201.7.8.6%200%201.2-.1%201.8-.5.5-.3%201-.9%201.4-1.5.4-.7.6-1.5.7-2.3.1-.7.1-1.2%200-1.7-.1-.9-.3-1.6-.8-2.1-.4-.5-1-.8-1.7-.8-1%200-1.9.4-2.6%201.2-.7.8-1.1%201.9-1.3%203.2v.3zM195.8%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zm3.2-13c0-.3.1-.6.3-.8.2-.2.5-.3.8-.4.3%200%20.6.1.8.3.2.2.3.5.3.8s-.1.6-.3.8c-.2.2-.5.3-.8.3s-.6-.1-.8-.3c-.2-.1-.3-.4-.3-.7zM208.1%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2.7.8%201.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1-.8-1.8-.8zM220.6%20118.9c.1-.8-.3-1.4-1.3-1.7l-2-.6c-1.7-.6-2.5-1.6-2.5-2.9%200-1%20.5-1.9%201.4-2.5.9-.7%201.9-1%203.1-1%201.2%200%202.1.4%202.9%201%20.7.7%201.1%201.5%201.1%202.6h-1.9c0-.6-.2-1.1-.5-1.4s-.9-.6-1.5-.6c-.7%200-1.3.2-1.7.5-.5.3-.7.7-.8%201.3-.1.7.3%201.2%201.2%201.5l1%20.3c1.3.3%202.3.8%202.8%201.3s.8%201.2.8%202.1c0%20.7-.3%201.4-.7%201.9s-1%20.9-1.7%201.2c-.7.3-1.5.4-2.3.4-1.2%200-2.2-.4-3.1-1.1-.8-.7-1.2-1.6-1.2-2.7h1.9c0%20.7.2%201.2.6%201.6.4.4%201%20.6%201.7.6s1.3-.1%201.8-.4c.5-.5.8-.9.9-1.4zM225.5%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM241.3%20110.4l-2.2%2013c-.1%201.1-.5%201.9-1.1%202.5s-1.4.9-2.3.8c-.4%200-.8-.1-1.3-.2l.2-1.6c.3.1.6.1.9.1.9%200%201.5-.6%201.7-1.7l2.2-13%201.9.1zm-1.6-3.1c0-.3.1-.6.3-.8.2-.2.5-.3.8-.4.3%200%20.6.1.8.3.2.2.3.5.3.8s-.1.6-.3.8-.5.3-.8.3-.6-.1-.8-.3-.3-.4-.3-.7zM246.4%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2s1.1%201.8%201.1%203.1v1.4l-.1.9H244c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1-.8-1.8-.8zM266.5%20116.2c-.1%201.2-.5%202.2-1%203.2s-1.1%201.7-1.8%202.1c-.7.5-1.5.7-2.4.7-1.3%200-2.4-.5-3.1-1.4l-1%205.6h-1.9l2.8-16.1h1.8l-.2%201.3c1-1%202.1-1.5%203.4-1.5%201.1%200%202%20.4%202.6%201.2s1%201.9%201%203.3c0%20.5%200%20.9-.1%201.3l-.1.3zm-1.9-.2l.1-.9c0-1-.2-1.8-.6-2.4s-1-.8-1.7-.9c-1.1%200-2.1.5-2.9%201.6l-1%205.6c.4%201%201.2%201.6%202.4%201.6%201%200%201.8-.4%202.5-1.1.5-.8.9-2%201.2-3.5zM274.2%20112.1l-.9-.1c-1.2%200-2.2.6-2.9%201.7L269%20122h-1.9l2-11.6h1.9l-.3%201.4c.8-1.1%201.8-1.6%202.9-1.6.2%200%20.5.1.9.2l-.3%201.7zM275.3%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM287.6%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2s1.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.4-.5-1-.8-1.8-.8zM297.8%20120.6c.7%200%201.3-.2%201.9-.6s.9-1%201.1-1.6h1.8c-.1.7-.4%201.4-.9%202s-1.1%201.1-1.8%201.4-1.5.5-2.2.5c-1%200-1.8-.3-2.5-.8s-1.2-1.2-1.5-2.1-.4-1.9-.3-3l.1-.5c.1-1.1.5-2.1%201-3s1.2-1.6%202-2.1%201.7-.7%202.8-.7c1.2%200%202.2.4%202.9%201.2s1.1%201.8%201.1%203h-1.8c0-.8-.2-1.4-.6-1.8-.4-.5-1-.7-1.7-.7-1%200-1.8.4-2.5%201.1-.7.8-1.1%201.9-1.3%203.2v1.7c0%20.8.3%201.5.7%202s1%20.8%201.7.8zM305.2%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM316.8%20119.1l4.1-8.7h2.1l-6.9%2013.6c-1%201.9-2.2%202.8-3.7%202.8-.3%200-.7-.1-1.2-.2l.2-1.6.5.1c.6%200%201.1-.1%201.6-.4.4-.3.8-.8%201.2-1.5l.7-1.3-2-11.4h2l1.4%208.6z%22%2F%3E%3Cpath%20class%3D%22st1%22%20d%3D%22M326.9%20107.6l-.5%202.8h2.1l-.3%201.5h-2.1l-1.2%207.2v.5c0%20.6.4.8.9.8.2%200%20.6%200%20.9-.1l-.2%201.6c-.5.1-.9.2-1.4.2-.8%200-1.4-.3-1.7-.9-.4-.6-.5-1.3-.5-2.2l1.2-7.2H322l.3-1.5h2.1l.5-2.8%202%20.1zM334.8%20122c0-.2-.1-.4-.1-.6v-.6c-1.1%201-2.2%201.5-3.4%201.4-1%200-1.9-.3-2.5-1-.6-.6-.9-1.4-.9-2.4.1-1.2.6-2.2%201.6-2.9%201-.7%202.3-1%204-1h2l.1-.9c.1-.7-.1-1.2-.4-1.6s-.9-.6-1.5-.6c-.7%200-1.3.1-1.8.5-.5.3-.8.8-.9%201.3h-2c.1-.7.3-1.3.8-1.8s1.1-.9%201.8-1.2c.7-.3%201.5-.4%202.3-.4%201.2%200%202.2.4%202.8%201.1.7.7%201%201.6.8%202.8l-.9%205.8-.1.8c0%20.4%200%20.8.1%201.2v.2h-1.8v-.1zm-3-1.5c.6%200%201.2-.1%201.8-.4.6-.3%201-.7%201.4-1.3l.4-2.4h-1.5c-1.2%200-2.2.2-2.8.6s-1%20.9-1.1%201.6c-.1.5.1%201%20.4%201.4.3.3.8.5%201.4.5zM343.3%20120.6c.7%200%201.3-.2%201.9-.6s.9-1%201.1-1.6h1.8c-.1.7-.4%201.4-.9%202s-1.1%201.1-1.8%201.4-1.5.5-2.2.5c-1%200-1.8-.3-2.5-.8s-1.2-1.2-1.5-2.1-.4-1.9-.3-3l.1-.5c.1-1.1.5-2.1%201-3s1.2-1.6%202-2.1%201.7-.7%202.8-.7c1.2%200%202.2.4%202.9%201.2s1.1%201.8%201.1%203H347c0-.8-.2-1.4-.6-1.8-.4-.5-1-.7-1.7-.7-1%200-1.8.4-2.5%201.1-.7.8-1.1%201.9-1.3%203.2v1.7c0%20.8.3%201.5.7%202s.9.8%201.7.8zm2.7-15.1h2.5l-3.3%203.2h-1.7l2.5-3.2z%22%2F%3E%3Cpath%20id%3D%22box%22%20d%3D%22M92.9%20124H7.1c-3.9%200-7.1-3.2-7.1-7.1V31.1C0%2027.2%203.2%2024%207.1%2024h85.8c3.9%200%207.1%203.2%207.1%207.1v85.8c0%203.9-3.2%207.1-7.1%207.1z%22%20fill%3D%22%23f60%22%2F%3E%3Cpath%20d%3D%22M68%2044.1c-6.8-7-15.9-12.2-24.6-14.4C28.8%2026%2025%2023.9%2017.8%2019c-4.1-2.8-7.2-8.4-10.6-9.8-1.6-.6-2.6.1-3.1.7-.9%201-1.6%202.9-.4%205.2%2010.1%2019.8%2048.4%2076.8%2048.4%2076.8-5.9%201.1-12.4%205.7-17%2012.9-6.9%2010.7-7%2022.9-.1%2027.2%206.9%204.3%2018.1-.9%2025-11.6%205.9-9%206.8-19.1%202.8-24.6L29.2%2042.7s-3.9-4.7%202.3-6.1c4.5-1%2013.9.1%2019%202.8C56%2042.3%2069%2051.7%2072.3%2065.1c1.5%206.2.6%209.6%201.4%2015.1.4%202.6%203.2%204.8%205.5%201.2%201.2-1.9%201.4-8.4%201.1-13.5-.5-6.7-6-17.3-12.3-23.8zm-10.4%2048c.3.1.7.2%201%20.3-.3-.1-.6-.2-1-.3zm-2.5-.4h.4-.4zm1.1.1c.3%200%20.6.1.8.1-.2%200-.5-.1-.8-.1zm-3%200c.1%200%20.1%200%200%200%20.1%200%20.1%200%200%200zm5.8.8l1.2.6c-.4-.3-.8-.5-1.2-.6zm2.9%202zm-.7-.7c-.3-.3-.6-.5-1-.7.3.2.6.5%201%20.7z%22%20opacity%3D%22.2%22%2F%3E%3ClinearGradient%20id%3D%22SVGID_1_%22%20gradientUnits%3D%22userSpaceOnUse%22%20x1%3D%2239.888%22%20y1%3D%2292.284%22%20x2%3D%2252.096%22%20y2%3D%22125.824%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%238fe132%22%2F%3E%3Cstop%20offset%3D%22.362%22%20stop-color%3D%22%2365bd23%22%2F%3E%3Cstop%20offset%3D%22.668%22%20stop-color%3D%22%2349a519%22%2F%3E%3Cstop%20offset%3D%22.844%22%20stop-color%3D%22%233e9c15%22%2F%3E%3C%2FlinearGradient%3E%3Cpath%20d%3D%22M57.9%20117.8c-7.6%2010.4-19.2%2014.9-25.7%2010.1s-5.7-17.2%201.9-27.6%2019.2-14.9%2025.7-10.1c6.6%204.9%205.7%2017.2-1.9%2027.6z%22%20fill%3D%22url%28%23SVGID_1_%29%22%2F%3E%3ClinearGradient%20id%3D%22SVGID_2_%22%20gradientUnits%3D%22userSpaceOnUse%22%20x1%3D%22-3.574%22%20y1%3D%2224.316%22%20x2%3D%2282.773%22%20y2%3D%2274.169%22%3E%3Cstop%20offset%3D%22.184%22%20stop-color%3D%22%238fe132%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%233e9c15%22%2F%3E%3C%2FlinearGradient%3E%3Cpath%20d%3D%22M70.9%2041c-6.3-7.6-15-13.4-23.6-16.2-14.4-4.7-18-7.1-24.8-12.5-3.9-3.1-6.6-9-9.9-10.7-1.6-.8-2.6-.1-3.2.5-.9.9-1.8%202.8-.7%205.2C17.4%2028%2051.9%2088.4%2051.9%2088.4c3-.3%205.7.2%207.9%201.8%201%20.8%201.8%201.7%202.5%202.8l-30-56s-3.6-5%202.7-6c4.5-.7%2013.9%201.1%2018.8%204.1%205.3%203.3%2017.6%2013.7%2020%2027.5%201.1%206.4%200%209.7.4%2015.4.2%202.6%202.9%205.1%205.4%201.6%201.3-1.8%202-8.4%202-13.6%200-6.8-4.9-17.9-10.7-25z%22%20fill%3D%22url%28%23SVGID_2_%29%22%2F%3E%3CradialGradient%20id%3D%22SVGID_3_%22%20cx%3D%2221.505%22%20cy%3D%22103.861%22%20r%3D%2214.934%22%20gradientTransform%3D%22matrix%28.2966%20.4025%20-.805%20.5933%20123.22%2029.092%29%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%23f4ff72%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%2373c928%22%20stop-opacity%3D%220%22%2F%3E%3C%2FradialGradient%3E%3Cpath%20d%3D%22M50.4%20105.4c-6.6%204.9-14%206.2-16.5%202.8-2.4-3.3%201-10%207.6-14.9s14-6.2%2016.5-2.8c2.5%203.3-.9%2010-7.6%2014.9z%22%20fill%3D%22url%28%23SVGID_3_%29%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E\" class=\"img-fluid\" alt=\"tekstowo.pl\" title=\"Teksty piosenek, tłumaczenia, teledyski\">\t    </div>\r\n\t</div>\r\n\t<div class=\"login-wrap\">\r\n\t    <fieldset class=\"login rejestracja edycja okienko\">\r\n\r\n\t\t<div class=\"row\">\r\n\t\t    <div class=\"col text-center\">\r\n\t\t\t<p><strong>Aby wykonać tę operację należy się zalogować:</strong></p>\r\n\t\t    </div>\r\n\t\t</div>\r\n\r\n\t\t<div class=\"l-box\">\r\n\t\t    <div class=\"formRow\">\r\n\t\t\tZaloguj się przy użyciu loginu i hasła:\r\n\t\t\t<br>\r\n\t\t\t<a class=\"green-button btn btn-primary btn-lg btn-block mt-1\" href=\"https://www.tekstowo.pl/logowanie.html\">Zaloguj</a>\r\n\t\t\t<div class=\"row my-2 mx-0\">\r\n\t\t\t    <div class=\"col-sm\">\r\n\t\t\t\t<a href=\"/rejestracja.html\" class=\"green underline bold\" title=\"Rejestracja\">Rejestracja</a>\r\n\t\t\t    </div>\r\n\t\t\t    <div class=\"col-auto\">\r\n\t\t\t\t<a href=\"/przypomnij.html\" class=\"green underline bold marginl10\" title=\"Przypomnienie hasła\">Przypomnienie hasła</a>\r\n\t\t\t    </div>\r\n\t\t\t</div>\r\n\t\t    </div>\r\n\r\n\t\t    <div class=\"fb-box mt-3\">Inne metody logowania:\r\n\t\t\t<a href=\"javascript:;\" class=\"fb-button my-fb-login-button btn btn-block btn-fb btn-lg mt-1\" title=\"Zaloguj się z Facebookiem\">\r\n\t\t\t    <span class=\"icon\"></span>Zaloguj się przez Facebooka</a>\r\n\t\t    </div>\r\n\t    </div></fieldset>\r\n\t</div>\r\n    </div>\r\n</div>\r\n\r\n    <div id=\"sendsong-modal\" style=\"display: none;\">\r\n    <div>\r\n\t<a id=\"yt-close\" href=\"javascript:modalFadeOut();\" title=\"Zamknij\"></a>\r\n    </div>\r\n    <div class=\"login_box\">\r\n\r\n\t<form id=\"formSendSongInvite\" action=\"\" onsubmit=\"ajxSendSongInvite();\r\n\t\treturn false;\" method=\"post\">\r\n\r\n\t    <div class=\"row\">\r\n\t\t<div class=\"col text-center lead my-3\">\r\n\t\t    Podaj adres E-mail znajomego, któremu chcesz polecić ten utwór.\r\n\t\t</div>\r\n\t    </div>\r\n\r\n\t    \t    \r\n\t    <div class=\"form-group row\">\r\n\t\t<label for=\"invite_email\" class=\"col-sm-2 col-form-label\">E-mail:</label>\r\n\t\t<div class=\"col-sm-10\">\r\n\t\t    <input type=\"text\" class=\"form-control\" name=\"invite_email\" id=\"invite_email\" value=\"\">\r\n\t\t    <div class=\"invalid-feedback\" id=\"error_invemail\" style=\"display: none\">\r\n\t\t\tPodany E-mail jest nieprawidłowy.\r\n\t\t    </div>\r\n\t\t</div>\r\n\t    </div>\r\n\r\n\t    <div class=\"form-group row\">\r\n\t\t<div class=\"col d-flex justify-content-center mt-3\">\r\n\t\t    <input type=\"hidden\" name=\"song_id\" value=\"1817532\">\r\n\t\t    <button type=\"submit\" class=\"btn btn-primary mr-3 px-5\">Wyślij</button>\r\n\t\t    <button type=\"button\" onclick=\"modalFadeOut();\" class=\"btn btn-secondary px-5\">Anuluj</button>\r\n\t\t</div>\r\n\t    </div>\r\n\t</form>\r\n    </div>\r\n</div>\r\n<div class=\"container order-3\">\r\n    <div id=\"bottom\" class=\"row\">\r\n        <div id=\"stopka\" class=\"col\">\r\n\r\n            \t    <div class=\"row footbar py-3 my-3 d-md-none\">\r\n\t\t<div class=\"col text-center\">\r\n\t\t    2 170 744 tekstów, 20 217 poszukiwanych i 501 oczekujących\r\n\t\t</div>\r\n\t    </div>\r\n            <hr>\r\n            <p>Największy serwis z tekstami piosenek w Polsce. Każdy może znaleźć u nas teksty piosenek, teledyski oraz tłumaczenia swoich ulubionych utworów.<br> Zachęcamy wszystkich użytkowników do dodawania nowych tekstów, tłumaczeń i teledysków! </p>\r\n            <hr>\r\n            <a href=\"/reklama.html\" class=\"bottom-links\" title=\"Reklama\">Reklama</a> |\r\n            <a href=\"/kontakt.html\" class=\"bottom-links\" title=\"Kontakt\">Kontakt</a> |\r\n            <a href=\"/faq.html\" class=\"bottom-links\" title=\"FAQ\">FAQ</a>\r\n            <a href=\"/polityka-prywatnosci.html\" class=\"bottom-links\" title=\"Polityka prywatności\">Polityka prywatności</a>\r\n        </div>\r\n    </div>\r\n</div><!-- end container -->\r\n\r\n<div style=\"color: white; text-align: center; background-color: white; width: 100%; margin: 0 auto;\"><span id=\"debug\"></span></div>\r\n\r\n<div id=\"spinner\" style=\"display:none;\">\r\n    <div id=\"spinner\" class=\"spinner-grow text-primary\" role=\"status\">\r\n\t<span class=\"sr-only\">Proszę czekać...</span>\r\n    </div>\r\n</div>\r\n\r\n<div class=\" d-none d-md-block fb-panel \">\r\n    <a href=\"https://www.facebook.com/tekstowo/\" target=\"_blank\" class=\"slide_button\"></a>\r\n    <div class=\"fb\"></div>\r\n</div>\r\n\r\n    \t    \r\n\r\n    \r\n\r\n<script type=\"text/javascript\">\r\n\r\n  var _gaq = _gaq || [];\r\n  _gaq.push(['_setAccount', 'UA-261303-4']);\r\n  _gaq.push(['_trackPageview']);\r\n  _gaq.push(['_trackPageLoadTime']); \r\n  \r\n  (function() {\r\n    var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;\r\n    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';\r\n    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);\r\n  })();\r\n\r\n</script>\r\n\r\n<script src=\"//s1.adform.net/banners/scripts/adx.js\" async=\"\" defer=\"\"></script>\r\n\r\n<script async=\"\" src=\"//www.statsforads.com/tag/d5e49d0e-64d6-4751-ae6c-eb53cd6568f6.min.js\"></script>\r\n\r\n\r\n  <script type=\"text/javascript\" src=\"https://lib.ads4g.pl/publisher/maxart/91c4f3e3d35dc73f574b.js\" async=\"\"></script>\r\n\r\n  \r\n\r\n<div id=\"fb-root\" class=\" fb_reset\"><div style=\"position: absolute; top: -10000px; width: 0px; height: 0px;\"><div></div></div></div>\r\n\r\n\r\n<!-- bootstrap -->\r\n<script defer=\"\" src=\"https://code.jquery.com/jquery-3.5.1.min.js\" integrity=\"sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=\" crossorigin=\"anonymous\"></script>\r\n<script defer=\"\" src=\"https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.bundle.min.js\" integrity=\"sha384-1CmrxMRARb6aLqgBO7yyAxTOQE2AKb9GfXnEo760AUcUmFx3ibVJJAzGytlQcNXd\" crossorigin=\"anonymous\"></script>\r\n<script defer=\"\" type=\"text/javascript\" src=\"/static/v2/js/bs4/bootstrap-autocomplete.min.js\"></script>\r\n<!-- end of bootstrap -->\r\n\r\n<script defer=\"\" type=\"text/javascript\" src=\"/static/js/mootools-core-1.6.0.min.js\"></script>\r\n<script defer=\"\" type=\"text/javascript\" src=\"/static/js/mootools-more-1.6.0.min.js\"></script>\r\n<script defer=\"\" type=\"text/javascript\" src=\"/static/js/pulse.min.js\"></script>\r\n<script defer=\"\" type=\"text/javascript\" src=\"/static/v2/js/app.js?v=220105\"></script>\r\n<script defer=\"\" type=\"text/javascript\" src=\"/static/js/main-4.js?v=211027\"></script>\r\n<!--polyfills-->\r\n<script async=\"\" type=\"text/javascript\" src=\"/static/v2/js/loading-attribute-polyfill.min.js\"></script>\r\n\r\n    <script async=\"\" defer=\"\" src=\"https://connect.facebook.net/pl_PL/sdk.js\"></script>\r\n<script>\r\n    \r\n\twindow.addEventListener('DOMContentLoaded', () => {\r\n\t    if ($defined(window.asyncEventHoler))\r\n\t    {\r\n\t\tfor (var i = 0; i < window.asyncEventHoler.length; i++) {\r\n\t\t    var o = window.asyncEventHoler[i];\r\n\t\t    window.addEvent(o.event, o.fn);\r\n\t\t}\r\n\t\tdelete window.asyncEventHoler;\r\n\t    }\r\n\t});\r\n    \r\n</script>\r\n\r\n    \t        \r\n\r\n</body></html>"
  },
  {
    "path": "test/rsrc/lyrics/tekstowopl/piosenkabaileybiggerblackeyedsusan.txt",
    "content": "<!DOCTYPE html>\r\n<html lang=\"pl\" prefix=\"og: http://ogp.me/ns#\" itemscope=\"\" itemtype=\"http://schema.org/Article\" slick-uniqueid=\"3\"><head>\r\n\t<!-- Required meta tags -->\r\n\t<meta charset=\"utf-8\">\r\n\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\r\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\r\n\r\n\t                \t<title>Bailey Bigger - Black Eyed Susan - tekst i tłumaczenie piosenki na Tekstowo.pl</title>\r\n\t<meta name=\"Description\" content=\"Bailey Bigger - Black Eyed Susan - tekst piosenki, tłumaczenie piosenki i teledysk. Zobacz słowa utworu Black Eyed Susan wraz z teledyskiem i tłumaczeniem.\">\r\n\t<meta name=\"Keywords\" content=\"Black Eyed Susan, Bailey Bigger, tekst, słowa, tłumaczenie, tekst piosenki, słowa piosenki, tłumaczenie piosenki, teledysk\">\r\n\t<meta name=\"revisit-after\" content=\"12 hours\">\r\n\t<meta http-equiv=\"Content-language\" content=\"pl\">\r\n\t<meta name=\"robots\" content=\"INDEX, FOLLOW\">\r\n\t<link rel=\"manifest\" href=\"/manifest.webmanifest\">\r\n        <link rel=\"search\" type=\"application/opensearchdescription+xml\" title=\"Tekstowo: Po tytule piosenki\" href=\"https://www.tekstowo.pl/piosenki_osd.xml\">\r\n        <link rel=\"search\" type=\"application/opensearchdescription+xml\" title=\"Tekstowo: Po tytule soundtracka\" href=\"https://www.tekstowo.pl/soundtracki_osd.xml\">\r\n\r\n\t\t<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin=\"\">\t\r\n\t<link rel=\"preload\" as=\"style\" href=\"https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700;900&amp;display=swap\">\r\n\t<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700;900&amp;display=swap\" media=\"all\" onload=\"this.media = 'all'\">\r\n\r\n\t\t<link rel=\"preconnect\" href=\"https://cdn.jsdelivr.net\" crossorigin=\"\">\r\n\t<link rel=\"preconnect\" href=\"https://ssl.google-analytics.com\" crossorigin=\"\">\r\n\t\t    <link rel=\"preconnect\" href=\"https://ls.hit.gemius.pl\" crossorigin=\"\">\r\n\t    <link rel=\"preconnect\" href=\"https://get.optad360.io\" crossorigin=\"\">\r\n\t    <link rel=\"preconnect\" href=\"https://www.google.com\" crossorigin=\"\">\r\n\t    <link rel=\"preconnect\" href=\"https://adservice.google.com\" crossorigin=\"\">\r\n\t    <link rel=\"preconnect\" href=\"https://tpc.googlesyndication.com\" crossorigin=\"\">\r\n\t    <link rel=\"preconnect\" href=\"https://connect.facebook.net\" crossorigin=\"\">\r\n\t\r\n\r\n\t\t<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/v2/css/glyphs.css?v=201216\" media=\"all\" onload=\"this.media = 'all'\">\r\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/v2/css/main.css?v=220210\">\r\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/v2/css/media/main-print.css?v=220210\" media=\"print\">\r\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/v2/css/media/main-md.css?v=220210\" media=\"screen and (min-width: 768px)\">\r\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/v2/css/media/main-lg.css?v=220210\" media=\"screen and (min-width: 992px)\">\r\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/v2/css/media/main-xl.css?v=220210\" media=\"screen and (min-width: 1200px)\">\r\n\t<!-- generics -->\r\n\t<link rel=\"icon\" href=\"https://www.tekstowo.pl/static/v2/images/favicons/icon-32.png\" sizes=\"32x32\">\r\n\t<link rel=\"icon\" href=\"https://www.tekstowo.pl/static/v2/images/favicons/icon-128.png\" sizes=\"128x128\">\r\n\t<link rel=\"icon\" href=\"https://www.tekstowo.pl/static/v2/images/favicons/icon-192.png\" sizes=\"192x192\">\r\n\r\n\t<!-- Android -->\r\n\t<link rel=\"shortcut icon\" href=\"https://www.tekstowo.pl/static/v2/images/favicons/icon-196.png\" sizes=\"196x196\">\r\n\r\n\t<!-- iOS -->\r\n\t<link rel=\"apple-touch-icon\" href=\"https://www.tekstowo.pl/static/v2/images/favicons/icon-152.png\" sizes=\"152x152\">\r\n\t<link rel=\"apple-touch-icon\" href=\"https://www.tekstowo.pl/static/v2/images/favicons/icon-167.png\" sizes=\"167x167\">\r\n\t<link rel=\"apple-touch-icon\" href=\"https://www.tekstowo.pl/static/v2/images/favicons/icon-180.png\" sizes=\"180x180\">\r\n\r\n\t<meta name=\"msapplication-config\" content=\"/browserconfig.xml\">\r\n\r\n\t\t    <link rel=\"canonical\" href=\"https://www.tekstowo.pl/piosenka,bailey_bigger,black_eyed_susan.html\">\r\n\t    <meta property=\"og:title\" content=\"Bailey Bigger - Black Eyed Susan - tekst piosenki na Tekstowo.pl\">\r\n\t    \t\t<meta property=\"og:image\" content=\"https://www.tekstowo.pl/miniatura_teledysku,bX4gXCOtrkI.jpg\">\r\n\t    \t    <meta property=\"og:url\" content=\"https://www.tekstowo.pl/piosenka,bailey_bigger,black_eyed_susan.html\">\r\n\t    <meta property=\"og:type\" content=\"website\">\r\n\t    <meta name=\"twitter:card\" content=\"summary_large_image\">\r\n\t    <meta name=\"twitter:title\" content=\"Bailey Bigger - Black Eyed Susan\">\r\n\t    \t\t<meta name=\"twitter:image\" content=\"https://www.tekstowo.pl/miniatura_teledysku,bX4gXCOtrkI.jpg\">\r\n\t    \t    \r\n\t    \r\n\t    <meta name=\"twitter:description\" content=\"Bailey Bigger - Black Eyed Susan - tekst piosenki, tłumaczenie piosenki i teledysk. Zobacz słowa utworu Black Eyed Susan wraz z teledyskiem i tłumaczeniem.\">\r\n\t    <meta property=\"og:description\" content=\"Bailey Bigger - Black Eyed Susan - tekst piosenki, tłumaczenie piosenki i teledysk. Zobacz słowa utworu Black Eyed Susan wraz z teledyskiem i tłumaczeniem.\">\r\n\t    \t\t<meta itemprop=\"name\" content=\"Bailey Bigger - Black Eyed Susan - tekst piosenki na Tekstowo.pl\">\r\n\t\t<meta itemprop=\"description\" content=\"Bailey Bigger - Black Eyed Susan - tekst piosenki, tłumaczenie piosenki i teledysk. Zobacz słowa utworu Black Eyed Susan wraz z teledyskiem i tłumaczeniem.\">\r\n\t\t\t\t    <meta itemprop=\"image\" content=\"https://www.tekstowo.pl/miniatura_teledysku,bX4gXCOtrkI.jpg\">\r\n\t\t\t    \t\r\n    <meta property=\"og:site_name\" content=\"Tekstowo.pl\">\r\n    <meta property=\"fb:app_id\" content=\"131858753537922\">\r\n\r\n\r\n    <script src=\"https://connect.facebook.net/pl_PL/sdk.js?hash=85583b32560f14af5837c0fa700e803e\" async=\"\" crossorigin=\"anonymous\"></script><script type=\"text/javascript\" async=\"\" src=\"https://ssl.google-analytics.com/ga.js\"></script><script type=\"text/javascript\">\r\n\t\t\t    var ytProxy = '//filmiki4.maxart.pl/ytp.php';\r\n\t    </script>\r\n    <script>\r\n\t\t\r\n\t    window.addEvent = function (event, fn) {\r\n\t\tif (window.asyncEventHoler == undefined)\r\n\t\t{\r\n\t\t    window.asyncEventHoler = [];\r\n\t\t}\r\n\r\n\t\twindow.asyncEventHoler.push({\r\n\t\t    'event': event,\r\n\t\t    'fn': fn\r\n\t\t});\r\n\t    }\r\n\t\r\n    </script>\r\n\t<script async=\"\" src=\"//cmp.optad360.io/items/6a750fad-191a-4309-bcd6-81e330cb392d.min.js\"></script>  \r\n\t\r\n<style type=\"text/css\" data-fbcssmodules=\"css:fb.css.base css:fb.css.dialog css:fb.css.iframewidget css:fb.css.customer_chat_plugin_iframe\">.fb_hidden{position:absolute;top:-10000px;z-index:10001}.fb_reposition{overflow:hidden;position:relative}.fb_invisible{display:none}.fb_reset{background:none;border:0;border-spacing:0;color:#000;cursor:auto;direction:ltr;font-family:'lucida grande', tahoma, verdana, arial, sans-serif;font-size:11px;font-style:normal;font-variant:normal;font-weight:normal;letter-spacing:normal;line-height:1;margin:0;overflow:visible;padding:0;text-align:left;text-decoration:none;text-indent:0;text-shadow:none;text-transform:none;visibility:visible;white-space:normal;word-spacing:normal}.fb_reset>div{overflow:hidden}@keyframes fb_transform{from{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.fb_animate{animation:fb_transform .3s forwards}\r\n.fb_hidden{position:absolute;top:-10000px;z-index:10001}.fb_reposition{overflow:hidden;position:relative}.fb_invisible{display:none}.fb_reset{background:none;border:0;border-spacing:0;color:#000;cursor:auto;direction:ltr;font-family:'lucida grande', tahoma, verdana, arial, sans-serif;font-size:11px;font-style:normal;font-variant:normal;font-weight:normal;letter-spacing:normal;line-height:1;margin:0;overflow:visible;padding:0;text-align:left;text-decoration:none;text-indent:0;text-shadow:none;text-transform:none;visibility:visible;white-space:normal;word-spacing:normal}.fb_reset>div{overflow:hidden}@keyframes fb_transform{from{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.fb_animate{animation:fb_transform .3s forwards}\r\n.fb_dialog{background:rgba(82, 82, 82, .7);position:absolute;top:-10000px;z-index:10001}.fb_dialog_advanced{border-radius:8px;padding:10px}.fb_dialog_content{background:#fff;color:#373737}.fb_dialog_close_icon{background:url(https://connect.facebook.net/rsrc.php/v3/yq/r/IE9JII6Z1Ys.png) no-repeat scroll 0 0 transparent;cursor:pointer;display:block;height:15px;position:absolute;right:18px;top:17px;width:15px}.fb_dialog_mobile .fb_dialog_close_icon{left:5px;right:auto;top:5px}.fb_dialog_padding{background-color:transparent;position:absolute;width:1px;z-index:-1}.fb_dialog_close_icon:hover{background:url(https://connect.facebook.net/rsrc.php/v3/yq/r/IE9JII6Z1Ys.png) no-repeat scroll 0 -15px transparent}.fb_dialog_close_icon:active{background:url(https://connect.facebook.net/rsrc.php/v3/yq/r/IE9JII6Z1Ys.png) no-repeat scroll 0 -30px transparent}.fb_dialog_iframe{line-height:0}.fb_dialog_content .dialog_title{background:#6d84b4;border:1px solid #365899;color:#fff;font-size:14px;font-weight:bold;margin:0}.fb_dialog_content .dialog_title>span{background:url(https://connect.facebook.net/rsrc.php/v3/yd/r/Cou7n-nqK52.gif) no-repeat 5px 50%;float:left;padding:5px 0 7px 26px}body.fb_hidden{height:100%;left:0;margin:0;overflow:visible;position:absolute;top:-10000px;transform:none;width:100%}.fb_dialog.fb_dialog_mobile.loading{background:url(https://connect.facebook.net/rsrc.php/v3/ya/r/3rhSv5V8j3o.gif) white no-repeat 50% 50%;min-height:100%;min-width:100%;overflow:hidden;position:absolute;top:0;z-index:10001}.fb_dialog.fb_dialog_mobile.loading.centered{background:none;height:auto;min-height:initial;min-width:initial;width:auto}.fb_dialog.fb_dialog_mobile.loading.centered #fb_dialog_loader_spinner{width:100%}.fb_dialog.fb_dialog_mobile.loading.centered .fb_dialog_content{background:none}.loading.centered #fb_dialog_loader_close{clear:both;color:#fff;display:block;font-size:18px;padding-top:20px}#fb-root #fb_dialog_ipad_overlay{background:rgba(0, 0, 0, .4);bottom:0;left:0;min-height:100%;position:absolute;right:0;top:0;width:100%;z-index:10000}#fb-root #fb_dialog_ipad_overlay.hidden{display:none}.fb_dialog.fb_dialog_mobile.loading iframe{visibility:hidden}.fb_dialog_mobile .fb_dialog_iframe{position:sticky;top:0}.fb_dialog_content .dialog_header{background:linear-gradient(from(#738aba), to(#2c4987));border-bottom:1px solid;border-color:#043b87;box-shadow:white 0 1px 1px -1px inset;color:#fff;font:bold 14px Helvetica, sans-serif;text-overflow:ellipsis;text-shadow:rgba(0, 30, 84, .296875) 0 -1px 0;vertical-align:middle;white-space:nowrap}.fb_dialog_content .dialog_header table{height:43px;width:100%}.fb_dialog_content .dialog_header td.header_left{font-size:12px;padding-left:5px;vertical-align:middle;width:60px}.fb_dialog_content .dialog_header td.header_right{font-size:12px;padding-right:5px;vertical-align:middle;width:60px}.fb_dialog_content .touchable_button{background:linear-gradient(from(#4267B2), to(#2a4887));background-clip:padding-box;border:1px solid #29487d;border-radius:3px;display:inline-block;line-height:18px;margin-top:3px;max-width:85px;padding:4px 12px;position:relative}.fb_dialog_content .dialog_header .touchable_button input{background:none;border:none;color:#fff;font:bold 12px Helvetica, sans-serif;margin:2px -12px;padding:2px 6px 3px 6px;text-shadow:rgba(0, 30, 84, .296875) 0 -1px 0}.fb_dialog_content .dialog_header .header_center{color:#fff;font-size:16px;font-weight:bold;line-height:18px;text-align:center;vertical-align:middle}.fb_dialog_content .dialog_content{background:url(https://connect.facebook.net/rsrc.php/v3/y9/r/jKEcVPZFk-2.gif) no-repeat 50% 50%;border:1px solid #4a4a4a;border-bottom:0;border-top:0;height:150px}.fb_dialog_content .dialog_footer{background:#f5f6f7;border:1px solid #4a4a4a;border-top-color:#ccc;height:40px}#fb_dialog_loader_close{float:left}.fb_dialog.fb_dialog_mobile .fb_dialog_close_icon{visibility:hidden}#fb_dialog_loader_spinner{animation:rotateSpinner 1.2s linear infinite;background-color:transparent;background-image:url(https://connect.facebook.net/rsrc.php/v3/yD/r/t-wz8gw1xG1.png);background-position:50% 50%;background-repeat:no-repeat;height:24px;width:24px}@keyframes rotateSpinner{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}\r\n.fb_iframe_widget{display:inline-block;position:relative}.fb_iframe_widget span{display:inline-block;position:relative;text-align:justify}.fb_iframe_widget iframe{position:absolute}.fb_iframe_widget_fluid_desktop,.fb_iframe_widget_fluid_desktop span,.fb_iframe_widget_fluid_desktop iframe{max-width:100%}.fb_iframe_widget_fluid_desktop iframe{min-width:220px;position:relative}.fb_iframe_widget_lift{z-index:1}.fb_iframe_widget_fluid{display:inline}.fb_iframe_widget_fluid span{width:100%}\r\n.fb_mpn_mobile_landing_page_slide_out{animation-duration:200ms;animation-name:fb_mpn_landing_page_slide_out;transition-timing-function:ease-in}.fb_mpn_mobile_landing_page_slide_out_from_left{animation-duration:200ms;animation-name:fb_mpn_landing_page_slide_out_from_left;transition-timing-function:ease-in}.fb_mpn_mobile_landing_page_slide_up{animation-duration:500ms;animation-name:fb_mpn_landing_page_slide_up;transition-timing-function:ease-in}.fb_mpn_mobile_bounce_in{animation-duration:300ms;animation-name:fb_mpn_bounce_in;transition-timing-function:ease-in}.fb_mpn_mobile_bounce_out{animation-duration:300ms;animation-name:fb_mpn_bounce_out;transition-timing-function:ease-in}.fb_mpn_mobile_bounce_out_v2{animation-duration:300ms;animation-name:fb_mpn_fade_out;transition-timing-function:ease-in}.fb_customer_chat_bounce_in_v2{animation-duration:300ms;animation-name:fb_bounce_in_v2;transition-timing-function:ease-in}.fb_customer_chat_bounce_in_from_left{animation-duration:300ms;animation-name:fb_bounce_in_from_left;transition-timing-function:ease-in}.fb_customer_chat_bounce_out_v2{animation-duration:300ms;animation-name:fb_bounce_out_v2;transition-timing-function:ease-in}.fb_customer_chat_bounce_out_from_left{animation-duration:300ms;animation-name:fb_bounce_out_from_left;transition-timing-function:ease-in}.fb_invisible_flow{display:inherit;height:0;overflow-x:hidden;width:0}@keyframes fb_mpn_landing_page_slide_out{0%{margin:0 12px;width:100% - 24px}60%{border-radius:18px}100%{border-radius:50%;margin:0 24px;width:60px}}@keyframes fb_mpn_landing_page_slide_out_from_left{0%{left:12px;width:100% - 24px}60%{border-radius:18px}100%{border-radius:50%;left:12px;width:60px}}@keyframes fb_mpn_landing_page_slide_up{0%{bottom:0;opacity:0}100%{bottom:24px;opacity:1}}@keyframes fb_mpn_bounce_in{0%{opacity:.5;top:100%}100%{opacity:1;top:0}}@keyframes fb_mpn_fade_out{0%{bottom:30px;opacity:1}100%{bottom:0;opacity:0}}@keyframes fb_mpn_bounce_out{0%{opacity:1;top:0}100%{opacity:.5;top:100%}}@keyframes fb_bounce_in_v2{0%{opacity:0;transform:scale(0, 0);transform-origin:bottom right}50%{transform:scale(1.03, 1.03);transform-origin:bottom right}100%{opacity:1;transform:scale(1, 1);transform-origin:bottom right}}@keyframes fb_bounce_in_from_left{0%{opacity:0;transform:scale(0, 0);transform-origin:bottom left}50%{transform:scale(1.03, 1.03);transform-origin:bottom left}100%{opacity:1;transform:scale(1, 1);transform-origin:bottom left}}@keyframes fb_bounce_out_v2{0%{opacity:1;transform:scale(1, 1);transform-origin:bottom right}100%{opacity:0;transform:scale(0, 0);transform-origin:bottom right}}@keyframes fb_bounce_out_from_left{0%{opacity:1;transform:scale(1, 1);transform-origin:bottom left}100%{opacity:0;transform:scale(0, 0);transform-origin:bottom left}}@keyframes slideInFromBottom{0%{opacity:.1;transform:translateY(100%)}100%{opacity:1;transform:translateY(0)}}@keyframes slideInFromBottomDelay{0%{opacity:0;transform:translateY(100%)}97%{opacity:0;transform:translateY(100%)}100%{opacity:1;transform:translateY(0)}}</style></head>\r\n<body class=\"\">\r\n    \r\n\r\n    <nav class=\"navbar navbar-expand-lg navbar-dark sticky-top d-md-none\" id=\"navbar\">\r\n\t<div class=\"container\" id=\"navbarMobile\">\r\n\t    <a href=\"/\" class=\"navbar-brand logo\">\r\n\t\t\t\t<img src=\"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20441.8%20136%22%3E%3Cpath%20fill%3D%22%23FFF%22%20d%3D%22M130.7%2046v9h6.5v6.2h-6.5v20.6c0%201.4.3%202.4.8%203.1.6.6%201.6.9%203%20.9%201%200%201.9-.1%202.9-.3v6.4c-1.9.5-3.7.8-5.5.8-6.4%200-9.6-3.5-9.6-10.6v-21h-6.1V55h6.1v-9h8.4zM156.3%2092.7c-5.3%200-9.5-1.7-12.8-5s-4.9-7.7-4.9-13.2v-1c0-3.7.7-7%202.1-9.9%201.4-2.9%203.4-5.2%206-6.8s5.4-2.4%208.6-2.4c5%200%208.9%201.6%2011.7%204.8%202.7%203.2%204.1%207.8%204.1%2013.6v3.3H147c.3%203.1%201.3%205.5%203.1%207.2%201.8%201.8%204%202.7%206.8%202.7%203.8%200%206.9-1.5%209.3-4.6l4.5%204.3c-1.5%202.2-3.5%203.9-5.9%205.1-2.6%201.3-5.4%201.9-8.5%201.9zm-1-31.7c-2.3%200-4.1.8-5.5%202.4-1.4%201.6-2.3%203.8-2.7%206.7h15.8v-.6c-.2-2.8-.9-4.9-2.2-6.3-1.3-1.5-3.1-2.2-5.4-2.2zM186.1%2076.1l-3.7%203.8V92h-8.3V39.5h8.3v30.3l2.6-3.2L195.2%2055h10l-13.7%2015.4L206.7%2092h-9.6l-11-15.9z%22%2F%3E%3Cpath%20fill%3D%22%23FFF%22%20d%3D%22M227.9%2082c0-1.5-.6-2.6-1.8-3.4s-3.2-1.5-6.1-2.1c-2.8-.6-5.2-1.3-7.1-2.3-4.1-2-6.2-4.9-6.2-8.7%200-3.2%201.3-5.9%204-8s6.1-3.2%2010.3-3.2c4.4%200%208%201.1%2010.7%203.3s4.1%205%204.1%208.5h-8.3c0-1.6-.6-2.9-1.8-4s-2.8-1.6-4.7-1.6c-1.8%200-3.3.4-4.5%201.3-1.2.8-1.7%202-1.7%203.4%200%201.3.5%202.3%201.6%203s3.2%201.4%206.5%202.1%205.8%201.6%207.7%202.6%203.2%202.2%204.1%203.6c.9%201.4%201.3%203.1%201.3%205.1%200%203.3-1.4%206-4.1%208.1-2.8%202.1-6.4%203.1-10.8%203.1-3%200-5.7-.5-8.1-1.6-2.4-1.1-4.2-2.6-5.5-4.5s-2-4-2-6.2h8.1c.1%202%20.9%203.5%202.2%204.5%201.4%201.1%203.2%201.6%205.4%201.6s3.9-.4%205-1.2c1.1-1%201.7-2.1%201.7-3.4zM250.2%2046v9h6.5v6.2h-6.5v20.6c0%201.4.3%202.4.8%203.1.6.6%201.6.9%203%20.9%201%200%201.9-.1%202.9-.3v6.4c-1.9.5-3.7.8-5.5.8-6.4%200-9.6-3.5-9.6-10.6v-21h-6.1V55h6.1v-9h8.4zM257%2073.2c0-3.6.7-6.9%202.2-9.8s3.5-5.1%206-6.7c2.6-1.6%205.6-2.3%209-2.3%205%200%209%201.6%2012.2%204.8%203.1%203.2%204.8%207.5%205%2012.8v1.9c0%203.6-.7%206.9-2.1%209.8s-3.4%205.1-6%206.7-5.6%202.4-9%202.4c-5.2%200-9.4-1.7-12.5-5.2s-4.7-8.1-4.7-13.9v-.5h-.1zm8.3.7c0%203.8.8%206.8%202.4%208.9%201.6%202.2%203.8%203.2%206.6%203.2s5-1.1%206.5-3.3c1.6-2.2%202.3-5.4%202.3-9.6%200-3.7-.8-6.7-2.4-8.9-1.6-2.2-3.8-3.3-6.5-3.3s-4.9%201.1-6.5%203.2c-1.6%202.3-2.4%205.5-2.4%209.8zM327.4%2080.5l5.9-25.5h8.1l-10.1%2037h-6.8l-7.9-25.4-7.9%2025.4h-6.8l-10.1-37h8.1l6%2025.3%207.6-25.3h6.3l7.6%2025.5zM341.8%2073.2c0-3.6.7-6.9%202.2-9.8s3.5-5.1%206-6.7c2.6-1.6%205.6-2.3%209-2.3%205%200%209%201.6%2012.2%204.8%203.1%203.2%204.8%207.5%205%2012.8v1.9c0%203.6-.7%206.9-2.1%209.8s-3.4%205.1-6%206.7-5.6%202.4-9%202.4c-5.2%200-9.4-1.7-12.5-5.2s-4.7-8.1-4.7-13.9v-.5h-.1zm8.3.7c0%203.8.8%206.8%202.4%208.9%201.6%202.2%203.8%203.2%206.6%203.2s5-1.1%206.5-3.3c1.6-2.2%202.3-5.4%202.3-9.6%200-3.7-.8-6.7-2.4-8.9-1.6-2.2-3.8-3.3-6.5-3.3s-4.9%201.1-6.5%203.2c-1.6%202.3-2.4%205.5-2.4%209.8zM384.9%2083.3c1.5%200%202.7.4%203.6%201.3.8.9%201.3%202%201.3%203.3s-.4%202.4-1.3%203.2c-.8.9-2%201.3-3.6%201.3-1.5%200-2.7-.4-3.5-1.3-.9-.8-1.3-1.9-1.3-3.2%200-1.3.4-2.4%201.3-3.3.8-.9%202-1.3%203.5-1.3zM428.2%2073.9c0%205.7-1.3%2010.3-3.9%2013.7s-6.1%205.1-10.5%205.1c-4.1%200-7.3-1.3-9.7-4v17.5h-8.3V55h7.7l.3%203.8c2.4-3%205.8-4.4%209.9-4.4%204.5%200%208%201.7%2010.6%205%202.6%203.4%203.8%208%203.8%2014v.5h.1zm-8.3-.7c0-3.7-.7-6.6-2.2-8.8-1.5-2.2-3.6-3.2-6.3-3.2-3.4%200-5.8%201.4-7.3%204.2v16.4c1.5%202.9%204%204.3%207.4%204.3%202.6%200%204.7-1.1%206.2-3.2%201.5-2.2%202.2-5.4%202.2-9.7zM440.5%2092h-8.3V39.5h8.3V92zM123.5%20107.6l-.5%202.8h2.1l-.3%201.5h-2.1l-1.2%207.2v.5c0%20.6.4.8.9.8.2%200%20.6%200%20.9-.1l-.2%201.6c-.5.1-.9.2-1.4.2-.8%200-1.4-.3-1.7-.9-.4-.6-.5-1.3-.5-2.2l1.2-7.2h-2.1l.3-1.5h2.1l.5-2.8%202%20.1zM129.3%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2.7.8%201.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.6.8-2.6.8zm1.3-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1.1-.8-1.8-.8zM141.8%20112.1l-.9-.1c-1.2%200-2.2.6-2.9%201.7l-1.4%208.3h-1.9l2-11.6h1.9l-.3%201.4c.8-1.1%201.8-1.6%202.9-1.6.2%200%20.5.1.9.2l-.3%201.7zM147.5%20122c-.1-.2-.1-.4-.1-.6v-.6c-1.1%201-2.2%201.5-3.4%201.4-1%200-1.9-.3-2.5-1-.6-.6-.9-1.4-.9-2.4.1-1.2.6-2.2%201.6-2.9%201-.7%202.3-1%204-1h2l.1-.9c.1-.7-.1-1.2-.4-1.6-.3-.4-.9-.6-1.5-.6-.7%200-1.3.1-1.8.5-.5.3-.8.8-.9%201.3h-2c.1-.7.3-1.3.8-1.8s1.1-.9%201.8-1.2c.7-.3%201.5-.4%202.3-.4%201.2%200%202.2.4%202.8%201.1.7.7%201%201.6.8%202.8l-.9%205.8-.1.8c0%20.4%200%20.8.1%201.2v.2h-1.8v-.1zm-3-1.5c.6%200%201.2-.1%201.8-.4s1-.7%201.4-1.3l.4-2.4h-1.5c-1.2%200-2.2.2-2.8.6-.7.4-1%20.9-1.1%201.6-.1.5.1%201%20.4%201.4.3.3.8.5%201.4.5zM153.2%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM168.8%20110.4l-.2%201.3c1-1%202.2-1.5%203.5-1.5.7%200%201.4.2%201.9.5s.9.8%201.1%201.4c1.1-1.3%202.4-1.9%203.9-1.9%201.2%200%202%20.4%202.6%201.2.6.8.8%201.8.6%203l-1.3%207.6H179l1.3-7.7v-1c-.1-1-.8-1.5-1.9-1.5-.7%200-1.4.2-1.9.7-.6.5-.9%201.1-1.1%201.8l-1.3%207.7h-2l1.3-7.6c.1-.8%200-1.4-.4-1.8s-.9-.7-1.6-.7c-1.2%200-2.2.5-2.9%201.7l-1.5%208.5h-1.9l2-11.6%201.7-.1zM189.1%20110.2c1%200%201.8.3%202.5.8s1.2%201.2%201.5%202.1.4%201.9.3%203v.2c-.1%201.1-.5%202.2-1%203.1s-1.2%201.6-2.1%202.1c-.9.5-1.8.7-2.8.7s-1.8-.3-2.5-.8-1.2-1.2-1.5-2.1-.4-1.9-.3-2.9c.1-1.2.4-2.3%201-3.2.5-1%201.2-1.7%202.1-2.2.8-.6%201.8-.9%202.8-.8zm-4%206.2c-.1.5-.1.9%200%201.4.1.8.3%201.5.8%202%20.4.5%201%20.8%201.7.8.6%200%201.2-.1%201.8-.5.5-.3%201-.9%201.4-1.5.4-.7.6-1.5.7-2.3.1-.7.1-1.2%200-1.7-.1-.9-.3-1.6-.8-2.1-.4-.5-1-.8-1.7-.8-1%200-1.9.4-2.6%201.2-.7.8-1.1%201.9-1.3%203.2v.3zM195.8%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zm3.2-13c0-.3.1-.6.3-.8.2-.2.5-.3.8-.4.3%200%20.6.1.8.3.2.2.3.5.3.8s-.1.6-.3.8c-.2.2-.5.3-.8.3s-.6-.1-.8-.3c-.2-.1-.3-.4-.3-.7zM208.1%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2.7.8%201.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1-.8-1.8-.8zM220.6%20118.9c.1-.8-.3-1.4-1.3-1.7l-2-.6c-1.7-.6-2.5-1.6-2.5-2.9%200-1%20.5-1.9%201.4-2.5.9-.7%201.9-1%203.1-1%201.2%200%202.1.4%202.9%201%20.7.7%201.1%201.5%201.1%202.6h-1.9c0-.6-.2-1.1-.5-1.4s-.9-.6-1.5-.6c-.7%200-1.3.2-1.7.5-.5.3-.7.7-.8%201.3-.1.7.3%201.2%201.2%201.5l1%20.3c1.3.3%202.3.8%202.8%201.3s.8%201.2.8%202.1c0%20.7-.3%201.4-.7%201.9s-1%20.9-1.7%201.2c-.7.3-1.5.4-2.3.4-1.2%200-2.2-.4-3.1-1.1-.8-.7-1.2-1.6-1.2-2.7h1.9c0%20.7.2%201.2.6%201.6.4.4%201%20.6%201.7.6s1.3-.1%201.8-.4c.5-.5.8-.9.9-1.4zM225.5%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM241.3%20110.4l-2.2%2013c-.1%201.1-.5%201.9-1.1%202.5s-1.4.9-2.3.8c-.4%200-.8-.1-1.3-.2l.2-1.6c.3.1.6.1.9.1.9%200%201.5-.6%201.7-1.7l2.2-13%201.9.1zm-1.6-3.1c0-.3.1-.6.3-.8.2-.2.5-.3.8-.4.3%200%20.6.1.8.3.2.2.3.5.3.8s-.1.6-.3.8-.5.3-.8.3-.6-.1-.8-.3-.3-.4-.3-.7zM246.4%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2s1.1%201.8%201.1%203.1v1.4l-.1.9H244c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1-.8-1.8-.8zM266.5%20116.2c-.1%201.2-.5%202.2-1%203.2s-1.1%201.7-1.8%202.1c-.7.5-1.5.7-2.4.7-1.3%200-2.4-.5-3.1-1.4l-1%205.6h-1.9l2.8-16.1h1.8l-.2%201.3c1-1%202.1-1.5%203.4-1.5%201.1%200%202%20.4%202.6%201.2s1%201.9%201%203.3c0%20.5%200%20.9-.1%201.3l-.1.3zm-1.9-.2l.1-.9c0-1-.2-1.8-.6-2.4s-1-.8-1.7-.9c-1.1%200-2.1.5-2.9%201.6l-1%205.6c.4%201%201.2%201.6%202.4%201.6%201%200%201.8-.4%202.5-1.1.5-.8.9-2%201.2-3.5zM274.2%20112.1l-.9-.1c-1.2%200-2.2.6-2.9%201.7L269%20122h-1.9l2-11.6h1.9l-.3%201.4c.8-1.1%201.8-1.6%202.9-1.6.2%200%20.5.1.9.2l-.3%201.7zM275.3%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM287.6%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2s1.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.4-.5-1-.8-1.8-.8zM297.8%20120.6c.7%200%201.3-.2%201.9-.6s.9-1%201.1-1.6h1.8c-.1.7-.4%201.4-.9%202s-1.1%201.1-1.8%201.4-1.5.5-2.2.5c-1%200-1.8-.3-2.5-.8s-1.2-1.2-1.5-2.1-.4-1.9-.3-3l.1-.5c.1-1.1.5-2.1%201-3s1.2-1.6%202-2.1%201.7-.7%202.8-.7c1.2%200%202.2.4%202.9%201.2s1.1%201.8%201.1%203h-1.8c0-.8-.2-1.4-.6-1.8-.4-.5-1-.7-1.7-.7-1%200-1.8.4-2.5%201.1-.7.8-1.1%201.9-1.3%203.2v1.7c0%20.8.3%201.5.7%202s1%20.8%201.7.8zM305.2%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM316.8%20119.1l4.1-8.7h2.1l-6.9%2013.6c-1%201.9-2.2%202.8-3.7%202.8-.3%200-.7-.1-1.2-.2l.2-1.6.5.1c.6%200%201.1-.1%201.6-.4.4-.3.8-.8%201.2-1.5l.7-1.3-2-11.4h2l1.4%208.6z%22%2F%3E%3Cpath%20fill%3D%22%23FFF%22%20d%3D%22M326.9%20107.6l-.5%202.8h2.1l-.3%201.5h-2.1l-1.2%207.2v.5c0%20.6.4.8.9.8.2%200%20.6%200%20.9-.1l-.2%201.6c-.5.1-.9.2-1.4.2-.8%200-1.4-.3-1.7-.9-.4-.6-.5-1.3-.5-2.2l1.2-7.2H322l.3-1.5h2.1l.5-2.8%202%20.1zM334.8%20122c0-.2-.1-.4-.1-.6v-.6c-1.1%201-2.2%201.5-3.4%201.4-1%200-1.9-.3-2.5-1-.6-.6-.9-1.4-.9-2.4.1-1.2.6-2.2%201.6-2.9%201-.7%202.3-1%204-1h2l.1-.9c.1-.7-.1-1.2-.4-1.6s-.9-.6-1.5-.6c-.7%200-1.3.1-1.8.5-.5.3-.8.8-.9%201.3h-2c.1-.7.3-1.3.8-1.8s1.1-.9%201.8-1.2c.7-.3%201.5-.4%202.3-.4%201.2%200%202.2.4%202.8%201.1.7.7%201%201.6.8%202.8l-.9%205.8-.1.8c0%20.4%200%20.8.1%201.2v.2h-1.8v-.1zm-3-1.5c.6%200%201.2-.1%201.8-.4.6-.3%201-.7%201.4-1.3l.4-2.4h-1.5c-1.2%200-2.2.2-2.8.6s-1%20.9-1.1%201.6c-.1.5.1%201%20.4%201.4.3.3.8.5%201.4.5zM343.3%20120.6c.7%200%201.3-.2%201.9-.6s.9-1%201.1-1.6h1.8c-.1.7-.4%201.4-.9%202s-1.1%201.1-1.8%201.4-1.5.5-2.2.5c-1%200-1.8-.3-2.5-.8s-1.2-1.2-1.5-2.1-.4-1.9-.3-3l.1-.5c.1-1.1.5-2.1%201-3s1.2-1.6%202-2.1%201.7-.7%202.8-.7c1.2%200%202.2.4%202.9%201.2s1.1%201.8%201.1%203H347c0-.8-.2-1.4-.6-1.8-.4-.5-1-.7-1.7-.7-1%200-1.8.4-2.5%201.1-.7.8-1.1%201.9-1.3%203.2v1.7c0%20.8.3%201.5.7%202s.9.8%201.7.8zm2.7-15.1h2.5l-3.3%203.2h-1.7l2.5-3.2z%22%2F%3E%3Cpath%20fill%3D%22%23F60%22%20d%3D%22M92.9%20124H7.1c-3.9%200-7.1-3.2-7.1-7.1V31.1C0%2027.2%203.2%2024%207.1%2024h85.8c3.9%200%207.1%203.2%207.1%207.1v85.8c0%203.9-3.2%207.1-7.1%207.1z%22%2F%3E%3Cpath%20opacity%3D%22.2%22%20d%3D%22M68%2044.1c-6.8-7-15.9-12.2-24.6-14.4C28.8%2026%2025%2023.9%2017.8%2019c-4.1-2.8-7.2-8.4-10.6-9.8-1.6-.6-2.6.1-3.1.7-.9%201-1.6%202.9-.4%205.2%2010.1%2019.8%2048.4%2076.8%2048.4%2076.8-5.9%201.1-12.4%205.7-17%2012.9-6.9%2010.7-7%2022.9-.1%2027.2%206.9%204.3%2018.1-.9%2025-11.6%205.9-9%206.8-19.1%202.8-24.6L29.2%2042.7s-3.9-4.7%202.3-6.1c4.5-1%2013.9.1%2019%202.8C56%2042.3%2069%2051.7%2072.3%2065.1c1.5%206.2.6%209.6%201.4%2015.1.4%202.6%203.2%204.8%205.5%201.2%201.2-1.9%201.4-8.4%201.1-13.5-.5-6.7-6-17.3-12.3-23.8zm-10.4%2048c.3.1.7.2%201%20.3-.3-.1-.6-.2-1-.3zm-2.5-.4h.4-.4zm1.1.1c.3%200%20.6.1.8.1-.2%200-.5-.1-.8-.1zm-3%200c.1%200%20.1%200%200%200%20.1%200%20.1%200%200%200zm5.8.8l1.2.6c-.4-.3-.8-.5-1.2-.6zm2.9%202zm-.7-.7c-.3-.3-.6-.5-1-.7.3.2.6.5%201%20.7z%22%2F%3E%3ClinearGradient%20id%3D%22a%22%20gradientUnits%3D%22userSpaceOnUse%22%20x1%3D%2239.902%22%20y1%3D%2245.741%22%20x2%3D%2252.109%22%20y2%3D%2212.201%22%20gradientTransform%3D%22matrix%281%200%200%20-1%200%20138%29%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%238fe132%22%2F%3E%3Cstop%20offset%3D%22.362%22%20stop-color%3D%22%2365bd23%22%2F%3E%3Cstop%20offset%3D%22.668%22%20stop-color%3D%22%2349a519%22%2F%3E%3Cstop%20offset%3D%22.844%22%20stop-color%3D%22%233e9c15%22%2F%3E%3C%2FlinearGradient%3E%3Cpath%20fill%3D%22url%28%23a%29%22%20d%3D%22M57.9%20117.8c-7.6%2010.4-19.2%2014.9-25.7%2010.1s-5.7-17.2%201.9-27.6%2019.2-14.9%2025.7-10.1c6.6%204.9%205.7%2017.2-1.9%2027.6z%22%2F%3E%3ClinearGradient%20id%3D%22b%22%20gradientUnits%3D%22userSpaceOnUse%22%20x1%3D%22-3.575%22%20y1%3D%22113.687%22%20x2%3D%2282.758%22%20y2%3D%2263.843%22%20gradientTransform%3D%22matrix%281%200%200%20-1%200%20138%29%22%3E%3Cstop%20offset%3D%22.184%22%20stop-color%3D%22%238fe132%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%233e9c15%22%2F%3E%3C%2FlinearGradient%3E%3Cpath%20fill%3D%22url%28%23b%29%22%20d%3D%22M70.9%2041c-6.3-7.6-15-13.4-23.6-16.2-14.4-4.7-18-7.1-24.8-12.5-3.9-3.1-6.6-9-9.9-10.7-1.6-.8-2.6-.1-3.2.5-.9.9-1.8%202.8-.7%205.2C17.4%2028%2051.9%2088.4%2051.9%2088.4c3-.3%205.7.2%207.9%201.8%201%20.8%201.8%201.7%202.5%202.8l-30-56s-3.6-5%202.7-6c4.5-.7%2013.9%201.1%2018.8%204.1%205.3%203.3%2017.6%2013.7%2020%2027.5%201.1%206.4%200%209.7.4%2015.4.2%202.6%202.9%205.1%205.4%201.6%201.3-1.8%202-8.4%202-13.6%200-6.8-4.9-17.9-10.7-25z%22%2F%3E%3CradialGradient%20id%3D%22c%22%20cx%3D%22426.647%22%20cy%3D%22271.379%22%20r%3D%2214.948%22%20gradientTransform%3D%22matrix%28.2966%20.4025%20.805%20-.5933%20-299.03%2088.664%29%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%23f4ff72%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%2373c928%22%20stop-opacity%3D%220%22%2F%3E%3C%2FradialGradient%3E%3Cpath%20fill%3D%22url%28%23c%29%22%20d%3D%22M50.4%20105.4c-6.6%204.9-14%206.2-16.5%202.8-2.4-3.3%201-10%207.6-14.9s14-6.2%2016.5-2.8c2.5%203.3-.9%2010-7.6%2014.9z%22%2F%3E%3C%2Fsvg%3E\" class=\"img-fluid\" alt=\"tekstowo.pl\" title=\"Teksty piosenek, tłumaczenia, teledyski\" style=\"width: 140px\">\t    </a>\r\n\r\n\t    <div class=\"btn-group\" role=\"group\">\r\n\t\t<button class=\"navbar-toggler pr-2 pr-lg-0\" type=\"button\" data-toggle=\"collapse\" data-target=\"#navbarUser\" aria-controls=\"navbarUser\" aria-expanded=\"false\" aria-label=\"Konto\">\r\n\t\t    <span class=\"navbar-toggler-icon user \"></span>\r\n\t\t</button>\r\n\r\n\t\t<button class=\"navbar-toggler pr-2 pr-lg-0\" type=\"button\" data-toggle=\"collapse\" data-target=\"#navbarSearch\" aria-controls=\"navbarSearch\" aria-expanded=\"false\" aria-label=\"Wyszukiwarka\">\r\n\t\t    <span class=\"navbar-toggler-icon search\"></span>\r\n\t\t</button>\r\n\r\n\t\t<button class=\"navbar-toggler pr-2 pr-lg-0\" type=\"button\" data-toggle=\"collapse\" data-target=\"#navbarSupportedContent\" aria-controls=\"navbarSupportedContent\" aria-expanded=\"false\" aria-label=\"Menu główne\">\r\n\t\t    <span class=\"navbar-toggler-icon\"></span>\r\n\t\t</button>\r\n\t    </div>\r\n\r\n\t    \t</div>\r\n    </nav>\r\n\r\n    <div class=\"container\">\r\n\t<div class=\"row top-row d-none d-md-flex\">\r\n\t    <div class=\"col-md-4 col-lg-3 align-self-center\">\r\n\t\t<a href=\"/\" class=\"logo\">\r\n\t\t    <img src=\"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20441.8%20136%22%3E%3Cstyle%3E.st0%7Bfill%3A%2362ae25%7D.st1%7Bfill%3A%23999%7D%3C%2Fstyle%3E%3Cg%20id%3D%22logo_1_%22%3E%3Cpath%20class%3D%22st0%22%20d%3D%22M130.7%2046v9h6.5v6.2h-6.5v20.6c0%201.4.3%202.4.8%203.1.6.6%201.6.9%203%20.9%201%200%201.9-.1%202.9-.3v6.4c-1.9.5-3.7.8-5.5.8-6.4%200-9.6-3.5-9.6-10.6v-21h-6.1V55h6.1v-9h8.4zM156.3%2092.7c-5.3%200-9.5-1.7-12.8-5s-4.9-7.7-4.9-13.2v-1c0-3.7.7-7%202.1-9.9%201.4-2.9%203.4-5.2%206-6.8s5.4-2.4%208.6-2.4c5%200%208.9%201.6%2011.7%204.8%202.7%203.2%204.1%207.8%204.1%2013.6v3.3H147c.3%203.1%201.3%205.5%203.1%207.2%201.8%201.8%204%202.7%206.8%202.7%203.8%200%206.9-1.5%209.3-4.6l4.5%204.3c-1.5%202.2-3.5%203.9-5.9%205.1-2.6%201.3-5.4%201.9-8.5%201.9zm-1-31.7c-2.3%200-4.1.8-5.5%202.4-1.4%201.6-2.3%203.8-2.7%206.7h15.8v-.6c-.2-2.8-.9-4.9-2.2-6.3-1.3-1.5-3.1-2.2-5.4-2.2zM186.1%2076.1l-3.7%203.8V92h-8.3V39.5h8.3v30.3l2.6-3.2L195.2%2055h10l-13.7%2015.4L206.7%2092h-9.6l-11-15.9z%22%2F%3E%3Cpath%20class%3D%22st0%22%20d%3D%22M227.9%2082c0-1.5-.6-2.6-1.8-3.4s-3.2-1.5-6.1-2.1c-2.8-.6-5.2-1.3-7.1-2.3-4.1-2-6.2-4.9-6.2-8.7%200-3.2%201.3-5.9%204-8s6.1-3.2%2010.3-3.2c4.4%200%208%201.1%2010.7%203.3s4.1%205%204.1%208.5h-8.3c0-1.6-.6-2.9-1.8-4s-2.8-1.6-4.7-1.6c-1.8%200-3.3.4-4.5%201.3-1.2.8-1.7%202-1.7%203.4%200%201.3.5%202.3%201.6%203s3.2%201.4%206.5%202.1%205.8%201.6%207.7%202.6%203.2%202.2%204.1%203.6c.9%201.4%201.3%203.1%201.3%205.1%200%203.3-1.4%206-4.1%208.1-2.8%202.1-6.4%203.1-10.8%203.1-3%200-5.7-.5-8.1-1.6-2.4-1.1-4.2-2.6-5.5-4.5s-2-4-2-6.2h8.1c.1%202%20.9%203.5%202.2%204.5%201.4%201.1%203.2%201.6%205.4%201.6s3.9-.4%205-1.2c1.1-1%201.7-2.1%201.7-3.4zM250.2%2046v9h6.5v6.2h-6.5v20.6c0%201.4.3%202.4.8%203.1.6.6%201.6.9%203%20.9%201%200%201.9-.1%202.9-.3v6.4c-1.9.5-3.7.8-5.5.8-6.4%200-9.6-3.5-9.6-10.6v-21h-6.1V55h6.1v-9h8.4zM257%2073.2c0-3.6.7-6.9%202.2-9.8s3.5-5.1%206-6.7c2.6-1.6%205.6-2.3%209-2.3%205%200%209%201.6%2012.2%204.8%203.1%203.2%204.8%207.5%205%2012.8v1.9c0%203.6-.7%206.9-2.1%209.8s-3.4%205.1-6%206.7-5.6%202.4-9%202.4c-5.2%200-9.4-1.7-12.5-5.2s-4.7-8.1-4.7-13.9v-.5h-.1zm8.3.7c0%203.8.8%206.8%202.4%208.9%201.6%202.2%203.8%203.2%206.6%203.2s5-1.1%206.5-3.3c1.6-2.2%202.3-5.4%202.3-9.6%200-3.7-.8-6.7-2.4-8.9-1.6-2.2-3.8-3.3-6.5-3.3s-4.9%201.1-6.5%203.2c-1.6%202.3-2.4%205.5-2.4%209.8zM327.4%2080.5l5.9-25.5h8.1l-10.1%2037h-6.8l-7.9-25.4-7.9%2025.4h-6.8l-10.1-37h8.1l6%2025.3%207.6-25.3h6.3l7.6%2025.5zM341.8%2073.2c0-3.6.7-6.9%202.2-9.8s3.5-5.1%206-6.7c2.6-1.6%205.6-2.3%209-2.3%205%200%209%201.6%2012.2%204.8%203.1%203.2%204.8%207.5%205%2012.8v1.9c0%203.6-.7%206.9-2.1%209.8s-3.4%205.1-6%206.7-5.6%202.4-9%202.4c-5.2%200-9.4-1.7-12.5-5.2s-4.7-8.1-4.7-13.9v-.5h-.1zm8.3.7c0%203.8.8%206.8%202.4%208.9%201.6%202.2%203.8%203.2%206.6%203.2s5-1.1%206.5-3.3c1.6-2.2%202.3-5.4%202.3-9.6%200-3.7-.8-6.7-2.4-8.9-1.6-2.2-3.8-3.3-6.5-3.3s-4.9%201.1-6.5%203.2c-1.6%202.3-2.4%205.5-2.4%209.8zM384.9%2083.3c1.5%200%202.7.4%203.6%201.3.8.9%201.3%202%201.3%203.3s-.4%202.4-1.3%203.2c-.8.9-2%201.3-3.6%201.3-1.5%200-2.7-.4-3.5-1.3-.9-.8-1.3-1.9-1.3-3.2%200-1.3.4-2.4%201.3-3.3.8-.9%202-1.3%203.5-1.3zM428.2%2073.9c0%205.7-1.3%2010.3-3.9%2013.7s-6.1%205.1-10.5%205.1c-4.1%200-7.3-1.3-9.7-4v17.5h-8.3V55h7.7l.3%203.8c2.4-3%205.8-4.4%209.9-4.4%204.5%200%208%201.7%2010.6%205%202.6%203.4%203.8%208%203.8%2014v.5h.1zm-8.3-.7c0-3.7-.7-6.6-2.2-8.8-1.5-2.2-3.6-3.2-6.3-3.2-3.4%200-5.8%201.4-7.3%204.2v16.4c1.5%202.9%204%204.3%207.4%204.3%202.6%200%204.7-1.1%206.2-3.2%201.5-2.2%202.2-5.4%202.2-9.7zM440.5%2092h-8.3V39.5h8.3V92z%22%2F%3E%3Cpath%20class%3D%22st1%22%20d%3D%22M123.5%20107.6l-.5%202.8h2.1l-.3%201.5h-2.1l-1.2%207.2v.5c0%20.6.4.8.9.8.2%200%20.6%200%20.9-.1l-.2%201.6c-.5.1-.9.2-1.4.2-.8%200-1.4-.3-1.7-.9-.4-.6-.5-1.3-.5-2.2l1.2-7.2h-2.1l.3-1.5h2.1l.5-2.8%202%20.1zM129.3%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2.7.8%201.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.6.8-2.6.8zm1.3-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1.1-.8-1.8-.8zM141.8%20112.1l-.9-.1c-1.2%200-2.2.6-2.9%201.7l-1.4%208.3h-1.9l2-11.6h1.9l-.3%201.4c.8-1.1%201.8-1.6%202.9-1.6.2%200%20.5.1.9.2l-.3%201.7zM147.5%20122c-.1-.2-.1-.4-.1-.6v-.6c-1.1%201-2.2%201.5-3.4%201.4-1%200-1.9-.3-2.5-1-.6-.6-.9-1.4-.9-2.4.1-1.2.6-2.2%201.6-2.9%201-.7%202.3-1%204-1h2l.1-.9c.1-.7-.1-1.2-.4-1.6-.3-.4-.9-.6-1.5-.6-.7%200-1.3.1-1.8.5-.5.3-.8.8-.9%201.3h-2c.1-.7.3-1.3.8-1.8s1.1-.9%201.8-1.2c.7-.3%201.5-.4%202.3-.4%201.2%200%202.2.4%202.8%201.1.7.7%201%201.6.8%202.8l-.9%205.8-.1.8c0%20.4%200%20.8.1%201.2v.2h-1.8v-.1zm-3-1.5c.6%200%201.2-.1%201.8-.4s1-.7%201.4-1.3l.4-2.4h-1.5c-1.2%200-2.2.2-2.8.6-.7.4-1%20.9-1.1%201.6-.1.5.1%201%20.4%201.4.3.3.8.5%201.4.5zM153.2%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM168.8%20110.4l-.2%201.3c1-1%202.2-1.5%203.5-1.5.7%200%201.4.2%201.9.5s.9.8%201.1%201.4c1.1-1.3%202.4-1.9%203.9-1.9%201.2%200%202%20.4%202.6%201.2.6.8.8%201.8.6%203l-1.3%207.6H179l1.3-7.7v-1c-.1-1-.8-1.5-1.9-1.5-.7%200-1.4.2-1.9.7-.6.5-.9%201.1-1.1%201.8l-1.3%207.7h-2l1.3-7.6c.1-.8%200-1.4-.4-1.8s-.9-.7-1.6-.7c-1.2%200-2.2.5-2.9%201.7l-1.5%208.5h-1.9l2-11.6%201.7-.1zM189.1%20110.2c1%200%201.8.3%202.5.8s1.2%201.2%201.5%202.1.4%201.9.3%203v.2c-.1%201.1-.5%202.2-1%203.1s-1.2%201.6-2.1%202.1c-.9.5-1.8.7-2.8.7s-1.8-.3-2.5-.8-1.2-1.2-1.5-2.1-.4-1.9-.3-2.9c.1-1.2.4-2.3%201-3.2.5-1%201.2-1.7%202.1-2.2.8-.6%201.8-.9%202.8-.8zm-4%206.2c-.1.5-.1.9%200%201.4.1.8.3%201.5.8%202%20.4.5%201%20.8%201.7.8.6%200%201.2-.1%201.8-.5.5-.3%201-.9%201.4-1.5.4-.7.6-1.5.7-2.3.1-.7.1-1.2%200-1.7-.1-.9-.3-1.6-.8-2.1-.4-.5-1-.8-1.7-.8-1%200-1.9.4-2.6%201.2-.7.8-1.1%201.9-1.3%203.2v.3zM195.8%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zm3.2-13c0-.3.1-.6.3-.8.2-.2.5-.3.8-.4.3%200%20.6.1.8.3.2.2.3.5.3.8s-.1.6-.3.8c-.2.2-.5.3-.8.3s-.6-.1-.8-.3c-.2-.1-.3-.4-.3-.7zM208.1%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2.7.8%201.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1-.8-1.8-.8zM220.6%20118.9c.1-.8-.3-1.4-1.3-1.7l-2-.6c-1.7-.6-2.5-1.6-2.5-2.9%200-1%20.5-1.9%201.4-2.5.9-.7%201.9-1%203.1-1%201.2%200%202.1.4%202.9%201%20.7.7%201.1%201.5%201.1%202.6h-1.9c0-.6-.2-1.1-.5-1.4s-.9-.6-1.5-.6c-.7%200-1.3.2-1.7.5-.5.3-.7.7-.8%201.3-.1.7.3%201.2%201.2%201.5l1%20.3c1.3.3%202.3.8%202.8%201.3s.8%201.2.8%202.1c0%20.7-.3%201.4-.7%201.9s-1%20.9-1.7%201.2c-.7.3-1.5.4-2.3.4-1.2%200-2.2-.4-3.1-1.1-.8-.7-1.2-1.6-1.2-2.7h1.9c0%20.7.2%201.2.6%201.6.4.4%201%20.6%201.7.6s1.3-.1%201.8-.4c.5-.5.8-.9.9-1.4zM225.5%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM241.3%20110.4l-2.2%2013c-.1%201.1-.5%201.9-1.1%202.5s-1.4.9-2.3.8c-.4%200-.8-.1-1.3-.2l.2-1.6c.3.1.6.1.9.1.9%200%201.5-.6%201.7-1.7l2.2-13%201.9.1zm-1.6-3.1c0-.3.1-.6.3-.8.2-.2.5-.3.8-.4.3%200%20.6.1.8.3.2.2.3.5.3.8s-.1.6-.3.8-.5.3-.8.3-.6-.1-.8-.3-.3-.4-.3-.7zM246.4%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2s1.1%201.8%201.1%203.1v1.4l-.1.9H244c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1-.8-1.8-.8zM266.5%20116.2c-.1%201.2-.5%202.2-1%203.2s-1.1%201.7-1.8%202.1c-.7.5-1.5.7-2.4.7-1.3%200-2.4-.5-3.1-1.4l-1%205.6h-1.9l2.8-16.1h1.8l-.2%201.3c1-1%202.1-1.5%203.4-1.5%201.1%200%202%20.4%202.6%201.2s1%201.9%201%203.3c0%20.5%200%20.9-.1%201.3l-.1.3zm-1.9-.2l.1-.9c0-1-.2-1.8-.6-2.4s-1-.8-1.7-.9c-1.1%200-2.1.5-2.9%201.6l-1%205.6c.4%201%201.2%201.6%202.4%201.6%201%200%201.8-.4%202.5-1.1.5-.8.9-2%201.2-3.5zM274.2%20112.1l-.9-.1c-1.2%200-2.2.6-2.9%201.7L269%20122h-1.9l2-11.6h1.9l-.3%201.4c.8-1.1%201.8-1.6%202.9-1.6.2%200%20.5.1.9.2l-.3%201.7zM275.3%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM287.6%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2s1.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.4-.5-1-.8-1.8-.8zM297.8%20120.6c.7%200%201.3-.2%201.9-.6s.9-1%201.1-1.6h1.8c-.1.7-.4%201.4-.9%202s-1.1%201.1-1.8%201.4-1.5.5-2.2.5c-1%200-1.8-.3-2.5-.8s-1.2-1.2-1.5-2.1-.4-1.9-.3-3l.1-.5c.1-1.1.5-2.1%201-3s1.2-1.6%202-2.1%201.7-.7%202.8-.7c1.2%200%202.2.4%202.9%201.2s1.1%201.8%201.1%203h-1.8c0-.8-.2-1.4-.6-1.8-.4-.5-1-.7-1.7-.7-1%200-1.8.4-2.5%201.1-.7.8-1.1%201.9-1.3%203.2v1.7c0%20.8.3%201.5.7%202s1%20.8%201.7.8zM305.2%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM316.8%20119.1l4.1-8.7h2.1l-6.9%2013.6c-1%201.9-2.2%202.8-3.7%202.8-.3%200-.7-.1-1.2-.2l.2-1.6.5.1c.6%200%201.1-.1%201.6-.4.4-.3.8-.8%201.2-1.5l.7-1.3-2-11.4h2l1.4%208.6z%22%2F%3E%3Cpath%20class%3D%22st1%22%20d%3D%22M326.9%20107.6l-.5%202.8h2.1l-.3%201.5h-2.1l-1.2%207.2v.5c0%20.6.4.8.9.8.2%200%20.6%200%20.9-.1l-.2%201.6c-.5.1-.9.2-1.4.2-.8%200-1.4-.3-1.7-.9-.4-.6-.5-1.3-.5-2.2l1.2-7.2H322l.3-1.5h2.1l.5-2.8%202%20.1zM334.8%20122c0-.2-.1-.4-.1-.6v-.6c-1.1%201-2.2%201.5-3.4%201.4-1%200-1.9-.3-2.5-1-.6-.6-.9-1.4-.9-2.4.1-1.2.6-2.2%201.6-2.9%201-.7%202.3-1%204-1h2l.1-.9c.1-.7-.1-1.2-.4-1.6s-.9-.6-1.5-.6c-.7%200-1.3.1-1.8.5-.5.3-.8.8-.9%201.3h-2c.1-.7.3-1.3.8-1.8s1.1-.9%201.8-1.2c.7-.3%201.5-.4%202.3-.4%201.2%200%202.2.4%202.8%201.1.7.7%201%201.6.8%202.8l-.9%205.8-.1.8c0%20.4%200%20.8.1%201.2v.2h-1.8v-.1zm-3-1.5c.6%200%201.2-.1%201.8-.4.6-.3%201-.7%201.4-1.3l.4-2.4h-1.5c-1.2%200-2.2.2-2.8.6s-1%20.9-1.1%201.6c-.1.5.1%201%20.4%201.4.3.3.8.5%201.4.5zM343.3%20120.6c.7%200%201.3-.2%201.9-.6s.9-1%201.1-1.6h1.8c-.1.7-.4%201.4-.9%202s-1.1%201.1-1.8%201.4-1.5.5-2.2.5c-1%200-1.8-.3-2.5-.8s-1.2-1.2-1.5-2.1-.4-1.9-.3-3l.1-.5c.1-1.1.5-2.1%201-3s1.2-1.6%202-2.1%201.7-.7%202.8-.7c1.2%200%202.2.4%202.9%201.2s1.1%201.8%201.1%203H347c0-.8-.2-1.4-.6-1.8-.4-.5-1-.7-1.7-.7-1%200-1.8.4-2.5%201.1-.7.8-1.1%201.9-1.3%203.2v1.7c0%20.8.3%201.5.7%202s.9.8%201.7.8zm2.7-15.1h2.5l-3.3%203.2h-1.7l2.5-3.2z%22%2F%3E%3Cpath%20id%3D%22box%22%20d%3D%22M92.9%20124H7.1c-3.9%200-7.1-3.2-7.1-7.1V31.1C0%2027.2%203.2%2024%207.1%2024h85.8c3.9%200%207.1%203.2%207.1%207.1v85.8c0%203.9-3.2%207.1-7.1%207.1z%22%20fill%3D%22%23f60%22%2F%3E%3Cpath%20d%3D%22M68%2044.1c-6.8-7-15.9-12.2-24.6-14.4C28.8%2026%2025%2023.9%2017.8%2019c-4.1-2.8-7.2-8.4-10.6-9.8-1.6-.6-2.6.1-3.1.7-.9%201-1.6%202.9-.4%205.2%2010.1%2019.8%2048.4%2076.8%2048.4%2076.8-5.9%201.1-12.4%205.7-17%2012.9-6.9%2010.7-7%2022.9-.1%2027.2%206.9%204.3%2018.1-.9%2025-11.6%205.9-9%206.8-19.1%202.8-24.6L29.2%2042.7s-3.9-4.7%202.3-6.1c4.5-1%2013.9.1%2019%202.8C56%2042.3%2069%2051.7%2072.3%2065.1c1.5%206.2.6%209.6%201.4%2015.1.4%202.6%203.2%204.8%205.5%201.2%201.2-1.9%201.4-8.4%201.1-13.5-.5-6.7-6-17.3-12.3-23.8zm-10.4%2048c.3.1.7.2%201%20.3-.3-.1-.6-.2-1-.3zm-2.5-.4h.4-.4zm1.1.1c.3%200%20.6.1.8.1-.2%200-.5-.1-.8-.1zm-3%200c.1%200%20.1%200%200%200%20.1%200%20.1%200%200%200zm5.8.8l1.2.6c-.4-.3-.8-.5-1.2-.6zm2.9%202zm-.7-.7c-.3-.3-.6-.5-1-.7.3.2.6.5%201%20.7z%22%20opacity%3D%22.2%22%2F%3E%3ClinearGradient%20id%3D%22SVGID_1_%22%20gradientUnits%3D%22userSpaceOnUse%22%20x1%3D%2239.888%22%20y1%3D%2292.284%22%20x2%3D%2252.096%22%20y2%3D%22125.824%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%238fe132%22%2F%3E%3Cstop%20offset%3D%22.362%22%20stop-color%3D%22%2365bd23%22%2F%3E%3Cstop%20offset%3D%22.668%22%20stop-color%3D%22%2349a519%22%2F%3E%3Cstop%20offset%3D%22.844%22%20stop-color%3D%22%233e9c15%22%2F%3E%3C%2FlinearGradient%3E%3Cpath%20d%3D%22M57.9%20117.8c-7.6%2010.4-19.2%2014.9-25.7%2010.1s-5.7-17.2%201.9-27.6%2019.2-14.9%2025.7-10.1c6.6%204.9%205.7%2017.2-1.9%2027.6z%22%20fill%3D%22url%28%23SVGID_1_%29%22%2F%3E%3ClinearGradient%20id%3D%22SVGID_2_%22%20gradientUnits%3D%22userSpaceOnUse%22%20x1%3D%22-3.574%22%20y1%3D%2224.316%22%20x2%3D%2282.773%22%20y2%3D%2274.169%22%3E%3Cstop%20offset%3D%22.184%22%20stop-color%3D%22%238fe132%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%233e9c15%22%2F%3E%3C%2FlinearGradient%3E%3Cpath%20d%3D%22M70.9%2041c-6.3-7.6-15-13.4-23.6-16.2-14.4-4.7-18-7.1-24.8-12.5-3.9-3.1-6.6-9-9.9-10.7-1.6-.8-2.6-.1-3.2.5-.9.9-1.8%202.8-.7%205.2C17.4%2028%2051.9%2088.4%2051.9%2088.4c3-.3%205.7.2%207.9%201.8%201%20.8%201.8%201.7%202.5%202.8l-30-56s-3.6-5%202.7-6c4.5-.7%2013.9%201.1%2018.8%204.1%205.3%203.3%2017.6%2013.7%2020%2027.5%201.1%206.4%200%209.7.4%2015.4.2%202.6%202.9%205.1%205.4%201.6%201.3-1.8%202-8.4%202-13.6%200-6.8-4.9-17.9-10.7-25z%22%20fill%3D%22url%28%23SVGID_2_%29%22%2F%3E%3CradialGradient%20id%3D%22SVGID_3_%22%20cx%3D%2221.505%22%20cy%3D%22103.861%22%20r%3D%2214.934%22%20gradientTransform%3D%22matrix%28.2966%20.4025%20-.805%20.5933%20123.22%2029.092%29%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%23f4ff72%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%2373c928%22%20stop-opacity%3D%220%22%2F%3E%3C%2FradialGradient%3E%3Cpath%20d%3D%22M50.4%20105.4c-6.6%204.9-14%206.2-16.5%202.8-2.4-3.3%201-10%207.6-14.9s14-6.2%2016.5-2.8c2.5%203.3-.9%2010-7.6%2014.9z%22%20fill%3D%22url%28%23SVGID_3_%29%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E\" class=\"img-fluid\" alt=\"tekstowo.pl\" title=\"Teksty piosenek, tłumaczenia, teledyski\">\t\t</a>\r\n\t    </div>\r\n\r\n\t    <div class=\"col\">\r\n\t\t<div class=\"top-links\">\r\n\t\t    \t\t\t<a href=\"/logowanie.html\" title=\"Logowanie\">Logowanie</a> |\r\n\t\t\t<a href=\"/rejestracja.html\" title=\"Rejestracja\">Rejestracja</a> |\r\n\t\t    \t\t    <a href=\"/faq.html\" title=\"FAQ\">FAQ</a> |\r\n\t\t    <a href=\"/regulamin.html\" title=\"Regulamin\">Regulamin</a> |\r\n\t\t    <a href=\"/kontakt.html\" title=\"Kontakt\">Kontakt</a>\r\n\t\t</div>\r\n\r\n\t\t<div class=\"topmenu\">\r\n\t\t    <ul class=\"\">\r\n    <li class=\"topmenu-first\"><a href=\"/\" title=\"Główna\">Główna</a></li>\r\n    <li><a href=\"/przegladaj_teksty.html\" title=\"Teksty\">Teksty</a></li>\r\n    <li><a href=\"/szukane_utwory,6-miesiecy.html\" title=\"Poszukiwane teksty\">Poszukiwane teksty</a></li>\r\n    <li><a href=\"/soundtracki_najnowsze.html\" title=\"Soundtracki\">Soundtracki</a></li>\r\n    <li><a href=\"/rankingi\" title=\"Rankingi\">Rankingi</a></li>\r\n    <li class=\"topmenu-last\"><a href=\"/uzytkownicy.html\" title=\"Użytkownicy\" class=\"no-bg \">Użytkownicy</a></li>\r\n\t</ul>\t\t</div>\r\n\t    </div>\r\n\t</div>\r\n\t<div id=\"t170319\">\r\n\t    <div class=\"adv-top\" style=\"min-height: 300px\"> <!-- reklama bill -->\r\n\t\t<div>\r\n\t\t    <a title=\"Ukryj reklamy\" href=\"javascript:;\" rel=\"loginbox\" id=\"hide-ads\"></a>\t\t    \r\n    \t\t    \t    \r\n    \r\n    \t    \r\n    \r\n   \r\n\r\n<!-- kod reklamy desktop -->\r\n<center>\r\n<script data-adfscript=\"adx.adform.net/adx/?mid=668393&amp;rnd=<random_number>\"></script>\r\n</center> \r\n \r\n\r\n\t\t</div>\r\n\t    </div> <!-- end reklama bill -->\r\n\t</div>\r\n\t\t<div class=\"row topbar\">\r\n\t    <div class=\"col-auto\">\r\n\t\t<a href=\"/\" class=\"green\" title=\"Teksty piosenek\">Teksty piosenek</a> &gt; <a href=\"/artysci_na,B.html\" class=\"green\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę B\">B</a>\r\n\t\t &gt; <a href=\"/piosenki_artysty,bailey_bigger.html\" class=\"green\" title=\"Bailey Bigger\">Bailey Bigger</a>\r\n\t\t &gt; <a href=\"/piosenka,bailey_bigger,black_eyed_susan.html\" class=\"green\" title=\"Black Eyed Susan\">Black Eyed Susan</a>\r\n\t\t\t\t</div>\r\n\t\t<div class=\"col d-none text-right d-md-block\">\r\n\t\t    2 170 745 tekstów, 20 217 poszukiwanych i 502 oczekujących\r\n\t\t</div>\r\n\t    </div>\r\n\r\n\t    <div class=\"row\">\r\n\r\n\t\t<div class=\"col-sm-4 col-lg-3 order-2 order-sm-1\">\r\n    <div class=\"big-buttons row mr-0\" role=\"group\">\r\n    <a href=\"/dodaj_tekst.html\" rel=\"loginbox\" class=\"dodaj-tekst\" title=\"Dodaj tekst\">\r\n\t<i class=\"icon\"></i>\r\n\tDodaj tekst\r\n    </a>\r\n    <a href=\"/zaproponuj_utwor.html\" rel=\"loginbox\" class=\"zaproponuj-utwor\" title=\"Szukasz utworu?\">\r\n\t<i class=\"icon\"></i>\r\n\tBrak tekstu?</a>\r\n    <a href=\"/dodaj_soundtrack.html\" rel=\"loginbox\" class=\"dodaj-soundtrack\" title=\"Dodaj soundtrack\">\r\n\t<i class=\"icon\"></i>\r\n\tDodaj soundtrack</a>\r\n</div>\r\n        \t<div class=\"left-box account-box mt-3\">\r\n\t    <h4>Zaloguj się</h4>\r\n\t    i wykorzystaj wszystkie możliwości serwisu!\r\n\r\n\t    <fieldset class=\"login mt-2 text-center\">\r\n\t\t<a class=\"login-send btn btn-block btn-primary mb-2\" href=\"https://www.tekstowo.pl/logowanie.html\">Zaloguj się</a>\r\n\t\t<a href=\"javascript:;\" class=\"fb-button my-fb-login-button btn btn-block btn-fb\" title=\"Zaloguj się z Facebookiem\"><span class=\"icon\"></span>Zaloguj się przez Facebooka</a>\r\n\t\t\t    </fieldset>\r\n\r\n\t    <ul class=\"arrows mt-2\">\r\n\t\t<li><a href=\"/przypomnij.html\" class=\"green bold\" title=\"Zapomniałem hasła\">Przypomnienie hasła</a></li>\r\n\t\t<li><a href=\"/rejestracja.html\" class=\"green bold\" title=\"Nie mam jeszcze konta\">Nie mam jeszcze konta</a></li>\r\n\t    </ul>\r\n\r\n\t</div>\r\n    \r\n    \t\t\t\t\t\t\t<div class=\"left-box mt-3\">\r\n\t\t\t\t<h4>Inne teksty piosenek</h4>\r\n\t\t\t\t<h4>Bailey Bigger</h4>\r\n\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t\t<b>1.</b> <a href=\"piosenka,bailey_bigger,green_eyes.html\" class=\"title\" title=\"Bailey Bigger - Green Eyes\">Bailey Bigger - Green Eyes </a>\r\n\t\t\t\t\t<b title=\"teledysk\" class=\"icon_kamera\"></b>\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t\t<b>2.</b> <a href=\"piosenka,bailey_bigger,god_help_me_stop_forgiving.html\" class=\"title\" title=\"Bailey Bigger - God Help Me Stop Forgiving\">Bailey Bigger - God Help Me Stop Forgiving </a>\r\n\t\t\t\t\t<b title=\"teledysk\" class=\"icon_kamera\"></b>\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t\t<b>3.</b> <a href=\"piosenka,bailey_bigger,running_from_the_water.html\" class=\"title\" title=\"Bailey Bigger - Running from the Water\">Bailey Bigger - Running from the Water </a>\r\n\t\t\t\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t\t<b>4.</b> <a href=\"piosenka,bailey_bigger,coyote_red.html\" class=\"title\" title=\"Bailey Bigger - Coyote Red\">Bailey Bigger - Coyote Red </a>\r\n\t\t\t\t\t<b title=\"teledysk\" class=\"icon_kamera\"></b>\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t<div class=\"box-przeboje no-bg\">\r\n\t\t\t\t\t<b>5.</b> <a href=\"piosenka,bailey_bigger,wyly.html\" class=\"title\" title=\"Bailey Bigger - Wyly\">Bailey Bigger - Wyly </a>\r\n\t\t\t\t\t<b title=\"teledysk\" class=\"icon_kamera\"></b>\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t</div> <!-- end inne utwory -->\r\n\t\t\t<a href=\"/piosenki_artysty,bailey_bigger.html\" class=\"block text-right\" title=\"Zobacz więcej >>\">Zobacz więcej &gt;&gt;</a>\r\n\t\t\t\t        \r\n    \t<div class=\"left-box mt-3\">\r\n\t    <h4>Poszukiwane teksty</h4>\r\n\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>1.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,Henning+Wehland,tytul,Weil+wir+Champions+sind,poszukiwany,354712.html\" class=\"title\">Henning Wehland - Weil wir Champions sind</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>2.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,Demis+Roussos,tytul,For+Love,poszukiwany,355966.html\" class=\"title\">Demis Roussos - For Love</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>3.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,David+Bowie,tytul,Move+On+%28Moonage+Daydream+A+Cappella+Mix+Edit%29,poszukiwany,357748.html\" class=\"title\">David Bowie - Move On (Moonage Daydream A Cappella Mix Edit)</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>4.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,Rob+Zombie,tytul,El+Vampiro,poszukiwany,358402.html\" class=\"title\">Rob Zombie - El Vampiro</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>5.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,Inside,tytul,Wizzard+King,poszukiwany,355937.html\" class=\"title\">Inside - Wizzard King</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>6.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,H%C3%A9loise+Janiaud,tytul,Be+a+Good+Girl,poszukiwany,356157.html\" class=\"title\">Héloise Janiaud - Be a Good Girl</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>7.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,Soul+Superiors,tytul,A+Great+Day,poszukiwany,355739.html\" class=\"title\">Soul Superiors - A Great Day</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>8.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,Jefferson+State,tytul,White+Out,poszukiwany,357438.html\" class=\"title\">Jefferson State - White Out</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>9.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,Jennifer+McNutt,tytul,After+Everyone,poszukiwany,355721.html\" class=\"title\">Jennifer McNutt - After Everyone</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje no-bg\">\r\n\t\t    <b>10.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,The+Merced+Blue+Notes,tytul,Your+tender+lips,poszukiwany,355933.html\" class=\"title\">The Merced Blue Notes - Your tender lips</a>\r\n\t\t</div>\r\n\t    \t</div>\r\n\t<a href=\"/szukane_utwory,6-miesiecy.html\" class=\"block text-right\" title=\"Zobacz więcej >>\">Zobacz więcej &gt;&gt;</a>\r\n\r\n\t\t    <div class=\"left-box mt-3\">\r\n\t\t<h4>Poszukiwane tłumaczenia</h4>\r\n\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>1.</b> <a href=\"/piosenka,gorillaz,new_gold.html\" class=\"title\">Gorillaz - New Gold</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>2.</b> <a href=\"/piosenka,johnny_cash,the_folk_singer.html\" class=\"title\">Johnny Cash - The Folk Singer</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>3.</b> <a href=\"/piosenka,johnny_cash,chattanooga_sugarbabe.html\" class=\"title\">Johnny Cash - Chattanooga Sugarbabe</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>4.</b> <a href=\"/piosenka,johnny_cash,rockabilly_blues__texas_1955_.html\" class=\"title\">Johnny Cash - Rockabilly Blues (Texas 1955)</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>5.</b> <a href=\"/piosenka,johnny_cash,pick_a_bale_of_cotton.html\" class=\"title\">Johnny Cash - Pick A Bale Of Cotton</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>6.</b> <a href=\"/piosenka,gigi_d_agostino,melody_x_feat__luca_noise_.html\" class=\"title\">Gigi D'Agostino - Melody X feat. Luca Noise </a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>7.</b> <a href=\"/piosenka,johnny_cash,o_come_all_ye_faithful.html\" class=\"title\">Johnny Cash - O Come All Ye Faithful</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>8.</b> <a href=\"/piosenka,enhypen_,one_in_a_billion.html\" class=\"title\">Enhypen  - One in a billion</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>9.</b> <a href=\"/piosenka,johnny_cash,chunk_of_coal.html\" class=\"title\">Johnny Cash - Chunk of Coal</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje no-bg\">\r\n\t\t\t\t<b>10.</b> <a href=\"/piosenka,johnny_cash,take_me_home.html\" class=\"title\">Johnny Cash - Take Me Home</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t    </div>\r\n\t    <a href=\"/szukane_tlumaczenia,6-miesiecy.html\" class=\"block text-right\" title=\"Zobacz więcej >>\">Zobacz więcej &gt;&gt;</a>\r\n\t    \r\n    <!-- polecamy -->\r\n    <div class=\"left-box mt-3\">\r\n\t<h4>Polecamy</h4>\r\n\t<ul class=\"arrows\">\r\n    <li><a href=\"http://www.giercownia.pl\" title=\"Gry online\" class=\"blank\">Gry online</a></li>\r\n    <li><a href=\"http://www.maxior.pl\" title=\"Filmy\" class=\"blank\">Śmieszne filmy</a></li>\r\n    <li><a href=\"http://www.bajer.pl\" title=\"Dziewczyny\" class=\"blank\">Polskie dziewczyny</a></li>\r\n    <li><a href=\"http://m.giercownia.pl\" title=\"Gry na telefon i tablet\" class=\"blank\">Gry na telefon i tablet</a></li>\r\n</ul>    </div>\r\n    <!-- end polecamy -->\r\n\r\n</div> <!-- end left-column -->\r\n\t\t<div class=\"col-sm-8 col-lg-9 order-1 order-sm-2\">\r\n\t\t    \t\t\t<div class=\"row search-box\">\r\n    <div class=\"inner col\">\r\n\r\n\t<div class=\"search-form\">\r\n\t    <form action=\"/wyszukaj.html\" method=\"get\" class=\"form-inline\">\r\n\t\t<label for=\"s-autor\" class=\"big-text my-1 mr-2\">Szukaj tekstu piosenki</label>\r\n\t\t<input type=\"text\" id=\"s-autor\" name=\"search-artist\" class=\"form-control form-control input-artist mb-2 mr-sm-2\" placeholder=\"Podaj wykonawcę\" value=\"\" autocomplete=\"off\">\r\n\t\t<label class=\"my-1 mr-2\" for=\"s-title\"> i/lub </label>\r\n\t\t<input type=\"text\" id=\"s-title\" name=\"search-title\" class=\"form-control form-control input-title mb-2 mr-sm-2\" placeholder=\"Podaj tytuł\" value=\"\" autocomplete=\"off\">\r\n\t\t<input type=\"submit\" class=\"submit\" value=\"Szukaj\">\r\n\t    </form>\r\n\r\n\t    <div class=\"search-adv\">\r\n\t\t<a href=\"/wyszukiwanie-zaawansowane.html\">wyszukiwanie zaawansowane &gt;</a>\r\n\t    </div>\r\n\r\n\t    <hr class=\"d-none d-md-block\">\r\n\t</div>\r\n\t<div class=\"search-browse\">\r\n\t    <div class=\"przegladaj big-text\">Przeglądaj wykonawców na literę</div>\r\n\t    <ul class=\"alfabet\">\r\n\t\t\t\t    <li><a href=\"/artysci_na,A.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę A\">A</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,B.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę B\">B</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,C.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę C\">C</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,D.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę D\">D</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,E.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę E\">E</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,F.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę F\">F</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,G.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę G\">G</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,H.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę H\">H</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,I.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę I\">I</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,J.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę J\">J</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,K.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę K\">K</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,L.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę L\">L</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,M.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę M\">M</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,N.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę N\">N</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,O.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę O\">O</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,P.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę P\">P</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,Q.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę Q\">Q</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,R.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę R\">R</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,S.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę S\">S</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,T.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę T\">T</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,U.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę U\">U</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,V.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę V\">V</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,W.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę W\">W</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,X.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę X\">X</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,Y.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę Y\">Y</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,Z.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę Z\">Z</a></li>\r\n\t\t    \t\t<li><a href=\"/artysci_na,pozostale.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców w kategorii &quot;Pozostałe&quot;\">0-9</a></li>\r\n\t    </ul>\r\n\t</div>\r\n\r\n    </div> <!-- end green-box -->\r\n</div>\t\t    \r\n<div class=\"row right-column\"> <!-- right column -->\r\n    <div class=\"col\">\r\n\r\n\t \r\n\r\n\t<div class=\"belka row mx-0 px-3\">\r\n\t    <div class=\"col-lg-7 px-0\">\r\n\t\t<h1 class=\"strong\">Bailey Bigger - Black Eyed Susan</h1>\r\n\t    </div>\r\n\t    <div class=\"col-lg-5\">\r\n\t\t<div class=\"row belka-right\">\r\n\t\t    <div class=\"col\">\r\n\t\t\t<div class=\"odslon\">Odsłon: 22</div>\r\n\t\t    </div>\r\n\t\t    <div class=\"col-auto\">\r\n\t\t\t<a href=\"/wykonawca,bailey_bigger.html\" class=\"link-wykonawca\" title=\"Przejdź na stronę wykonawcy >\">Przejdź na stronę wykonawcy &gt;</a>\r\n\t\t    </div>\r\n\t\t</div>  \r\n\t    </div>\r\n\t</div>\r\n\r\n\r\n\t\r\n\t\r\n\t\r\n\t<div class=\"row mx-0\">\r\n\t    \t\t<div id=\"advSong\" class=\"adv-home col-lg-6 mt-2 order-2 order-lg-1\"> <!-- reklama środek -->\r\n\t\t        \t    \r\n    \r\n    \t    \r\n    \r\n\r\n   \r\n\r\n<!-- kod reklamy desktop -->\r\n<center>\r\n<script async=\"\" src=\"https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js\"></script>\r\n<!-- tekstowo desktop 336x280 -->\r\n<ins class=\"adsbygoogle\" style=\"display:inline-block;width:336px;height:280px\" data-ad-client=\"ca-pub-3653916656187385\" data-ad-slot=\"1803845785\"><iframe id=\"aswift_0\" style=\"height: 1px !important; max-height: 1px !important; max-width: 1px !important; width: 1px !important;\"><iframe id=\"google_ads_frame0\"></iframe></iframe></ins>\r\n<script>\r\n     (adsbygoogle = window.adsbygoogle || []).push({});\r\n</script>\r\n</center>\r\n   \r\n\t\t</div> <!-- end reklama środek -->\r\n\t    \r\n\t    <div class=\"teledysk zdjecie col-lg-6 order-1 order-lg-2 px-0\">\r\n\t\t\t\t    <div class=\"movieDivWrap\">\r\n\t\t\t<div id=\"movieDiv\">\r\n\t\t\t    \t\t\t\t\t\t\t\t\t\t\t\t\t\t    \t\t\t\t\t\t\t\t\t    <iframe style=\"border: 0; margin: 0; padding: 0; overflow: hidden\" width=\"365\" height=\"280\" src=\"//filmiki4.maxart.pl/tplayer3n/#adText=1&amp;autoplay=1&amp;videoId=bX4gXCOtrkI&amp;loadVideoTimeout=500000&amp;volume=0&amp;aoID=DVJAPyL5hymQ7Q0.2FRYVWxk34YuoC7ExS6BHR_5ALr.c7\" frameborder=\"0\" allowfullscreen=\"1\" scrolling=\"no\"></iframe>\r\n\t\t\t\t    \t\t\t\t\t\t\t    </div>\r\n\t\t\t</div>\r\n\t\t    \t\t</div>\r\n\t    </div>\r\n\r\n\t    <div class=\"row mt-3 mx-0\">\r\n\t\t<div class=\"col-lg px-md-0 pr-lg-3\">\r\n\t\t    \t\t\t<div class=\"teledysk-left\">\r\n\t\t\t    <div class=\"box-przeboje\">\r\n\t\t\t\t<span>\r\n\t\t\t\t    Tekst dodał(a): <a title=\"Tekst dodany 01.04.2022 przez Adelle\" href=\"profil,adelle.html\">Adelle</a>\r\n\t\t\t\t</span>\r\n\t\t\t\t<a rel=\"loginbox\" href=\"/edytuj_tekst,bailey_bigger,black_eyed_susan.html\" class=\"new\" title=\"Edytuj tekst\">Edytuj tekst</a>\r\n\t\t\t    </div>\r\n\t\t\t    <div class=\"box-przeboje\">\r\n\t\t\t\t<span>\r\n\t\t\t\t    Tłumaczenie dodał(a): brak\t\t\t\t</span>\r\n\t\t\t\t<a rel=\"loginbox\" href=\"/dodaj_tlumaczenie,bailey_bigger,black_eyed_susan.html\" class=\"new\" title=\"Dodaj tłumaczenie\">Dodaj tłumaczenie</a>\r\n\t\t\t    </div>\r\n\t\t\t    <div class=\"box-przeboje no-bg\">\r\n\t\t\t\t<span>\r\n\t\t\t\t    Teledysk dodał(a): <a title=\"Teledysk dodany 02.04.2022 przez Adelle\" href=\"profil,adelle.html\">Adelle</a>\t\t\t\t</span>\r\n\t\t\t\t<a rel=\"loginbox\" href=\"javascript:editTeledisc('bailey_bigger','black_eyed_susan', '');\" class=\"new\" title=\"Edytuj teledysk\">Edytuj teledysk</a>\r\n\t\t\t    </div>\r\n\t\t\t</div>\r\n\t\t    \t\t</div>\r\n\r\n\t\t<div class=\"col-lg px-md-0 mt-3 mt-lg-0\">\r\n\t\t    <div class=\"teledysk-right\">\r\n\t\t\t<div class=\"social\">\r\n\t\t\t    <label for=\"emb\">Skopiuj link:</label>\r\n\t\t\t    \t\t\t    \t\t\t</div>\r\n\t\t\t<fieldset class=\"mt-0 mb-2 mt-2 mt-lg-0\">\r\n\t\t\t    <input type=\"text\" id=\"emb\" name=\"emb\" readonly=\"\" value=\"https://www.tekstowo.pl/piosenka,bailey_bigger,black_eyed_susan.html\" class=\"emb form-control\" onfocus=\"this.select();\" onclick=\"this.select();\r\n\t\t\t\t\t   document.execCommand('copy');\r\n\t\t\t\t\t   modalAlert('Udostępnianie tekstu', 'Link skopiowano do schowka!')\">\r\n\t\t\t</fieldset>\r\n\t\t\t\t\t\t    <div class=\"social mt-1 social-even\">\r\n\t\t\t\t<a class=\"btn btn-sm btn-fb share-pop\" href=\"https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fwww.tekstowo.pl%2Fpiosenka%2Cbailey_bigger%2Cblack_eyed_susan.html\"><i class=\"icon\"></i>&nbsp;Udostępnij</a>\r\n\t\t\t\t<a class=\"btn btn-sm btn-tweeter share-pop\" href=\"https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.tekstowo.pl%2Fpiosenka%2Cbailey_bigger%2Cblack_eyed_susan.html&amp;text=Bailey Bigger - Black Eyed Susan\"><i class=\"icon\"></i>&nbsp;Tweetnij</a>\r\n\t\t\t\t<a class=\"btn btn-sm btn-messenger share-pop\" href=\"https://www.facebook.com/dialog/send?app_id=131858753537922&amp;link=https%3A%2F%2Fwww.tekstowo.pl%2Fpiosenka%2Cbailey_bigger%2Cblack_eyed_susan.html&amp;redirect_uri=https%3A%2F%2Fwww.tekstowo.pl%2Fpiosenka%2Cbailey_bigger%2Cblack_eyed_susan.html\"><i class=\"icon\"></i>&nbsp;Messenger</a>\r\n\t\t\t    </div>\r\n\t\t\t\t\t    </div>\r\n\t\t</div>\r\n\t    </div>\r\n\r\n\t    <div class=\"mt-3\">\r\n\t\t<!-- glosowanie -->\r\n\t\t<div class=\"glosowanie\">            \r\n\t\t    <div class=\"vote-group\">        \r\n\t\t\t<span>Głosuj na ten utwór</span>\r\n\t\t\t<div class=\"flex-group song-rank\" data-id=\"2229585\">\r\n    <a rel=\"loginbox\" title=\"ranking +\" href=\"javascript:ajxRankSong('Up',2229585);\" class=\"na-plus\"></a>\r\n    <a rel=\"loginbox\" title=\"ranking -\" href=\"javascript:ajxRankSong('Down', 2229585);\" class=\"na-minus\"></a>\r\n    <span class=\"rank\">(0)</span>\r\n</div>\t\t    </div>\r\n\r\n\t\t    <div class=\"action-group\">\r\n<a href=\"javascript:ajxAddFav('2229585')\" rel=\"loginbox\" class=\"glo-links glo-ulubione\" title=\"Dodaj do ulubionych\">Dodaj do ulubionych</a>\r\n<a href=\"#dodaj_komentarz\" rel=\"loginbox\" class=\"glo-links glo-komentuj\" title=\"Komentuj\">Komentuj</a>\r\n<a href=\"javascript:sendSongBox();\" rel=\"loginbox\" class=\"glo-links glo-polec\" title=\"Poleć znajomemu\">Poleć znajomemu</a>\r\n<a href=\"javascript:errorBox();\" rel=\"loginbox\" class=\"glo-links glo-zglos\" title=\"Zgłoś błąd\">Zgłoś błąd</a>\r\n</div>\t\t</div>\r\n\t\t\t    </div>\r\n\r\n\t    \t\t<!-- tekst -->\r\n\t\t<div class=\"row mx-0 mt-3\"> \r\n\t\t\r\n\t\r\n\t\t    <div class=\"col-12 d-lg-none\">\r\n\t\t\t<div class=\"text-right mb-2\">\r\n\t\t\t    <div class=\"custom-control custom-switch\">\r\n\t\t\t\t<input type=\"checkbox\" class=\"custom-control-input loginbox\" id=\"toggleColumnsSwitch\">\r\n\t\t\t\t<label class=\"custom-control-label\" for=\"toggleColumnsSwitch\">Układ 2-kolumnowy</label>\r\n\t\t\t    </div>\r\n\t\t\t</div>\r\n\t\t    </div>\r\n\t\t    <div class=\"col-lg\">\t\t\t\r\n\t\t\t<div class=\"song-text\" id=\"songText\" data-id=\"2229585\">                          \r\n\t\t\t    <h2 class=\"mb-2\">Tekst piosenki:</h2>\r\n\r\n\t\t\t    <div class=\"inner-text\">Black eyed Susan<br>\r\nSun shines in your veins<br>\r\nIf the clouds are moving<br>\r\nNever hear her complain<br>\r\nYeah, black eyed Susan<br>\r\nJust waiting on a drop of rain<br>\r\n<br>\r\nBlack eyed Susan<br>\r\nStands true and tall<br>\r\nIf the storms are brewing<br>\r\nShe ain't worried at all<br>\r\nYeah, black eyed Susan<br>\r\nJust waiting on a drop to fall<br>\r\n<br>\r\nEveryone calls her the sunflower<br>\r\nAnd no one knows her name<br>\r\nShe's a little girl on the side of the road<br>\r\nWaiting on a drop of rain<br>\r\nRain<br>\r\nRain<br>\r\n<br>\r\nBlack eyed Susan<br>\r\nAin't it just a shame?<br>\r\n'Cause now you're losing<br>\r\nAll your petals in a vase<br>\r\nIn a vase<br>\r\nShe got picked and I never really knew her name<br>\r\nEveryone calls her the sunflower<br>\r\nAnd no one knows her name<br>\r\nShe's a little girl on the side of the road<br>\r\nWaiting on a drop of rain<br>\r\nRain<br>\r\nRain<br>\r\nYeah, she got picked and I never really knew her name</div>\r\n\t\t\t    <a rel=\"loginbox\" class=\"btn-secondary btn-block btn-sm my-2 py-1 add-annotation\" href=\"javascript:;\">Dodaj interpretację do tego tekstu »</a>\r\n\r\n\r\n\t\t\t    \r\n\t\t\t    \t\t\t\t<div class=\"adv-home\"> <!-- reklama środek -->\r\n\t\t\t\t        \t    \r\n\r\n\r\n\r\n<script async=\"\" src=\"//www.statsforads.com/tag/d5e49d0e-64d6-4751-ae6c-eb53cd6568f6.min.js\"></script>\r\n\r\n\r\n\r\n<ins class=\"staticpubads89354\" data-sizes-desktop=\"300x250\" data-sizes-mobile=\"300x250\" data-slot=\"4\">\r\n</ins>\r\n\t\t\t\t</div>\r\n\t\t\t    \t\t\t    <p>&nbsp;</p>\r\n\t\t\t    \t\t\t    <a href=\"javascript:;\" id=\"song_revisions_link\" class=\"pokaz-rev\" song_id=\"2229585\">Historia edycji tekstu <span class=\"icon\"></span></a>\r\n\t\t\t    <div style=\"margin: 0px; position: static; overflow: hidden; height: 0px;\"><div id=\"song_revisions\" class=\"revisions\" style=\"margin: -14px 0px 0px; overflow: hidden;\"></div></div>\r\n\t\t\t    \t\t\t</div>\r\n\t\t    </div>\r\n\r\n\t\t    <div class=\"col-lg mt-3 mt-md-0\"> \t\t\t    \r\n\t\t\t<div class=\"tlumaczenie\" id=\"songTranslation\" data-id=\"2229585\">\r\n\t\t\t    <h2 class=\"mb-2\">Tłumaczenie:</h2>\r\n\t\t\t    \t\t\t\t<p>\r\n\t\t\t\t    Niestety nikt nie dodał jeszcze tłumaczenia tego utworu.\r\n\t\t\t\t</p>\r\n\t\t\t\t\t\t\t\t<p>\r\n\t\t\t\t    <a href=\"/dodaj_tlumaczenie,bailey_bigger,black_eyed_susan.html\" class=\"pokaz-tlumaczenie\" rel=\"loginbox\" title=\"Dodaj tłumaczenie\">Dodaj tłumaczenie</a> lub \t\t\t\t    <a href=\"javascript:ajxSearchTrans('2229585');\" rel=\"loginbox\" class=\"pokaz-tlumaczenie\" title=\"wyślij prośbę o tłumaczenie\">wyślij prośbę o tłumaczenie</a>\r\n\t\t\t\t\t\t\t\t</p>\r\n\t\t\t    \r\n\t\t\t    <div id=\"translation\" class=\"id-\" data-id=\"\">\r\n\t\t\t\t<div class=\"inner-text\"></div>\r\n\t\t\t\t\r\n\t\t\t\t\t\t\t\t<p>&nbsp;</p>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t    </div>\r\n\r\n\t\t\t    \t\t\t\t<div class=\"adv-home\"> <!-- reklama środek -->\r\n\t\t\t\t        \t    \r\n\r\n    \r\n\r\n<script async=\"\" src=\"https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js\"></script>\r\n<!-- tekstowo kwadrat tłumaczenie elastyczna -->\r\n<ins class=\"adsbygoogle\" style=\"display:block\" data-ad-client=\"ca-pub-3653916656187385\" data-ad-slot=\"9521483524\" data-ad-format=\"auto\" data-full-width-responsive=\"true\"><iframe id=\"aswift_1\" style=\"height: 1px !important; max-height: 1px !important; max-width: 1px !important; width: 1px !important;\"><iframe id=\"google_ads_frame1\"></iframe></iframe></ins>\r\n<script>\r\n     (adsbygoogle = window.adsbygoogle || []).push({});\r\n</script>\r\n\t\t\t\t</div>  \r\n\t\t\t    \r\n\t\t\t</div>\r\n\t\t    </div>\r\n\t\t    <div class=\"col-12\">\r\n\t\t\t<a href=\"/drukuj,bailey_bigger,black_eyed_susan.html\" class=\"drukuj blank btn btn-light2 my-3\" rel=\"nofollow\" title=\"Drukuj tekst\">\r\n\t\t\t    Drukuj tekst <span class=\"icon\"></span>\r\n\t\t\t</a>\r\n\t\t    </div>\r\n\t\t</div>\r\n\t\t<!-- end tekst -->\r\n\r\n\t\t<div class=\"row mx-0\">\r\n\t\t    <div class=\"col-lg px-md-0\">\r\n\t\t\t<div class=\"metric\">\r\n\t\t\t    <table>\r\n\t\t\t\t\t\t\t\t<tbody><tr><th>Autor:</th><td><p>(brak) </p><a rel=\"loginbox\" href=\"/edytuj_tekst,bailey_bigger,black_eyed_susan.html?metric=1\" class=\"edit btn btn-sm btn-primary\" title=\"Edytuj metrykę\">Edytuj metrykę</a></td></tr>\r\n\t\t\t\t    \t\t\t\t    \t\t\t</tbody></table>\r\n\t\t    </div>\r\n\t\t</div>\r\n\t    </div>\r\n\r\n\r\n\t    <!-- glosowanie -->\r\n\t    <div class=\"glosowanie mt-3\">            \r\n\t\t<div class=\"vote-group\">\r\n\t\t    <span>Głosuj na ten utwór</span>\r\n\t\t    <div class=\"flex-group song-rank\" data-id=\"2229585\">\r\n    <a rel=\"loginbox\" title=\"ranking +\" href=\"javascript:ajxRankSong('Up',2229585);\" class=\"na-plus\"></a>\r\n    <a rel=\"loginbox\" title=\"ranking -\" href=\"javascript:ajxRankSong('Down', 2229585);\" class=\"na-minus\"></a>\r\n    <span class=\"rank\">(0)</span>\r\n</div>\t\t</div>\r\n\r\n\t\t<div class=\"action-group\">\r\n<a href=\"javascript:ajxAddFav('2229585')\" rel=\"loginbox\" class=\"glo-links glo-ulubione\" title=\"Dodaj do ulubionych\">Dodaj do ulubionych</a>\r\n<a href=\"#dodaj_komentarz\" rel=\"loginbox\" class=\"glo-links glo-komentuj\" title=\"Komentuj\">Komentuj</a>\r\n<a href=\"javascript:sendSongBox();\" rel=\"loginbox\" class=\"glo-links glo-polec\" title=\"Poleć znajomemu\">Poleć znajomemu</a>\r\n<a href=\"javascript:errorBox();\" rel=\"loginbox\" class=\"glo-links glo-zglos\" title=\"Zgłoś błąd\">Zgłoś błąd</a>\r\n</div>\t    </div>\r\n\t    \t\t<!-- komentarze -->\r\n\t<div class=\"row mt-4 mx-0\">\r\n\t    <div class=\"col-12 px-md-0\">\r\n\t\t<a name=\"komentarze\"></a>\r\n\r\n\t\t<div id=\"comments_content\" class=\"d-none\">\r\n\t\t    <h2 class=\"margint10\">Komentarze (0):</h2>\r\n\t\t    \t\t    \t\t    \r\n\t\t</div>\r\n\r\n\t\t<div id=\"comm_show_more\" class=\"comm_show_more d-none\"></div>\r\n\t    </div>\r\n\r\n\t    <div class=\"col-12\">\r\n\t\t\t\t<noscript>\r\n</noscript>\r\n\t    </div>\r\n\t</div>\r\n\r\n\t<!-- end komentarze -->\r\n\r\n    </div>\r\n</div> <!-- end right column -->\r\n\r\n    <script type=\"text/javascript\">\r\n\t\r\n\t    function addLink() {\r\n\t\tvar body_element = document.getElementsByTagName('body')[0];\r\n\t\tvar selection;\r\n\t\tselection = window.getSelection();\r\n\t\tvar pagelink = \"<br /><br />Tekst pochodzi z <a href='\" + document.location.href + \"'>\" + document.location.href + \"</a>\";\r\n\t\tvar copytext = selection + pagelink;\r\n\t\tvar newdiv = document.createElement('div');\r\n\t\tnewdiv.style.position = 'absolute';\r\n\t\tnewdiv.style.left = '-99999px';\r\n\t\tbody_element.appendChild(newdiv);\r\n\t\tnewdiv.innerHTML = copytext;\r\n\t\tselection.selectAllChildren(newdiv);\r\n\t\twindow.setTimeout(function () {\r\n\t\t    body_element.removeChild(newdiv);\r\n\t\t}, 0);\r\n\t    }\r\n\t    document.getElementById('songText').oncopy = addLink;\r\n\t    document.getElementById('songTranslation').oncopy = addLink;\r\n\r\n\t\r\n    </script>\r\n\r\n    <script type=\"text/javascript\">\r\n\t\r\n\t    var i18nAnn = {\r\n\t\tadd: 'Dodaj',\r\n\t\tadd_title: 'Dodawanie interpretacji',\r\n\t\tadd_desc: 'Dodawanie interpretacji do zaznaczonego tekstu:',\r\n\t\tfunction_unavailble: 'Funkcja niedostępna na urządzeniach dotykowych. Skorzystaj z komputera klasy PC.',\r\n\t\tenabled: 'Uruchomiony został tryb dodawania interpretacji. Zaznacz fragment tekstu, który chcesz zinterpretować',\r\n\t\texit: 'Kliknij tutaj aby wyjść',\r\n\t\texists: 'Istnieje już interpretacja do tego fragmentu, zaznacz inny fragment tekstu',\r\n\t\ttoo_short: 'Treść jest za krótka, napisz coś więcej.',\r\n\t\ttextarea_placehodler: 'Tutaj pisz interpretację',\r\n\t\tsuccess_msg: 'Interpretacja została dodana i oczekuje akceptacji moderatora. Dziękujemy.',\r\n\t\tadded_msg: 'Dodałeś/aś już jedną interpretacje do tego utworu, która oczekuje akceptacji moderatora. <br>Poczekaj na akceptację.'\r\n\t    }\r\n\t\r\n    </script>\r\n</div>\r\n\r\n</div> <!-- end center -->\r\n</div>\r\n\r\n\r\n    <div class=\"generic_dialog outer\" id=\"fb-modal\" style=\"opacity: 0; display: none; visibility: hidden;\">\r\n\t<div class=\"generic_dialog_popup middle\">\r\n\t\t<table class=\"pop_dialog_table inner\" id=\"pop_dialog_table\">\r\n\t\t\t<tbody>\r\n\t\t\t\t<tr>\r\n\t\t\t\t\t<td id=\"pop_content\" class=\"pop_content\">\r\n\t\t\t\t\t\t<h2 class=\"dialog_title\"><span></span></h2>\r\n\t\t\t\t\t\t<div class=\"dialog_content\">\r\n\t\t\t\t\t\t\t<p id=\"modal-p\"></p>\r\n\t\t\t\t\t\t\t<div class=\"dialog_buttons\">\r\n\t\t\t\t\t\t\t\t<input type=\"button\" value=\"Zamknij\" name=\"close\" class=\"inputsubmit\" id=\"fb-close\">\r\n\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t</div>\r\n\t\t\t\t\t</td>\r\n\t\t\t\t</tr>\r\n\t\t\t</tbody>\r\n\t\t</table>\r\n\t</div>\r\n</div>    <div id=\"login-modal\">\r\n    <div style=\"position: relative\">\r\n\t<a id=\"yt-close\" href=\"javascript:modalFadeOut();\" title=\"Zamknij\"></a>\r\n    </div>\r\n    <div class=\"login_box\">\r\n\t<div class=\"row justify-content-md-center\">\r\n\t    <div class=\"col col-10\">\r\n\t\t<img src=\"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20441.8%20136%22%3E%3Cstyle%3E.st0%7Bfill%3A%2362ae25%7D.st1%7Bfill%3A%23999%7D%3C%2Fstyle%3E%3Cg%20id%3D%22logo_1_%22%3E%3Cpath%20class%3D%22st0%22%20d%3D%22M130.7%2046v9h6.5v6.2h-6.5v20.6c0%201.4.3%202.4.8%203.1.6.6%201.6.9%203%20.9%201%200%201.9-.1%202.9-.3v6.4c-1.9.5-3.7.8-5.5.8-6.4%200-9.6-3.5-9.6-10.6v-21h-6.1V55h6.1v-9h8.4zM156.3%2092.7c-5.3%200-9.5-1.7-12.8-5s-4.9-7.7-4.9-13.2v-1c0-3.7.7-7%202.1-9.9%201.4-2.9%203.4-5.2%206-6.8s5.4-2.4%208.6-2.4c5%200%208.9%201.6%2011.7%204.8%202.7%203.2%204.1%207.8%204.1%2013.6v3.3H147c.3%203.1%201.3%205.5%203.1%207.2%201.8%201.8%204%202.7%206.8%202.7%203.8%200%206.9-1.5%209.3-4.6l4.5%204.3c-1.5%202.2-3.5%203.9-5.9%205.1-2.6%201.3-5.4%201.9-8.5%201.9zm-1-31.7c-2.3%200-4.1.8-5.5%202.4-1.4%201.6-2.3%203.8-2.7%206.7h15.8v-.6c-.2-2.8-.9-4.9-2.2-6.3-1.3-1.5-3.1-2.2-5.4-2.2zM186.1%2076.1l-3.7%203.8V92h-8.3V39.5h8.3v30.3l2.6-3.2L195.2%2055h10l-13.7%2015.4L206.7%2092h-9.6l-11-15.9z%22%2F%3E%3Cpath%20class%3D%22st0%22%20d%3D%22M227.9%2082c0-1.5-.6-2.6-1.8-3.4s-3.2-1.5-6.1-2.1c-2.8-.6-5.2-1.3-7.1-2.3-4.1-2-6.2-4.9-6.2-8.7%200-3.2%201.3-5.9%204-8s6.1-3.2%2010.3-3.2c4.4%200%208%201.1%2010.7%203.3s4.1%205%204.1%208.5h-8.3c0-1.6-.6-2.9-1.8-4s-2.8-1.6-4.7-1.6c-1.8%200-3.3.4-4.5%201.3-1.2.8-1.7%202-1.7%203.4%200%201.3.5%202.3%201.6%203s3.2%201.4%206.5%202.1%205.8%201.6%207.7%202.6%203.2%202.2%204.1%203.6c.9%201.4%201.3%203.1%201.3%205.1%200%203.3-1.4%206-4.1%208.1-2.8%202.1-6.4%203.1-10.8%203.1-3%200-5.7-.5-8.1-1.6-2.4-1.1-4.2-2.6-5.5-4.5s-2-4-2-6.2h8.1c.1%202%20.9%203.5%202.2%204.5%201.4%201.1%203.2%201.6%205.4%201.6s3.9-.4%205-1.2c1.1-1%201.7-2.1%201.7-3.4zM250.2%2046v9h6.5v6.2h-6.5v20.6c0%201.4.3%202.4.8%203.1.6.6%201.6.9%203%20.9%201%200%201.9-.1%202.9-.3v6.4c-1.9.5-3.7.8-5.5.8-6.4%200-9.6-3.5-9.6-10.6v-21h-6.1V55h6.1v-9h8.4zM257%2073.2c0-3.6.7-6.9%202.2-9.8s3.5-5.1%206-6.7c2.6-1.6%205.6-2.3%209-2.3%205%200%209%201.6%2012.2%204.8%203.1%203.2%204.8%207.5%205%2012.8v1.9c0%203.6-.7%206.9-2.1%209.8s-3.4%205.1-6%206.7-5.6%202.4-9%202.4c-5.2%200-9.4-1.7-12.5-5.2s-4.7-8.1-4.7-13.9v-.5h-.1zm8.3.7c0%203.8.8%206.8%202.4%208.9%201.6%202.2%203.8%203.2%206.6%203.2s5-1.1%206.5-3.3c1.6-2.2%202.3-5.4%202.3-9.6%200-3.7-.8-6.7-2.4-8.9-1.6-2.2-3.8-3.3-6.5-3.3s-4.9%201.1-6.5%203.2c-1.6%202.3-2.4%205.5-2.4%209.8zM327.4%2080.5l5.9-25.5h8.1l-10.1%2037h-6.8l-7.9-25.4-7.9%2025.4h-6.8l-10.1-37h8.1l6%2025.3%207.6-25.3h6.3l7.6%2025.5zM341.8%2073.2c0-3.6.7-6.9%202.2-9.8s3.5-5.1%206-6.7c2.6-1.6%205.6-2.3%209-2.3%205%200%209%201.6%2012.2%204.8%203.1%203.2%204.8%207.5%205%2012.8v1.9c0%203.6-.7%206.9-2.1%209.8s-3.4%205.1-6%206.7-5.6%202.4-9%202.4c-5.2%200-9.4-1.7-12.5-5.2s-4.7-8.1-4.7-13.9v-.5h-.1zm8.3.7c0%203.8.8%206.8%202.4%208.9%201.6%202.2%203.8%203.2%206.6%203.2s5-1.1%206.5-3.3c1.6-2.2%202.3-5.4%202.3-9.6%200-3.7-.8-6.7-2.4-8.9-1.6-2.2-3.8-3.3-6.5-3.3s-4.9%201.1-6.5%203.2c-1.6%202.3-2.4%205.5-2.4%209.8zM384.9%2083.3c1.5%200%202.7.4%203.6%201.3.8.9%201.3%202%201.3%203.3s-.4%202.4-1.3%203.2c-.8.9-2%201.3-3.6%201.3-1.5%200-2.7-.4-3.5-1.3-.9-.8-1.3-1.9-1.3-3.2%200-1.3.4-2.4%201.3-3.3.8-.9%202-1.3%203.5-1.3zM428.2%2073.9c0%205.7-1.3%2010.3-3.9%2013.7s-6.1%205.1-10.5%205.1c-4.1%200-7.3-1.3-9.7-4v17.5h-8.3V55h7.7l.3%203.8c2.4-3%205.8-4.4%209.9-4.4%204.5%200%208%201.7%2010.6%205%202.6%203.4%203.8%208%203.8%2014v.5h.1zm-8.3-.7c0-3.7-.7-6.6-2.2-8.8-1.5-2.2-3.6-3.2-6.3-3.2-3.4%200-5.8%201.4-7.3%204.2v16.4c1.5%202.9%204%204.3%207.4%204.3%202.6%200%204.7-1.1%206.2-3.2%201.5-2.2%202.2-5.4%202.2-9.7zM440.5%2092h-8.3V39.5h8.3V92z%22%2F%3E%3Cpath%20class%3D%22st1%22%20d%3D%22M123.5%20107.6l-.5%202.8h2.1l-.3%201.5h-2.1l-1.2%207.2v.5c0%20.6.4.8.9.8.2%200%20.6%200%20.9-.1l-.2%201.6c-.5.1-.9.2-1.4.2-.8%200-1.4-.3-1.7-.9-.4-.6-.5-1.3-.5-2.2l1.2-7.2h-2.1l.3-1.5h2.1l.5-2.8%202%20.1zM129.3%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2.7.8%201.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.6.8-2.6.8zm1.3-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1.1-.8-1.8-.8zM141.8%20112.1l-.9-.1c-1.2%200-2.2.6-2.9%201.7l-1.4%208.3h-1.9l2-11.6h1.9l-.3%201.4c.8-1.1%201.8-1.6%202.9-1.6.2%200%20.5.1.9.2l-.3%201.7zM147.5%20122c-.1-.2-.1-.4-.1-.6v-.6c-1.1%201-2.2%201.5-3.4%201.4-1%200-1.9-.3-2.5-1-.6-.6-.9-1.4-.9-2.4.1-1.2.6-2.2%201.6-2.9%201-.7%202.3-1%204-1h2l.1-.9c.1-.7-.1-1.2-.4-1.6-.3-.4-.9-.6-1.5-.6-.7%200-1.3.1-1.8.5-.5.3-.8.8-.9%201.3h-2c.1-.7.3-1.3.8-1.8s1.1-.9%201.8-1.2c.7-.3%201.5-.4%202.3-.4%201.2%200%202.2.4%202.8%201.1.7.7%201%201.6.8%202.8l-.9%205.8-.1.8c0%20.4%200%20.8.1%201.2v.2h-1.8v-.1zm-3-1.5c.6%200%201.2-.1%201.8-.4s1-.7%201.4-1.3l.4-2.4h-1.5c-1.2%200-2.2.2-2.8.6-.7.4-1%20.9-1.1%201.6-.1.5.1%201%20.4%201.4.3.3.8.5%201.4.5zM153.2%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM168.8%20110.4l-.2%201.3c1-1%202.2-1.5%203.5-1.5.7%200%201.4.2%201.9.5s.9.8%201.1%201.4c1.1-1.3%202.4-1.9%203.9-1.9%201.2%200%202%20.4%202.6%201.2.6.8.8%201.8.6%203l-1.3%207.6H179l1.3-7.7v-1c-.1-1-.8-1.5-1.9-1.5-.7%200-1.4.2-1.9.7-.6.5-.9%201.1-1.1%201.8l-1.3%207.7h-2l1.3-7.6c.1-.8%200-1.4-.4-1.8s-.9-.7-1.6-.7c-1.2%200-2.2.5-2.9%201.7l-1.5%208.5h-1.9l2-11.6%201.7-.1zM189.1%20110.2c1%200%201.8.3%202.5.8s1.2%201.2%201.5%202.1.4%201.9.3%203v.2c-.1%201.1-.5%202.2-1%203.1s-1.2%201.6-2.1%202.1c-.9.5-1.8.7-2.8.7s-1.8-.3-2.5-.8-1.2-1.2-1.5-2.1-.4-1.9-.3-2.9c.1-1.2.4-2.3%201-3.2.5-1%201.2-1.7%202.1-2.2.8-.6%201.8-.9%202.8-.8zm-4%206.2c-.1.5-.1.9%200%201.4.1.8.3%201.5.8%202%20.4.5%201%20.8%201.7.8.6%200%201.2-.1%201.8-.5.5-.3%201-.9%201.4-1.5.4-.7.6-1.5.7-2.3.1-.7.1-1.2%200-1.7-.1-.9-.3-1.6-.8-2.1-.4-.5-1-.8-1.7-.8-1%200-1.9.4-2.6%201.2-.7.8-1.1%201.9-1.3%203.2v.3zM195.8%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zm3.2-13c0-.3.1-.6.3-.8.2-.2.5-.3.8-.4.3%200%20.6.1.8.3.2.2.3.5.3.8s-.1.6-.3.8c-.2.2-.5.3-.8.3s-.6-.1-.8-.3c-.2-.1-.3-.4-.3-.7zM208.1%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2.7.8%201.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1-.8-1.8-.8zM220.6%20118.9c.1-.8-.3-1.4-1.3-1.7l-2-.6c-1.7-.6-2.5-1.6-2.5-2.9%200-1%20.5-1.9%201.4-2.5.9-.7%201.9-1%203.1-1%201.2%200%202.1.4%202.9%201%20.7.7%201.1%201.5%201.1%202.6h-1.9c0-.6-.2-1.1-.5-1.4s-.9-.6-1.5-.6c-.7%200-1.3.2-1.7.5-.5.3-.7.7-.8%201.3-.1.7.3%201.2%201.2%201.5l1%20.3c1.3.3%202.3.8%202.8%201.3s.8%201.2.8%202.1c0%20.7-.3%201.4-.7%201.9s-1%20.9-1.7%201.2c-.7.3-1.5.4-2.3.4-1.2%200-2.2-.4-3.1-1.1-.8-.7-1.2-1.6-1.2-2.7h1.9c0%20.7.2%201.2.6%201.6.4.4%201%20.6%201.7.6s1.3-.1%201.8-.4c.5-.5.8-.9.9-1.4zM225.5%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM241.3%20110.4l-2.2%2013c-.1%201.1-.5%201.9-1.1%202.5s-1.4.9-2.3.8c-.4%200-.8-.1-1.3-.2l.2-1.6c.3.1.6.1.9.1.9%200%201.5-.6%201.7-1.7l2.2-13%201.9.1zm-1.6-3.1c0-.3.1-.6.3-.8.2-.2.5-.3.8-.4.3%200%20.6.1.8.3.2.2.3.5.3.8s-.1.6-.3.8-.5.3-.8.3-.6-.1-.8-.3-.3-.4-.3-.7zM246.4%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2s1.1%201.8%201.1%203.1v1.4l-.1.9H244c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1-.8-1.8-.8zM266.5%20116.2c-.1%201.2-.5%202.2-1%203.2s-1.1%201.7-1.8%202.1c-.7.5-1.5.7-2.4.7-1.3%200-2.4-.5-3.1-1.4l-1%205.6h-1.9l2.8-16.1h1.8l-.2%201.3c1-1%202.1-1.5%203.4-1.5%201.1%200%202%20.4%202.6%201.2s1%201.9%201%203.3c0%20.5%200%20.9-.1%201.3l-.1.3zm-1.9-.2l.1-.9c0-1-.2-1.8-.6-2.4s-1-.8-1.7-.9c-1.1%200-2.1.5-2.9%201.6l-1%205.6c.4%201%201.2%201.6%202.4%201.6%201%200%201.8-.4%202.5-1.1.5-.8.9-2%201.2-3.5zM274.2%20112.1l-.9-.1c-1.2%200-2.2.6-2.9%201.7L269%20122h-1.9l2-11.6h1.9l-.3%201.4c.8-1.1%201.8-1.6%202.9-1.6.2%200%20.5.1.9.2l-.3%201.7zM275.3%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM287.6%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2s1.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.4-.5-1-.8-1.8-.8zM297.8%20120.6c.7%200%201.3-.2%201.9-.6s.9-1%201.1-1.6h1.8c-.1.7-.4%201.4-.9%202s-1.1%201.1-1.8%201.4-1.5.5-2.2.5c-1%200-1.8-.3-2.5-.8s-1.2-1.2-1.5-2.1-.4-1.9-.3-3l.1-.5c.1-1.1.5-2.1%201-3s1.2-1.6%202-2.1%201.7-.7%202.8-.7c1.2%200%202.2.4%202.9%201.2s1.1%201.8%201.1%203h-1.8c0-.8-.2-1.4-.6-1.8-.4-.5-1-.7-1.7-.7-1%200-1.8.4-2.5%201.1-.7.8-1.1%201.9-1.3%203.2v1.7c0%20.8.3%201.5.7%202s1%20.8%201.7.8zM305.2%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM316.8%20119.1l4.1-8.7h2.1l-6.9%2013.6c-1%201.9-2.2%202.8-3.7%202.8-.3%200-.7-.1-1.2-.2l.2-1.6.5.1c.6%200%201.1-.1%201.6-.4.4-.3.8-.8%201.2-1.5l.7-1.3-2-11.4h2l1.4%208.6z%22%2F%3E%3Cpath%20class%3D%22st1%22%20d%3D%22M326.9%20107.6l-.5%202.8h2.1l-.3%201.5h-2.1l-1.2%207.2v.5c0%20.6.4.8.9.8.2%200%20.6%200%20.9-.1l-.2%201.6c-.5.1-.9.2-1.4.2-.8%200-1.4-.3-1.7-.9-.4-.6-.5-1.3-.5-2.2l1.2-7.2H322l.3-1.5h2.1l.5-2.8%202%20.1zM334.8%20122c0-.2-.1-.4-.1-.6v-.6c-1.1%201-2.2%201.5-3.4%201.4-1%200-1.9-.3-2.5-1-.6-.6-.9-1.4-.9-2.4.1-1.2.6-2.2%201.6-2.9%201-.7%202.3-1%204-1h2l.1-.9c.1-.7-.1-1.2-.4-1.6s-.9-.6-1.5-.6c-.7%200-1.3.1-1.8.5-.5.3-.8.8-.9%201.3h-2c.1-.7.3-1.3.8-1.8s1.1-.9%201.8-1.2c.7-.3%201.5-.4%202.3-.4%201.2%200%202.2.4%202.8%201.1.7.7%201%201.6.8%202.8l-.9%205.8-.1.8c0%20.4%200%20.8.1%201.2v.2h-1.8v-.1zm-3-1.5c.6%200%201.2-.1%201.8-.4.6-.3%201-.7%201.4-1.3l.4-2.4h-1.5c-1.2%200-2.2.2-2.8.6s-1%20.9-1.1%201.6c-.1.5.1%201%20.4%201.4.3.3.8.5%201.4.5zM343.3%20120.6c.7%200%201.3-.2%201.9-.6s.9-1%201.1-1.6h1.8c-.1.7-.4%201.4-.9%202s-1.1%201.1-1.8%201.4-1.5.5-2.2.5c-1%200-1.8-.3-2.5-.8s-1.2-1.2-1.5-2.1-.4-1.9-.3-3l.1-.5c.1-1.1.5-2.1%201-3s1.2-1.6%202-2.1%201.7-.7%202.8-.7c1.2%200%202.2.4%202.9%201.2s1.1%201.8%201.1%203H347c0-.8-.2-1.4-.6-1.8-.4-.5-1-.7-1.7-.7-1%200-1.8.4-2.5%201.1-.7.8-1.1%201.9-1.3%203.2v1.7c0%20.8.3%201.5.7%202s.9.8%201.7.8zm2.7-15.1h2.5l-3.3%203.2h-1.7l2.5-3.2z%22%2F%3E%3Cpath%20id%3D%22box%22%20d%3D%22M92.9%20124H7.1c-3.9%200-7.1-3.2-7.1-7.1V31.1C0%2027.2%203.2%2024%207.1%2024h85.8c3.9%200%207.1%203.2%207.1%207.1v85.8c0%203.9-3.2%207.1-7.1%207.1z%22%20fill%3D%22%23f60%22%2F%3E%3Cpath%20d%3D%22M68%2044.1c-6.8-7-15.9-12.2-24.6-14.4C28.8%2026%2025%2023.9%2017.8%2019c-4.1-2.8-7.2-8.4-10.6-9.8-1.6-.6-2.6.1-3.1.7-.9%201-1.6%202.9-.4%205.2%2010.1%2019.8%2048.4%2076.8%2048.4%2076.8-5.9%201.1-12.4%205.7-17%2012.9-6.9%2010.7-7%2022.9-.1%2027.2%206.9%204.3%2018.1-.9%2025-11.6%205.9-9%206.8-19.1%202.8-24.6L29.2%2042.7s-3.9-4.7%202.3-6.1c4.5-1%2013.9.1%2019%202.8C56%2042.3%2069%2051.7%2072.3%2065.1c1.5%206.2.6%209.6%201.4%2015.1.4%202.6%203.2%204.8%205.5%201.2%201.2-1.9%201.4-8.4%201.1-13.5-.5-6.7-6-17.3-12.3-23.8zm-10.4%2048c.3.1.7.2%201%20.3-.3-.1-.6-.2-1-.3zm-2.5-.4h.4-.4zm1.1.1c.3%200%20.6.1.8.1-.2%200-.5-.1-.8-.1zm-3%200c.1%200%20.1%200%200%200%20.1%200%20.1%200%200%200zm5.8.8l1.2.6c-.4-.3-.8-.5-1.2-.6zm2.9%202zm-.7-.7c-.3-.3-.6-.5-1-.7.3.2.6.5%201%20.7z%22%20opacity%3D%22.2%22%2F%3E%3ClinearGradient%20id%3D%22SVGID_1_%22%20gradientUnits%3D%22userSpaceOnUse%22%20x1%3D%2239.888%22%20y1%3D%2292.284%22%20x2%3D%2252.096%22%20y2%3D%22125.824%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%238fe132%22%2F%3E%3Cstop%20offset%3D%22.362%22%20stop-color%3D%22%2365bd23%22%2F%3E%3Cstop%20offset%3D%22.668%22%20stop-color%3D%22%2349a519%22%2F%3E%3Cstop%20offset%3D%22.844%22%20stop-color%3D%22%233e9c15%22%2F%3E%3C%2FlinearGradient%3E%3Cpath%20d%3D%22M57.9%20117.8c-7.6%2010.4-19.2%2014.9-25.7%2010.1s-5.7-17.2%201.9-27.6%2019.2-14.9%2025.7-10.1c6.6%204.9%205.7%2017.2-1.9%2027.6z%22%20fill%3D%22url%28%23SVGID_1_%29%22%2F%3E%3ClinearGradient%20id%3D%22SVGID_2_%22%20gradientUnits%3D%22userSpaceOnUse%22%20x1%3D%22-3.574%22%20y1%3D%2224.316%22%20x2%3D%2282.773%22%20y2%3D%2274.169%22%3E%3Cstop%20offset%3D%22.184%22%20stop-color%3D%22%238fe132%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%233e9c15%22%2F%3E%3C%2FlinearGradient%3E%3Cpath%20d%3D%22M70.9%2041c-6.3-7.6-15-13.4-23.6-16.2-14.4-4.7-18-7.1-24.8-12.5-3.9-3.1-6.6-9-9.9-10.7-1.6-.8-2.6-.1-3.2.5-.9.9-1.8%202.8-.7%205.2C17.4%2028%2051.9%2088.4%2051.9%2088.4c3-.3%205.7.2%207.9%201.8%201%20.8%201.8%201.7%202.5%202.8l-30-56s-3.6-5%202.7-6c4.5-.7%2013.9%201.1%2018.8%204.1%205.3%203.3%2017.6%2013.7%2020%2027.5%201.1%206.4%200%209.7.4%2015.4.2%202.6%202.9%205.1%205.4%201.6%201.3-1.8%202-8.4%202-13.6%200-6.8-4.9-17.9-10.7-25z%22%20fill%3D%22url%28%23SVGID_2_%29%22%2F%3E%3CradialGradient%20id%3D%22SVGID_3_%22%20cx%3D%2221.505%22%20cy%3D%22103.861%22%20r%3D%2214.934%22%20gradientTransform%3D%22matrix%28.2966%20.4025%20-.805%20.5933%20123.22%2029.092%29%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%23f4ff72%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%2373c928%22%20stop-opacity%3D%220%22%2F%3E%3C%2FradialGradient%3E%3Cpath%20d%3D%22M50.4%20105.4c-6.6%204.9-14%206.2-16.5%202.8-2.4-3.3%201-10%207.6-14.9s14-6.2%2016.5-2.8c2.5%203.3-.9%2010-7.6%2014.9z%22%20fill%3D%22url%28%23SVGID_3_%29%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E\" class=\"img-fluid\" alt=\"tekstowo.pl\" title=\"Teksty piosenek, tłumaczenia, teledyski\">\t    </div>\r\n\t</div>\r\n\t<div class=\"login-wrap\">\r\n\t    <fieldset class=\"login rejestracja edycja okienko\">\r\n\r\n\t\t<div class=\"row\">\r\n\t\t    <div class=\"col text-center\">\r\n\t\t\t<p><strong>Aby wykonać tę operację należy się zalogować:</strong></p>\r\n\t\t    </div>\r\n\t\t</div>\r\n\r\n\t\t<div class=\"l-box\">\r\n\t\t    <div class=\"formRow\">\r\n\t\t\tZaloguj się przy użyciu loginu i hasła:\r\n\t\t\t<br>\r\n\t\t\t<a class=\"green-button btn btn-primary btn-lg btn-block mt-1\" href=\"https://www.tekstowo.pl/logowanie.html\">Zaloguj</a>\r\n\t\t\t<div class=\"row my-2 mx-0\">\r\n\t\t\t    <div class=\"col-sm\">\r\n\t\t\t\t<a href=\"/rejestracja.html\" class=\"green underline bold\" title=\"Rejestracja\">Rejestracja</a>\r\n\t\t\t    </div>\r\n\t\t\t    <div class=\"col-auto\">\r\n\t\t\t\t<a href=\"/przypomnij.html\" class=\"green underline bold marginl10\" title=\"Przypomnienie hasła\">Przypomnienie hasła</a>\r\n\t\t\t    </div>\r\n\t\t\t</div>\r\n\t\t    </div>\r\n\r\n\t\t    <div class=\"fb-box mt-3\">Inne metody logowania:\r\n\t\t\t<a href=\"javascript:;\" class=\"fb-button my-fb-login-button btn btn-block btn-fb btn-lg mt-1\" title=\"Zaloguj się z Facebookiem\">\r\n\t\t\t    <span class=\"icon\"></span>Zaloguj się przez Facebooka</a>\r\n\t\t    </div>\r\n\t    </div></fieldset>\r\n\t</div>\r\n    </div>\r\n</div>\r\n\r\n    <div id=\"sendsong-modal\" style=\"display: none;\">\r\n    <div>\r\n\t<a id=\"yt-close\" href=\"javascript:modalFadeOut();\" title=\"Zamknij\"></a>\r\n    </div>\r\n    <div class=\"login_box\">\r\n\r\n\t<form id=\"formSendSongInvite\" action=\"\" onsubmit=\"ajxSendSongInvite();\r\n\t\treturn false;\" method=\"post\">\r\n\r\n\t    <div class=\"row\">\r\n\t\t<div class=\"col text-center lead my-3\">\r\n\t\t    Podaj adres E-mail znajomego, któremu chcesz polecić ten utwór.\r\n\t\t</div>\r\n\t    </div>\r\n\r\n\t    \t    \r\n\t    <div class=\"form-group row\">\r\n\t\t<label for=\"invite_email\" class=\"col-sm-2 col-form-label\">E-mail:</label>\r\n\t\t<div class=\"col-sm-10\">\r\n\t\t    <input type=\"text\" class=\"form-control\" name=\"invite_email\" id=\"invite_email\" value=\"\">\r\n\t\t    <div class=\"invalid-feedback\" id=\"error_invemail\" style=\"display: none\">\r\n\t\t\tPodany E-mail jest nieprawidłowy.\r\n\t\t    </div>\r\n\t\t</div>\r\n\t    </div>\r\n\r\n\t    <div class=\"form-group row\">\r\n\t\t<div class=\"col d-flex justify-content-center mt-3\">\r\n\t\t    <input type=\"hidden\" name=\"song_id\" value=\"2229585\">\r\n\t\t    <button type=\"submit\" class=\"btn btn-primary mr-3 px-5\">Wyślij</button>\r\n\t\t    <button type=\"button\" onclick=\"modalFadeOut();\" class=\"btn btn-secondary px-5\">Anuluj</button>\r\n\t\t</div>\r\n\t    </div>\r\n\t</form>\r\n    </div>\r\n</div>\r\n<div class=\"container order-3\">\r\n    <div id=\"bottom\" class=\"row\">\r\n        <div id=\"stopka\" class=\"col\">\r\n\r\n            \t    <div class=\"row footbar py-3 my-3 d-md-none\">\r\n\t\t<div class=\"col text-center\">\r\n\t\t    2 170 745 tekstów, 20 217 poszukiwanych i 502 oczekujących\r\n\t\t</div>\r\n\t    </div>\r\n            <hr>\r\n            <p>Największy serwis z tekstami piosenek w Polsce. Każdy może znaleźć u nas teksty piosenek, teledyski oraz tłumaczenia swoich ulubionych utworów.<br> Zachęcamy wszystkich użytkowników do dodawania nowych tekstów, tłumaczeń i teledysków! </p>\r\n            <hr>\r\n            <a href=\"/reklama.html\" class=\"bottom-links\" title=\"Reklama\">Reklama</a> |\r\n            <a href=\"/kontakt.html\" class=\"bottom-links\" title=\"Kontakt\">Kontakt</a> |\r\n            <a href=\"/faq.html\" class=\"bottom-links\" title=\"FAQ\">FAQ</a>\r\n            <a href=\"/polityka-prywatnosci.html\" class=\"bottom-links\" title=\"Polityka prywatności\">Polityka prywatności</a>\r\n        </div>\r\n    </div>\r\n</div><!-- end container -->\r\n\r\n<div style=\"color: white; text-align: center; background-color: white; width: 100%; margin: 0 auto;\"><span id=\"debug\"></span></div>\r\n\r\n<div id=\"spinner\" style=\"display:none;\">\r\n    <div id=\"spinner\" class=\"spinner-grow text-primary\" role=\"status\">\r\n\t<span class=\"sr-only\">Proszę czekać...</span>\r\n    </div>\r\n</div>\r\n\r\n<div class=\" d-none d-md-block fb-panel \">\r\n    <a href=\"https://www.facebook.com/tekstowo/\" target=\"_blank\" class=\"slide_button\"></a>\r\n    <div class=\"fb\"></div>\r\n</div>\r\n\r\n    \t    \r\n\r\n    \r\n\r\n<script type=\"text/javascript\">\r\n\r\n  var _gaq = _gaq || [];\r\n  _gaq.push(['_setAccount', 'UA-261303-4']);\r\n  _gaq.push(['_trackPageview']);\r\n  _gaq.push(['_trackPageLoadTime']); \r\n  \r\n  (function() {\r\n    var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;\r\n    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';\r\n    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);\r\n  })();\r\n\r\n</script>\r\n\r\n<script src=\"//s1.adform.net/banners/scripts/adx.js\" async=\"\" defer=\"\"></script>\r\n\r\n<script async=\"\" src=\"//www.statsforads.com/tag/d5e49d0e-64d6-4751-ae6c-eb53cd6568f6.min.js\"></script>\r\n\r\n\r\n  <script type=\"text/javascript\" src=\"https://lib.ads4g.pl/publisher/maxart/91c4f3e3d35dc73f574b.js\" async=\"\"></script>\r\n\r\n  \r\n\r\n<div id=\"fb-root\" class=\" fb_reset\"><div style=\"position: absolute; top: -10000px; width: 0px; height: 0px;\"><div></div></div></div>\r\n\r\n\r\n<!-- bootstrap -->\r\n<script defer=\"\" src=\"https://code.jquery.com/jquery-3.5.1.min.js\" integrity=\"sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=\" crossorigin=\"anonymous\"></script>\r\n<script defer=\"\" src=\"https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.bundle.min.js\" integrity=\"sha384-1CmrxMRARb6aLqgBO7yyAxTOQE2AKb9GfXnEo760AUcUmFx3ibVJJAzGytlQcNXd\" crossorigin=\"anonymous\"></script>\r\n<script defer=\"\" type=\"text/javascript\" src=\"/static/v2/js/bs4/bootstrap-autocomplete.min.js\"></script>\r\n<!-- end of bootstrap -->\r\n\r\n<script defer=\"\" type=\"text/javascript\" src=\"/static/js/mootools-core-1.6.0.min.js\"></script>\r\n<script defer=\"\" type=\"text/javascript\" src=\"/static/js/mootools-more-1.6.0.min.js\"></script>\r\n<script defer=\"\" type=\"text/javascript\" src=\"/static/js/pulse.min.js\"></script>\r\n<script defer=\"\" type=\"text/javascript\" src=\"/static/v2/js/app.js?v=220105\"></script>\r\n<script defer=\"\" type=\"text/javascript\" src=\"/static/js/main-4.js?v=211027\"></script>\r\n<!--polyfills-->\r\n<script async=\"\" type=\"text/javascript\" src=\"/static/v2/js/loading-attribute-polyfill.min.js\"></script>\r\n\r\n    <script async=\"\" defer=\"\" src=\"https://connect.facebook.net/pl_PL/sdk.js\"></script>\r\n<script>\r\n    \r\n\twindow.addEventListener('DOMContentLoaded', () => {\r\n\t    if ($defined(window.asyncEventHoler))\r\n\t    {\r\n\t\tfor (var i = 0; i < window.asyncEventHoler.length; i++) {\r\n\t\t    var o = window.asyncEventHoler[i];\r\n\t\t    window.addEvent(o.event, o.fn);\r\n\t\t}\r\n\t\tdelete window.asyncEventHoler;\r\n\t    }\r\n\t});\r\n    \r\n</script>\r\n\r\n    \t        \r\n\r\n</body></html>"
  },
  {
    "path": "test/rsrc/lyrics/tekstowopl/piosenkabeethovenbeethovenpianosonata17tempestthe3rdmovement.txt",
    "content": "<!DOCTYPE html>\r\n<html lang=\"pl\" prefix=\"og: http://ogp.me/ns#\" itemscope=\"\" itemtype=\"http://schema.org/Article\" slick-uniqueid=\"3\"><head>\r\n\t<!-- Required meta tags -->\r\n\t<meta charset=\"utf-8\">\r\n\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\r\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\r\n\r\n\t                \t<title>Beethoven - Beethoven Piano Sonata 17 Tempest The 3rd Movement - na Tekstowo.pl</title>\r\n\t<meta name=\"Description\" content=\"Beethoven - Beethoven Piano Sonata 17 Tempest The 3rd Movement - zobacz cały soundtrack z filmu Pokojówka (The Housemaid) wraz z teledyskami.\">\r\n\t<meta name=\"Keywords\" content=\"Beethoven, Beethoven Piano Sonata 17 Tempest The 3rd Movement, piosenka, film, Pokojówka, The Housemaid, soundtrack, muzyka, ścieżka dźwiękowa, OST, muzyka filmowa, płyta, piosenki, utwory, teksty piosenek, tłumaczenia piosenek, teledyski\">\r\n\t<meta name=\"revisit-after\" content=\"12 hours\">\r\n\t<meta http-equiv=\"Content-language\" content=\"pl\">\r\n\t<meta name=\"robots\" content=\"INDEX, FOLLOW\">\r\n\t<link rel=\"manifest\" href=\"/manifest.webmanifest\">\r\n        <link rel=\"search\" type=\"application/opensearchdescription+xml\" title=\"Tekstowo: Po tytule piosenki\" href=\"https://www.tekstowo.pl/piosenki_osd.xml\">\r\n        <link rel=\"search\" type=\"application/opensearchdescription+xml\" title=\"Tekstowo: Po tytule soundtracka\" href=\"https://www.tekstowo.pl/soundtracki_osd.xml\">\r\n\r\n\t\t<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin=\"\">\t\r\n\t<link rel=\"preload\" as=\"style\" href=\"https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700;900&amp;display=swap\">\r\n\t<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700;900&amp;display=swap\" media=\"all\" onload=\"this.media = 'all'\">\r\n\r\n\t\t<link rel=\"preconnect\" href=\"https://cdn.jsdelivr.net\" crossorigin=\"\">\r\n\t<link rel=\"preconnect\" href=\"https://ssl.google-analytics.com\" crossorigin=\"\">\r\n\t\t    <link rel=\"preconnect\" href=\"https://ls.hit.gemius.pl\" crossorigin=\"\">\r\n\t    <link rel=\"preconnect\" href=\"https://get.optad360.io\" crossorigin=\"\">\r\n\t    <link rel=\"preconnect\" href=\"https://www.google.com\" crossorigin=\"\">\r\n\t    <link rel=\"preconnect\" href=\"https://adservice.google.com\" crossorigin=\"\">\r\n\t    <link rel=\"preconnect\" href=\"https://tpc.googlesyndication.com\" crossorigin=\"\">\r\n\t    <link rel=\"preconnect\" href=\"https://connect.facebook.net\" crossorigin=\"\">\r\n\t\r\n\r\n\t\t<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/v2/css/glyphs.css?v=201216\" media=\"all\" onload=\"this.media = 'all'\">\r\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/v2/css/main.css?v=220210\">\r\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/v2/css/media/main-print.css?v=220210\" media=\"print\">\r\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/v2/css/media/main-md.css?v=220210\" media=\"screen and (min-width: 768px)\">\r\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/v2/css/media/main-lg.css?v=220210\" media=\"screen and (min-width: 992px)\">\r\n\t<link rel=\"stylesheet\" type=\"text/css\" href=\"/static/v2/css/media/main-xl.css?v=220210\" media=\"screen and (min-width: 1200px)\">\r\n\t<!-- generics -->\r\n\t<link rel=\"icon\" href=\"https://www.tekstowo.pl/static/v2/images/favicons/icon-32.png\" sizes=\"32x32\">\r\n\t<link rel=\"icon\" href=\"https://www.tekstowo.pl/static/v2/images/favicons/icon-128.png\" sizes=\"128x128\">\r\n\t<link rel=\"icon\" href=\"https://www.tekstowo.pl/static/v2/images/favicons/icon-192.png\" sizes=\"192x192\">\r\n\r\n\t<!-- Android -->\r\n\t<link rel=\"shortcut icon\" href=\"https://www.tekstowo.pl/static/v2/images/favicons/icon-196.png\" sizes=\"196x196\">\r\n\r\n\t<!-- iOS -->\r\n\t<link rel=\"apple-touch-icon\" href=\"https://www.tekstowo.pl/static/v2/images/favicons/icon-152.png\" sizes=\"152x152\">\r\n\t<link rel=\"apple-touch-icon\" href=\"https://www.tekstowo.pl/static/v2/images/favicons/icon-167.png\" sizes=\"167x167\">\r\n\t<link rel=\"apple-touch-icon\" href=\"https://www.tekstowo.pl/static/v2/images/favicons/icon-180.png\" sizes=\"180x180\">\r\n\r\n\t<meta name=\"msapplication-config\" content=\"/browserconfig.xml\">\r\n\r\n\t\t    <link rel=\"canonical\" href=\"https://www.tekstowo.pl/piosenka,beethoven,beethoven_piano_sonata_17_tempest_the_3rd_movement.html\">\r\n\t    <meta property=\"og:title\" content=\"Beethoven - Beethoven Piano Sonata 17 Tempest The 3rd Movement - tekst piosenki na Tekstowo.pl\">\r\n\t    \t\t<meta property=\"og:image\" content=\"https://www.tekstowo.pl/miniatura_teledysku,hKkR4YFtyJk.jpg\">\r\n\t    \t    <meta property=\"og:url\" content=\"https://www.tekstowo.pl/piosenka,beethoven,beethoven_piano_sonata_17_tempest_the_3rd_movement.html\">\r\n\t    <meta property=\"og:type\" content=\"website\">\r\n\t    <meta name=\"twitter:card\" content=\"summary_large_image\">\r\n\t    <meta name=\"twitter:title\" content=\"Beethoven - Beethoven Piano Sonata 17 Tempest The 3rd Movement\">\r\n\t    \t\t<meta name=\"twitter:image\" content=\"https://www.tekstowo.pl/miniatura_teledysku,hKkR4YFtyJk.jpg\">\r\n\t    \t    \r\n\t    \t\t\t\t    \t\t\t    \r\n\t    <meta name=\"twitter:description\" content=\"Beethoven - Beethoven Piano Sonata 17 Tempest The 3rd Movement - zobacz cały soundtrack z filmu Pokojówka (The Housemaid) wraz z teledyskami.\">\r\n\t    <meta property=\"og:description\" content=\"Beethoven - Beethoven Piano Sonata 17 Tempest The 3rd Movement - zobacz cały soundtrack z filmu Pokojówka (The Housemaid) wraz z teledyskami.\">\r\n\t    \t\t<meta itemprop=\"name\" content=\"Beethoven - Beethoven Piano Sonata 17 Tempest The 3rd Movement - tekst piosenki na Tekstowo.pl\">\r\n\t\t<meta itemprop=\"description\" content=\"Beethoven - Beethoven Piano Sonata 17 Tempest The 3rd Movement - zobacz cały soundtrack z filmu Pokojówka (The Housemaid) wraz z teledyskami.\">\r\n\t\t\t\t    <meta itemprop=\"image\" content=\"https://www.tekstowo.pl/miniatura_teledysku,hKkR4YFtyJk.jpg\">\r\n\t\t\t    \t\r\n    <meta property=\"og:site_name\" content=\"Tekstowo.pl\">\r\n    <meta property=\"fb:app_id\" content=\"131858753537922\">\r\n\r\n\r\n    <script src=\"https://connect.facebook.net/pl_PL/sdk.js?hash=85583b32560f14af5837c0fa700e803e\" async=\"\" crossorigin=\"anonymous\"></script><script type=\"text/javascript\" async=\"\" src=\"https://ssl.google-analytics.com/ga.js\"></script><script type=\"text/javascript\">\r\n\t\t\t    var ytProxy = '//filmiki4.maxart.pl/ytp.php';\r\n\t    </script>\r\n    <script>\r\n\t\t\r\n\t    window.addEvent = function (event, fn) {\r\n\t\tif (window.asyncEventHoler == undefined)\r\n\t\t{\r\n\t\t    window.asyncEventHoler = [];\r\n\t\t}\r\n\r\n\t\twindow.asyncEventHoler.push({\r\n\t\t    'event': event,\r\n\t\t    'fn': fn\r\n\t\t});\r\n\t    }\r\n\t\r\n    </script>\r\n\t<script async=\"\" src=\"//cmp.optad360.io/items/6a750fad-191a-4309-bcd6-81e330cb392d.min.js\"></script>  \r\n\t\r\n<style type=\"text/css\" data-fbcssmodules=\"css:fb.css.base css:fb.css.dialog css:fb.css.iframewidget css:fb.css.customer_chat_plugin_iframe\">.fb_hidden{position:absolute;top:-10000px;z-index:10001}.fb_reposition{overflow:hidden;position:relative}.fb_invisible{display:none}.fb_reset{background:none;border:0;border-spacing:0;color:#000;cursor:auto;direction:ltr;font-family:'lucida grande', tahoma, verdana, arial, sans-serif;font-size:11px;font-style:normal;font-variant:normal;font-weight:normal;letter-spacing:normal;line-height:1;margin:0;overflow:visible;padding:0;text-align:left;text-decoration:none;text-indent:0;text-shadow:none;text-transform:none;visibility:visible;white-space:normal;word-spacing:normal}.fb_reset>div{overflow:hidden}@keyframes fb_transform{from{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.fb_animate{animation:fb_transform .3s forwards}\r\n.fb_hidden{position:absolute;top:-10000px;z-index:10001}.fb_reposition{overflow:hidden;position:relative}.fb_invisible{display:none}.fb_reset{background:none;border:0;border-spacing:0;color:#000;cursor:auto;direction:ltr;font-family:'lucida grande', tahoma, verdana, arial, sans-serif;font-size:11px;font-style:normal;font-variant:normal;font-weight:normal;letter-spacing:normal;line-height:1;margin:0;overflow:visible;padding:0;text-align:left;text-decoration:none;text-indent:0;text-shadow:none;text-transform:none;visibility:visible;white-space:normal;word-spacing:normal}.fb_reset>div{overflow:hidden}@keyframes fb_transform{from{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}.fb_animate{animation:fb_transform .3s forwards}\r\n.fb_dialog{background:rgba(82, 82, 82, .7);position:absolute;top:-10000px;z-index:10001}.fb_dialog_advanced{border-radius:8px;padding:10px}.fb_dialog_content{background:#fff;color:#373737}.fb_dialog_close_icon{background:url(https://connect.facebook.net/rsrc.php/v3/yq/r/IE9JII6Z1Ys.png) no-repeat scroll 0 0 transparent;cursor:pointer;display:block;height:15px;position:absolute;right:18px;top:17px;width:15px}.fb_dialog_mobile .fb_dialog_close_icon{left:5px;right:auto;top:5px}.fb_dialog_padding{background-color:transparent;position:absolute;width:1px;z-index:-1}.fb_dialog_close_icon:hover{background:url(https://connect.facebook.net/rsrc.php/v3/yq/r/IE9JII6Z1Ys.png) no-repeat scroll 0 -15px transparent}.fb_dialog_close_icon:active{background:url(https://connect.facebook.net/rsrc.php/v3/yq/r/IE9JII6Z1Ys.png) no-repeat scroll 0 -30px transparent}.fb_dialog_iframe{line-height:0}.fb_dialog_content .dialog_title{background:#6d84b4;border:1px solid #365899;color:#fff;font-size:14px;font-weight:bold;margin:0}.fb_dialog_content .dialog_title>span{background:url(https://connect.facebook.net/rsrc.php/v3/yd/r/Cou7n-nqK52.gif) no-repeat 5px 50%;float:left;padding:5px 0 7px 26px}body.fb_hidden{height:100%;left:0;margin:0;overflow:visible;position:absolute;top:-10000px;transform:none;width:100%}.fb_dialog.fb_dialog_mobile.loading{background:url(https://connect.facebook.net/rsrc.php/v3/ya/r/3rhSv5V8j3o.gif) white no-repeat 50% 50%;min-height:100%;min-width:100%;overflow:hidden;position:absolute;top:0;z-index:10001}.fb_dialog.fb_dialog_mobile.loading.centered{background:none;height:auto;min-height:initial;min-width:initial;width:auto}.fb_dialog.fb_dialog_mobile.loading.centered #fb_dialog_loader_spinner{width:100%}.fb_dialog.fb_dialog_mobile.loading.centered .fb_dialog_content{background:none}.loading.centered #fb_dialog_loader_close{clear:both;color:#fff;display:block;font-size:18px;padding-top:20px}#fb-root #fb_dialog_ipad_overlay{background:rgba(0, 0, 0, .4);bottom:0;left:0;min-height:100%;position:absolute;right:0;top:0;width:100%;z-index:10000}#fb-root #fb_dialog_ipad_overlay.hidden{display:none}.fb_dialog.fb_dialog_mobile.loading iframe{visibility:hidden}.fb_dialog_mobile .fb_dialog_iframe{position:sticky;top:0}.fb_dialog_content .dialog_header{background:linear-gradient(from(#738aba), to(#2c4987));border-bottom:1px solid;border-color:#043b87;box-shadow:white 0 1px 1px -1px inset;color:#fff;font:bold 14px Helvetica, sans-serif;text-overflow:ellipsis;text-shadow:rgba(0, 30, 84, .296875) 0 -1px 0;vertical-align:middle;white-space:nowrap}.fb_dialog_content .dialog_header table{height:43px;width:100%}.fb_dialog_content .dialog_header td.header_left{font-size:12px;padding-left:5px;vertical-align:middle;width:60px}.fb_dialog_content .dialog_header td.header_right{font-size:12px;padding-right:5px;vertical-align:middle;width:60px}.fb_dialog_content .touchable_button{background:linear-gradient(from(#4267B2), to(#2a4887));background-clip:padding-box;border:1px solid #29487d;border-radius:3px;display:inline-block;line-height:18px;margin-top:3px;max-width:85px;padding:4px 12px;position:relative}.fb_dialog_content .dialog_header .touchable_button input{background:none;border:none;color:#fff;font:bold 12px Helvetica, sans-serif;margin:2px -12px;padding:2px 6px 3px 6px;text-shadow:rgba(0, 30, 84, .296875) 0 -1px 0}.fb_dialog_content .dialog_header .header_center{color:#fff;font-size:16px;font-weight:bold;line-height:18px;text-align:center;vertical-align:middle}.fb_dialog_content .dialog_content{background:url(https://connect.facebook.net/rsrc.php/v3/y9/r/jKEcVPZFk-2.gif) no-repeat 50% 50%;border:1px solid #4a4a4a;border-bottom:0;border-top:0;height:150px}.fb_dialog_content .dialog_footer{background:#f5f6f7;border:1px solid #4a4a4a;border-top-color:#ccc;height:40px}#fb_dialog_loader_close{float:left}.fb_dialog.fb_dialog_mobile .fb_dialog_close_icon{visibility:hidden}#fb_dialog_loader_spinner{animation:rotateSpinner 1.2s linear infinite;background-color:transparent;background-image:url(https://connect.facebook.net/rsrc.php/v3/yD/r/t-wz8gw1xG1.png);background-position:50% 50%;background-repeat:no-repeat;height:24px;width:24px}@keyframes rotateSpinner{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}\r\n.fb_iframe_widget{display:inline-block;position:relative}.fb_iframe_widget span{display:inline-block;position:relative;text-align:justify}.fb_iframe_widget iframe{position:absolute}.fb_iframe_widget_fluid_desktop,.fb_iframe_widget_fluid_desktop span,.fb_iframe_widget_fluid_desktop iframe{max-width:100%}.fb_iframe_widget_fluid_desktop iframe{min-width:220px;position:relative}.fb_iframe_widget_lift{z-index:1}.fb_iframe_widget_fluid{display:inline}.fb_iframe_widget_fluid span{width:100%}\r\n.fb_mpn_mobile_landing_page_slide_out{animation-duration:200ms;animation-name:fb_mpn_landing_page_slide_out;transition-timing-function:ease-in}.fb_mpn_mobile_landing_page_slide_out_from_left{animation-duration:200ms;animation-name:fb_mpn_landing_page_slide_out_from_left;transition-timing-function:ease-in}.fb_mpn_mobile_landing_page_slide_up{animation-duration:500ms;animation-name:fb_mpn_landing_page_slide_up;transition-timing-function:ease-in}.fb_mpn_mobile_bounce_in{animation-duration:300ms;animation-name:fb_mpn_bounce_in;transition-timing-function:ease-in}.fb_mpn_mobile_bounce_out{animation-duration:300ms;animation-name:fb_mpn_bounce_out;transition-timing-function:ease-in}.fb_mpn_mobile_bounce_out_v2{animation-duration:300ms;animation-name:fb_mpn_fade_out;transition-timing-function:ease-in}.fb_customer_chat_bounce_in_v2{animation-duration:300ms;animation-name:fb_bounce_in_v2;transition-timing-function:ease-in}.fb_customer_chat_bounce_in_from_left{animation-duration:300ms;animation-name:fb_bounce_in_from_left;transition-timing-function:ease-in}.fb_customer_chat_bounce_out_v2{animation-duration:300ms;animation-name:fb_bounce_out_v2;transition-timing-function:ease-in}.fb_customer_chat_bounce_out_from_left{animation-duration:300ms;animation-name:fb_bounce_out_from_left;transition-timing-function:ease-in}.fb_invisible_flow{display:inherit;height:0;overflow-x:hidden;width:0}@keyframes fb_mpn_landing_page_slide_out{0%{margin:0 12px;width:100% - 24px}60%{border-radius:18px}100%{border-radius:50%;margin:0 24px;width:60px}}@keyframes fb_mpn_landing_page_slide_out_from_left{0%{left:12px;width:100% - 24px}60%{border-radius:18px}100%{border-radius:50%;left:12px;width:60px}}@keyframes fb_mpn_landing_page_slide_up{0%{bottom:0;opacity:0}100%{bottom:24px;opacity:1}}@keyframes fb_mpn_bounce_in{0%{opacity:.5;top:100%}100%{opacity:1;top:0}}@keyframes fb_mpn_fade_out{0%{bottom:30px;opacity:1}100%{bottom:0;opacity:0}}@keyframes fb_mpn_bounce_out{0%{opacity:1;top:0}100%{opacity:.5;top:100%}}@keyframes fb_bounce_in_v2{0%{opacity:0;transform:scale(0, 0);transform-origin:bottom right}50%{transform:scale(1.03, 1.03);transform-origin:bottom right}100%{opacity:1;transform:scale(1, 1);transform-origin:bottom right}}@keyframes fb_bounce_in_from_left{0%{opacity:0;transform:scale(0, 0);transform-origin:bottom left}50%{transform:scale(1.03, 1.03);transform-origin:bottom left}100%{opacity:1;transform:scale(1, 1);transform-origin:bottom left}}@keyframes fb_bounce_out_v2{0%{opacity:1;transform:scale(1, 1);transform-origin:bottom right}100%{opacity:0;transform:scale(0, 0);transform-origin:bottom right}}@keyframes fb_bounce_out_from_left{0%{opacity:1;transform:scale(1, 1);transform-origin:bottom left}100%{opacity:0;transform:scale(0, 0);transform-origin:bottom left}}@keyframes slideInFromBottom{0%{opacity:.1;transform:translateY(100%)}100%{opacity:1;transform:translateY(0)}}@keyframes slideInFromBottomDelay{0%{opacity:0;transform:translateY(100%)}97%{opacity:0;transform:translateY(100%)}100%{opacity:1;transform:translateY(0)}}</style></head>\r\n<body class=\"\">\r\n    \r\n\r\n    <nav class=\"navbar navbar-expand-lg navbar-dark sticky-top d-md-none\" id=\"navbar\">\r\n\t<div class=\"container\" id=\"navbarMobile\">\r\n\t    <a href=\"/\" class=\"navbar-brand logo\">\r\n\t\t\t\t<img src=\"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20441.8%20136%22%3E%3Cpath%20fill%3D%22%23FFF%22%20d%3D%22M130.7%2046v9h6.5v6.2h-6.5v20.6c0%201.4.3%202.4.8%203.1.6.6%201.6.9%203%20.9%201%200%201.9-.1%202.9-.3v6.4c-1.9.5-3.7.8-5.5.8-6.4%200-9.6-3.5-9.6-10.6v-21h-6.1V55h6.1v-9h8.4zM156.3%2092.7c-5.3%200-9.5-1.7-12.8-5s-4.9-7.7-4.9-13.2v-1c0-3.7.7-7%202.1-9.9%201.4-2.9%203.4-5.2%206-6.8s5.4-2.4%208.6-2.4c5%200%208.9%201.6%2011.7%204.8%202.7%203.2%204.1%207.8%204.1%2013.6v3.3H147c.3%203.1%201.3%205.5%203.1%207.2%201.8%201.8%204%202.7%206.8%202.7%203.8%200%206.9-1.5%209.3-4.6l4.5%204.3c-1.5%202.2-3.5%203.9-5.9%205.1-2.6%201.3-5.4%201.9-8.5%201.9zm-1-31.7c-2.3%200-4.1.8-5.5%202.4-1.4%201.6-2.3%203.8-2.7%206.7h15.8v-.6c-.2-2.8-.9-4.9-2.2-6.3-1.3-1.5-3.1-2.2-5.4-2.2zM186.1%2076.1l-3.7%203.8V92h-8.3V39.5h8.3v30.3l2.6-3.2L195.2%2055h10l-13.7%2015.4L206.7%2092h-9.6l-11-15.9z%22%2F%3E%3Cpath%20fill%3D%22%23FFF%22%20d%3D%22M227.9%2082c0-1.5-.6-2.6-1.8-3.4s-3.2-1.5-6.1-2.1c-2.8-.6-5.2-1.3-7.1-2.3-4.1-2-6.2-4.9-6.2-8.7%200-3.2%201.3-5.9%204-8s6.1-3.2%2010.3-3.2c4.4%200%208%201.1%2010.7%203.3s4.1%205%204.1%208.5h-8.3c0-1.6-.6-2.9-1.8-4s-2.8-1.6-4.7-1.6c-1.8%200-3.3.4-4.5%201.3-1.2.8-1.7%202-1.7%203.4%200%201.3.5%202.3%201.6%203s3.2%201.4%206.5%202.1%205.8%201.6%207.7%202.6%203.2%202.2%204.1%203.6c.9%201.4%201.3%203.1%201.3%205.1%200%203.3-1.4%206-4.1%208.1-2.8%202.1-6.4%203.1-10.8%203.1-3%200-5.7-.5-8.1-1.6-2.4-1.1-4.2-2.6-5.5-4.5s-2-4-2-6.2h8.1c.1%202%20.9%203.5%202.2%204.5%201.4%201.1%203.2%201.6%205.4%201.6s3.9-.4%205-1.2c1.1-1%201.7-2.1%201.7-3.4zM250.2%2046v9h6.5v6.2h-6.5v20.6c0%201.4.3%202.4.8%203.1.6.6%201.6.9%203%20.9%201%200%201.9-.1%202.9-.3v6.4c-1.9.5-3.7.8-5.5.8-6.4%200-9.6-3.5-9.6-10.6v-21h-6.1V55h6.1v-9h8.4zM257%2073.2c0-3.6.7-6.9%202.2-9.8s3.5-5.1%206-6.7c2.6-1.6%205.6-2.3%209-2.3%205%200%209%201.6%2012.2%204.8%203.1%203.2%204.8%207.5%205%2012.8v1.9c0%203.6-.7%206.9-2.1%209.8s-3.4%205.1-6%206.7-5.6%202.4-9%202.4c-5.2%200-9.4-1.7-12.5-5.2s-4.7-8.1-4.7-13.9v-.5h-.1zm8.3.7c0%203.8.8%206.8%202.4%208.9%201.6%202.2%203.8%203.2%206.6%203.2s5-1.1%206.5-3.3c1.6-2.2%202.3-5.4%202.3-9.6%200-3.7-.8-6.7-2.4-8.9-1.6-2.2-3.8-3.3-6.5-3.3s-4.9%201.1-6.5%203.2c-1.6%202.3-2.4%205.5-2.4%209.8zM327.4%2080.5l5.9-25.5h8.1l-10.1%2037h-6.8l-7.9-25.4-7.9%2025.4h-6.8l-10.1-37h8.1l6%2025.3%207.6-25.3h6.3l7.6%2025.5zM341.8%2073.2c0-3.6.7-6.9%202.2-9.8s3.5-5.1%206-6.7c2.6-1.6%205.6-2.3%209-2.3%205%200%209%201.6%2012.2%204.8%203.1%203.2%204.8%207.5%205%2012.8v1.9c0%203.6-.7%206.9-2.1%209.8s-3.4%205.1-6%206.7-5.6%202.4-9%202.4c-5.2%200-9.4-1.7-12.5-5.2s-4.7-8.1-4.7-13.9v-.5h-.1zm8.3.7c0%203.8.8%206.8%202.4%208.9%201.6%202.2%203.8%203.2%206.6%203.2s5-1.1%206.5-3.3c1.6-2.2%202.3-5.4%202.3-9.6%200-3.7-.8-6.7-2.4-8.9-1.6-2.2-3.8-3.3-6.5-3.3s-4.9%201.1-6.5%203.2c-1.6%202.3-2.4%205.5-2.4%209.8zM384.9%2083.3c1.5%200%202.7.4%203.6%201.3.8.9%201.3%202%201.3%203.3s-.4%202.4-1.3%203.2c-.8.9-2%201.3-3.6%201.3-1.5%200-2.7-.4-3.5-1.3-.9-.8-1.3-1.9-1.3-3.2%200-1.3.4-2.4%201.3-3.3.8-.9%202-1.3%203.5-1.3zM428.2%2073.9c0%205.7-1.3%2010.3-3.9%2013.7s-6.1%205.1-10.5%205.1c-4.1%200-7.3-1.3-9.7-4v17.5h-8.3V55h7.7l.3%203.8c2.4-3%205.8-4.4%209.9-4.4%204.5%200%208%201.7%2010.6%205%202.6%203.4%203.8%208%203.8%2014v.5h.1zm-8.3-.7c0-3.7-.7-6.6-2.2-8.8-1.5-2.2-3.6-3.2-6.3-3.2-3.4%200-5.8%201.4-7.3%204.2v16.4c1.5%202.9%204%204.3%207.4%204.3%202.6%200%204.7-1.1%206.2-3.2%201.5-2.2%202.2-5.4%202.2-9.7zM440.5%2092h-8.3V39.5h8.3V92zM123.5%20107.6l-.5%202.8h2.1l-.3%201.5h-2.1l-1.2%207.2v.5c0%20.6.4.8.9.8.2%200%20.6%200%20.9-.1l-.2%201.6c-.5.1-.9.2-1.4.2-.8%200-1.4-.3-1.7-.9-.4-.6-.5-1.3-.5-2.2l1.2-7.2h-2.1l.3-1.5h2.1l.5-2.8%202%20.1zM129.3%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2.7.8%201.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.6.8-2.6.8zm1.3-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1.1-.8-1.8-.8zM141.8%20112.1l-.9-.1c-1.2%200-2.2.6-2.9%201.7l-1.4%208.3h-1.9l2-11.6h1.9l-.3%201.4c.8-1.1%201.8-1.6%202.9-1.6.2%200%20.5.1.9.2l-.3%201.7zM147.5%20122c-.1-.2-.1-.4-.1-.6v-.6c-1.1%201-2.2%201.5-3.4%201.4-1%200-1.9-.3-2.5-1-.6-.6-.9-1.4-.9-2.4.1-1.2.6-2.2%201.6-2.9%201-.7%202.3-1%204-1h2l.1-.9c.1-.7-.1-1.2-.4-1.6-.3-.4-.9-.6-1.5-.6-.7%200-1.3.1-1.8.5-.5.3-.8.8-.9%201.3h-2c.1-.7.3-1.3.8-1.8s1.1-.9%201.8-1.2c.7-.3%201.5-.4%202.3-.4%201.2%200%202.2.4%202.8%201.1.7.7%201%201.6.8%202.8l-.9%205.8-.1.8c0%20.4%200%20.8.1%201.2v.2h-1.8v-.1zm-3-1.5c.6%200%201.2-.1%201.8-.4s1-.7%201.4-1.3l.4-2.4h-1.5c-1.2%200-2.2.2-2.8.6-.7.4-1%20.9-1.1%201.6-.1.5.1%201%20.4%201.4.3.3.8.5%201.4.5zM153.2%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM168.8%20110.4l-.2%201.3c1-1%202.2-1.5%203.5-1.5.7%200%201.4.2%201.9.5s.9.8%201.1%201.4c1.1-1.3%202.4-1.9%203.9-1.9%201.2%200%202%20.4%202.6%201.2.6.8.8%201.8.6%203l-1.3%207.6H179l1.3-7.7v-1c-.1-1-.8-1.5-1.9-1.5-.7%200-1.4.2-1.9.7-.6.5-.9%201.1-1.1%201.8l-1.3%207.7h-2l1.3-7.6c.1-.8%200-1.4-.4-1.8s-.9-.7-1.6-.7c-1.2%200-2.2.5-2.9%201.7l-1.5%208.5h-1.9l2-11.6%201.7-.1zM189.1%20110.2c1%200%201.8.3%202.5.8s1.2%201.2%201.5%202.1.4%201.9.3%203v.2c-.1%201.1-.5%202.2-1%203.1s-1.2%201.6-2.1%202.1c-.9.5-1.8.7-2.8.7s-1.8-.3-2.5-.8-1.2-1.2-1.5-2.1-.4-1.9-.3-2.9c.1-1.2.4-2.3%201-3.2.5-1%201.2-1.7%202.1-2.2.8-.6%201.8-.9%202.8-.8zm-4%206.2c-.1.5-.1.9%200%201.4.1.8.3%201.5.8%202%20.4.5%201%20.8%201.7.8.6%200%201.2-.1%201.8-.5.5-.3%201-.9%201.4-1.5.4-.7.6-1.5.7-2.3.1-.7.1-1.2%200-1.7-.1-.9-.3-1.6-.8-2.1-.4-.5-1-.8-1.7-.8-1%200-1.9.4-2.6%201.2-.7.8-1.1%201.9-1.3%203.2v.3zM195.8%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zm3.2-13c0-.3.1-.6.3-.8.2-.2.5-.3.8-.4.3%200%20.6.1.8.3.2.2.3.5.3.8s-.1.6-.3.8c-.2.2-.5.3-.8.3s-.6-.1-.8-.3c-.2-.1-.3-.4-.3-.7zM208.1%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2.7.8%201.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1-.8-1.8-.8zM220.6%20118.9c.1-.8-.3-1.4-1.3-1.7l-2-.6c-1.7-.6-2.5-1.6-2.5-2.9%200-1%20.5-1.9%201.4-2.5.9-.7%201.9-1%203.1-1%201.2%200%202.1.4%202.9%201%20.7.7%201.1%201.5%201.1%202.6h-1.9c0-.6-.2-1.1-.5-1.4s-.9-.6-1.5-.6c-.7%200-1.3.2-1.7.5-.5.3-.7.7-.8%201.3-.1.7.3%201.2%201.2%201.5l1%20.3c1.3.3%202.3.8%202.8%201.3s.8%201.2.8%202.1c0%20.7-.3%201.4-.7%201.9s-1%20.9-1.7%201.2c-.7.3-1.5.4-2.3.4-1.2%200-2.2-.4-3.1-1.1-.8-.7-1.2-1.6-1.2-2.7h1.9c0%20.7.2%201.2.6%201.6.4.4%201%20.6%201.7.6s1.3-.1%201.8-.4c.5-.5.8-.9.9-1.4zM225.5%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM241.3%20110.4l-2.2%2013c-.1%201.1-.5%201.9-1.1%202.5s-1.4.9-2.3.8c-.4%200-.8-.1-1.3-.2l.2-1.6c.3.1.6.1.9.1.9%200%201.5-.6%201.7-1.7l2.2-13%201.9.1zm-1.6-3.1c0-.3.1-.6.3-.8.2-.2.5-.3.8-.4.3%200%20.6.1.8.3.2.2.3.5.3.8s-.1.6-.3.8-.5.3-.8.3-.6-.1-.8-.3-.3-.4-.3-.7zM246.4%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2s1.1%201.8%201.1%203.1v1.4l-.1.9H244c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1-.8-1.8-.8zM266.5%20116.2c-.1%201.2-.5%202.2-1%203.2s-1.1%201.7-1.8%202.1c-.7.5-1.5.7-2.4.7-1.3%200-2.4-.5-3.1-1.4l-1%205.6h-1.9l2.8-16.1h1.8l-.2%201.3c1-1%202.1-1.5%203.4-1.5%201.1%200%202%20.4%202.6%201.2s1%201.9%201%203.3c0%20.5%200%20.9-.1%201.3l-.1.3zm-1.9-.2l.1-.9c0-1-.2-1.8-.6-2.4s-1-.8-1.7-.9c-1.1%200-2.1.5-2.9%201.6l-1%205.6c.4%201%201.2%201.6%202.4%201.6%201%200%201.8-.4%202.5-1.1.5-.8.9-2%201.2-3.5zM274.2%20112.1l-.9-.1c-1.2%200-2.2.6-2.9%201.7L269%20122h-1.9l2-11.6h1.9l-.3%201.4c.8-1.1%201.8-1.6%202.9-1.6.2%200%20.5.1.9.2l-.3%201.7zM275.3%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM287.6%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2s1.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.4-.5-1-.8-1.8-.8zM297.8%20120.6c.7%200%201.3-.2%201.9-.6s.9-1%201.1-1.6h1.8c-.1.7-.4%201.4-.9%202s-1.1%201.1-1.8%201.4-1.5.5-2.2.5c-1%200-1.8-.3-2.5-.8s-1.2-1.2-1.5-2.1-.4-1.9-.3-3l.1-.5c.1-1.1.5-2.1%201-3s1.2-1.6%202-2.1%201.7-.7%202.8-.7c1.2%200%202.2.4%202.9%201.2s1.1%201.8%201.1%203h-1.8c0-.8-.2-1.4-.6-1.8-.4-.5-1-.7-1.7-.7-1%200-1.8.4-2.5%201.1-.7.8-1.1%201.9-1.3%203.2v1.7c0%20.8.3%201.5.7%202s1%20.8%201.7.8zM305.2%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM316.8%20119.1l4.1-8.7h2.1l-6.9%2013.6c-1%201.9-2.2%202.8-3.7%202.8-.3%200-.7-.1-1.2-.2l.2-1.6.5.1c.6%200%201.1-.1%201.6-.4.4-.3.8-.8%201.2-1.5l.7-1.3-2-11.4h2l1.4%208.6z%22%2F%3E%3Cpath%20fill%3D%22%23FFF%22%20d%3D%22M326.9%20107.6l-.5%202.8h2.1l-.3%201.5h-2.1l-1.2%207.2v.5c0%20.6.4.8.9.8.2%200%20.6%200%20.9-.1l-.2%201.6c-.5.1-.9.2-1.4.2-.8%200-1.4-.3-1.7-.9-.4-.6-.5-1.3-.5-2.2l1.2-7.2H322l.3-1.5h2.1l.5-2.8%202%20.1zM334.8%20122c0-.2-.1-.4-.1-.6v-.6c-1.1%201-2.2%201.5-3.4%201.4-1%200-1.9-.3-2.5-1-.6-.6-.9-1.4-.9-2.4.1-1.2.6-2.2%201.6-2.9%201-.7%202.3-1%204-1h2l.1-.9c.1-.7-.1-1.2-.4-1.6s-.9-.6-1.5-.6c-.7%200-1.3.1-1.8.5-.5.3-.8.8-.9%201.3h-2c.1-.7.3-1.3.8-1.8s1.1-.9%201.8-1.2c.7-.3%201.5-.4%202.3-.4%201.2%200%202.2.4%202.8%201.1.7.7%201%201.6.8%202.8l-.9%205.8-.1.8c0%20.4%200%20.8.1%201.2v.2h-1.8v-.1zm-3-1.5c.6%200%201.2-.1%201.8-.4.6-.3%201-.7%201.4-1.3l.4-2.4h-1.5c-1.2%200-2.2.2-2.8.6s-1%20.9-1.1%201.6c-.1.5.1%201%20.4%201.4.3.3.8.5%201.4.5zM343.3%20120.6c.7%200%201.3-.2%201.9-.6s.9-1%201.1-1.6h1.8c-.1.7-.4%201.4-.9%202s-1.1%201.1-1.8%201.4-1.5.5-2.2.5c-1%200-1.8-.3-2.5-.8s-1.2-1.2-1.5-2.1-.4-1.9-.3-3l.1-.5c.1-1.1.5-2.1%201-3s1.2-1.6%202-2.1%201.7-.7%202.8-.7c1.2%200%202.2.4%202.9%201.2s1.1%201.8%201.1%203H347c0-.8-.2-1.4-.6-1.8-.4-.5-1-.7-1.7-.7-1%200-1.8.4-2.5%201.1-.7.8-1.1%201.9-1.3%203.2v1.7c0%20.8.3%201.5.7%202s.9.8%201.7.8zm2.7-15.1h2.5l-3.3%203.2h-1.7l2.5-3.2z%22%2F%3E%3Cpath%20fill%3D%22%23F60%22%20d%3D%22M92.9%20124H7.1c-3.9%200-7.1-3.2-7.1-7.1V31.1C0%2027.2%203.2%2024%207.1%2024h85.8c3.9%200%207.1%203.2%207.1%207.1v85.8c0%203.9-3.2%207.1-7.1%207.1z%22%2F%3E%3Cpath%20opacity%3D%22.2%22%20d%3D%22M68%2044.1c-6.8-7-15.9-12.2-24.6-14.4C28.8%2026%2025%2023.9%2017.8%2019c-4.1-2.8-7.2-8.4-10.6-9.8-1.6-.6-2.6.1-3.1.7-.9%201-1.6%202.9-.4%205.2%2010.1%2019.8%2048.4%2076.8%2048.4%2076.8-5.9%201.1-12.4%205.7-17%2012.9-6.9%2010.7-7%2022.9-.1%2027.2%206.9%204.3%2018.1-.9%2025-11.6%205.9-9%206.8-19.1%202.8-24.6L29.2%2042.7s-3.9-4.7%202.3-6.1c4.5-1%2013.9.1%2019%202.8C56%2042.3%2069%2051.7%2072.3%2065.1c1.5%206.2.6%209.6%201.4%2015.1.4%202.6%203.2%204.8%205.5%201.2%201.2-1.9%201.4-8.4%201.1-13.5-.5-6.7-6-17.3-12.3-23.8zm-10.4%2048c.3.1.7.2%201%20.3-.3-.1-.6-.2-1-.3zm-2.5-.4h.4-.4zm1.1.1c.3%200%20.6.1.8.1-.2%200-.5-.1-.8-.1zm-3%200c.1%200%20.1%200%200%200%20.1%200%20.1%200%200%200zm5.8.8l1.2.6c-.4-.3-.8-.5-1.2-.6zm2.9%202zm-.7-.7c-.3-.3-.6-.5-1-.7.3.2.6.5%201%20.7z%22%2F%3E%3ClinearGradient%20id%3D%22a%22%20gradientUnits%3D%22userSpaceOnUse%22%20x1%3D%2239.902%22%20y1%3D%2245.741%22%20x2%3D%2252.109%22%20y2%3D%2212.201%22%20gradientTransform%3D%22matrix%281%200%200%20-1%200%20138%29%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%238fe132%22%2F%3E%3Cstop%20offset%3D%22.362%22%20stop-color%3D%22%2365bd23%22%2F%3E%3Cstop%20offset%3D%22.668%22%20stop-color%3D%22%2349a519%22%2F%3E%3Cstop%20offset%3D%22.844%22%20stop-color%3D%22%233e9c15%22%2F%3E%3C%2FlinearGradient%3E%3Cpath%20fill%3D%22url%28%23a%29%22%20d%3D%22M57.9%20117.8c-7.6%2010.4-19.2%2014.9-25.7%2010.1s-5.7-17.2%201.9-27.6%2019.2-14.9%2025.7-10.1c6.6%204.9%205.7%2017.2-1.9%2027.6z%22%2F%3E%3ClinearGradient%20id%3D%22b%22%20gradientUnits%3D%22userSpaceOnUse%22%20x1%3D%22-3.575%22%20y1%3D%22113.687%22%20x2%3D%2282.758%22%20y2%3D%2263.843%22%20gradientTransform%3D%22matrix%281%200%200%20-1%200%20138%29%22%3E%3Cstop%20offset%3D%22.184%22%20stop-color%3D%22%238fe132%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%233e9c15%22%2F%3E%3C%2FlinearGradient%3E%3Cpath%20fill%3D%22url%28%23b%29%22%20d%3D%22M70.9%2041c-6.3-7.6-15-13.4-23.6-16.2-14.4-4.7-18-7.1-24.8-12.5-3.9-3.1-6.6-9-9.9-10.7-1.6-.8-2.6-.1-3.2.5-.9.9-1.8%202.8-.7%205.2C17.4%2028%2051.9%2088.4%2051.9%2088.4c3-.3%205.7.2%207.9%201.8%201%20.8%201.8%201.7%202.5%202.8l-30-56s-3.6-5%202.7-6c4.5-.7%2013.9%201.1%2018.8%204.1%205.3%203.3%2017.6%2013.7%2020%2027.5%201.1%206.4%200%209.7.4%2015.4.2%202.6%202.9%205.1%205.4%201.6%201.3-1.8%202-8.4%202-13.6%200-6.8-4.9-17.9-10.7-25z%22%2F%3E%3CradialGradient%20id%3D%22c%22%20cx%3D%22426.647%22%20cy%3D%22271.379%22%20r%3D%2214.948%22%20gradientTransform%3D%22matrix%28.2966%20.4025%20.805%20-.5933%20-299.03%2088.664%29%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%23f4ff72%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%2373c928%22%20stop-opacity%3D%220%22%2F%3E%3C%2FradialGradient%3E%3Cpath%20fill%3D%22url%28%23c%29%22%20d%3D%22M50.4%20105.4c-6.6%204.9-14%206.2-16.5%202.8-2.4-3.3%201-10%207.6-14.9s14-6.2%2016.5-2.8c2.5%203.3-.9%2010-7.6%2014.9z%22%2F%3E%3C%2Fsvg%3E\" class=\"img-fluid\" alt=\"tekstowo.pl\" title=\"Teksty piosenek, tłumaczenia, teledyski\" style=\"width: 140px\">\t    </a>\r\n\r\n\t    <div class=\"btn-group\" role=\"group\">\r\n\t\t<button class=\"navbar-toggler pr-2 pr-lg-0\" type=\"button\" data-toggle=\"collapse\" data-target=\"#navbarUser\" aria-controls=\"navbarUser\" aria-expanded=\"false\" aria-label=\"Konto\">\r\n\t\t    <span class=\"navbar-toggler-icon user \"></span>\r\n\t\t</button>\r\n\r\n\t\t<button class=\"navbar-toggler pr-2 pr-lg-0\" type=\"button\" data-toggle=\"collapse\" data-target=\"#navbarSearch\" aria-controls=\"navbarSearch\" aria-expanded=\"false\" aria-label=\"Wyszukiwarka\">\r\n\t\t    <span class=\"navbar-toggler-icon search\"></span>\r\n\t\t</button>\r\n\r\n\t\t<button class=\"navbar-toggler pr-2 pr-lg-0\" type=\"button\" data-toggle=\"collapse\" data-target=\"#navbarSupportedContent\" aria-controls=\"navbarSupportedContent\" aria-expanded=\"false\" aria-label=\"Menu główne\">\r\n\t\t    <span class=\"navbar-toggler-icon\"></span>\r\n\t\t</button>\r\n\t    </div>\r\n\r\n\t    \t</div>\r\n    </nav>\r\n\r\n    <div class=\"container\">\r\n\t<div class=\"row top-row d-none d-md-flex\">\r\n\t    <div class=\"col-md-4 col-lg-3 align-self-center\">\r\n\t\t<a href=\"/\" class=\"logo\">\r\n\t\t    <img src=\"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20441.8%20136%22%3E%3Cstyle%3E.st0%7Bfill%3A%2362ae25%7D.st1%7Bfill%3A%23999%7D%3C%2Fstyle%3E%3Cg%20id%3D%22logo_1_%22%3E%3Cpath%20class%3D%22st0%22%20d%3D%22M130.7%2046v9h6.5v6.2h-6.5v20.6c0%201.4.3%202.4.8%203.1.6.6%201.6.9%203%20.9%201%200%201.9-.1%202.9-.3v6.4c-1.9.5-3.7.8-5.5.8-6.4%200-9.6-3.5-9.6-10.6v-21h-6.1V55h6.1v-9h8.4zM156.3%2092.7c-5.3%200-9.5-1.7-12.8-5s-4.9-7.7-4.9-13.2v-1c0-3.7.7-7%202.1-9.9%201.4-2.9%203.4-5.2%206-6.8s5.4-2.4%208.6-2.4c5%200%208.9%201.6%2011.7%204.8%202.7%203.2%204.1%207.8%204.1%2013.6v3.3H147c.3%203.1%201.3%205.5%203.1%207.2%201.8%201.8%204%202.7%206.8%202.7%203.8%200%206.9-1.5%209.3-4.6l4.5%204.3c-1.5%202.2-3.5%203.9-5.9%205.1-2.6%201.3-5.4%201.9-8.5%201.9zm-1-31.7c-2.3%200-4.1.8-5.5%202.4-1.4%201.6-2.3%203.8-2.7%206.7h15.8v-.6c-.2-2.8-.9-4.9-2.2-6.3-1.3-1.5-3.1-2.2-5.4-2.2zM186.1%2076.1l-3.7%203.8V92h-8.3V39.5h8.3v30.3l2.6-3.2L195.2%2055h10l-13.7%2015.4L206.7%2092h-9.6l-11-15.9z%22%2F%3E%3Cpath%20class%3D%22st0%22%20d%3D%22M227.9%2082c0-1.5-.6-2.6-1.8-3.4s-3.2-1.5-6.1-2.1c-2.8-.6-5.2-1.3-7.1-2.3-4.1-2-6.2-4.9-6.2-8.7%200-3.2%201.3-5.9%204-8s6.1-3.2%2010.3-3.2c4.4%200%208%201.1%2010.7%203.3s4.1%205%204.1%208.5h-8.3c0-1.6-.6-2.9-1.8-4s-2.8-1.6-4.7-1.6c-1.8%200-3.3.4-4.5%201.3-1.2.8-1.7%202-1.7%203.4%200%201.3.5%202.3%201.6%203s3.2%201.4%206.5%202.1%205.8%201.6%207.7%202.6%203.2%202.2%204.1%203.6c.9%201.4%201.3%203.1%201.3%205.1%200%203.3-1.4%206-4.1%208.1-2.8%202.1-6.4%203.1-10.8%203.1-3%200-5.7-.5-8.1-1.6-2.4-1.1-4.2-2.6-5.5-4.5s-2-4-2-6.2h8.1c.1%202%20.9%203.5%202.2%204.5%201.4%201.1%203.2%201.6%205.4%201.6s3.9-.4%205-1.2c1.1-1%201.7-2.1%201.7-3.4zM250.2%2046v9h6.5v6.2h-6.5v20.6c0%201.4.3%202.4.8%203.1.6.6%201.6.9%203%20.9%201%200%201.9-.1%202.9-.3v6.4c-1.9.5-3.7.8-5.5.8-6.4%200-9.6-3.5-9.6-10.6v-21h-6.1V55h6.1v-9h8.4zM257%2073.2c0-3.6.7-6.9%202.2-9.8s3.5-5.1%206-6.7c2.6-1.6%205.6-2.3%209-2.3%205%200%209%201.6%2012.2%204.8%203.1%203.2%204.8%207.5%205%2012.8v1.9c0%203.6-.7%206.9-2.1%209.8s-3.4%205.1-6%206.7-5.6%202.4-9%202.4c-5.2%200-9.4-1.7-12.5-5.2s-4.7-8.1-4.7-13.9v-.5h-.1zm8.3.7c0%203.8.8%206.8%202.4%208.9%201.6%202.2%203.8%203.2%206.6%203.2s5-1.1%206.5-3.3c1.6-2.2%202.3-5.4%202.3-9.6%200-3.7-.8-6.7-2.4-8.9-1.6-2.2-3.8-3.3-6.5-3.3s-4.9%201.1-6.5%203.2c-1.6%202.3-2.4%205.5-2.4%209.8zM327.4%2080.5l5.9-25.5h8.1l-10.1%2037h-6.8l-7.9-25.4-7.9%2025.4h-6.8l-10.1-37h8.1l6%2025.3%207.6-25.3h6.3l7.6%2025.5zM341.8%2073.2c0-3.6.7-6.9%202.2-9.8s3.5-5.1%206-6.7c2.6-1.6%205.6-2.3%209-2.3%205%200%209%201.6%2012.2%204.8%203.1%203.2%204.8%207.5%205%2012.8v1.9c0%203.6-.7%206.9-2.1%209.8s-3.4%205.1-6%206.7-5.6%202.4-9%202.4c-5.2%200-9.4-1.7-12.5-5.2s-4.7-8.1-4.7-13.9v-.5h-.1zm8.3.7c0%203.8.8%206.8%202.4%208.9%201.6%202.2%203.8%203.2%206.6%203.2s5-1.1%206.5-3.3c1.6-2.2%202.3-5.4%202.3-9.6%200-3.7-.8-6.7-2.4-8.9-1.6-2.2-3.8-3.3-6.5-3.3s-4.9%201.1-6.5%203.2c-1.6%202.3-2.4%205.5-2.4%209.8zM384.9%2083.3c1.5%200%202.7.4%203.6%201.3.8.9%201.3%202%201.3%203.3s-.4%202.4-1.3%203.2c-.8.9-2%201.3-3.6%201.3-1.5%200-2.7-.4-3.5-1.3-.9-.8-1.3-1.9-1.3-3.2%200-1.3.4-2.4%201.3-3.3.8-.9%202-1.3%203.5-1.3zM428.2%2073.9c0%205.7-1.3%2010.3-3.9%2013.7s-6.1%205.1-10.5%205.1c-4.1%200-7.3-1.3-9.7-4v17.5h-8.3V55h7.7l.3%203.8c2.4-3%205.8-4.4%209.9-4.4%204.5%200%208%201.7%2010.6%205%202.6%203.4%203.8%208%203.8%2014v.5h.1zm-8.3-.7c0-3.7-.7-6.6-2.2-8.8-1.5-2.2-3.6-3.2-6.3-3.2-3.4%200-5.8%201.4-7.3%204.2v16.4c1.5%202.9%204%204.3%207.4%204.3%202.6%200%204.7-1.1%206.2-3.2%201.5-2.2%202.2-5.4%202.2-9.7zM440.5%2092h-8.3V39.5h8.3V92z%22%2F%3E%3Cpath%20class%3D%22st1%22%20d%3D%22M123.5%20107.6l-.5%202.8h2.1l-.3%201.5h-2.1l-1.2%207.2v.5c0%20.6.4.8.9.8.2%200%20.6%200%20.9-.1l-.2%201.6c-.5.1-.9.2-1.4.2-.8%200-1.4-.3-1.7-.9-.4-.6-.5-1.3-.5-2.2l1.2-7.2h-2.1l.3-1.5h2.1l.5-2.8%202%20.1zM129.3%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2.7.8%201.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.6.8-2.6.8zm1.3-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1.1-.8-1.8-.8zM141.8%20112.1l-.9-.1c-1.2%200-2.2.6-2.9%201.7l-1.4%208.3h-1.9l2-11.6h1.9l-.3%201.4c.8-1.1%201.8-1.6%202.9-1.6.2%200%20.5.1.9.2l-.3%201.7zM147.5%20122c-.1-.2-.1-.4-.1-.6v-.6c-1.1%201-2.2%201.5-3.4%201.4-1%200-1.9-.3-2.5-1-.6-.6-.9-1.4-.9-2.4.1-1.2.6-2.2%201.6-2.9%201-.7%202.3-1%204-1h2l.1-.9c.1-.7-.1-1.2-.4-1.6-.3-.4-.9-.6-1.5-.6-.7%200-1.3.1-1.8.5-.5.3-.8.8-.9%201.3h-2c.1-.7.3-1.3.8-1.8s1.1-.9%201.8-1.2c.7-.3%201.5-.4%202.3-.4%201.2%200%202.2.4%202.8%201.1.7.7%201%201.6.8%202.8l-.9%205.8-.1.8c0%20.4%200%20.8.1%201.2v.2h-1.8v-.1zm-3-1.5c.6%200%201.2-.1%201.8-.4s1-.7%201.4-1.3l.4-2.4h-1.5c-1.2%200-2.2.2-2.8.6-.7.4-1%20.9-1.1%201.6-.1.5.1%201%20.4%201.4.3.3.8.5%201.4.5zM153.2%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM168.8%20110.4l-.2%201.3c1-1%202.2-1.5%203.5-1.5.7%200%201.4.2%201.9.5s.9.8%201.1%201.4c1.1-1.3%202.4-1.9%203.9-1.9%201.2%200%202%20.4%202.6%201.2.6.8.8%201.8.6%203l-1.3%207.6H179l1.3-7.7v-1c-.1-1-.8-1.5-1.9-1.5-.7%200-1.4.2-1.9.7-.6.5-.9%201.1-1.1%201.8l-1.3%207.7h-2l1.3-7.6c.1-.8%200-1.4-.4-1.8s-.9-.7-1.6-.7c-1.2%200-2.2.5-2.9%201.7l-1.5%208.5h-1.9l2-11.6%201.7-.1zM189.1%20110.2c1%200%201.8.3%202.5.8s1.2%201.2%201.5%202.1.4%201.9.3%203v.2c-.1%201.1-.5%202.2-1%203.1s-1.2%201.6-2.1%202.1c-.9.5-1.8.7-2.8.7s-1.8-.3-2.5-.8-1.2-1.2-1.5-2.1-.4-1.9-.3-2.9c.1-1.2.4-2.3%201-3.2.5-1%201.2-1.7%202.1-2.2.8-.6%201.8-.9%202.8-.8zm-4%206.2c-.1.5-.1.9%200%201.4.1.8.3%201.5.8%202%20.4.5%201%20.8%201.7.8.6%200%201.2-.1%201.8-.5.5-.3%201-.9%201.4-1.5.4-.7.6-1.5.7-2.3.1-.7.1-1.2%200-1.7-.1-.9-.3-1.6-.8-2.1-.4-.5-1-.8-1.7-.8-1%200-1.9.4-2.6%201.2-.7.8-1.1%201.9-1.3%203.2v.3zM195.8%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zm3.2-13c0-.3.1-.6.3-.8.2-.2.5-.3.8-.4.3%200%20.6.1.8.3.2.2.3.5.3.8s-.1.6-.3.8c-.2.2-.5.3-.8.3s-.6-.1-.8-.3c-.2-.1-.3-.4-.3-.7zM208.1%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2.7.8%201.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1-.8-1.8-.8zM220.6%20118.9c.1-.8-.3-1.4-1.3-1.7l-2-.6c-1.7-.6-2.5-1.6-2.5-2.9%200-1%20.5-1.9%201.4-2.5.9-.7%201.9-1%203.1-1%201.2%200%202.1.4%202.9%201%20.7.7%201.1%201.5%201.1%202.6h-1.9c0-.6-.2-1.1-.5-1.4s-.9-.6-1.5-.6c-.7%200-1.3.2-1.7.5-.5.3-.7.7-.8%201.3-.1.7.3%201.2%201.2%201.5l1%20.3c1.3.3%202.3.8%202.8%201.3s.8%201.2.8%202.1c0%20.7-.3%201.4-.7%201.9s-1%20.9-1.7%201.2c-.7.3-1.5.4-2.3.4-1.2%200-2.2-.4-3.1-1.1-.8-.7-1.2-1.6-1.2-2.7h1.9c0%20.7.2%201.2.6%201.6.4.4%201%20.6%201.7.6s1.3-.1%201.8-.4c.5-.5.8-.9.9-1.4zM225.5%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM241.3%20110.4l-2.2%2013c-.1%201.1-.5%201.9-1.1%202.5s-1.4.9-2.3.8c-.4%200-.8-.1-1.3-.2l.2-1.6c.3.1.6.1.9.1.9%200%201.5-.6%201.7-1.7l2.2-13%201.9.1zm-1.6-3.1c0-.3.1-.6.3-.8.2-.2.5-.3.8-.4.3%200%20.6.1.8.3.2.2.3.5.3.8s-.1.6-.3.8-.5.3-.8.3-.6-.1-.8-.3-.3-.4-.3-.7zM246.4%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2s1.1%201.8%201.1%203.1v1.4l-.1.9H244c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1-.8-1.8-.8zM266.5%20116.2c-.1%201.2-.5%202.2-1%203.2s-1.1%201.7-1.8%202.1c-.7.5-1.5.7-2.4.7-1.3%200-2.4-.5-3.1-1.4l-1%205.6h-1.9l2.8-16.1h1.8l-.2%201.3c1-1%202.1-1.5%203.4-1.5%201.1%200%202%20.4%202.6%201.2s1%201.9%201%203.3c0%20.5%200%20.9-.1%201.3l-.1.3zm-1.9-.2l.1-.9c0-1-.2-1.8-.6-2.4s-1-.8-1.7-.9c-1.1%200-2.1.5-2.9%201.6l-1%205.6c.4%201%201.2%201.6%202.4%201.6%201%200%201.8-.4%202.5-1.1.5-.8.9-2%201.2-3.5zM274.2%20112.1l-.9-.1c-1.2%200-2.2.6-2.9%201.7L269%20122h-1.9l2-11.6h1.9l-.3%201.4c.8-1.1%201.8-1.6%202.9-1.6.2%200%20.5.1.9.2l-.3%201.7zM275.3%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM287.6%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2s1.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.4-.5-1-.8-1.8-.8zM297.8%20120.6c.7%200%201.3-.2%201.9-.6s.9-1%201.1-1.6h1.8c-.1.7-.4%201.4-.9%202s-1.1%201.1-1.8%201.4-1.5.5-2.2.5c-1%200-1.8-.3-2.5-.8s-1.2-1.2-1.5-2.1-.4-1.9-.3-3l.1-.5c.1-1.1.5-2.1%201-3s1.2-1.6%202-2.1%201.7-.7%202.8-.7c1.2%200%202.2.4%202.9%201.2s1.1%201.8%201.1%203h-1.8c0-.8-.2-1.4-.6-1.8-.4-.5-1-.7-1.7-.7-1%200-1.8.4-2.5%201.1-.7.8-1.1%201.9-1.3%203.2v1.7c0%20.8.3%201.5.7%202s1%20.8%201.7.8zM305.2%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM316.8%20119.1l4.1-8.7h2.1l-6.9%2013.6c-1%201.9-2.2%202.8-3.7%202.8-.3%200-.7-.1-1.2-.2l.2-1.6.5.1c.6%200%201.1-.1%201.6-.4.4-.3.8-.8%201.2-1.5l.7-1.3-2-11.4h2l1.4%208.6z%22%2F%3E%3Cpath%20class%3D%22st1%22%20d%3D%22M326.9%20107.6l-.5%202.8h2.1l-.3%201.5h-2.1l-1.2%207.2v.5c0%20.6.4.8.9.8.2%200%20.6%200%20.9-.1l-.2%201.6c-.5.1-.9.2-1.4.2-.8%200-1.4-.3-1.7-.9-.4-.6-.5-1.3-.5-2.2l1.2-7.2H322l.3-1.5h2.1l.5-2.8%202%20.1zM334.8%20122c0-.2-.1-.4-.1-.6v-.6c-1.1%201-2.2%201.5-3.4%201.4-1%200-1.9-.3-2.5-1-.6-.6-.9-1.4-.9-2.4.1-1.2.6-2.2%201.6-2.9%201-.7%202.3-1%204-1h2l.1-.9c.1-.7-.1-1.2-.4-1.6s-.9-.6-1.5-.6c-.7%200-1.3.1-1.8.5-.5.3-.8.8-.9%201.3h-2c.1-.7.3-1.3.8-1.8s1.1-.9%201.8-1.2c.7-.3%201.5-.4%202.3-.4%201.2%200%202.2.4%202.8%201.1.7.7%201%201.6.8%202.8l-.9%205.8-.1.8c0%20.4%200%20.8.1%201.2v.2h-1.8v-.1zm-3-1.5c.6%200%201.2-.1%201.8-.4.6-.3%201-.7%201.4-1.3l.4-2.4h-1.5c-1.2%200-2.2.2-2.8.6s-1%20.9-1.1%201.6c-.1.5.1%201%20.4%201.4.3.3.8.5%201.4.5zM343.3%20120.6c.7%200%201.3-.2%201.9-.6s.9-1%201.1-1.6h1.8c-.1.7-.4%201.4-.9%202s-1.1%201.1-1.8%201.4-1.5.5-2.2.5c-1%200-1.8-.3-2.5-.8s-1.2-1.2-1.5-2.1-.4-1.9-.3-3l.1-.5c.1-1.1.5-2.1%201-3s1.2-1.6%202-2.1%201.7-.7%202.8-.7c1.2%200%202.2.4%202.9%201.2s1.1%201.8%201.1%203H347c0-.8-.2-1.4-.6-1.8-.4-.5-1-.7-1.7-.7-1%200-1.8.4-2.5%201.1-.7.8-1.1%201.9-1.3%203.2v1.7c0%20.8.3%201.5.7%202s.9.8%201.7.8zm2.7-15.1h2.5l-3.3%203.2h-1.7l2.5-3.2z%22%2F%3E%3Cpath%20id%3D%22box%22%20d%3D%22M92.9%20124H7.1c-3.9%200-7.1-3.2-7.1-7.1V31.1C0%2027.2%203.2%2024%207.1%2024h85.8c3.9%200%207.1%203.2%207.1%207.1v85.8c0%203.9-3.2%207.1-7.1%207.1z%22%20fill%3D%22%23f60%22%2F%3E%3Cpath%20d%3D%22M68%2044.1c-6.8-7-15.9-12.2-24.6-14.4C28.8%2026%2025%2023.9%2017.8%2019c-4.1-2.8-7.2-8.4-10.6-9.8-1.6-.6-2.6.1-3.1.7-.9%201-1.6%202.9-.4%205.2%2010.1%2019.8%2048.4%2076.8%2048.4%2076.8-5.9%201.1-12.4%205.7-17%2012.9-6.9%2010.7-7%2022.9-.1%2027.2%206.9%204.3%2018.1-.9%2025-11.6%205.9-9%206.8-19.1%202.8-24.6L29.2%2042.7s-3.9-4.7%202.3-6.1c4.5-1%2013.9.1%2019%202.8C56%2042.3%2069%2051.7%2072.3%2065.1c1.5%206.2.6%209.6%201.4%2015.1.4%202.6%203.2%204.8%205.5%201.2%201.2-1.9%201.4-8.4%201.1-13.5-.5-6.7-6-17.3-12.3-23.8zm-10.4%2048c.3.1.7.2%201%20.3-.3-.1-.6-.2-1-.3zm-2.5-.4h.4-.4zm1.1.1c.3%200%20.6.1.8.1-.2%200-.5-.1-.8-.1zm-3%200c.1%200%20.1%200%200%200%20.1%200%20.1%200%200%200zm5.8.8l1.2.6c-.4-.3-.8-.5-1.2-.6zm2.9%202zm-.7-.7c-.3-.3-.6-.5-1-.7.3.2.6.5%201%20.7z%22%20opacity%3D%22.2%22%2F%3E%3ClinearGradient%20id%3D%22SVGID_1_%22%20gradientUnits%3D%22userSpaceOnUse%22%20x1%3D%2239.888%22%20y1%3D%2292.284%22%20x2%3D%2252.096%22%20y2%3D%22125.824%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%238fe132%22%2F%3E%3Cstop%20offset%3D%22.362%22%20stop-color%3D%22%2365bd23%22%2F%3E%3Cstop%20offset%3D%22.668%22%20stop-color%3D%22%2349a519%22%2F%3E%3Cstop%20offset%3D%22.844%22%20stop-color%3D%22%233e9c15%22%2F%3E%3C%2FlinearGradient%3E%3Cpath%20d%3D%22M57.9%20117.8c-7.6%2010.4-19.2%2014.9-25.7%2010.1s-5.7-17.2%201.9-27.6%2019.2-14.9%2025.7-10.1c6.6%204.9%205.7%2017.2-1.9%2027.6z%22%20fill%3D%22url%28%23SVGID_1_%29%22%2F%3E%3ClinearGradient%20id%3D%22SVGID_2_%22%20gradientUnits%3D%22userSpaceOnUse%22%20x1%3D%22-3.574%22%20y1%3D%2224.316%22%20x2%3D%2282.773%22%20y2%3D%2274.169%22%3E%3Cstop%20offset%3D%22.184%22%20stop-color%3D%22%238fe132%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%233e9c15%22%2F%3E%3C%2FlinearGradient%3E%3Cpath%20d%3D%22M70.9%2041c-6.3-7.6-15-13.4-23.6-16.2-14.4-4.7-18-7.1-24.8-12.5-3.9-3.1-6.6-9-9.9-10.7-1.6-.8-2.6-.1-3.2.5-.9.9-1.8%202.8-.7%205.2C17.4%2028%2051.9%2088.4%2051.9%2088.4c3-.3%205.7.2%207.9%201.8%201%20.8%201.8%201.7%202.5%202.8l-30-56s-3.6-5%202.7-6c4.5-.7%2013.9%201.1%2018.8%204.1%205.3%203.3%2017.6%2013.7%2020%2027.5%201.1%206.4%200%209.7.4%2015.4.2%202.6%202.9%205.1%205.4%201.6%201.3-1.8%202-8.4%202-13.6%200-6.8-4.9-17.9-10.7-25z%22%20fill%3D%22url%28%23SVGID_2_%29%22%2F%3E%3CradialGradient%20id%3D%22SVGID_3_%22%20cx%3D%2221.505%22%20cy%3D%22103.861%22%20r%3D%2214.934%22%20gradientTransform%3D%22matrix%28.2966%20.4025%20-.805%20.5933%20123.22%2029.092%29%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%23f4ff72%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%2373c928%22%20stop-opacity%3D%220%22%2F%3E%3C%2FradialGradient%3E%3Cpath%20d%3D%22M50.4%20105.4c-6.6%204.9-14%206.2-16.5%202.8-2.4-3.3%201-10%207.6-14.9s14-6.2%2016.5-2.8c2.5%203.3-.9%2010-7.6%2014.9z%22%20fill%3D%22url%28%23SVGID_3_%29%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E\" class=\"img-fluid\" alt=\"tekstowo.pl\" title=\"Teksty piosenek, tłumaczenia, teledyski\">\t\t</a>\r\n\t    </div>\r\n\r\n\t    <div class=\"col\">\r\n\t\t<div class=\"top-links\">\r\n\t\t    \t\t\t<a href=\"/logowanie.html\" title=\"Logowanie\">Logowanie</a> |\r\n\t\t\t<a href=\"/rejestracja.html\" title=\"Rejestracja\">Rejestracja</a> |\r\n\t\t    \t\t    <a href=\"/faq.html\" title=\"FAQ\">FAQ</a> |\r\n\t\t    <a href=\"/regulamin.html\" title=\"Regulamin\">Regulamin</a> |\r\n\t\t    <a href=\"/kontakt.html\" title=\"Kontakt\">Kontakt</a>\r\n\t\t</div>\r\n\r\n\t\t<div class=\"topmenu\">\r\n\t\t    <ul class=\"\">\r\n    <li class=\"topmenu-first\"><a href=\"/\" title=\"Główna\">Główna</a></li>\r\n    <li><a href=\"/przegladaj_teksty.html\" title=\"Teksty\">Teksty</a></li>\r\n    <li><a href=\"/szukane_utwory,6-miesiecy.html\" title=\"Poszukiwane teksty\">Poszukiwane teksty</a></li>\r\n    <li><a href=\"/soundtracki_najnowsze.html\" title=\"Soundtracki\">Soundtracki</a></li>\r\n    <li><a href=\"/rankingi\" title=\"Rankingi\">Rankingi</a></li>\r\n    <li class=\"topmenu-last\"><a href=\"/uzytkownicy.html\" title=\"Użytkownicy\" class=\"no-bg \">Użytkownicy</a></li>\r\n\t</ul>\t\t</div>\r\n\t    </div>\r\n\t</div>\r\n\t<div id=\"t170319\">\r\n\t    <div class=\"adv-top\" style=\"min-height: 300px\"> <!-- reklama bill -->\r\n\t\t<div>\r\n\t\t    <a title=\"Ukryj reklamy\" href=\"javascript:;\" rel=\"loginbox\" id=\"hide-ads\"></a>\t\t    \r\n    \t\t    \t    \r\n    \r\n    \t    \r\n    \r\n   \r\n\r\n<!-- kod reklamy desktop -->\r\n<center>\r\n<script data-adfscript=\"adx.adform.net/adx/?mid=668393&amp;rnd=<random_number>\"></script>\r\n</center> \r\n \r\n\r\n\t\t</div>\r\n\t    </div> <!-- end reklama bill -->\r\n\t</div>\r\n\t\t<div class=\"row topbar\">\r\n\t    <div class=\"col-auto\">\r\n\t\t<a href=\"/\" class=\"green\" title=\"Teksty piosenek\">Teksty piosenek</a> &gt; <a href=\"/artysci_na,B.html\" class=\"green\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę B\">B</a>\r\n\t\t &gt; <a href=\"/piosenki_artysty,beethoven.html\" class=\"green\" title=\"Beethoven\">Beethoven</a>\r\n\t\t &gt; <a href=\"/piosenka,beethoven,beethoven_piano_sonata_17_tempest_the_3rd_movement.html\" class=\"green\" title=\"Beethoven Piano Sonata 17 Tempest The 3rd Movement\">Beethoven Piano Sonata 17 Tempest The 3rd Movement</a>\r\n\t\t\t\t</div>\r\n\t\t<div class=\"col d-none text-right d-md-block\">\r\n\t\t    2 170 744 tekstów, 20 217 poszukiwanych i 502 oczekujących\r\n\t\t</div>\r\n\t    </div>\r\n\r\n\t    <div class=\"row\">\r\n\r\n\t\t<div class=\"col-sm-4 col-lg-3 order-2 order-sm-1\">\r\n    <div class=\"big-buttons row mr-0\" role=\"group\">\r\n    <a href=\"/dodaj_tekst.html\" rel=\"loginbox\" class=\"dodaj-tekst\" title=\"Dodaj tekst\">\r\n\t<i class=\"icon\"></i>\r\n\tDodaj tekst\r\n    </a>\r\n    <a href=\"/zaproponuj_utwor.html\" rel=\"loginbox\" class=\"zaproponuj-utwor\" title=\"Szukasz utworu?\">\r\n\t<i class=\"icon\"></i>\r\n\tBrak tekstu?</a>\r\n    <a href=\"/dodaj_soundtrack.html\" rel=\"loginbox\" class=\"dodaj-soundtrack\" title=\"Dodaj soundtrack\">\r\n\t<i class=\"icon\"></i>\r\n\tDodaj soundtrack</a>\r\n</div>\r\n        \t<div class=\"left-box account-box mt-3\">\r\n\t    <h4>Zaloguj się</h4>\r\n\t    i wykorzystaj wszystkie możliwości serwisu!\r\n\r\n\t    <fieldset class=\"login mt-2 text-center\">\r\n\t\t<a class=\"login-send btn btn-block btn-primary mb-2\" href=\"https://www.tekstowo.pl/logowanie.html\">Zaloguj się</a>\r\n\t\t<a href=\"javascript:;\" class=\"fb-button my-fb-login-button btn btn-block btn-fb\" title=\"Zaloguj się z Facebookiem\"><span class=\"icon\"></span>Zaloguj się przez Facebooka</a>\r\n\t\t\t    </fieldset>\r\n\r\n\t    <ul class=\"arrows mt-2\">\r\n\t\t<li><a href=\"/przypomnij.html\" class=\"green bold\" title=\"Zapomniałem hasła\">Przypomnienie hasła</a></li>\r\n\t\t<li><a href=\"/rejestracja.html\" class=\"green bold\" title=\"Nie mam jeszcze konta\">Nie mam jeszcze konta</a></li>\r\n\t    </ul>\r\n\r\n\t</div>\r\n    \r\n    \t\t\t\t\t\t\t<div class=\"left-box mt-3\">\r\n\t\t\t\t<h4>Inne teksty piosenek</h4>\r\n\t\t\t\t<h4>Beethoven</h4>\r\n\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t\t<b>1.</b> <a href=\"piosenka,beethoven,piano_sonata_216_les_adieux_the_1st_movement.html\" class=\"title\" title=\"Beethoven - Piano Sonata 216 Les Adieux The 1st Movement\">Beethoven - Piano Sonata 216 Les Adieux The 1st Movement </a>\r\n\t\t\t\t\t<b title=\"utwór instrumentalny\" class=\"icon_inst\"></b><b title=\"teledysk\" class=\"icon_kamera\"></b>\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t\t<b>2.</b> <a href=\"piosenka,beethoven,beethoven_5th_symphony_opening_evil.html\" class=\"title\" title=\"Beethoven - Beethoven 5th Symphony opening Evil\">Beethoven - Beethoven 5th Symphony opening Evil </a>\r\n\t\t\t\t\t<b title=\"utwór instrumentalny\" class=\"icon_inst\"></b>\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t\t\t<div class=\"box-przeboje no-bg\">\r\n\t\t\t\t\t<b>3.</b> <a href=\"piosenka,beethoven,symphony_no__9_choral___iv__finale__ode_to_joy.html\" class=\"title\" title=\"Beethoven - Symphony No. 9 Choral - IV. Finale: Ode to Joy\">Beethoven - Symphony No. 9 Choral - IV. Finale: Ode to Joy </a>\r\n\t\t\t\t\t<b title=\"utwór instrumentalny\" class=\"icon_inst\"></b><b title=\"teledysk\" class=\"icon_kamera\"></b>\t\t\t\t\t</div>\r\n\t\t\t\t\t\t\t</div> <!-- end inne utwory -->\r\n\t\t\t<a href=\"/piosenki_artysty,beethoven.html\" class=\"block text-right\" title=\"Zobacz więcej >>\">Zobacz więcej &gt;&gt;</a>\r\n\t\t\t\t        \r\n    \t<div class=\"left-box mt-3\">\r\n\t    <h4>Poszukiwane teksty</h4>\r\n\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>1.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,Henning+Wehland,tytul,Weil+wir+Champions+sind,poszukiwany,354712.html\" class=\"title\">Henning Wehland - Weil wir Champions sind</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>2.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,Demis+Roussos,tytul,For+Love,poszukiwany,355966.html\" class=\"title\">Demis Roussos - For Love</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>3.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,David+Bowie,tytul,Move+On+%28Moonage+Daydream+A+Cappella+Mix+Edit%29,poszukiwany,357748.html\" class=\"title\">David Bowie - Move On (Moonage Daydream A Cappella Mix Edit)</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>4.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,Rob+Zombie,tytul,El+Vampiro,poszukiwany,358402.html\" class=\"title\">Rob Zombie - El Vampiro</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>5.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,Inside,tytul,Wizzard+King,poszukiwany,355937.html\" class=\"title\">Inside - Wizzard King</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>6.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,Jennifer+McNutt,tytul,After+Everyone,poszukiwany,355721.html\" class=\"title\">Jennifer McNutt - After Everyone</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>7.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,The+Merced+Blue+Notes,tytul,Your+tender+lips,poszukiwany,355933.html\" class=\"title\">The Merced Blue Notes - Your tender lips</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>8.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,Demis+Roussos,tytul,Il+Faut+Qu%27il+Revienne,poszukiwany,355730.html\" class=\"title\">Demis Roussos - Il Faut Qu'il Revienne</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje \">\r\n\t\t    <b>9.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,Soul+Superiors,tytul,A+Great+Day,poszukiwany,355739.html\" class=\"title\">Soul Superiors - A Great Day</a>\r\n\t\t</div>\r\n\t    \t\t<div class=\"box-przeboje no-bg\">\r\n\t\t    <b>10.</b> <a rel=\"loginbox\" href=\"/dodaj_tekst,wykonawca,Jefferson+State,tytul,White+Out,poszukiwany,357438.html\" class=\"title\">Jefferson State - White Out</a>\r\n\t\t</div>\r\n\t    \t</div>\r\n\t<a href=\"/szukane_utwory,6-miesiecy.html\" class=\"block text-right\" title=\"Zobacz więcej >>\">Zobacz więcej &gt;&gt;</a>\r\n\r\n\t\t    <div class=\"left-box mt-3\">\r\n\t\t<h4>Poszukiwane tłumaczenia</h4>\r\n\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>1.</b> <a href=\"/piosenka,ub40,reggae_music.html\" class=\"title\">UB40 - Reggae Music</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>2.</b> <a href=\"/piosenka,rosa_chemical,polka_____feat__ernia__gu__pequeno_.html\" class=\"title\">Rosa Chemical - Polka :-/ (feat. Ernia, Guè Pequeno)</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>3.</b> <a href=\"/piosenka,ub40,so_destructive.html\" class=\"title\">UB40 - So Destructive</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>4.</b> <a href=\"/piosenka,ub40,hurry_come_on_up.html\" class=\"title\">UB40 - Hurry Come On Up</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>5.</b> <a href=\"/piosenka,ub40,two_in_a_one__feat__pablo__gunslinger_.html\" class=\"title\">UB40 - Two In A One  feat. Pablo &amp; Gunslinger </a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>6.</b> <a href=\"/piosenka,demis_roussos,mountains_beyond.html\" class=\"title\">Demis Roussos - Mountains Beyond</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>7.</b> <a href=\"/piosenka,ub40,mi_spliff.html\" class=\"title\">UB40 - Mi Spliff</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>8.</b> <a href=\"/piosenka,the_rubettes,it_s_just_make_believe.html\" class=\"title\">The Rubettes - It's Just Make Believe</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje \">\r\n\t\t\t\t<b>9.</b> <a href=\"/piosenka,ub40,if_it_happens_again.html\" class=\"title\">UB40 - If It Happens Again</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t\t\t\t<div class=\"box-przeboje no-bg\">\r\n\t\t\t\t<b>10.</b> <a href=\"/piosenka,supertramp,where_there_s_a_will.html\" class=\"title\">Supertramp - Where There's a Will</a>\r\n\t\t\t\t</div>\r\n\t\t\t\t    </div>\r\n\t    <a href=\"/szukane_tlumaczenia,6-miesiecy.html\" class=\"block text-right\" title=\"Zobacz więcej >>\">Zobacz więcej &gt;&gt;</a>\r\n\t    \r\n    <!-- polecamy -->\r\n    <div class=\"left-box mt-3\">\r\n\t<h4>Polecamy</h4>\r\n\t<ul class=\"arrows\">\r\n    <li><a href=\"http://www.giercownia.pl\" title=\"Gry online\" class=\"blank\">Gry online</a></li>\r\n    <li><a href=\"http://www.maxior.pl\" title=\"Filmy\" class=\"blank\">Śmieszne filmy</a></li>\r\n    <li><a href=\"http://www.bajer.pl\" title=\"Dziewczyny\" class=\"blank\">Polskie dziewczyny</a></li>\r\n    <li><a href=\"http://m.giercownia.pl\" title=\"Gry na telefon i tablet\" class=\"blank\">Gry na telefon i tablet</a></li>\r\n</ul>    </div>\r\n    <!-- end polecamy -->\r\n\r\n</div> <!-- end left-column -->\r\n\t\t<div class=\"col-sm-8 col-lg-9 order-1 order-sm-2\">\r\n\t\t    \t\t\t<div class=\"row search-box\">\r\n    <div class=\"inner col\">\r\n\r\n\t<div class=\"search-form\">\r\n\t    <form action=\"/wyszukaj.html\" method=\"get\" class=\"form-inline\">\r\n\t\t<label for=\"s-autor\" class=\"big-text my-1 mr-2\">Szukaj tekstu piosenki</label>\r\n\t\t<input type=\"text\" id=\"s-autor\" name=\"search-artist\" class=\"form-control form-control input-artist mb-2 mr-sm-2\" placeholder=\"Podaj wykonawcę\" value=\"\" autocomplete=\"off\">\r\n\t\t<label class=\"my-1 mr-2\" for=\"s-title\"> i/lub </label>\r\n\t\t<input type=\"text\" id=\"s-title\" name=\"search-title\" class=\"form-control form-control input-title mb-2 mr-sm-2\" placeholder=\"Podaj tytuł\" value=\"\" autocomplete=\"off\">\r\n\t\t<input type=\"submit\" class=\"submit\" value=\"Szukaj\">\r\n\t    </form>\r\n\r\n\t    <div class=\"search-adv\">\r\n\t\t<a href=\"/wyszukiwanie-zaawansowane.html\">wyszukiwanie zaawansowane &gt;</a>\r\n\t    </div>\r\n\r\n\t    <hr class=\"d-none d-md-block\">\r\n\t</div>\r\n\t<div class=\"search-browse\">\r\n\t    <div class=\"przegladaj big-text\">Przeglądaj wykonawców na literę</div>\r\n\t    <ul class=\"alfabet\">\r\n\t\t\t\t    <li><a href=\"/artysci_na,A.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę A\">A</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,B.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę B\">B</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,C.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę C\">C</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,D.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę D\">D</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,E.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę E\">E</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,F.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę F\">F</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,G.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę G\">G</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,H.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę H\">H</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,I.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę I\">I</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,J.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę J\">J</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,K.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę K\">K</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,L.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę L\">L</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,M.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę M\">M</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,N.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę N\">N</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,O.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę O\">O</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,P.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę P\">P</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,Q.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę Q\">Q</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,R.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę R\">R</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,S.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę S\">S</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,T.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę T\">T</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,U.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę U\">U</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,V.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę V\">V</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,W.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę W\">W</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,X.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę X\">X</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,Y.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę Y\">Y</a></li>\r\n\t\t    \t\t    <li><a href=\"/artysci_na,Z.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców na literę Z\">Z</a></li>\r\n\t\t    \t\t<li><a href=\"/artysci_na,pozostale.html\" title=\"Teksty piosenek, tłumaczenia piosenek i teledyski wykonawców w kategorii &quot;Pozostałe&quot;\">0-9</a></li>\r\n\t    </ul>\r\n\t</div>\r\n\r\n    </div> <!-- end green-box -->\r\n</div>\t\t    \r\n<div class=\"row right-column\"> <!-- right column -->\r\n    <div class=\"col\">\r\n\r\n\t \r\n\r\n\t<div class=\"belka row mx-0 px-3\">\r\n\t    <div class=\"col-lg-7 px-0\">\r\n\t\t<h1 class=\"strong\">Beethoven - Beethoven Piano Sonata 17 Tempest The 3rd Movement</h1>\r\n\t    </div>\r\n\t    <div class=\"col-lg-5\">\r\n\t\t<div class=\"row belka-right\">\r\n\t\t    <div class=\"col\">\r\n\t\t\t<div class=\"odslon\">Odsłon: 25</div>\r\n\t\t    </div>\r\n\t\t    <div class=\"col-auto\">\r\n\t\t\t<a href=\"/wykonawca,beethoven.html\" class=\"link-wykonawca\" title=\"Przejdź na stronę wykonawcy >\">Przejdź na stronę wykonawcy &gt;</a>\r\n\t\t    </div>\r\n\t\t</div>  \r\n\t    </div>\r\n\t</div>\r\n\r\n\r\n\t\r\n\t\r\n\t\t    \r\n                <script type=\"text/javascript\">\r\n\t\t    window.addEvent('domready', function () {\r\n\t\t\t_gaq.push(['_setCustomVar', 1, 'Instrumental Visit', '2299782', 1]);\r\n\t\t\t_gaq.push(['_trackEvent', 'Instrumental', 'Visited']);\r\n\t\t    });\r\n                </script>\r\n\t    \r\n\t\r\n\t<div class=\"row mx-0\">\r\n\t    \t\t<div id=\"advSong\" class=\"adv-home col-lg-6 mt-2 order-2 order-lg-1\"> <!-- reklama środek -->\r\n\t\t        \t    \r\n    \t    \r\n    \t    \r\n    \r\n\r\n   \r\n\r\n<!-- kod reklamy desktop -->\r\n<center>\r\n<script async=\"\" src=\"https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js\"></script>\r\n<!-- tekstowo desktop 336x280 -->\r\n<ins class=\"adsbygoogle\" style=\"display:inline-block;width:336px;height:280px\" data-ad-client=\"ca-pub-3653916656187385\" data-ad-slot=\"1803845785\"><iframe id=\"aswift_0\" style=\"height: 1px !important; max-height: 1px !important; max-width: 1px !important; width: 1px !important;\"><iframe id=\"google_ads_frame0\"></iframe></iframe></ins>\r\n<script>\r\n     (adsbygoogle = window.adsbygoogle || []).push({});\r\n</script>\r\n</center>\r\n   \r\n\t\t</div> <!-- end reklama środek -->\r\n\t    \r\n\t    <div class=\"teledysk zdjecie col-lg-6 order-1 order-lg-2 px-0\">\r\n\t\t\t\t    <div class=\"movieDivWrap\">\r\n\t\t\t<div id=\"movieDiv\">\r\n\t\t\t    \t\t\t\t\t\t\t\t\t\t\t\t\t\t    \t\t\t\t\t\t\t\t\t    <iframe style=\"border: 0; margin: 0; padding: 0; overflow: hidden\" width=\"365\" height=\"280\" src=\"//filmiki4.maxart.pl/tplayer3n/#adText=1&amp;autoplay=1&amp;videoId=hKkR4YFtyJk&amp;loadVideoTimeout=500000&amp;volume=0&amp;aoID=DVJAPyL5hymQ7Q0.2FRYVWxk34YuoC7ExS6BHR_5ALr.c7\" frameborder=\"0\" allowfullscreen=\"1\" scrolling=\"no\"></iframe>\r\n\t\t\t\t    \t\t\t\t\t\t\t    </div>\r\n\t\t\t</div>\r\n\t\t    \t\t</div>\r\n\t    </div>\r\n\r\n\t    <div class=\"row mt-3 mx-0\">\r\n\t\t<div class=\"col-lg px-md-0 pr-lg-3\">\r\n\t\t    \t\t\t<div class=\"teledysk-left\">\r\n\t\t\t    <div class=\"box-przeboje\">\r\n\t\t\t\tUtwór dodał(a): <a title=\"Tekst dodany 29.08.2022 przez anmar09\" href=\"profil,anmar09.html\">anmar09</a>\r\n\t\t\t    </div>\r\n\t\t\t    <div class=\"box-przeboje\">\r\n\t\t\t\tUtwór instrumentalny <a rel=\"loginbox\" href=\"/edytuj_tekst,beethoven,beethoven_piano_sonata_17_tempest_the_3rd_movement.html\" class=\"new\" title=\"Ten utwór ma słowa? Dodaj tekst\">Ten utwór ma słowa? Dodaj tekst</a>\r\n\t\t\t    </div>\r\n\t\t\t    <div class=\"box-przeboje no-bg\">\r\n\t\t\t\t<span>Teledysk dodał(a): <a title=\"Teledysk dodany 29.08.2022 przez anmar09\" href=\"profil,anmar09.html\">anmar09</a></span>\r\n\t\t\t\t<a rel=\"loginbox\" href=\"javascript:editTeledisc('beethoven','beethoven_piano_sonata_17_tempest_the_3rd_movement', '');\" class=\"new\" title=\"Edytuj teledysk\">Edytuj teledysk</a>\r\n\t\t\t    </div>\r\n\t\t\t</div>\r\n\t\t    \t\t</div>\r\n\r\n\t\t<div class=\"col-lg px-md-0 mt-3 mt-lg-0\">\r\n\t\t    <div class=\"teledysk-right\">\r\n\t\t\t<div class=\"social\">\r\n\t\t\t    <label for=\"emb\">Skopiuj link:</label>\r\n\t\t\t    \t\t\t    \t\t\t</div>\r\n\t\t\t<fieldset class=\"mt-0 mb-2 mt-2 mt-lg-0\">\r\n\t\t\t    <input type=\"text\" id=\"emb\" name=\"emb\" readonly=\"\" value=\"https://www.tekstowo.pl/piosenka,beethoven,beethoven_piano_sonata_17_tempest_the_3rd_movement.html\" class=\"emb form-control\" onfocus=\"this.select();\" onclick=\"this.select();\r\n\t\t\t\t\t   document.execCommand('copy');\r\n\t\t\t\t\t   modalAlert('Udostępnianie tekstu', 'Link skopiowano do schowka!')\">\r\n\t\t\t</fieldset>\r\n\t\t\t\t\t\t    <div class=\"social mt-1 social-even\">\r\n\t\t\t\t<a class=\"btn btn-sm btn-fb share-pop\" href=\"https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fwww.tekstowo.pl%2Fpiosenka%2Cbeethoven%2Cbeethoven_piano_sonata_17_tempest_the_3rd_movement.html\"><i class=\"icon\"></i>&nbsp;Udostępnij</a>\r\n\t\t\t\t<a class=\"btn btn-sm btn-tweeter share-pop\" href=\"https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.tekstowo.pl%2Fpiosenka%2Cbeethoven%2Cbeethoven_piano_sonata_17_tempest_the_3rd_movement.html&amp;text=Beethoven - Beethoven Piano Sonata 17 Tempest The 3rd Movement\"><i class=\"icon\"></i>&nbsp;Tweetnij</a>\r\n\t\t\t\t<a class=\"btn btn-sm btn-messenger share-pop\" href=\"https://www.facebook.com/dialog/send?app_id=131858753537922&amp;link=https%3A%2F%2Fwww.tekstowo.pl%2Fpiosenka%2Cbeethoven%2Cbeethoven_piano_sonata_17_tempest_the_3rd_movement.html&amp;redirect_uri=https%3A%2F%2Fwww.tekstowo.pl%2Fpiosenka%2Cbeethoven%2Cbeethoven_piano_sonata_17_tempest_the_3rd_movement.html\"><i class=\"icon\"></i>&nbsp;Messenger</a>\r\n\t\t\t    </div>\r\n\t\t\t\t\t    </div>\r\n\t\t</div>\r\n\t    </div>\r\n\r\n\t    <div class=\"mt-3\">\r\n\t\t<!-- glosowanie -->\r\n\t\t<div class=\"glosowanie\">            \r\n\t\t    <div class=\"vote-group\">        \r\n\t\t\t<span>Głosuj na ten utwór</span>\r\n\t\t\t<div class=\"flex-group song-rank\" data-id=\"2299782\">\r\n    <a rel=\"loginbox\" title=\"ranking +\" href=\"javascript:ajxRankSong('Up',2299782);\" class=\"na-plus\"></a>\r\n    <a rel=\"loginbox\" title=\"ranking -\" href=\"javascript:ajxRankSong('Down', 2299782);\" class=\"na-minus\"></a>\r\n    <span class=\"rank\">(0)</span>\r\n</div>\t\t    </div>\r\n\r\n\t\t    <div class=\"action-group\">\r\n<a href=\"javascript:ajxAddFav('2299782')\" rel=\"loginbox\" class=\"glo-links glo-ulubione\" title=\"Dodaj do ulubionych\">Dodaj do ulubionych</a>\r\n<a href=\"#dodaj_komentarz\" rel=\"loginbox\" class=\"glo-links glo-komentuj\" title=\"Komentuj\">Komentuj</a>\r\n<a href=\"javascript:sendSongBox();\" rel=\"loginbox\" class=\"glo-links glo-polec\" title=\"Poleć znajomemu\">Poleć znajomemu</a>\r\n<a href=\"javascript:errorBox();\" rel=\"loginbox\" class=\"glo-links glo-zglos\" title=\"Zgłoś błąd\">Zgłoś błąd</a>\r\n</div>\t\t</div>\r\n\t\t\t    </div>\r\n\r\n\t    \t    <div class=\"metric\">\r\n\t\t<table>\r\n\t\t    <tbody><tr><th>Ścieżka dźwiękowa:</th><td><p><a href=\"/soundtrack,pokojowka.html\" title=\"Soundtrack Pokojówka\">Pokojówka</a></p></td></tr>\r\n\t\t</tbody></table>\r\n\t    </div>\r\n\t\t<!-- komentarze -->\r\n\t<div class=\"row mt-4 mx-0\">\r\n\t    <div class=\"col-12 px-md-0\">\r\n\t\t<a name=\"komentarze\"></a>\r\n\r\n\t\t<div id=\"comments_content\" class=\"d-none\">\r\n\t\t    <h2 class=\"margint10\">Komentarze (0):</h2>\r\n\t\t    \t\t    \t\t    \r\n\t\t</div>\r\n\r\n\t\t<div id=\"comm_show_more\" class=\"comm_show_more d-none\"></div>\r\n\t    </div>\r\n\r\n\t    <div class=\"col-12\">\r\n\t\t\t\t<noscript>\r\n</noscript>\r\n\t    </div>\r\n\t</div>\r\n\r\n\t<!-- end komentarze -->\r\n\r\n    </div>\r\n</div> <!-- end right column -->\r\n\r\n    <script type=\"text/javascript\">\r\n\t\r\n\t    function addLink() {\r\n\t\tvar body_element = document.getElementsByTagName('body')[0];\r\n\t\tvar selection;\r\n\t\tselection = window.getSelection();\r\n\t\tvar pagelink = \"<br /><br />Tekst pochodzi z <a href='\" + document.location.href + \"'>\" + document.location.href + \"</a>\";\r\n\t\tvar copytext = selection + pagelink;\r\n\t\tvar newdiv = document.createElement('div');\r\n\t\tnewdiv.style.position = 'absolute';\r\n\t\tnewdiv.style.left = '-99999px';\r\n\t\tbody_element.appendChild(newdiv);\r\n\t\tnewdiv.innerHTML = copytext;\r\n\t\tselection.selectAllChildren(newdiv);\r\n\t\twindow.setTimeout(function () {\r\n\t\t    body_element.removeChild(newdiv);\r\n\t\t}, 0);\r\n\t    }\r\n\t    document.getElementById('songText').oncopy = addLink;\r\n\t    document.getElementById('songTranslation').oncopy = addLink;\r\n\r\n\t\r\n    </script>\r\n\r\n    <script type=\"text/javascript\">\r\n\t\r\n\t    var i18nAnn = {\r\n\t\tadd: 'Dodaj',\r\n\t\tadd_title: 'Dodawanie interpretacji',\r\n\t\tadd_desc: 'Dodawanie interpretacji do zaznaczonego tekstu:',\r\n\t\tfunction_unavailble: 'Funkcja niedostępna na urządzeniach dotykowych. Skorzystaj z komputera klasy PC.',\r\n\t\tenabled: 'Uruchomiony został tryb dodawania interpretacji. Zaznacz fragment tekstu, który chcesz zinterpretować',\r\n\t\texit: 'Kliknij tutaj aby wyjść',\r\n\t\texists: 'Istnieje już interpretacja do tego fragmentu, zaznacz inny fragment tekstu',\r\n\t\ttoo_short: 'Treść jest za krótka, napisz coś więcej.',\r\n\t\ttextarea_placehodler: 'Tutaj pisz interpretację',\r\n\t\tsuccess_msg: 'Interpretacja została dodana i oczekuje akceptacji moderatora. Dziękujemy.',\r\n\t\tadded_msg: 'Dodałeś/aś już jedną interpretacje do tego utworu, która oczekuje akceptacji moderatora. <br>Poczekaj na akceptację.'\r\n\t    }\r\n\t\r\n    </script>\r\n</div>\r\n\r\n</div> <!-- end center -->\r\n</div>\r\n\r\n\r\n    <div class=\"generic_dialog outer\" id=\"fb-modal\" style=\"opacity: 0; display: none; visibility: hidden;\">\r\n\t<div class=\"generic_dialog_popup middle\">\r\n\t\t<table class=\"pop_dialog_table inner\" id=\"pop_dialog_table\">\r\n\t\t\t<tbody>\r\n\t\t\t\t<tr>\r\n\t\t\t\t\t<td id=\"pop_content\" class=\"pop_content\">\r\n\t\t\t\t\t\t<h2 class=\"dialog_title\"><span></span></h2>\r\n\t\t\t\t\t\t<div class=\"dialog_content\">\r\n\t\t\t\t\t\t\t<p id=\"modal-p\"></p>\r\n\t\t\t\t\t\t\t<div class=\"dialog_buttons\">\r\n\t\t\t\t\t\t\t\t<input type=\"button\" value=\"Zamknij\" name=\"close\" class=\"inputsubmit\" id=\"fb-close\">\r\n\t\t\t\t\t\t\t</div>\r\n\t\t\t\t\t\t</div>\r\n\t\t\t\t\t</td>\r\n\t\t\t\t</tr>\r\n\t\t\t</tbody>\r\n\t\t</table>\r\n\t</div>\r\n</div>    <div id=\"login-modal\">\r\n    <div style=\"position: relative\">\r\n\t<a id=\"yt-close\" href=\"javascript:modalFadeOut();\" title=\"Zamknij\"></a>\r\n    </div>\r\n    <div class=\"login_box\">\r\n\t<div class=\"row justify-content-md-center\">\r\n\t    <div class=\"col col-10\">\r\n\t\t<img src=\"data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%20441.8%20136%22%3E%3Cstyle%3E.st0%7Bfill%3A%2362ae25%7D.st1%7Bfill%3A%23999%7D%3C%2Fstyle%3E%3Cg%20id%3D%22logo_1_%22%3E%3Cpath%20class%3D%22st0%22%20d%3D%22M130.7%2046v9h6.5v6.2h-6.5v20.6c0%201.4.3%202.4.8%203.1.6.6%201.6.9%203%20.9%201%200%201.9-.1%202.9-.3v6.4c-1.9.5-3.7.8-5.5.8-6.4%200-9.6-3.5-9.6-10.6v-21h-6.1V55h6.1v-9h8.4zM156.3%2092.7c-5.3%200-9.5-1.7-12.8-5s-4.9-7.7-4.9-13.2v-1c0-3.7.7-7%202.1-9.9%201.4-2.9%203.4-5.2%206-6.8s5.4-2.4%208.6-2.4c5%200%208.9%201.6%2011.7%204.8%202.7%203.2%204.1%207.8%204.1%2013.6v3.3H147c.3%203.1%201.3%205.5%203.1%207.2%201.8%201.8%204%202.7%206.8%202.7%203.8%200%206.9-1.5%209.3-4.6l4.5%204.3c-1.5%202.2-3.5%203.9-5.9%205.1-2.6%201.3-5.4%201.9-8.5%201.9zm-1-31.7c-2.3%200-4.1.8-5.5%202.4-1.4%201.6-2.3%203.8-2.7%206.7h15.8v-.6c-.2-2.8-.9-4.9-2.2-6.3-1.3-1.5-3.1-2.2-5.4-2.2zM186.1%2076.1l-3.7%203.8V92h-8.3V39.5h8.3v30.3l2.6-3.2L195.2%2055h10l-13.7%2015.4L206.7%2092h-9.6l-11-15.9z%22%2F%3E%3Cpath%20class%3D%22st0%22%20d%3D%22M227.9%2082c0-1.5-.6-2.6-1.8-3.4s-3.2-1.5-6.1-2.1c-2.8-.6-5.2-1.3-7.1-2.3-4.1-2-6.2-4.9-6.2-8.7%200-3.2%201.3-5.9%204-8s6.1-3.2%2010.3-3.2c4.4%200%208%201.1%2010.7%203.3s4.1%205%204.1%208.5h-8.3c0-1.6-.6-2.9-1.8-4s-2.8-1.6-4.7-1.6c-1.8%200-3.3.4-4.5%201.3-1.2.8-1.7%202-1.7%203.4%200%201.3.5%202.3%201.6%203s3.2%201.4%206.5%202.1%205.8%201.6%207.7%202.6%203.2%202.2%204.1%203.6c.9%201.4%201.3%203.1%201.3%205.1%200%203.3-1.4%206-4.1%208.1-2.8%202.1-6.4%203.1-10.8%203.1-3%200-5.7-.5-8.1-1.6-2.4-1.1-4.2-2.6-5.5-4.5s-2-4-2-6.2h8.1c.1%202%20.9%203.5%202.2%204.5%201.4%201.1%203.2%201.6%205.4%201.6s3.9-.4%205-1.2c1.1-1%201.7-2.1%201.7-3.4zM250.2%2046v9h6.5v6.2h-6.5v20.6c0%201.4.3%202.4.8%203.1.6.6%201.6.9%203%20.9%201%200%201.9-.1%202.9-.3v6.4c-1.9.5-3.7.8-5.5.8-6.4%200-9.6-3.5-9.6-10.6v-21h-6.1V55h6.1v-9h8.4zM257%2073.2c0-3.6.7-6.9%202.2-9.8s3.5-5.1%206-6.7c2.6-1.6%205.6-2.3%209-2.3%205%200%209%201.6%2012.2%204.8%203.1%203.2%204.8%207.5%205%2012.8v1.9c0%203.6-.7%206.9-2.1%209.8s-3.4%205.1-6%206.7-5.6%202.4-9%202.4c-5.2%200-9.4-1.7-12.5-5.2s-4.7-8.1-4.7-13.9v-.5h-.1zm8.3.7c0%203.8.8%206.8%202.4%208.9%201.6%202.2%203.8%203.2%206.6%203.2s5-1.1%206.5-3.3c1.6-2.2%202.3-5.4%202.3-9.6%200-3.7-.8-6.7-2.4-8.9-1.6-2.2-3.8-3.3-6.5-3.3s-4.9%201.1-6.5%203.2c-1.6%202.3-2.4%205.5-2.4%209.8zM327.4%2080.5l5.9-25.5h8.1l-10.1%2037h-6.8l-7.9-25.4-7.9%2025.4h-6.8l-10.1-37h8.1l6%2025.3%207.6-25.3h6.3l7.6%2025.5zM341.8%2073.2c0-3.6.7-6.9%202.2-9.8s3.5-5.1%206-6.7c2.6-1.6%205.6-2.3%209-2.3%205%200%209%201.6%2012.2%204.8%203.1%203.2%204.8%207.5%205%2012.8v1.9c0%203.6-.7%206.9-2.1%209.8s-3.4%205.1-6%206.7-5.6%202.4-9%202.4c-5.2%200-9.4-1.7-12.5-5.2s-4.7-8.1-4.7-13.9v-.5h-.1zm8.3.7c0%203.8.8%206.8%202.4%208.9%201.6%202.2%203.8%203.2%206.6%203.2s5-1.1%206.5-3.3c1.6-2.2%202.3-5.4%202.3-9.6%200-3.7-.8-6.7-2.4-8.9-1.6-2.2-3.8-3.3-6.5-3.3s-4.9%201.1-6.5%203.2c-1.6%202.3-2.4%205.5-2.4%209.8zM384.9%2083.3c1.5%200%202.7.4%203.6%201.3.8.9%201.3%202%201.3%203.3s-.4%202.4-1.3%203.2c-.8.9-2%201.3-3.6%201.3-1.5%200-2.7-.4-3.5-1.3-.9-.8-1.3-1.9-1.3-3.2%200-1.3.4-2.4%201.3-3.3.8-.9%202-1.3%203.5-1.3zM428.2%2073.9c0%205.7-1.3%2010.3-3.9%2013.7s-6.1%205.1-10.5%205.1c-4.1%200-7.3-1.3-9.7-4v17.5h-8.3V55h7.7l.3%203.8c2.4-3%205.8-4.4%209.9-4.4%204.5%200%208%201.7%2010.6%205%202.6%203.4%203.8%208%203.8%2014v.5h.1zm-8.3-.7c0-3.7-.7-6.6-2.2-8.8-1.5-2.2-3.6-3.2-6.3-3.2-3.4%200-5.8%201.4-7.3%204.2v16.4c1.5%202.9%204%204.3%207.4%204.3%202.6%200%204.7-1.1%206.2-3.2%201.5-2.2%202.2-5.4%202.2-9.7zM440.5%2092h-8.3V39.5h8.3V92z%22%2F%3E%3Cpath%20class%3D%22st1%22%20d%3D%22M123.5%20107.6l-.5%202.8h2.1l-.3%201.5h-2.1l-1.2%207.2v.5c0%20.6.4.8.9.8.2%200%20.6%200%20.9-.1l-.2%201.6c-.5.1-.9.2-1.4.2-.8%200-1.4-.3-1.7-.9-.4-.6-.5-1.3-.5-2.2l1.2-7.2h-2.1l.3-1.5h2.1l.5-2.8%202%20.1zM129.3%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2.7.8%201.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.6.8-2.6.8zm1.3-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1.1-.8-1.8-.8zM141.8%20112.1l-.9-.1c-1.2%200-2.2.6-2.9%201.7l-1.4%208.3h-1.9l2-11.6h1.9l-.3%201.4c.8-1.1%201.8-1.6%202.9-1.6.2%200%20.5.1.9.2l-.3%201.7zM147.5%20122c-.1-.2-.1-.4-.1-.6v-.6c-1.1%201-2.2%201.5-3.4%201.4-1%200-1.9-.3-2.5-1-.6-.6-.9-1.4-.9-2.4.1-1.2.6-2.2%201.6-2.9%201-.7%202.3-1%204-1h2l.1-.9c.1-.7-.1-1.2-.4-1.6-.3-.4-.9-.6-1.5-.6-.7%200-1.3.1-1.8.5-.5.3-.8.8-.9%201.3h-2c.1-.7.3-1.3.8-1.8s1.1-.9%201.8-1.2c.7-.3%201.5-.4%202.3-.4%201.2%200%202.2.4%202.8%201.1.7.7%201%201.6.8%202.8l-.9%205.8-.1.8c0%20.4%200%20.8.1%201.2v.2h-1.8v-.1zm-3-1.5c.6%200%201.2-.1%201.8-.4s1-.7%201.4-1.3l.4-2.4h-1.5c-1.2%200-2.2.2-2.8.6-.7.4-1%20.9-1.1%201.6-.1.5.1%201%20.4%201.4.3.3.8.5%201.4.5zM153.2%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM168.8%20110.4l-.2%201.3c1-1%202.2-1.5%203.5-1.5.7%200%201.4.2%201.9.5s.9.8%201.1%201.4c1.1-1.3%202.4-1.9%203.9-1.9%201.2%200%202%20.4%202.6%201.2.6.8.8%201.8.6%203l-1.3%207.6H179l1.3-7.7v-1c-.1-1-.8-1.5-1.9-1.5-.7%200-1.4.2-1.9.7-.6.5-.9%201.1-1.1%201.8l-1.3%207.7h-2l1.3-7.6c.1-.8%200-1.4-.4-1.8s-.9-.7-1.6-.7c-1.2%200-2.2.5-2.9%201.7l-1.5%208.5h-1.9l2-11.6%201.7-.1zM189.1%20110.2c1%200%201.8.3%202.5.8s1.2%201.2%201.5%202.1.4%201.9.3%203v.2c-.1%201.1-.5%202.2-1%203.1s-1.2%201.6-2.1%202.1c-.9.5-1.8.7-2.8.7s-1.8-.3-2.5-.8-1.2-1.2-1.5-2.1-.4-1.9-.3-2.9c.1-1.2.4-2.3%201-3.2.5-1%201.2-1.7%202.1-2.2.8-.6%201.8-.9%202.8-.8zm-4%206.2c-.1.5-.1.9%200%201.4.1.8.3%201.5.8%202%20.4.5%201%20.8%201.7.8.6%200%201.2-.1%201.8-.5.5-.3%201-.9%201.4-1.5.4-.7.6-1.5.7-2.3.1-.7.1-1.2%200-1.7-.1-.9-.3-1.6-.8-2.1-.4-.5-1-.8-1.7-.8-1%200-1.9.4-2.6%201.2-.7.8-1.1%201.9-1.3%203.2v.3zM195.8%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zm3.2-13c0-.3.1-.6.3-.8.2-.2.5-.3.8-.4.3%200%20.6.1.8.3.2.2.3.5.3.8s-.1.6-.3.8c-.2.2-.5.3-.8.3s-.6-.1-.8-.3c-.2-.1-.3-.4-.3-.7zM208.1%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2.7.8%201.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1-.8-1.8-.8zM220.6%20118.9c.1-.8-.3-1.4-1.3-1.7l-2-.6c-1.7-.6-2.5-1.6-2.5-2.9%200-1%20.5-1.9%201.4-2.5.9-.7%201.9-1%203.1-1%201.2%200%202.1.4%202.9%201%20.7.7%201.1%201.5%201.1%202.6h-1.9c0-.6-.2-1.1-.5-1.4s-.9-.6-1.5-.6c-.7%200-1.3.2-1.7.5-.5.3-.7.7-.8%201.3-.1.7.3%201.2%201.2%201.5l1%20.3c1.3.3%202.3.8%202.8%201.3s.8%201.2.8%202.1c0%20.7-.3%201.4-.7%201.9s-1%20.9-1.7%201.2c-.7.3-1.5.4-2.3.4-1.2%200-2.2-.4-3.1-1.1-.8-.7-1.2-1.6-1.2-2.7h1.9c0%20.7.2%201.2.6%201.6.4.4%201%20.6%201.7.6s1.3-.1%201.8-.4c.5-.5.8-.9.9-1.4zM225.5%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM241.3%20110.4l-2.2%2013c-.1%201.1-.5%201.9-1.1%202.5s-1.4.9-2.3.8c-.4%200-.8-.1-1.3-.2l.2-1.6c.3.1.6.1.9.1.9%200%201.5-.6%201.7-1.7l2.2-13%201.9.1zm-1.6-3.1c0-.3.1-.6.3-.8.2-.2.5-.3.8-.4.3%200%20.6.1.8.3.2.2.3.5.3.8s-.1.6-.3.8-.5.3-.8.3-.6-.1-.8-.3-.3-.4-.3-.7zM246.4%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2s1.1%201.8%201.1%203.1v1.4l-.1.9H244c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.5-.5-1-.8-1.8-.8zM266.5%20116.2c-.1%201.2-.5%202.2-1%203.2s-1.1%201.7-1.8%202.1c-.7.5-1.5.7-2.4.7-1.3%200-2.4-.5-3.1-1.4l-1%205.6h-1.9l2.8-16.1h1.8l-.2%201.3c1-1%202.1-1.5%203.4-1.5%201.1%200%202%20.4%202.6%201.2s1%201.9%201%203.3c0%20.5%200%20.9-.1%201.3l-.1.3zm-1.9-.2l.1-.9c0-1-.2-1.8-.6-2.4s-1-.8-1.7-.9c-1.1%200-2.1.5-2.9%201.6l-1%205.6c.4%201%201.2%201.6%202.4%201.6%201%200%201.8-.4%202.5-1.1.5-.8.9-2%201.2-3.5zM274.2%20112.1l-.9-.1c-1.2%200-2.2.6-2.9%201.7L269%20122h-1.9l2-11.6h1.9l-.3%201.4c.8-1.1%201.8-1.6%202.9-1.6.2%200%20.5.1.9.2l-.3%201.7zM275.3%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM287.6%20122.2c-1.4%200-2.6-.6-3.4-1.6s-1.2-2.4-1-4l.1-.5c.1-1.2.5-2.2%201-3.2.6-1%201.2-1.7%202.1-2.2.8-.5%201.7-.7%202.6-.7%201.2%200%202.1.4%202.8%201.2s1.1%201.8%201.1%203.1v1.4l-.1.9h-7.6c-.1%201.1%200%201.9.5%202.7.5.7%201.2%201.1%202.1%201.1%201.1%200%202.2-.5%203.2-1.5l1.1.9c-.5.7-1.1%201.3-1.9%201.6-.8.6-1.7.8-2.6.8zm1.2-10.4c-.8%200-1.5.3-2.1.8-.6.6-1.1%201.4-1.4%202.5h5.7v-.2c.1-.9%200-1.7-.4-2.3-.4-.5-1-.8-1.8-.8zM297.8%20120.6c.7%200%201.3-.2%201.9-.6s.9-1%201.1-1.6h1.8c-.1.7-.4%201.4-.9%202s-1.1%201.1-1.8%201.4-1.5.5-2.2.5c-1%200-1.8-.3-2.5-.8s-1.2-1.2-1.5-2.1-.4-1.9-.3-3l.1-.5c.1-1.1.5-2.1%201-3s1.2-1.6%202-2.1%201.7-.7%202.8-.7c1.2%200%202.2.4%202.9%201.2s1.1%201.8%201.1%203h-1.8c0-.8-.2-1.4-.6-1.8-.4-.5-1-.7-1.7-.7-1%200-1.8.4-2.5%201.1-.7.8-1.1%201.9-1.3%203.2v1.7c0%20.8.3%201.5.7%202s1%20.8%201.7.8zM305.2%20120.4h6.5l-.3%201.6h-9l.3-1.6%207.6-8.4h-6.1l.3-1.6h8.6l-.3%201.5-7.6%208.5zM316.8%20119.1l4.1-8.7h2.1l-6.9%2013.6c-1%201.9-2.2%202.8-3.7%202.8-.3%200-.7-.1-1.2-.2l.2-1.6.5.1c.6%200%201.1-.1%201.6-.4.4-.3.8-.8%201.2-1.5l.7-1.3-2-11.4h2l1.4%208.6z%22%2F%3E%3Cpath%20class%3D%22st1%22%20d%3D%22M326.9%20107.6l-.5%202.8h2.1l-.3%201.5h-2.1l-1.2%207.2v.5c0%20.6.4.8.9.8.2%200%20.6%200%20.9-.1l-.2%201.6c-.5.1-.9.2-1.4.2-.8%200-1.4-.3-1.7-.9-.4-.6-.5-1.3-.5-2.2l1.2-7.2H322l.3-1.5h2.1l.5-2.8%202%20.1zM334.8%20122c0-.2-.1-.4-.1-.6v-.6c-1.1%201-2.2%201.5-3.4%201.4-1%200-1.9-.3-2.5-1-.6-.6-.9-1.4-.9-2.4.1-1.2.6-2.2%201.6-2.9%201-.7%202.3-1%204-1h2l.1-.9c.1-.7-.1-1.2-.4-1.6s-.9-.6-1.5-.6c-.7%200-1.3.1-1.8.5-.5.3-.8.8-.9%201.3h-2c.1-.7.3-1.3.8-1.8s1.1-.9%201.8-1.2c.7-.3%201.5-.4%202.3-.4%201.2%200%202.2.4%202.8%201.1.7.7%201%201.6.8%202.8l-.9%205.8-.1.8c0%20.4%200%20.8.1%201.2v.2h-1.8v-.1zm-3-1.5c.6%200%201.2-.1%201.8-.4.6-.3%201-.7%201.4-1.3l.4-2.4h-1.5c-1.2%200-2.2.2-2.8.6s-1%20.9-1.1%201.6c-.1.5.1%201%20.4%201.4.3.3.8.5%201.4.5zM343.3%20120.6c.7%200%201.3-.2%201.9-.6s.9-1%201.1-1.6h1.8c-.1.7-.4%201.4-.9%202s-1.1%201.1-1.8%201.4-1.5.5-2.2.5c-1%200-1.8-.3-2.5-.8s-1.2-1.2-1.5-2.1-.4-1.9-.3-3l.1-.5c.1-1.1.5-2.1%201-3s1.2-1.6%202-2.1%201.7-.7%202.8-.7c1.2%200%202.2.4%202.9%201.2s1.1%201.8%201.1%203H347c0-.8-.2-1.4-.6-1.8-.4-.5-1-.7-1.7-.7-1%200-1.8.4-2.5%201.1-.7.8-1.1%201.9-1.3%203.2v1.7c0%20.8.3%201.5.7%202s.9.8%201.7.8zm2.7-15.1h2.5l-3.3%203.2h-1.7l2.5-3.2z%22%2F%3E%3Cpath%20id%3D%22box%22%20d%3D%22M92.9%20124H7.1c-3.9%200-7.1-3.2-7.1-7.1V31.1C0%2027.2%203.2%2024%207.1%2024h85.8c3.9%200%207.1%203.2%207.1%207.1v85.8c0%203.9-3.2%207.1-7.1%207.1z%22%20fill%3D%22%23f60%22%2F%3E%3Cpath%20d%3D%22M68%2044.1c-6.8-7-15.9-12.2-24.6-14.4C28.8%2026%2025%2023.9%2017.8%2019c-4.1-2.8-7.2-8.4-10.6-9.8-1.6-.6-2.6.1-3.1.7-.9%201-1.6%202.9-.4%205.2%2010.1%2019.8%2048.4%2076.8%2048.4%2076.8-5.9%201.1-12.4%205.7-17%2012.9-6.9%2010.7-7%2022.9-.1%2027.2%206.9%204.3%2018.1-.9%2025-11.6%205.9-9%206.8-19.1%202.8-24.6L29.2%2042.7s-3.9-4.7%202.3-6.1c4.5-1%2013.9.1%2019%202.8C56%2042.3%2069%2051.7%2072.3%2065.1c1.5%206.2.6%209.6%201.4%2015.1.4%202.6%203.2%204.8%205.5%201.2%201.2-1.9%201.4-8.4%201.1-13.5-.5-6.7-6-17.3-12.3-23.8zm-10.4%2048c.3.1.7.2%201%20.3-.3-.1-.6-.2-1-.3zm-2.5-.4h.4-.4zm1.1.1c.3%200%20.6.1.8.1-.2%200-.5-.1-.8-.1zm-3%200c.1%200%20.1%200%200%200%20.1%200%20.1%200%200%200zm5.8.8l1.2.6c-.4-.3-.8-.5-1.2-.6zm2.9%202zm-.7-.7c-.3-.3-.6-.5-1-.7.3.2.6.5%201%20.7z%22%20opacity%3D%22.2%22%2F%3E%3ClinearGradient%20id%3D%22SVGID_1_%22%20gradientUnits%3D%22userSpaceOnUse%22%20x1%3D%2239.888%22%20y1%3D%2292.284%22%20x2%3D%2252.096%22%20y2%3D%22125.824%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%238fe132%22%2F%3E%3Cstop%20offset%3D%22.362%22%20stop-color%3D%22%2365bd23%22%2F%3E%3Cstop%20offset%3D%22.668%22%20stop-color%3D%22%2349a519%22%2F%3E%3Cstop%20offset%3D%22.844%22%20stop-color%3D%22%233e9c15%22%2F%3E%3C%2FlinearGradient%3E%3Cpath%20d%3D%22M57.9%20117.8c-7.6%2010.4-19.2%2014.9-25.7%2010.1s-5.7-17.2%201.9-27.6%2019.2-14.9%2025.7-10.1c6.6%204.9%205.7%2017.2-1.9%2027.6z%22%20fill%3D%22url%28%23SVGID_1_%29%22%2F%3E%3ClinearGradient%20id%3D%22SVGID_2_%22%20gradientUnits%3D%22userSpaceOnUse%22%20x1%3D%22-3.574%22%20y1%3D%2224.316%22%20x2%3D%2282.773%22%20y2%3D%2274.169%22%3E%3Cstop%20offset%3D%22.184%22%20stop-color%3D%22%238fe132%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%233e9c15%22%2F%3E%3C%2FlinearGradient%3E%3Cpath%20d%3D%22M70.9%2041c-6.3-7.6-15-13.4-23.6-16.2-14.4-4.7-18-7.1-24.8-12.5-3.9-3.1-6.6-9-9.9-10.7-1.6-.8-2.6-.1-3.2.5-.9.9-1.8%202.8-.7%205.2C17.4%2028%2051.9%2088.4%2051.9%2088.4c3-.3%205.7.2%207.9%201.8%201%20.8%201.8%201.7%202.5%202.8l-30-56s-3.6-5%202.7-6c4.5-.7%2013.9%201.1%2018.8%204.1%205.3%203.3%2017.6%2013.7%2020%2027.5%201.1%206.4%200%209.7.4%2015.4.2%202.6%202.9%205.1%205.4%201.6%201.3-1.8%202-8.4%202-13.6%200-6.8-4.9-17.9-10.7-25z%22%20fill%3D%22url%28%23SVGID_2_%29%22%2F%3E%3CradialGradient%20id%3D%22SVGID_3_%22%20cx%3D%2221.505%22%20cy%3D%22103.861%22%20r%3D%2214.934%22%20gradientTransform%3D%22matrix%28.2966%20.4025%20-.805%20.5933%20123.22%2029.092%29%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%23f4ff72%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%2373c928%22%20stop-opacity%3D%220%22%2F%3E%3C%2FradialGradient%3E%3Cpath%20d%3D%22M50.4%20105.4c-6.6%204.9-14%206.2-16.5%202.8-2.4-3.3%201-10%207.6-14.9s14-6.2%2016.5-2.8c2.5%203.3-.9%2010-7.6%2014.9z%22%20fill%3D%22url%28%23SVGID_3_%29%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E\" class=\"img-fluid\" alt=\"tekstowo.pl\" title=\"Teksty piosenek, tłumaczenia, teledyski\">\t    </div>\r\n\t</div>\r\n\t<div class=\"login-wrap\">\r\n\t    <fieldset class=\"login rejestracja edycja okienko\">\r\n\r\n\t\t<div class=\"row\">\r\n\t\t    <div class=\"col text-center\">\r\n\t\t\t<p><strong>Aby wykonać tę operację należy się zalogować:</strong></p>\r\n\t\t    </div>\r\n\t\t</div>\r\n\r\n\t\t<div class=\"l-box\">\r\n\t\t    <div class=\"formRow\">\r\n\t\t\tZaloguj się przy użyciu loginu i hasła:\r\n\t\t\t<br>\r\n\t\t\t<a class=\"green-button btn btn-primary btn-lg btn-block mt-1\" href=\"https://www.tekstowo.pl/logowanie.html\">Zaloguj</a>\r\n\t\t\t<div class=\"row my-2 mx-0\">\r\n\t\t\t    <div class=\"col-sm\">\r\n\t\t\t\t<a href=\"/rejestracja.html\" class=\"green underline bold\" title=\"Rejestracja\">Rejestracja</a>\r\n\t\t\t    </div>\r\n\t\t\t    <div class=\"col-auto\">\r\n\t\t\t\t<a href=\"/przypomnij.html\" class=\"green underline bold marginl10\" title=\"Przypomnienie hasła\">Przypomnienie hasła</a>\r\n\t\t\t    </div>\r\n\t\t\t</div>\r\n\t\t    </div>\r\n\r\n\t\t    <div class=\"fb-box mt-3\">Inne metody logowania:\r\n\t\t\t<a href=\"javascript:;\" class=\"fb-button my-fb-login-button btn btn-block btn-fb btn-lg mt-1\" title=\"Zaloguj się z Facebookiem\">\r\n\t\t\t    <span class=\"icon\"></span>Zaloguj się przez Facebooka</a>\r\n\t\t    </div>\r\n\t    </div></fieldset>\r\n\t</div>\r\n    </div>\r\n</div>\r\n\r\n    <div id=\"sendsong-modal\" style=\"display: none;\">\r\n    <div>\r\n\t<a id=\"yt-close\" href=\"javascript:modalFadeOut();\" title=\"Zamknij\"></a>\r\n    </div>\r\n    <div class=\"login_box\">\r\n\r\n\t<form id=\"formSendSongInvite\" action=\"\" onsubmit=\"ajxSendSongInvite();\r\n\t\treturn false;\" method=\"post\">\r\n\r\n\t    <div class=\"row\">\r\n\t\t<div class=\"col text-center lead my-3\">\r\n\t\t    Podaj adres E-mail znajomego, któremu chcesz polecić ten utwór.\r\n\t\t</div>\r\n\t    </div>\r\n\r\n\t    \t    \r\n\t    <div class=\"form-group row\">\r\n\t\t<label for=\"invite_email\" class=\"col-sm-2 col-form-label\">E-mail:</label>\r\n\t\t<div class=\"col-sm-10\">\r\n\t\t    <input type=\"text\" class=\"form-control\" name=\"invite_email\" id=\"invite_email\" value=\"\">\r\n\t\t    <div class=\"invalid-feedback\" id=\"error_invemail\" style=\"display: none\">\r\n\t\t\tPodany E-mail jest nieprawidłowy.\r\n\t\t    </div>\r\n\t\t</div>\r\n\t    </div>\r\n\r\n\t    <div class=\"form-group row\">\r\n\t\t<div class=\"col d-flex justify-content-center mt-3\">\r\n\t\t    <input type=\"hidden\" name=\"song_id\" value=\"2299782\">\r\n\t\t    <button type=\"submit\" class=\"btn btn-primary mr-3 px-5\">Wyślij</button>\r\n\t\t    <button type=\"button\" onclick=\"modalFadeOut();\" class=\"btn btn-secondary px-5\">Anuluj</button>\r\n\t\t</div>\r\n\t    </div>\r\n\t</form>\r\n    </div>\r\n</div>\r\n<div class=\"container order-3\">\r\n    <div id=\"bottom\" class=\"row\">\r\n        <div id=\"stopka\" class=\"col\">\r\n\r\n            \t    <div class=\"row footbar py-3 my-3 d-md-none\">\r\n\t\t<div class=\"col text-center\">\r\n\t\t    2 170 744 tekstów, 20 217 poszukiwanych i 502 oczekujących\r\n\t\t</div>\r\n\t    </div>\r\n            <hr>\r\n            <p>Największy serwis z tekstami piosenek w Polsce. Każdy może znaleźć u nas teksty piosenek, teledyski oraz tłumaczenia swoich ulubionych utworów.<br> Zachęcamy wszystkich użytkowników do dodawania nowych tekstów, tłumaczeń i teledysków! </p>\r\n            <hr>\r\n            <a href=\"/reklama.html\" class=\"bottom-links\" title=\"Reklama\">Reklama</a> |\r\n            <a href=\"/kontakt.html\" class=\"bottom-links\" title=\"Kontakt\">Kontakt</a> |\r\n            <a href=\"/faq.html\" class=\"bottom-links\" title=\"FAQ\">FAQ</a>\r\n            <a href=\"/polityka-prywatnosci.html\" class=\"bottom-links\" title=\"Polityka prywatności\">Polityka prywatności</a>\r\n        </div>\r\n    </div>\r\n</div><!-- end container -->\r\n\r\n<div style=\"color: white; text-align: center; background-color: white; width: 100%; margin: 0 auto;\"><span id=\"debug\"></span></div>\r\n\r\n<div id=\"spinner\" style=\"display:none;\">\r\n    <div id=\"spinner\" class=\"spinner-grow text-primary\" role=\"status\">\r\n\t<span class=\"sr-only\">Proszę czekać...</span>\r\n    </div>\r\n</div>\r\n\r\n<div class=\" d-none d-md-block fb-panel \">\r\n    <a href=\"https://www.facebook.com/tekstowo/\" target=\"_blank\" class=\"slide_button\"></a>\r\n    <div class=\"fb\"></div>\r\n</div>\r\n\r\n    \t    \r\n\r\n    \r\n\r\n<script type=\"text/javascript\">\r\n\r\n  var _gaq = _gaq || [];\r\n  _gaq.push(['_setAccount', 'UA-261303-4']);\r\n  _gaq.push(['_trackPageview']);\r\n  _gaq.push(['_trackPageLoadTime']); \r\n  \r\n  (function() {\r\n    var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;\r\n    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';\r\n    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);\r\n  })();\r\n\r\n</script>\r\n\r\n<script src=\"//s1.adform.net/banners/scripts/adx.js\" async=\"\" defer=\"\"></script>\r\n\r\n<script async=\"\" src=\"//www.statsforads.com/tag/d5e49d0e-64d6-4751-ae6c-eb53cd6568f6.min.js\"></script>\r\n\r\n\r\n  <script type=\"text/javascript\" src=\"https://lib.ads4g.pl/publisher/maxart/91c4f3e3d35dc73f574b.js\" async=\"\"></script>\r\n\r\n  \r\n\r\n<div id=\"fb-root\" class=\" fb_reset\"><div style=\"position: absolute; top: -10000px; width: 0px; height: 0px;\"><div></div></div></div>\r\n\r\n\r\n<!-- bootstrap -->\r\n<script defer=\"\" src=\"https://code.jquery.com/jquery-3.5.1.min.js\" integrity=\"sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=\" crossorigin=\"anonymous\"></script>\r\n<script defer=\"\" src=\"https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.bundle.min.js\" integrity=\"sha384-1CmrxMRARb6aLqgBO7yyAxTOQE2AKb9GfXnEo760AUcUmFx3ibVJJAzGytlQcNXd\" crossorigin=\"anonymous\"></script>\r\n<script defer=\"\" type=\"text/javascript\" src=\"/static/v2/js/bs4/bootstrap-autocomplete.min.js\"></script>\r\n<!-- end of bootstrap -->\r\n\r\n<script defer=\"\" type=\"text/javascript\" src=\"/static/js/mootools-core-1.6.0.min.js\"></script>\r\n<script defer=\"\" type=\"text/javascript\" src=\"/static/js/mootools-more-1.6.0.min.js\"></script>\r\n<script defer=\"\" type=\"text/javascript\" src=\"/static/js/pulse.min.js\"></script>\r\n<script defer=\"\" type=\"text/javascript\" src=\"/static/v2/js/app.js?v=220105\"></script>\r\n<script defer=\"\" type=\"text/javascript\" src=\"/static/js/main-4.js?v=211027\"></script>\r\n<!--polyfills-->\r\n<script async=\"\" type=\"text/javascript\" src=\"/static/v2/js/loading-attribute-polyfill.min.js\"></script>\r\n\r\n    <script async=\"\" defer=\"\" src=\"https://connect.facebook.net/pl_PL/sdk.js\"></script>\r\n<script>\r\n    \r\n\twindow.addEventListener('DOMContentLoaded', () => {\r\n\t    if ($defined(window.asyncEventHoler))\r\n\t    {\r\n\t\tfor (var i = 0; i < window.asyncEventHoler.length; i++) {\r\n\t\t    var o = window.asyncEventHoler[i];\r\n\t\t    window.addEvent(o.event, o.fn);\r\n\t\t}\r\n\t\tdelete window.asyncEventHoler;\r\n\t    }\r\n\t});\r\n    \r\n</script>\r\n\r\n    \t        \r\n\r\n</body></html>"
  },
  {
    "path": "test/rsrc/mbpseudo/official_release.json",
    "content": "{\n  \"aliases\": [\n    {\n      \"begin\": null,\n      \"end\": null,\n      \"ended\": false,\n      \"locale\": \"en\",\n      \"name\": \"In Bloom\",\n      \"primary\": true,\n      \"sort-name\": \"In Bloom\",\n      \"type\": \"Release name\",\n      \"type-id\": \"df187855-059b-3514-9d5e-d240de0b4228\"\n    }\n  ],\n  \"artist-credit\": [\n    {\n      \"artist\": {\n        \"aliases\": [\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": \"en\",\n            \"name\": \"Lilas Ikuta\",\n            \"primary\": true,\n            \"sort-name\": \"Ikuta, Lilas\",\n            \"type\": \"Artist name\",\n            \"type-id\": \"894afba6-2816-3c24-8072-eadb66bd04bc\"\n          }\n        ],\n        \"country\": \"JP\",\n        \"disambiguation\": \"\",\n        \"genres\": [\n          {\n            \"count\": 1,\n            \"disambiguation\": \"\",\n            \"id\": \"eba7715e-ee26-4989-8d49-9db382955419\",\n            \"name\": \"j-pop\"\n          },\n          {\n            \"count\": 1,\n            \"disambiguation\": \"\",\n            \"id\": \"455f264b-db00-4716-991d-fbd32dc24523\",\n            \"name\": \"singer-songwriter\"\n          }\n        ],\n        \"id\": \"55e42264-ef27-49d8-93fd-29f930dc96e4\",\n        \"name\": \"幾田りら\",\n        \"sort-name\": \"Ikuta, Lilas\",\n        \"tags\": [\n          {\n            \"count\": 1,\n            \"name\": \"j-pop\"\n          },\n          {\n            \"count\": 1,\n            \"name\": \"singer-songwriter\"\n          }\n        ],\n        \"type\": \"Person\",\n        \"type-id\": \"b6e035f4-3ce9-331c-97df-83397230b0df\"\n      },\n      \"joinphrase\": \"\",\n      \"name\": \"幾田りら\"\n    }\n  ],\n  \"artist-relations\": [\n    {\n      \"artist\": {\n        \"country\": \"JP\",\n        \"disambiguation\": \"\",\n        \"id\": \"55e42264-ef27-49d8-93fd-29f930dc96e4\",\n        \"name\": \"幾田りら\",\n        \"sort-name\": \"Ikuta, Lilas\",\n        \"type\": \"Person\",\n        \"type-id\": \"b6e035f4-3ce9-331c-97df-83397230b0df\"\n      },\n      \"attribute-ids\": {},\n      \"attribute-values\": {},\n      \"attributes\": [],\n      \"begin\": \"2025\",\n      \"direction\": \"backward\",\n      \"end\": \"2025\",\n      \"ended\": true,\n      \"source-credit\": \"\",\n      \"target-credit\": \"Lilas Ikuta\",\n      \"type\": \"copyright\",\n      \"type-id\": \"730b5251-7432-4896-8fc6-e1cba943bfe1\"\n    },\n    {\n      \"artist\": {\n        \"country\": \"JP\",\n        \"disambiguation\": \"\",\n        \"id\": \"55e42264-ef27-49d8-93fd-29f930dc96e4\",\n        \"name\": \"幾田りら\",\n        \"sort-name\": \"Ikuta, Lilas\",\n        \"type\": \"Person\",\n        \"type-id\": \"b6e035f4-3ce9-331c-97df-83397230b0df\"\n      },\n      \"attribute-ids\": {},\n      \"attribute-values\": {},\n      \"attributes\": [],\n      \"begin\": \"2025\",\n      \"direction\": \"backward\",\n      \"end\": \"2025\",\n      \"ended\": true,\n      \"source-credit\": \"\",\n      \"target-credit\": \"Lilas Ikuta\",\n      \"type\": \"phonographic copyright\",\n      \"type-id\": \"01d3488d-8d2a-4cff-9226-5250404db4dc\"\n    }\n  ],\n  \"asin\": \"B0DR8Y2YDC\",\n  \"barcode\": \"199066336168\",\n  \"country\": \"XW\",\n  \"cover-art-archive\": {\n    \"artwork\": true,\n    \"back\": false,\n    \"count\": 1,\n    \"darkened\": false,\n    \"front\": true\n  },\n  \"date\": \"2025-01-10\",\n  \"disambiguation\": \"\",\n  \"genres\": [],\n  \"id\": \"a5ce1d11-2e32-45a4-b37f-c1589d46b103\",\n  \"label-info\": [\n    {\n      \"catalog-number\": \"Lilas-020\",\n      \"label\": {\n        \"aliases\": [\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"2636621 Records DK\",\n            \"primary\": null,\n            \"sort-name\": \"2636621 Records DK\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Antipole\",\n            \"primary\": null,\n            \"sort-name\": \"Antipole\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Auto production\",\n            \"primary\": null,\n            \"sort-name\": \"Auto production\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Auto-Edición\",\n            \"primary\": null,\n            \"sort-name\": \"Auto-Edición\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Auto-Product\",\n            \"primary\": null,\n            \"sort-name\": \"Auto-Product\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Autoedición\",\n            \"primary\": null,\n            \"sort-name\": \"Autoedición\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Autoeditado\",\n            \"primary\": null,\n            \"sort-name\": \"Autoeditado\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Autoproduit\",\n            \"primary\": null,\n            \"sort-name\": \"Autoproduit\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Banana Skin Records\",\n            \"primary\": null,\n            \"sort-name\": \"Banana Skin Records\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Cannelle\",\n            \"primary\": null,\n            \"sort-name\": \"Cannelle\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Cece Natalie\",\n            \"primary\": null,\n            \"sort-name\": \"Cece Natalie\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Cherry X\",\n            \"primary\": null,\n            \"sort-name\": \"Cherry X\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Chung\",\n            \"primary\": null,\n            \"sort-name\": \"Chung\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Cody Johnson\",\n            \"primary\": null,\n            \"sort-name\": \"Cody Johnson\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Cowgirl Clue\",\n            \"primary\": null,\n            \"sort-name\": \"Cowgirl Clue\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"D.I.Y.\",\n            \"primary\": null,\n            \"sort-name\": \"D.I.Y.\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Damjan Mravunac Self-released)\",\n            \"primary\": null,\n            \"sort-name\": \"Damjan Mravunac Self-released)\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Demo\",\n            \"primary\": null,\n            \"sort-name\": \"Demo\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"DistroKid\",\n            \"primary\": null,\n            \"sort-name\": \"DistroKid\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Egzod\",\n            \"primary\": null,\n            \"sort-name\": \"Egzod\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Eigenverlag\",\n            \"primary\": null,\n            \"sort-name\": \"Eigenverlag\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Eigenvertrieb\",\n            \"primary\": null,\n            \"sort-name\": \"Eigenvertrieb\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"GRIND MODE\",\n            \"primary\": null,\n            \"sort-name\": \"GRIND MODE\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"INDIPENDANT\",\n            \"primary\": null,\n            \"sort-name\": \"INDIPENDANT\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Indepandant\",\n            \"primary\": null,\n            \"sort-name\": \"Indepandant\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Independant release\",\n            \"primary\": null,\n            \"sort-name\": \"Independant release\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Independent\",\n            \"primary\": null,\n            \"sort-name\": \"Independent\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Independente\",\n            \"primary\": null,\n            \"sort-name\": \"Independente\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Independiente\",\n            \"primary\": null,\n            \"sort-name\": \"Independiente\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Indie\",\n            \"primary\": null,\n            \"sort-name\": \"Indie\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Joost Klein\",\n            \"primary\": null,\n            \"sort-name\": \"Joost Klein\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Millington Records\",\n            \"primary\": null,\n            \"sort-name\": \"Millington Records\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"MoroseSound\",\n            \"primary\": null,\n            \"sort-name\": \"MoroseSound\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"N/A\",\n            \"primary\": null,\n            \"sort-name\": \"N/A\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"No Label\",\n            \"primary\": null,\n            \"sort-name\": \"No Label\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"None\",\n            \"primary\": null,\n            \"sort-name\": \"None\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"None Like Joshua\",\n            \"primary\": null,\n            \"sort-name\": \"None Like Joshua\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Not On A Lebel\",\n            \"primary\": null,\n            \"sort-name\": \"Not On A Lebel\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Not On Label\",\n            \"primary\": null,\n            \"sort-name\": \"Not On Label\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Offensively Average Productions\",\n            \"primary\": null,\n            \"sort-name\": \"Offensively Average Productions\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Ours\",\n            \"primary\": null,\n            \"sort-name\": \"Ours\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"P2019\",\n            \"primary\": null,\n            \"sort-name\": \"P2019\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"P2020\",\n            \"primary\": null,\n            \"sort-name\": \"P2020\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"P2021\",\n            \"primary\": null,\n            \"sort-name\": \"P2021\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"P2022\",\n            \"primary\": null,\n            \"sort-name\": \"P2022\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"P2023\",\n            \"primary\": null,\n            \"sort-name\": \"P2023\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"P2024\",\n            \"primary\": null,\n            \"sort-name\": \"P2024\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"P2025\",\n            \"primary\": null,\n            \"sort-name\": \"P2025\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Patriarchy\",\n            \"primary\": null,\n            \"sort-name\": \"Patriarchy\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Plini\",\n            \"primary\": null,\n            \"sort-name\": \"Plini\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Records DK\",\n            \"primary\": null,\n            \"sort-name\": \"Records DK\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Self Digital\",\n            \"primary\": null,\n            \"sort-name\": \"Self Digital\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Self Release\",\n            \"primary\": null,\n            \"sort-name\": \"Self Release\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Self Released\",\n            \"primary\": null,\n            \"sort-name\": \"Self Released\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Self-release\",\n            \"primary\": null,\n            \"sort-name\": \"Self-release\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Self-released\",\n            \"primary\": null,\n            \"sort-name\": \"Self-released\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Self-released/independent\",\n            \"primary\": null,\n            \"sort-name\": \"Self-released/independent\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Sevdaliza\",\n            \"primary\": null,\n            \"sort-name\": \"Sevdaliza\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"TOMMY CASH\",\n            \"primary\": null,\n            \"sort-name\": \"TOMMY CASH\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Take Van\",\n            \"primary\": null,\n            \"sort-name\": \"Take Van\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Talwiinder\",\n            \"primary\": null,\n            \"sort-name\": \"Talwiinder\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Unsigned\",\n            \"primary\": null,\n            \"sort-name\": \"Unsigned\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"VGR\",\n            \"primary\": null,\n            \"sort-name\": \"VGR\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"Woo Da Savage\",\n            \"primary\": null,\n            \"sort-name\": \"Woo Da Savage\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"YANAA\",\n            \"primary\": null,\n            \"sort-name\": \"YANAA\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": \"fi\",\n            \"name\": \"[ei levymerkkiä]\",\n            \"primary\": true,\n            \"sort-name\": \"ei levymerkkiä\",\n            \"type\": \"Label name\",\n            \"type-id\": \"3a1a0c48-d885-3b89-87b2-9e8a483c5675\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": \"nl\",\n            \"name\": \"[geen platenmaatschappij]\",\n            \"primary\": true,\n            \"sort-name\": \"[geen platenmaatschappij]\",\n            \"type\": \"Label name\",\n            \"type-id\": \"3a1a0c48-d885-3b89-87b2-9e8a483c5675\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": \"et\",\n            \"name\": \"[ilma plaadifirmata]\",\n            \"primary\": false,\n            \"sort-name\": \"[ilma plaadifirmata]\",\n            \"type\": \"Label name\",\n            \"type-id\": \"3a1a0c48-d885-3b89-87b2-9e8a483c5675\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": \"es\",\n            \"name\": \"[nada]\",\n            \"primary\": true,\n            \"sort-name\": \"[nada]\",\n            \"type\": \"Label name\",\n            \"type-id\": \"3a1a0c48-d885-3b89-87b2-9e8a483c5675\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": \"en\",\n            \"name\": \"[no label]\",\n            \"primary\": true,\n            \"sort-name\": \"[no label]\",\n            \"type\": \"Label name\",\n            \"type-id\": \"3a1a0c48-d885-3b89-87b2-9e8a483c5675\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"[nolabel]\",\n            \"primary\": null,\n            \"sort-name\": \"[nolabel]\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"[none]\",\n            \"primary\": null,\n            \"sort-name\": \"[none]\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": \"lt\",\n            \"name\": \"[nėra leidybinės kompanijos]\",\n            \"primary\": false,\n            \"sort-name\": \"[nėra leidybinės kompanijos]\",\n            \"type\": \"Label name\",\n            \"type-id\": \"3a1a0c48-d885-3b89-87b2-9e8a483c5675\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": \"lt\",\n            \"name\": \"[nėra leidyklos]\",\n            \"primary\": false,\n            \"sort-name\": \"[nėra leidyklos]\",\n            \"type\": \"Label name\",\n            \"type-id\": \"3a1a0c48-d885-3b89-87b2-9e8a483c5675\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": \"lt\",\n            \"name\": \"[nėra įrašų kompanijos]\",\n            \"primary\": true,\n            \"sort-name\": \"[nėra įrašų kompanijos]\",\n            \"type\": \"Label name\",\n            \"type-id\": \"3a1a0c48-d885-3b89-87b2-9e8a483c5675\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": \"et\",\n            \"name\": \"[puudub]\",\n            \"primary\": false,\n            \"sort-name\": \"[puudub]\",\n            \"type\": \"Label name\",\n            \"type-id\": \"3a1a0c48-d885-3b89-87b2-9e8a483c5675\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": \"ru\",\n            \"name\": \"[самиздат]\",\n            \"primary\": false,\n            \"sort-name\": \"samizdat\",\n            \"type\": \"Label name\",\n            \"type-id\": \"3a1a0c48-d885-3b89-87b2-9e8a483c5675\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": \"ja\",\n            \"name\": \"[レーベルなし]\",\n            \"primary\": true,\n            \"sort-name\": \"[レーベルなし]\",\n            \"type\": \"Label name\",\n            \"type-id\": \"3a1a0c48-d885-3b89-87b2-9e8a483c5675\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"annapantsu music\",\n            \"primary\": null,\n            \"sort-name\": \"annapantsu music\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"auto-release\",\n            \"primary\": null,\n            \"sort-name\": \"auto-release\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"autoprod.\",\n            \"primary\": null,\n            \"sort-name\": \"autoprod.\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"ayesha erotica\",\n            \"primary\": null,\n            \"sort-name\": \"ayesha erotica\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"blank\",\n            \"primary\": null,\n            \"sort-name\": \"blank\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"cupcakKe\",\n            \"primary\": null,\n            \"sort-name\": \"cupcakKe\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"d.silvestre\",\n            \"primary\": null,\n            \"sort-name\": \"d.silvestre\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"dj-Jo\",\n            \"primary\": null,\n            \"sort-name\": \"dj-Jo\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"independent release\",\n            \"primary\": null,\n            \"sort-name\": \"independent release\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"lor2mg\",\n            \"primary\": null,\n            \"sort-name\": \"lor2mg\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"nyamura\",\n            \"primary\": null,\n            \"sort-name\": \"nyamura\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"pls dnt stp\",\n            \"primary\": null,\n            \"sort-name\": \"pls dnt stp\",\n            \"type\": null,\n            \"type-id\": null\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"self\",\n            \"primary\": null,\n            \"sort-name\": \"self\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"self issued\",\n            \"primary\": null,\n            \"sort-name\": \"self issued\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"self-issued\",\n            \"primary\": null,\n            \"sort-name\": \"self-issued\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"white label\",\n            \"primary\": null,\n            \"sort-name\": \"white label\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"но лабел\",\n            \"primary\": null,\n            \"sort-name\": \"но лабел\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          },\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": null,\n            \"name\": \"独立发行\",\n            \"primary\": null,\n            \"sort-name\": \"独立发行\",\n            \"type\": \"Search hint\",\n            \"type-id\": \"829662f2-a781-3ec8-8b46-fbcea6196f81\"\n          }\n        ],\n        \"disambiguation\": \"Special purpose label – white labels, self-published releases and other “no label” releases\",\n        \"genres\": [],\n        \"id\": \"157afde4-4bf5-4039-8ad2-5a15acc85176\",\n        \"label-code\": null,\n        \"name\": \"[no label]\",\n        \"sort-name\": \"[no label]\",\n        \"tags\": [\n          {\n            \"count\": 12,\n            \"name\": \"special purpose\"\n          },\n          {\n            \"count\": 18,\n            \"name\": \"special purpose label\"\n          }\n        ],\n        \"type\": \"Production\",\n        \"type-id\": \"a2426aab-2dd4-339c-b47d-b4923a241678\"\n      }\n    }\n  ],\n  \"media\": [\n    {\n      \"format\": \"Digital Media\",\n      \"format-id\": \"907a28d9-b3b2-3ef6-89a8-7b18d91d4794\",\n      \"id\": \"43f08d54-a896-3561-be75-b881cbc832d5\",\n      \"position\": 1,\n      \"title\": \"\",\n      \"track-count\": 1,\n      \"track-offset\": 0,\n      \"tracks\": [\n        {\n          \"artist-credit\": [\n            {\n              \"artist\": {\n                \"aliases\": [\n                  {\n                    \"begin\": null,\n                    \"end\": null,\n                    \"ended\": false,\n                    \"locale\": \"en\",\n                    \"name\": \"Lilas Ikuta\",\n                    \"primary\": true,\n                    \"sort-name\": \"Ikuta, Lilas\",\n                    \"type\": \"Artist name\",\n                    \"type-id\": \"894afba6-2816-3c24-8072-eadb66bd04bc\"\n                  }\n                ],\n                \"country\": \"JP\",\n                \"disambiguation\": \"\",\n                \"id\": \"55e42264-ef27-49d8-93fd-29f930dc96e4\",\n                \"name\": \"幾田りら\",\n                \"sort-name\": \"Ikuta, Lilas\",\n                \"type\": \"Person\",\n                \"type-id\": \"b6e035f4-3ce9-331c-97df-83397230b0df\"\n              },\n              \"joinphrase\": \"\",\n              \"name\": \"幾田りら\"\n            }\n          ],\n          \"id\": \"0bd01e8b-18e1-4708-b0a3-c9603b89ab97\",\n          \"length\": 179239,\n          \"number\": \"1\",\n          \"position\": 1,\n          \"recording\": {\n            \"aliases\": [],\n            \"artist-credit\": [\n              {\n                \"artist\": {\n                  \"country\": \"JP\",\n                  \"disambiguation\": \"\",\n                  \"id\": \"55e42264-ef27-49d8-93fd-29f930dc96e4\",\n                  \"name\": \"幾田りら\",\n                  \"sort-name\": \"Ikuta, Lilas\",\n                  \"type\": \"Person\",\n                  \"type-id\": \"b6e035f4-3ce9-331c-97df-83397230b0df\"\n                },\n                \"joinphrase\": \"\",\n                \"name\": \"幾田りら\"\n              }\n            ],\n            \"artist-relations\": [\n              {\n                \"artist\": {\n                  \"country\": \"JP\",\n                  \"disambiguation\": \"Japanese composer/arranger/guitarist, agehasprings\",\n                  \"id\": \"f24241fb-4d89-4bf2-8336-3f2a7d2c0025\",\n                  \"name\": \"KOHD\",\n                  \"sort-name\": \"KOHD\",\n                  \"type\": \"Person\",\n                  \"type-id\": \"b6e035f4-3ce9-331c-97df-83397230b0df\"\n                },\n                \"attribute-ids\": {},\n                \"attribute-values\": {},\n                \"attributes\": [],\n                \"begin\": null,\n                \"direction\": \"backward\",\n                \"end\": null,\n                \"ended\": false,\n                \"source-credit\": \"\",\n                \"target-credit\": \"\",\n                \"type\": \"arranger\",\n                \"type-id\": \"22661fb8-cdb7-4f67-8385-b2a8be6c9f0d\"\n              },\n              {\n                \"artist\": {\n                  \"country\": \"JP\",\n                  \"disambiguation\": \"\",\n                  \"id\": \"55e42264-ef27-49d8-93fd-29f930dc96e4\",\n                  \"name\": \"幾田りら\",\n                  \"sort-name\": \"Ikuta, Lilas\",\n                  \"type\": \"Person\",\n                  \"type-id\": \"b6e035f4-3ce9-331c-97df-83397230b0df\"\n                },\n                \"attribute-ids\": {},\n                \"attribute-values\": {},\n                \"attributes\": [],\n                \"begin\": \"2025\",\n                \"direction\": \"backward\",\n                \"end\": \"2025\",\n                \"ended\": true,\n                \"source-credit\": \"\",\n                \"target-credit\": \"Lilas Ikuta\",\n                \"type\": \"phonographic copyright\",\n                \"type-id\": \"7fd5fbc0-fbf4-4d04-be23-417d50a4dc30\"\n              },\n              {\n                \"artist\": {\n                  \"country\": \"JP\",\n                  \"disambiguation\": \"\",\n                  \"id\": \"1d27ab8a-a0df-47cf-b4cc-d2d7a0712a05\",\n                  \"name\": \"山本秀哉\",\n                  \"sort-name\": \"Yamamoto, Shuya\",\n                  \"type\": \"Person\",\n                  \"type-id\": \"b6e035f4-3ce9-331c-97df-83397230b0df\"\n                },\n                \"attribute-ids\": {},\n                \"attribute-values\": {},\n                \"attributes\": [],\n                \"begin\": null,\n                \"direction\": \"backward\",\n                \"end\": null,\n                \"ended\": false,\n                \"source-credit\": \"\",\n                \"target-credit\": \"\",\n                \"type\": \"producer\",\n                \"type-id\": \"5c0ceac3-feb4-41f0-868d-dc06f6e27fc0\"\n              },\n              {\n                \"artist\": {\n                  \"country\": \"JP\",\n                  \"disambiguation\": \"\",\n                  \"id\": \"55e42264-ef27-49d8-93fd-29f930dc96e4\",\n                  \"name\": \"幾田りら\",\n                  \"sort-name\": \"Ikuta, Lilas\",\n                  \"type\": \"Person\",\n                  \"type-id\": \"b6e035f4-3ce9-331c-97df-83397230b0df\"\n                },\n                \"attribute-ids\": {},\n                \"attribute-values\": {},\n                \"attributes\": [],\n                \"begin\": null,\n                \"direction\": \"backward\",\n                \"end\": null,\n                \"ended\": false,\n                \"source-credit\": \"\",\n                \"target-credit\": \"\",\n                \"type\": \"vocal\",\n                \"type-id\": \"0fdbe3c6-7700-4a31-ae54-b53f06ae1cfa\"\n              }\n            ],\n            \"disambiguation\": \"\",\n            \"first-release-date\": \"2025-01-10\",\n            \"genres\": [],\n            \"id\": \"781724c1-a039-41e6-bd9b-770c3b9d5b8e\",\n            \"isrcs\": [\n              \"JPP302400868\"\n            ],\n            \"length\": 179546,\n            \"tags\": [],\n            \"title\": \"百花繚乱\",\n            \"url-relations\": [\n              {\n                \"attribute-ids\": {},\n                \"attribute-values\": {},\n                \"attributes\": [],\n                \"begin\": null,\n                \"direction\": \"forward\",\n                \"end\": null,\n                \"ended\": false,\n                \"source-credit\": \"\",\n                \"target-credit\": \"\",\n                \"type\": \"free streaming\",\n                \"type-id\": \"7e41ef12-a124-4324-afdb-fdbae687a89c\",\n                \"url\": {\n                  \"id\": \"d076eaf9-5fde-4f6e-a946-cde16b67aa3b\",\n                  \"resource\": \"https://open.spotify.com/track/782PTXsbAWB70ySDZ5NHmP\"\n                }\n              },\n              {\n                \"attribute-ids\": {},\n                \"attribute-values\": {},\n                \"attributes\": [],\n                \"begin\": null,\n                \"direction\": \"forward\",\n                \"end\": null,\n                \"ended\": false,\n                \"source-credit\": \"\",\n                \"target-credit\": \"\",\n                \"type\": \"purchase for download\",\n                \"type-id\": \"92777657-504c-4acb-bd33-51a201bd57e1\",\n                \"url\": {\n                  \"id\": \"64879627-6eca-4755-98b5-b2234a8dbc61\",\n                  \"resource\": \"https://music.apple.com/jp/song/1857886416\"\n                }\n              },\n              {\n                \"attribute-ids\": {},\n                \"attribute-values\": {},\n                \"attributes\": [],\n                \"begin\": null,\n                \"direction\": \"forward\",\n                \"end\": null,\n                \"ended\": false,\n                \"source-credit\": \"\",\n                \"target-credit\": \"\",\n                \"type\": \"streaming\",\n                \"type-id\": \"b5f3058a-666c-406f-aafb-f9249fc7b122\",\n                \"url\": {\n                  \"id\": \"64879627-6eca-4755-98b5-b2234a8dbc61\",\n                  \"resource\": \"https://music.apple.com/jp/song/1857886416\"\n                }\n              }\n            ],\n            \"video\": false,\n            \"work-relations\": [\n              {\n                \"attribute-ids\": {},\n                \"attribute-values\": {},\n                \"attributes\": [],\n                \"begin\": null,\n                \"direction\": \"forward\",\n                \"end\": null,\n                \"ended\": false,\n                \"source-credit\": \"\",\n                \"target-credit\": \"\",\n                \"type\": \"performance\",\n                \"type-id\": \"a3005666-a872-32c3-ad06-98af558e99b0\",\n                \"work\": {\n                  \"artist-relations\": [\n                    {\n                      \"artist\": {\n                        \"country\": \"JP\",\n                        \"disambiguation\": \"\",\n                        \"id\": \"55e42264-ef27-49d8-93fd-29f930dc96e4\",\n                        \"name\": \"幾田りら\",\n                        \"sort-name\": \"Ikuta, Lilas\",\n                        \"type\": \"Person\",\n                        \"type-id\": \"b6e035f4-3ce9-331c-97df-83397230b0df\"\n                      },\n                      \"attribute-ids\": {},\n                      \"attribute-values\": {},\n                      \"attributes\": [],\n                      \"begin\": null,\n                      \"direction\": \"backward\",\n                      \"end\": null,\n                      \"ended\": false,\n                      \"source-credit\": \"\",\n                      \"target-credit\": \"\",\n                      \"type\": \"composer\",\n                      \"type-id\": \"d59d99ea-23d4-4a80-b066-edca32ee158f\"\n                    },\n                    {\n                      \"artist\": {\n                        \"country\": \"JP\",\n                        \"disambiguation\": \"\",\n                        \"id\": \"55e42264-ef27-49d8-93fd-29f930dc96e4\",\n                        \"name\": \"幾田りら\",\n                        \"sort-name\": \"Ikuta, Lilas\",\n                        \"type\": \"Person\",\n                        \"type-id\": \"b6e035f4-3ce9-331c-97df-83397230b0df\"\n                      },\n                      \"attribute-ids\": {},\n                      \"attribute-values\": {},\n                      \"attributes\": [],\n                      \"begin\": null,\n                      \"direction\": \"backward\",\n                      \"end\": null,\n                      \"ended\": false,\n                      \"source-credit\": \"\",\n                      \"target-credit\": \"\",\n                      \"type\": \"lyricist\",\n                      \"type-id\": \"3e48faba-ec01-47fd-8e89-30e81161661c\"\n                    }\n                  ],\n                  \"attributes\": [],\n                  \"disambiguation\": \"\",\n                  \"id\": \"9e14d6b2-ac7d-43e9-82a9-561bc76ce2ed\",\n                  \"iswcs\": [],\n                  \"language\": \"jpn\",\n                  \"languages\": [\n                    \"jpn\"\n                  ],\n                  \"title\": \"百花繚乱\",\n                  \"type\": \"Song\",\n                  \"type-id\": \"f061270a-2fd6-32f1-a641-f0f8676d14e6\",\n                  \"url-relations\": [\n                    {\n                      \"attribute-ids\": {},\n                      \"attribute-values\": {},\n                      \"attributes\": [],\n                      \"begin\": null,\n                      \"direction\": \"backward\",\n                      \"end\": null,\n                      \"ended\": false,\n                      \"source-credit\": \"\",\n                      \"target-credit\": \"\",\n                      \"type\": \"lyrics\",\n                      \"type-id\": \"e38e65aa-75e0-42ba-ace0-072aeb91a538\",\n                      \"url\": {\n                        \"id\": \"dfac3640-6b23-4991-a59c-7cb80e8eb950\",\n                        \"resource\": \"https://utaten.com/lyric/tt24121002/\"\n                      }\n                    },\n                    {\n                      \"attribute-ids\": {},\n                      \"attribute-values\": {},\n                      \"attributes\": [],\n                      \"begin\": null,\n                      \"direction\": \"backward\",\n                      \"end\": null,\n                      \"ended\": false,\n                      \"source-credit\": \"\",\n                      \"target-credit\": \"\",\n                      \"type\": \"lyrics\",\n                      \"type-id\": \"e38e65aa-75e0-42ba-ace0-072aeb91a538\",\n                      \"url\": {\n                        \"id\": \"b1b5d5df-e79d-4cda-bb2a-8014e5505415\",\n                        \"resource\": \"https://www.uta-net.com/song/366579/\"\n                      }\n                    }\n                  ]\n                }\n              }\n            ]\n          },\n          \"title\": \"百花繚乱\"\n        }\n      ]\n    }\n  ],\n  \"packaging\": \"None\",\n  \"packaging-id\": \"119eba76-b343-3e02-a292-f0f00644bb9b\",\n  \"quality\": \"normal\",\n  \"release-events\": [\n    {\n      \"area\": {\n        \"disambiguation\": \"\",\n        \"id\": \"525d4e18-3d00-31b9-a58b-a146a916de8f\",\n        \"iso-3166-1-codes\": [\n          \"XW\"\n        ],\n        \"name\": \"[Worldwide]\",\n        \"sort-name\": \"[Worldwide]\",\n        \"type\": null,\n        \"type-id\": null\n      },\n      \"date\": \"2025-01-10\"\n    }\n  ],\n  \"release-group\": {\n    \"aliases\": [],\n    \"artist-credit\": [\n      {\n        \"artist\": {\n          \"aliases\": [\n            {\n              \"begin\": null,\n              \"end\": null,\n              \"ended\": false,\n              \"locale\": \"en\",\n              \"name\": \"Lilas Ikuta\",\n              \"primary\": true,\n              \"sort-name\": \"Ikuta, Lilas\",\n              \"type\": \"Artist name\",\n              \"type-id\": \"894afba6-2816-3c24-8072-eadb66bd04bc\"\n            }\n          ],\n          \"country\": \"JP\",\n          \"disambiguation\": \"\",\n          \"id\": \"55e42264-ef27-49d8-93fd-29f930dc96e4\",\n          \"name\": \"幾田りら\",\n          \"sort-name\": \"Ikuta, Lilas\",\n          \"type\": \"Person\",\n          \"type-id\": \"b6e035f4-3ce9-331c-97df-83397230b0df\"\n        },\n        \"joinphrase\": \"\",\n        \"name\": \"幾田りら\"\n      }\n    ],\n    \"disambiguation\": \"\",\n    \"first-release-date\": \"2025-01-10\",\n    \"genres\": [],\n    \"id\": \"da0d6bbb-f44b-4fff-8739-9d72db0402a1\",\n    \"primary-type\": \"Single\",\n    \"primary-type-id\": \"d6038452-8ee0-3f68-affc-2de9a1ede0b9\",\n    \"secondary-type-ids\": [],\n    \"secondary-types\": [],\n    \"tags\": [],\n    \"title\": \"百花繚乱\"\n  },\n  \"release-relations\": [\n    {\n      \"attribute-ids\": {},\n      \"attribute-values\": {},\n      \"attributes\": [],\n      \"begin\": null,\n      \"direction\": \"forward\",\n      \"end\": null,\n      \"ended\": false,\n      \"release\": {\n        \"artist-credit\": [\n          {\n            \"artist\": {\n              \"country\": \"JP\",\n              \"disambiguation\": \"\",\n              \"id\": \"55e42264-ef27-49d8-93fd-29f930dc96e4\",\n              \"name\": \"幾田りら\",\n              \"sort-name\": \"Ikuta, Lilas\",\n              \"type\": null,\n              \"type-id\": null\n            },\n            \"joinphrase\": \"\",\n            \"name\": \"Lilas Ikuta\"\n          }\n        ],\n        \"barcode\": null,\n        \"disambiguation\": \"\",\n        \"id\": \"dc3ee2df-0bc1-49eb-b8c4-34473d279a43\",\n        \"media\": [],\n        \"packaging\": null,\n        \"packaging-id\": null,\n        \"quality\": \"normal\",\n        \"release-group\": null,\n        \"status\": null,\n        \"status-id\": null,\n        \"text-representation\": {\n          \"language\": \"eng\",\n          \"script\": \"Latn\"\n        },\n        \"title\": \"In Bloom\"\n      },\n      \"source-credit\": \"\",\n      \"target-credit\": \"\",\n      \"type\": \"transl-tracklisting\",\n      \"type-id\": \"fc399d47-23a7-4c28-bfcf-0607a562b644\"\n    }\n  ],\n  \"status\": \"Official\",\n  \"status-id\": \"4e304316-386d-3409-af2e-78857eec5cfe\",\n  \"tags\": [],\n  \"text-representation\": {\n    \"language\": \"jpn\",\n    \"script\": \"Jpan\"\n  },\n  \"title\": \"百花繚乱\",\n  \"url-relations\": [\n    {\n      \"attribute-ids\": {},\n      \"attribute-values\": {},\n      \"attributes\": [],\n      \"begin\": null,\n      \"direction\": \"forward\",\n      \"end\": null,\n      \"ended\": false,\n      \"source-credit\": \"\",\n      \"target-credit\": \"\",\n      \"type\": \"amazon asin\",\n      \"type-id\": \"4f2e710d-166c-480c-a293-2e2c8d658d87\",\n      \"url\": {\n        \"id\": \"b50c7fb8-2327-4a05-b989-f2211a41afee\",\n        \"resource\": \"https://www.amazon.co.jp/gp/product/B0DR8Y2YDC\"\n      }\n    },\n    {\n      \"attribute-ids\": {},\n      \"attribute-values\": {},\n      \"attributes\": [],\n      \"begin\": null,\n      \"direction\": \"forward\",\n      \"end\": null,\n      \"ended\": false,\n      \"source-credit\": \"\",\n      \"target-credit\": \"\",\n      \"type\": \"free streaming\",\n      \"type-id\": \"08445ccf-7b99-4438-9f9a-fb9ac18099ee\",\n      \"url\": {\n        \"id\": \"5106a7b0-1443-4803-91a2-28cac2cfb5e0\",\n        \"resource\": \"https://open.spotify.com/album/3LDV2xGL9HiqCsQujEPQLb\"\n      }\n    },\n    {\n      \"attribute-ids\": {},\n      \"attribute-values\": {},\n      \"attributes\": [],\n      \"begin\": null,\n      \"direction\": \"forward\",\n      \"end\": null,\n      \"ended\": false,\n      \"source-credit\": \"\",\n      \"target-credit\": \"\",\n      \"type\": \"free streaming\",\n      \"type-id\": \"08445ccf-7b99-4438-9f9a-fb9ac18099ee\",\n      \"url\": {\n        \"id\": \"d481d94b-a7bf-4e82-8da0-1757fedcda62\",\n        \"resource\": \"https://www.deezer.com/album/687686261\"\n      }\n    },\n    {\n      \"attribute-ids\": {},\n      \"attribute-values\": {},\n      \"attributes\": [],\n      \"begin\": null,\n      \"direction\": \"forward\",\n      \"end\": null,\n      \"ended\": false,\n      \"source-credit\": \"\",\n      \"target-credit\": \"\",\n      \"type\": \"purchase for download\",\n      \"type-id\": \"98e08c20-8402-4163-8970-53504bb6a1e4\",\n      \"url\": {\n        \"id\": \"6156d2e4-d107-43f9-8f44-52f04d39c78e\",\n        \"resource\": \"https://mora.jp/package/43000011/199066336168/\"\n      }\n    },\n    {\n      \"attribute-ids\": {},\n      \"attribute-values\": {},\n      \"attributes\": [],\n      \"begin\": null,\n      \"direction\": \"forward\",\n      \"end\": null,\n      \"ended\": false,\n      \"source-credit\": \"\",\n      \"target-credit\": \"\",\n      \"type\": \"purchase for download\",\n      \"type-id\": \"98e08c20-8402-4163-8970-53504bb6a1e4\",\n      \"url\": {\n        \"id\": \"a4eabb88-1746-4aa2-ab09-c28cfbe65efb\",\n        \"resource\": \"https://mora.jp/package/43000011/199066336168_HD/\"\n      }\n    },\n    {\n      \"attribute-ids\": {},\n      \"attribute-values\": {},\n      \"attributes\": [],\n      \"begin\": null,\n      \"direction\": \"forward\",\n      \"end\": null,\n      \"ended\": false,\n      \"source-credit\": \"\",\n      \"target-credit\": \"\",\n      \"type\": \"purchase for download\",\n      \"type-id\": \"98e08c20-8402-4163-8970-53504bb6a1e4\",\n      \"url\": {\n        \"id\": \"ab8440f0-3b13-4436-b3ad-f4695c9d8875\",\n        \"resource\": \"https://mora.jp/package/43000011/199066336168_LL/\"\n      }\n    },\n    {\n      \"attribute-ids\": {},\n      \"attribute-values\": {},\n      \"attributes\": [],\n      \"begin\": null,\n      \"direction\": \"forward\",\n      \"end\": null,\n      \"ended\": false,\n      \"source-credit\": \"\",\n      \"target-credit\": \"\",\n      \"type\": \"purchase for download\",\n      \"type-id\": \"98e08c20-8402-4163-8970-53504bb6a1e4\",\n      \"url\": {\n        \"id\": \"9a8ee8d1-f946-44a1-be16-8f7a77c951e9\",\n        \"resource\": \"https://music.apple.com/jp/album/1786972161\"\n      }\n    },\n    {\n      \"attribute-ids\": {},\n      \"attribute-values\": {},\n      \"attributes\": [],\n      \"begin\": null,\n      \"direction\": \"forward\",\n      \"end\": null,\n      \"ended\": false,\n      \"source-credit\": \"\",\n      \"target-credit\": \"\",\n      \"type\": \"purchase for download\",\n      \"type-id\": \"98e08c20-8402-4163-8970-53504bb6a1e4\",\n      \"url\": {\n        \"id\": \"c6faaa80-38fb-46a4-aa2b-78cddc5cbe70\",\n        \"resource\": \"https://ototoy.jp/_/default/p/2501951\"\n      }\n    },\n    {\n      \"attribute-ids\": {},\n      \"attribute-values\": {},\n      \"attributes\": [],\n      \"begin\": null,\n      \"direction\": \"forward\",\n      \"end\": null,\n      \"ended\": false,\n      \"source-credit\": \"\",\n      \"target-credit\": \"\",\n      \"type\": \"purchase for download\",\n      \"type-id\": \"98e08c20-8402-4163-8970-53504bb6a1e4\",\n      \"url\": {\n        \"id\": \"0e7e8bc5-0779-492d-a9db-9ab58f96d23b\",\n        \"resource\": \"https://www.qobuz.com/jp-ja/album/lilas-ikuta-/fl9tx2j78reza\"\n      }\n    },\n    {\n      \"attribute-ids\": {},\n      \"attribute-values\": {},\n      \"attributes\": [],\n      \"begin\": null,\n      \"direction\": \"forward\",\n      \"end\": null,\n      \"ended\": false,\n      \"source-credit\": \"\",\n      \"target-credit\": \"\",\n      \"type\": \"purchase for download\",\n      \"type-id\": \"98e08c20-8402-4163-8970-53504bb6a1e4\",\n      \"url\": {\n        \"id\": \"c0cf8fe0-3413-4544-a026-37d346a59a77\",\n        \"resource\": \"https://www.qobuz.com/jp-ja/album/lilas-ikuta-/l1dnc4xoi6l7a\"\n      }\n    },\n    {\n      \"attribute-ids\": {},\n      \"attribute-values\": {},\n      \"attributes\": [],\n      \"begin\": null,\n      \"direction\": \"forward\",\n      \"end\": null,\n      \"ended\": false,\n      \"source-credit\": \"\",\n      \"target-credit\": \"\",\n      \"type\": \"streaming\",\n      \"type-id\": \"320adf26-96fa-4183-9045-1f5f32f833cb\",\n      \"url\": {\n        \"id\": \"e4ce55a9-a5e1-4842-b42d-11be6a31fdab\",\n        \"resource\": \"https://music.amazon.co.jp/albums/B0DR8Y2YDC\"\n      }\n    },\n    {\n      \"attribute-ids\": {},\n      \"attribute-values\": {},\n      \"attributes\": [],\n      \"begin\": null,\n      \"direction\": \"forward\",\n      \"end\": null,\n      \"ended\": false,\n      \"source-credit\": \"\",\n      \"target-credit\": \"\",\n      \"type\": \"streaming\",\n      \"type-id\": \"320adf26-96fa-4183-9045-1f5f32f833cb\",\n      \"url\": {\n        \"id\": \"9a8ee8d1-f946-44a1-be16-8f7a77c951e9\",\n        \"resource\": \"https://music.apple.com/jp/album/1786972161\"\n      }\n    },\n    {\n      \"attribute-ids\": {},\n      \"attribute-values\": {},\n      \"attributes\": [],\n      \"begin\": null,\n      \"direction\": \"forward\",\n      \"end\": null,\n      \"ended\": false,\n      \"source-credit\": \"\",\n      \"target-credit\": \"\",\n      \"type\": \"vgmdb\",\n      \"type-id\": \"6af0134a-df6a-425a-96e2-895f9cd342ba\",\n      \"url\": {\n        \"id\": \"1885772a-4004-4d45-9512-d0c8822506c9\",\n        \"resource\": \"https://vgmdb.net/album/145936\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "test/rsrc/mbpseudo/pseudo_release.json",
    "content": "{\n  \"aliases\": [],\n  \"artist-credit\": [\n    {\n      \"artist\": {\n        \"aliases\": [\n          {\n            \"begin\": null,\n            \"end\": null,\n            \"ended\": false,\n            \"locale\": \"en\",\n            \"name\": \"Lilas Ikuta\",\n            \"primary\": true,\n            \"sort-name\": \"Ikuta, Lilas\",\n            \"type\": \"Artist name\",\n            \"type-id\": \"894afba6-2816-3c24-8072-eadb66bd04bc\"\n          }\n        ],\n        \"country\": \"JP\",\n        \"disambiguation\": \"\",\n        \"genres\": [\n          {\n            \"count\": 1,\n            \"disambiguation\": \"\",\n            \"id\": \"eba7715e-ee26-4989-8d49-9db382955419\",\n            \"name\": \"j-pop\"\n          },\n          {\n            \"count\": 1,\n            \"disambiguation\": \"\",\n            \"id\": \"455f264b-db00-4716-991d-fbd32dc24523\",\n            \"name\": \"singer-songwriter\"\n          }\n        ],\n        \"id\": \"55e42264-ef27-49d8-93fd-29f930dc96e4\",\n        \"name\": \"幾田りら\",\n        \"sort-name\": \"Ikuta, Lilas\",\n        \"tags\": [\n          {\n            \"count\": 1,\n            \"name\": \"j-pop\"\n          },\n          {\n            \"count\": 1,\n            \"name\": \"singer-songwriter\"\n          }\n        ],\n        \"type\": \"Person\",\n        \"type-id\": \"b6e035f4-3ce9-331c-97df-83397230b0df\"\n      },\n      \"joinphrase\": \"\",\n      \"name\": \"Lilas Ikuta\"\n    }\n  ],\n  \"asin\": null,\n  \"barcode\": null,\n  \"cover-art-archive\": {\n    \"artwork\": false,\n    \"back\": false,\n    \"count\": 0,\n    \"darkened\": false,\n    \"front\": false\n  },\n  \"disambiguation\": \"\",\n  \"genres\": [],\n  \"id\": \"dc3ee2df-0bc1-49eb-b8c4-34473d279a43\",\n  \"label-info\": [],\n  \"media\": [\n    {\n      \"format\": \"Digital Media\",\n      \"format-id\": \"907a28d9-b3b2-3ef6-89a8-7b18d91d4794\",\n      \"id\": \"606faab7-60fa-3a8b-a40f-2c66150cce81\",\n      \"position\": 1,\n      \"title\": \"\",\n      \"track-count\": 1,\n      \"track-offset\": 0,\n      \"tracks\": [\n        {\n          \"artist-credit\": [\n            {\n              \"artist\": {\n                \"aliases\": [\n                  {\n                    \"begin\": null,\n                    \"end\": null,\n                    \"ended\": false,\n                    \"locale\": \"en\",\n                    \"name\": \"Lilas Ikuta\",\n                    \"primary\": true,\n                    \"sort-name\": \"Ikuta, Lilas\",\n                    \"type\": \"Artist name\",\n                    \"type-id\": \"894afba6-2816-3c24-8072-eadb66bd04bc\"\n                  }\n                ],\n                \"country\": \"JP\",\n                \"disambiguation\": \"\",\n                \"id\": \"55e42264-ef27-49d8-93fd-29f930dc96e4\",\n                \"name\": \"幾田りら\",\n                \"sort-name\": \"Ikuta, Lilas\",\n                \"type\": \"Person\",\n                \"type-id\": \"b6e035f4-3ce9-331c-97df-83397230b0df\"\n              },\n              \"joinphrase\": \"\",\n              \"name\": \"Lilas Ikuta\"\n            }\n          ],\n          \"id\": \"2018b012-a184-49a2-a464-fb4628a89588\",\n          \"length\": 179239,\n          \"number\": \"1\",\n          \"position\": 1,\n          \"recording\": {\n            \"aliases\": [],\n            \"artist-credit\": [\n              {\n                \"artist\": {\n                  \"country\": \"JP\",\n                  \"disambiguation\": \"\",\n                  \"id\": \"55e42264-ef27-49d8-93fd-29f930dc96e4\",\n                  \"name\": \"幾田りら\",\n                  \"sort-name\": \"Ikuta, Lilas\",\n                  \"type\": \"Person\",\n                  \"type-id\": \"b6e035f4-3ce9-331c-97df-83397230b0df\"\n                },\n                \"joinphrase\": \"\",\n                \"name\": \"幾田りら\"\n              }\n            ],\n            \"artist-relations\": [\n              {\n                \"artist\": {\n                  \"country\": \"JP\",\n                  \"disambiguation\": \"Japanese composer/arranger/guitarist, agehasprings\",\n                  \"id\": \"f24241fb-4d89-4bf2-8336-3f2a7d2c0025\",\n                  \"name\": \"KOHD\",\n                  \"sort-name\": \"KOHD\",\n                  \"type\": \"Person\",\n                  \"type-id\": \"b6e035f4-3ce9-331c-97df-83397230b0df\"\n                },\n                \"attribute-ids\": {},\n                \"attribute-values\": {},\n                \"attributes\": [],\n                \"begin\": null,\n                \"direction\": \"backward\",\n                \"end\": null,\n                \"ended\": false,\n                \"source-credit\": \"\",\n                \"target-credit\": \"\",\n                \"type\": \"arranger\",\n                \"type-id\": \"22661fb8-cdb7-4f67-8385-b2a8be6c9f0d\"\n              },\n              {\n                \"artist\": {\n                  \"country\": \"JP\",\n                  \"disambiguation\": \"\",\n                  \"id\": \"55e42264-ef27-49d8-93fd-29f930dc96e4\",\n                  \"name\": \"幾田りら\",\n                  \"sort-name\": \"Ikuta, Lilas\",\n                  \"type\": \"Person\",\n                  \"type-id\": \"b6e035f4-3ce9-331c-97df-83397230b0df\"\n                },\n                \"attribute-ids\": {},\n                \"attribute-values\": {},\n                \"attributes\": [],\n                \"begin\": \"2025\",\n                \"direction\": \"backward\",\n                \"end\": \"2025\",\n                \"ended\": true,\n                \"source-credit\": \"\",\n                \"target-credit\": \"Lilas Ikuta\",\n                \"type\": \"phonographic copyright\",\n                \"type-id\": \"7fd5fbc0-fbf4-4d04-be23-417d50a4dc30\"\n              },\n              {\n                \"artist\": {\n                  \"country\": \"JP\",\n                  \"disambiguation\": \"\",\n                  \"id\": \"1d27ab8a-a0df-47cf-b4cc-d2d7a0712a05\",\n                  \"name\": \"山本秀哉\",\n                  \"sort-name\": \"Yamamoto, Shuya\",\n                  \"type\": \"Person\",\n                  \"type-id\": \"b6e035f4-3ce9-331c-97df-83397230b0df\"\n                },\n                \"attribute-ids\": {},\n                \"attribute-values\": {},\n                \"attributes\": [],\n                \"begin\": null,\n                \"direction\": \"backward\",\n                \"end\": null,\n                \"ended\": false,\n                \"source-credit\": \"\",\n                \"target-credit\": \"\",\n                \"type\": \"producer\",\n                \"type-id\": \"5c0ceac3-feb4-41f0-868d-dc06f6e27fc0\"\n              },\n              {\n                \"artist\": {\n                  \"country\": \"JP\",\n                  \"disambiguation\": \"\",\n                  \"id\": \"55e42264-ef27-49d8-93fd-29f930dc96e4\",\n                  \"name\": \"幾田りら\",\n                  \"sort-name\": \"Ikuta, Lilas\",\n                  \"type\": \"Person\",\n                  \"type-id\": \"b6e035f4-3ce9-331c-97df-83397230b0df\"\n                },\n                \"attribute-ids\": {},\n                \"attribute-values\": {},\n                \"attributes\": [],\n                \"begin\": null,\n                \"direction\": \"backward\",\n                \"end\": null,\n                \"ended\": false,\n                \"source-credit\": \"\",\n                \"target-credit\": \"\",\n                \"type\": \"vocal\",\n                \"type-id\": \"0fdbe3c6-7700-4a31-ae54-b53f06ae1cfa\"\n              }\n            ],\n            \"disambiguation\": \"\",\n            \"first-release-date\": \"2025-01-10\",\n            \"genres\": [],\n            \"id\": \"781724c1-a039-41e6-bd9b-770c3b9d5b8e\",\n            \"isrcs\": [\n              \"JPP302400868\"\n            ],\n            \"length\": 179546,\n            \"tags\": [],\n            \"title\": \"百花繚乱\",\n            \"url-relations\": [\n              {\n                \"attribute-ids\": {},\n                \"attribute-values\": {},\n                \"attributes\": [],\n                \"begin\": null,\n                \"direction\": \"forward\",\n                \"end\": null,\n                \"ended\": false,\n                \"source-credit\": \"\",\n                \"target-credit\": \"\",\n                \"type\": \"free streaming\",\n                \"type-id\": \"7e41ef12-a124-4324-afdb-fdbae687a89c\",\n                \"url\": {\n                  \"id\": \"d076eaf9-5fde-4f6e-a946-cde16b67aa3b\",\n                  \"resource\": \"https://open.spotify.com/track/782PTXsbAWB70ySDZ5NHmP\"\n                }\n              },\n              {\n                \"attribute-ids\": {},\n                \"attribute-values\": {},\n                \"attributes\": [],\n                \"begin\": null,\n                \"direction\": \"forward\",\n                \"end\": null,\n                \"ended\": false,\n                \"source-credit\": \"\",\n                \"target-credit\": \"\",\n                \"type\": \"purchase for download\",\n                \"type-id\": \"92777657-504c-4acb-bd33-51a201bd57e1\",\n                \"url\": {\n                  \"id\": \"64879627-6eca-4755-98b5-b2234a8dbc61\",\n                  \"resource\": \"https://music.apple.com/jp/song/1857886416\"\n                }\n              },\n              {\n                \"attribute-ids\": {},\n                \"attribute-values\": {},\n                \"attributes\": [],\n                \"begin\": null,\n                \"direction\": \"forward\",\n                \"end\": null,\n                \"ended\": false,\n                \"source-credit\": \"\",\n                \"target-credit\": \"\",\n                \"type\": \"streaming\",\n                \"type-id\": \"b5f3058a-666c-406f-aafb-f9249fc7b122\",\n                \"url\": {\n                  \"id\": \"64879627-6eca-4755-98b5-b2234a8dbc61\",\n                  \"resource\": \"https://music.apple.com/jp/song/1857886416\"\n                }\n              }\n            ],\n            \"video\": false,\n            \"work-relations\": [\n              {\n                \"attribute-ids\": {},\n                \"attribute-values\": {},\n                \"attributes\": [],\n                \"begin\": null,\n                \"direction\": \"forward\",\n                \"end\": null,\n                \"ended\": false,\n                \"source-credit\": \"\",\n                \"target-credit\": \"\",\n                \"type\": \"performance\",\n                \"type-id\": \"a3005666-a872-32c3-ad06-98af558e99b0\",\n                \"work\": {\n                  \"artist-relations\": [\n                    {\n                      \"artist\": {\n                        \"country\": \"JP\",\n                        \"disambiguation\": \"\",\n                        \"id\": \"55e42264-ef27-49d8-93fd-29f930dc96e4\",\n                        \"name\": \"幾田りら\",\n                        \"sort-name\": \"Ikuta, Lilas\",\n                        \"type\": \"Person\",\n                        \"type-id\": \"b6e035f4-3ce9-331c-97df-83397230b0df\"\n                      },\n                      \"attribute-ids\": {},\n                      \"attribute-values\": {},\n                      \"attributes\": [],\n                      \"begin\": null,\n                      \"direction\": \"backward\",\n                      \"end\": null,\n                      \"ended\": false,\n                      \"source-credit\": \"\",\n                      \"target-credit\": \"\",\n                      \"type\": \"composer\",\n                      \"type-id\": \"d59d99ea-23d4-4a80-b066-edca32ee158f\"\n                    },\n                    {\n                      \"artist\": {\n                        \"country\": \"JP\",\n                        \"disambiguation\": \"\",\n                        \"id\": \"55e42264-ef27-49d8-93fd-29f930dc96e4\",\n                        \"name\": \"幾田りら\",\n                        \"sort-name\": \"Ikuta, Lilas\",\n                        \"type\": \"Person\",\n                        \"type-id\": \"b6e035f4-3ce9-331c-97df-83397230b0df\"\n                      },\n                      \"attribute-ids\": {},\n                      \"attribute-values\": {},\n                      \"attributes\": [],\n                      \"begin\": null,\n                      \"direction\": \"backward\",\n                      \"end\": null,\n                      \"ended\": false,\n                      \"source-credit\": \"\",\n                      \"target-credit\": \"\",\n                      \"type\": \"lyricist\",\n                      \"type-id\": \"3e48faba-ec01-47fd-8e89-30e81161661c\"\n                    }\n                  ],\n                  \"attributes\": [],\n                  \"disambiguation\": \"\",\n                  \"id\": \"9e14d6b2-ac7d-43e9-82a9-561bc76ce2ed\",\n                  \"iswcs\": [],\n                  \"language\": \"jpn\",\n                  \"languages\": [\n                    \"jpn\"\n                  ],\n                  \"title\": \"百花繚乱\",\n                  \"type\": \"Song\",\n                  \"type-id\": \"f061270a-2fd6-32f1-a641-f0f8676d14e6\",\n                  \"url-relations\": [\n                    {\n                      \"attribute-ids\": {},\n                      \"attribute-values\": {},\n                      \"attributes\": [],\n                      \"begin\": null,\n                      \"direction\": \"backward\",\n                      \"end\": null,\n                      \"ended\": false,\n                      \"source-credit\": \"\",\n                      \"target-credit\": \"\",\n                      \"type\": \"lyrics\",\n                      \"type-id\": \"e38e65aa-75e0-42ba-ace0-072aeb91a538\",\n                      \"url\": {\n                        \"id\": \"dfac3640-6b23-4991-a59c-7cb80e8eb950\",\n                        \"resource\": \"https://utaten.com/lyric/tt24121002/\"\n                      }\n                    },\n                    {\n                      \"attribute-ids\": {},\n                      \"attribute-values\": {},\n                      \"attributes\": [],\n                      \"begin\": null,\n                      \"direction\": \"backward\",\n                      \"end\": null,\n                      \"ended\": false,\n                      \"source-credit\": \"\",\n                      \"target-credit\": \"\",\n                      \"type\": \"lyrics\",\n                      \"type-id\": \"e38e65aa-75e0-42ba-ace0-072aeb91a538\",\n                      \"url\": {\n                        \"id\": \"b1b5d5df-e79d-4cda-bb2a-8014e5505415\",\n                        \"resource\": \"https://www.uta-net.com/song/366579/\"\n                      }\n                    }\n                  ]\n                }\n              }\n            ]\n          },\n          \"title\": \"In Bloom\"\n        }\n      ]\n    }\n  ],\n  \"packaging\": null,\n  \"packaging-id\": null,\n  \"quality\": \"normal\",\n  \"release-group\": {\n    \"aliases\": [],\n    \"artist-credit\": [\n      {\n        \"artist\": {\n          \"aliases\": [\n            {\n              \"begin\": null,\n              \"end\": null,\n              \"ended\": false,\n              \"locale\": \"en\",\n              \"name\": \"Lilas Ikuta\",\n              \"primary\": true,\n              \"sort-name\": \"Ikuta, Lilas\",\n              \"type\": \"Artist name\",\n              \"type-id\": \"894afba6-2816-3c24-8072-eadb66bd04bc\"\n            }\n          ],\n          \"country\": \"JP\",\n          \"disambiguation\": \"\",\n          \"id\": \"55e42264-ef27-49d8-93fd-29f930dc96e4\",\n          \"name\": \"幾田りら\",\n          \"sort-name\": \"Ikuta, Lilas\",\n          \"type\": \"Person\",\n          \"type-id\": \"b6e035f4-3ce9-331c-97df-83397230b0df\"\n        },\n        \"joinphrase\": \"\",\n        \"name\": \"幾田りら\"\n      }\n    ],\n    \"disambiguation\": \"\",\n    \"first-release-date\": \"2025-01-10\",\n    \"genres\": [],\n    \"id\": \"da0d6bbb-f44b-4fff-8739-9d72db0402a1\",\n    \"primary-type\": \"Single\",\n    \"primary-type-id\": \"d6038452-8ee0-3f68-affc-2de9a1ede0b9\",\n    \"secondary-type-ids\": [],\n    \"secondary-types\": [],\n    \"tags\": [],\n    \"title\": \"百花繚乱\"\n  },\n  \"release-relations\": [\n    {\n      \"attribute-ids\": {},\n      \"attribute-values\": {},\n      \"attributes\": [],\n      \"begin\": null,\n      \"direction\": \"backward\",\n      \"end\": null,\n      \"ended\": false,\n      \"release\": {\n        \"artist-credit\": [\n          {\n            \"artist\": {\n              \"country\": \"JP\",\n              \"disambiguation\": \"\",\n              \"id\": \"55e42264-ef27-49d8-93fd-29f930dc96e4\",\n              \"name\": \"幾田りら\",\n              \"sort-name\": \"Ikuta, Lilas\",\n              \"type\": null,\n              \"type-id\": null\n            },\n            \"joinphrase\": \"\",\n            \"name\": \"幾田りら\"\n          }\n        ],\n        \"barcode\": \"199066336168\",\n        \"country\": \"XW\",\n        \"date\": \"2025-01-10\",\n        \"disambiguation\": \"\",\n        \"id\": \"a5ce1d11-2e32-45a4-b37f-c1589d46b103\",\n        \"media\": [],\n        \"packaging\": null,\n        \"packaging-id\": null,\n        \"quality\": \"normal\",\n        \"release-events\": [\n          {\n            \"area\": {\n              \"disambiguation\": \"\",\n              \"id\": \"525d4e18-3d00-31b9-a58b-a146a916de8f\",\n              \"iso-3166-1-codes\": [\n                \"XW\"\n              ],\n              \"name\": \"[Worldwide]\",\n              \"sort-name\": \"[Worldwide]\",\n              \"type\": null,\n              \"type-id\": null\n            },\n            \"date\": \"2025-01-10\"\n          }\n        ],\n        \"release-group\": null,\n        \"status\": null,\n        \"status-id\": null,\n        \"text-representation\": {\n          \"language\": \"jpn\",\n          \"script\": \"Jpan\"\n        },\n        \"title\": \"百花繚乱\"\n      },\n      \"source-credit\": \"\",\n      \"target-credit\": \"\",\n      \"type\": \"transl-tracklisting\",\n      \"type-id\": \"fc399d47-23a7-4c28-bfcf-0607a562b644\"\n    }\n  ],\n  \"status\": \"Pseudo-Release\",\n  \"status-id\": \"41121bb9-3413-3818-8a9a-9742318349aa\",\n  \"tags\": [],\n  \"text-representation\": {\n    \"language\": \"eng\",\n    \"script\": \"Latn\"\n  },\n  \"title\": \"In Bloom\"\n}\n"
  },
  {
    "path": "test/rsrc/playlist.m3u",
    "content": "#EXTM3U\n/This/is/a/path/to_a_file.mp3\n/This/is/another/path/to_a_file.mp3\n"
  },
  {
    "path": "test/rsrc/playlist.m3u8",
    "content": "#EXTM3U\n/This/is/å/path/to_a_file.mp3\n/This/is/another/path/tö_a_file.mp3\n"
  },
  {
    "path": "test/rsrc/playlist_non_ext.m3u",
    "content": "/This/is/a/path/to_a_file.mp3\n/This/is/another/path/to_a_file.mp3\n"
  },
  {
    "path": "test/rsrc/playlist_windows.m3u8",
    "content": "﻿#EXTM3U\r\nx:\\This\\is\\å\\path\\to_a_file.mp3\r\nx:\\This\\is\\another\\path\\tö_a_file.mp3\r\n"
  },
  {
    "path": "test/rsrc/spotify/album_info.json",
    "content": "{\n    \"album_type\": \"compilation\",\n    \"artists\": [\n      {\n        \"external_urls\": {\n          \"spotify\": \"https://open.spotify.com/artist/0LyfQWJT6nXafLPZqxe9Of\"\n        },\n        \"href\": \"https://api.spotify.com/v1/artists/0LyfQWJT6nXafLPZqxe9Of\",\n        \"id\": \"0LyfQWJT6nXafLPZqxe9Of\",\n        \"name\": \"Various Artists\",\n        \"type\": \"artist\",\n        \"uri\": \"spotify:artist:0LyfQWJT6nXafLPZqxe9Of\"\n      }\n    ],\n    \"available_markets\": [],\n    \"copyrights\": [\n      {\n        \"text\": \"2013 Back Lot Music\",\n        \"type\": \"C\"\n      },\n      {\n        \"text\": \"2013 Back Lot Music\",\n        \"type\": \"P\"\n      }\n    ],\n    \"external_ids\": {\n      \"upc\": \"857970002363\"\n    },\n    \"external_urls\": {\n      \"spotify\": \"https://open.spotify.com/album/5l3zEmMrOhOzG8d8s83GOL\"\n    },\n    \"genres\": [],\n    \"href\": \"https://api.spotify.com/v1/albums/5l3zEmMrOhOzG8d8s83GOL\",\n    \"id\": \"5l3zEmMrOhOzG8d8s83GOL\",\n    \"images\": [\n      {\n        \"height\": 640,\n        \"url\": \"https://i.scdn.co/image/ab67616d0000b27399140a62d43aec760f6172a2\",\n        \"width\": 640\n      },\n      {\n        \"height\": 300,\n        \"url\": \"https://i.scdn.co/image/ab67616d00001e0299140a62d43aec760f6172a2\",\n        \"width\": 300\n      },\n      {\n        \"height\": 64,\n        \"url\": \"https://i.scdn.co/image/ab67616d0000485199140a62d43aec760f6172a2\",\n        \"width\": 64\n      }\n    ],\n    \"label\": \"Back Lot Music\",\n    \"name\": \"Despicable Me 2 (Original Motion Picture Soundtrack)\",\n    \"popularity\": 0,\n    \"release_date\": \"2013-06-18\",\n    \"release_date_precision\": \"day\",\n    \"total_tracks\": 24,\n    \"tracks\": {\n      \"href\": \"https://api.spotify.com/v1/albums/5l3zEmMrOhOzG8d8s83GOL/tracks?offset=0&limit=50\",\n      \"items\": [\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/5nLYd9ST4Cnwy6NHaCxbj8\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/5nLYd9ST4Cnwy6NHaCxbj8\",\n              \"id\": \"5nLYd9ST4Cnwy6NHaCxbj8\",\n              \"name\": \"CeeLo Green\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:5nLYd9ST4Cnwy6NHaCxbj8\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 221805,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/3EiEbQAR44icEkz3rsMI0N\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/3EiEbQAR44icEkz3rsMI0N\",\n          \"id\": \"3EiEbQAR44icEkz3rsMI0N\",\n          \"is_local\": false,\n          \"name\": \"Scream\",\n          \"preview_url\": null,\n          \"track_number\": 1,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:3EiEbQAR44icEkz3rsMI0N\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/3NVrWkcHOtmPbMSvgHmijZ\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/3NVrWkcHOtmPbMSvgHmijZ\",\n              \"id\": \"3NVrWkcHOtmPbMSvgHmijZ\",\n              \"name\": \"The Minions\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:3NVrWkcHOtmPbMSvgHmijZ\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 39065,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/1G4Z91vvEGTYd2ZgOD0MuN\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/1G4Z91vvEGTYd2ZgOD0MuN\",\n          \"id\": \"1G4Z91vvEGTYd2ZgOD0MuN\",\n          \"is_local\": false,\n          \"name\": \"Another Irish Drinking Song\",\n          \"preview_url\": null,\n          \"track_number\": 2,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:1G4Z91vvEGTYd2ZgOD0MuN\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/2RdwBSPQiwcmiDo9kixcl8\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/2RdwBSPQiwcmiDo9kixcl8\",\n              \"id\": \"2RdwBSPQiwcmiDo9kixcl8\",\n              \"name\": \"Pharrell Williams\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:2RdwBSPQiwcmiDo9kixcl8\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 176078,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/7DKqhn3Aa0NT9N9GAcagda\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/7DKqhn3Aa0NT9N9GAcagda\",\n          \"id\": \"7DKqhn3Aa0NT9N9GAcagda\",\n          \"is_local\": false,\n          \"name\": \"Just a Cloud Away\",\n          \"preview_url\": null,\n          \"track_number\": 3,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:7DKqhn3Aa0NT9N9GAcagda\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/2RdwBSPQiwcmiDo9kixcl8\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/2RdwBSPQiwcmiDo9kixcl8\",\n              \"id\": \"2RdwBSPQiwcmiDo9kixcl8\",\n              \"name\": \"Pharrell Williams\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:2RdwBSPQiwcmiDo9kixcl8\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 233305,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/6NPVjNh8Jhru9xOmyQigds\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/6NPVjNh8Jhru9xOmyQigds\",\n          \"id\": \"6NPVjNh8Jhru9xOmyQigds\",\n          \"is_local\": false,\n          \"name\": \"Happy\",\n          \"preview_url\": null,\n          \"track_number\": 4,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:6NPVjNh8Jhru9xOmyQigds\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/3NVrWkcHOtmPbMSvgHmijZ\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/3NVrWkcHOtmPbMSvgHmijZ\",\n              \"id\": \"3NVrWkcHOtmPbMSvgHmijZ\",\n              \"name\": \"The Minions\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:3NVrWkcHOtmPbMSvgHmijZ\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 98211,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/5HSqCeDCn2EEGR5ORwaHA0\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/5HSqCeDCn2EEGR5ORwaHA0\",\n          \"id\": \"5HSqCeDCn2EEGR5ORwaHA0\",\n          \"is_local\": false,\n          \"name\": \"I Swear\",\n          \"preview_url\": null,\n          \"track_number\": 5,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:5HSqCeDCn2EEGR5ORwaHA0\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/3NVrWkcHOtmPbMSvgHmijZ\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/3NVrWkcHOtmPbMSvgHmijZ\",\n              \"id\": \"3NVrWkcHOtmPbMSvgHmijZ\",\n              \"name\": \"The Minions\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:3NVrWkcHOtmPbMSvgHmijZ\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 175291,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/2Ls4QknWvBoGSeAlNKw0Xj\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/2Ls4QknWvBoGSeAlNKw0Xj\",\n          \"id\": \"2Ls4QknWvBoGSeAlNKw0Xj\",\n          \"is_local\": false,\n          \"name\": \"Y.M.C.A.\",\n          \"preview_url\": null,\n          \"track_number\": 6,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:2Ls4QknWvBoGSeAlNKw0Xj\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/2RdwBSPQiwcmiDo9kixcl8\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/2RdwBSPQiwcmiDo9kixcl8\",\n              \"id\": \"2RdwBSPQiwcmiDo9kixcl8\",\n              \"name\": \"Pharrell Williams\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:2RdwBSPQiwcmiDo9kixcl8\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 206105,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/1XkUmKLbm1tzVtrkdj2Ou8\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/1XkUmKLbm1tzVtrkdj2Ou8\",\n          \"id\": \"1XkUmKLbm1tzVtrkdj2Ou8\",\n          \"is_local\": false,\n          \"name\": \"Fun, Fun, Fun\",\n          \"preview_url\": null,\n          \"track_number\": 7,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:1XkUmKLbm1tzVtrkdj2Ou8\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/2RdwBSPQiwcmiDo9kixcl8\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/2RdwBSPQiwcmiDo9kixcl8\",\n              \"id\": \"2RdwBSPQiwcmiDo9kixcl8\",\n              \"name\": \"Pharrell Williams\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:2RdwBSPQiwcmiDo9kixcl8\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 254705,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/42lHGtAZd6xVLC789afLWt\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/42lHGtAZd6xVLC789afLWt\",\n          \"id\": \"42lHGtAZd6xVLC789afLWt\",\n          \"is_local\": false,\n          \"name\": \"Despicable Me\",\n          \"preview_url\": null,\n          \"track_number\": 8,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:42lHGtAZd6xVLC789afLWt\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz\",\n              \"id\": \"2RaHCHhZWBXn460JpMaicz\",\n              \"name\": \"Heitor Pereira\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:2RaHCHhZWBXn460JpMaicz\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 126825,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/7uAC260NViRKyYW4st4vri\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/7uAC260NViRKyYW4st4vri\",\n          \"id\": \"7uAC260NViRKyYW4st4vri\",\n          \"is_local\": false,\n          \"name\": \"PX-41 Labs\",\n          \"preview_url\": null,\n          \"track_number\": 9,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:7uAC260NViRKyYW4st4vri\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz\",\n              \"id\": \"2RaHCHhZWBXn460JpMaicz\",\n              \"name\": \"Heitor Pereira\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:2RaHCHhZWBXn460JpMaicz\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 87118,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/6YLmc6yT7OGiNwbShHuEN2\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/6YLmc6yT7OGiNwbShHuEN2\",\n          \"id\": \"6YLmc6yT7OGiNwbShHuEN2\",\n          \"is_local\": false,\n          \"name\": \"The Fairy Party\",\n          \"preview_url\": null,\n          \"track_number\": 10,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:6YLmc6yT7OGiNwbShHuEN2\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz\",\n              \"id\": \"2RaHCHhZWBXn460JpMaicz\",\n              \"name\": \"Heitor Pereira\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:2RaHCHhZWBXn460JpMaicz\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 339478,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/5lwsXhSXKFoxoGOFLZdQX6\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/5lwsXhSXKFoxoGOFLZdQX6\",\n          \"id\": \"5lwsXhSXKFoxoGOFLZdQX6\",\n          \"is_local\": false,\n          \"name\": \"Lucy And The AVL\",\n          \"preview_url\": null,\n          \"track_number\": 11,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:5lwsXhSXKFoxoGOFLZdQX6\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz\",\n              \"id\": \"2RaHCHhZWBXn460JpMaicz\",\n              \"name\": \"Heitor Pereira\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:2RaHCHhZWBXn460JpMaicz\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 87478,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/2FlWtPuBMGo0a0X7LGETyk\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/2FlWtPuBMGo0a0X7LGETyk\",\n          \"id\": \"2FlWtPuBMGo0a0X7LGETyk\",\n          \"is_local\": false,\n          \"name\": \"Goodbye Nefario\",\n          \"preview_url\": null,\n          \"track_number\": 12,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:2FlWtPuBMGo0a0X7LGETyk\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz\",\n              \"id\": \"2RaHCHhZWBXn460JpMaicz\",\n              \"name\": \"Heitor Pereira\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:2RaHCHhZWBXn460JpMaicz\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 86998,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/3YnhGNADeUaoBTjB1uGUjh\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/3YnhGNADeUaoBTjB1uGUjh\",\n          \"id\": \"3YnhGNADeUaoBTjB1uGUjh\",\n          \"is_local\": false,\n          \"name\": \"Time for Bed\",\n          \"preview_url\": null,\n          \"track_number\": 13,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:3YnhGNADeUaoBTjB1uGUjh\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz\",\n              \"id\": \"2RaHCHhZWBXn460JpMaicz\",\n              \"name\": \"Heitor Pereira\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:2RaHCHhZWBXn460JpMaicz\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 180265,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/6npUKThV4XI20VLW5ryr5O\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/6npUKThV4XI20VLW5ryr5O\",\n          \"id\": \"6npUKThV4XI20VLW5ryr5O\",\n          \"is_local\": false,\n          \"name\": \"Break-In\",\n          \"preview_url\": null,\n          \"track_number\": 14,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:6npUKThV4XI20VLW5ryr5O\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz\",\n              \"id\": \"2RaHCHhZWBXn460JpMaicz\",\n              \"name\": \"Heitor Pereira\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:2RaHCHhZWBXn460JpMaicz\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 95011,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/1qyFlqVfbgyiM7tQ2Jy9vC\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/1qyFlqVfbgyiM7tQ2Jy9vC\",\n          \"id\": \"1qyFlqVfbgyiM7tQ2Jy9vC\",\n          \"is_local\": false,\n          \"name\": \"Stalking Floyd Eaglesan\",\n          \"preview_url\": null,\n          \"track_number\": 15,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:1qyFlqVfbgyiM7tQ2Jy9vC\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz\",\n              \"id\": \"2RaHCHhZWBXn460JpMaicz\",\n              \"name\": \"Heitor Pereira\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:2RaHCHhZWBXn460JpMaicz\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 189771,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/4DRQctGiqjJkbFa7iTK4pb\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/4DRQctGiqjJkbFa7iTK4pb\",\n          \"id\": \"4DRQctGiqjJkbFa7iTK4pb\",\n          \"is_local\": false,\n          \"name\": \"Moving to Australia\",\n          \"preview_url\": null,\n          \"track_number\": 16,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:4DRQctGiqjJkbFa7iTK4pb\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz\",\n              \"id\": \"2RaHCHhZWBXn460JpMaicz\",\n              \"name\": \"Heitor Pereira\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:2RaHCHhZWBXn460JpMaicz\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 85878,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/1TSjM9GY2oN6RO6aYGN25n\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/1TSjM9GY2oN6RO6aYGN25n\",\n          \"id\": \"1TSjM9GY2oN6RO6aYGN25n\",\n          \"is_local\": false,\n          \"name\": \"Going to Save the World\",\n          \"preview_url\": null,\n          \"track_number\": 17,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:1TSjM9GY2oN6RO6aYGN25n\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz\",\n              \"id\": \"2RaHCHhZWBXn460JpMaicz\",\n              \"name\": \"Heitor Pereira\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:2RaHCHhZWBXn460JpMaicz\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 87158,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/3AEMuoglM1myQ8ouIyh8LG\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/3AEMuoglM1myQ8ouIyh8LG\",\n          \"id\": \"3AEMuoglM1myQ8ouIyh8LG\",\n          \"is_local\": false,\n          \"name\": \"El Macho\",\n          \"preview_url\": null,\n          \"track_number\": 18,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:3AEMuoglM1myQ8ouIyh8LG\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz\",\n              \"id\": \"2RaHCHhZWBXn460JpMaicz\",\n              \"name\": \"Heitor Pereira\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:2RaHCHhZWBXn460JpMaicz\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 47438,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/2d7fEVYdZnjlya3MPEma21\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/2d7fEVYdZnjlya3MPEma21\",\n          \"id\": \"2d7fEVYdZnjlya3MPEma21\",\n          \"is_local\": false,\n          \"name\": \"Jillian\",\n          \"preview_url\": null,\n          \"track_number\": 19,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:2d7fEVYdZnjlya3MPEma21\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz\",\n              \"id\": \"2RaHCHhZWBXn460JpMaicz\",\n              \"name\": \"Heitor Pereira\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:2RaHCHhZWBXn460JpMaicz\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 89398,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/7h8WnOo4Fh6NvfTUnR7nOa\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/7h8WnOo4Fh6NvfTUnR7nOa\",\n          \"id\": \"7h8WnOo4Fh6NvfTUnR7nOa\",\n          \"is_local\": false,\n          \"name\": \"Take Her Home\",\n          \"preview_url\": null,\n          \"track_number\": 20,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:7h8WnOo4Fh6NvfTUnR7nOa\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz\",\n              \"id\": \"2RaHCHhZWBXn460JpMaicz\",\n              \"name\": \"Heitor Pereira\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:2RaHCHhZWBXn460JpMaicz\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 212691,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/25A9ZlegjJ0z2fI1PgTqy2\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/25A9ZlegjJ0z2fI1PgTqy2\",\n          \"id\": \"25A9ZlegjJ0z2fI1PgTqy2\",\n          \"is_local\": false,\n          \"name\": \"El Macho's Lair\",\n          \"preview_url\": null,\n          \"track_number\": 21,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:25A9ZlegjJ0z2fI1PgTqy2\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz\",\n              \"id\": \"2RaHCHhZWBXn460JpMaicz\",\n              \"name\": \"Heitor Pereira\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:2RaHCHhZWBXn460JpMaicz\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 117745,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/48GwOCuPhWKDktq3efmfRg\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/48GwOCuPhWKDktq3efmfRg\",\n          \"id\": \"48GwOCuPhWKDktq3efmfRg\",\n          \"is_local\": false,\n          \"name\": \"Home Invasion\",\n          \"preview_url\": null,\n          \"track_number\": 22,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:48GwOCuPhWKDktq3efmfRg\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/2RaHCHhZWBXn460JpMaicz\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/2RaHCHhZWBXn460JpMaicz\",\n              \"id\": \"2RaHCHhZWBXn460JpMaicz\",\n              \"name\": \"Heitor Pereira\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:2RaHCHhZWBXn460JpMaicz\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 443251,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/6dZkl2egcKVm8rO9W7pPWa\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/6dZkl2egcKVm8rO9W7pPWa\",\n          \"id\": \"6dZkl2egcKVm8rO9W7pPWa\",\n          \"is_local\": false,\n          \"name\": \"The Big Battle\",\n          \"preview_url\": null,\n          \"track_number\": 23,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:6dZkl2egcKVm8rO9W7pPWa\"\n        },\n        {\n          \"artists\": [\n            {\n              \"external_urls\": {\n                \"spotify\": \"https://open.spotify.com/artist/3NVrWkcHOtmPbMSvgHmijZ\"\n              },\n              \"href\": \"https://api.spotify.com/v1/artists/3NVrWkcHOtmPbMSvgHmijZ\",\n              \"id\": \"3NVrWkcHOtmPbMSvgHmijZ\",\n              \"name\": \"The Minions\",\n              \"type\": \"artist\",\n              \"uri\": \"spotify:artist:3NVrWkcHOtmPbMSvgHmijZ\"\n            }\n          ],\n          \"available_markets\": [],\n          \"disc_number\": 1,\n          \"duration_ms\": 13886,\n          \"explicit\": false,\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/track/2L0OyiAepqAbKvUZfWovOJ\"\n          },\n          \"href\": \"https://api.spotify.com/v1/tracks/2L0OyiAepqAbKvUZfWovOJ\",\n          \"id\": \"2L0OyiAepqAbKvUZfWovOJ\",\n          \"is_local\": false,\n          \"name\": \"Ba Do Bleep\",\n          \"preview_url\": null,\n          \"track_number\": 24,\n          \"type\": \"track\",\n          \"uri\": \"spotify:track:2L0OyiAepqAbKvUZfWovOJ\"\n        }\n      ],\n      \"limit\": 50,\n      \"next\": null,\n      \"offset\": 0,\n      \"previous\": null,\n      \"total\": 24\n    },\n    \"type\": \"album\",\n    \"uri\": \"spotify:album:5l3zEmMrOhOzG8d8s83GOL\"\n}"
  },
  {
    "path": "test/rsrc/spotify/japanese_track_request.json",
    "content": "{\n    \"tracks\":{\n        \"href\":\"https://api.spotify.com/v1/search?query=Happy+album%3ADespicable+Me+2+artist%3APharrell+Williams&offset=0&limit=20&type=track\",\n        \"items\":[\n            {\n                \"album\":{\n                    \"album_type\":\"compilation\",\n                    \"available_markets\":[\n                        \"AD\", \"AR\", \"AT\", \"AU\", \"BE\", \"BG\", \"BO\", \"BR\", \"CA\",\n                        \"CH\", \"CL\", \"CO\", \"CR\", \"CY\", \"CZ\", \"DE\", \"DK\", \"DO\",\n                        \"EC\", \"EE\", \"ES\", \"FI\", \"FR\", \"GB\", \"GR\", \"GT\", \"HK\",\n                        \"HN\", \"HU\", \"IE\", \"IS\", \"IT\", \"LI\", \"LT\", \"LU\", \"LV\",\n                        \"MC\", \"MT\", \"MX\", \"MY\", \"NI\", \"NL\", \"NO\", \"NZ\", \"PA\",\n                        \"PE\", \"PH\", \"PL\", \"PT\", \"PY\", \"RO\", \"SE\", \"SG\", \"SI\",\n                        \"SK\", \"SV\", \"TR\", \"TW\", \"US\", \"UY\"\n                    ],\n                    \"external_urls\":{\n                        \"spotify\":\"https://open.spotify.com/album/5l3zEmMrOhOzG8d8s83GOL\"\n                    },\n                    \"href\":\"https://api.spotify.com/v1/albums/5l3zEmMrOhOzG8d8s83GOL\",\n                    \"id\":\"5l3zEmMrOhOzG8d8s83GOL\",\n                    \"images\":[\n                        {\n                            \"height\":640,\n                            \"width\":640,\n                            \"url\":\"https://i.scdn.co/image/cb7905340c132365bbaee3f17498f062858382e8\"\n                        },\n                        {\n                            \"height\":300,\n                            \"width\":300,\n                            \"url\":\"https://i.scdn.co/image/af369120f0b20099d6784ab31c88256113f10ffb\"\n                        },\n                        {\n                            \"height\":64,\n                            \"width\":64,\n                            \"url\":\"https://i.scdn.co/image/9dad385ddf2e7db0bef20cec1fcbdb08689d9ae8\"\n                        }\n                    ],\n                    \"name\":\"盗作\",\n                    \"type\":\"album\",\n                    \"uri\":\"spotify:album:5l3zEmMrOhOzG8d8s83GOL\"\n                },\n                \"artists\":[\n                    {\n                        \"external_urls\":{\n                            \"spotify\":\"https://open.spotify.com/artist/2RdwBSPQiwcmiDo9kixcl8\"\n                        },\n                        \"href\":\"https://api.spotify.com/v1/artists/2RdwBSPQiwcmiDo9kixcl8\",\n                        \"id\":\"2RdwBSPQiwcmiDo9kixcl8\",\n                        \"name\":\"ヨルシカ\",\n                        \"type\":\"artist\",\n                        \"uri\":\"spotify:artist:2RdwBSPQiwcmiDo9kixcl8\"\n                    }\n                ],\n                \"available_markets\":[\n                    \"AD\", \"AR\", \"AT\", \"AU\", \"BE\", \"BG\", \"BO\", \"BR\", \"CA\",\n                    \"CH\", \"CL\", \"CO\", \"CR\", \"CY\", \"CZ\", \"DE\", \"DK\", \"DO\",\n                    \"EC\", \"EE\", \"ES\", \"FI\", \"FR\", \"GB\", \"GR\", \"GT\", \"HK\",\n                    \"HN\", \"HU\", \"IE\", \"IS\", \"IT\", \"LI\", \"LT\", \"LU\", \"LV\",\n                    \"MC\", \"MT\", \"MX\", \"MY\", \"NI\", \"NL\", \"NO\", \"NZ\", \"PA\",\n                    \"PE\", \"PH\", \"PL\", \"PT\", \"PY\", \"RO\", \"SE\", \"SG\", \"SI\",\n                    \"SK\", \"SV\", \"TR\", \"TW\", \"US\", \"UY\"\n                ],\n                \"disc_number\":1,\n                \"duration_ms\":233305,\n                \"explicit\":false,\n                \"external_ids\":{\n                    \"isrc\":\"USQ4E1300686\"\n                },\n                \"external_urls\":{\n                    \"spotify\":\"https://open.spotify.com/track/6NPVjNh8Jhru9xOmyQigds\"\n                },\n                \"href\":\"https://api.spotify.com/v1/tracks/6NPVjNh8Jhru9xOmyQigds\",\n                \"id\":\"6NPVjNh8Jhru9xOmyQigds\",\n                \"name\":\"思想犯\",\n                \"popularity\":89,\n                \"preview_url\":\"https://p.scdn.co/mp3-preview/6b00000be293e6b25f61c33e206a0c522b5cbc87\",\n                \"track_number\":4,\n                \"type\":\"track\",\n                \"uri\":\"spotify:track:6NPVjNh8Jhru9xOmyQigds\"\n            }\n        ],\n        \"limit\":20,\n        \"next\":null,\n        \"offset\":0,\n        \"previous\":null,\n        \"total\":1\n    }\n}\n"
  },
  {
    "path": "test/rsrc/spotify/missing_request.json",
    "content": "{\n    \"tracks\" : {\n        \"href\" : \"https://api.spotify.com/v1/search?query=duifhjslkef+album%3Alkajsdflakjsd+artist%3A&offset=0&limit=20&type=track\",\n        \"items\" : [ ],\n        \"limit\" : 20,\n        \"next\" : null,\n        \"offset\" : 0,\n        \"previous\" : null,\n        \"total\" : 0\n    }\n}"
  },
  {
    "path": "test/rsrc/spotify/multiartist_album.json",
    "content": "{\n  \"album_type\": \"single\",\n  \"total_tracks\": 1,\n  \"available_markets\": [\n    \"AR\", \"AU\", \"AT\", \"BE\", \"BO\", \"BR\", \"BG\", \"CA\", \"CL\", \"CO\", \"CR\", \"CY\",\n    \"CZ\", \"DK\", \"DO\", \"DE\", \"EC\", \"EE\", \"SV\", \"FI\", \"FR\", \"GR\", \"GT\", \"HN\",\n    \"HK\", \"HU\", \"IS\", \"IE\", \"IT\", \"LV\", \"LT\", \"LU\", \"MY\", \"MT\", \"MX\", \"NL\",\n    \"NZ\", \"NI\", \"NO\", \"PA\", \"PY\", \"PE\", \"PH\", \"PL\", \"PT\", \"SG\", \"SK\", \"ES\",\n    \"SE\", \"CH\", \"TW\", \"TR\", \"UY\", \"US\", \"GB\", \"AD\", \"LI\", \"MC\", \"ID\", \"JP\",\n    \"TH\", \"VN\", \"RO\", \"IL\", \"ZA\", \"SA\", \"AE\", \"BH\", \"QA\", \"OM\", \"KW\", \"EG\",\n    \"MA\", \"DZ\", \"TN\", \"LB\", \"JO\", \"PS\", \"IN\", \"BY\", \"KZ\", \"MD\", \"UA\", \"AL\",\n    \"BA\", \"HR\", \"ME\", \"MK\", \"RS\", \"SI\", \"KR\", \"BD\", \"PK\", \"LK\", \"GH\", \"KE\",\n    \"NG\", \"TZ\", \"UG\", \"AG\", \"AM\", \"BS\", \"BB\", \"BZ\", \"BT\", \"BW\", \"BF\", \"CV\",\n    \"CW\", \"DM\", \"FJ\", \"GM\", \"GE\", \"GD\", \"GW\", \"GY\", \"HT\", \"JM\", \"KI\", \"LS\",\n    \"LR\", \"MW\", \"MV\", \"ML\", \"MH\", \"FM\", \"NA\", \"NR\", \"NE\", \"PW\", \"PG\", \"PR\",\n    \"WS\", \"SM\", \"ST\", \"SN\", \"SC\", \"SL\", \"SB\", \"KN\", \"LC\", \"VC\", \"SR\", \"TL\",\n    \"TO\", \"TT\", \"TV\", \"VU\", \"AZ\", \"BN\", \"BI\", \"KH\", \"CM\", \"TD\", \"KM\", \"GQ\",\n    \"SZ\", \"GA\", \"GN\", \"KG\", \"LA\", \"MO\", \"MR\", \"MN\", \"NP\", \"RW\", \"TG\", \"UZ\",\n    \"ZW\", \"BJ\", \"MG\", \"MU\", \"MZ\", \"AO\", \"CI\", \"DJ\", \"ZM\", \"CD\", \"CG\", \"IQ\",\n    \"LY\", \"TJ\", \"VE\", \"ET\", \"XK\"\n  ],\n  \"external_urls\": {\n    \"spotify\": \"https://open.spotify.com/album/0yhKyyjyKXWUieJ4w1IAEa\"\n  },\n  \"href\": \"https://api.spotify.com/v1/albums/0yhKyyjyKXWUieJ4w1IAEa\",\n  \"id\": \"0yhKyyjyKXWUieJ4w1IAEa\",\n  \"images\": [\n    {\n      \"url\": \"https://i.scdn.co/image/ab67616d0000b2739a26f5e04909c87cead97c77\",\n      \"height\": 640,\n      \"width\": 640\n    },\n    {\n      \"url\": \"https://i.scdn.co/image/ab67616d00001e029a26f5e04909c87cead97c77\",\n      \"height\": 300,\n      \"width\": 300\n    },\n    {\n      \"url\": \"https://i.scdn.co/image/ab67616d000048519a26f5e04909c87cead97c77\",\n      \"height\": 64,\n      \"width\": 64\n    }\n  ],\n  \"name\": \"Akiba Night\",\n  \"release_date\": \"2017-12-22\",\n  \"release_date_precision\": \"day\",\n  \"type\": \"album\",\n  \"uri\": \"spotify:album:0yhKyyjyKXWUieJ4w1IAEa\",\n  \"artists\": [\n    {\n      \"external_urls\": {\n        \"spotify\": \"https://open.spotify.com/artist/6m8MRXIVKb6wQaPlBIDMr1\"\n      },\n      \"href\": \"https://api.spotify.com/v1/artists/6m8MRXIVKb6wQaPlBIDMr1\",\n      \"id\": \"6m8MRXIVKb6wQaPlBIDMr1\",\n      \"name\": \"Project Skylate\",\n      \"type\": \"artist\",\n      \"uri\": \"spotify:artist:6m8MRXIVKb6wQaPlBIDMr1\"\n    },\n    {\n      \"external_urls\": {\n        \"spotify\": \"https://open.spotify.com/artist/4kkAIoQmNT5xEoNH5BuQLe\"\n      },\n      \"href\": \"https://api.spotify.com/v1/artists/4kkAIoQmNT5xEoNH5BuQLe\",\n      \"id\": \"4kkAIoQmNT5xEoNH5BuQLe\",\n      \"name\": \"Sugar Shrill\",\n      \"type\": \"artist\",\n      \"uri\": \"spotify:artist:4kkAIoQmNT5xEoNH5BuQLe\"\n    }\n  ],\n  \"tracks\": {\n    \"href\": \"https://api.spotify.com/v1/albums/0yhKyyjyKXWUieJ4w1IAEa/tracks?offset=0&limit=50\",\n    \"limit\": 50,\n    \"next\": null,\n    \"offset\": 0,\n    \"previous\": null,\n    \"total\": 1,\n    \"items\": [\n      {\n        \"artists\": [\n          {\n            \"external_urls\": {\n              \"spotify\": \"https://open.spotify.com/artist/6m8MRXIVKb6wQaPlBIDMr1\"\n            },\n            \"href\": \"https://api.spotify.com/v1/artists/6m8MRXIVKb6wQaPlBIDMr1\",\n            \"id\": \"12345\",\n            \"name\": \"Foo\",\n            \"type\": \"artist\",\n            \"uri\": \"spotify:artist:6m8MRXIVKb6wQaPlBIDMr1\"\n          },\n          {\n            \"external_urls\": {\n              \"spotify\": \"https://open.spotify.com/artist/4kkAIoQmNT5xEoNH5BuQLe\"\n            },\n            \"href\": \"https://api.spotify.com/v1/artists/4kkAIoQmNT5xEoNH5BuQLe\",\n            \"id\": \"67890\",\n            \"name\": \"Bar\",\n            \"type\": \"artist\",\n            \"uri\": \"spotify:artist:4kkAIoQmNT5xEoNH5BuQLe\"\n          }\n        ],\n        \"available_markets\": [\n          \"AR\", \"AU\", \"AT\", \"BE\", \"BO\", \"BR\", \"BG\", \"CA\", \"CL\", \"CO\", \"CR\",\n          \"CY\", \"CZ\", \"DK\", \"DO\", \"DE\", \"EC\", \"EE\", \"SV\", \"FI\", \"FR\", \"GR\",\n          \"GT\", \"HN\", \"HK\", \"HU\", \"IS\", \"IE\", \"IT\", \"LV\", \"LT\", \"LU\", \"MY\",\n          \"MT\", \"MX\", \"NL\", \"NZ\", \"NI\", \"NO\", \"PA\", \"PY\", \"PE\", \"PH\", \"PL\",\n          \"PT\", \"SG\", \"SK\", \"ES\", \"SE\", \"CH\", \"TW\", \"TR\", \"UY\", \"US\", \"GB\",\n          \"AD\", \"LI\", \"MC\", \"ID\", \"JP\", \"TH\", \"VN\", \"RO\", \"IL\", \"ZA\", \"SA\",\n          \"AE\", \"BH\", \"QA\", \"OM\", \"KW\", \"EG\", \"MA\", \"DZ\", \"TN\", \"LB\", \"JO\",\n          \"PS\", \"IN\", \"BY\", \"KZ\", \"MD\", \"UA\", \"AL\", \"BA\", \"HR\", \"ME\", \"MK\",\n          \"RS\", \"SI\", \"KR\", \"BD\", \"PK\", \"LK\", \"GH\", \"KE\", \"NG\", \"TZ\", \"UG\",\n          \"AG\", \"AM\", \"BS\", \"BB\", \"BZ\", \"BT\", \"BW\", \"BF\", \"CV\", \"CW\", \"DM\",\n          \"FJ\", \"GM\", \"GE\", \"GD\", \"GW\", \"GY\", \"HT\", \"JM\", \"KI\", \"LS\", \"LR\",\n          \"MW\", \"MV\", \"ML\", \"MH\", \"FM\", \"NA\", \"NR\", \"NE\", \"PW\", \"PG\", \"PR\",\n          \"WS\", \"SM\", \"ST\", \"SN\", \"SC\", \"SL\", \"SB\", \"KN\", \"LC\", \"VC\", \"SR\",\n          \"TL\", \"TO\", \"TT\", \"TV\", \"VU\", \"AZ\", \"BN\", \"BI\", \"KH\", \"CM\", \"TD\",\n          \"KM\", \"GQ\", \"SZ\", \"GA\", \"GN\", \"KG\", \"LA\", \"MO\", \"MR\", \"MN\", \"NP\",\n          \"RW\", \"TG\", \"UZ\", \"ZW\", \"BJ\", \"MG\", \"MU\", \"MZ\", \"AO\", \"CI\", \"DJ\",\n          \"ZM\", \"CD\", \"CG\", \"IQ\", \"LY\", \"TJ\", \"VE\", \"ET\", \"XK\"\n        ],\n        \"disc_number\": 1,\n        \"duration_ms\": 225268,\n        \"explicit\": false,\n        \"external_urls\": {\n          \"spotify\": \"https://open.spotify.com/track/6sjZfVJworBX6TqyjkxIJ1\"\n        },\n        \"href\": \"https://api.spotify.com/v1/tracks/6sjZfVJworBX6TqyjkxIJ1\",\n        \"id\": \"6sjZfVJworBX6TqyjkxIJ1\",\n        \"name\": \"Akiba Nights\",\n        \"preview_url\": \"https://p.scdn.co/mp3-preview/a1c6c0c71f42caff0b19d988849602fefbf7754a?cid=4e414367a1d14c75a5c5129a627fcab8\",\n        \"track_number\": 1,\n        \"type\": \"track\",\n        \"uri\": \"spotify:track:6sjZfVJworBX6TqyjkxIJ1\",\n        \"is_local\": false\n      }\n    ]\n  },\n  \"copyrights\": [\n    {\n      \"text\": \"2017 Sugar Shrill\",\n      \"type\": \"C\"\n    },\n    {\n      \"text\": \"2017 Project Skylate\",\n      \"type\": \"P\"\n    }\n  ],\n  \"external_ids\": {\n    \"upc\": \"5057728789361\"\n  },\n  \"genres\": [],\n  \"label\": \"Project Skylate\",\n  \"popularity\": 21\n}\n"
  },
  {
    "path": "test/rsrc/spotify/multiartist_track.json",
    "content": "{\n  \"album\": {\n    \"album_type\": \"single\",\n    \"artists\": [\n      {\n        \"external_urls\": {\n          \"spotify\": \"https://open.spotify.com/artist/6m8MRXIVKb6wQaPlBIDMr1\"\n        },\n        \"href\": \"https://api.spotify.com/v1/artists/6m8MRXIVKb6wQaPlBIDMr1\",\n        \"id\": \"6m8MRXIVKb6wQaPlBIDMr1\",\n        \"name\": \"Project Skylate\",\n        \"type\": \"artist\",\n        \"uri\": \"spotify:artist:6m8MRXIVKb6wQaPlBIDMr1\"\n      },\n      {\n        \"external_urls\": {\n          \"spotify\": \"https://open.spotify.com/artist/4kkAIoQmNT5xEoNH5BuQLe\"\n        },\n        \"href\": \"https://api.spotify.com/v1/artists/4kkAIoQmNT5xEoNH5BuQLe\",\n        \"id\": \"4kkAIoQmNT5xEoNH5BuQLe\",\n        \"name\": \"Sugar Shrill\",\n        \"type\": \"artist\",\n        \"uri\": \"spotify:artist:4kkAIoQmNT5xEoNH5BuQLe\"\n      }\n    ],\n    \"available_markets\": [\n      \"AR\", \"AU\", \"AT\", \"BE\", \"BO\", \"BR\", \"BG\", \"CA\", \"CL\", \"CO\", \"CR\", \"CY\",\n      \"CZ\", \"DK\", \"DO\", \"DE\", \"EC\", \"EE\", \"SV\", \"FI\", \"FR\", \"GR\", \"GT\", \"HN\",\n      \"HK\", \"HU\", \"IS\", \"IE\", \"IT\", \"LV\", \"LT\", \"LU\", \"MY\", \"MT\", \"MX\", \"NL\",\n      \"NZ\", \"NI\", \"NO\", \"PA\", \"PY\", \"PE\", \"PH\", \"PL\", \"PT\", \"SG\", \"SK\", \"ES\",\n      \"SE\", \"CH\", \"TW\", \"TR\", \"UY\", \"US\", \"GB\", \"AD\", \"LI\", \"MC\", \"ID\", \"JP\",\n      \"TH\", \"VN\", \"RO\", \"IL\", \"ZA\", \"SA\", \"AE\", \"BH\", \"QA\", \"OM\", \"KW\", \"EG\",\n      \"MA\", \"DZ\", \"TN\", \"LB\", \"JO\", \"PS\", \"IN\", \"BY\", \"KZ\", \"MD\", \"UA\", \"AL\",\n      \"BA\", \"HR\", \"ME\", \"MK\", \"RS\", \"SI\", \"KR\", \"BD\", \"PK\", \"LK\", \"GH\", \"KE\",\n      \"NG\", \"TZ\", \"UG\", \"AG\", \"AM\", \"BS\", \"BB\", \"BZ\", \"BT\", \"BW\", \"BF\", \"CV\",\n      \"CW\", \"DM\", \"FJ\", \"GM\", \"GE\", \"GD\", \"GW\", \"GY\", \"HT\", \"JM\", \"KI\", \"LS\",\n      \"LR\", \"MW\", \"MV\", \"ML\", \"MH\", \"FM\", \"NA\", \"NR\", \"NE\", \"PW\", \"PG\", \"PR\",\n      \"WS\", \"SM\", \"ST\", \"SN\", \"SC\", \"SL\", \"SB\", \"KN\", \"LC\", \"VC\", \"SR\", \"TL\",\n      \"TO\", \"TT\", \"TV\", \"VU\", \"AZ\", \"BN\", \"BI\", \"KH\", \"CM\", \"TD\", \"KM\", \"GQ\",\n      \"SZ\", \"GA\", \"GN\", \"KG\", \"LA\", \"MO\", \"MR\", \"MN\", \"NP\", \"RW\", \"TG\", \"UZ\",\n      \"ZW\", \"BJ\", \"MG\", \"MU\", \"MZ\", \"AO\", \"CI\", \"DJ\", \"ZM\", \"CD\", \"CG\", \"IQ\",\n      \"LY\", \"TJ\", \"VE\", \"ET\", \"XK\"\n    ],\n    \"external_urls\": {\n      \"spotify\": \"https://open.spotify.com/album/0yhKyyjyKXWUieJ4w1IAEa\"\n    },\n    \"href\": \"https://api.spotify.com/v1/albums/0yhKyyjyKXWUieJ4w1IAEa\",\n    \"id\": \"0yhKyyjyKXWUieJ4w1IAEa\",\n    \"images\": [\n      {\n        \"url\": \"https://i.scdn.co/image/ab67616d0000b2739a26f5e04909c87cead97c77\",\n        \"width\": 640,\n        \"height\": 640\n      },\n      {\n        \"url\": \"https://i.scdn.co/image/ab67616d00001e029a26f5e04909c87cead97c77\",\n        \"width\": 300,\n        \"height\": 300\n      },\n      {\n        \"url\": \"https://i.scdn.co/image/ab67616d000048519a26f5e04909c87cead97c77\",\n        \"width\": 64,\n        \"height\": 64\n      }\n    ],\n    \"name\": \"Akiba Night\",\n    \"release_date\": \"2017-12-22\",\n    \"release_date_precision\": \"day\",\n    \"total_tracks\": 1,\n    \"type\": \"album\",\n    \"uri\": \"spotify:album:0yhKyyjyKXWUieJ4w1IAEa\"\n  },\n  \"artists\": [\n    {\n      \"external_urls\": {\n        \"spotify\": \"https://open.spotify.com/artist/6m8MRXIVKb6wQaPlBIDMr1\"\n      },\n      \"href\": \"https://api.spotify.com/v1/artists/6m8MRXIVKb6wQaPlBIDMr1\",\n      \"id\": \"12345\",\n      \"name\": \"Foo\",\n      \"type\": \"artist\",\n      \"uri\": \"spotify:artist:6m8MRXIVKb6wQaPlBIDMr1\"\n    },\n    {\n      \"external_urls\": {\n        \"spotify\": \"https://open.spotify.com/artist/4kkAIoQmNT5xEoNH5BuQLe\"\n      },\n      \"href\": \"https://api.spotify.com/v1/artists/4kkAIoQmNT5xEoNH5BuQLe\",\n      \"id\": \"67890\",\n      \"name\": \"Bar\",\n      \"type\": \"artist\",\n      \"uri\": \"spotify:artist:4kkAIoQmNT5xEoNH5BuQLe\"\n    }\n  ],\n  \"available_markets\": [\n    \"AR\", \"AU\", \"AT\", \"BE\", \"BO\", \"BR\", \"BG\", \"CA\", \"CL\", \"CO\", \"CR\", \"CY\",\n    \"CZ\", \"DK\", \"DO\", \"DE\", \"EC\", \"EE\", \"SV\", \"FI\", \"FR\", \"GR\", \"GT\", \"HN\",\n    \"HK\", \"HU\", \"IS\", \"IE\", \"IT\", \"LV\", \"LT\", \"LU\", \"MY\", \"MT\", \"MX\", \"NL\",\n    \"NZ\", \"NI\", \"NO\", \"PA\", \"PY\", \"PE\", \"PH\", \"PL\", \"PT\", \"SG\", \"SK\", \"ES\",\n    \"SE\", \"CH\", \"TW\", \"TR\", \"UY\", \"US\", \"GB\", \"AD\", \"LI\", \"MC\", \"ID\", \"JP\",\n    \"TH\", \"VN\", \"RO\", \"IL\", \"ZA\", \"SA\", \"AE\", \"BH\", \"QA\", \"OM\", \"KW\", \"EG\",\n    \"MA\", \"DZ\", \"TN\", \"LB\", \"JO\", \"PS\", \"IN\", \"BY\", \"KZ\", \"MD\", \"UA\", \"AL\",\n    \"BA\", \"HR\", \"ME\", \"MK\", \"RS\", \"SI\", \"KR\", \"BD\", \"PK\", \"LK\", \"GH\", \"KE\",\n    \"NG\", \"TZ\", \"UG\", \"AG\", \"AM\", \"BS\", \"BB\", \"BZ\", \"BT\", \"BW\", \"BF\", \"CV\",\n    \"CW\", \"DM\", \"FJ\", \"GM\", \"GE\", \"GD\", \"GW\", \"GY\", \"HT\", \"JM\", \"KI\", \"LS\",\n    \"LR\", \"MW\", \"MV\", \"ML\", \"MH\", \"FM\", \"NA\", \"NR\", \"NE\", \"PW\", \"PG\", \"PR\",\n    \"WS\", \"SM\", \"ST\", \"SN\", \"SC\", \"SL\", \"SB\", \"KN\", \"LC\", \"VC\", \"SR\", \"TL\",\n    \"TO\", \"TT\", \"TV\", \"VU\", \"AZ\", \"BN\", \"BI\", \"KH\", \"CM\", \"TD\", \"KM\", \"GQ\",\n    \"SZ\", \"GA\", \"GN\", \"KG\", \"LA\", \"MO\", \"MR\", \"MN\", \"NP\", \"RW\", \"TG\", \"UZ\",\n    \"ZW\", \"BJ\", \"MG\", \"MU\", \"MZ\", \"AO\", \"CI\", \"DJ\", \"ZM\", \"CD\", \"CG\", \"IQ\",\n    \"LY\", \"TJ\", \"VE\", \"ET\", \"XK\"\n  ],\n  \"disc_number\": 1,\n  \"duration_ms\": 225268,\n  \"explicit\": false,\n  \"external_ids\": {\n    \"isrc\": \"GB-SMU-45-66095\"\n  },\n  \"external_urls\": {\n    \"spotify\": \"https://open.spotify.com/track/6sjZfVJworBX6TqyjkxIJ1\"\n  },\n  \"href\": \"https://api.spotify.com/v1/tracks/6sjZfVJworBX6TqyjkxIJ1\",\n  \"id\": \"6sjZfVJworBX6TqyjkxIJ1\",\n  \"is_local\": false,\n  \"name\": \"Akiba Nights\",\n  \"popularity\": 29,\n  \"preview_url\": \"https://p.scdn.co/mp3-preview/a1c6c0c71f42caff0b19d988849602fefbf7754a?cid=4e414367a1d14c75a5c5129a627fcab8\",\n  \"track_number\": 1,\n  \"type\": \"track\",\n  \"uri\": \"spotify:track:6sjZfVJworBX6TqyjkxIJ1\"\n}\n"
  },
  {
    "path": "test/rsrc/spotify/track_info.json",
    "content": "{\n    \"album\": {\n      \"album_type\": \"compilation\",\n      \"artists\": [\n        {\n          \"external_urls\": {\n            \"spotify\": \"https://open.spotify.com/artist/0LyfQWJT6nXafLPZqxe9Of\"\n          },\n          \"href\": \"https://api.spotify.com/v1/artists/0LyfQWJT6nXafLPZqxe9Of\",\n          \"id\": \"0LyfQWJT6nXafLPZqxe9Of\",\n          \"name\": \"Various Artists\",\n          \"type\": \"artist\",\n          \"uri\": \"spotify:artist:0LyfQWJT6nXafLPZqxe9Of\"\n        }\n      ],\n      \"available_markets\": [],\n      \"external_urls\": {\n        \"spotify\": \"https://open.spotify.com/album/5l3zEmMrOhOzG8d8s83GOL\"\n      },\n      \"href\": \"https://api.spotify.com/v1/albums/5l3zEmMrOhOzG8d8s83GOL\",\n      \"id\": \"5l3zEmMrOhOzG8d8s83GOL\",\n      \"images\": [\n        {\n          \"height\": 640,\n          \"url\": \"https://i.scdn.co/image/ab67616d0000b27399140a62d43aec760f6172a2\",\n          \"width\": 640\n        },\n        {\n          \"height\": 300,\n          \"url\": \"https://i.scdn.co/image/ab67616d00001e0299140a62d43aec760f6172a2\",\n          \"width\": 300\n        },\n        {\n          \"height\": 64,\n          \"url\": \"https://i.scdn.co/image/ab67616d0000485199140a62d43aec760f6172a2\",\n          \"width\": 64\n        }\n      ],\n      \"name\": \"Despicable Me 2 (Original Motion Picture Soundtrack)\",\n      \"release_date\": \"2013-06-18\",\n      \"release_date_precision\": \"day\",\n      \"total_tracks\": 24,\n      \"type\": \"album\",\n      \"uri\": \"spotify:album:5l3zEmMrOhOzG8d8s83GOL\"\n    },\n    \"artists\": [\n      {\n        \"external_urls\": {\n          \"spotify\": \"https://open.spotify.com/artist/2RdwBSPQiwcmiDo9kixcl8\"\n        },\n        \"href\": \"https://api.spotify.com/v1/artists/2RdwBSPQiwcmiDo9kixcl8\",\n        \"id\": \"2RdwBSPQiwcmiDo9kixcl8\",\n        \"name\": \"Pharrell Williams\",\n        \"type\": \"artist\",\n        \"uri\": \"spotify:artist:2RdwBSPQiwcmiDo9kixcl8\"\n      }\n    ],\n    \"available_markets\": [],\n    \"disc_number\": 1,\n    \"duration_ms\": 233305,\n    \"explicit\": false,\n    \"external_ids\": {\n      \"isrc\": \"USQ4E1300686\"\n    },\n    \"external_urls\": {\n      \"spotify\": \"https://open.spotify.com/track/6NPVjNh8Jhru9xOmyQigds\"\n    },\n    \"href\": \"https://api.spotify.com/v1/tracks/6NPVjNh8Jhru9xOmyQigds\",\n    \"id\": \"6NPVjNh8Jhru9xOmyQigds\",\n    \"is_local\": false,\n    \"name\": \"Happy\",\n    \"popularity\": 1,\n    \"preview_url\": null,\n    \"track_number\": 4,\n    \"type\": \"track\",\n    \"uri\": \"spotify:track:6NPVjNh8Jhru9xOmyQigds\"\n}"
  },
  {
    "path": "test/rsrc/spotify/track_request.json",
    "content": "{\n    \"tracks\":{\n        \"href\":\"https://api.spotify.com/v1/search?query=Happy+album%3ADespicable+Me+2+artist%3APharrell+Williams&offset=0&limit=20&type=track\",\n        \"items\":[\n            {\n                \"album\":{\n                    \"album_type\":\"compilation\",\n                    \"available_markets\":[\n                        \"AD\", \"AR\", \"AT\", \"AU\", \"BE\", \"BG\", \"BO\", \"BR\", \"CA\",\n                        \"CH\", \"CL\", \"CO\", \"CR\", \"CY\", \"CZ\", \"DE\", \"DK\", \"DO\",\n                        \"EC\", \"EE\", \"ES\", \"FI\", \"FR\", \"GB\", \"GR\", \"GT\", \"HK\",\n                        \"HN\", \"HU\", \"IE\", \"IS\", \"IT\", \"LI\", \"LT\", \"LU\", \"LV\",\n                        \"MC\", \"MT\", \"MX\", \"MY\", \"NI\", \"NL\", \"NO\", \"NZ\", \"PA\",\n                        \"PE\", \"PH\", \"PL\", \"PT\", \"PY\", \"RO\", \"SE\", \"SG\", \"SI\",\n                        \"SK\", \"SV\", \"TR\", \"TW\", \"US\", \"UY\"\n                    ],\n                    \"external_urls\":{\n                        \"spotify\":\"https://open.spotify.com/album/5l3zEmMrOhOzG8d8s83GOL\"\n                    },\n                    \"href\":\"https://api.spotify.com/v1/albums/5l3zEmMrOhOzG8d8s83GOL\",\n                    \"id\":\"5l3zEmMrOhOzG8d8s83GOL\",\n                    \"images\":[\n                        {\n                            \"height\":640,\n                            \"width\":640,\n                            \"url\":\"https://i.scdn.co/image/cb7905340c132365bbaee3f17498f062858382e8\"\n                        },\n                        {\n                            \"height\":300,\n                            \"width\":300,\n                            \"url\":\"https://i.scdn.co/image/af369120f0b20099d6784ab31c88256113f10ffb\"\n                        },\n                        {\n                            \"height\":64,\n                            \"width\":64,\n                            \"url\":\"https://i.scdn.co/image/9dad385ddf2e7db0bef20cec1fcbdb08689d9ae8\"\n                        }\n                    ],\n                    \"name\":\"Despicable Me 2 (Original Motion Picture Soundtrack)\",\n                    \"type\":\"album\",\n                    \"uri\":\"spotify:album:5l3zEmMrOhOzG8d8s83GOL\"\n                },\n                \"artists\":[\n                    {\n                        \"external_urls\":{\n                            \"spotify\":\"https://open.spotify.com/artist/2RdwBSPQiwcmiDo9kixcl8\"\n                        },\n                        \"href\":\"https://api.spotify.com/v1/artists/2RdwBSPQiwcmiDo9kixcl8\",\n                        \"id\":\"2RdwBSPQiwcmiDo9kixcl8\",\n                        \"name\":\"Pharrell Williams\",\n                        \"type\":\"artist\",\n                        \"uri\":\"spotify:artist:2RdwBSPQiwcmiDo9kixcl8\"\n                    }\n                ],\n                \"available_markets\":[\n                    \"AD\", \"AR\", \"AT\", \"AU\", \"BE\", \"BG\", \"BO\", \"BR\", \"CA\",\n                    \"CH\", \"CL\", \"CO\", \"CR\", \"CY\", \"CZ\", \"DE\", \"DK\", \"DO\",\n                    \"EC\", \"EE\", \"ES\", \"FI\", \"FR\", \"GB\", \"GR\", \"GT\", \"HK\",\n                    \"HN\", \"HU\", \"IE\", \"IS\", \"IT\", \"LI\", \"LT\", \"LU\", \"LV\",\n                    \"MC\", \"MT\", \"MX\", \"MY\", \"NI\", \"NL\", \"NO\", \"NZ\", \"PA\",\n                    \"PE\", \"PH\", \"PL\", \"PT\", \"PY\", \"RO\", \"SE\", \"SG\", \"SI\",\n                    \"SK\", \"SV\", \"TR\", \"TW\", \"US\", \"UY\"\n                ],\n                \"disc_number\":1,\n                \"duration_ms\":233305,\n                \"explicit\":false,\n                \"external_ids\":{\n                    \"isrc\":\"USQ4E1300686\"\n                },\n                \"external_urls\":{\n                    \"spotify\":\"https://open.spotify.com/track/6NPVjNh8Jhru9xOmyQigds\"\n                },\n                \"href\":\"https://api.spotify.com/v1/tracks/6NPVjNh8Jhru9xOmyQigds\",\n                \"id\":\"6NPVjNh8Jhru9xOmyQigds\",\n                \"name\":\"Happy\",\n                \"popularity\":89,\n                \"preview_url\":\"https://p.scdn.co/mp3-preview/6b00000be293e6b25f61c33e206a0c522b5cbc87\",\n                \"track_number\":4,\n                \"type\":\"track\",\n                \"uri\":\"spotify:track:6NPVjNh8Jhru9xOmyQigds\"\n            }\n        ],\n        \"limit\":20,\n        \"next\":null,\n        \"offset\":0,\n        \"previous\":null,\n        \"total\":1\n    }\n}\n"
  },
  {
    "path": "test/rsrc/test_completion.sh",
    "content": "# Function stub\ncompopt() { return 0; }\n\ninitcli() {\n  COMP_WORDS=( \"beet\" \"$@\" )\n  let COMP_CWORD=${#COMP_WORDS[@]}-1\n  COMP_LINE=\"${COMP_WORDS[@]}\"\n  let COMP_POINT=${#COMP_LINE}\n  _beet\n}\n\ncompletes() {\n  for word in \"$@\"; do\n    [[ \" ${COMPREPLY[@]} \" == *[[:space:]]$word[[:space:]]* ]] || return 1\n  done\n}\n\nCOMMANDS='fields import list update remove\n          stats version modify move write\n          help'\n\nHELP_OPTS='-h --help'\n\n\ntest_commands() {\n  initcli '' &&\n  completes $COMMANDS &&\n\n  initcli -v '' &&\n  completes $COMMANDS &&\n\n  initcli -l help '' &&\n  completes $COMMANDS &&\n\n  initcli -d list '' &&\n  completes $COMMANDS &&\n\n  initcli -h '' &&\n  completes $COMMANDS &&\n  true\n}\n\ntest_command_aliases() {\n  initcli ls &&\n  completes list &&\n\n  initcli l &&\n  ! completes ls &&\n\n  initcli im &&\n  completes import &&\n  true\n}\n\ntest_global_opts() {\n  initcli - &&\n  completes \\\n    -l --library \\\n    -d --directory \\\n    -h --help \\\n    -c --config \\\n    -v --verbose &&\n  true\n}\n\n\ntest_global_file_opts() {\n  # FIXME somehow file completion only works when the completion\n  # function is called by the shell completion utilities. So we can't\n  # test it here\n  initcli --library '' &&\n  completes $(compgen -d) &&\n\n  initcli -l '' &&\n  completes $(compgen -d) &&\n\n  initcli --config '' &&\n  completes $(compgen -d) &&\n\n  initcli -c '' &&\n  completes $(compgen -d) &&\n  true\n}\n\n\ntest_global_dir_opts() {\n  initcli --directory '' &&\n  completes $(compgen -d) &&\n\n  initcli -d '' &&\n  completes $(compgen -d) &&\n  true\n}\n\n\ntest_fields_command() {\n  initcli fields - &&\n  completes -h --help &&\n\n  initcli fields '' &&\n  completes $(compgen -d) &&\n  true\n}\n\n\ntest_import_files() {\n  initcli import '' &&\n  completes $(compgen -d) &&\n\n  initcli import --copy -P '' &&\n  completes $(compgen -d) &&\n\n  initcli import --log '' &&\n  completes $(compgen -d) &&\n  true\n}\n\n\ntest_import_options() {\n  initcli imp -\n  completes \\\n    -h --help \\\n    -c --copy -C --nocopy \\\n    -w --write -W --nowrite \\\n    -a --autotag -A --noautotag \\\n    -p --resume -P --noresume \\\n    -l --log --flat\n}\n\n\ntest_list_options() {\n  initcli list -\n  completes \\\n    -h --help \\\n    -a --album \\\n    -p --path\n}\n\ntest_list_query() {\n  initcli list 'x' &&\n  [[ -z \"${COMPREPLY[@]}\" ]] &&\n\n  initcli list 'art' &&\n  completes \\\n      'artist:' \\\n      'artpath:' &&\n\n  initcli list 'artits:x' &&\n  [[ -z \"${COMPREPLY[@]}\" ]] &&\n  true\n}\n\ntest_help_command() {\n  initcli help '' &&\n  completes $COMMANDS &&\n  true\n}\n\ntest_plugin_command() {\n  initcli te &&\n  completes test &&\n\n  initcli test - &&\n  completes -o --option &&\n  true\n}\n\nrun_tests() {\n  local tests=$(set | \\\n    grep --extended-regexp --only-matching '^test_[a-zA-Z_]* \\(\\) $' |\\\n    grep --extended-regexp --only-matching '[a-zA-Z_]*'\n  )\n  local fail=0\n\n  if  [[ -n $@ ]]; then\n    tests=\"$@\"\n  fi\n\n  for t in $tests; do\n    $t || { fail=1 && echo \"$t failed\" >&2; }\n  done\n  return $fail\n}\n\nrun_tests \"$@\" && echo \"completion tests passed\"\n"
  },
  {
    "path": "test/test_art_resize.py",
    "content": "# This file is part of beets.\n# Copyright 2020, David Swarbrick.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for image resizing based on filesize.\"\"\"\n\nimport os\nimport unittest\nfrom pathlib import Path\nfrom unittest.mock import patch\n\nfrom beets.test import _common\nfrom beets.test.helper import BeetsTestCase, CleanupModulesMixin\nfrom beets.util import command_output, syspath\nfrom beets.util.artresizer import IMBackend, PILBackend\n\n\nclass DummyIMBackend(IMBackend):\n    \"\"\"An `IMBackend` which pretends that ImageMagick is available.\n\n    The version is sufficiently recent to support image comparison.\n    \"\"\"\n\n    def __init__(self):\n        \"\"\"Init a dummy backend class for mocked ImageMagick tests.\"\"\"\n        self.version = (7, 0, 0)\n        self.legacy = False\n        self.convert_cmd = [\"magick\"]\n        self.identify_cmd = [\"magick\", \"identify\"]\n        self.compare_cmd = [\"magick\", \"compare\"]\n\n\nclass DummyPILBackend(PILBackend):\n    \"\"\"An `PILBackend` which pretends that PIL is available.\"\"\"\n\n    def __init__(self):\n        \"\"\"Init a dummy backend class for mocked PIL tests.\"\"\"\n        pass\n\n\nclass ArtResizerFileSizeTest(CleanupModulesMixin, BeetsTestCase):\n    \"\"\"Unittest test case for Art Resizer to a specific filesize.\"\"\"\n\n    modules = (IMBackend.__module__,)\n\n    IMG_225x225 = os.path.join(_common.RSRC, b\"abbey.jpg\")\n    IMG_225x225_SIZE = os.stat(syspath(IMG_225x225)).st_size\n\n    def _test_img_resize(self, backend):\n        \"\"\"Test resizing based on file size, given a resize_func.\"\"\"\n        # Check quality setting unaffected by new parameter\n        im_95_qual = backend.resize(\n            225,\n            self.IMG_225x225,\n            quality=95,\n            max_filesize=0,\n        )\n        # check valid path returned - max_filesize hasn't broken resize command\n        assert Path(os.fsdecode(im_95_qual)).exists()\n\n        # Attempt a lower filesize with same quality\n        im_a = backend.resize(\n            225,\n            self.IMG_225x225,\n            quality=95,\n            max_filesize=0.9 * os.stat(syspath(im_95_qual)).st_size,\n        )\n        assert Path(os.fsdecode(im_a)).exists()\n        # target size was achieved\n        assert (\n            os.stat(syspath(im_a)).st_size\n            < os.stat(syspath(im_95_qual)).st_size\n        )\n\n        # Attempt with lower initial quality\n        im_75_qual = backend.resize(\n            225,\n            self.IMG_225x225,\n            quality=75,\n            max_filesize=0,\n        )\n        assert Path(os.fsdecode(im_75_qual)).exists()\n\n        im_b = backend.resize(\n            225,\n            self.IMG_225x225,\n            quality=95,\n            max_filesize=0.9 * os.stat(syspath(im_75_qual)).st_size,\n        )\n        assert Path(os.fsdecode(im_b)).exists()\n        # Check high (initial) quality still gives a smaller filesize\n        assert (\n            os.stat(syspath(im_b)).st_size\n            < os.stat(syspath(im_75_qual)).st_size\n        )\n\n    @unittest.skipUnless(PILBackend.available(), \"PIL not available\")\n    def test_pil_file_resize(self):\n        \"\"\"Test PIL resize function is lowering file size.\"\"\"\n        self._test_img_resize(PILBackend())\n\n    @unittest.skipUnless(IMBackend.available(), \"ImageMagick not available\")\n    def test_im_file_resize(self):\n        \"\"\"Test IM resize function is lowering file size.\"\"\"\n        self._test_img_resize(IMBackend())\n\n    @unittest.skipUnless(PILBackend.available(), \"PIL not available\")\n    def test_pil_file_deinterlace(self):\n        \"\"\"Test PIL deinterlace function.\n\n        Check if the `PILBackend.deinterlace()` function returns images\n        that are non-progressive\n        \"\"\"\n        path = PILBackend().deinterlace(self.IMG_225x225)\n        from PIL import Image\n\n        with Image.open(path) as img:\n            assert \"progression\" not in img.info\n\n    @unittest.skipUnless(IMBackend.available(), \"ImageMagick not available\")\n    def test_im_file_deinterlace(self):\n        \"\"\"Test ImageMagick deinterlace function.\n\n        Check if the `IMBackend.deinterlace()` function returns images\n        that are non-progressive.\n        \"\"\"\n        im = IMBackend()\n        path = im.deinterlace(self.IMG_225x225)\n        cmd = [\n            *im.identify_cmd,\n            \"-format\",\n            \"%[interlace]\",\n            syspath(path, prefix=False),\n        ]\n        out = command_output(cmd).stdout\n        assert out == b\"None\"\n\n    @patch(\"beets.util.artresizer.util\")\n    def test_write_metadata_im(self, mock_util):\n        \"\"\"Test writing image metadata.\"\"\"\n        metadata = {\"a\": \"A\", \"b\": \"B\"}\n        im = DummyIMBackend()\n        im.write_metadata(\"foo\", metadata)\n        command = [*im.convert_cmd, *\"foo -set a A -set b B foo\".split()]\n        mock_util.command_output.assert_called_once_with(command)\n"
  },
  {
    "path": "test/test_datequery.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Test for dbcore's date-based queries.\"\"\"\n\nimport time\nimport unittest\nfrom datetime import datetime, timedelta\n\nimport pytest\n\nfrom beets.dbcore.query import (\n    DateInterval,\n    DateQuery,\n    InvalidQueryArgumentValueError,\n    _parse_periods,\n)\nfrom beets.test.helper import ItemInDBTestCase\n\n\nclass TestDateInterval:\n    now = datetime.now().replace(microsecond=0, second=0).isoformat()\n\n    @pytest.mark.parametrize(\n        \"pattern, datestr, include\",\n        [\n            # year precision\n            (\"2000..2001\", \"2000-01-01T00:00:00\", True),\n            (\"2000..2001\", \"2001-06-20T14:15:16\", True),\n            (\"2000..2001\", \"2001-12-31T23:59:59\", True),\n            (\"2000..2001\", \"1999-12-31T23:59:59\", False),\n            (\"2000..2001\", \"2002-01-01T00:00:00\", False),\n            (\"2000..\", \"2000-01-01T00:00:00\", True),\n            (\"2000..\", \"2099-10-11T00:00:00\", True),\n            (\"2000..\", \"1999-12-31T23:59:59\", False),\n            (\"..2001\", \"2001-12-31T23:59:59\", True),\n            (\"..2001\", \"2002-01-01T00:00:00\", False),\n            (\"-1d..1d\", now, True),\n            (\"-2d..-1d\", now, False),\n            # month precision\n            (\"2000-06-20..2000-06-20\", \"2000-06-20T00:00:00\", True),\n            (\"2000-06-20..2000-06-20\", \"2000-06-20T10:20:30\", True),\n            (\"2000-06-20..2000-06-20\", \"2000-06-20T23:59:59\", True),\n            (\"2000-06-20..2000-06-20\", \"2000-06-19T23:59:59\", False),\n            (\"2000-06-20..2000-06-20\", \"2000-06-21T00:00:00\", False),\n            # day precision\n            (\"1999-12..2000-02\", \"1999-12-01T00:00:00\", True),\n            (\"1999-12..2000-02\", \"2000-02-15T05:06:07\", True),\n            (\"1999-12..2000-02\", \"2000-02-29T23:59:59\", True),\n            (\"1999-12..2000-02\", \"1999-11-30T23:59:59\", False),\n            (\"1999-12..2000-02\", \"2000-03-01T00:00:00\", False),\n            # hour precision with 'T' separator\n            (\"2000-01-01T12..2000-01-01T13\", \"2000-01-01T11:59:59\", False),\n            (\"2000-01-01T12..2000-01-01T13\", \"2000-01-01T12:00:00\", True),\n            (\"2000-01-01T12..2000-01-01T13\", \"2000-01-01T12:30:00\", True),\n            (\"2000-01-01T12..2000-01-01T13\", \"2000-01-01T13:30:00\", True),\n            (\"2000-01-01T12..2000-01-01T13\", \"2000-01-01T13:59:59\", True),\n            (\"2000-01-01T12..2000-01-01T13\", \"2000-01-01T14:00:00\", False),\n            (\"2000-01-01T12..2000-01-01T13\", \"2000-01-01T14:30:00\", False),\n            # hour precision non-range query\n            (\"2008-12-01T22\", \"2008-12-01T22:30:00\", True),\n            (\"2008-12-01T22\", \"2008-12-01T23:30:00\", False),\n            # minute precision\n            (\"2000-01-01T12:30..2000-01-01T12:31\", \"2000-01-01T12:29:59\", False),\n            (\"2000-01-01T12:30..2000-01-01T12:31\", \"2000-01-01T12:30:00\", True),\n            (\"2000-01-01T12:30..2000-01-01T12:31\", \"2000-01-01T12:30:30\", True),\n            (\"2000-01-01T12:30..2000-01-01T12:31\", \"2000-01-01T12:31:59\", True),\n            (\"2000-01-01T12:30..2000-01-01T12:31\", \"2000-01-01T12:32:00\", False),\n            # second precision\n            (\"2000-01-01T12:30:50..2000-01-01T12:30:55\", \"2000-01-01T12:30:49\", False),\n            (\"2000-01-01T12:30:50..2000-01-01T12:30:55\", \"2000-01-01T12:30:50\", True),\n            (\"2000-01-01T12:30:50..2000-01-01T12:30:55\", \"2000-01-01T12:30:55\", True),\n            (\"2000-01-01T12:30:50..2000-01-01T12:30:55\", \"2000-01-01T12:30:56\", False), # unbounded  # noqa: E501\n            (\"..\", datetime.max.isoformat(), True),\n            (\"..\", datetime.min.isoformat(), True),\n            (\"..\", \"1000-01-01T00:00:00\", True),\n        ],\n    )  # fmt: skip\n    def test_intervals(self, pattern, datestr, include):\n        (start, end) = _parse_periods(pattern)\n        interval = DateInterval.from_periods(start, end)\n        assert interval.contains(datetime.fromisoformat(datestr)) == include\n\n\ndef _parsetime(s):\n    return time.mktime(datetime.strptime(s, \"%Y-%m-%d %H:%M\").timetuple())\n\n\nclass DateQueryTest(ItemInDBTestCase):\n    def setUp(self):\n        super().setUp()\n        self.i.added = _parsetime(\"2013-03-30 22:21\")\n        self.i.store()\n\n    def test_single_month_match_fast(self):\n        query = DateQuery(\"added\", \"2013-03\")\n        matched = self.lib.items(query)\n        assert len(matched) == 1\n\n    def test_single_month_nonmatch_fast(self):\n        query = DateQuery(\"added\", \"2013-04\")\n        matched = self.lib.items(query)\n        assert len(matched) == 0\n\n    def test_single_month_match_slow(self):\n        query = DateQuery(\"added\", \"2013-03\")\n        assert query.match(self.i)\n\n    def test_single_month_nonmatch_slow(self):\n        query = DateQuery(\"added\", \"2013-04\")\n        assert not query.match(self.i)\n\n    def test_single_day_match_fast(self):\n        query = DateQuery(\"added\", \"2013-03-30\")\n        matched = self.lib.items(query)\n        assert len(matched) == 1\n\n    def test_single_day_nonmatch_fast(self):\n        query = DateQuery(\"added\", \"2013-03-31\")\n        matched = self.lib.items(query)\n        assert len(matched) == 0\n\n\nclass DateQueryTestRelative(ItemInDBTestCase):\n    def setUp(self):\n        super().setUp()\n\n        # We pick a date near a month changeover, which can reveal some time\n        # zone bugs.\n        self._now = datetime(2017, 12, 31, 22, 55, 4, 101332)\n\n        self.i.added = _parsetime(self._now.strftime(\"%Y-%m-%d %H:%M\"))\n        self.i.store()\n\n    def test_single_month_match_fast(self):\n        query = DateQuery(\"added\", self._now.strftime(\"%Y-%m\"))\n        matched = self.lib.items(query)\n        assert len(matched) == 1\n\n    def test_single_month_nonmatch_fast(self):\n        query = DateQuery(\n            \"added\", (self._now + timedelta(days=30)).strftime(\"%Y-%m\")\n        )\n        matched = self.lib.items(query)\n        assert len(matched) == 0\n\n    def test_single_month_match_slow(self):\n        query = DateQuery(\"added\", self._now.strftime(\"%Y-%m\"))\n        assert query.match(self.i)\n\n    def test_single_month_nonmatch_slow(self):\n        query = DateQuery(\n            \"added\", (self._now + timedelta(days=30)).strftime(\"%Y-%m\")\n        )\n        assert not query.match(self.i)\n\n    def test_single_day_match_fast(self):\n        query = DateQuery(\"added\", self._now.strftime(\"%Y-%m-%d\"))\n        matched = self.lib.items(query)\n        assert len(matched) == 1\n\n    def test_single_day_nonmatch_fast(self):\n        query = DateQuery(\n            \"added\", (self._now + timedelta(days=1)).strftime(\"%Y-%m-%d\")\n        )\n        matched = self.lib.items(query)\n        assert len(matched) == 0\n\n\nclass DateQueryTestRelativeMore(ItemInDBTestCase):\n    def setUp(self):\n        super().setUp()\n        self.i.added = _parsetime(datetime.now().strftime(\"%Y-%m-%d %H:%M\"))\n        self.i.store()\n\n    def test_relative(self):\n        for timespan in [\"d\", \"w\", \"m\", \"y\"]:\n            query = DateQuery(\"added\", f\"-4{timespan}..+4{timespan}\")\n            matched = self.lib.items(query)\n            assert len(matched) == 1\n\n    def test_relative_fail(self):\n        for timespan in [\"d\", \"w\", \"m\", \"y\"]:\n            query = DateQuery(\"added\", f\"-2{timespan}..-1{timespan}\")\n            matched = self.lib.items(query)\n            assert len(matched) == 0\n\n    def test_start_relative(self):\n        for timespan in [\"d\", \"w\", \"m\", \"y\"]:\n            query = DateQuery(\"added\", f\"-4{timespan}..\")\n            matched = self.lib.items(query)\n            assert len(matched) == 1\n\n    def test_start_relative_fail(self):\n        for timespan in [\"d\", \"w\", \"m\", \"y\"]:\n            query = DateQuery(\"added\", f\"4{timespan}..\")\n            matched = self.lib.items(query)\n            assert len(matched) == 0\n\n    def test_end_relative(self):\n        for timespan in [\"d\", \"w\", \"m\", \"y\"]:\n            query = DateQuery(\"added\", f\"..+4{timespan}\")\n            matched = self.lib.items(query)\n            assert len(matched) == 1\n\n    def test_end_relative_fail(self):\n        for timespan in [\"d\", \"w\", \"m\", \"y\"]:\n            query = DateQuery(\"added\", f\"..-4{timespan}\")\n            matched = self.lib.items(query)\n            assert len(matched) == 0\n\n\nclass DateQueryConstructTest(unittest.TestCase):\n    def test_long_numbers(self):\n        with pytest.raises(InvalidQueryArgumentValueError):\n            DateQuery(\"added\", \"1409830085..1412422089\")\n\n    def test_too_many_components(self):\n        with pytest.raises(InvalidQueryArgumentValueError):\n            DateQuery(\"added\", \"12-34-56-78\")\n\n    def test_invalid_date_query(self):\n        q_list = [\n            \"2001-01-0a\",\n            \"2001-0a\",\n            \"200a\",\n            \"2001-01-01..2001-01-0a\",\n            \"2001-0a..2001-01\",\n            \"200a..2002\",\n            \"20aa..\",\n            \"..2aa\",\n        ]\n        for q in q_list:\n            with pytest.raises(InvalidQueryArgumentValueError):\n                DateQuery(\"added\", q)\n\n    def test_datetime_uppercase_t_separator(self):\n        date_query = DateQuery(\"added\", \"2000-01-01T12\")\n        assert date_query.interval.start == datetime(2000, 1, 1, 12)\n        assert date_query.interval.end == datetime(2000, 1, 1, 13)\n\n    def test_datetime_lowercase_t_separator(self):\n        date_query = DateQuery(\"added\", \"2000-01-01t12\")\n        assert date_query.interval.start == datetime(2000, 1, 1, 12)\n        assert date_query.interval.end == datetime(2000, 1, 1, 13)\n\n    def test_datetime_space_separator(self):\n        date_query = DateQuery(\"added\", \"2000-01-01 12\")\n        assert date_query.interval.start == datetime(2000, 1, 1, 12)\n        assert date_query.interval.end == datetime(2000, 1, 1, 13)\n\n    def test_datetime_invalid_separator(self):\n        with pytest.raises(InvalidQueryArgumentValueError):\n            DateQuery(\"added\", \"2000-01-01x12\")\n"
  },
  {
    "path": "test/test_dbcore.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for the DBCore database abstraction.\"\"\"\n\nimport os\nimport shutil\nimport sqlite3\nimport unittest\nfrom tempfile import mkstemp\nfrom typing import ClassVar\n\nimport pytest\n\nfrom beets import dbcore\nfrom beets.dbcore.db import DBCustomFunctionError, Index\nfrom beets.library import LibModel\nfrom beets.test import _common\nfrom beets.util import cached_classproperty\n\n# Fixture: concrete database and model classes. For migration tests, we\n# have multiple models with different numbers of fields.\n\n\n@pytest.fixture\ndef db(model):\n    db = model(\":memory:\")\n    yield db\n    db._connection().close()\n\n\nclass SortFixture(dbcore.query.FieldSort):\n    pass\n\n\nclass QueryFixture(dbcore.query.FieldQuery):\n    def __init__(self, pattern):\n        self.pattern = pattern\n\n    def clause(self):\n        return None, ()\n\n    def match(self):\n        return True\n\n\nclass ModelFixture1(LibModel):\n    _table = \"test\"\n    _flex_table = \"testflex\"\n    _fields: ClassVar[dict[str, dbcore.types.Type]] = {\n        \"id\": dbcore.types.PRIMARY_ID,\n        \"field_one\": dbcore.types.INTEGER,\n        \"field_two\": dbcore.types.STRING,\n    }\n\n    _sorts: ClassVar[dict[str, type[dbcore.query.FieldSort]]] = {\n        \"some_sort\": SortFixture,\n    }\n    _indices = (Index(\"field_one_index\", (\"field_one\",)),)\n\n    @cached_classproperty\n    def _types(cls):\n        return {\n            \"some_float_field\": dbcore.types.FLOAT,\n        }\n\n    @cached_classproperty\n    def _queries(cls):\n        return {\n            \"some_query\": QueryFixture,\n        }\n\n    @classmethod\n    def _getters(cls):\n        return {}\n\n    def _template_funcs(self):\n        return {}\n\n\nclass DatabaseFixture1(dbcore.Database):\n    _models = (ModelFixture1,)\n\n\nclass ModelFixture2(ModelFixture1):\n    _fields: ClassVar[dict[str, dbcore.types.Type]] = {\n        \"id\": dbcore.types.PRIMARY_ID,\n        \"field_one\": dbcore.types.INTEGER,\n        \"field_two\": dbcore.types.INTEGER,\n    }\n\n\nclass DatabaseFixture2(dbcore.Database):\n    _models = (ModelFixture2,)\n\n\nclass ModelFixture3(ModelFixture1):\n    _fields: ClassVar[dict[str, dbcore.types.Type]] = {\n        \"id\": dbcore.types.PRIMARY_ID,\n        \"field_one\": dbcore.types.INTEGER,\n        \"field_two\": dbcore.types.INTEGER,\n        \"field_three\": dbcore.types.INTEGER,\n    }\n\n\nclass DatabaseFixture3(dbcore.Database):\n    _models = (ModelFixture3,)\n\n\nclass ModelFixture4(ModelFixture1):\n    _fields: ClassVar[dict[str, dbcore.types.Type]] = {\n        \"id\": dbcore.types.PRIMARY_ID,\n        \"field_one\": dbcore.types.INTEGER,\n        \"field_two\": dbcore.types.INTEGER,\n        \"field_three\": dbcore.types.INTEGER,\n        \"field_four\": dbcore.types.INTEGER,\n    }\n\n\nclass DatabaseFixture4(dbcore.Database):\n    _models = (ModelFixture4,)\n\n\nclass AnotherModelFixture(ModelFixture1):\n    _table = \"another\"\n    _flex_table = \"anotherflex\"\n    _fields: ClassVar[dict[str, dbcore.types.Type]] = {\n        \"id\": dbcore.types.PRIMARY_ID,\n        \"foo\": dbcore.types.INTEGER,\n    }\n    _indices = (Index(\"another_foo_index\", (\"foo\",)),)\n\n\nclass ModelFixture5(ModelFixture1):\n    _fields: ClassVar[dict[str, dbcore.types.Type]] = {\n        \"some_string_field\": dbcore.types.STRING,\n        \"some_float_field\": dbcore.types.FLOAT,\n        \"some_boolean_field\": dbcore.types.BOOLEAN,\n    }\n\n\nclass DatabaseFixture5(dbcore.Database):\n    _models = (ModelFixture5,)\n\n\nclass DatabaseFixtureTwoModels(dbcore.Database):\n    _models = (ModelFixture2, AnotherModelFixture)\n\n\nclass ModelFixtureWithGetters(dbcore.Model):\n    @classmethod\n    def _getters(cls):\n        return {\"aComputedField\": (lambda s: \"thing\")}\n\n    def _template_funcs(self):\n        return {}\n\n\n@_common.slow_test()\nclass MigrationTest(unittest.TestCase):\n    \"\"\"Tests the ability to change the database schema between\n    versions.\n    \"\"\"\n\n    @classmethod\n    def setUpClass(cls):\n        handle, cls.orig_libfile = mkstemp(\"orig_db\")\n        os.close(handle)\n        # Set up a database with the two-field schema.\n        old_lib = DatabaseFixture2(cls.orig_libfile)\n\n        # Add an item to the old library.\n        old_lib._connection().execute(\n            \"insert into test (field_one, field_two) values (4, 2)\"\n        )\n        old_lib._connection().commit()\n        old_lib._connection().close()\n        del old_lib\n\n    @classmethod\n    def tearDownClass(cls):\n        os.remove(cls.orig_libfile)\n\n    def setUp(self):\n        handle, self.libfile = mkstemp(\"db\")\n        os.close(handle)\n        shutil.copyfile(self.orig_libfile, self.libfile)\n\n    def tearDown(self):\n        os.remove(self.libfile)\n\n    def test_open_with_same_fields_leaves_untouched(self):\n        new_lib = DatabaseFixture2(self.libfile)\n        c = new_lib._connection().cursor()\n        c.execute(\"select * from test\")\n        row = c.fetchone()\n        c.connection.close()\n        assert len(row.keys()) == len(ModelFixture2._fields)\n\n    def test_open_with_new_field_adds_column(self):\n        new_lib = DatabaseFixture3(self.libfile)\n        c = new_lib._connection().cursor()\n        c.execute(\"select * from test\")\n        row = c.fetchone()\n        c.connection.close()\n        assert len(row.keys()) == len(ModelFixture3._fields)\n\n    def test_open_with_fewer_fields_leaves_untouched(self):\n        new_lib = DatabaseFixture1(self.libfile)\n        c = new_lib._connection().cursor()\n        c.execute(\"select * from test\")\n        row = c.fetchone()\n        c.connection.close()\n        assert len(row.keys()) == len(ModelFixture2._fields)\n\n    def test_open_with_multiple_new_fields(self):\n        new_lib = DatabaseFixture4(self.libfile)\n        c = new_lib._connection().cursor()\n        c.execute(\"select * from test\")\n        row = c.fetchone()\n        c.connection.close()\n        assert len(row.keys()) == len(ModelFixture4._fields)\n\n    def test_extra_model_adds_table(self):\n        new_lib = DatabaseFixtureTwoModels(self.libfile)\n        try:\n            c = new_lib._connection()\n            c.execute(\"select * from another\")\n            c.close()\n        except sqlite3.OperationalError:\n            self.fail(\"select failed\")\n\n    def test_index_creation(self):\n        \"\"\"Test that declared indices are created on database initialization.\"\"\"\n        db = DatabaseFixture1(\":memory:\")\n        with db.transaction() as tx:\n            rows = tx.query(\"PRAGMA index_info(field_one_index)\")\n            assert len(rows) > 0  # Index exists\n        db._connection().close()\n\n\nclass TransactionTest(unittest.TestCase):\n    def setUp(self):\n        self.db = DatabaseFixture1(\":memory:\")\n\n    def tearDown(self):\n        self.db._connection().close()\n\n    def test_mutate_increase_revision(self):\n        old_rev = self.db.revision\n        with self.db.transaction() as tx:\n            tx.mutate(\n                f\"INSERT INTO {ModelFixture1._table} (field_one) VALUES (?);\",\n                (111,),\n            )\n        assert self.db.revision > old_rev\n\n    def test_query_no_increase_revision(self):\n        old_rev = self.db.revision\n        with self.db.transaction() as tx:\n            tx.query(f\"PRAGMA table_info({ModelFixture1._table})\")\n        assert self.db.revision == old_rev\n\n\nclass ModelTest(unittest.TestCase):\n    def setUp(self):\n        self.db = DatabaseFixture1(\":memory:\")\n\n    def tearDown(self):\n        self.db._connection().close()\n\n    def test_add_model(self):\n        model = ModelFixture1()\n        model.add(self.db)\n        rows = self.db._connection().execute(\"select * from test\").fetchall()\n        assert len(rows) == 1\n\n    def test_store_fixed_field(self):\n        model = ModelFixture1()\n        model.add(self.db)\n        model.field_one = 123\n        model.store()\n        row = self.db._connection().execute(\"select * from test\").fetchone()\n        assert row[\"field_one\"] == 123\n\n    def test_revision(self):\n        old_rev = self.db.revision\n        model = ModelFixture1()\n        model.add(self.db)\n        model.store()\n        assert model._revision == self.db.revision\n        assert self.db.revision > old_rev\n\n        mid_rev = self.db.revision\n        model2 = ModelFixture1()\n        model2.add(self.db)\n        model2.store()\n        assert model2._revision > mid_rev\n        assert self.db.revision > model._revision\n\n        # revision changed, so the model should be re-loaded\n        model.load()\n        assert model._revision == self.db.revision\n\n        # revision did not change, so no reload\n        mod2_old_rev = model2._revision\n        model2.load()\n        assert model2._revision == mod2_old_rev\n\n    def test_retrieve_by_id(self):\n        model = ModelFixture1()\n        model.add(self.db)\n        other_model = self.db._get(ModelFixture1, model.id)\n        assert model.id == other_model.id\n\n    def test_store_and_retrieve_flexattr(self):\n        model = ModelFixture1()\n        model.add(self.db)\n        model.foo = \"bar\"\n        model.store()\n\n        other_model = self.db._get(ModelFixture1, model.id)\n        assert other_model.foo == \"bar\"\n\n    def test_delete_flexattr(self):\n        model = ModelFixture1()\n        model[\"foo\"] = \"bar\"\n        assert \"foo\" in model\n        del model[\"foo\"]\n        assert \"foo\" not in model\n\n    def test_delete_flexattr_via_dot(self):\n        model = ModelFixture1()\n        model[\"foo\"] = \"bar\"\n        assert \"foo\" in model\n        del model.foo\n        assert \"foo\" not in model\n\n    def test_delete_flexattr_persists(self):\n        model = ModelFixture1()\n        model.add(self.db)\n        model.foo = \"bar\"\n        model.store()\n\n        model = self.db._get(ModelFixture1, model.id)\n        del model[\"foo\"]\n        model.store()\n\n        model = self.db._get(ModelFixture1, model.id)\n        assert \"foo\" not in model\n\n    def test_delete_non_existent_attribute(self):\n        model = ModelFixture1()\n        with pytest.raises(KeyError):\n            del model[\"foo\"]\n\n    def test_delete_fixed_attribute(self):\n        model = ModelFixture5()\n        model.some_string_field = \"foo\"\n        model.some_float_field = 1.23\n        model.some_boolean_field = True\n\n        for field, type_ in model._fields.items():\n            assert model[field] != type_.null\n\n        for field, type_ in model._fields.items():\n            del model[field]\n            assert model[field] == type_.null\n\n    def test_null_value_normalization_by_type(self):\n        model = ModelFixture1()\n        model.field_one = None\n        assert model.field_one == 0\n\n    def test_null_value_stays_none_for_untyped_field(self):\n        model = ModelFixture1()\n        model.foo = None\n        assert model.foo is None\n\n    def test_normalization_for_typed_flex_fields(self):\n        model = ModelFixture1()\n        model.some_float_field = None\n        assert model.some_float_field == 0.0\n\n    def test_load_deleted_flex_field(self):\n        model1 = ModelFixture1()\n        model1[\"flex_field\"] = True\n        model1.add(self.db)\n\n        model2 = self.db._get(ModelFixture1, model1.id)\n        assert \"flex_field\" in model2\n\n        del model1[\"flex_field\"]\n        model1.store()\n\n        model2.load()\n        assert \"flex_field\" not in model2\n\n    def test_check_db_fails(self):\n        with pytest.raises(ValueError, match=\"no database\"):\n            dbcore.Model()._check_db()\n        with pytest.raises(ValueError, match=\"no id\"):\n            ModelFixture1(self.db)._check_db()\n\n        dbcore.Model(self.db)._check_db(need_id=False)\n\n    def test_missing_field(self):\n        with pytest.raises(AttributeError):\n            ModelFixture1(self.db).nonExistingKey\n\n    def test_computed_field(self):\n        model = ModelFixtureWithGetters()\n        assert model.aComputedField == \"thing\"\n        with pytest.raises(KeyError, match=r\"computed field .+ deleted\"):\n            del model.aComputedField\n\n    def test_items(self):\n        model = ModelFixture1(self.db)\n        model.id = 5\n        assert {(\"id\", 5), (\"field_one\", 0), (\"field_two\", \"\")} == set(\n            model.items()\n        )\n\n    def test_delete_internal_field(self):\n        model = dbcore.Model()\n        del model._db\n        with pytest.raises(AttributeError):\n            model._db\n\n    def test_parse_nonstring(self):\n        with pytest.raises(TypeError, match=\"must be a string\"):\n            dbcore.Model._parse(None, 42)\n\n    def test_pickle_dump(self):\n        \"\"\"Tries to pickle an item. This tests the __getstate__ method\n        of the Model ABC\"\"\"\n        import pickle\n\n        model = ModelFixture1(self.db)\n        model.add(self.db)\n        model.field_one = 123\n\n        model.store()\n        assert model._db is not None\n\n        pickle.dumps(model)\n\n\nclass FormatTest(unittest.TestCase):\n    def test_format_fixed_field_integer(self):\n        model = ModelFixture1()\n        model.field_one = 155\n        value = model.formatted().get(\"field_one\")\n        assert value == \"155\"\n\n    def test_format_fixed_field_integer_normalized(self):\n        \"\"\"The normalize method of the Integer class rounds floats\"\"\"\n        model = ModelFixture1()\n        model.field_one = 142.432\n        value = model.formatted().get(\"field_one\")\n        assert value == \"142\"\n\n        model.field_one = 142.863\n        value = model.formatted().get(\"field_one\")\n        assert value == \"143\"\n\n    def test_format_fixed_field_string(self):\n        model = ModelFixture1()\n        model.field_two = \"caf\\xe9\"\n        value = model.formatted().get(\"field_two\")\n        assert value == \"caf\\xe9\"\n\n    def test_format_flex_field(self):\n        model = ModelFixture1()\n        model.other_field = \"caf\\xe9\"\n        value = model.formatted().get(\"other_field\")\n        assert value == \"caf\\xe9\"\n\n    def test_format_flex_field_bytes(self):\n        model = ModelFixture1()\n        model.other_field = \"caf\\xe9\".encode()\n        value = model.formatted().get(\"other_field\")\n        assert isinstance(value, str)\n        assert value == \"caf\\xe9\"\n\n    def test_format_unset_field(self):\n        model = ModelFixture1()\n        value = model.formatted().get(\"other_field\")\n        assert value == \"\"\n\n    def test_format_typed_flex_field(self):\n        model = ModelFixture1()\n        model.some_float_field = 3.14159265358979\n        value = model.formatted().get(\"some_float_field\")\n        assert value == \"3.1\"\n\n\nclass FormattedMappingTest(unittest.TestCase):\n    def test_keys_equal_model_keys(self):\n        model = ModelFixture1()\n        formatted = model.formatted()\n        assert set(model.keys(True)) == set(formatted.keys())\n\n    def test_get_unset_field(self):\n        model = ModelFixture1()\n        formatted = model.formatted()\n        with pytest.raises(KeyError):\n            formatted[\"other_field\"]\n\n    def test_get_method_with_default(self):\n        model = ModelFixture1()\n        formatted = model.formatted()\n        assert formatted.get(\"other_field\") == \"\"\n\n    def test_get_method_with_specified_default(self):\n        model = ModelFixture1()\n        formatted = model.formatted()\n        assert formatted.get(\"other_field\", \"default\") == \"default\"\n\n\nclass ParseTest(unittest.TestCase):\n    def test_parse_fixed_field(self):\n        value = ModelFixture1._parse(\"field_one\", \"2\")\n        assert isinstance(value, int)\n        assert value == 2\n\n    def test_parse_flex_field(self):\n        value = ModelFixture1._parse(\"some_float_field\", \"2\")\n        assert isinstance(value, float)\n        assert value == 2.0\n\n    def test_parse_untyped_field(self):\n        value = ModelFixture1._parse(\"field_nine\", \"2\")\n        assert value == \"2\"\n\n\nclass QueryParseTest(unittest.TestCase):\n    def pqp(self, part):\n        return dbcore.queryparse.parse_query_part(\n            part,\n            {\"year\": dbcore.query.NumericQuery},\n            {\":\": dbcore.query.RegexpQuery},\n        )[:-1]  # remove the negate flag\n\n    def test_one_basic_term(self):\n        q = \"test\"\n        r = (None, \"test\", dbcore.query.SubstringQuery)\n        assert self.pqp(q) == r\n\n    def test_one_keyed_term(self):\n        q = \"test:val\"\n        r = (\"test\", \"val\", dbcore.query.SubstringQuery)\n        assert self.pqp(q) == r\n\n    def test_colon_at_end(self):\n        q = \"test:\"\n        r = (\"test\", \"\", dbcore.query.SubstringQuery)\n        assert self.pqp(q) == r\n\n    def test_one_basic_regexp(self):\n        q = r\":regexp\"\n        r = (None, \"regexp\", dbcore.query.RegexpQuery)\n        assert self.pqp(q) == r\n\n    def test_keyed_regexp(self):\n        q = r\"test::regexp\"\n        r = (\"test\", \"regexp\", dbcore.query.RegexpQuery)\n        assert self.pqp(q) == r\n\n    def test_escaped_colon(self):\n        q = r\"test\\:val\"\n        r = (None, \"test:val\", dbcore.query.SubstringQuery)\n        assert self.pqp(q) == r\n\n    def test_escaped_colon_in_regexp(self):\n        q = r\":test\\:regexp\"\n        r = (None, \"test:regexp\", dbcore.query.RegexpQuery)\n        assert self.pqp(q) == r\n\n    def test_single_year(self):\n        q = \"year:1999\"\n        r = (\"year\", \"1999\", dbcore.query.NumericQuery)\n        assert self.pqp(q) == r\n\n    def test_multiple_years(self):\n        q = \"year:1999..2010\"\n        r = (\"year\", \"1999..2010\", dbcore.query.NumericQuery)\n        assert self.pqp(q) == r\n\n    def test_empty_query_part(self):\n        q = \"\"\n        r = (None, \"\", dbcore.query.SubstringQuery)\n        assert self.pqp(q) == r\n\n\nclass QueryFromStringsTest(unittest.TestCase):\n    def qfs(self, strings):\n        return dbcore.queryparse.query_from_strings(\n            dbcore.query.AndQuery,\n            ModelFixture1,\n            {\":\": dbcore.query.RegexpQuery},\n            strings,\n        )\n\n    def test_zero_parts(self):\n        q = self.qfs([])\n        assert isinstance(q, dbcore.query.AndQuery)\n        assert len(q.subqueries) == 1\n        assert isinstance(q.subqueries[0], dbcore.query.TrueQuery)\n\n    def test_two_parts(self):\n        q = self.qfs([\"foo\", \"bar:baz\"])\n        assert isinstance(q, dbcore.query.AndQuery)\n        assert len(q.subqueries) == 2\n        assert isinstance(q.subqueries[0], dbcore.query.OrQuery)\n        assert isinstance(q.subqueries[1], dbcore.query.SubstringQuery)\n\n    def test_parse_fixed_type_query(self):\n        q = self.qfs([\"field_one:2..3\"])\n        assert isinstance(q.subqueries[0], dbcore.query.NumericQuery)\n\n    def test_parse_flex_type_query(self):\n        q = self.qfs([\"some_float_field:2..3\"])\n        assert isinstance(q.subqueries[0], dbcore.query.NumericQuery)\n\n    def test_empty_query_part(self):\n        q = self.qfs([\"\"])\n        assert isinstance(q.subqueries[0], dbcore.query.TrueQuery)\n\n\nclass SortFromStringsTest(unittest.TestCase):\n    def sfs(self, strings):\n        return dbcore.queryparse.sort_from_strings(\n            ModelFixture1,\n            strings,\n        )\n\n    def test_zero_parts(self):\n        s = self.sfs([])\n        assert isinstance(s, dbcore.query.NullSort)\n        assert s == dbcore.query.NullSort()\n\n    def test_one_parts(self):\n        s = self.sfs([\"field+\"])\n        assert isinstance(s, dbcore.query.Sort)\n\n    def test_two_parts(self):\n        s = self.sfs([\"field+\", \"another_field-\"])\n        assert isinstance(s, dbcore.query.MultipleSort)\n        assert len(s.sorts) == 2\n\n    def test_fixed_field_sort(self):\n        s = self.sfs([\"field_one+\"])\n        assert isinstance(s, dbcore.query.FixedFieldSort)\n        assert s == dbcore.query.FixedFieldSort(\"field_one\")\n\n    def test_flex_field_sort(self):\n        s = self.sfs([\"flex_field+\"])\n        assert isinstance(s, dbcore.query.SlowFieldSort)\n        assert s == dbcore.query.SlowFieldSort(\"flex_field\")\n\n    def test_special_sort(self):\n        s = self.sfs([\"some_sort+\"])\n        assert isinstance(s, SortFixture)\n\n\nclass ParseSortedQueryTest(unittest.TestCase):\n    def psq(self, parts):\n        return dbcore.parse_sorted_query(\n            ModelFixture1,\n            parts.split(),\n        )\n\n    def test_and_query(self):\n        q, s = self.psq(\"foo bar\")\n        assert isinstance(q, dbcore.query.AndQuery)\n        assert isinstance(s, dbcore.query.NullSort)\n        assert len(q.subqueries) == 2\n\n    def test_or_query(self):\n        q, s = self.psq(\"foo , bar\")\n        assert isinstance(q, dbcore.query.OrQuery)\n        assert isinstance(s, dbcore.query.NullSort)\n        assert len(q.subqueries) == 2\n\n    def test_no_space_before_comma_or_query(self):\n        q, s = self.psq(\"foo, bar\")\n        assert isinstance(q, dbcore.query.OrQuery)\n        assert isinstance(s, dbcore.query.NullSort)\n        assert len(q.subqueries) == 2\n\n    def test_no_spaces_or_query(self):\n        q, s = self.psq(\"foo,bar\")\n        assert isinstance(q, dbcore.query.AndQuery)\n        assert isinstance(s, dbcore.query.NullSort)\n        assert len(q.subqueries) == 1\n\n    def test_trailing_comma_or_query(self):\n        q, s = self.psq(\"foo , bar ,\")\n        assert isinstance(q, dbcore.query.OrQuery)\n        assert isinstance(s, dbcore.query.NullSort)\n        assert len(q.subqueries) == 3\n\n    def test_leading_comma_or_query(self):\n        q, s = self.psq(\", foo , bar\")\n        assert isinstance(q, dbcore.query.OrQuery)\n        assert isinstance(s, dbcore.query.NullSort)\n        assert len(q.subqueries) == 3\n\n    def test_only_direction(self):\n        q, s = self.psq(\"-\")\n        assert isinstance(q, dbcore.query.AndQuery)\n        assert isinstance(s, dbcore.query.NullSort)\n        assert len(q.subqueries) == 1\n\n\nclass ResultsIteratorTest(unittest.TestCase):\n    def setUp(self):\n        self.db = DatabaseFixture1(\":memory:\")\n        model = ModelFixture1()\n        model[\"foo\"] = \"baz\"\n        model.add(self.db)\n        model = ModelFixture1()\n        model[\"foo\"] = \"bar\"\n        model.add(self.db)\n\n    def tearDown(self):\n        self.db._connection().close()\n\n    def test_iterate_once(self):\n        objs = self.db._fetch(ModelFixture1)\n        assert len(list(objs)) == 2\n\n    def test_iterate_twice(self):\n        objs = self.db._fetch(ModelFixture1)\n        list(objs)\n        assert len(list(objs)) == 2\n\n    def test_concurrent_iterators(self):\n        results = self.db._fetch(ModelFixture1)\n        it1 = iter(results)\n        it2 = iter(results)\n        next(it1)\n        list(it2)\n        assert len(list(it1)) == 1\n\n    def test_slow_query(self):\n        q = dbcore.query.SubstringQuery(\"foo\", \"ba\", False)\n        objs = self.db._fetch(ModelFixture1, q)\n        assert len(list(objs)) == 2\n\n    def test_slow_query_negative(self):\n        q = dbcore.query.SubstringQuery(\"foo\", \"qux\", False)\n        objs = self.db._fetch(ModelFixture1, q)\n        assert len(list(objs)) == 0\n\n    def test_iterate_slow_sort(self):\n        s = dbcore.query.SlowFieldSort(\"foo\")\n        res = self.db._fetch(ModelFixture1, sort=s)\n        objs = list(res)\n        assert objs[0].foo == \"bar\"\n        assert objs[1].foo == \"baz\"\n\n    def test_unsorted_subscript(self):\n        objs = self.db._fetch(ModelFixture1)\n        assert objs[0].foo == \"baz\"\n        assert objs[1].foo == \"bar\"\n\n    def test_slow_sort_subscript(self):\n        s = dbcore.query.SlowFieldSort(\"foo\")\n        objs = self.db._fetch(ModelFixture1, sort=s)\n        assert objs[0].foo == \"bar\"\n        assert objs[1].foo == \"baz\"\n\n    def test_length(self):\n        objs = self.db._fetch(ModelFixture1)\n        assert len(objs) == 2\n\n    def test_out_of_range(self):\n        objs = self.db._fetch(ModelFixture1)\n        with pytest.raises(IndexError):\n            objs[100]\n\n    def test_no_results(self):\n        assert (\n            self.db._fetch(ModelFixture1, dbcore.query.FalseQuery()).get()\n            is None\n        )\n\n\nclass TestException:\n    @pytest.mark.parametrize(\"model\", [DatabaseFixture1])\n    @pytest.mark.filterwarnings(\n        \"ignore: .*plz_raise.*: pytest.PytestUnraisableExceptionWarning\"\n    )\n    @pytest.mark.filterwarnings(\n        \"error: .*: pytest.PytestUnraisableExceptionWarning\"\n    )\n    def test_custom_function_error(self, db: DatabaseFixture1):\n        def plz_raise():\n            raise Exception(\"i haz raized\")\n\n        db._connection().create_function(\"plz_raise\", 0, plz_raise)\n\n        with db.transaction() as tx:\n            tx.mutate(\"insert into test (field_one) values (1)\")\n\n        with pytest.raises(DBCustomFunctionError):\n            with db.transaction() as tx:\n                tx.query(\"select * from test where plz_raise()\")\n"
  },
  {
    "path": "test/test_files.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Test file manipulation functionality of Item.\"\"\"\n\nimport os\nimport shutil\nimport stat\nimport unittest\nfrom os.path import join\nfrom pathlib import Path\n\nimport pytest\n\nimport beets.library\nfrom beets import util\nfrom beets.test import _common\nfrom beets.test._common import item, touch\nfrom beets.test.helper import NEEDS_REFLINK, BeetsTestCase\nfrom beets.util import MoveOperation, syspath\n\n\nclass MoveTest(BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n\n        # make a temporary file\n        self.temp_music_file_name = \"temp.mp3\"\n        self.path = self.temp_dir_path / self.temp_music_file_name\n        shutil.copy(self.resource_path, self.path)\n\n        # add it to a temporary library\n        self.i = beets.library.Item.from_path(self.path)\n        self.lib.add(self.i)\n\n        # set up the destination\n        self.lib.path_formats = [\n            (\"default\", join(\"$artist\", \"$album\", \"$title\"))\n        ]\n        self.i.artist = \"one\"\n        self.i.album = \"two\"\n        self.i.title = \"three\"\n        self.dest = self.lib_path / \"one\" / \"two\" / \"three.mp3\"\n\n        self.otherdir = self.temp_dir_path / \"testotherdir\"\n\n    def test_move_arrives(self):\n        self.i.move()\n        assert self.dest.exists()\n\n    def test_move_to_custom_dir(self):\n        self.i.move(basedir=os.fsencode(self.otherdir))\n        assert (self.otherdir / \"one\" / \"two\" / \"three.mp3\").exists()\n\n    def test_move_departs(self):\n        self.i.move()\n        assert not self.path.exists()\n\n    def test_move_in_lib_prunes_empty_dir(self):\n        self.i.move()\n        old_path = self.i.filepath\n        assert old_path.exists()\n\n        self.i.artist = \"newArtist\"\n        self.i.move()\n        assert not old_path.exists()\n        assert not old_path.parent.exists()\n\n    def test_copy_arrives(self):\n        self.i.move(operation=MoveOperation.COPY)\n        assert self.dest.exists()\n\n    def test_copy_does_not_depart(self):\n        self.i.move(operation=MoveOperation.COPY)\n        assert self.path.exists()\n\n    def test_reflink_arrives(self):\n        self.i.move(operation=MoveOperation.REFLINK_AUTO)\n        assert self.dest.exists()\n\n    def test_reflink_does_not_depart(self):\n        self.i.move(operation=MoveOperation.REFLINK_AUTO)\n        assert self.path.exists()\n\n    @NEEDS_REFLINK\n    def test_force_reflink_arrives(self):\n        self.i.move(operation=MoveOperation.REFLINK)\n        assert self.dest.exists()\n\n    @NEEDS_REFLINK\n    def test_force_reflink_does_not_depart(self):\n        self.i.move(operation=MoveOperation.REFLINK)\n        assert self.path.exists()\n\n    def test_move_changes_path(self):\n        self.i.move()\n        assert self.i.path == util.normpath(self.dest)\n\n    def test_copy_already_at_destination(self):\n        self.i.move()\n        old_path = self.i.path\n        self.i.move(operation=MoveOperation.COPY)\n        assert self.i.path == old_path\n\n    def test_move_already_at_destination(self):\n        self.i.move()\n        old_path = self.i.path\n        self.i.move()\n        assert self.i.path == old_path\n\n    def test_move_file_with_colon(self):\n        self.i.artist = \"C:DOS\"\n        self.i.move()\n        assert \"C_DOS\" in self.i.path.decode()\n\n    def test_move_file_with_multiple_colons(self):\n        # print(beets.config[\"replace\"])\n        self.i.artist = \"COM:DOS\"\n        self.i.move()\n        assert \"COM_DOS\" in self.i.path.decode()\n\n    def test_move_file_with_colon_alt_separator(self):\n        old = beets.config[\"drive_sep_replace\"]\n        beets.config[\"drive_sep_replace\"] = \"0\"\n        self.i.artist = \"C:DOS\"\n        self.i.move()\n        assert \"C0DOS\" in self.i.path.decode()\n        beets.config[\"drive_sep_replace\"] = old\n\n    def test_read_only_file_copied_writable(self):\n        # Make the source file read-only.\n        os.chmod(syspath(self.path), 0o444)\n\n        try:\n            self.i.move(operation=MoveOperation.COPY)\n            assert os.access(syspath(self.i.path), os.W_OK)\n        finally:\n            # Make everything writable so it can be cleaned up.\n            os.chmod(syspath(self.path), 0o777)\n            os.chmod(syspath(self.i.path), 0o777)\n\n    def test_move_avoids_collision_with_existing_file(self):\n        # Make a conflicting file at the destination.\n        dest = self.i.destination()\n        os.makedirs(syspath(os.path.dirname(dest)))\n        touch(dest)\n\n        self.i.move()\n        assert self.i.path != dest\n        assert os.path.dirname(self.i.path) == os.path.dirname(dest)\n\n    @unittest.skipUnless(_common.HAVE_SYMLINK, \"need symlinks\")\n    def test_link_arrives(self):\n        self.i.move(operation=MoveOperation.LINK)\n        assert self.dest.exists()\n        assert os.path.islink(syspath(self.dest))\n        assert self.dest.resolve() == self.path.resolve()\n\n    @unittest.skipUnless(_common.HAVE_SYMLINK, \"need symlinks\")\n    def test_link_does_not_depart(self):\n        self.i.move(operation=MoveOperation.LINK)\n        assert self.path.exists()\n\n    @unittest.skipUnless(_common.HAVE_SYMLINK, \"need symlinks\")\n    def test_link_changes_path(self):\n        self.i.move(operation=MoveOperation.LINK)\n        assert self.i.path == util.normpath(self.dest)\n\n    @unittest.skipUnless(_common.HAVE_HARDLINK, \"need hardlinks\")\n    def test_hardlink_arrives(self):\n        self.i.move(operation=MoveOperation.HARDLINK)\n        assert self.dest.exists()\n        s1 = os.stat(syspath(self.path))\n        s2 = os.stat(syspath(self.dest))\n        assert (s1[stat.ST_INO], s1[stat.ST_DEV]) == (\n            s2[stat.ST_INO],\n            s2[stat.ST_DEV],\n        )\n\n    @unittest.skipUnless(_common.HAVE_HARDLINK, \"need hardlinks\")\n    def test_hardlink_does_not_depart(self):\n        self.i.move(operation=MoveOperation.HARDLINK)\n        assert self.path.exists()\n\n    @unittest.skipUnless(_common.HAVE_HARDLINK, \"need hardlinks\")\n    def test_hardlink_changes_path(self):\n        self.i.move(operation=MoveOperation.HARDLINK)\n        assert self.i.path == util.normpath(self.dest)\n\n    @unittest.skipUnless(_common.HAVE_HARDLINK, \"need hardlinks\")\n    def test_hardlink_from_symlink(self):\n        link_path = join(self.temp_dir, b\"temp_link.mp3\")\n        link_source = join(\"./\", self.temp_music_file_name)\n        os.symlink(syspath(link_source), syspath(link_path))\n        self.i.path = link_path\n        self.i.move(operation=MoveOperation.HARDLINK)\n\n        s1 = os.stat(syspath(self.path))\n        s2 = os.stat(syspath(self.dest))\n        assert (s1[stat.ST_INO], s1[stat.ST_DEV]) == (\n            s2[stat.ST_INO],\n            s2[stat.ST_DEV],\n        )\n\n\nclass HelperTest(unittest.TestCase):\n    def test_ancestry_works_on_file(self):\n        p = \"/a/b/c\"\n        a = [\"/\", \"/a\", \"/a/b\"]\n        assert util.ancestry(p) == a\n\n    def test_ancestry_works_on_dir(self):\n        p = \"/a/b/c/\"\n        a = [\"/\", \"/a\", \"/a/b\", \"/a/b/c\"]\n        assert util.ancestry(p) == a\n\n    def test_ancestry_works_on_relative(self):\n        p = \"a/b/c\"\n        a = [\"a\", \"a/b\"]\n        assert util.ancestry(p) == a\n\n    def test_components_works_on_file(self):\n        p = \"/a/b/c\"\n        a = [\"/\", \"a\", \"b\", \"c\"]\n        assert util.components(p) == a\n\n    def test_components_works_on_dir(self):\n        p = \"/a/b/c/\"\n        a = [\"/\", \"a\", \"b\", \"c\"]\n        assert util.components(p) == a\n\n    def test_components_works_on_relative(self):\n        p = \"a/b/c\"\n        a = [\"a\", \"b\", \"c\"]\n        assert util.components(p) == a\n\n    def test_forward_slash(self):\n        p = rb\"C:\\a\\b\\c\"\n        a = rb\"C:/a/b/c\"\n        assert util.path_as_posix(p) == a\n\n\nclass AlbumFileTest(BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n\n        # Make library and item.\n        self.lib.path_formats = [\n            (\"default\", join(\"$albumartist\", \"$album\", \"$title\"))\n        ]\n        self.i = item(self.lib)\n        # Make a file for the item.\n        self.i.path = self.i.destination()\n        util.mkdirall(self.i.path)\n        touch(self.i.path)\n        # Make an album.\n        self.ai = self.lib.add_album((self.i,))\n        # Alternate destination dir.\n        self.otherdir = os.path.join(self.temp_dir, b\"testotherdir\")\n\n    def test_albuminfo_move_changes_paths(self):\n        self.ai.album = \"newAlbumName\"\n        self.ai.move()\n        self.ai.store()\n        self.i.load()\n\n        assert b\"newAlbumName\" in self.i.path\n\n    def test_albuminfo_move_moves_file(self):\n        oldpath = self.i.filepath\n        self.ai.album = \"newAlbumName\"\n        self.ai.move()\n        self.ai.store()\n        self.i.load()\n\n        assert not oldpath.exists()\n        assert self.i.filepath.exists()\n\n    def test_albuminfo_move_copies_file(self):\n        oldpath = self.i.filepath\n        self.ai.album = \"newAlbumName\"\n        self.ai.move(operation=MoveOperation.COPY)\n        self.ai.store()\n        self.i.load()\n\n        assert oldpath.exists()\n        assert self.i.filepath.exists()\n\n    @NEEDS_REFLINK\n    def test_albuminfo_move_reflinks_file(self):\n        oldpath = self.i.path\n        self.ai.album = \"newAlbumName\"\n        self.ai.move(operation=MoveOperation.REFLINK)\n        self.ai.store()\n        self.i.load()\n\n        assert os.path.exists(oldpath)\n        assert os.path.exists(self.i.path)\n\n    def test_albuminfo_move_to_custom_dir(self):\n        self.ai.move(basedir=self.otherdir)\n        self.i.load()\n        self.ai.store()\n        assert b\"testotherdir\" in self.i.path\n\n\nclass ArtFileTest(BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n\n        # Make library and item.\n        self.i = item(self.lib)\n        self.i.path = self.i.destination()\n        # Make a music file.\n        util.mkdirall(self.i.path)\n        touch(self.i.path)\n        # Make an album.\n        self.ai = self.lib.add_album((self.i,))\n        # Make an art file too.\n        art_bytes = self.lib.get_album(self.i).art_destination(\"something.jpg\")\n        self.art = Path(os.fsdecode(art_bytes))\n        self.art.touch()\n        self.ai.artpath = art_bytes\n        self.ai.store()\n        # Alternate destination dir.\n        self.otherdir = os.path.join(self.temp_dir, b\"testotherdir\")\n\n    def test_art_deleted_when_items_deleted(self):\n        assert self.art.exists()\n        self.ai.remove(True)\n        assert not self.art.exists()\n\n    def test_art_moves_with_album(self):\n        assert self.art.exists()\n        oldpath = self.i.path\n        self.ai.album = \"newAlbum\"\n        self.ai.move()\n        self.i.load()\n\n        assert self.i.path != oldpath\n        assert not self.art.exists()\n        newart = self.lib.get_album(self.i).art_destination(self.art)\n        assert Path(os.fsdecode(newart)).exists()\n\n    def test_art_moves_with_album_to_custom_dir(self):\n        # Move the album to another directory.\n        self.ai.move(basedir=self.otherdir)\n        self.ai.store()\n        self.i.load()\n\n        # Art should be in new directory.\n        assert not self.art.exists()\n        newart = self.lib.get_album(self.i).art_filepath\n        assert newart.exists()\n        assert \"testotherdir\" in str(newart)\n\n    def test_setart_copies_image(self):\n        util.remove(self.art)\n\n        newart = os.path.join(self.libdir, b\"newart.jpg\")\n        touch(newart)\n        i2 = item()\n        i2.path = self.i.path\n        i2.artist = \"someArtist\"\n        ai = self.lib.add_album((i2,))\n        i2.move(operation=MoveOperation.COPY)\n\n        assert ai.artpath is None\n        ai.set_art(newart)\n        assert ai.art_filepath.exists()\n\n    def test_setart_to_existing_art_works(self):\n        util.remove(self.art)\n\n        # Original art.\n        newart = os.path.join(self.libdir, b\"newart.jpg\")\n        touch(newart)\n        i2 = item()\n        i2.path = self.i.path\n        i2.artist = \"someArtist\"\n        ai = self.lib.add_album((i2,))\n        i2.move(operation=MoveOperation.COPY)\n        ai.set_art(newart)\n\n        # Set the art again.\n        ai.set_art(ai.artpath)\n        assert ai.art_filepath.exists()\n\n    def test_setart_to_existing_but_unset_art_works(self):\n        newart = os.path.join(self.libdir, b\"newart.jpg\")\n        touch(newart)\n        i2 = item()\n        i2.path = self.i.path\n        i2.artist = \"someArtist\"\n        ai = self.lib.add_album((i2,))\n        i2.move(operation=MoveOperation.COPY)\n\n        # Copy the art to the destination.\n        artdest = ai.art_destination(newart)\n        shutil.copy(syspath(newart), syspath(artdest))\n\n        # Set the art again.\n        ai.set_art(artdest)\n        assert ai.art_filepath.exists()\n\n    def test_setart_to_conflicting_file_gets_new_path(self):\n        newart = os.path.join(self.libdir, b\"newart.jpg\")\n        touch(newart)\n        i2 = item()\n        i2.path = self.i.path\n        i2.artist = \"someArtist\"\n        ai = self.lib.add_album((i2,))\n        i2.move(operation=MoveOperation.COPY)\n\n        # Make a file at the destination.\n        artdest = ai.art_destination(newart)\n        touch(artdest)\n\n        # Set the art.\n        ai.set_art(newart)\n        assert artdest != ai.artpath\n        assert os.path.dirname(artdest) == os.path.dirname(ai.artpath)\n\n    def test_setart_sets_permissions(self):\n        util.remove(self.art)\n\n        newart = os.path.join(self.libdir, b\"newart.jpg\")\n        touch(newart)\n        os.chmod(syspath(newart), 0o400)  # read-only\n\n        try:\n            i2 = item()\n            i2.path = self.i.path\n            i2.artist = \"someArtist\"\n            ai = self.lib.add_album((i2,))\n            i2.move(operation=MoveOperation.COPY)\n            ai.set_art(newart)\n\n            mode = stat.S_IMODE(os.stat(syspath(ai.artpath)).st_mode)\n            assert mode & stat.S_IRGRP\n            assert os.access(syspath(ai.artpath), os.W_OK)\n\n        finally:\n            # Make everything writable so it can be cleaned up.\n            os.chmod(syspath(newart), 0o777)\n            os.chmod(syspath(ai.artpath), 0o777)\n\n    def test_move_last_file_moves_albumart(self):\n        oldartpath = self.lib.albums()[0].art_filepath\n        assert oldartpath.exists()\n\n        self.ai.album = \"different_album\"\n        self.ai.store()\n        self.ai.items()[0].move()\n\n        artpath = self.lib.albums()[0].art_filepath\n        assert \"different_album\" in str(artpath)\n        assert artpath.exists()\n        assert not oldartpath.exists()\n\n    def test_move_not_last_file_does_not_move_albumart(self):\n        i2 = item()\n        i2.albumid = self.ai.id\n        self.lib.add(i2)\n\n        oldartpath = self.lib.albums()[0].art_filepath\n        assert oldartpath.exists()\n\n        self.i.album = \"different_album\"\n        self.i.album_id = None  # detach from album\n        self.i.move()\n\n        artpath = self.lib.albums()[0].art_filepath\n        assert \"different_album\" not in str(artpath)\n        assert artpath == oldartpath\n        assert oldartpath.exists()\n\n\nclass RemoveTest(BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n\n        # Make library and item.\n        self.i = item(self.lib)\n        self.i.path = self.i.destination()\n        # Make a music file.\n        util.mkdirall(self.i.path)\n        touch(self.i.path)\n        # Make an album with the item.\n        self.ai = self.lib.add_album((self.i,))\n\n    def test_removing_last_item_prunes_empty_dir(self):\n        assert self.i.filepath.parent.exists()\n        self.i.remove(True)\n        assert not self.i.filepath.parent.exists()\n\n    def test_removing_last_item_preserves_nonempty_dir(self):\n        (self.i.filepath.parent / \"dummy.txt\").touch()\n        self.i.remove(True)\n        assert self.i.filepath.parent.exists()\n\n    def test_removing_last_item_prunes_dir_with_blacklisted_file(self):\n        (self.i.filepath.parent / \".DS_Store\").touch()\n        self.i.remove(True)\n        assert not self.i.filepath.parent.exists()\n\n    def test_removing_without_delete_leaves_file(self):\n        self.i.remove(False)\n        assert self.i.filepath.parent.exists()\n\n    def test_removing_last_item_preserves_library_dir(self):\n        self.i.remove(True)\n        assert self.lib_path.exists()\n\n    def test_removing_item_outside_of_library_deletes_nothing(self):\n        self.lib.directory = os.path.join(self.temp_dir, b\"xxx\")\n        self.i.remove(True)\n        assert self.i.filepath.parent.exists()\n\n    def test_removing_last_item_in_album_with_albumart_prunes_dir(self):\n        artfile = os.path.join(self.temp_dir, b\"testart.jpg\")\n        touch(artfile)\n        self.ai.set_art(artfile)\n        self.ai.store()\n\n        self.i.remove(True)\n        assert not self.i.filepath.parent.exists()\n\n\nclass FilePathTestCase(BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n\n        self.path = self.temp_dir_path / \"testfile\"\n        self.path.touch()\n\n\n# Tests that we can \"delete\" nonexistent files.\nclass SoftRemoveTest(FilePathTestCase):\n    def test_soft_remove_deletes_file(self):\n        util.remove(self.path, True)\n        assert not self.path.exists()\n\n    def test_soft_remove_silent_on_no_file(self):\n        try:\n            util.remove(self.path / \"XXX\", True)\n        except OSError:\n            self.fail(\"OSError when removing path\")\n\n\nclass SafeMoveCopyTest(FilePathTestCase):\n    def setUp(self):\n        super().setUp()\n\n        self.otherpath = self.temp_dir_path / \"testfile2\"\n        self.otherpath.touch()\n        self.dest = Path(f\"{self.path}.dest\")\n\n    def test_successful_move(self):\n        util.move(self.path, self.dest)\n        assert self.dest.exists()\n        assert not self.path.exists()\n\n    def test_successful_copy(self):\n        util.copy(self.path, self.dest)\n        assert self.dest.exists()\n        assert self.path.exists()\n\n    @NEEDS_REFLINK\n    def test_successful_reflink(self):\n        util.reflink(str(self.path), str(self.dest))\n        assert self.dest.exists()\n        assert self.path.exists()\n\n    def test_unsuccessful_move(self):\n        with pytest.raises(util.FilesystemError):\n            util.move(self.path, self.otherpath)\n\n    def test_unsuccessful_copy(self):\n        with pytest.raises(util.FilesystemError):\n            util.copy(self.path, self.otherpath)\n\n    def test_unsuccessful_reflink(self):\n        with pytest.raises(util.FilesystemError, match=\"target exists\"):\n            util.reflink(self.path, self.otherpath)\n\n    def test_self_move(self):\n        util.move(self.path, self.path)\n        assert self.path.exists()\n\n    def test_self_copy(self):\n        util.copy(self.path, self.path)\n        assert self.path.exists()\n\n\nclass PruneTest(BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n\n        self.base = self.temp_dir_path / \"testdir\"\n        self.base.mkdir()\n        self.sub = self.base / \"subdir\"\n        self.sub.mkdir()\n\n    def test_prune_existent_directory(self):\n        util.prune_dirs(self.sub, self.base)\n        assert self.base.exists()\n        assert not self.sub.exists()\n\n    def test_prune_nonexistent_directory(self):\n        util.prune_dirs(self.sub / \"another\", self.base)\n        assert self.base.exists()\n        assert not self.sub.exists()\n\n\nclass WalkTest(BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n\n        self.base = os.path.join(self.temp_dir, b\"testdir\")\n        os.mkdir(syspath(self.base))\n        touch(os.path.join(self.base, b\"y\"))\n        touch(os.path.join(self.base, b\"x\"))\n        os.mkdir(syspath(os.path.join(self.base, b\"d\")))\n        touch(os.path.join(self.base, b\"d\", b\"z\"))\n\n    def test_sorted_files(self):\n        res = list(util.sorted_walk(self.base))\n        assert len(res) == 2\n        assert res[0] == (self.base, [b\"d\"], [b\"x\", b\"y\"])\n        assert res[1] == (os.path.join(self.base, b\"d\"), [], [b\"z\"])\n\n    def test_ignore_file(self):\n        res = list(util.sorted_walk(self.base, (b\"x\",)))\n        assert len(res) == 2\n        assert res[0] == (self.base, [b\"d\"], [b\"y\"])\n        assert res[1] == (os.path.join(self.base, b\"d\"), [], [b\"z\"])\n\n    def test_ignore_directory(self):\n        res = list(util.sorted_walk(self.base, (b\"d\",)))\n        assert len(res) == 1\n        assert res[0] == (self.base, [], [b\"x\", b\"y\"])\n\n    def test_ignore_everything(self):\n        res = list(util.sorted_walk(self.base, (b\"*\",)))\n        assert len(res) == 1\n        assert res[0] == (self.base, [], [])\n\n\nclass UniquePathTest(BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n\n        self.base = os.path.join(self.temp_dir, b\"testdir\")\n        os.mkdir(syspath(self.base))\n        touch(os.path.join(self.base, b\"x.mp3\"))\n        touch(os.path.join(self.base, b\"x.1.mp3\"))\n        touch(os.path.join(self.base, b\"x.2.mp3\"))\n        touch(os.path.join(self.base, b\"y.mp3\"))\n\n    def test_new_file_unchanged(self):\n        path = util.unique_path(os.path.join(self.base, b\"z.mp3\"))\n        assert path == os.path.join(self.base, b\"z.mp3\")\n\n    def test_conflicting_file_appends_1(self):\n        path = util.unique_path(os.path.join(self.base, b\"y.mp3\"))\n        assert path == os.path.join(self.base, b\"y.1.mp3\")\n\n    def test_conflicting_file_appends_higher_number(self):\n        path = util.unique_path(os.path.join(self.base, b\"x.mp3\"))\n        assert path == os.path.join(self.base, b\"x.3.mp3\")\n\n    def test_conflicting_file_with_number_increases_number(self):\n        path = util.unique_path(os.path.join(self.base, b\"x.1.mp3\"))\n        assert path == os.path.join(self.base, b\"x.3.mp3\")\n\n\nclass MkDirAllTest(BeetsTestCase):\n    def test_mkdirall(self):\n        child = self.temp_dir_path / \"foo\" / \"bar\" / \"baz\" / \"quz.mp3\"\n        util.mkdirall(child)\n        assert not child.exists()\n        assert child.parent.exists()\n        assert child.parent.is_dir()\n"
  },
  {
    "path": "test/test_hidden.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Fabrice Laporte.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for the 'hidden' utility.\"\"\"\n\nimport ctypes\nimport errno\nimport subprocess\nimport sys\nimport tempfile\nimport unittest\n\nfrom beets import util\nfrom beets.util import bytestring_path, hidden\n\n\nclass HiddenFileTest(unittest.TestCase):\n    def setUp(self):\n        pass\n\n    def test_osx_hidden(self):\n        if not sys.platform == \"darwin\":\n            self.skipTest(\"sys.platform is not darwin\")\n            return\n\n        with tempfile.NamedTemporaryFile(delete=False) as f:\n            try:\n                command = [\"chflags\", \"hidden\", f.name]\n                subprocess.Popen(command).wait()\n            except OSError as e:\n                if e.errno == errno.ENOENT:\n                    self.skipTest(\"unable to find chflags\")\n                else:\n                    raise e\n\n            assert hidden.is_hidden(bytestring_path(f.name))\n\n    def test_windows_hidden(self):\n        if not sys.platform == \"win32\":\n            self.skipTest(\"sys.platform is not windows\")\n            return\n\n        # FILE_ATTRIBUTE_HIDDEN = 2 (0x2) from GetFileAttributes documentation.\n        hidden_mask = 2\n\n        with tempfile.NamedTemporaryFile() as f:\n            # Hide the file using\n            success = ctypes.windll.kernel32.SetFileAttributesW(\n                f.name, hidden_mask\n            )\n\n            if not success:\n                self.skipTest(\"unable to set file attributes\")\n\n            assert hidden.is_hidden(f.name)\n\n    def test_other_hidden(self):\n        if sys.platform == \"darwin\" or sys.platform == \"win32\":\n            self.skipTest(\"sys.platform is known\")\n            return\n\n        with tempfile.NamedTemporaryFile(prefix=\".tmp\") as f:\n            fn = util.bytestring_path(f.name)\n            assert hidden.is_hidden(fn)\n"
  },
  {
    "path": "test/test_importer.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\n\"\"\"Tests for the general importer functionality.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport re\nimport shutil\nimport stat\nimport sys\nimport unicodedata\nimport unittest\nfrom functools import cached_property\nfrom io import StringIO\nfrom pathlib import Path\nfrom tarfile import TarFile\nfrom tempfile import mkstemp\nfrom unittest.mock import Mock, patch\nfrom zipfile import ZipFile\n\nimport pytest\nfrom mediafile import MediaFile\n\nfrom beets import config, importer, logging, util\nfrom beets.autotag import AlbumInfo, AlbumMatch, TrackInfo\nfrom beets.importer.tasks import albums_in_dir\nfrom beets.test import _common\nfrom beets.test.helper import (\n    NEEDS_REFLINK,\n    AsIsImporterMixin,\n    AutotagImportTestCase,\n    AutotagStub,\n    BeetsTestCase,\n    ImportTestCase,\n    IOMixin,\n    PluginMixin,\n    capture_log,\n    has_program,\n)\nfrom beets.util import bytestring_path, displayable_path, syspath\n\n\nclass PathsMixin:\n    import_media: list[MediaFile]\n\n    @cached_property\n    def track_import_path(self) -> Path:\n        return Path(self.import_media[0].path)\n\n    @cached_property\n    def album_path(self) -> Path:\n        return self.track_import_path.parent\n\n    @cached_property\n    def track_lib_path(self):\n        return self.lib_path / \"Tag Artist\" / \"Tag Album\" / \"Tag Track 1.mp3\"\n\n\n@_common.slow_test()\nclass NonAutotaggedImportTest(PathsMixin, AsIsImporterMixin, ImportTestCase):\n    db_on_disk = True\n\n    def test_album_created_with_track_artist(self):\n        self.run_asis_importer()\n\n        albums = self.lib.albums()\n        assert len(albums) == 1\n        assert albums[0].albumartist == \"Tag Artist\"\n\n    def test_import_copy_arrives(self):\n        self.run_asis_importer()\n\n        assert self.track_lib_path.exists()\n\n    def test_threaded_import_copy_arrives(self):\n        config[\"threaded\"] = True\n\n        self.run_asis_importer()\n        assert self.track_lib_path.exists()\n\n    def test_import_with_move_deletes_import_files(self):\n        assert self.album_path.exists()\n        assert self.track_import_path.exists()\n        (self.album_path / \"alog.log\").touch()\n        config[\"clutter\"] = [\"*.log\"]\n\n        self.run_asis_importer(move=True)\n\n        assert not self.track_import_path.exists()\n        assert not self.album_path.exists()\n\n    def test_threaded_import_move_arrives(self):\n        self.run_asis_importer(move=True, threaded=True)\n\n        assert self.track_lib_path.exists()\n        assert not self.track_import_path.exists()\n\n    def test_import_without_delete_retains_files(self):\n        self.run_asis_importer(delete=False)\n\n        assert self.track_import_path.exists()\n\n    def test_import_with_delete_removes_files(self):\n        self.run_asis_importer(delete=True)\n\n        assert not self.album_path.exists()\n        assert not self.track_import_path.exists()\n\n    def test_album_mb_albumartistids(self):\n        self.run_asis_importer()\n        album = self.lib.albums()[0]\n        assert album.mb_albumartistids == album.items()[0].mb_albumartistids\n\n    @unittest.skipUnless(_common.HAVE_SYMLINK, \"need symlinks\")\n    def test_import_link_arrives(self):\n        self.run_asis_importer(link=True)\n\n        assert self.track_lib_path.exists()\n        assert self.track_lib_path.is_symlink()\n        assert self.track_lib_path.resolve() == self.track_import_path.resolve()\n\n    @unittest.skipUnless(_common.HAVE_HARDLINK, \"need hardlinks\")\n    def test_import_hardlink_arrives(self):\n        self.run_asis_importer(hardlink=True)\n\n        assert self.track_lib_path.exists()\n        media_stat = self.track_import_path.stat()\n        lib_media_stat = self.track_lib_path.stat()\n        assert media_stat[stat.ST_INO] == lib_media_stat[stat.ST_INO]\n        assert media_stat[stat.ST_DEV] == lib_media_stat[stat.ST_DEV]\n\n    @NEEDS_REFLINK\n    def test_import_reflink_arrives(self):\n        # Detecting reflinks is currently tricky due to various fs\n        # implementations, we'll just check the file exists.\n        self.run_asis_importer(reflink=True)\n\n        assert self.track_lib_path.exists()\n\n    def test_import_reflink_auto_arrives(self):\n        # Should pass regardless of reflink support due to fallback.\n        self.run_asis_importer(reflink=\"auto\")\n\n        assert self.track_lib_path.exists()\n\n\ndef create_archive(session):\n    handle, path = mkstemp(dir=session.temp_dir_path)\n    path = bytestring_path(path)\n    os.close(handle)\n    archive = ZipFile(os.fsdecode(path), mode=\"w\")\n    archive.write(syspath(os.path.join(_common.RSRC, b\"full.mp3\")), \"full.mp3\")\n    archive.close()\n    path = bytestring_path(path)\n    return path\n\n\nclass RmTempTest(BeetsTestCase):\n    \"\"\"Tests that temporarily extracted archives are properly removed\n    after usage.\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.want_resume = False\n        self.config[\"incremental\"] = False\n        self._old_home = None\n\n    def test_rm(self):\n        zip_path = create_archive(self)\n        archive_task = importer.ArchiveImportTask(zip_path)\n        archive_task.extract()\n        tmp_path = Path(os.fsdecode(archive_task.toppath))\n        assert tmp_path.exists()\n        archive_task.finalize(self)\n        assert not tmp_path.exists()\n\n\nclass ImportZipTest(AsIsImporterMixin, ImportTestCase):\n    def test_import_zip(self):\n        zip_path = create_archive(self)\n        assert len(self.lib.items()) == 0\n        assert len(self.lib.albums()) == 0\n\n        self.run_asis_importer(import_dir=zip_path)\n        assert len(self.lib.items()) == 1\n        assert len(self.lib.albums()) == 1\n\n\nclass ImportTarTest(ImportZipTest):\n    def create_archive(self):\n        (handle, path) = mkstemp(dir=syspath(self.temp_dir))\n        path = bytestring_path(path)\n        os.close(handle)\n        archive = TarFile(os.fsdecode(path), mode=\"w\")\n        archive.add(\n            syspath(os.path.join(_common.RSRC, b\"full.mp3\")), \"full.mp3\"\n        )\n        archive.close()\n        return path\n\n\n@unittest.skipIf(not has_program(\"unrar\"), \"unrar program not found\")\nclass ImportRarTest(ImportZipTest):\n    def create_archive(self):\n        return os.path.join(_common.RSRC, b\"archive.rar\")\n\n\nclass Import7zTest(ImportZipTest):\n    def create_archive(self):\n        return os.path.join(_common.RSRC, b\"archive.7z\")\n\n\n@unittest.skip(\"Implement me!\")\nclass ImportPasswordRarTest(ImportZipTest):\n    def create_archive(self):\n        return os.path.join(_common.RSRC, b\"password.rar\")\n\n\nclass ImportSingletonTest(AutotagImportTestCase):\n    \"\"\"Test ``APPLY`` and ``ASIS`` choices for an import session with\n    singletons config set to True.\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.prepare_album_for_import(1)\n        self.importer = self.setup_singleton_importer()\n\n    def test_apply_asis_adds_only_singleton_track(self):\n        self.importer.add_choice(importer.Action.ASIS)\n        self.importer.run()\n\n        # album not added\n        assert not self.lib.albums()\n        assert self.lib.items().get().title == \"Tag Track 1\"\n        assert (self.lib_path / \"singletons\" / \"Tag Track 1.mp3\").exists()\n\n    def test_apply_candidate_adds_track(self):\n        self.importer.add_choice(importer.Action.APPLY)\n        self.importer.run()\n\n        assert not self.lib.albums()\n        assert self.lib.items().get().title == \"Applied Track 1\"\n        assert (self.lib_path / \"singletons\" / \"Applied Track 1.mp3\").exists()\n\n    def test_apply_from_scratch_removes_other_metadata(self):\n        config[\"import\"][\"from_scratch\"] = True\n\n        for mediafile in self.import_media:\n            mediafile.comments = \"Tag Comment\"\n            mediafile.save()\n\n        self.importer.add_choice(importer.Action.APPLY)\n        self.importer.run()\n        assert self.lib.items().get().comments == \"\"\n\n    def test_skip_does_not_add_track(self):\n        self.importer.add_choice(importer.Action.SKIP)\n        self.importer.run()\n\n        assert not self.lib.items()\n\n    def test_skip_first_add_second_asis(self):\n        self.prepare_album_for_import(2)\n\n        self.importer.add_choice(importer.Action.SKIP)\n        self.importer.add_choice(importer.Action.ASIS)\n        self.importer.run()\n\n        assert len(self.lib.items()) == 1\n\n    def test_import_single_files(self):\n        resource_path = os.path.join(_common.RSRC, b\"empty.mp3\")\n        single_path = os.path.join(self.import_dir, b\"track_2.mp3\")\n\n        util.copy(resource_path, single_path)\n        import_files = [\n            os.path.join(self.import_dir, b\"album\"),\n            single_path,\n        ]\n        self.setup_importer()\n        self.importer.paths = import_files\n\n        self.importer.add_choice(importer.Action.ASIS)\n        self.importer.add_choice(importer.Action.ASIS)\n        self.importer.run()\n\n        assert len(self.lib.items()) == 2\n        assert len(self.lib.albums()) == 2\n\n    def test_set_fields(self):\n        genres = [\"\\U0001f3b7 Jazz\", \"Rock\"]\n        collection = \"To Listen\"\n        disc = 0\n\n        config[\"import\"][\"set_fields\"] = {\n            \"genres\": \"; \".join(genres),\n            \"collection\": collection,\n            \"disc\": disc,\n            \"title\": \"$title - formatted\",\n        }\n\n        # As-is item import.\n        assert self.lib.albums().get() is None\n        self.importer.add_choice(importer.Action.ASIS)\n        self.importer.run()\n\n        for item in self.lib.items():\n            item.load()  # TODO: Not sure this is necessary.\n            assert item.genres == genres\n            assert item.collection == collection\n            assert item.title == \"Tag Track 1 - formatted\"\n            assert item.disc == disc\n            # Remove item from library to test again with APPLY choice.\n            item.remove()\n\n        # Autotagged.\n        assert not self.lib.albums()\n        self.importer.clear_choices()\n        self.importer.add_choice(importer.Action.APPLY)\n        self.importer.run()\n\n        for item in self.lib.items():\n            item.load()\n            assert item.genres == genres\n            assert item.collection == collection\n            assert item.title == \"Applied Track 1 - formatted\"\n            assert item.disc == disc\n\n\nclass ImportTest(PathsMixin, AutotagImportTestCase):\n    \"\"\"Test APPLY, ASIS and SKIP choices.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.prepare_album_for_import(1)\n        self.setup_importer()\n\n    def test_asis_moves_album_and_track(self):\n        self.importer.add_choice(importer.Action.ASIS)\n        self.importer.run()\n\n        assert self.lib.albums().get().album == \"Tag Album\"\n        item = self.lib.items().get()\n        assert item.title == \"Tag Track 1\"\n        assert item.filepath.exists()\n\n    def test_apply_moves_album_and_track(self):\n        self.importer.add_choice(importer.Action.APPLY)\n        self.importer.run()\n\n        assert self.lib.albums().get().album == \"Applied Album\"\n        item = self.lib.items().get()\n        assert item.title == \"Applied Track 1\"\n        assert item.filepath.exists()\n\n    def test_apply_from_scratch_removes_other_metadata(self):\n        config[\"import\"][\"from_scratch\"] = True\n\n        for mediafile in self.import_media:\n            mediafile.genres = [\"Tag Genre\"]\n            mediafile.save()\n\n        self.importer.add_choice(importer.Action.APPLY)\n        self.importer.run()\n        assert not self.lib.items().get().genres\n\n    def test_apply_from_scratch_keeps_format(self):\n        config[\"import\"][\"from_scratch\"] = True\n\n        self.importer.add_choice(importer.Action.APPLY)\n        self.importer.run()\n        assert self.lib.items().get().format == \"MP3\"\n\n    def test_apply_from_scratch_keeps_bitrate(self):\n        config[\"import\"][\"from_scratch\"] = True\n        bitrate = 80000\n\n        self.importer.add_choice(importer.Action.APPLY)\n        self.importer.run()\n        assert self.lib.items().get().bitrate == bitrate\n\n    def test_apply_with_move_deletes_import(self):\n        assert self.track_import_path.exists()\n\n        config[\"import\"][\"move\"] = True\n        self.importer.add_choice(importer.Action.APPLY)\n        self.importer.run()\n\n        assert not self.track_import_path.exists()\n\n    def test_apply_with_delete_deletes_import(self):\n        assert self.track_import_path.exists()\n\n        config[\"import\"][\"delete\"] = True\n        self.importer.add_choice(importer.Action.APPLY)\n        self.importer.run()\n\n        assert not self.track_import_path.exists()\n\n    def test_skip_does_not_add_track(self):\n        self.importer.add_choice(importer.Action.SKIP)\n        self.importer.run()\n\n        assert not self.lib.items()\n\n    def test_skip_non_album_dirs(self):\n        assert (self.import_path / \"album\").exists()\n        self.touch(b\"cruft\", dir=self.import_dir)\n        self.importer.add_choice(importer.Action.APPLY)\n        self.importer.run()\n\n        assert len(self.lib.albums()) == 1\n\n    def test_unmatched_tracks_not_added(self):\n        self.prepare_album_for_import(2)\n        self.matcher.matching = self.matcher.MISSING\n        self.importer.add_choice(importer.Action.APPLY)\n        self.importer.run()\n        assert len(self.lib.items()) == 1\n\n    def test_empty_directory_warning(self):\n        import_dir = os.path.join(self.temp_dir, b\"empty\")\n        self.touch(b\"non-audio\", dir=import_dir)\n        self.setup_importer(import_dir=import_dir)\n        with capture_log() as logs:\n            self.importer.run()\n\n        import_dir = displayable_path(import_dir)\n        assert f\"No files imported from {import_dir}\" in logs\n\n    def test_empty_directory_singleton_warning(self):\n        import_dir = os.path.join(self.temp_dir, b\"empty\")\n        self.touch(b\"non-audio\", dir=import_dir)\n        self.setup_singleton_importer(import_dir=import_dir)\n        with capture_log() as logs:\n            self.importer.run()\n\n        import_dir = displayable_path(import_dir)\n        assert f\"No files imported from {import_dir}\" in logs\n\n    def test_asis_no_data_source(self):\n        assert self.lib.items().get() is None\n\n        self.importer.add_choice(importer.Action.ASIS)\n        self.importer.run()\n\n        with pytest.raises(AttributeError):\n            self.lib.items().get().data_source\n\n    def test_set_fields(self):\n        genres = [\"\\U0001f3b7 Jazz\", \"Rock\"]\n        collection = \"To Listen\"\n        disc = 0\n        comments = \"managed by beets\"\n\n        config[\"import\"][\"set_fields\"] = {\n            \"genres\": \"; \".join(genres),\n            \"collection\": collection,\n            \"disc\": disc,\n            \"comments\": comments,\n            \"album\": \"$album - formatted\",\n        }\n\n        # As-is album import.\n        assert self.lib.albums().get() is None\n        self.importer.add_choice(importer.Action.ASIS)\n        self.importer.run()\n\n        for album in self.lib.albums():\n            assert album.genres == genres\n            assert album.comments == comments\n            for item in album.items():\n                assert item.get(\"genres\", with_album=False) == genres\n                assert item.get(\"collection\", with_album=False) == collection\n                assert item.get(\"comments\", with_album=False) == comments\n                assert (\n                    item.get(\"album\", with_album=False)\n                    == \"Tag Album - formatted\"\n                )\n                assert item.disc == disc\n            # Remove album from library to test again with APPLY choice.\n            album.remove()\n\n        # Autotagged.\n        assert self.lib.albums().get() is None\n        self.importer.clear_choices()\n        self.importer.add_choice(importer.Action.APPLY)\n        self.importer.run()\n\n        for album in self.lib.albums():\n            assert album.genres == genres\n            assert album.comments == comments\n            for item in album.items():\n                assert item.get(\"genres\", with_album=False) == genres\n                assert item.get(\"collection\", with_album=False) == collection\n                assert item.get(\"comments\", with_album=False) == comments\n                assert (\n                    item.get(\"album\", with_album=False)\n                    == \"Applied Album - formatted\"\n                )\n                assert item.disc == disc\n\n\nclass ImportTracksTest(AutotagImportTestCase):\n    \"\"\"Test TRACKS and APPLY choice.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.prepare_album_for_import(1)\n        self.setup_importer()\n\n    def test_apply_tracks_adds_singleton_track(self):\n        self.importer.add_choice(importer.Action.TRACKS)\n        self.importer.add_choice(importer.Action.APPLY)\n        self.importer.add_choice(importer.Action.APPLY)\n        self.importer.run()\n\n        assert self.lib.items().get().title == \"Applied Track 1\"\n        assert not self.lib.albums()\n\n    def test_apply_tracks_adds_singleton_path(self):\n        self.importer.add_choice(importer.Action.TRACKS)\n        self.importer.add_choice(importer.Action.APPLY)\n        self.importer.add_choice(importer.Action.APPLY)\n        self.importer.run()\n\n        assert (self.lib_path / \"singletons\" / \"Applied Track 1.mp3\").exists()\n\n\nclass ImportCompilationTest(AutotagImportTestCase):\n    \"\"\"Test ASIS import of a folder containing tracks with different artists.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.prepare_album_for_import(3)\n        self.setup_importer()\n\n    def test_asis_homogenous_sets_albumartist(self):\n        self.importer.add_choice(importer.Action.ASIS)\n        self.importer.run()\n        assert self.lib.albums().get().albumartist == \"Tag Artist\"\n        for item in self.lib.items():\n            assert item.albumartist == \"Tag Artist\"\n\n    def test_asis_heterogenous_sets_various_albumartist(self):\n        self.import_media[0].artist = \"Other Artist\"\n        self.import_media[0].save()\n        self.import_media[1].artist = \"Another Artist\"\n        self.import_media[1].save()\n\n        self.importer.add_choice(importer.Action.ASIS)\n        self.importer.run()\n        assert self.lib.albums().get().albumartist == \"Various Artists\"\n        for item in self.lib.items():\n            assert item.albumartist == \"Various Artists\"\n\n    def test_asis_heterogenous_sets_compilation(self):\n        self.import_media[0].artist = \"Other Artist\"\n        self.import_media[0].save()\n        self.import_media[1].artist = \"Another Artist\"\n        self.import_media[1].save()\n\n        self.importer.add_choice(importer.Action.ASIS)\n        self.importer.run()\n        for item in self.lib.items():\n            assert item.comp\n\n    def test_asis_sets_majority_albumartist(self):\n        self.import_media[0].artist = \"Other Artist\"\n        self.import_media[0].save()\n        self.import_media[1].artist = \"Other Artist\"\n        self.import_media[1].save()\n\n        self.importer.add_choice(importer.Action.ASIS)\n        self.importer.run()\n        assert self.lib.albums().get().albumartist == \"Other Artist\"\n        for item in self.lib.items():\n            assert item.albumartist == \"Other Artist\"\n\n    def test_asis_albumartist_tag_sets_albumartist(self):\n        self.import_media[0].artist = \"Other Artist\"\n        self.import_media[1].artist = \"Another Artist\"\n        for mediafile in self.import_media:\n            mediafile.albumartist = \"Album Artist\"\n            mediafile.mb_albumartistid = \"Album Artist ID\"\n            mediafile.save()\n\n        self.importer.add_choice(importer.Action.ASIS)\n        self.importer.run()\n        assert self.lib.albums().get().albumartist == \"Album Artist\"\n        assert self.lib.albums().get().mb_albumartistid == \"Album Artist ID\"\n        for item in self.lib.items():\n            assert item.albumartist == \"Album Artist\"\n            assert item.mb_albumartistid == \"Album Artist ID\"\n\n    def test_asis_albumartists_tag_sets_multi_albumartists(self):\n        self.import_media[0].artist = \"Other Artist\"\n        self.import_media[0].artists = [\"Other Artist\", \"Other Artist 2\"]\n        self.import_media[1].artist = \"Another Artist\"\n        self.import_media[1].artists = [\"Another Artist\", \"Another Artist 2\"]\n        for mediafile in self.import_media:\n            mediafile.albumartist = \"Album Artist\"\n            mediafile.albumartists = [\"Album Artist 1\", \"Album Artist 2\"]\n            mediafile.mb_albumartistid = \"Album Artist ID\"\n            mediafile.save()\n\n        self.importer.add_choice(importer.Action.ASIS)\n        self.importer.run()\n        assert self.lib.albums().get().albumartist == \"Album Artist\"\n        assert self.lib.albums().get().albumartists == [\n            \"Album Artist 1\",\n            \"Album Artist 2\",\n        ]\n        assert self.lib.albums().get().mb_albumartistid == \"Album Artist ID\"\n\n        # Make sure both custom media items get tested\n        asserted_multi_artists_0 = False\n        asserted_multi_artists_1 = False\n        for item in self.lib.items():\n            assert item.albumartist == \"Album Artist\"\n            assert item.albumartists == [\"Album Artist 1\", \"Album Artist 2\"]\n            assert item.mb_albumartistid == \"Album Artist ID\"\n\n            if item.artist == \"Other Artist\":\n                asserted_multi_artists_0 = True\n                assert item.artists == [\"Other Artist\", \"Other Artist 2\"]\n            if item.artist == \"Another Artist\":\n                asserted_multi_artists_1 = True\n                assert item.artists == [\"Another Artist\", \"Another Artist 2\"]\n\n        assert asserted_multi_artists_0\n        assert asserted_multi_artists_1\n\n\nclass ImportExistingTest(PathsMixin, AutotagImportTestCase):\n    \"\"\"Test importing files that are already in the library directory.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.prepare_album_for_import(1)\n\n        self.reimporter = self.setup_importer(import_dir=self.libdir)\n        self.importer = self.setup_importer()\n\n    def tearDown(self):\n        super().tearDown()\n        self.matcher.restore()\n\n    @cached_property\n    def applied_track_path(self) -> Path:\n        return Path(str(self.track_lib_path).replace(\"Tag\", \"Applied\"))\n\n    def test_does_not_duplicate_item_nor_album(self):\n        self.importer.run()\n        assert len(self.lib.items()) == 1\n        assert len(self.lib.albums()) == 1\n\n        self.reimporter.add_choice(importer.Action.APPLY)\n        self.reimporter.run()\n\n        assert len(self.lib.items()) == 1\n        assert len(self.lib.albums()) == 1\n\n    def test_does_not_duplicate_singleton_track(self):\n        self.importer.add_choice(importer.Action.TRACKS)\n        self.importer.add_choice(importer.Action.APPLY)\n        self.importer.run()\n        assert len(self.lib.items()) == 1\n\n        self.reimporter.add_choice(importer.Action.TRACKS)\n        self.reimporter.add_choice(importer.Action.APPLY)\n        self.reimporter.run()\n        assert len(self.lib.items()) == 1\n\n    def test_asis_updates_metadata_and_moves_file(self):\n        self.importer.run()\n\n        medium = MediaFile(self.lib.items().get().path)\n        medium.title = \"New Title\"\n        medium.save()\n\n        self.reimporter.add_choice(importer.Action.ASIS)\n        self.reimporter.run()\n\n        assert self.lib.items().get().title == \"New Title\"\n        assert not self.applied_track_path.exists()\n        assert self.applied_track_path.with_name(\"New Title.mp3\").exists()\n\n    def test_asis_updated_without_copy_does_not_move_file(self):\n        self.importer.run()\n        medium = MediaFile(self.lib.items().get().path)\n        medium.title = \"New Title\"\n        medium.save()\n\n        config[\"import\"][\"copy\"] = False\n        self.reimporter.add_choice(importer.Action.ASIS)\n        self.reimporter.run()\n\n        assert self.applied_track_path.exists()\n        assert not self.applied_track_path.with_name(\"New Title.mp3\").exists()\n\n    def test_outside_file_is_copied(self):\n        config[\"import\"][\"copy\"] = False\n        self.importer.run()\n        assert self.lib.items().get().filepath == self.track_import_path\n\n        self.reimporter = self.setup_importer()\n        self.reimporter.add_choice(importer.Action.APPLY)\n        self.reimporter.run()\n\n        assert self.applied_track_path.exists()\n        assert self.lib.items().get().filepath == self.applied_track_path\n\n\nclass GroupAlbumsImportTest(AutotagImportTestCase):\n    matching = AutotagStub.NONE\n\n    def setUp(self):\n        super().setUp()\n        self.prepare_album_for_import(3)\n        self.setup_importer()\n\n        # Split tracks into two albums and use both as-is\n        self.importer.add_choice(importer.Action.ALBUMS)\n        self.importer.add_choice(importer.Action.ASIS)\n        self.importer.add_choice(importer.Action.ASIS)\n\n    def test_add_album_for_different_artist_and_different_album(self):\n        self.import_media[0].artist = \"Artist B\"\n        self.import_media[0].album = \"Album B\"\n        self.import_media[0].save()\n\n        self.importer.run()\n        albums = {album.album for album in self.lib.albums()}\n        assert albums == {\"Album B\", \"Tag Album\"}\n\n    def test_add_album_for_different_artist_and_same_albumartist(self):\n        self.import_media[0].artist = \"Artist B\"\n        self.import_media[0].albumartist = \"Album Artist\"\n        self.import_media[0].save()\n        self.import_media[1].artist = \"Artist C\"\n        self.import_media[1].albumartist = \"Album Artist\"\n        self.import_media[1].save()\n\n        self.importer.run()\n        artists = {album.albumartist for album in self.lib.albums()}\n        assert artists == {\"Album Artist\", \"Tag Artist\"}\n\n    def test_add_album_for_same_artist_and_different_album(self):\n        self.import_media[0].album = \"Album B\"\n        self.import_media[0].save()\n\n        self.importer.run()\n        albums = {album.album for album in self.lib.albums()}\n        assert albums == {\"Album B\", \"Tag Album\"}\n\n    def test_add_album_for_same_album_and_different_artist(self):\n        self.import_media[0].artist = \"Artist B\"\n        self.import_media[0].save()\n\n        self.importer.run()\n        artists = {album.albumartist for album in self.lib.albums()}\n        assert artists == {\"Artist B\", \"Tag Artist\"}\n\n    def test_incremental(self):\n        config[\"import\"][\"incremental\"] = True\n        self.import_media[0].album = \"Album B\"\n        self.import_media[0].save()\n\n        self.importer.run()\n        albums = {album.album for album in self.lib.albums()}\n        assert albums == {\"Album B\", \"Tag Album\"}\n\n\nclass GlobalGroupAlbumsImportTest(GroupAlbumsImportTest):\n    def setUp(self):\n        super().setUp()\n        self.importer.clear_choices()\n        self.importer.default_choice = importer.Action.ASIS\n        config[\"import\"][\"group_albums\"] = True\n\n\nclass ChooseCandidateTest(AutotagImportTestCase):\n    matching = AutotagStub.BAD\n\n    def setUp(self):\n        super().setUp()\n        self.prepare_album_for_import(1)\n        self.setup_importer()\n\n    def test_choose_first_candidate(self):\n        self.importer.add_choice(1)\n        self.importer.run()\n        assert self.lib.albums().get().album == \"Applied Album M\"\n\n    def test_choose_second_candidate(self):\n        self.importer.add_choice(2)\n        self.importer.run()\n        assert self.lib.albums().get().album == \"Applied Album MM\"\n\n\nclass InferAlbumDataTest(unittest.TestCase):\n    def setUp(self):\n        super().setUp()\n\n        i1 = _common.item()\n        i2 = _common.item()\n        i3 = _common.item()\n        i1.title = \"first item\"\n        i2.title = \"second item\"\n        i3.title = \"third item\"\n        i1.comp = i2.comp = i3.comp = False\n        i1.albumartist = i2.albumartist = i3.albumartist = \"\"\n        i1.mb_albumartistid = i2.mb_albumartistid = i3.mb_albumartistid = \"\"\n        self.items = [i1, i2, i3]\n\n        self.task = importer.ImportTask(\n            paths=[\"a path\"], toppath=\"top path\", items=self.items\n        )\n\n    def test_asis_homogenous_single_artist(self):\n        self.task.set_choice(importer.Action.ASIS)\n        self.task.align_album_level_fields()\n        assert not self.items[0].comp\n        assert self.items[0].albumartist == self.items[2].artist\n\n    def test_asis_heterogenous_va(self):\n        self.items[0].artist = \"another artist\"\n        self.items[1].artist = \"some other artist\"\n        self.task.set_choice(importer.Action.ASIS)\n\n        self.task.align_album_level_fields()\n\n        assert self.items[0].comp\n        assert self.items[0].albumartist == \"Various Artists\"\n\n    def test_asis_comp_applied_to_all_items(self):\n        self.items[0].artist = \"another artist\"\n        self.items[1].artist = \"some other artist\"\n        self.task.set_choice(importer.Action.ASIS)\n\n        self.task.align_album_level_fields()\n\n        for item in self.items:\n            assert item.comp\n            assert item.albumartist == \"Various Artists\"\n\n    def test_asis_majority_artist_single_artist(self):\n        self.items[0].artist = \"another artist\"\n        self.task.set_choice(importer.Action.ASIS)\n\n        self.task.align_album_level_fields()\n\n        assert not self.items[0].comp\n        assert self.items[0].albumartist == self.items[2].artist\n\n    def test_asis_track_albumartist_override(self):\n        self.items[0].artist = \"another artist\"\n        self.items[1].artist = \"some other artist\"\n        for item in self.items:\n            item.albumartist = \"some album artist\"\n            item.mb_albumartistid = \"some album artist id\"\n        self.task.set_choice(importer.Action.ASIS)\n\n        self.task.align_album_level_fields()\n\n        assert self.items[0].albumartist == \"some album artist\"\n        assert self.items[0].mb_albumartistid == \"some album artist id\"\n\n    def test_apply_gets_artist_and_id(self):\n        self.task.set_choice(AlbumMatch(0, None, {}, set(), set()))  # APPLY\n\n        self.task.align_album_level_fields()\n\n        assert self.items[0].albumartist == self.items[0].artist\n        assert self.items[0].mb_albumartistid == self.items[0].mb_artistid\n\n    def test_apply_lets_album_values_override(self):\n        for item in self.items:\n            item.albumartist = \"some album artist\"\n            item.mb_albumartistid = \"some album artist id\"\n        self.task.set_choice(AlbumMatch(0, None, {}, set(), set()))  # APPLY\n\n        self.task.align_album_level_fields()\n\n        assert self.items[0].albumartist == \"some album artist\"\n        assert self.items[0].mb_albumartistid == \"some album artist id\"\n\n    def test_small_single_artist_album(self):\n        self.items = [self.items[0]]\n        self.task.items = self.items\n        self.task.set_choice(importer.Action.ASIS)\n        self.task.align_album_level_fields()\n        assert not self.items[0].comp\n\n\ndef album_candidates_mock(*args, **kwargs):\n    \"\"\"Create an AlbumInfo object for testing.\"\"\"\n    yield AlbumInfo(\n        artist=\"artist\",\n        album=\"album\",\n        tracks=[TrackInfo(title=\"new title\", track_id=\"trackid\", index=0)],\n        album_id=\"albumid\",\n        artist_id=\"artistid\",\n        flex=\"flex\",\n    )\n\n\n@patch(\n    \"beets.metadata_plugins.candidates\", Mock(side_effect=album_candidates_mock)\n)\nclass ImportDuplicateAlbumTest(PluginMixin, ImportTestCase):\n    plugin = \"musicbrainz\"\n\n    def setUp(self):\n        super().setUp()\n\n        # Original album\n        self.add_album_fixture(albumartist=\"artist\", album=\"album\")\n\n        # Create import session\n        self.prepare_album_for_import(1)\n        self.importer = self.setup_importer(\n            duplicate_keys={\"album\": \"albumartist album\"}\n        )\n\n    def test_remove_duplicate_album(self):\n        item = self.lib.items().get()\n        assert item.title == \"t\\xeftle 0\"\n        assert item.filepath.exists()\n\n        self.importer.default_resolution = self.importer.Resolution.REMOVE\n        self.importer.run()\n\n        assert not item.filepath.exists()\n        assert len(self.lib.albums()) == 1\n        assert len(self.lib.items()) == 1\n        item = self.lib.items().get()\n        assert item.title == \"new title\"\n\n    def test_no_autotag_removes_duplicate_album(self):\n        config[\"import\"][\"autotag\"] = False\n        album = self.lib.albums().get()\n        item = self.lib.items().get()\n        assert item.title == \"t\\xeftle 0\"\n        assert item.filepath.exists()\n\n        # Imported item has the same albumartist and album as the one in the\n        # library album. We use album metadata (not item metadata) since\n        # duplicate detection uses album-level fields.\n        import_file = os.path.join(\n            self.importer.paths[0], b\"album\", b\"track_1.mp3\"\n        )\n        import_file = MediaFile(import_file)\n        import_file.artist = album.albumartist\n        import_file.albumartist = album.albumartist\n        import_file.album = album.album\n        import_file.title = \"new title\"\n        import_file.save()\n\n        self.importer.default_resolution = self.importer.Resolution.REMOVE\n        self.importer.run()\n\n        # Old duplicate should be removed, new one imported\n        assert len(self.lib.albums()) == 1\n        assert len(self.lib.items()) == 1\n        # The new item should be in the library\n        assert self.lib.items().get().title == \"new title\"\n\n    def test_keep_duplicate_album(self):\n        self.importer.default_resolution = self.importer.Resolution.KEEPBOTH\n        self.importer.run()\n\n        assert len(self.lib.albums()) == 2\n        assert len(self.lib.items()) == 2\n\n    def test_skip_duplicate_album(self):\n        item = self.lib.items().get()\n        assert item.title == \"t\\xeftle 0\"\n\n        self.importer.default_resolution = self.importer.Resolution.SKIP\n        self.importer.run()\n\n        assert len(self.lib.albums()) == 1\n        assert len(self.lib.items()) == 1\n        item = self.lib.items().get()\n        assert item.title == \"t\\xeftle 0\"\n\n    def test_merge_duplicate_album(self):\n        self.importer.default_resolution = self.importer.Resolution.MERGE\n        self.importer.run()\n\n        assert len(self.lib.albums()) == 1\n\n    def test_twice_in_import_dir(self):\n        self.skipTest(\"write me\")\n\n    def test_keep_when_extra_key_is_different(self):\n        config[\"import\"][\"duplicate_keys\"][\"album\"] = \"albumartist album flex\"\n\n        item = self.lib.items().get()\n        import_file = MediaFile(\n            os.path.join(self.importer.paths[0], b\"album\", b\"track_1.mp3\")\n        )\n        import_file.artist = item[\"artist\"]\n        import_file.albumartist = item[\"artist\"]\n        import_file.album = item[\"album\"]\n        import_file.title = item[\"title\"]\n        import_file.flex = \"different\"\n\n        self.importer.default_resolution = self.importer.Resolution.SKIP\n        self.importer.run()\n\n        assert len(self.lib.albums()) == 2\n        assert len(self.lib.items()) == 2\n\n    def add_album_fixture(self, **kwargs):\n        # TODO move this into upstream\n        album = super().add_album_fixture()\n        album.update(kwargs)\n        album.store()\n        return album\n\n\ndef item_candidates_mock(*args, **kwargs):\n    yield TrackInfo(\n        artist=\"artist\",\n        title=\"title\",\n        track_id=\"new trackid\",\n        index=0,\n    )\n\n\n@patch(\n    \"beets.metadata_plugins.item_candidates\",\n    Mock(side_effect=item_candidates_mock),\n)\nclass ImportDuplicateSingletonTest(ImportTestCase):\n    def setUp(self):\n        super().setUp()\n\n        # Original file in library\n        self.add_item_fixture(\n            artist=\"artist\", title=\"title\", mb_trackid=\"old trackid\"\n        )\n\n        # Import session\n        self.prepare_album_for_import(1)\n        self.importer = self.setup_singleton_importer(\n            duplicate_keys={\"album\": \"artist title\"}\n        )\n\n    def test_remove_duplicate(self):\n        item = self.lib.items().get()\n        assert item.mb_trackid == \"old trackid\"\n        assert item.filepath.exists()\n\n        self.importer.default_resolution = self.importer.Resolution.REMOVE\n        self.importer.run()\n\n        assert not item.filepath.exists()\n        assert len(self.lib.items()) == 1\n        item = self.lib.items().get()\n        assert item.mb_trackid == \"new trackid\"\n\n    def test_keep_duplicate(self):\n        assert len(self.lib.items()) == 1\n\n        self.importer.default_resolution = self.importer.Resolution.KEEPBOTH\n        self.importer.run()\n\n        assert len(self.lib.items()) == 2\n\n    def test_skip_duplicate(self):\n        item = self.lib.items().get()\n        assert item.mb_trackid == \"old trackid\"\n\n        self.importer.default_resolution = self.importer.Resolution.SKIP\n        self.importer.run()\n\n        assert len(self.lib.items()) == 1\n        item = self.lib.items().get()\n        assert item.mb_trackid == \"old trackid\"\n\n    def test_keep_when_extra_key_is_different(self):\n        config[\"import\"][\"duplicate_keys\"][\"item\"] = \"artist title flex\"\n        item = self.lib.items().get()\n        item.flex = \"different\"\n        item.store()\n        assert len(self.lib.items()) == 1\n\n        self.importer.default_resolution = self.importer.Resolution.SKIP\n        self.importer.run()\n\n        assert len(self.lib.items()) == 2\n\n    def test_no_autotag_removes_duplicate_singleton(self):\n        config[\"import\"][\"autotag\"] = False\n        item = self.lib.items().get()\n        assert item.mb_trackid == \"old trackid\"\n        assert item.filepath.exists()\n\n        # Imported item has the same artist and title as the one in the\n        # library. We use item metadata since duplicate detection uses\n        # item-level fields for singletons.\n        import_file = os.path.join(\n            self.importer.paths[0], b\"album\", b\"track_1.mp3\"\n        )\n        import_file = MediaFile(import_file)\n        import_file.artist = item.artist\n        import_file.title = item.title\n        import_file.mb_trackid = \"new trackid\"\n        import_file.save()\n\n        self.importer.default_resolution = self.importer.Resolution.REMOVE\n        self.importer.run()\n\n        # Old duplicate should be removed, new one imported\n        assert len(self.lib.items()) == 1\n        # The new item should be in the library\n        assert self.lib.items().get().mb_trackid == \"new trackid\"\n\n    def test_twice_in_import_dir(self):\n        self.skipTest(\"write me\")\n\n    def add_item_fixture(self, **kwargs):\n        # Move this to TestHelper\n        item = self.add_item_fixtures()[0]\n        item.update(kwargs)\n        item.store()\n        return item\n\n\nclass TagLogTest(unittest.TestCase):\n    def test_tag_log_line(self):\n        sio = StringIO()\n        handler = logging.StreamHandler(sio)\n        session = _common.import_session(loghandler=handler)\n        session.tag_log(\"status\", \"path\")\n        assert \"status path\" in sio.getvalue()\n\n    def test_tag_log_unicode(self):\n        sio = StringIO()\n        handler = logging.StreamHandler(sio)\n        session = _common.import_session(loghandler=handler)\n        session.tag_log(\"status\", \"caf\\xe9\")  # send unicode\n        assert \"status caf\\xe9\" in sio.getvalue()\n\n\nclass ResumeImportTest(ImportTestCase):\n    @patch(\"beets.plugins.send\")\n    def test_resume_album(self, plugins_send):\n        self.prepare_albums_for_import(2)\n        self.importer = self.setup_importer(autotag=False, resume=True)\n\n        # Aborts import after one album. This also ensures that we skip\n        # the first album in the second try.\n        def raise_exception(event, **kwargs):\n            if event == \"album_imported\":\n                raise importer.ImportAbortError\n\n        plugins_send.side_effect = raise_exception\n\n        self.importer.run()\n        assert len(self.lib.albums()) == 1\n        assert self.lib.albums(\"album:'Album 1'\").get() is not None\n\n        self.importer.run()\n        assert len(self.lib.albums()) == 2\n        assert self.lib.albums(\"album:'Album 2'\").get() is not None\n\n    @patch(\"beets.plugins.send\")\n    def test_resume_singleton(self, plugins_send):\n        self.prepare_album_for_import(2)\n        self.importer = self.setup_singleton_importer(\n            autotag=False, resume=True\n        )\n\n        # Aborts import after one track. This also ensures that we skip\n        # the first album in the second try.\n        def raise_exception(event, **kwargs):\n            if event == \"item_imported\":\n                raise importer.ImportAbortError\n\n        plugins_send.side_effect = raise_exception\n\n        self.importer.run()\n        assert len(self.lib.items()) == 1\n        assert self.lib.items(\"title:'Track 1'\").get() is not None\n\n        self.importer.run()\n        assert len(self.lib.items()) == 2\n        assert self.lib.items(\"title:'Track 1'\").get() is not None\n\n\nclass IncrementalImportTest(AsIsImporterMixin, ImportTestCase):\n    def test_incremental_album(self):\n        importer = self.run_asis_importer(incremental=True)\n\n        # Change album name so the original file would be imported again\n        # if incremental was off.\n        album = self.lib.albums().get()\n        album[\"album\"] = \"edited album\"\n        album.store()\n\n        importer.run()\n        assert len(self.lib.albums()) == 2\n\n    def test_incremental_item(self):\n        importer = self.run_asis_importer(incremental=True, singletons=True)\n\n        # Change track name so the original file would be imported again\n        # if incremental was off.\n        item = self.lib.items().get()\n        item[\"artist\"] = \"edited artist\"\n        item.store()\n\n        importer.run()\n        assert len(self.lib.items()) == 2\n\n    def test_invalid_state_file(self):\n        with open(self.config[\"statefile\"].as_filename(), \"wb\") as f:\n            f.write(b\"000\")\n        self.run_asis_importer(incremental=True)\n        assert len(self.lib.albums()) == 1\n\n\ndef _mkmp3(path):\n    shutil.copyfile(\n        syspath(os.path.join(_common.RSRC, b\"min.mp3\")),\n        syspath(path),\n    )\n\n\nclass AlbumsInDirTest(BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n\n        # create a directory structure for testing\n        self.base = os.path.abspath(os.path.join(self.temp_dir, b\"tempdir\"))\n        os.mkdir(syspath(self.base))\n\n        os.mkdir(syspath(os.path.join(self.base, b\"album1\")))\n        os.mkdir(syspath(os.path.join(self.base, b\"album2\")))\n        os.mkdir(syspath(os.path.join(self.base, b\"more\")))\n        os.mkdir(syspath(os.path.join(self.base, b\"more\", b\"album3\")))\n        os.mkdir(syspath(os.path.join(self.base, b\"more\", b\"album4\")))\n\n        _mkmp3(os.path.join(self.base, b\"album1\", b\"album1song1.mp3\"))\n        _mkmp3(os.path.join(self.base, b\"album1\", b\"album1song2.mp3\"))\n        _mkmp3(os.path.join(self.base, b\"album2\", b\"album2song.mp3\"))\n        _mkmp3(os.path.join(self.base, b\"more\", b\"album3\", b\"album3song.mp3\"))\n        _mkmp3(os.path.join(self.base, b\"more\", b\"album4\", b\"album4song.mp3\"))\n\n    def test_finds_all_albums(self):\n        albums = list(albums_in_dir(self.base))\n        assert len(albums) == 4\n\n    def test_separates_contents(self):\n        found = []\n        for _, album in albums_in_dir(self.base):\n            found.append(re.search(rb\"album(.)song\", album[0]).group(1))\n        assert b\"1\" in found\n        assert b\"2\" in found\n        assert b\"3\" in found\n        assert b\"4\" in found\n\n    def test_finds_multiple_songs(self):\n        for _, album in albums_in_dir(self.base):\n            n = re.search(rb\"album(.)song\", album[0]).group(1)\n            if n == b\"1\":\n                assert len(album) == 2\n            else:\n                assert len(album) == 1\n\n\nclass MultiDiscAlbumsInDirTest(BeetsTestCase):\n    def create_music(self, files=True, ascii=True):\n        \"\"\"Create some music in multiple album directories.\n\n        `files` indicates whether to create the files (otherwise, only\n        directories are made). `ascii` indicates ACII-only filenames;\n        otherwise, we use Unicode names.\n        \"\"\"\n        self.base = os.path.abspath(os.path.join(self.temp_dir, b\"tempdir\"))\n        os.mkdir(syspath(self.base))\n\n        name = b\"CAT\" if ascii else util.bytestring_path(\"C\\xc1T\")\n        name_alt_case = b\"CAt\" if ascii else util.bytestring_path(\"C\\xc1t\")\n\n        self.dirs = [\n            # Nested album, multiple subdirs.\n            # Also, false positive marker in root dir, and subtitle for disc 3.\n            os.path.join(self.base, b\"ABCD1234\"),\n            os.path.join(self.base, b\"ABCD1234\", b\"cd 1\"),\n            os.path.join(self.base, b\"ABCD1234\", b\"cd 3 - bonus\"),\n            # Nested album, single subdir.\n            # Also, punctuation between marker and disc number.\n            os.path.join(self.base, b\"album\"),\n            os.path.join(self.base, b\"album\", b\"cd _ 1\"),\n            # Flattened album, case typo.\n            # Also, false positive marker in parent dir.\n            os.path.join(self.base, b\"artist [CD5]\"),\n            os.path.join(self.base, b\"artist [CD5]\", name + b\" disc 1\"),\n            os.path.join(\n                self.base, b\"artist [CD5]\", name_alt_case + b\" disc 2\"\n            ),\n            # Single disc album, sorted between CAT discs.\n            os.path.join(self.base, b\"artist [CD5]\", name + b\"S\"),\n        ]\n        self.files = [\n            os.path.join(self.base, b\"ABCD1234\", b\"cd 1\", b\"song1.mp3\"),\n            os.path.join(self.base, b\"ABCD1234\", b\"cd 3 - bonus\", b\"song2.mp3\"),\n            os.path.join(self.base, b\"ABCD1234\", b\"cd 3 - bonus\", b\"song3.mp3\"),\n            os.path.join(self.base, b\"album\", b\"cd _ 1\", b\"song4.mp3\"),\n            os.path.join(\n                self.base, b\"artist [CD5]\", name + b\" disc 1\", b\"song5.mp3\"\n            ),\n            os.path.join(\n                self.base,\n                b\"artist [CD5]\",\n                name_alt_case + b\" disc 2\",\n                b\"song6.mp3\",\n            ),\n            os.path.join(self.base, b\"artist [CD5]\", name + b\"S\", b\"song7.mp3\"),\n        ]\n\n        if not ascii:\n            self.dirs = [self._normalize_path(p) for p in self.dirs]\n            self.files = [self._normalize_path(p) for p in self.files]\n\n        for path in self.dirs:\n            os.mkdir(syspath(path))\n        if files:\n            for path in self.files:\n                _mkmp3(util.syspath(path))\n\n    def _normalize_path(self, path):\n        \"\"\"Normalize a path's Unicode combining form according to the\n        platform.\n        \"\"\"\n        path = path.decode(\"utf-8\")\n        norm_form = \"NFD\" if sys.platform == \"darwin\" else \"NFC\"\n        path = unicodedata.normalize(norm_form, path)\n        return path.encode(\"utf-8\")\n\n    def test_coalesce_nested_album_multiple_subdirs(self):\n        self.create_music()\n        albums = list(albums_in_dir(self.base))\n        assert len(albums) == 4\n        root, items = albums[0]\n        assert root == self.dirs[0:3]\n        assert len(items) == 3\n\n    def test_coalesce_nested_album_single_subdir(self):\n        self.create_music()\n        albums = list(albums_in_dir(self.base))\n        root, items = albums[1]\n        assert root == self.dirs[3:5]\n        assert len(items) == 1\n\n    def test_coalesce_flattened_album_case_typo(self):\n        self.create_music()\n        albums = list(albums_in_dir(self.base))\n        root, items = albums[2]\n        assert root == self.dirs[6:8]\n        assert len(items) == 2\n\n    def test_single_disc_album(self):\n        self.create_music()\n        albums = list(albums_in_dir(self.base))\n        root, items = albums[3]\n        assert root == self.dirs[8:]\n        assert len(items) == 1\n\n    def test_do_not_yield_empty_album(self):\n        self.create_music(files=False)\n        albums = list(albums_in_dir(self.base))\n        assert len(albums) == 0\n\n    def test_single_disc_unicode(self):\n        self.create_music(ascii=False)\n        albums = list(albums_in_dir(self.base))\n        root, items = albums[3]\n        assert root == self.dirs[8:]\n        assert len(items) == 1\n\n    def test_coalesce_multiple_unicode(self):\n        self.create_music(ascii=False)\n        albums = list(albums_in_dir(self.base))\n        assert len(albums) == 4\n        root, items = albums[0]\n        assert root == self.dirs[0:3]\n        assert len(items) == 3\n\n\nclass ReimportTest(AutotagImportTestCase):\n    \"\"\"Test \"re-imports\", in which the autotagging machinery is used for\n    music that's already in the library.\n\n    This works by importing new database entries for the same files and\n    replacing the old data with the new data. We also copy over flexible\n    attributes and the added date.\n    \"\"\"\n\n    matching = AutotagStub.GOOD\n\n    def setUp(self):\n        super().setUp()\n\n        # The existing album.\n        album = self.add_album_fixture()\n        album.added = 4242.0\n        album.foo = \"bar\"  # Some flexible attribute.\n        album.data_source = \"original_source\"\n        album.store()\n        item = album.items().get()\n        item.baz = \"qux\"\n        item.added = 4747.0\n        item.store()\n\n    def _setup_session(self, singletons=False):\n        self.setup_importer(import_dir=self.libdir, singletons=singletons)\n        self.importer.add_choice(importer.Action.APPLY)\n\n    def _album(self):\n        return self.lib.albums().get()\n\n    def _item(self):\n        return self.lib.items().get()\n\n    def test_reimported_album_gets_new_metadata(self):\n        self._setup_session()\n        assert self._album().album == \"\\xe4lbum\"\n        self.importer.run()\n        assert self._album().album == \"the album\"\n\n    def test_reimported_album_preserves_flexattr(self):\n        self._setup_session()\n        self.importer.run()\n        assert self._album().foo == \"bar\"\n\n    def test_reimported_album_preserves_added(self):\n        self._setup_session()\n        self.importer.run()\n        assert self._album().added == 4242.0\n\n    def test_reimported_album_preserves_item_flexattr(self):\n        self._setup_session()\n        self.importer.run()\n        assert self._item().baz == \"qux\"\n\n    def test_reimported_album_preserves_item_added(self):\n        self._setup_session()\n        self.importer.run()\n        assert self._item().added == 4747.0\n\n    def test_reimported_item_gets_new_metadata(self):\n        self._setup_session(True)\n        assert self._item().title == \"t\\xeftle 0\"\n        self.importer.run()\n        assert self._item().title == \"full\"\n\n    def test_reimported_item_preserves_flexattr(self):\n        self._setup_session(True)\n        self.importer.run()\n        assert self._item().baz == \"qux\"\n\n    def test_reimported_item_preserves_added(self):\n        self._setup_session(True)\n        self.importer.run()\n        assert self._item().added == 4747.0\n\n    def test_reimported_item_preserves_art(self):\n        self._setup_session()\n        art_source = os.path.join(_common.RSRC, b\"abbey.jpg\")\n        replaced_album = self._album()\n        replaced_album.set_art(art_source)\n        replaced_album.store()\n        old_artpath = replaced_album.art_filepath\n        self.importer.run()\n        new_album = self._album()\n        new_artpath = new_album.art_destination(art_source)\n        assert new_album.artpath == new_artpath\n        assert new_album.art_filepath.exists()\n        if new_artpath != old_artpath:\n            assert not old_artpath.exists()\n\n    def test_reimported_album_has_new_flexattr(self):\n        self._setup_session()\n        assert self._album().get(\"bandcamp_album_id\") is None\n        self.importer.run()\n        assert self._album().bandcamp_album_id == \"bc_url\"\n\n    def test_reimported_album_not_preserves_flexattr(self):\n        self._setup_session()\n\n        self.importer.run()\n        assert self._album().data_source == \"match_source\"\n\n\nclass ImportPretendTest(IOMixin, AutotagImportTestCase):\n    \"\"\"Test the pretend commandline option\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.album_track_path = self.prepare_album_for_import(1)[0]\n        self.single_path = self.prepare_track_for_import(2, self.import_path)\n        self.album_path = self.album_track_path.parent\n\n    def __run(self, importer):\n        with capture_log() as logs:\n            importer.run()\n\n        assert len(self.lib.items()) == 0\n        assert len(self.lib.albums()) == 0\n\n        return [line for line in logs if not line.startswith(\"Sending event:\")]\n        assert self._album().data_source == \"original_source\"\n\n    def test_import_singletons_pretend(self):\n        assert self.__run(self.setup_singleton_importer(pretend=True)) == [\n            f\"Singleton: {self.single_path}\",\n            f\"Singleton: {self.album_track_path}\",\n        ]\n\n    def test_import_album_pretend(self):\n        assert self.__run(self.setup_importer(pretend=True)) == [\n            f\"Album: {self.import_path}\",\n            f\"  {self.single_path}\",\n            f\"Album: {self.album_path}\",\n            f\"  {self.album_track_path}\",\n        ]\n\n    def test_import_pretend_empty(self):\n        empty_path = self.temp_dir_path / \"empty\"\n        empty_path.mkdir()\n\n        importer = self.setup_importer(pretend=True, import_dir=empty_path)\n\n        assert self.__run(importer) == [f\"No files imported from {empty_path}\"]\n\n\ndef mocked_get_albums_by_ids(ids):\n    \"\"\"Return album candidate for the given id.\n\n    The two albums differ only in the release title and artist name, so that\n    ID_RELEASE_0 is a closer match to the items created by\n    ImportHelper.prepare_album_for_import().\n    \"\"\"\n    # Map IDs to (release title, artist), so the distances are different.\n    album_artist_map = {\n        ImportIdTest.ID_RELEASE_0: (\"VALID_RELEASE_0\", \"TAG ARTIST\"),\n        ImportIdTest.ID_RELEASE_1: (\"VALID_RELEASE_1\", \"DISTANT_MATCH\"),\n    }\n\n    for id_ in ids:\n        album, artist = album_artist_map[id_]\n        yield AlbumInfo(\n            album_id=id_,\n            album=album,\n            artist_id=\"some-id\",\n            artist=artist,\n            albumstatus=\"Official\",\n            tracks=[\n                TrackInfo(\n                    track_id=\"bar\",\n                    title=\"foo\",\n                    artist_id=\"some-id\",\n                    artist=artist,\n                    length=59,\n                    index=9,\n                    track_allt=\"A2\",\n                )\n            ],\n        )\n\n\ndef mocked_get_tracks_by_ids(ids):\n    \"\"\"Return track candidate for the given id.\n\n    The two tracks differ only in the release title and artist name, so that\n    ID_RELEASE_0 is a closer match to the items created by\n    ImportHelper.prepare_album_for_import().\n    \"\"\"\n    # Map IDs to (recording title, artist), so the distances are different.\n    title_artist_map = {\n        ImportIdTest.ID_RECORDING_0: (\"VALID_RECORDING_0\", \"TAG ARTIST\"),\n        ImportIdTest.ID_RECORDING_1: (\"VALID_RECORDING_1\", \"DISTANT_MATCH\"),\n    }\n\n    for id_ in ids:\n        title, artist = title_artist_map[id_]\n        yield TrackInfo(\n            track_id=id_,\n            title=title,\n            artist_id=\"some-id\",\n            artist=artist,\n            length=59,\n        )\n\n\n@patch(\n    \"beets.metadata_plugins.tracks_for_ids\",\n    Mock(side_effect=mocked_get_tracks_by_ids),\n)\n@patch(\n    \"beets.metadata_plugins.albums_for_ids\",\n    Mock(side_effect=mocked_get_albums_by_ids),\n)\nclass ImportIdTest(ImportTestCase):\n    ID_RELEASE_0 = \"00000000-0000-0000-0000-000000000000\"\n    ID_RELEASE_1 = \"11111111-1111-1111-1111-111111111111\"\n    ID_RECORDING_0 = \"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\"\n    ID_RECORDING_1 = \"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb\"\n\n    def setUp(self):\n        super().setUp()\n        self.prepare_album_for_import(1)\n\n    def test_one_mbid_one_album(self):\n        self.setup_importer(search_ids=[self.ID_RELEASE_0])\n\n        self.importer.add_choice(importer.Action.APPLY)\n        self.importer.run()\n        assert self.lib.albums().get().album == \"VALID_RELEASE_0\"\n\n    def test_several_mbid_one_album(self):\n        self.setup_importer(search_ids=[self.ID_RELEASE_0, self.ID_RELEASE_1])\n\n        self.importer.add_choice(2)  # Pick the 2nd best match (release 1).\n        self.importer.add_choice(importer.Action.APPLY)\n        self.importer.run()\n        assert self.lib.albums().get().album == \"VALID_RELEASE_1\"\n\n    def test_one_mbid_one_singleton(self):\n        self.setup_singleton_importer(search_ids=[self.ID_RECORDING_0])\n\n        self.importer.add_choice(importer.Action.APPLY)\n        self.importer.run()\n        assert self.lib.items().get().title == \"VALID_RECORDING_0\"\n\n    def test_several_mbid_one_singleton(self):\n        self.setup_singleton_importer(\n            search_ids=[self.ID_RECORDING_0, self.ID_RECORDING_1]\n        )\n\n        self.importer.add_choice(2)  # Pick the 2nd best match (recording 1).\n        self.importer.add_choice(importer.Action.APPLY)\n        self.importer.run()\n        assert self.lib.items().get().title == \"VALID_RECORDING_1\"\n\n    def test_candidates_album(self):\n        \"\"\"Test directly ImportTask.lookup_candidates().\"\"\"\n        task = importer.ImportTask(\n            paths=self.import_dir, toppath=\"top path\", items=[_common.item()]\n        )\n\n        task.lookup_candidates([self.ID_RELEASE_0, self.ID_RELEASE_1])\n\n        assert {\"VALID_RELEASE_0\", \"VALID_RELEASE_1\"} == {\n            c.info.album for c in task.candidates\n        }\n\n    def test_candidates_singleton(self):\n        \"\"\"Test directly SingletonImportTask.lookup_candidates().\"\"\"\n        task = importer.SingletonImportTask(\n            toppath=\"top path\", item=_common.item()\n        )\n\n        task.lookup_candidates([self.ID_RECORDING_0, self.ID_RECORDING_1])\n\n        assert {\"VALID_RECORDING_0\", \"VALID_RECORDING_1\"} == {\n            c.info.title for c in task.candidates\n        }\n"
  },
  {
    "path": "test/test_library.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for non-query database functions of Item.\"\"\"\n\nimport os\nimport os.path\nimport re\nimport shutil\nimport stat\nimport unicodedata\nimport unittest\nfrom unittest.mock import patch\n\nimport pytest\nfrom mediafile import MediaFile, UnreadableFileError\n\nimport beets.dbcore.query\nimport beets.library\nimport beets.logging as blog\nfrom beets import config, plugins, util\nfrom beets.library import Album\nfrom beets.test import _common\nfrom beets.test._common import item\nfrom beets.test.helper import BeetsTestCase, ItemInDBTestCase, capture_log\nfrom beets.util import as_string, bytestring_path, normpath, syspath\n\n# Shortcut to path normalization.\nnp = util.normpath\n\n\nclass LoadTest(ItemInDBTestCase):\n    def test_load_restores_data_from_db(self):\n        original_title = self.i.title\n        self.i.title = \"something\"\n        self.i.load()\n        assert original_title == self.i.title\n\n    def test_load_clears_dirty_flags(self):\n        self.i.artist = \"something\"\n        assert \"artist\" in self.i._dirty\n        self.i.load()\n        assert \"artist\" not in self.i._dirty\n\n\nclass StoreTest(ItemInDBTestCase):\n    def test_store_changes_database_value(self):\n        new_year = 1987\n        self.i.year = new_year\n        self.i.store()\n\n        assert self.lib.get_item(self.i.id).year == new_year\n\n    def test_store_only_writes_dirty_fields(self):\n        new_year = 1987\n        self.i._values_fixed[\"year\"] = new_year  # change w/o dirtying\n        self.i.store()\n\n        assert self.lib.get_item(self.i.id).year != new_year\n\n    def test_store_clears_dirty_flags(self):\n        self.i.composer = \"tvp\"\n        self.i.store()\n        assert \"composer\" not in self.i._dirty\n\n    def test_store_album_cascades_flex_deletes(self):\n        album = Album(flex1=\"Flex-1\")\n        self.lib.add(album)\n        item = _common.item()\n        item.album_id = album.id\n        item.flex1 = \"Flex-1\"\n        self.lib.add(item)\n        del album.flex1\n        album.store()\n        assert \"flex1\" not in album\n        assert \"flex1\" not in album.items()[0]\n\n\nclass AddTest(BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n        self.i = item()\n\n    def test_item_add_inserts_row(self):\n        self.lib.add(self.i)\n        new_grouping = (\n            self.lib._connection()\n            .execute(\n                \"select grouping from items where composer = ?\",\n                (self.i.composer,),\n            )\n            .fetchone()[\"grouping\"]\n        )\n        assert new_grouping == self.i.grouping\n\n    def test_library_add_path_inserts_row(self):\n        i = beets.library.Item.from_path(\n            os.path.join(_common.RSRC, b\"full.mp3\")\n        )\n        self.lib.add(i)\n        new_grouping = (\n            self.lib._connection()\n            .execute(\n                \"select grouping from items where composer = ?\",\n                (self.i.composer,),\n            )\n            .fetchone()[\"grouping\"]\n        )\n        assert new_grouping == self.i.grouping\n\n    def test_library_add_one_database_change_event(self):\n        \"\"\"Test library.add emits only one database_change event.\"\"\"\n        self.item = _common.item()\n        self.item.path = beets.util.normpath(\n            os.path.join(\n                self.temp_dir,\n                b\"a\",\n                b\"b.mp3\",\n            )\n        )\n        self.item.album = \"a\"\n        self.item.title = \"b\"\n\n        blog.getLogger(\"beets\").set_global_level(blog.DEBUG)\n        with capture_log() as logs:\n            self.lib.add(self.item)\n\n        assert logs.count(\"Sending event: database_change\") == 1\n\n\nclass RemoveTest(ItemInDBTestCase):\n    def test_remove_deletes_from_db(self):\n        self.i.remove()\n        c = self.lib._connection().execute(\"select * from items\")\n        assert c.fetchone() is None\n\n\nclass GetSetTest(BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n        self.i = item()\n\n    def test_set_changes_value(self):\n        self.i.bpm = 4915\n        assert self.i.bpm == 4915\n\n    def test_set_sets_dirty_flag(self):\n        self.i.comp = not self.i.comp\n        assert \"comp\" in self.i._dirty\n\n    def test_set_does_not_dirty_if_value_unchanged(self):\n        self.i.title = self.i.title\n        assert \"title\" not in self.i._dirty\n\n    def test_invalid_field_raises_attributeerror(self):\n        with pytest.raises(AttributeError):\n            self.i.xyzzy\n\n    def test_album_fallback(self):\n        # integration test of item-album fallback\n        i = item(self.lib)\n        album = self.lib.add_album([i])\n        album[\"flex\"] = \"foo\"\n        album.store()\n\n        assert \"flex\" in i\n        assert \"flex\" not in i.keys(with_album=False)\n        assert i[\"flex\"] == \"foo\"\n        assert i.get(\"flex\") == \"foo\"\n        assert i.get(\"flex\", with_album=False) is None\n        assert i.get(\"flexx\") is None\n\n\nclass DestinationTest(BeetsTestCase):\n    \"\"\"Confirm tests handle temporary directory path containing '.'\"\"\"\n\n    def create_temp_dir(self, **kwargs):\n        kwargs[\"prefix\"] = \".\"\n        return super().create_temp_dir(**kwargs)\n\n    def setUp(self):\n        super().setUp()\n        self.i = item(self.lib)\n\n    def test_directory_works_with_trailing_slash(self):\n        self.lib.directory = b\"one/\"\n        self.lib.path_formats = [(\"default\", \"two\")]\n        assert self.i.destination() == np(\"one/two\")\n\n    def test_directory_works_without_trailing_slash(self):\n        self.lib.directory = b\"one\"\n        self.lib.path_formats = [(\"default\", \"two\")]\n        assert self.i.destination() == np(\"one/two\")\n\n    def test_destination_substitutes_metadata_values(self):\n        self.lib.directory = b\"base\"\n        self.lib.path_formats = [(\"default\", \"$album/$artist $title\")]\n        self.i.title = \"three\"\n        self.i.artist = \"two\"\n        self.i.album = \"one\"\n        assert self.i.destination() == np(\"base/one/two three\")\n\n    def test_destination_preserves_extension(self):\n        self.lib.directory = b\"base\"\n        self.lib.path_formats = [(\"default\", \"$title\")]\n        self.i.path = \"hey.audioformat\"\n        assert self.i.destination() == np(\"base/the title.audioformat\")\n\n    def test_lower_case_extension(self):\n        self.lib.directory = b\"base\"\n        self.lib.path_formats = [(\"default\", \"$title\")]\n        self.i.path = \"hey.MP3\"\n        assert self.i.destination() == np(\"base/the title.mp3\")\n\n    def test_destination_pads_some_indices(self):\n        self.lib.directory = b\"base\"\n        self.lib.path_formats = [\n            (\"default\", \"$track $tracktotal $disc $disctotal $bpm\")\n        ]\n        self.i.track = 1\n        self.i.tracktotal = 2\n        self.i.disc = 3\n        self.i.disctotal = 4\n        self.i.bpm = 5\n        assert self.i.destination() == np(\"base/01 02 03 04 5\")\n\n    def test_destination_pads_date_values(self):\n        self.lib.directory = b\"base\"\n        self.lib.path_formats = [(\"default\", \"$year-$month-$day\")]\n        self.i.year = 1\n        self.i.month = 2\n        self.i.day = 3\n        assert self.i.destination() == np(\"base/0001-02-03\")\n\n    def test_destination_escapes_slashes(self):\n        self.i.album = \"one/two\"\n        dest = self.i.destination()\n        assert b\"one\" in dest\n        assert b\"two\" in dest\n        assert b\"one/two\" not in dest\n\n    def test_destination_escapes_leading_dot(self):\n        self.i.album = \".something\"\n        dest = self.i.destination()\n        assert b\"something\" in dest\n        assert b\"/.something\" not in dest\n\n    def test_destination_preserves_legitimate_slashes(self):\n        self.i.artist = \"one\"\n        self.i.album = \"two\"\n        dest = self.i.destination()\n        assert os.path.join(b\"one\", b\"two\") in dest\n\n    def test_destination_long_names_truncated(self):\n        self.i.title = \"X\" * 300\n        self.i.artist = \"Y\" * 300\n        for c in self.i.destination().split(util.PATH_SEP):\n            assert len(c) <= 255\n\n    def test_destination_long_names_keep_extension(self):\n        self.i.title = \"X\" * 300\n        self.i.path = b\"something.extn\"\n        dest = self.i.destination()\n        assert dest[-5:] == b\".extn\"\n\n    def test_distination_windows_removes_both_separators(self):\n        self.i.title = \"one \\\\ two / three.mp3\"\n        with _common.platform_windows():\n            p = self.i.destination()\n        assert b\"one \\\\ two\" not in p\n        assert b\"one / two\" not in p\n        assert b\"two \\\\ three\" not in p\n        assert b\"two / three\" not in p\n\n    def test_path_with_format(self):\n        self.lib.path_formats = [(\"default\", \"$artist/$album ($format)\")]\n        p = self.i.destination()\n        assert b\"(FLAC)\" in p\n\n    def test_heterogeneous_album_gets_single_directory(self):\n        i1, i2 = item(), item()\n        self.lib.add_album([i1, i2])\n        i1.year, i2.year = 2009, 2010\n        self.lib.path_formats = [(\"default\", \"$album ($year)/$track $title\")]\n        dest1, dest2 = i1.destination(), i2.destination()\n        assert os.path.dirname(dest1) == os.path.dirname(dest2)\n\n    def test_default_path_for_non_compilations(self):\n        self.i.comp = False\n        self.lib.add_album([self.i])\n        self.lib.directory = b\"one\"\n        self.lib.path_formats = [(\"default\", \"two\"), (\"comp:true\", \"three\")]\n        assert self.i.destination() == np(\"one/two\")\n\n    def test_singleton_path(self):\n        i = item(self.lib)\n        self.lib.directory = b\"one\"\n        self.lib.path_formats = [\n            (\"default\", \"two\"),\n            (\"singleton:true\", \"four\"),\n            (\"comp:true\", \"three\"),\n        ]\n        assert i.destination() == np(\"one/four\")\n\n    def test_comp_before_singleton_path(self):\n        i = item(self.lib)\n        i.comp = True\n        self.lib.directory = b\"one\"\n        self.lib.path_formats = [\n            (\"default\", \"two\"),\n            (\"comp:true\", \"three\"),\n            (\"singleton:true\", \"four\"),\n        ]\n        assert i.destination() == np(\"one/three\")\n\n    def test_comp_path(self):\n        self.i.comp = True\n        self.lib.add_album([self.i])\n        self.lib.directory = b\"one\"\n        self.lib.path_formats = [(\"default\", \"two\"), (\"comp:true\", \"three\")]\n        assert self.i.destination() == np(\"one/three\")\n\n    def test_albumtype_query_path(self):\n        self.i.comp = True\n        self.lib.add_album([self.i])\n        self.i.albumtype = \"sometype\"\n        self.lib.directory = b\"one\"\n        self.lib.path_formats = [\n            (\"default\", \"two\"),\n            (\"albumtype:sometype\", \"four\"),\n            (\"comp:true\", \"three\"),\n        ]\n        assert self.i.destination() == np(\"one/four\")\n\n    def test_albumtype_path_fallback_to_comp(self):\n        self.i.comp = True\n        self.lib.add_album([self.i])\n        self.i.albumtype = \"sometype\"\n        self.lib.directory = b\"one\"\n        self.lib.path_formats = [\n            (\"default\", \"two\"),\n            (\"albumtype:anothertype\", \"four\"),\n            (\"comp:true\", \"three\"),\n        ]\n        assert self.i.destination() == np(\"one/three\")\n\n    def test_get_formatted_does_not_replace_separators(self):\n        with _common.platform_posix():\n            name = os.path.join(\"a\", \"b\")\n            self.i.title = name\n            newname = self.i.formatted().get(\"title\")\n        assert name == newname\n\n    def test_get_formatted_pads_with_zero(self):\n        with _common.platform_posix():\n            self.i.track = 1\n            name = self.i.formatted().get(\"track\")\n        assert name.startswith(\"0\")\n\n    def test_get_formatted_uses_kbps_bitrate(self):\n        with _common.platform_posix():\n            self.i.bitrate = 12345\n            val = self.i.formatted().get(\"bitrate\")\n        assert val == \"12kbps\"\n\n    def test_get_formatted_uses_khz_samplerate(self):\n        with _common.platform_posix():\n            self.i.samplerate = 12345\n            val = self.i.formatted().get(\"samplerate\")\n        assert val == \"12kHz\"\n\n    def test_get_formatted_datetime(self):\n        with _common.platform_posix():\n            self.i.added = 1368302461.210265\n            val = self.i.formatted().get(\"added\")\n        assert val.startswith(\"2013\")\n\n    def test_get_formatted_none(self):\n        with _common.platform_posix():\n            self.i.some_other_field = None\n            val = self.i.formatted().get(\"some_other_field\")\n        assert val == \"\"\n\n    def test_artist_falls_back_to_albumartist(self):\n        self.i.artist = \"\"\n        self.i.albumartist = \"something\"\n        self.lib.path_formats = [(\"default\", \"$artist\")]\n        p = self.i.destination()\n        assert p.rsplit(util.PATH_SEP, 1)[1] == b\"something\"\n\n    def test_albumartist_falls_back_to_artist(self):\n        self.i.artist = \"trackartist\"\n        self.i.albumartist = \"\"\n        self.lib.path_formats = [(\"default\", \"$albumartist\")]\n        p = self.i.destination()\n        assert p.rsplit(util.PATH_SEP, 1)[1] == b\"trackartist\"\n\n    def test_artist_overrides_albumartist(self):\n        self.i.artist = \"theartist\"\n        self.i.albumartist = \"something\"\n        self.lib.path_formats = [(\"default\", \"$artist\")]\n        p = self.i.destination()\n        assert p.rsplit(util.PATH_SEP, 1)[1] == b\"theartist\"\n\n    def test_albumartist_overrides_artist(self):\n        self.i.artist = \"theartist\"\n        self.i.albumartist = \"something\"\n        self.lib.path_formats = [(\"default\", \"$albumartist\")]\n        p = self.i.destination()\n        assert p.rsplit(util.PATH_SEP, 1)[1] == b\"something\"\n\n    def test_unicode_normalized_nfd_on_mac(self):\n        instr = unicodedata.normalize(\"NFC\", \"caf\\xe9\")\n        self.lib.path_formats = [(\"default\", instr)]\n        with patch(\"sys.platform\", \"darwin\"):\n            dest = self.i.destination(relative_to_libdir=True)\n        assert as_string(dest) == unicodedata.normalize(\"NFD\", instr)\n\n    def test_unicode_normalized_nfc_on_linux(self):\n        instr = unicodedata.normalize(\"NFD\", \"caf\\xe9\")\n        self.lib.path_formats = [(\"default\", instr)]\n        with patch(\"sys.platform\", \"linux\"):\n            dest = self.i.destination(relative_to_libdir=True)\n        assert as_string(dest) == unicodedata.normalize(\"NFC\", instr)\n\n    def test_unicode_extension_in_fragment(self):\n        self.lib.path_formats = [(\"default\", \"foo\")]\n        self.i.path = util.bytestring_path(\"bar.caf\\xe9\")\n        with patch(\"sys.platform\", \"linux\"):\n            dest = self.i.destination(relative_to_libdir=True)\n        assert as_string(dest) == \"foo.caf\\xe9\"\n\n    def test_asciify_and_replace(self):\n        config[\"asciify_paths\"] = True\n        self.lib.replacements = [(re.compile('\"'), \"q\")]\n        self.lib.directory = b\"lib\"\n        self.lib.path_formats = [(\"default\", \"$title\")]\n        self.i.title = \"\\u201c\\u00f6\\u2014\\u00cf\\u201d\"\n        assert self.i.destination() == np(\"lib/qo--Iq\")\n\n    def test_asciify_character_expanding_to_slash(self):\n        config[\"asciify_paths\"] = True\n        self.lib.directory = b\"lib\"\n        self.lib.path_formats = [(\"default\", \"$title\")]\n        self.i.title = \"ab\\xa2\\xbdd\"\n        assert self.i.destination() == np(\"lib/abC_ 1_2d\")\n\n    def test_destination_with_replacements(self):\n        self.lib.directory = b\"base\"\n        self.lib.replacements = [(re.compile(r\"a\"), \"e\")]\n        self.lib.path_formats = [(\"default\", \"$album/$title\")]\n        self.i.title = \"foo\"\n        self.i.album = \"bar\"\n        assert self.i.destination() == np(\"base/ber/foo\")\n\n    @unittest.skip(\"unimplemented: #359\")\n    def test_destination_with_empty_component(self):\n        self.lib.directory = b\"base\"\n        self.lib.replacements = [(re.compile(r\"^$\"), \"_\")]\n        self.lib.path_formats = [(\"default\", \"$album/$artist/$title\")]\n        self.i.title = \"three\"\n        self.i.artist = \"\"\n        self.i.albumartist = \"\"\n        self.i.album = \"one\"\n        assert self.i.destination() == np(\"base/one/_/three\")\n\n    @unittest.skip(\"unimplemented: #359\")\n    def test_destination_with_empty_final_component(self):\n        self.lib.directory = b\"base\"\n        self.lib.replacements = [(re.compile(r\"^$\"), \"_\")]\n        self.lib.path_formats = [(\"default\", \"$album/$title\")]\n        self.i.title = \"\"\n        self.i.album = \"one\"\n        self.i.path = \"foo.mp3\"\n        assert self.i.destination() == np(\"base/one/_.mp3\")\n\n    def test_album_field_query(self):\n        self.lib.directory = b\"one\"\n        self.lib.path_formats = [(\"default\", \"two\"), (\"flex:foo\", \"three\")]\n        album = self.lib.add_album([self.i])\n        assert self.i.destination() == np(\"one/two\")\n        album[\"flex\"] = \"foo\"\n        album.store()\n        assert self.i.destination() == np(\"one/three\")\n\n    def test_album_field_in_template(self):\n        self.lib.directory = b\"one\"\n        self.lib.path_formats = [(\"default\", \"$flex/two\")]\n        album = self.lib.add_album([self.i])\n        album[\"flex\"] = \"foo\"\n        album.store()\n        assert self.i.destination() == np(\"one/foo/two\")\n\n\nclass ItemFormattedMappingTest(ItemInDBTestCase):\n    def test_formatted_item_value(self):\n        formatted = self.i.formatted()\n        assert formatted[\"artist\"] == \"the artist\"\n\n    def test_get_unset_field(self):\n        formatted = self.i.formatted()\n        with pytest.raises(KeyError):\n            formatted[\"other_field\"]\n\n    def test_get_method_with_default(self):\n        formatted = self.i.formatted()\n        assert formatted.get(\"other_field\") == \"\"\n\n    def test_get_method_with_specified_default(self):\n        formatted = self.i.formatted()\n        assert formatted.get(\"other_field\", \"default\") == \"default\"\n\n    def test_item_precedence(self):\n        album = self.lib.add_album([self.i])\n        album[\"artist\"] = \"foo\"\n        album.store()\n        assert \"foo\" != self.i.formatted().get(\"artist\")\n\n    def test_album_flex_field(self):\n        album = self.lib.add_album([self.i])\n        album[\"flex\"] = \"foo\"\n        album.store()\n        assert \"foo\" == self.i.formatted().get(\"flex\")\n\n    def test_album_field_overrides_item_field_for_path(self):\n        # Make the album inconsistent with the item.\n        album = self.lib.add_album([self.i])\n        album.album = \"foo\"\n        album.store()\n        self.i.album = \"bar\"\n        self.i.store()\n\n        # Ensure the album takes precedence.\n        formatted = self.i.formatted(for_path=True)\n        assert formatted[\"album\"] == \"foo\"\n\n    def test_artist_falls_back_to_albumartist(self):\n        self.i.artist = \"\"\n        formatted = self.i.formatted()\n        assert formatted[\"artist\"] == \"the album artist\"\n\n    def test_albumartist_falls_back_to_artist(self):\n        self.i.albumartist = \"\"\n        formatted = self.i.formatted()\n        assert formatted[\"albumartist\"] == \"the artist\"\n\n    def test_both_artist_and_albumartist_empty(self):\n        self.i.artist = \"\"\n        self.i.albumartist = \"\"\n        formatted = self.i.formatted()\n        assert formatted[\"albumartist\"] == \"\"\n\n\nclass PathFormattingMixin:\n    \"\"\"Utilities for testing path formatting.\"\"\"\n\n    i: beets.library.Item\n    lib: beets.library.Library\n\n    def _setf(self, fmt):\n        self.lib.path_formats.insert(0, (\"default\", fmt))\n\n    def _assert_dest(self, dest, i=None):\n        if i is None:\n            i = self.i\n\n        # Handle paths on Windows.\n        if os.path.sep != \"/\":\n            dest = dest.replace(b\"/\", os.path.sep.encode())\n\n            # Paths are normalized based on the CWD.\n            dest = normpath(dest)\n\n        actual = i.destination()\n\n        assert actual == dest\n\n\nclass DestinationFunctionTest(BeetsTestCase, PathFormattingMixin):\n    def setUp(self):\n        super().setUp()\n        self.lib.directory = b\"/base\"\n        self.lib.path_formats = [(\"default\", \"path\")]\n        self.i = item(self.lib)\n\n    def test_upper_case_literal(self):\n        self._setf(\"%upper{foo}\")\n        self._assert_dest(b\"/base/FOO\")\n\n    def test_upper_case_variable(self):\n        self._setf(\"%upper{$title}\")\n        self._assert_dest(b\"/base/THE TITLE\")\n\n    def test_capitalize_variable(self):\n        self._setf(\"%capitalize{$title}\")\n        self._assert_dest(b\"/base/The title\")\n\n    def test_title_case_variable(self):\n        self._setf(\"%title{$title}\")\n        self._assert_dest(b\"/base/The Title\")\n\n    def test_title_case_variable_aphostrophe(self):\n        self._setf(\"%title{I can't}\")\n        self._assert_dest(b\"/base/I Can't\")\n\n    def test_asciify_variable(self):\n        self._setf(\"%asciify{ab\\xa2\\xbdd}\")\n        self._assert_dest(b\"/base/abC_ 1_2d\")\n\n    def test_left_variable(self):\n        self._setf(\"%left{$title, 3}\")\n        self._assert_dest(b\"/base/the\")\n\n    def test_right_variable(self):\n        self._setf(\"%right{$title,3}\")\n        self._assert_dest(b\"/base/tle\")\n\n    def test_if_false(self):\n        self._setf(\"x%if{,foo}\")\n        self._assert_dest(b\"/base/x\")\n\n    def test_if_false_value(self):\n        self._setf(\"x%if{false,foo}\")\n        self._assert_dest(b\"/base/x\")\n\n    def test_if_true(self):\n        self._setf(\"%if{bar,foo}\")\n        self._assert_dest(b\"/base/foo\")\n\n    def test_if_else_false(self):\n        self._setf(\"%if{,foo,baz}\")\n        self._assert_dest(b\"/base/baz\")\n\n    def test_if_else_false_value(self):\n        self._setf(\"%if{false,foo,baz}\")\n        self._assert_dest(b\"/base/baz\")\n\n    def test_if_int_value(self):\n        self._setf(\"%if{0,foo,baz}\")\n        self._assert_dest(b\"/base/baz\")\n\n    def test_nonexistent_function(self):\n        self._setf(\"%foo{bar}\")\n        self._assert_dest(b\"/base/%foo{bar}\")\n\n    def test_if_def_field_return_self(self):\n        self.i.bar = 3\n        self._setf(\"%ifdef{bar}\")\n        self._assert_dest(b\"/base/3\")\n\n    def test_if_def_field_not_defined(self):\n        self._setf(\" %ifdef{bar}/$artist\")\n        self._assert_dest(b\"/base/the artist\")\n\n    def test_if_def_field_not_defined_2(self):\n        self._setf(\"$artist/%ifdef{bar}\")\n        self._assert_dest(b\"/base/the artist\")\n\n    def test_if_def_true(self):\n        self._setf(\"%ifdef{artist,cool}\")\n        self._assert_dest(b\"/base/cool\")\n\n    def test_if_def_true_complete(self):\n        self.i.series = \"Now\"\n        self._setf(\"%ifdef{series,$series Series,Albums}/$album\")\n        self._assert_dest(b\"/base/Now Series/the album\")\n\n    def test_if_def_false_complete(self):\n        self._setf(\"%ifdef{plays,$plays,not_played}\")\n        self._assert_dest(b\"/base/not_played\")\n\n    def test_first(self):\n        self.i.albumtypes = [\"album\", \"compilation\"]\n        self._setf(\"%first{$albumtypes}\")\n        self._assert_dest(b\"/base/album\")\n\n    def test_first_skip(self):\n        self.i.albumtype = \"album; ep; compilation\"\n        self._setf(\"%first{$albumtype,1,2}\")\n        self._assert_dest(b\"/base/compilation\")\n\n    def test_first_different_sep(self):\n        self._setf(\"%first{Alice / Bob / Eve,2,0, / , & }\")\n        self._assert_dest(b\"/base/Alice & Bob\")\n\n\nclass DisambiguationTest(BeetsTestCase, PathFormattingMixin):\n    def setUp(self):\n        super().setUp()\n        self.lib.directory = b\"/base\"\n        self.lib.path_formats = [(\"default\", \"path\")]\n\n        self.i1 = item()\n        self.i1.year = 2001\n        self.lib.add_album([self.i1])\n        self.i2 = item()\n        self.i2.year = 2002\n        self.lib.add_album([self.i2])\n        self.lib._connection().commit()\n\n        self._setf(\"foo%aunique{albumartist album,year}/$title\")\n\n    def test_unique_expands_to_disambiguating_year(self):\n        self._assert_dest(b\"/base/foo [2001]/the title\", self.i1)\n\n    def test_unique_with_default_arguments_uses_albumtype(self):\n        album2 = self.lib.get_album(self.i1)\n        album2.albumtype = \"bar\"\n        album2.store()\n        self._setf(\"foo%aunique{}/$title\")\n        self._assert_dest(b\"/base/foo [bar]/the title\", self.i1)\n\n    def test_unique_expands_to_nothing_for_distinct_albums(self):\n        album2 = self.lib.get_album(self.i2)\n        album2.album = \"different album\"\n        album2.store()\n\n        self._assert_dest(b\"/base/foo/the title\", self.i1)\n\n    def test_use_fallback_numbers_when_identical(self):\n        album2 = self.lib.get_album(self.i2)\n        album2.year = 2001\n        album2.store()\n\n        self._assert_dest(b\"/base/foo [1]/the title\", self.i1)\n        self._assert_dest(b\"/base/foo [2]/the title\", self.i2)\n\n    def test_unique_falls_back_to_second_distinguishing_field(self):\n        self._setf(\"foo%aunique{albumartist album,month year}/$title\")\n        self._assert_dest(b\"/base/foo [2001]/the title\", self.i1)\n\n    def test_unique_sanitized(self):\n        album2 = self.lib.get_album(self.i2)\n        album2.year = 2001\n        album1 = self.lib.get_album(self.i1)\n        album1.albumtype = \"foo/bar\"\n        album2.store()\n        album1.store()\n        self._setf(\"foo%aunique{albumartist album,albumtype}/$title\")\n        self._assert_dest(b\"/base/foo [foo_bar]/the title\", self.i1)\n\n    def test_drop_empty_disambig_string(self):\n        album1 = self.lib.get_album(self.i1)\n        album1.albumdisambig = None\n        album2 = self.lib.get_album(self.i2)\n        album2.albumdisambig = \"foo\"\n        album1.store()\n        album2.store()\n        self._setf(\"foo%aunique{albumartist album,albumdisambig}/$title\")\n        self._assert_dest(b\"/base/foo/the title\", self.i1)\n\n    def test_change_brackets(self):\n        self._setf(\"foo%aunique{albumartist album,year,()}/$title\")\n        self._assert_dest(b\"/base/foo (2001)/the title\", self.i1)\n\n    def test_remove_brackets(self):\n        self._setf(\"foo%aunique{albumartist album,year,}/$title\")\n        self._assert_dest(b\"/base/foo 2001/the title\", self.i1)\n\n    def test_key_flexible_attribute(self):\n        album1 = self.lib.get_album(self.i1)\n        album1.flex = \"flex1\"\n        album2 = self.lib.get_album(self.i2)\n        album2.flex = \"flex2\"\n        album1.store()\n        album2.store()\n        self._setf(\"foo%aunique{albumartist album flex,year}/$title\")\n        self._assert_dest(b\"/base/foo/the title\", self.i1)\n\n\nclass SingletonDisambiguationTest(BeetsTestCase, PathFormattingMixin):\n    def setUp(self):\n        super().setUp()\n        self.lib.directory = b\"/base\"\n        self.lib.path_formats = [(\"default\", \"path\")]\n\n        self.i1 = item()\n        self.i1.year = 2001\n        self.lib.add(self.i1)\n        self.i2 = item()\n        self.i2.year = 2002\n        self.lib.add(self.i2)\n        self.lib._connection().commit()\n\n        self._setf(\"foo/$title%sunique{artist title,year}\")\n\n    def test_sunique_expands_to_disambiguating_year(self):\n        self._assert_dest(b\"/base/foo/the title [2001]\", self.i1)\n\n    def test_sunique_with_default_arguments_uses_trackdisambig(self):\n        self.i1.trackdisambig = \"live version\"\n        self.i1.year = self.i2.year\n        self.i1.store()\n        self._setf(\"foo/$title%sunique{}\")\n        self._assert_dest(b\"/base/foo/the title [live version]\", self.i1)\n\n    def test_sunique_expands_to_nothing_for_distinct_singletons(self):\n        self.i2.title = \"different track\"\n        self.i2.store()\n\n        self._assert_dest(b\"/base/foo/the title\", self.i1)\n\n    def test_sunique_does_not_match_album(self):\n        self.lib.add_album([self.i2])\n        self._assert_dest(b\"/base/foo/the title\", self.i1)\n\n    def test_sunique_use_fallback_numbers_when_identical(self):\n        self.i2.year = self.i1.year\n        self.i2.store()\n\n        self._assert_dest(b\"/base/foo/the title [1]\", self.i1)\n        self._assert_dest(b\"/base/foo/the title [2]\", self.i2)\n\n    def test_sunique_falls_back_to_second_distinguishing_field(self):\n        self._setf(\"foo/$title%sunique{albumartist album,month year}\")\n        self._assert_dest(b\"/base/foo/the title [2001]\", self.i1)\n\n    def test_sunique_sanitized(self):\n        self.i2.year = self.i1.year\n        self.i1.trackdisambig = \"foo/bar\"\n        self.i2.store()\n        self.i1.store()\n        self._setf(\"foo/$title%sunique{artist title,trackdisambig}\")\n        self._assert_dest(b\"/base/foo/the title [foo_bar]\", self.i1)\n\n    def test_drop_empty_disambig_string(self):\n        self.i1.trackdisambig = None\n        self.i2.trackdisambig = \"foo\"\n        self.i1.store()\n        self.i2.store()\n        self._setf(\"foo/$title%sunique{albumartist album,trackdisambig}\")\n        self._assert_dest(b\"/base/foo/the title\", self.i1)\n\n    def test_change_brackets(self):\n        self._setf(\"foo/$title%sunique{artist title,year,()}\")\n        self._assert_dest(b\"/base/foo/the title (2001)\", self.i1)\n\n    def test_remove_brackets(self):\n        self._setf(\"foo/$title%sunique{artist title,year,}\")\n        self._assert_dest(b\"/base/foo/the title 2001\", self.i1)\n\n    def test_key_flexible_attribute(self):\n        self.i1.flex = \"flex1\"\n        self.i2.flex = \"flex2\"\n        self.i1.store()\n        self.i2.store()\n        self._setf(\"foo/$title%sunique{artist title flex,year}\")\n        self._assert_dest(b\"/base/foo/the title\", self.i1)\n\n\nclass PluginDestinationTest(BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n\n        # Mock beets.plugins.item_field_getters.\n        self._tv_map = {}\n\n        def field_getters():\n            getters = {}\n            for key, value in self._tv_map.items():\n                getters[key] = lambda _: value\n            return getters\n\n        self.old_field_getters = plugins.item_field_getters\n        plugins.item_field_getters = field_getters\n\n        self.lib.directory = b\"/base\"\n        self.lib.path_formats = [(\"default\", \"$artist $foo\")]\n        self.i = item(self.lib)\n\n    def tearDown(self):\n        super().tearDown()\n        plugins.item_field_getters = self.old_field_getters\n\n    def _assert_dest(self, dest):\n        with _common.platform_posix():\n            the_dest = self.i.destination()\n        assert the_dest == b\"/base/\" + dest\n\n    def test_undefined_value_not_substituted(self):\n        self._assert_dest(b\"the artist $foo\")\n\n    def test_plugin_value_not_substituted(self):\n        self._tv_map = {\n            \"foo\": \"bar\",\n        }\n        self._assert_dest(b\"the artist bar\")\n\n    def test_plugin_value_overrides_attribute(self):\n        self._tv_map = {\n            \"artist\": \"bar\",\n        }\n        self._assert_dest(b\"bar $foo\")\n\n    def test_plugin_value_sanitized(self):\n        self._tv_map = {\n            \"foo\": \"bar/baz\",\n        }\n        self._assert_dest(b\"the artist bar_baz\")\n\n\nclass AlbumInfoTest(BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n        self.i = item()\n        self.lib.add_album((self.i,))\n\n    def test_albuminfo_reflects_metadata(self):\n        ai = self.lib.get_album(self.i)\n        assert ai.mb_albumartistid == self.i.mb_albumartistid\n        assert ai.albumartist == self.i.albumartist\n        assert ai.album == self.i.album\n        assert ai.year == self.i.year\n\n    def test_albuminfo_stores_art(self):\n        ai = self.lib.get_album(self.i)\n        ai.artpath = \"/my/great/art\"\n        ai.store()\n        new_ai = self.lib.get_album(self.i)\n        assert new_ai.artpath == b\"/my/great/art\"\n\n    def test_albuminfo_for_two_items_doesnt_duplicate_row(self):\n        i2 = item(self.lib)\n        self.lib.get_album(self.i)\n        self.lib.get_album(i2)\n\n        c = self.lib._connection().cursor()\n        c.execute(\"select * from albums where album=?\", (self.i.album,))\n        # Cursor should only return one row.\n        assert c.fetchone() is not None\n        assert c.fetchone() is None\n\n    def test_individual_tracks_have_no_albuminfo(self):\n        i2 = item()\n        i2.album = \"aTotallyDifferentAlbum\"\n        self.lib.add(i2)\n        ai = self.lib.get_album(i2)\n        assert ai is None\n\n    def test_get_album_by_id(self):\n        ai = self.lib.get_album(self.i)\n        ai = self.lib.get_album(self.i.id)\n        assert ai is not None\n\n    def test_album_items_consistent(self):\n        ai = self.lib.get_album(self.i)\n        for i in ai.items():\n            if i.id == self.i.id:\n                break\n        else:\n            self.fail(\"item not found\")\n\n    def test_albuminfo_changes_affect_items(self):\n        ai = self.lib.get_album(self.i)\n        ai.album = \"myNewAlbum\"\n        ai.store()\n        i = self.lib.items()[0]\n        assert i.album == \"myNewAlbum\"\n\n    def test_albuminfo_change_albumartist_changes_items(self):\n        ai = self.lib.get_album(self.i)\n        ai.albumartist = \"myNewArtist\"\n        ai.store()\n        i = self.lib.items()[0]\n        assert i.albumartist == \"myNewArtist\"\n        assert i.artist != \"myNewArtist\"\n\n    def test_albuminfo_change_artist_does_change_items(self):\n        ai = self.lib.get_album(self.i)\n        ai.artist = \"myNewArtist\"\n        ai.store(inherit=True)\n        i = self.lib.items()[0]\n        assert i.artist == \"myNewArtist\"\n\n    def test_albuminfo_change_artist_does_not_change_items(self):\n        ai = self.lib.get_album(self.i)\n        ai.artist = \"myNewArtist\"\n        ai.store(inherit=False)\n        i = self.lib.items()[0]\n        assert i.artist != \"myNewArtist\"\n\n    def test_albuminfo_remove_removes_items(self):\n        item_id = self.i.id\n        self.lib.get_album(self.i).remove()\n        c = self.lib._connection().execute(\n            \"SELECT id FROM items WHERE id=?\", (item_id,)\n        )\n        assert c.fetchone() is None\n\n    def test_removing_last_item_removes_album(self):\n        assert len(self.lib.albums()) == 1\n        self.i.remove()\n        assert len(self.lib.albums()) == 0\n\n    def test_noop_albuminfo_changes_affect_items(self):\n        i = self.lib.items()[0]\n        i.album = \"foobar\"\n        i.store()\n        ai = self.lib.get_album(self.i)\n        ai.album = ai.album\n        ai.store()\n        i = self.lib.items()[0]\n        assert i.album == ai.album\n\n\nclass ArtDestinationTest(BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n        config[\"art_filename\"] = \"artimage\"\n        config[\"replace\"] = {\"X\": \"Y\"}\n        self.lib.replacements = [(re.compile(\"X\"), \"Y\")]\n        self.i = item(self.lib)\n        self.i.path = self.i.destination()\n        self.ai = self.lib.add_album((self.i,))\n\n    def test_art_filename_respects_setting(self):\n        art = self.ai.art_destination(\"something.jpg\")\n        new_art = bytestring_path(f\"{os.path.sep}artimage.jpg\")\n        assert new_art in art\n\n    def test_art_path_in_item_dir(self):\n        art = self.ai.art_destination(\"something.jpg\")\n        track = self.i.destination()\n        assert os.path.dirname(art) == os.path.dirname(track)\n\n    def test_art_path_sanitized(self):\n        config[\"art_filename\"] = \"artXimage\"\n        art = self.ai.art_destination(\"something.jpg\")\n        assert b\"artYimage\" in art\n\n\nclass PathStringTest(BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n        self.i = item(self.lib)\n\n    def test_item_path_is_bytestring(self):\n        assert isinstance(self.i.path, bytes)\n\n    def test_fetched_item_path_is_bytestring(self):\n        i = next(iter(self.lib.items()))\n        assert isinstance(i.path, bytes)\n\n    def test_unicode_path_becomes_bytestring(self):\n        self.i.path = \"unicodepath\"\n        assert isinstance(self.i.path, bytes)\n\n    def test_unicode_in_database_becomes_bytestring(self):\n        self.lib._connection().execute(\n            \"\"\"\n        update items set path=? where id=?\n        \"\"\",\n            (self.i.id, \"somepath\"),\n        )\n        i = next(iter(self.lib.items()))\n        assert isinstance(i.path, bytes)\n\n    def test_special_chars_preserved_in_database(self):\n        path = \"b\\xe1r\".encode()\n        self.i.path = path\n        self.i.store()\n        i = next(iter(self.lib.items()))\n        assert i.path == path\n\n    def test_special_char_path_added_to_database(self):\n        self.i.remove()\n        path = \"b\\xe1r\".encode()\n        i = item()\n        i.path = path\n        self.lib.add(i)\n        i = next(iter(self.lib.items()))\n        assert i.path == path\n\n    def test_destination_returns_bytestring(self):\n        self.i.artist = \"b\\xe1r\"\n        dest = self.i.destination()\n        assert isinstance(dest, bytes)\n\n    def test_art_destination_returns_bytestring(self):\n        self.i.artist = \"b\\xe1r\"\n        alb = self.lib.add_album([self.i])\n        dest = alb.art_destination(\"image.jpg\")\n        assert isinstance(dest, bytes)\n\n    def test_artpath_stores_special_chars(self):\n        path = b\"b\\xe1r\"\n        alb = self.lib.add_album([self.i])\n        alb.artpath = path\n        alb.store()\n        alb = self.lib.get_album(self.i)\n        assert path == alb.artpath\n\n    def test_sanitize_path_with_special_chars(self):\n        path = \"b\\xe1r?\"\n        new_path = util.sanitize_path(path)\n        assert new_path.startswith(\"b\\xe1r\")\n\n    def test_sanitize_path_returns_unicode(self):\n        path = \"b\\xe1r?\"\n        new_path = util.sanitize_path(path)\n        assert isinstance(new_path, str)\n\n    def test_unicode_artpath_becomes_bytestring(self):\n        alb = self.lib.add_album([self.i])\n        alb.artpath = \"somep\\xe1th\"\n        assert isinstance(alb.artpath, bytes)\n\n    def test_unicode_artpath_in_database_decoded(self):\n        alb = self.lib.add_album([self.i])\n        self.lib._connection().execute(\n            \"update albums set artpath=? where id=?\", (\"somep\\xe1th\", alb.id)\n        )\n        alb = self.lib.get_album(alb.id)\n        assert isinstance(alb.artpath, bytes)\n\n\nclass MtimeTest(BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n        self.ipath = os.path.join(self.temp_dir, b\"testfile.mp3\")\n        shutil.copy(\n            syspath(os.path.join(_common.RSRC, b\"full.mp3\")),\n            syspath(self.ipath),\n        )\n        self.i = beets.library.Item.from_path(self.ipath)\n        self.lib.add(self.i)\n\n    def tearDown(self):\n        super().tearDown()\n        if os.path.exists(self.ipath):\n            os.remove(self.ipath)\n\n    def _mtime(self):\n        return int(os.path.getmtime(self.ipath))\n\n    def test_mtime_initially_up_to_date(self):\n        assert self.i.mtime >= self._mtime()\n\n    def test_mtime_reset_on_db_modify(self):\n        self.i.title = \"something else\"\n        assert self.i.mtime < self._mtime()\n\n    def test_mtime_up_to_date_after_write(self):\n        self.i.title = \"something else\"\n        self.i.write()\n        assert self.i.mtime >= self._mtime()\n\n    def test_mtime_up_to_date_after_read(self):\n        self.i.title = \"something else\"\n        self.i.read()\n        assert self.i.mtime >= self._mtime()\n\n\nclass ImportTimeTest(BeetsTestCase):\n    def added(self):\n        self.track = item()\n        self.album = self.lib.add_album((self.track,))\n        assert self.album.added > 0\n        assert self.track.added > 0\n\n    def test_atime_for_singleton(self):\n        self.singleton = item(self.lib)\n        assert self.singleton.added > 0\n\n\nclass TemplateTest(ItemInDBTestCase):\n    def test_year_formatted_in_template(self):\n        self.i.year = 123\n        self.i.store()\n        assert self.i.evaluate_template(\"$year\") == \"0123\"\n\n    def test_album_flexattr_appears_in_item_template(self):\n        self.album = self.lib.add_album([self.i])\n        self.album.foo = \"baz\"\n        self.album.store()\n        assert self.i.evaluate_template(\"$foo\") == \"baz\"\n\n    def test_album_and_item_format(self):\n        config[\"format_album\"] = \"foö $foo\"\n        album = beets.library.Album()\n        album.foo = \"bar\"\n        album.tagada = \"togodo\"\n        assert f\"{album}\" == \"foö bar\"\n        assert f\"{album:$tagada}\" == \"togodo\"\n        assert str(album) == \"foö bar\"\n        assert bytes(album) == b\"fo\\xc3\\xb6 bar\"\n\n        config[\"format_item\"] = \"bar $foo\"\n        item = beets.library.Item()\n        item.foo = \"bar\"\n        item.tagada = \"togodo\"\n        assert f\"{item}\" == \"bar bar\"\n        assert f\"{item:$tagada}\" == \"togodo\"\n\n\nclass UnicodePathTest(ItemInDBTestCase):\n    def test_unicode_path(self):\n        self.i.path = os.path.join(_common.RSRC, \"unicode\\u2019d.mp3\".encode())\n        # If there are any problems with unicode paths, we will raise\n        # here and fail.\n        self.i.read()\n        self.i.write()\n\n\nclass WriteTest(BeetsTestCase):\n    def test_write_nonexistant(self):\n        item = self.create_item()\n        item.path = b\"/path/does/not/exist\"\n        with pytest.raises(beets.library.ReadError):\n            item.write()\n\n    def test_no_write_permission(self):\n        item = self.add_item_fixture()\n        path = syspath(item.path)\n        os.chmod(path, stat.S_IRUSR)\n\n        try:\n            with pytest.raises(beets.library.WriteError):\n                item.write()\n\n        finally:\n            # Restore write permissions so the file can be cleaned up.\n            os.chmod(path, stat.S_IRUSR | stat.S_IWUSR)\n\n    def test_write_with_custom_path(self):\n        item = self.add_item_fixture()\n        custom_path = os.path.join(self.temp_dir, b\"custom.mp3\")\n        shutil.copy(syspath(item.path), syspath(custom_path))\n\n        item[\"artist\"] = \"new artist\"\n        assert MediaFile(syspath(custom_path)).artist != \"new artist\"\n        assert MediaFile(syspath(item.path)).artist != \"new artist\"\n\n        item.write(custom_path)\n        assert MediaFile(syspath(custom_path)).artist == \"new artist\"\n        assert MediaFile(syspath(item.path)).artist != \"new artist\"\n\n    def test_write_custom_tags(self):\n        item = self.add_item_fixture(artist=\"old artist\")\n        item.write(tags={\"artist\": \"new artist\"})\n        assert item.artist != \"new artist\"\n        assert MediaFile(syspath(item.path)).artist == \"new artist\"\n\n    def test_write_multi_tags(self):\n        item = self.add_item_fixture(artist=\"old artist\")\n        item.write(tags={\"artists\": [\"old artist\", \"another artist\"]})\n\n        assert MediaFile(syspath(item.path)).artists == [\n            \"old artist\",\n            \"another artist\",\n        ]\n\n    def test_write_multi_tags_id3v23(self):\n        item = self.add_item_fixture(artist=\"old artist\")\n        item.write(\n            tags={\"artists\": [\"old artist\", \"another artist\"]}, id3v23=True\n        )\n\n        assert MediaFile(syspath(item.path)).artists == [\n            \"old artist/another artist\"\n        ]\n\n    def test_write_date_field(self):\n        # Since `date` is not a MediaField, this should do nothing.\n        item = self.add_item_fixture()\n        clean_year = item.year\n        item.date = \"foo\"\n        item.write()\n        assert MediaFile(syspath(item.path)).year == clean_year\n\n\nclass ItemReadTest(unittest.TestCase):\n    def test_unreadable_raise_read_error(self):\n        unreadable = os.path.join(_common.RSRC, b\"image-2x3.png\")\n        item = beets.library.Item()\n        with pytest.raises(beets.library.ReadError) as exc_info:\n            item.read(unreadable)\n        assert isinstance(exc_info.value.reason, UnreadableFileError)\n\n    def test_nonexistent_raise_read_error(self):\n        item = beets.library.Item()\n        with pytest.raises(beets.library.ReadError):\n            item.read(\"/thisfiledoesnotexist\")\n\n\nclass FilesizeTest(BeetsTestCase):\n    def test_filesize(self):\n        item = self.add_item_fixture()\n        assert item.filesize != 0\n\n    def test_nonexistent_file(self):\n        item = beets.library.Item()\n        assert item.filesize == 0\n\n\nclass ParseQueryTest(unittest.TestCase):\n    def test_parse_invalid_query_string(self):\n        with pytest.raises(beets.dbcore.query.ParsingError):\n            beets.library.parse_query_string('foo\"', None)\n\n    def test_parse_bytes(self):\n        with pytest.raises(AssertionError):\n            beets.library.parse_query_string(b\"query\", None)\n"
  },
  {
    "path": "test/test_logging.py",
    "content": "\"\"\"Stupid tests that ensure logging works as expected\"\"\"\n\nimport logging as log\nimport sys\nimport threading\nfrom types import ModuleType\nfrom unittest.mock import patch\n\nimport pytest\n\nimport beets.logging as blog\nfrom beets import plugins, ui\nfrom beets.test import _common, helper\nfrom beets.test.helper import AsIsImporterMixin, ImportTestCase, PluginMixin\n\n\nclass TestStrFormatLogger:\n    \"\"\"Tests for the custom str-formatting logger.\"\"\"\n\n    def test_logger_creation(self):\n        l1 = log.getLogger(\"foo123\")\n        l2 = blog.getLogger(\"foo123\")\n        assert l1 == l2\n        assert l1.__class__ == log.Logger\n\n        l3 = blog.getLogger(\"bar123\")\n        l4 = log.getLogger(\"bar123\")\n        assert l3 == l4\n        assert l3.__class__ == blog.BeetsLogger\n        assert isinstance(\n            l3, (blog.StrFormatLogger, blog.ThreadLocalLevelLogger)\n        )\n\n        l5 = l3.getChild(\"shalala\")\n        assert l5.__class__ == blog.BeetsLogger\n\n        l6 = blog.getLogger()\n        assert l1 != l6\n\n    @pytest.mark.parametrize(\n        \"level\", [log.DEBUG, log.INFO, log.WARNING, log.ERROR]\n    )\n    @pytest.mark.parametrize(\n        \"msg, args, kwargs, expected\",\n        [\n            (\"foo {} bar {}\", (\"oof\", \"baz\"), {}, \"foo oof bar baz\"),\n            (\n                \"foo {bar} baz {foo}\",\n                (),\n                {\"foo\": \"oof\", \"bar\": \"baz\"},\n                \"foo baz baz oof\",\n            ),\n            (\"no args\", (), {}, \"no args\"),\n            (\"foo {} bar {baz}\", (\"oof\",), {\"baz\": \"baz\"}, \"foo oof bar baz\"),\n        ],\n    )\n    def test_str_format_logging(\n        self, level, msg, args, kwargs, expected, caplog\n    ):\n        logger = blog.getLogger(\"test_logger\")\n        logger.setLevel(level)\n\n        with caplog.at_level(level, logger=\"test_logger\"):\n            logger.log(level, msg, *args, **kwargs)\n\n        assert caplog.records, \"No log records were captured\"\n        assert str(caplog.records[0].msg) == expected\n\n\nclass TestLogSanitization:\n    \"\"\"Log messages should have control characters removed from:\n    - String arguments\n    - Keyword argument values\n    - Bytes arguments (which get decoded first)\n    \"\"\"\n\n    @pytest.mark.parametrize(\n        \"msg, args, kwargs, expected\",\n        [\n            # Valid UTF-8 bytes are decoded and preserved\n            (\n                \"foo {} bar {bar}\",\n                (b\"oof \\xc3\\xa9\",),\n                {\"bar\": b\"baz \\xc3\\xa9\"},\n                \"foo oof é bar baz é\",\n            ),\n            # Invalid UTF-8 bytes are decoded with replacement characters\n            (\n                \"foo {} bar {bar}\",\n                (b\"oof \\xff\",),\n                {\"bar\": b\"baz \\xff\"},\n                \"foo oof � bar baz �\",\n            ),\n            # Control characters should be removed\n            (\n                \"foo {} bar {bar}\",\n                (\"oof \\x9e\",),\n                {\"bar\": \"baz \\x9e\"},\n                \"foo oof � bar baz �\",\n            ),\n            # Whitespace control characters should be preserved\n            (\n                \"foo {} bar {bar}\",\n                (\"foo\\t\\n\",),\n                {\"bar\": \"bar\\r\"},\n                \"foo foo\\t\\n bar bar\\r\",\n            ),\n        ],\n    )\n    def test_sanitization(self, msg, args, kwargs, expected, caplog):\n        level = log.INFO\n        logger = blog.getLogger(\"test_logger\")\n        logger.setLevel(level)\n\n        with caplog.at_level(level, logger=\"test_logger\"):\n            logger.log(level, msg, *args, **kwargs)\n\n        assert caplog.records, \"No log records were captured\"\n        assert str(caplog.records[0].msg) == expected\n\n\nclass DummyModule(ModuleType):\n    class DummyPlugin(plugins.BeetsPlugin):\n        def __init__(self):\n            plugins.BeetsPlugin.__init__(self, \"dummy\")\n            self.import_stages = [self.import_stage]\n            self.register_listener(\"dummy_event\", self.listener)\n\n        def log_all(self, name):\n            self._log.debug(\"debug {}\", name)\n            self._log.info(\"info {}\", name)\n            self._log.warning(\"warning {}\", name)\n\n        def commands(self):\n            cmd = ui.Subcommand(\"dummy\")\n            cmd.func = lambda _, __, ___: self.log_all(\"cmd\")\n            return (cmd,)\n\n        def import_stage(self, session, task):\n            self.log_all(\"import_stage\")\n\n        def listener(self):\n            self.log_all(\"listener\")\n\n    def __init__(self, *_, **__):\n        module_name = \"beetsplug.dummy\"\n        super().__init__(module_name)\n        self.DummyPlugin.__module__ = module_name\n        self.DummyPlugin = self.DummyPlugin\n\n\nclass LoggingLevelTest(AsIsImporterMixin, PluginMixin, ImportTestCase):\n    plugin = \"dummy\"\n\n    @classmethod\n    def setUpClass(cls):\n        patcher = patch.dict(sys.modules, {\"beetsplug.dummy\": DummyModule()})\n        patcher.start()\n        cls.addClassCleanup(patcher.stop)\n\n        super().setUpClass()\n\n    def test_command_level0(self):\n        self.config[\"verbose\"] = 0\n        with helper.capture_log() as logs:\n            self.run_command(\"dummy\")\n        assert \"dummy: warning cmd\" in logs\n        assert \"dummy: info cmd\" in logs\n        assert \"dummy: debug cmd\" not in logs\n\n    def test_command_level1(self):\n        self.config[\"verbose\"] = 1\n        with helper.capture_log() as logs:\n            self.run_command(\"dummy\")\n        assert \"dummy: warning cmd\" in logs\n        assert \"dummy: info cmd\" in logs\n        assert \"dummy: debug cmd\" in logs\n\n    def test_command_level2(self):\n        self.config[\"verbose\"] = 2\n        with helper.capture_log() as logs:\n            self.run_command(\"dummy\")\n        assert \"dummy: warning cmd\" in logs\n        assert \"dummy: info cmd\" in logs\n        assert \"dummy: debug cmd\" in logs\n\n    def test_listener_level0(self):\n        self.config[\"verbose\"] = 0\n        with helper.capture_log() as logs:\n            plugins.send(\"dummy_event\")\n        assert \"dummy: warning listener\" in logs\n        assert \"dummy: info listener\" not in logs\n        assert \"dummy: debug listener\" not in logs\n\n    def test_listener_level1(self):\n        self.config[\"verbose\"] = 1\n        with helper.capture_log() as logs:\n            plugins.send(\"dummy_event\")\n        assert \"dummy: warning listener\" in logs\n        assert \"dummy: info listener\" in logs\n        assert \"dummy: debug listener\" not in logs\n\n    def test_listener_level2(self):\n        self.config[\"verbose\"] = 2\n        with helper.capture_log() as logs:\n            plugins.send(\"dummy_event\")\n        assert \"dummy: warning listener\" in logs\n        assert \"dummy: info listener\" in logs\n        assert \"dummy: debug listener\" in logs\n\n    def test_import_stage_level0(self):\n        self.config[\"verbose\"] = 0\n        with helper.capture_log() as logs:\n            self.run_asis_importer()\n        assert \"dummy: warning import_stage\" in logs\n        assert \"dummy: info import_stage\" not in logs\n        assert \"dummy: debug import_stage\" not in logs\n\n    def test_import_stage_level1(self):\n        self.config[\"verbose\"] = 1\n        with helper.capture_log() as logs:\n            self.run_asis_importer()\n        assert \"dummy: warning import_stage\" in logs\n        assert \"dummy: info import_stage\" in logs\n        assert \"dummy: debug import_stage\" not in logs\n\n    def test_import_stage_level2(self):\n        self.config[\"verbose\"] = 2\n        with helper.capture_log() as logs:\n            self.run_asis_importer()\n        assert \"dummy: warning import_stage\" in logs\n        assert \"dummy: info import_stage\" in logs\n        assert \"dummy: debug import_stage\" in logs\n\n\n@_common.slow_test()\nclass ConcurrentEventsTest(AsIsImporterMixin, ImportTestCase):\n    \"\"\"Similar to LoggingLevelTest but lower-level and focused on multiple\n    events interaction. Since this is a bit heavy we don't do it in\n    LoggingLevelTest.\n    \"\"\"\n\n    db_on_disk = True\n\n    class DummyPlugin(plugins.BeetsPlugin):\n        def __init__(self, test_case):\n            plugins.BeetsPlugin.__init__(self, \"dummy\")\n            self.register_listener(\"dummy_event1\", self.listener1)\n            self.register_listener(\"dummy_event2\", self.listener2)\n            self.lock1 = threading.Lock()\n            self.lock2 = threading.Lock()\n            self.test_case = test_case\n            self.exc = None\n            self.t1_step = self.t2_step = 0\n\n        def log_all(self, name):\n            self._log.debug(\"debug {}\", name)\n            self._log.info(\"info {}\", name)\n            self._log.warning(\"warning {}\", name)\n\n        def listener1(self):\n            try:\n                assert self._log.level == log.INFO\n                self.t1_step = 1\n                self.lock1.acquire()\n                assert self._log.level == log.INFO\n                self.t1_step = 2\n            except Exception as e:\n                self.exc = e\n\n        def listener2(self):\n            try:\n                assert self._log.level == log.DEBUG\n                self.t2_step = 1\n                self.lock2.acquire()\n                assert self._log.level == log.DEBUG\n                self.t2_step = 2\n            except Exception as e:\n                self.exc = e\n\n    def test_concurrent_events(self):\n        dp = self.DummyPlugin(self)\n\n        def check_dp_exc():\n            if dp.exc:\n                raise dp.exc\n\n        try:\n            dp.lock1.acquire()\n            dp.lock2.acquire()\n            assert dp._log.level == log.NOTSET\n\n            self.config[\"verbose\"] = 1\n            t1 = threading.Thread(target=dp.listeners[\"dummy_event1\"][0])\n            t1.start()  # blocked. t1 tested its log level\n            while dp.t1_step != 1:\n                check_dp_exc()\n            assert t1.is_alive()\n            assert dp._log.level == log.NOTSET\n\n            self.config[\"verbose\"] = 2\n            t2 = threading.Thread(target=dp.listeners[\"dummy_event2\"][0])\n            t2.start()  # blocked. t2 tested its log level\n            while dp.t2_step != 1:\n                check_dp_exc()\n            assert t2.is_alive()\n            assert dp._log.level == log.NOTSET\n\n            dp.lock1.release()  # dummy_event1 tests its log level + finishes\n            while dp.t1_step != 2:\n                check_dp_exc()\n            t1.join(0.1)\n            assert not t1.is_alive()\n            assert t2.is_alive()\n            assert dp._log.level == log.NOTSET\n\n            dp.lock2.release()  # dummy_event2 tests its log level + finishes\n            while dp.t2_step != 2:\n                check_dp_exc()\n            t2.join(0.1)\n            assert not t2.is_alive()\n\n        except Exception:\n            print(\"Alive threads:\", threading.enumerate())\n            if dp.lock1.locked():\n                print(\"Releasing lock1 after exception in test\")\n                dp.lock1.release()\n            if dp.lock2.locked():\n                print(\"Releasing lock2 after exception in test\")\n                dp.lock2.release()\n            print(\"Alive threads:\", threading.enumerate())\n            raise\n\n    def test_root_logger_levels(self):\n        \"\"\"Root logger level should be shared between threads.\"\"\"\n        self.config[\"threaded\"] = True\n\n        blog.getLogger(\"beets\").set_global_level(blog.WARNING)\n        with helper.capture_log() as logs:\n            self.run_asis_importer()\n        assert logs == []\n\n        blog.getLogger(\"beets\").set_global_level(blog.INFO)\n        with helper.capture_log() as logs:\n            self.run_asis_importer()\n        for line in logs:\n            assert \"import\" in line\n            assert \"album\" in line\n\n        blog.getLogger(\"beets\").set_global_level(blog.DEBUG)\n        with helper.capture_log() as logs:\n            self.run_asis_importer()\n        assert \"Sending event: database_change\" in logs\n"
  },
  {
    "path": "test/test_m3ufile.py",
    "content": "# This file is part of beets.\n# Copyright 2022, J0J0 Todos.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\"\"\"Testsuite for the M3UFile class.\"\"\"\n\nimport sys\nimport unittest\nfrom os import path\nfrom shutil import rmtree\nfrom tempfile import mkdtemp\n\nimport pytest\n\nfrom beets.test._common import RSRC\nfrom beets.util import bytestring_path\nfrom beets.util.m3u import EmptyPlaylistError, M3UFile\n\n\nclass M3UFileTest(unittest.TestCase):\n    \"\"\"Tests the M3UFile class.\"\"\"\n\n    def test_playlist_write_empty(self):\n        \"\"\"Test whether saving an empty playlist file raises an error.\"\"\"\n        tempdir = bytestring_path(mkdtemp())\n        the_playlist_file = path.join(tempdir, b\"playlist.m3u8\")\n        m3ufile = M3UFile(the_playlist_file)\n        with pytest.raises(EmptyPlaylistError):\n            m3ufile.write()\n        rmtree(tempdir)\n\n    def test_playlist_write(self):\n        \"\"\"Test saving ascii paths to a playlist file.\"\"\"\n        tempdir = bytestring_path(mkdtemp())\n        the_playlist_file = path.join(tempdir, b\"playlist.m3u\")\n        m3ufile = M3UFile(the_playlist_file)\n        m3ufile.set_contents(\n            [\n                bytestring_path(\"/This/is/a/path/to_a_file.mp3\"),\n                bytestring_path(\"/This/is/another/path/to_a_file.mp3\"),\n            ]\n        )\n        m3ufile.write()\n        assert path.exists(the_playlist_file)\n        rmtree(tempdir)\n\n    def test_playlist_write_unicode(self):\n        \"\"\"Test saving unicode paths to a playlist file.\"\"\"\n        tempdir = bytestring_path(mkdtemp())\n        the_playlist_file = path.join(tempdir, b\"playlist.m3u8\")\n        m3ufile = M3UFile(the_playlist_file)\n        m3ufile.set_contents(\n            [\n                bytestring_path(\"/This/is/å/path/to_a_file.mp3\"),\n                bytestring_path(\"/This/is/another/path/tö_a_file.mp3\"),\n            ]\n        )\n        m3ufile.write()\n        assert path.exists(the_playlist_file)\n        rmtree(tempdir)\n\n    @unittest.skipUnless(sys.platform == \"win32\", \"win32\")\n    def test_playlist_write_and_read_unicode_windows(self):\n        \"\"\"Test saving unicode paths to a playlist file on Windows.\"\"\"\n        tempdir = bytestring_path(mkdtemp())\n        the_playlist_file = path.join(\n            tempdir, b\"playlist_write_and_read_windows.m3u8\"\n        )\n        m3ufile = M3UFile(the_playlist_file)\n        m3ufile.set_contents(\n            [\n                bytestring_path(r\"x:\\This\\is\\å\\path\\to_a_file.mp3\"),\n                bytestring_path(r\"x:\\This\\is\\another\\path\\tö_a_file.mp3\"),\n            ]\n        )\n        m3ufile.write()\n        assert path.exists(the_playlist_file)\n        m3ufile_read = M3UFile(the_playlist_file)\n        m3ufile_read.load()\n        assert m3ufile.media_list[0] == bytestring_path(\n            path.join(\"x:\\\\\", \"This\", \"is\", \"å\", \"path\", \"to_a_file.mp3\")\n        )\n        assert m3ufile.media_list[1] == bytestring_path(\n            r\"x:\\This\\is\\another\\path\\tö_a_file.mp3\"\n        ), bytestring_path(\n            path.join(\"x:\\\\\", \"This\", \"is\", \"another\", \"path\", \"tö_a_file.mp3\")\n        )\n        rmtree(tempdir)\n\n    @unittest.skipIf(sys.platform == \"win32\", \"win32\")\n    def test_playlist_load_ascii(self):\n        \"\"\"Test loading ascii paths from a playlist file.\"\"\"\n        the_playlist_file = path.join(RSRC, b\"playlist.m3u\")\n        m3ufile = M3UFile(the_playlist_file)\n        m3ufile.load()\n        assert m3ufile.media_list[0] == bytestring_path(\n            \"/This/is/a/path/to_a_file.mp3\"\n        )\n\n    @unittest.skipIf(sys.platform == \"win32\", \"win32\")\n    def test_playlist_load_unicode(self):\n        \"\"\"Test loading unicode paths from a playlist file.\"\"\"\n        the_playlist_file = path.join(RSRC, b\"playlist.m3u8\")\n        m3ufile = M3UFile(the_playlist_file)\n        m3ufile.load()\n        assert m3ufile.media_list[0] == bytestring_path(\n            \"/This/is/å/path/to_a_file.mp3\"\n        )\n\n    @unittest.skipUnless(sys.platform == \"win32\", \"win32\")\n    def test_playlist_load_unicode_windows(self):\n        \"\"\"Test loading unicode paths from a playlist file.\"\"\"\n        the_playlist_file = path.join(RSRC, b\"playlist_windows.m3u8\")\n        winpath = bytestring_path(\n            path.join(\"x:\\\\\", \"This\", \"is\", \"å\", \"path\", \"to_a_file.mp3\")\n        )\n        m3ufile = M3UFile(the_playlist_file)\n        m3ufile.load()\n        assert m3ufile.media_list[0] == winpath\n\n    def test_playlist_load_extm3u(self):\n        \"\"\"Test loading a playlist with an #EXTM3U header.\"\"\"\n        the_playlist_file = path.join(RSRC, b\"playlist.m3u\")\n        m3ufile = M3UFile(the_playlist_file)\n        m3ufile.load()\n        assert m3ufile.extm3u\n\n    def test_playlist_load_non_extm3u(self):\n        \"\"\"Test loading a playlist without an #EXTM3U header.\"\"\"\n        the_playlist_file = path.join(RSRC, b\"playlist_non_ext.m3u\")\n        m3ufile = M3UFile(the_playlist_file)\n        m3ufile.load()\n        assert not m3ufile.extm3u\n"
  },
  {
    "path": "test/test_metadata_plugins.py",
    "content": "from collections.abc import Iterable\n\nimport pytest\n\nfrom beets import metadata_plugins\nfrom beets.test.helper import PluginMixin\n\n\nclass ErrorMetadataMockPlugin(metadata_plugins.MetadataSourcePlugin):\n    \"\"\"A metadata source plugin that raises errors in all its methods.\"\"\"\n\n    def candidates(self, *args, **kwargs):\n        raise ValueError(\"Mocked error\")\n\n    def item_candidates(self, *args, **kwargs):\n        for i in range(3):\n            raise ValueError(\"Mocked error\")\n            yield  # This is just to make this a generator\n\n    def album_for_id(self, *args, **kwargs):\n        raise ValueError(\"Mocked error\")\n\n    def track_for_id(self, *args, **kwargs):\n        raise ValueError(\"Mocked error\")\n\n\nclass TestMetadataPluginsException(PluginMixin):\n    \"\"\"Check that errors during the metadata plugins do not crash beets.\n    They should be logged as errors instead.\n    \"\"\"\n\n    @pytest.fixture(autouse=True)\n    def setup(self):\n        metadata_plugins.find_metadata_source_plugins.cache_clear()\n        metadata_plugins.get_metadata_source.cache_clear()\n        self.register_plugin(ErrorMetadataMockPlugin)\n        yield\n        self.unload_plugins()\n\n    @pytest.fixture\n    def call_method(self, method_name, args):\n        def _call():\n            result = getattr(metadata_plugins, method_name)(*args)\n            return list(result) if isinstance(result, Iterable) else result\n\n        return _call\n\n    @pytest.mark.parametrize(\n        \"method_name,args\",\n        [\n            (\"candidates\", ()),\n            (\"item_candidates\", ()),\n            (\"albums_for_ids\", ([\"some_id\"],)),\n            (\"tracks_for_ids\", ([\"some_id\"],)),\n            (\"album_for_id\", (\"some_id\", \"ErrorMetadataMock\")),\n            (\"track_for_id\", (\"some_id\", \"ErrorMetadataMock\")),\n        ],\n    )\n    def test_logging(self, caplog, call_method, method_name):\n        self.config[\"raise_on_error\"] = False\n\n        call_method()\n\n        assert (\n            f\"Error in 'ErrorMetadataMock.{method_name}': Mocked error\"\n            in caplog.text\n        )\n\n    @pytest.mark.parametrize(\n        \"method_name,args\",\n        [\n            (\"candidates\", ()),\n            (\"item_candidates\", ()),\n            (\"albums_for_ids\", ([\"some_id\"],)),\n            (\"tracks_for_ids\", ([\"some_id\"],)),\n            (\"album_for_id\", (\"some_id\", \"ErrorMetadataMock\")),\n            (\"track_for_id\", (\"some_id\", \"ErrorMetadataMock\")),\n        ],\n    )\n    def test_raising(self, call_method):\n        self.config[\"raise_on_error\"] = True\n\n        with pytest.raises(ValueError, match=\"Mocked error\"):\n            call_method()\n\n\nclass TestSearchApiMetadataSourcePlugin(PluginMixin):\n    plugin = \"none\"\n    preload_plugin = False\n\n    class RaisingSearchApiMetadataMockPlugin(\n        metadata_plugins.SearchApiMetadataSourcePlugin[\n            metadata_plugins.IDResponse\n        ]\n    ):\n        def get_search_query_with_filters(self, _):\n            return \"\", {}\n\n        def get_search_response(self, _):\n            raise ValueError(\"Search failure\")\n\n        def album_for_id(self, _):\n            return None\n\n        def track_for_id(self, _):\n            return None\n\n    @pytest.fixture\n    def search_plugin(self):\n        return self.RaisingSearchApiMetadataMockPlugin()\n\n    def test_search_api_returns_empty_when_raise_on_error_disabled(\n        self, config, search_plugin, caplog\n    ):\n        config[\"raise_on_error\"] = False\n\n        assert search_plugin._search_api(\"track\", \"query\", {}) == ()\n        assert \"Search failure\" in caplog.text\n\n    def test_search_api_raises_when_raise_on_error_enabled(\n        self, config, search_plugin\n    ):\n        config[\"raise_on_error\"] = True\n\n        with pytest.raises(ValueError, match=\"Search failure\"):\n            search_plugin._search_api(\"track\", \"query\", {})\n"
  },
  {
    "path": "test/test_metasync.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Tom Jaspers.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nimport os\nimport platform\nimport time\nfrom datetime import datetime\n\nfrom beets.library import Item\nfrom beets.test import _common\nfrom beets.test.helper import IOMixin, PluginTestCase\n\n\ndef _parsetime(s):\n    return time.mktime(datetime.strptime(s, \"%Y-%m-%d %H:%M:%S\").timetuple())\n\n\ndef _is_windows():\n    return platform.system() == \"Windows\"\n\n\nclass MetaSyncTest(IOMixin, PluginTestCase):\n    plugin = \"metasync\"\n    itunes_library_unix = os.path.join(_common.RSRC, b\"itunes_library_unix.xml\")\n    itunes_library_windows = os.path.join(\n        _common.RSRC, b\"itunes_library_windows.xml\"\n    )\n\n    def setUp(self):\n        super().setUp()\n\n        self.config[\"metasync\"][\"source\"] = \"itunes\"\n\n        if _is_windows():\n            self.config[\"metasync\"][\"itunes\"][\"library\"] = os.fsdecode(\n                self.itunes_library_windows\n            )\n        else:\n            self.config[\"metasync\"][\"itunes\"][\"library\"] = os.fsdecode(\n                self.itunes_library_unix\n            )\n\n        self._set_up_data()\n\n    def _set_up_data(self):\n        items = [_common.item() for _ in range(2)]\n\n        items[0].title = \"Tessellate\"\n        items[0].artist = \"alt-J\"\n        items[0].albumartist = \"alt-J\"\n        items[0].album = \"An Awesome Wave\"\n        items[0].itunes_rating = 60\n\n        items[1].title = \"Breezeblocks\"\n        items[1].artist = \"alt-J\"\n        items[1].albumartist = \"alt-J\"\n        items[1].album = \"An Awesome Wave\"\n\n        if _is_windows():\n            items[\n                0\n            ].path = \"G:\\\\Music\\\\Alt-J\\\\An Awesome Wave\\\\03 Tessellate.mp3\"\n            items[\n                1\n            ].path = \"G:\\\\Music\\\\Alt-J\\\\An Awesome Wave\\\\04 Breezeblocks.mp3\"\n        else:\n            items[0].path = \"/Music/Alt-J/An Awesome Wave/03 Tessellate.mp3\"\n            items[1].path = \"/Music/Alt-J/An Awesome Wave/04 Breezeblocks.mp3\"\n\n        for item in items:\n            self.lib.add(item)\n\n    def test_load_item_types(self):\n        # This test also verifies that the MetaSources have loaded correctly\n        assert \"amarok_score\" in Item._types\n        assert \"itunes_rating\" in Item._types\n\n    def test_pretend_sync_from_itunes(self):\n        out = self.run_with_output(\"metasync\", \"-p\")\n\n        assert \"itunes_rating: 60 -> 80\" in out\n        assert \"itunes_rating: 100\" in out\n        assert \"itunes_playcount: 31\" in out\n        assert \"itunes_skipcount: 3\" in out\n        assert \"itunes_lastplayed: 2015-05-04 12:20:51\" in out\n        assert \"itunes_lastskipped: 2015-02-05 15:41:04\" in out\n        assert \"itunes_dateadded: 2014-04-24 09:28:38\" in out\n        assert self.lib.items()[0].itunes_rating == 60\n\n    def test_sync_from_itunes(self):\n        self.run_command(\"metasync\")\n\n        assert self.lib.items()[0].itunes_rating == 80\n        assert self.lib.items()[0].itunes_playcount == 0\n        assert self.lib.items()[0].itunes_skipcount == 3\n        assert not hasattr(self.lib.items()[0], \"itunes_lastplayed\")\n        assert self.lib.items()[0].itunes_lastskipped == _parsetime(\n            \"2015-02-05 15:41:04\"\n        )\n        assert self.lib.items()[0].itunes_dateadded == _parsetime(\n            \"2014-04-24 09:28:38\"\n        )\n\n        assert self.lib.items()[1].itunes_rating == 100\n        assert self.lib.items()[1].itunes_playcount == 31\n        assert self.lib.items()[1].itunes_skipcount == 0\n        assert self.lib.items()[1].itunes_lastplayed == _parsetime(\n            \"2015-05-04 12:20:51\"\n        )\n        assert self.lib.items()[1].itunes_dateadded == _parsetime(\n            \"2014-04-24 09:28:38\"\n        )\n        assert not hasattr(self.lib.items()[1], \"itunes_lastskipped\")\n"
  },
  {
    "path": "test/test_pipeline.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Test the \"pipeline.py\" restricted parallel programming library.\"\"\"\n\nimport unittest\n\nimport pytest\n\nfrom beets.util import pipeline\n\n\n# Some simple pipeline stages for testing.\ndef _produce(num=5):\n    yield from range(num)\n\n\ndef _work():\n    i = None\n    while True:\n        i = yield i\n        i *= 2\n\n\ndef _consume(result):\n    while True:\n        i = yield\n        result.append(i)\n\n\n# Pipeline stages that raise an exception.\nclass PipelineError(Exception):\n    pass\n\n\ndef _exc_produce(num=5):\n    yield from range(num)\n    raise PipelineError()\n\n\ndef _exc_work(num=3):\n    i = None\n    while True:\n        i = yield i\n        if i == num:\n            raise PipelineError()\n        i *= 2\n\n\ndef _exc_consume(result, num=4):\n    while True:\n        i = yield\n        if i == num:\n            raise PipelineError()\n        result.append(i)\n\n\n# A worker that yields a bubble.\ndef _bub_work(num=3):\n    i = None\n    while True:\n        i = yield i\n        if i == num:\n            i = pipeline.BUBBLE\n        else:\n            i *= 2\n\n\n# Yet another worker that yields multiple messages.\ndef _multi_work():\n    i = None\n    while True:\n        i = yield i\n        i = pipeline.multiple([i, -i])\n\n\nclass SimplePipelineTest(unittest.TestCase):\n    def setUp(self):\n        self.result = []\n        self.pl = pipeline.Pipeline(\n            (_produce(), _work(), _consume(self.result))\n        )\n\n    def test_run_sequential(self):\n        self.pl.run_sequential()\n        assert self.result == [0, 2, 4, 6, 8]\n\n    def test_run_parallel(self):\n        self.pl.run_parallel()\n        assert self.result == [0, 2, 4, 6, 8]\n\n    def test_pull(self):\n        pl = pipeline.Pipeline((_produce(), _work()))\n        assert list(pl.pull()) == [0, 2, 4, 6, 8]\n\n    def test_pull_chain(self):\n        pl = pipeline.Pipeline((_produce(), _work()))\n        pl2 = pipeline.Pipeline((pl.pull(), _work()))\n        assert list(pl2.pull()) == [0, 4, 8, 12, 16]\n\n\nclass ParallelStageTest(unittest.TestCase):\n    def setUp(self):\n        self.result = []\n        self.pl = pipeline.Pipeline(\n            (_produce(), (_work(), _work()), _consume(self.result))\n        )\n\n    def test_run_sequential(self):\n        self.pl.run_sequential()\n        assert self.result == [0, 2, 4, 6, 8]\n\n    def test_run_parallel(self):\n        self.pl.run_parallel()\n        # Order possibly not preserved; use set equality.\n        assert set(self.result) == {0, 2, 4, 6, 8}\n\n    def test_pull(self):\n        pl = pipeline.Pipeline((_produce(), (_work(), _work())))\n        assert list(pl.pull()) == [0, 2, 4, 6, 8]\n\n\nclass ExceptionTest(unittest.TestCase):\n    def setUp(self):\n        self.result = []\n\n    def run_sequential(self, *stages):\n        pl = pipeline.Pipeline(stages)\n        with pytest.raises(PipelineError):\n            pl.run_sequential()\n\n    def run_parallel(self, *stages):\n        pl = pipeline.Pipeline(stages)\n        with pytest.raises(PipelineError):\n            pl.run_parallel()\n\n    def test_run_sequential(self):\n        \"\"\"Test that exceptions from various stages of the pipeline are\n        properly propagated when running sequentially.\n        \"\"\"\n        self.run_sequential(_exc_produce(), _work(), _consume(self.result))\n        self.run_sequential(_produce(), _exc_work(), _consume(self.result))\n        self.run_sequential(_produce(), _work(), _exc_consume(self.result))\n\n    def test_run_parallel(self):\n        \"\"\"Test that exceptions from various stages of the pipeline are\n        properly propagated when running in parallel.\n        \"\"\"\n        self.run_parallel(_exc_produce(), _work(), _consume(self.result))\n        self.run_parallel(_produce(), _exc_work(), _consume(self.result))\n        self.run_parallel(_produce(), _work(), _exc_consume(self.result))\n\n    def test_pull(self):\n        pl = pipeline.Pipeline((_produce(), _exc_work()))\n        pull = pl.pull()\n        for i in range(3):\n            next(pull)\n        with pytest.raises(PipelineError):\n            next(pull)\n\n\nclass ParallelExceptionTest(unittest.TestCase):\n    def setUp(self):\n        self.result = []\n        self.pl = pipeline.Pipeline(\n            (_produce(), (_exc_work(), _exc_work()), _consume(self.result))\n        )\n\n    def test_run_parallel(self):\n        with pytest.raises(PipelineError):\n            self.pl.run_parallel()\n\n\nclass ConstrainedThreadedPipelineTest(unittest.TestCase):\n    def setUp(self):\n        self.result = []\n\n    def test_constrained(self):\n        # Do a \"significant\" amount of work...\n        self.pl = pipeline.Pipeline(\n            (_produce(1000), _work(), _consume(self.result))\n        )\n        # ... with only a single queue slot.\n        self.pl.run_parallel(1)\n        assert self.result == [i * 2 for i in range(1000)]\n\n    def test_constrained_exception(self):\n        # Raise an exception in a constrained pipeline.\n        self.pl = pipeline.Pipeline(\n            (_produce(1000), _exc_work(), _consume(self.result))\n        )\n        with pytest.raises(PipelineError):\n            self.pl.run_parallel(1)\n\n    def test_constrained_parallel(self):\n        self.pl = pipeline.Pipeline(\n            (_produce(1000), (_work(), _work()), _consume(self.result))\n        )\n        self.pl.run_parallel(1)\n        assert set(self.result) == {i * 2 for i in range(1000)}\n\n\nclass BubbleTest(unittest.TestCase):\n    def setUp(self):\n        self.result = []\n        self.pl = pipeline.Pipeline(\n            (_produce(), _bub_work(), _consume(self.result))\n        )\n\n    def test_run_sequential(self):\n        self.pl.run_sequential()\n        assert self.result == [0, 2, 4, 8]\n\n    def test_run_parallel(self):\n        self.pl.run_parallel()\n        assert self.result == [0, 2, 4, 8]\n\n    def test_pull(self):\n        pl = pipeline.Pipeline((_produce(), _bub_work()))\n        assert list(pl.pull()) == [0, 2, 4, 8]\n\n\nclass MultiMessageTest(unittest.TestCase):\n    def setUp(self):\n        self.result = []\n        self.pl = pipeline.Pipeline(\n            (_produce(), _multi_work(), _consume(self.result))\n        )\n\n    def test_run_sequential(self):\n        self.pl.run_sequential()\n        assert self.result == [0, 0, 1, -1, 2, -2, 3, -3, 4, -4]\n\n    def test_run_parallel(self):\n        self.pl.run_parallel()\n        assert self.result == [0, 0, 1, -1, 2, -2, 3, -3, 4, -4]\n\n    def test_pull(self):\n        pl = pipeline.Pipeline((_produce(), _multi_work()))\n        assert list(pl.pull()) == [0, 0, 1, -1, 2, -2, 3, -3, 4, -4]\n\n\nclass StageDecoratorTest(unittest.TestCase):\n    def test_stage_decorator(self):\n        @pipeline.stage\n        def add(n, i):\n            return i + n\n\n        pl = pipeline.Pipeline([iter([1, 2, 3]), add(2)])\n        assert list(pl.pull()) == [3, 4, 5]\n\n    def test_mutator_stage_decorator(self):\n        @pipeline.mutator_stage\n        def setkey(key, item):\n            item[key] = True\n\n        pl = pipeline.Pipeline(\n            [iter([{\"x\": False}, {\"a\": False}]), setkey(\"x\")]\n        )\n        assert list(pl.pull()) == [{\"x\": True}, {\"a\": False, \"x\": True}]\n"
  },
  {
    "path": "test/test_plugins.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Thomas Scholtes.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nimport importlib\nimport itertools\nimport logging\nimport os\nimport pkgutil\nimport sys\nfrom typing import ClassVar\nfrom unittest.mock import ANY, Mock, patch\n\nimport pytest\nfrom mediafile import MediaFile\n\nfrom beets import config, plugins, ui\nfrom beets.dbcore import types\nfrom beets.importer import (\n    Action,\n    ArchiveImportTask,\n    SentinelImportTask,\n    SingletonImportTask,\n)\nfrom beets.library import Item\nfrom beets.test import helper\nfrom beets.test.helper import (\n    AutotagStub,\n    ImportHelper,\n    IOMixin,\n    PluginMixin,\n    PluginTestCase,\n    TerminalImportMixin,\n)\nfrom beets.util import PromptChoice, displayable_path, syspath\n\n\nclass TestPluginRegistration(IOMixin, PluginTestCase):\n    class RatingPlugin(plugins.BeetsPlugin):\n        item_types: ClassVar[dict[str, types.Type]] = {\n            \"rating\": types.Float(),\n            \"multi_value\": types.MULTI_VALUE_DSV,\n        }\n\n        def __init__(self):\n            super().__init__()\n            self.register_listener(\"write\", self.on_write)\n\n        @staticmethod\n        def on_write(item=None, path=None, tags=None):\n            if tags[\"artist\"] == \"XXX\":\n                tags[\"artist\"] = \"YYY\"\n\n    def setUp(self):\n        super().setUp()\n\n        self.register_plugin(self.RatingPlugin)\n\n    def test_field_type_registered(self):\n        assert isinstance(Item._types.get(\"rating\"), types.Float)\n\n    def test_duplicate_type(self):\n        class DuplicateTypePlugin(plugins.BeetsPlugin):\n            item_types: ClassVar[dict[str, types.Type]] = {\n                \"rating\": types.INTEGER\n            }\n\n        self.register_plugin(DuplicateTypePlugin)\n        with pytest.raises(\n            plugins.PluginConflictError, match=\"already been defined\"\n        ):\n            Item._types\n\n    def test_listener_registered(self):\n        self.RatingPlugin()\n        item = self.add_item_fixture(artist=\"XXX\")\n\n        item.write()\n\n        assert MediaFile(syspath(item.path)).artist == \"YYY\"\n\n    def test_multi_value_flex_field_type(self):\n        item = Item(path=\"apath\", artist=\"aaa\")\n        item.multi_value = [\"one\", \"two\", \"three\"]\n        item.add(self.lib)\n\n        out = self.run_with_output(\"ls\", \"-f\", \"$multi_value\")\n        assert out == \"one; two; three\\n\"\n\n\nclass PluginImportTestCase(ImportHelper, PluginTestCase):\n    def setUp(self):\n        super().setUp()\n        self.prepare_album_for_import(2)\n\n\nclass EventsTest(PluginImportTestCase):\n    def test_import_task_created(self):\n        self.importer = self.setup_importer(pretend=True)\n\n        with helper.capture_log() as logs:\n            self.importer.run()\n\n        # Exactly one event should have been imported (for the album).\n        # Sentinels do not get emitted.\n        assert logs.count(\"Sending event: import_task_created\") == 1\n\n        logs = [line for line in logs if not line.startswith(\"Sending event:\")]\n        assert logs == [\n            f\"Album: {displayable_path(os.path.join(self.import_dir, b'album'))}\",\n            f\"  {displayable_path(self.import_media[0].path)}\",\n            f\"  {displayable_path(self.import_media[1].path)}\",\n        ]\n\n    def test_import_task_created_with_plugin(self):\n        class ToSingletonPlugin(plugins.BeetsPlugin):\n            def __init__(self):\n                super().__init__()\n\n                self.register_listener(\n                    \"import_task_created\", self.import_task_created_event\n                )\n\n            def import_task_created_event(self, session, task):\n                if (\n                    isinstance(task, SingletonImportTask)\n                    or isinstance(task, SentinelImportTask)\n                    or isinstance(task, ArchiveImportTask)\n                ):\n                    return task\n\n                new_tasks = []\n                for item in task.items:\n                    new_tasks.append(SingletonImportTask(task.toppath, item))\n\n                return new_tasks\n\n        to_singleton_plugin = ToSingletonPlugin\n        self.register_plugin(to_singleton_plugin)\n\n        self.importer = self.setup_importer(pretend=True)\n\n        with helper.capture_log() as logs:\n            self.importer.run()\n\n        # Exactly one event should have been imported (for the album).\n        # Sentinels do not get emitted.\n        assert logs.count(\"Sending event: import_task_created\") == 1\n\n        logs = [line for line in logs if not line.startswith(\"Sending event:\")]\n        assert logs == [\n            f\"Singleton: {displayable_path(self.import_media[0].path)}\",\n            f\"Singleton: {displayable_path(self.import_media[1].path)}\",\n        ]\n\n\nclass ListenersTest(PluginTestCase):\n    def test_register(self):\n        class DummyPlugin(plugins.BeetsPlugin):\n            def __init__(self):\n                super().__init__()\n                self.register_listener(\"cli_exit\", self.dummy)\n                self.register_listener(\"cli_exit\", self.dummy)\n\n            def dummy(self):\n                pass\n\n        d = DummyPlugin()\n        assert DummyPlugin._raw_listeners[\"cli_exit\"] == [d.dummy]\n\n        d2 = DummyPlugin()\n        assert DummyPlugin._raw_listeners[\"cli_exit\"] == [d.dummy, d2.dummy]\n\n        d.register_listener(\"cli_exit\", d2.dummy)\n        assert DummyPlugin._raw_listeners[\"cli_exit\"] == [d.dummy, d2.dummy]\n\n    def test_events_called(self):\n        class DummyPlugin(plugins.BeetsPlugin):\n            def __init__(self):\n                super().__init__()\n                self.foo = Mock(__name__=\"foo\")\n                self.register_listener(\"event_foo\", self.foo)\n                self.bar = Mock(__name__=\"bar\")\n                self.register_listener(\"event_bar\", self.bar)\n\n        d = DummyPlugin()\n\n        plugins.send(\"event\")\n        d.foo.assert_has_calls([])\n        d.bar.assert_has_calls([])\n\n        plugins.send(\"event_foo\", var=\"tagada\")\n        d.foo.assert_called_once_with(var=\"tagada\")\n        d.bar.assert_has_calls([])\n\n    def test_listener_params(self):\n        class DummyPlugin(plugins.BeetsPlugin):\n            def __init__(self):\n                super().__init__()\n                for i in itertools.count(1):\n                    try:\n                        meth = getattr(self, f\"dummy{i}\")\n                    except AttributeError:\n                        break\n                    self.register_listener(f\"event{i}\", meth)\n\n            def dummy1(self, foo):\n                assert foo == 5\n\n            def dummy2(self, foo=None):\n                assert foo == 5\n\n            def dummy3(self):\n                # argument cut off\n                pass\n\n            def dummy4(self, bar=None):\n                # argument cut off\n                pass\n\n            def dummy5(self, bar):\n                assert not True\n\n            # more complex examples\n\n            def dummy6(self, foo, bar=None):\n                assert foo == 5\n                assert bar is None\n\n            def dummy7(self, foo, **kwargs):\n                assert foo == 5\n                assert kwargs == {}\n\n            def dummy8(self, foo, bar, **kwargs):\n                assert not True\n\n            def dummy9(self, **kwargs):\n                assert kwargs == {\"foo\": 5}\n\n        DummyPlugin()\n\n        plugins.send(\"event1\", foo=5)\n        plugins.send(\"event2\", foo=5)\n        plugins.send(\"event3\", foo=5)\n        plugins.send(\"event4\", foo=5)\n\n        with pytest.raises(TypeError):\n            plugins.send(\"event5\", foo=5)\n\n        plugins.send(\"event6\", foo=5)\n        plugins.send(\"event7\", foo=5)\n\n        with pytest.raises(TypeError):\n            plugins.send(\"event8\", foo=5)\n\n        plugins.send(\"event9\", foo=5)\n\n\nclass PromptChoicesTest(TerminalImportMixin, PluginImportTestCase):\n    def setUp(self):\n        super().setUp()\n        self.setup_importer()\n        self.matcher = AutotagStub(AutotagStub.IDENT).install()\n        self.addCleanup(self.matcher.restore)\n        # keep track of ui.input_option() calls\n        self.input_options_patcher = patch(\n            \"beets.ui.input_options\", side_effect=ui.input_options\n        )\n        self.mock_input_options = self.input_options_patcher.start()\n\n    def tearDown(self):\n        super().tearDown()\n        self.input_options_patcher.stop()\n\n    def test_plugin_choices_in_ui_input_options_album(self):\n        \"\"\"Test the presence of plugin choices on the prompt (album).\"\"\"\n\n        class DummyPlugin(plugins.BeetsPlugin):\n            def __init__(self):\n                super().__init__()\n                self.register_listener(\n                    \"before_choose_candidate\", self.return_choices\n                )\n\n            def return_choices(self, session, task):\n                return [\n                    PromptChoice(\"f\", \"Foo\", None),\n                    PromptChoice(\"r\", \"baR\", None),\n                ]\n\n        self.register_plugin(DummyPlugin)\n        # Default options + extra choices by the plugin ('Foo', 'Bar')\n        opts = (\n            \"Apply\",\n            \"More candidates\",\n            \"Skip\",\n            \"Use as-is\",\n            \"as Tracks\",\n            \"Group albums\",\n            \"Enter search\",\n            \"enter Id\",\n            \"aBort\",\n            \"Foo\",\n            \"baR\",\n        )\n\n        self.importer.add_choice(Action.SKIP)\n        self.importer.run()\n        self.mock_input_options.assert_called_once_with(\n            opts, default=\"a\", require=ANY\n        )\n\n    def test_plugin_choices_in_ui_input_options_singleton(self):\n        \"\"\"Test the presence of plugin choices on the prompt (singleton).\"\"\"\n\n        class DummyPlugin(plugins.BeetsPlugin):\n            def __init__(self):\n                super().__init__()\n                self.register_listener(\n                    \"before_choose_candidate\", self.return_choices\n                )\n\n            def return_choices(self, session, task):\n                return [\n                    PromptChoice(\"f\", \"Foo\", None),\n                    PromptChoice(\"r\", \"baR\", None),\n                ]\n\n        self.register_plugin(DummyPlugin)\n        # Default options + extra choices by the plugin ('Foo', 'Bar')\n        opts = (\n            \"Apply\",\n            \"More candidates\",\n            \"Skip\",\n            \"Use as-is\",\n            \"Enter search\",\n            \"enter Id\",\n            \"aBort\",\n            \"Foo\",\n            \"baR\",\n        )\n\n        config[\"import\"][\"singletons\"] = True\n        self.importer.add_choice(Action.SKIP)\n        self.importer.run()\n        self.mock_input_options.assert_called_with(\n            opts, default=\"a\", require=ANY\n        )\n\n    def test_choices_conflicts(self):\n        \"\"\"Test the short letter conflict solving.\"\"\"\n\n        class DummyPlugin(plugins.BeetsPlugin):\n            def __init__(self):\n                super().__init__()\n                self.register_listener(\n                    \"before_choose_candidate\", self.return_choices\n                )\n\n            def return_choices(self, session, task):\n                return [\n                    PromptChoice(\"a\", \"A foo\", None),  # dupe\n                    PromptChoice(\"z\", \"baZ\", None),  # ok\n                    PromptChoice(\"z\", \"Zupe\", None),  # dupe\n                    PromptChoice(\"z\", \"Zoo\", None),\n                ]  # dupe\n\n        self.register_plugin(DummyPlugin)\n        # Default options + not dupe extra choices by the plugin ('baZ')\n        opts = (\n            \"Apply\",\n            \"More candidates\",\n            \"Skip\",\n            \"Use as-is\",\n            \"as Tracks\",\n            \"Group albums\",\n            \"Enter search\",\n            \"enter Id\",\n            \"aBort\",\n            \"baZ\",\n        )\n        self.importer.add_choice(Action.SKIP)\n        self.importer.run()\n        self.mock_input_options.assert_called_once_with(\n            opts, default=\"a\", require=ANY\n        )\n\n    def test_plugin_callback(self):\n        \"\"\"Test that plugin callbacks are being called upon user choice.\"\"\"\n\n        class DummyPlugin(plugins.BeetsPlugin):\n            def __init__(self):\n                super().__init__()\n                self.register_listener(\n                    \"before_choose_candidate\", self.return_choices\n                )\n\n            def return_choices(self, session, task):\n                return [PromptChoice(\"f\", \"Foo\", self.foo)]\n\n            def foo(self, session, task):\n                pass\n\n        self.register_plugin(DummyPlugin)\n        # Default options + extra choices by the plugin ('Foo', 'Bar')\n        opts = (\n            \"Apply\",\n            \"More candidates\",\n            \"Skip\",\n            \"Use as-is\",\n            \"as Tracks\",\n            \"Group albums\",\n            \"Enter search\",\n            \"enter Id\",\n            \"aBort\",\n            \"Foo\",\n        )\n\n        # DummyPlugin.foo() should be called once\n        with patch.object(DummyPlugin, \"foo\", autospec=True) as mock_foo:\n            self.io.addinput(\"f\")\n            self.io.addinput(\"n\")\n            self.importer.run()\n            assert mock_foo.call_count == 1\n\n        # input_options should be called twice, as foo() returns None\n        assert self.mock_input_options.call_count == 2\n        self.mock_input_options.assert_called_with(\n            opts, default=\"a\", require=ANY\n        )\n\n    def test_plugin_callback_return(self):\n        \"\"\"Test that plugin callbacks that return a value exit the loop.\"\"\"\n\n        class DummyPlugin(plugins.BeetsPlugin):\n            def __init__(self):\n                super().__init__()\n                self.register_listener(\n                    \"before_choose_candidate\", self.return_choices\n                )\n\n            def return_choices(self, session, task):\n                return [PromptChoice(\"f\", \"Foo\", self.foo)]\n\n            def foo(self, session, task):\n                return Action.SKIP\n\n        self.register_plugin(DummyPlugin)\n        # Default options + extra choices by the plugin ('Foo', 'Bar')\n        opts = (\n            \"Apply\",\n            \"More candidates\",\n            \"Skip\",\n            \"Use as-is\",\n            \"as Tracks\",\n            \"Group albums\",\n            \"Enter search\",\n            \"enter Id\",\n            \"aBort\",\n            \"Foo\",\n        )\n\n        # DummyPlugin.foo() should be called once\n        self.io.addinput(\"f\")\n        self.importer.run()\n\n        # input_options should be called once, as foo() returns SKIP\n        self.mock_input_options.assert_called_once_with(\n            opts, default=\"a\", require=ANY\n        )\n\n\ndef get_available_plugins():\n    \"\"\"Get all available plugins in the beetsplug namespace.\"\"\"\n    namespace_pkg = importlib.import_module(\"beetsplug\")\n\n    return [\n        m.name\n        for m in pkgutil.iter_modules(namespace_pkg.__path__)\n        if not m.name.startswith(\"_\")\n    ]\n\n\nclass TestImportPlugin(PluginMixin):\n    @pytest.fixture(params=get_available_plugins())\n    def plugin_name(self, request):\n        \"\"\"Fixture to provide the name of each available plugin.\"\"\"\n        name = request.param\n\n        # skip gstreamer plugins on windows\n        gstreamer_plugins = {\"bpd\", \"replaygain\"}\n        if sys.platform == \"win32\" and name in gstreamer_plugins:\n            pytest.skip(f\"GStreamer is not available on Windows: {name}\")\n\n        return name\n\n    def unload_plugins(self):\n        \"\"\"Unimport plugins before each test to avoid conflicts.\"\"\"\n        super().unload_plugins()\n        for mod in list(sys.modules):\n            if mod.startswith(\"beetsplug.\"):\n                del sys.modules[mod]\n\n    @pytest.fixture(autouse=True)\n    def cleanup(self):\n        \"\"\"Ensure plugins are unimported before and after each test.\"\"\"\n        self.unload_plugins()\n        yield\n        self.unload_plugins()\n\n    @pytest.mark.skipif(\n        os.environ.get(\"GITHUB_ACTIONS\") != \"true\",\n        reason=(\n            \"Requires all dependencies to be installed, which we can't\"\n            \" guarantee in the local environment.\"\n        ),\n    )\n    def test_import_plugin(self, caplog, plugin_name):\n        \"\"\"Test that a plugin is importable without an error.\"\"\"\n        caplog.set_level(logging.WARNING)\n        self.load_plugins(plugin_name)\n\n        assert \"PluginImportError\" not in caplog.text, (\n            f\"Plugin '{plugin_name}' has issues during import.\"\n        )\n\n\nclass TestDeprecationCopy:\n    # TODO: remove this test in Beets 3.0.0\n    def test_legacy_metadata_plugin_deprecation(self):\n        \"\"\"Test that a MetadataSourcePlugin with 'legacy' data_source\n        raises a deprecation warning and all function and properties are\n        copied from the base class.\n        \"\"\"\n        with pytest.warns(DeprecationWarning, match=\"LegacyMetadataPlugin\"):\n\n            class LegacyMetadataPlugin(plugins.BeetsPlugin):\n                data_source = \"legacy\"\n\n        # Assert all methods are present\n        assert hasattr(LegacyMetadataPlugin, \"albums_for_ids\")\n        assert hasattr(LegacyMetadataPlugin, \"tracks_for_ids\")\n        assert hasattr(LegacyMetadataPlugin, \"data_source_mismatch_penalty\")\n        assert hasattr(LegacyMetadataPlugin, \"_extract_id\")\n        assert hasattr(LegacyMetadataPlugin, \"get_artist\")\n\n\nclass TestMusicBrainzPluginLoading:\n    @pytest.fixture(autouse=True)\n    def config(self):\n        _config = config\n        _config.sources = []\n        _config.read(user=False, defaults=True)\n        return _config\n\n    def test_default(self):\n        assert \"musicbrainz\" in plugins.get_plugin_names()\n\n    def test_other_plugin_enabled(self, config):\n        config[\"plugins\"] = [\"anything\"]\n\n        assert \"musicbrainz\" not in plugins.get_plugin_names()\n\n    def test_deprecated_enabled(self, config, caplog):\n        config[\"plugins\"] = [\"anything\"]\n        config[\"musicbrainz\"][\"enabled\"] = True\n\n        assert \"musicbrainz\" in plugins.get_plugin_names()\n        assert (\n            \"musicbrainz.enabled' configuration option is deprecated\"\n            in caplog.text\n        )\n\n    def test_deprecated_disabled(self, config, caplog):\n        config[\"musicbrainz\"][\"enabled\"] = False\n\n        assert \"musicbrainz\" not in plugins.get_plugin_names()\n        assert (\n            \"musicbrainz.enabled' configuration option is deprecated\"\n            in caplog.text\n        )\n"
  },
  {
    "path": "test/test_query.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Various tests for querying the library database.\"\"\"\n\nimport sys\nfrom functools import partial\nfrom pathlib import Path\n\nimport pytest\n\nfrom beets.dbcore import types\nfrom beets.dbcore.query import (\n    AndQuery,\n    BooleanQuery,\n    DateQuery,\n    FalseQuery,\n    MatchQuery,\n    NoneQuery,\n    NotQuery,\n    NumericQuery,\n    OrQuery,\n    ParsingError,\n    PathQuery,\n    RegexpQuery,\n    StringFieldQuery,\n    StringQuery,\n    SubstringQuery,\n    TrueQuery,\n)\nfrom beets.library import Item\nfrom beets.test import _common\nfrom beets.test.helper import TestHelper\n\n# Because the absolute path begins with something like C:, we\n# can't disambiguate it from an ordinary query.\nWIN32_NO_IMPLICIT_PATHS = \"Implicit paths are not supported on Windows\"\n\n_p = pytest.param\n\n\n@pytest.fixture(scope=\"class\")\ndef helper():\n    helper = TestHelper()\n    helper.setup_beets()\n\n    yield helper\n\n    helper.teardown_beets()\n\n\nclass TestGet:\n    @pytest.fixture(scope=\"class\")\n    def lib(self, helper):\n        album_items = [\n            helper.create_item(\n                title=\"first\",\n                artist=\"one\",\n                artists=[\"one\", \"eleven\"],\n                album=\"baz\",\n                year=2001,\n                comp=True,\n                genres=[\"rock\"],\n            ),\n            helper.create_item(\n                title=\"second\",\n                artist=\"two\",\n                artists=[\"two\", \"twelve\"],\n                album=\"baz\",\n                year=2002,\n                comp=True,\n                genres=[\"Rock\"],\n            ),\n        ]\n        album = helper.lib.add_album(album_items)\n        album.albumflex = \"foo\"\n        album.store()\n\n        helper.add_item(\n            title=\"third\",\n            artist=\"three\",\n            artists=[\"three\", \"one\"],\n            album=\"foo\",\n            year=2003,\n            comp=False,\n            genres=[\"Hard Rock\"],\n            comments=\"caf\\xe9\",\n        )\n\n        return helper.lib\n\n    @pytest.mark.parametrize(\n        \"q, expected_titles\",\n        [\n            (\"\", [\"first\", \"second\", \"third\"]),\n            (None, [\"first\", \"second\", \"third\"]),\n            (\":oNE\", []),\n            (\":one\", [\"first\"]),\n            (\":sec :ond\", [\"second\"]),\n            (\":second\", [\"second\"]),\n            (\"=rock\", [\"first\"]),\n            ('=~\"hard rock\"', [\"third\"]),\n            (\":t$\", [\"first\"]),\n            (\"oNE\", [\"first\"]),\n            (\"baz\", [\"first\", \"second\"]),\n            (\"sec ond\", [\"second\"]),\n            (\"three\", [\"third\"]),\n            (\"albumflex:foo\", [\"first\", \"second\"]),\n            (\"artist::t.+r\", [\"third\"]),\n            (\"artist:thrEE\", [\"third\"]),\n            (\"artists::eleven\", [\"first\"]),\n            (\"artists::one\", [\"first\", \"third\"]),\n            (\"ArTiST:three\", [\"third\"]),\n            (\"comments:caf\\xe9\", [\"third\"]),\n            (\"comp:true\", [\"first\", \"second\"]),\n            (\"comp:false\", [\"third\"]),\n            (\"genres:=rock\", [\"first\"]),\n            (\"genres:=Rock\", [\"second\"]),\n            ('genres:=\"Hard Rock\"', [\"third\"]),\n            ('genres:=~\"hard rock\"', [\"third\"]),\n            (\"genres:=~rock\", [\"first\", \"second\"]),\n            ('genres:=\"hard rock\"', []),\n            (\"popebear\", []),\n            (\"pope:bear\", []),\n            (\"singleton:true\", [\"third\"]),\n            (\"singleton:1\", [\"third\"]),\n            (\"singleton:false\", [\"first\", \"second\"]),\n            (\"singleton:0\", [\"first\", \"second\"]),\n            (\"title:ond\", [\"second\"]),\n            (\"title::sec\", [\"second\"]),\n            (\"year:2001\", [\"first\"]),\n            (\"year:2000..2002\", [\"first\", \"second\"]),\n            (\"xyzzy:nonsense\", []),\n        ],\n    )\n    def test_get_query(self, lib, q, expected_titles):\n        assert {i.title for i in lib.items(q)} == set(expected_titles)\n\n    @pytest.mark.parametrize(\n        \"q, expected_titles\",\n        [\n            (BooleanQuery(\"comp\", True), (\"third\",)),\n            (DateQuery(\"added\", \"2000-01-01\"), (\"first\", \"second\", \"third\")),\n            (FalseQuery(), (\"first\", \"second\", \"third\")),\n            (MatchQuery(\"year\", \"2003\"), (\"first\", \"second\")),\n            (NoneQuery(\"rg_track_gain\"), ()),\n            (NumericQuery(\"year\", \"2001..2002\"), (\"third\",)),\n            (\n                AndQuery(\n                    [BooleanQuery(\"comp\", True), NumericQuery(\"year\", \"2002\")]\n                ),\n                (\"first\", \"third\"),\n            ),\n            (\n                OrQuery(\n                    [BooleanQuery(\"comp\", True), NumericQuery(\"year\", \"2002\")]\n                ),\n                (\"third\",),\n            ),\n            (RegexpQuery(\"artist\", \"^t\"), (\"first\",)),\n            (SubstringQuery(\"album\", \"ba\"), (\"third\",)),\n            (TrueQuery(), ()),\n        ],\n    )\n    def test_query_logic(self, lib, q, expected_titles):\n        def get_results(*args):\n            return {i.title for i in lib.items(*args)}\n\n        # not(a and b) <-> not(a) or not(b)\n        not_q = NotQuery(q)\n        not_q_results = get_results(not_q)\n        assert not_q_results == set(expected_titles)\n\n        # assert using OrQuery, AndQuery\n        q_or = OrQuery([q, not_q])\n\n        q_and = AndQuery([q, not_q])\n        assert get_results(q_or) == {\"first\", \"second\", \"third\"}\n        assert get_results(q_and) == set()\n\n        # assert manually checking the item titles\n        all_titles = get_results()\n        q_results = get_results(q)\n        assert q_results.union(not_q_results) == all_titles\n        assert q_results.intersection(not_q_results) == set()\n\n        # round trip\n        not_not_q = NotQuery(not_q)\n        assert get_results(q) == get_results(not_not_q)\n\n    @pytest.mark.parametrize(\n        \"q, expected_titles\",\n        [\n            (\"-artist::t.+r\", [\"first\", \"second\"]),\n            (\"-:t$\", [\"second\", \"third\"]),\n            (\"sec -bar\", [\"second\"]),\n            (\"sec -title:bar\", [\"second\"]),\n            (\"-ond\", [\"first\", \"third\"]),\n            (\"^ond\", [\"first\", \"third\"]),\n            (\"^title:sec\", [\"first\", \"third\"]),\n            (\"-title:sec\", [\"first\", \"third\"]),\n        ],\n    )\n    def test_negation_prefix(self, lib, q, expected_titles):\n        actual_titles = {i.title for i in lib.items(q)}\n        assert actual_titles == set(expected_titles)\n\n    @pytest.mark.parametrize(\n        \"make_q\",\n        [\n            partial(DateQuery, \"added\", \"2001-01-01\"),\n            partial(MatchQuery, \"artist\", \"one\"),\n            partial(NoneQuery, \"rg_track_gain\"),\n            partial(NumericQuery, \"year\", \"2002\"),\n            partial(StringQuery, \"year\", \"2001\"),\n            partial(RegexpQuery, \"album\", \"^.a\"),\n            partial(SubstringQuery, \"title\", \"x\"),\n        ],\n    )\n    def test_fast_vs_slow(self, lib, make_q):\n        \"\"\"Test that the results are the same regardless of the `fast` flag\n        for negated `FieldQuery`s.\n        \"\"\"\n        q_fast = make_q(True)\n        q_slow = make_q(False)\n\n        assert list(map(dict, lib.items(q_fast))) == list(\n            map(dict, lib.items(q_slow))\n        )\n\n\nclass TestMatch:\n    @pytest.fixture(scope=\"class\")\n    def item(self):\n        return _common.item(album=\"the album\", disc=6, year=1, bitrate=128000)\n\n    @pytest.mark.parametrize(\n        \"q, should_match\",\n        [\n            (RegexpQuery(\"album\", \"^the album$\"), True),\n            (RegexpQuery(\"album\", \"^album$\"), False),\n            (RegexpQuery(\"disc\", \"^6$\"), True),\n            (SubstringQuery(\"album\", \"album\"), True),\n            (SubstringQuery(\"album\", \"ablum\"), False),\n            (SubstringQuery(\"disc\", \"6\"), True),\n            (StringQuery(\"album\", \"the album\"), True),\n            (StringQuery(\"album\", \"THE ALBUM\"), True),\n            (StringQuery(\"album\", \"album\"), False),\n            (NumericQuery(\"year\", \"1\"), True),\n            (NumericQuery(\"year\", \"10\"), False),\n            (NumericQuery(\"bitrate\", \"100000..200000\"), True),\n            (NumericQuery(\"bitrate\", \"200000..300000\"), False),\n            (NumericQuery(\"bitrate\", \"100000..\"), True),\n        ],\n    )\n    def test_match(self, item, q, should_match):\n        assert q.match(item) == should_match\n        assert not NotQuery(q).match(item) == should_match\n\n\nclass TestPathQuery:\n    \"\"\"Tests for path-based querying functionality in the database system.\n\n    Verifies that path queries correctly match items by their file paths,\n    handling special characters, case sensitivity, parent directories,\n    and path separator detection across different platforms.\n    \"\"\"\n\n    @pytest.fixture(scope=\"class\")\n    def lib(self, helper):\n        helper.add_item(path=b\"/aaa/bb/c.mp3\", title=\"path item\")\n        helper.add_item(path=b\"/x/y/z.mp3\", title=\"another item\")\n        helper.add_item(path=b\"/c/_/title.mp3\", title=\"with underscore\")\n        helper.add_item(path=b\"/c/%/title.mp3\", title=\"with percent\")\n        helper.add_item(path=rb\"/c/\\x/title.mp3\", title=\"with backslash\")\n        helper.add_item(path=b\"/A/B/C2.mp3\", title=\"caps path\")\n\n        return helper.lib\n\n    @pytest.mark.parametrize(\n        \"q, expected_titles\",\n        [\n            _p(\"path:/aaa/bb/c.mp3\", [\"path item\"], id=\"exact-match\"),\n            _p(\"path:/aaa\", [\"path item\"], id=\"parent-dir-no-slash\"),\n            _p(\"path:/aaa/\", [\"path item\"], id=\"parent-dir-with-slash\"),\n            _p(\"path:/aa\", [], id=\"no-match-does-not-match-parent-dir\"),\n            _p(\"path:/xyzzy/\", [], id=\"no-match\"),\n            _p(\"path:/b/\", [], id=\"fragment-no-match\"),\n            _p(\"path:/x/../aaa/bb\", [\"path item\"], id=\"non-normalized\"),\n            _p(\"path::c\\\\.mp3$\", [\"path item\"], id=\"regex\"),\n            _p(\"path:/c/_\", [\"with underscore\"], id=\"underscore-escaped\"),\n            _p(\"path:/c/%\", [\"with percent\"], id=\"percent-escaped\"),\n            _p(\"path:/c/\\\\\\\\x\", [\"with backslash\"], id=\"backslash-escaped\"),\n        ],\n    )\n    def test_explicit(self, monkeypatch, lib, q, expected_titles):\n        \"\"\"Test explicit path queries with different path specifications.\"\"\"\n        monkeypatch.setattr(\"beets.util.case_sensitive\", lambda *_: True)\n\n        assert {i.title for i in lib.items(q)} == set(expected_titles)\n\n    @pytest.mark.skipif(sys.platform == \"win32\", reason=WIN32_NO_IMPLICIT_PATHS)\n    @pytest.mark.parametrize(\n        \"q, expected_titles\",\n        [\n            _p(\"/aaa/bb\", [\"path item\"], id=\"slashed-query\"),\n            _p(\"/aaa/bb , /aaa\", [\"path item\"], id=\"path-in-or-query\"),\n            _p(\"c.mp3\", [], id=\"no-slash-no-match\"),\n            _p(\"title:/a/b\", [], id=\"slash-with-explicit-field-no-match\"),\n        ],\n    )\n    def test_implicit(self, monkeypatch, lib, q, expected_titles):\n        \"\"\"Test implicit path detection when queries contain path separators.\"\"\"\n        monkeypatch.setattr(\n            \"beets.dbcore.query.PathQuery.is_path_query\", lambda path: True\n        )\n\n        assert {i.title for i in lib.items(q)} == set(expected_titles)\n\n    @pytest.mark.parametrize(\n        \"case_sensitive, expected_titles\",\n        [\n            _p(True, [], id=\"non-caps-dont-match-caps\"),\n            _p(False, [\"caps path\"], id=\"non-caps-match-caps\"),\n        ],\n    )\n    def test_case_sensitivity(\n        self, lib, monkeypatch, case_sensitive, expected_titles\n    ):\n        \"\"\"Test path matching with different case sensitivity settings.\"\"\"\n        q = \"path:/a/b/c2.mp3\"\n        monkeypatch.setattr(\n            \"beets.util.case_sensitive\", lambda *_: case_sensitive\n        )\n\n        assert {i.title for i in lib.items(q)} == set(expected_titles)\n\n    # FIXME: Also create a variant of this test for windows, which tests\n    # both os.sep and os.altsep\n    @pytest.mark.skipif(sys.platform == \"win32\", reason=WIN32_NO_IMPLICIT_PATHS)\n    @pytest.mark.parametrize(\n        \"q, is_path_query\",\n        [\n            (\"/foo/bar\", True),\n            (\"foo/bar\", True),\n            (\"foo/\", True),\n            (\"foo\", False),\n            (\"foo/:bar\", True),\n            (\"foo:bar/\", False),\n            (\"foo:/bar\", False),\n        ],\n    )\n    def test_path_sep_detection(self, monkeypatch, tmp_path, q, is_path_query):\n        \"\"\"Test detection of path queries based on the presence of path separators.\"\"\"\n        monkeypatch.chdir(tmp_path)\n        (tmp_path / \"foo\").mkdir()\n        (tmp_path / \"foo\" / \"bar\").touch()\n        if Path(q).is_absolute():\n            q = str(tmp_path / q[1:])\n\n        assert PathQuery.is_path_query(q) == is_path_query\n\n\nclass TestQuery:\n    ALBUM = \"album title\"\n    SINGLE = \"singleton\"\n\n    @pytest.fixture(scope=\"class\")\n    def lib(self, helper):\n        helper.add_album(\n            title=self.ALBUM,\n            comp=True,\n            flexbool=True,\n            bpm=120,\n            flexint=2,\n            rg_track_gain=0,\n        )\n        helper.add_item(\n            title=self.SINGLE, comp=False, flexbool=False, rg_track_gain=None\n        )\n\n        with pytest.MonkeyPatch.context() as monkeypatch:\n            monkeypatch.setattr(\n                Item,\n                \"_types\",\n                {\"flexbool\": types.Boolean(), \"flexint\": types.Integer()},\n            )\n            yield helper.lib\n\n    @pytest.mark.parametrize(\"query_class\", [MatchQuery, StringFieldQuery])\n    def test_equality(self, query_class):\n        assert query_class(\"foo\", \"bar\") == query_class(\"foo\", \"bar\")\n\n    @pytest.mark.parametrize(\n        \"make_q, expected_msg\",\n        [\n            (lambda: NumericQuery(\"year\", \"199a\"), \"not an int\"),\n            (lambda: RegexpQuery(\"year\", \"199(\"), r\"not a regular expression.*unterminated subpattern\"),  # noqa: E501\n        ]\n    )  # fmt: skip\n    def test_invalid_query(self, make_q, expected_msg):\n        with pytest.raises(ParsingError, match=expected_msg):\n            make_q()\n\n    @pytest.mark.parametrize(\n        \"q, expected_titles\",\n        [\n            # Boolean value\n            _p(\"comp:true\", {ALBUM}, id=\"parse-true\"),\n            _p(\"flexbool:true\", {ALBUM}, id=\"flex-parse-true\"),\n            _p(\"flexbool:false\", {SINGLE}, id=\"flex-parse-false\"),\n            _p(\"flexbool:1\", {ALBUM}, id=\"flex-parse-1\"),\n            _p(\"flexbool:0\", {SINGLE}, id=\"flex-parse-0\"),\n            # TODO: shouldn't this match 1 / true instead?\n            _p(\"flexbool:something\", {SINGLE}, id=\"flex-parse-true\"),\n            # Integer value\n            _p(\"bpm:120\", {ALBUM}, id=\"int-exact-value\"),\n            _p(\"bpm:110..125\", {ALBUM}, id=\"int-range\"),\n            _p(\"flexint:2\", {ALBUM}, id=\"int-flex\"),\n            _p(\"flexint:3\", set(), id=\"int-no-match\"),\n            _p(\"bpm:12\", set(), id=\"int-dont-match-substring\"),\n            # None value\n            _p(NoneQuery(\"album_id\"), {SINGLE}, id=\"none-match-singleton\"),\n            _p(NoneQuery(\"rg_track_gain\"), {SINGLE}, id=\"none-value\"),\n        ],\n    )\n    def test_value_type(self, lib, q, expected_titles):\n        assert {i.title for i in lib.items(q)} == expected_titles\n\n\nclass TestDefaultSearchFields:\n    @pytest.fixture(scope=\"class\")\n    def lib(self, helper):\n        helper.add_album(\n            title=\"title\",\n            album=\"album\",\n            albumartist=\"albumartist\",\n            catalognum=\"catalognum\",\n            year=2001,\n        )\n\n        return helper.lib\n\n    @pytest.mark.parametrize(\n        \"entity, q, should_match\",\n        [\n            _p(\"albums\", \"album\", True, id=\"album-match-album\"),\n            _p(\"albums\", \"albumartist\", True, id=\"album-match-albumartist\"),\n            _p(\"albums\", \"catalognum\", False, id=\"album-dont-match-catalognum\"),\n            _p(\"items\", \"title\", True, id=\"item-match-title\"),\n            _p(\"items\", \"2001\", False, id=\"item-dont-match-year\"),\n        ],\n    )\n    def test_search(self, lib, entity, q, should_match):\n        assert bool(getattr(lib, entity)(q)) == should_match\n\n\nclass TestRelatedQueries:\n    \"\"\"Test album-level queries with track-level filters and vice-versa.\"\"\"\n\n    @pytest.fixture(scope=\"class\")\n    def lib(self, helper):\n        for album_idx in range(1, 3):\n            album_name = f\"Album{album_idx}\"\n            items = [\n                helper.create_item(\n                    album=album_name, title=f\"{album_name} Item{idx}\"\n                )\n                for idx in range(1, 3)\n            ]\n            album = helper.lib.add_album(items)\n            album.artpath = f\"{album_name} Artpath\"\n            album.catalognum = \"ABC\"\n            album.store()\n\n        return helper.lib\n\n    @pytest.mark.parametrize(\n        \"q, expected_titles, expected_albums\",\n        [\n            _p(\n                \"title:Album1\",\n                [\"Album1 Item1\", \"Album1 Item2\"],\n                [\"Album1\"],\n                id=\"match-album-with-item-field-query\",\n            ),\n            _p(\n                \"title:Item2\",\n                [\"Album1 Item2\", \"Album2 Item2\"],\n                [\"Album1\", \"Album2\"],\n                id=\"match-albums-with-item-field-query\",\n            ),\n            _p(\n                \"artpath::Album1\",\n                [\"Album1 Item1\", \"Album1 Item2\"],\n                [\"Album1\"],\n                id=\"match-items-with-album-field-query\",\n            ),\n            _p(\n                \"catalognum:ABC Album1\",\n                [\"Album1 Item1\", \"Album1 Item2\"],\n                [\"Album1\"],\n                id=\"query-field-common-to-album-and-item\",\n            ),\n        ],\n    )\n    def test_related_query(self, lib, q, expected_titles, expected_albums):\n        assert {i.album for i in lib.albums(q)} == set(expected_albums)\n        assert {i.title for i in lib.items(q)} == set(expected_titles)\n"
  },
  {
    "path": "test/test_release.py",
    "content": "\"\"\"Tests for the release utils.\"\"\"\n\nimport os\nimport shutil\nimport sys\n\nimport pytest\n\nrelease = pytest.importorskip(\"extra.release\")\n\n\npytestmark = pytest.mark.skipif(\n    not (\n        (os.environ.get(\"GITHUB_ACTIONS\") == \"true\" and sys.platform != \"win32\")\n        or bool(shutil.which(\"pandoc\"))\n    ),\n    reason=\"pandoc isn't available\",\n)\n\n\n@pytest.fixture\ndef rst_changelog():\n    return \"\"\"\nUnreleased\n----------\n\nNew features\n~~~~~~~~~~~~\n\n- :doc:`/plugins/substitute`: Some substitute\n  multi-line change.\n  :bug:`5467`\n- :ref:`list-cmd` Update.\n- |BeetsPlugin| Some plugin change.\n- See :class:`~beetsplug._utils.musicbrainz.MusicBrainzAPI` for documentation.\n\nYou can do something with this command:\n\n::\n\n    $ do-something\n\nBug fixes\n~~~~~~~~~\n\n- Some fix that refers to an issue.\n  :bug:`5467`\n- Some fix that mentions user :user:`username`.\n- Some fix thanks to\n  :user:`username`. :bug:`5467`\n- Some fix with its own bullet points using incorrect indentation:\n\n  - First nested bullet point\n    with some text that wraps to the next line\n  - Second nested bullet point\n\n- Another fix with an enumerated list\n\n  1. First\n     and some details\n  2. Second\n     and some details\n\nLong parapgraph naaaaaaaaaaaaaaaaaaaaaaaammmmmmmmmmmmmmmmeeeeeeeeeeeeeee ending\nwith a colon:\n\n.. For plugin developers\n.. ~~~~~~~~~~~~~~~~~~~~~\n\nOther changes\n~~~~~~~~~~~~~\n\n- Changed ``bitesize`` label to ``good first issue``. Our `contribute`_ page is now\n  automatically populated with these issues. :bug:`4855`\n\n.. _contribute: https://github.com/beetbox/beets/contribute\n\n2.1.0 (November 22, 2024)\n-------------------------\n\nBug fixes\n~~~~~~~~~\n\n- Fixed something.\"\"\"\n\n\n@pytest.fixture\ndef md_changelog():\n    return r\"\"\"# Unreleased\n\n## New features\n\n- [beets.plugins.BeetsPlugin](https://beets.readthedocs.io/en/stable/api/generated/beets.plugins.BeetsPlugin.html#beets.plugins.BeetsPlugin) Some plugin change.\n- [list command](https://beets.readthedocs.io/en/stable/reference/cli.html#list-cmd) Update.\n- [Substitute Plugin](https://beets.readthedocs.io/en/stable/plugins/substitute.html): Some substitute multi-line change. :bug: (#5467)\n- See [beetsplug.\\_utils.musicbrainz.MusicBrainzAPI](https://beets.readthedocs.io/en/stable/api/generated/beetsplug._utils.musicbrainz.MusicBrainzAPI.html#beetsplug._utils.musicbrainz.MusicBrainzAPI) for documentation.\n\nYou can do something with this command:\n\n    $ do-something\n\n## Bug fixes\n\n- Another fix with an enumerated list\n  1.  First and some details\n  2.  Second and some details\n- Some fix thanks to @username. :bug: (#5467)\n- Some fix that mentions user @username.\n- Some fix that refers to an issue. :bug: (#5467)\n- Some fix with its own bullet points using incorrect indentation:\n  - First nested bullet point with some text that wraps to the next line\n  - Second nested bullet point\n\nLong parapgraph naaaaaaaaaaaaaaaaaaaaaaaammmmmmmmmmmmmmmmeeeeeeeeeeeeeee ending with a colon:\n\n## Other changes\n\n- Changed `bitesize` label to `good first issue`. Our [contribute](https://github.com/beetbox/beets/contribute) page is now automatically populated with these issues. :bug: (#4855)\n\n# 2.1.0 (November 22, 2024)\n\n## Bug fixes\n\n- Fixed something.\"\"\"  # noqa: E501\n\n\ndef test_convert_rst_to_md(rst_changelog, md_changelog):\n    actual = release.changelog_as_markdown(rst_changelog)\n\n    assert actual == md_changelog\n"
  },
  {
    "path": "test/test_sort.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Various tests for querying the library database.\"\"\"\n\nfrom unittest.mock import patch\n\nimport beets.library\nfrom beets import config, dbcore\nfrom beets.dbcore import types\nfrom beets.library import Album\nfrom beets.test import _common\nfrom beets.test.helper import BeetsTestCase\n\n\n# A test case class providing a library with some dummy data and some\n# assertions involving that data.\nclass DummyDataTestCase(BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n\n        albums = [\n            Album(\n                album=\"Album A\",\n                genres=[\"Rock\"],\n                year=2001,\n                flex1=\"Flex1-1\",\n                flex2=\"Flex2-A\",\n                albumartist=\"Foo\",\n            ),\n            Album(\n                album=\"Album B\",\n                genres=[\"Rock\"],\n                year=2001,\n                flex1=\"Flex1-2\",\n                flex2=\"Flex2-A\",\n                albumartist=\"Bar\",\n            ),\n            Album(\n                album=\"Album C\",\n                genres=[\"Jazz\"],\n                year=2005,\n                flex1=\"Flex1-1\",\n                flex2=\"Flex2-B\",\n                albumartist=\"Baz\",\n            ),\n        ]\n        for album in albums:\n            self.lib.add(album)\n\n        items = [_common.item() for _ in range(4)]\n        items[0].title = \"Foo bar\"\n        items[0].artist = \"One\"\n        items[0].album = \"Baz\"\n        items[0].year = 2001\n        items[0].comp = True\n        items[0].flex1 = \"Flex1-0\"\n        items[0].flex2 = \"Flex2-A\"\n        items[0].album_id = albums[0].id\n        items[0].artist_sort = None\n        items[0].path = \"/path0.mp3\"\n        items[0].track = 1\n        items[1].title = \"Baz qux\"\n        items[1].artist = \"Two\"\n        items[1].album = \"Baz\"\n        items[1].year = 2002\n        items[1].comp = True\n        items[1].flex1 = \"Flex1-1\"\n        items[1].flex2 = \"Flex2-A\"\n        items[1].album_id = albums[0].id\n        items[1].artist_sort = None\n        items[1].path = \"/patH1.mp3\"\n        items[1].track = 2\n        items[2].title = \"Beets 4 eva\"\n        items[2].artist = \"Three\"\n        items[2].album = \"Foo\"\n        items[2].year = 2003\n        items[2].comp = False\n        items[2].flex1 = \"Flex1-2\"\n        items[2].flex2 = \"Flex1-B\"\n        items[2].album_id = albums[1].id\n        items[2].artist_sort = None\n        items[2].path = \"/paTH2.mp3\"\n        items[2].track = 3\n        items[3].title = \"Beets 4 eva\"\n        items[3].artist = \"Three\"\n        items[3].album = \"Foo2\"\n        items[3].year = 2004\n        items[3].comp = False\n        items[3].flex1 = \"Flex1-2\"\n        items[3].flex2 = \"Flex1-C\"\n        items[3].album_id = albums[2].id\n        items[3].artist_sort = None\n        items[3].path = \"/PATH3.mp3\"\n        items[3].track = 4\n        for item in items:\n            self.lib.add(item)\n\n\nclass SortFixedFieldTest(DummyDataTestCase):\n    def test_sort_asc(self):\n        q = \"\"\n        sort = dbcore.query.FixedFieldSort(\"year\", True)\n        results = self.lib.items(q, sort)\n        assert results[0][\"year\"] <= results[1][\"year\"]\n        assert results[0][\"year\"] == 2001\n        # same thing with query string\n        q = \"year+\"\n        results2 = self.lib.items(q)\n        for r1, r2 in zip(results, results2):\n            assert r1.id == r2.id\n\n    def test_sort_desc(self):\n        q = \"\"\n        sort = dbcore.query.FixedFieldSort(\"year\", False)\n        results = self.lib.items(q, sort)\n        assert results[0][\"year\"] >= results[1][\"year\"]\n        assert results[0][\"year\"] == 2004\n        # same thing with query string\n        q = \"year-\"\n        results2 = self.lib.items(q)\n        for r1, r2 in zip(results, results2):\n            assert r1.id == r2.id\n\n    def test_sort_two_field_asc(self):\n        q = \"\"\n        s1 = dbcore.query.FixedFieldSort(\"album\", True)\n        s2 = dbcore.query.FixedFieldSort(\"year\", True)\n        sort = dbcore.query.MultipleSort()\n        sort.add_sort(s1)\n        sort.add_sort(s2)\n        results = self.lib.items(q, sort)\n        assert results[0][\"album\"] <= results[1][\"album\"]\n        assert results[1][\"album\"] <= results[2][\"album\"]\n        assert results[0][\"album\"] == \"Baz\"\n        assert results[1][\"album\"] == \"Baz\"\n        assert results[0][\"year\"] <= results[1][\"year\"]\n        # same thing with query string\n        q = \"album+ year+\"\n        results2 = self.lib.items(q)\n        for r1, r2 in zip(results, results2):\n            assert r1.id == r2.id\n\n    def test_sort_path_field(self):\n        q = \"\"\n        sort = dbcore.query.FixedFieldSort(\"path\", True)\n        results = self.lib.items(q, sort)\n        assert results[0][\"path\"] == b\"/path0.mp3\"\n        assert results[1][\"path\"] == b\"/patH1.mp3\"\n        assert results[2][\"path\"] == b\"/paTH2.mp3\"\n        assert results[3][\"path\"] == b\"/PATH3.mp3\"\n\n\nclass SortFlexFieldTest(DummyDataTestCase):\n    def test_sort_asc(self):\n        q = \"\"\n        sort = dbcore.query.SlowFieldSort(\"flex1\", True)\n        results = self.lib.items(q, sort)\n        assert results[0][\"flex1\"] <= results[1][\"flex1\"]\n        assert results[0][\"flex1\"] == \"Flex1-0\"\n        # same thing with query string\n        q = \"flex1+\"\n        results2 = self.lib.items(q)\n        for r1, r2 in zip(results, results2):\n            assert r1.id == r2.id\n\n    def test_sort_desc(self):\n        q = \"\"\n        sort = dbcore.query.SlowFieldSort(\"flex1\", False)\n        results = self.lib.items(q, sort)\n        assert results[0][\"flex1\"] >= results[1][\"flex1\"]\n        assert results[1][\"flex1\"] >= results[2][\"flex1\"]\n        assert results[2][\"flex1\"] >= results[3][\"flex1\"]\n        assert results[0][\"flex1\"] == \"Flex1-2\"\n        # same thing with query string\n        q = \"flex1-\"\n        results2 = self.lib.items(q)\n        for r1, r2 in zip(results, results2):\n            assert r1.id == r2.id\n\n    def test_sort_two_field(self):\n        q = \"\"\n        s1 = dbcore.query.SlowFieldSort(\"flex2\", False)\n        s2 = dbcore.query.SlowFieldSort(\"flex1\", True)\n        sort = dbcore.query.MultipleSort()\n        sort.add_sort(s1)\n        sort.add_sort(s2)\n        results = self.lib.items(q, sort)\n        assert results[0][\"flex2\"] >= results[1][\"flex2\"]\n        assert results[1][\"flex2\"] >= results[2][\"flex2\"]\n        assert results[0][\"flex2\"] == \"Flex2-A\"\n        assert results[1][\"flex2\"] == \"Flex2-A\"\n        assert results[0][\"flex1\"] <= results[1][\"flex1\"]\n        # same thing with query string\n        q = \"flex2- flex1+\"\n        results2 = self.lib.items(q)\n        for r1, r2 in zip(results, results2):\n            assert r1.id == r2.id\n\n\nclass SortAlbumFixedFieldTest(DummyDataTestCase):\n    def test_sort_asc(self):\n        q = \"\"\n        sort = dbcore.query.FixedFieldSort(\"year\", True)\n        results = self.lib.albums(q, sort)\n        assert results[0][\"year\"] <= results[1][\"year\"]\n        assert results[0][\"year\"] == 2001\n        # same thing with query string\n        q = \"year+\"\n        results2 = self.lib.albums(q)\n        for r1, r2 in zip(results, results2):\n            assert r1.id == r2.id\n\n    def test_sort_desc(self):\n        q = \"\"\n        sort = dbcore.query.FixedFieldSort(\"year\", False)\n        results = self.lib.albums(q, sort)\n        assert results[0][\"year\"] >= results[1][\"year\"]\n        assert results[0][\"year\"] == 2005\n        # same thing with query string\n        q = \"year-\"\n        results2 = self.lib.albums(q)\n        for r1, r2 in zip(results, results2):\n            assert r1.id == r2.id\n\n    def test_sort_two_field_asc(self):\n        q = \"\"\n        s1 = dbcore.query.FixedFieldSort(\"genres\", True)\n        s2 = dbcore.query.FixedFieldSort(\"album\", True)\n        sort = dbcore.query.MultipleSort()\n        sort.add_sort(s1)\n        sort.add_sort(s2)\n        results = self.lib.albums(q, sort)\n        assert results[0][\"genres\"] <= results[1][\"genres\"]\n        assert results[1][\"genres\"] <= results[2][\"genres\"]\n        assert results[1][\"genres\"] == [\"Rock\"]\n        assert results[2][\"genres\"] == [\"Rock\"]\n        assert results[1][\"album\"] <= results[2][\"album\"]\n        # same thing with query string\n        q = \"genres+ album+\"\n        results2 = self.lib.albums(q)\n        for r1, r2 in zip(results, results2):\n            assert r1.id == r2.id\n\n\nclass SortAlbumFlexFieldTest(DummyDataTestCase):\n    def test_sort_asc(self):\n        q = \"\"\n        sort = dbcore.query.SlowFieldSort(\"flex1\", True)\n        results = self.lib.albums(q, sort)\n        assert results[0][\"flex1\"] <= results[1][\"flex1\"]\n        assert results[1][\"flex1\"] <= results[2][\"flex1\"]\n        # same thing with query string\n        q = \"flex1+\"\n        results2 = self.lib.albums(q)\n        for r1, r2 in zip(results, results2):\n            assert r1.id == r2.id\n\n    def test_sort_desc(self):\n        q = \"\"\n        sort = dbcore.query.SlowFieldSort(\"flex1\", False)\n        results = self.lib.albums(q, sort)\n        assert results[0][\"flex1\"] >= results[1][\"flex1\"]\n        assert results[1][\"flex1\"] >= results[2][\"flex1\"]\n        # same thing with query string\n        q = \"flex1-\"\n        results2 = self.lib.albums(q)\n        for r1, r2 in zip(results, results2):\n            assert r1.id == r2.id\n\n    def test_sort_two_field_asc(self):\n        q = \"\"\n        s1 = dbcore.query.SlowFieldSort(\"flex2\", True)\n        s2 = dbcore.query.SlowFieldSort(\"flex1\", True)\n        sort = dbcore.query.MultipleSort()\n        sort.add_sort(s1)\n        sort.add_sort(s2)\n        results = self.lib.albums(q, sort)\n        assert results[0][\"flex2\"] <= results[1][\"flex2\"]\n        assert results[1][\"flex2\"] <= results[2][\"flex2\"]\n        assert results[0][\"flex2\"] == \"Flex2-A\"\n        assert results[1][\"flex2\"] == \"Flex2-A\"\n        assert results[0][\"flex1\"] <= results[1][\"flex1\"]\n        # same thing with query string\n        q = \"flex2+ flex1+\"\n        results2 = self.lib.albums(q)\n        for r1, r2 in zip(results, results2):\n            assert r1.id == r2.id\n\n\nclass SortAlbumComputedFieldTest(DummyDataTestCase):\n    def test_sort_asc(self):\n        q = \"\"\n        sort = dbcore.query.SlowFieldSort(\"path\", True)\n        results = self.lib.albums(q, sort)\n        assert results[0][\"path\"] <= results[1][\"path\"]\n        assert results[1][\"path\"] <= results[2][\"path\"]\n        # same thing with query string\n        q = \"path+\"\n        results2 = self.lib.albums(q)\n        for r1, r2 in zip(results, results2):\n            assert r1.id == r2.id\n\n    def test_sort_desc(self):\n        q = \"\"\n        sort = dbcore.query.SlowFieldSort(\"path\", False)\n        results = self.lib.albums(q, sort)\n        assert results[0][\"path\"] >= results[1][\"path\"]\n        assert results[1][\"path\"] >= results[2][\"path\"]\n        # same thing with query string\n        q = \"path-\"\n        results2 = self.lib.albums(q)\n        for r1, r2 in zip(results, results2):\n            assert r1.id == r2.id\n\n\nclass SortCombinedFieldTest(DummyDataTestCase):\n    def test_computed_first(self):\n        q = \"\"\n        s1 = dbcore.query.SlowFieldSort(\"path\", True)\n        s2 = dbcore.query.FixedFieldSort(\"year\", True)\n        sort = dbcore.query.MultipleSort()\n        sort.add_sort(s1)\n        sort.add_sort(s2)\n        results = self.lib.albums(q, sort)\n        assert results[0][\"path\"] <= results[1][\"path\"]\n        assert results[1][\"path\"] <= results[2][\"path\"]\n        q = \"path+ year+\"\n        results2 = self.lib.albums(q)\n        for r1, r2 in zip(results, results2):\n            assert r1.id == r2.id\n\n    def test_computed_second(self):\n        q = \"\"\n        s1 = dbcore.query.FixedFieldSort(\"year\", True)\n        s2 = dbcore.query.SlowFieldSort(\"path\", True)\n        sort = dbcore.query.MultipleSort()\n        sort.add_sort(s1)\n        sort.add_sort(s2)\n        results = self.lib.albums(q, sort)\n        assert results[0][\"year\"] <= results[1][\"year\"]\n        assert results[1][\"year\"] <= results[2][\"year\"]\n        assert results[0][\"path\"] <= results[1][\"path\"]\n        q = \"year+ path+\"\n        results2 = self.lib.albums(q)\n        for r1, r2 in zip(results, results2):\n            assert r1.id == r2.id\n\n\nclass ConfigSortTest(DummyDataTestCase):\n    def test_default_sort_item(self):\n        results = list(self.lib.items())\n        assert results[0].artist < results[1].artist\n\n    def test_config_opposite_sort_item(self):\n        config[\"sort_item\"] = \"artist-\"\n        results = list(self.lib.items())\n        assert results[0].artist > results[1].artist\n\n    def test_default_sort_album(self):\n        results = list(self.lib.albums())\n        assert results[0].albumartist < results[1].albumartist\n\n    def test_config_opposite_sort_album(self):\n        config[\"sort_album\"] = \"albumartist-\"\n        results = list(self.lib.albums())\n        assert results[0].albumartist > results[1].albumartist\n\n\nclass CaseSensitivityTest(DummyDataTestCase):\n    \"\"\"If case_insensitive is false, lower-case values should be placed\n    after all upper-case values. E.g., `Foo Qux bar`\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        album = Album(\n            album=\"album\",\n            genres=[\"alternative\"],\n            year=\"2001\",\n            flex1=\"flex1\",\n            flex2=\"flex2-A\",\n            albumartist=\"bar\",\n        )\n        self.lib.add(album)\n\n        item = _common.item()\n        item.title = \"another\"\n        item.artist = \"lowercase\"\n        item.album = \"album\"\n        item.year = 2001\n        item.comp = True\n        item.flex1 = \"flex1\"\n        item.flex2 = \"flex2-A\"\n        item.album_id = album.id\n        item.artist_sort = None\n        item.track = 10\n        self.lib.add(item)\n\n        self.new_album = album\n        self.new_item = item\n\n    def tearDown(self):\n        self.new_item.remove(delete=True)\n        self.new_album.remove(delete=True)\n        super().tearDown()\n\n    def test_smart_artist_case_insensitive(self):\n        config[\"sort_case_insensitive\"] = True\n        q = \"artist+\"\n        results = list(self.lib.items(q))\n        assert results[0].artist == \"lowercase\"\n        assert results[1].artist == \"One\"\n\n    def test_smart_artist_case_sensitive(self):\n        config[\"sort_case_insensitive\"] = False\n        q = \"artist+\"\n        results = list(self.lib.items(q))\n        assert results[0].artist == \"One\"\n        assert results[-1].artist == \"lowercase\"\n\n    def test_fixed_field_case_insensitive(self):\n        config[\"sort_case_insensitive\"] = True\n        q = \"album+\"\n        results = list(self.lib.albums(q))\n        assert results[0].album == \"album\"\n        assert results[1].album == \"Album A\"\n\n    def test_fixed_field_case_sensitive(self):\n        config[\"sort_case_insensitive\"] = False\n        q = \"album+\"\n        results = list(self.lib.albums(q))\n        assert results[0].album == \"Album A\"\n        assert results[-1].album == \"album\"\n\n    def test_flex_field_case_insensitive(self):\n        config[\"sort_case_insensitive\"] = True\n        q = \"flex1+\"\n        results = list(self.lib.items(q))\n        assert results[0].flex1 == \"flex1\"\n        assert results[1].flex1 == \"Flex1-0\"\n\n    def test_flex_field_case_sensitive(self):\n        config[\"sort_case_insensitive\"] = False\n        q = \"flex1+\"\n        results = list(self.lib.items(q))\n        assert results[0].flex1 == \"Flex1-0\"\n        assert results[-1].flex1 == \"flex1\"\n\n    def test_case_sensitive_only_affects_text(self):\n        config[\"sort_case_insensitive\"] = True\n        q = \"track+\"\n        results = list(self.lib.items(q))\n        # If the numerical values were sorted as strings,\n        # then ['1', '10', '2'] would be valid.\n        # print([r.track for r in results])\n        assert results[0].track == 1\n        assert results[1].track == 2\n        assert results[-1].track == 10\n\n\nclass NonExistingFieldTest(DummyDataTestCase):\n    \"\"\"Test sorting by non-existing fields\"\"\"\n\n    def test_non_existing_fields_not_fail(self):\n        qs = [\"foo+\", \"foo-\", \"--\", \"-+\", \"+-\", \"++\", \"-foo-\", \"-foo+\", \"---\"]\n\n        q0 = \"foo+\"\n        results0 = list(self.lib.items(q0))\n        for q1 in qs:\n            results1 = list(self.lib.items(q1))\n            for r1, r2 in zip(results0, results1):\n                assert r1.id == r2.id\n\n    def test_combined_non_existing_field_asc(self):\n        all_results = list(self.lib.items(\"id+\"))\n        q = \"foo+ id+\"\n        results = list(self.lib.items(q))\n        assert len(all_results) == len(results)\n        for r1, r2 in zip(all_results, results):\n            assert r1.id == r2.id\n\n    def test_combined_non_existing_field_desc(self):\n        all_results = list(self.lib.items(\"id+\"))\n        q = \"foo- id+\"\n        results = list(self.lib.items(q))\n        assert len(all_results) == len(results)\n        for r1, r2 in zip(all_results, results):\n            assert r1.id == r2.id\n\n    def test_field_present_in_some_items(self):\n        \"\"\"Test ordering by a (string) field not present on all items.\"\"\"\n        # append 'foo' to two items (1,2)\n        lower_foo_item, higher_foo_item, *items_without_foo = self.lib.items(\n            \"id+\"\n        )\n        lower_foo_item.foo, higher_foo_item.foo = \"bar1\", \"bar2\"\n        lower_foo_item.store()\n        higher_foo_item.store()\n\n        results_asc = list(self.lib.items(\"foo+ id+\"))\n        assert [i.id for i in results_asc] == [\n            # items without field first\n            *[i.id for i in items_without_foo],\n            lower_foo_item.id,\n            higher_foo_item.id,\n        ]\n\n        results_desc = list(self.lib.items(\"foo- id+\"))\n        assert [i.id for i in results_desc] == [\n            higher_foo_item.id,\n            lower_foo_item.id,\n            # items without field last\n            *[i.id for i in items_without_foo],\n        ]\n\n    @patch(\"beets.library.Item._types\", {\"myint\": types.Integer()})\n    def test_int_field_present_in_some_items(self):\n        \"\"\"Test ordering by an int-type field not present on all items.\"\"\"\n        # append int-valued 'myint' to two items (1,2)\n        lower_myint_item, higher_myint_item, *items_without_myint = (\n            self.lib.items(\"id+\")\n        )\n        lower_myint_item.myint, higher_myint_item.myint = 1, 2\n        lower_myint_item.store()\n        higher_myint_item.store()\n\n        results_asc = list(self.lib.items(\"myint+ id+\"))\n        assert [i.id for i in results_asc] == [\n            # items without field first\n            *[i.id for i in items_without_myint],\n            lower_myint_item.id,\n            higher_myint_item.id,\n        ]\n\n        results_desc = list(self.lib.items(\"myint- id+\"))\n        assert [i.id for i in results_desc] == [\n            higher_myint_item.id,\n            lower_myint_item.id,\n            # items without field last\n            *[i.id for i in items_without_myint],\n        ]\n\n    def test_negation_interaction(self):\n        \"\"\"Test the handling of negation and sorting together.\n\n        If a string ends with a sorting suffix, it takes precedence over the\n        NotQuery parsing.\n        \"\"\"\n        query, sort = beets.library.parse_query_string(\n            \"-bar+\", beets.library.Item\n        )\n        assert len(query.subqueries) == 1\n        assert isinstance(query.subqueries[0], dbcore.query.TrueQuery)\n        assert isinstance(sort, dbcore.query.SlowFieldSort)\n        assert sort.field == \"-bar\"\n"
  },
  {
    "path": "test/test_template.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for template engine.\"\"\"\n\nimport unittest\n\nfrom beets.util import functemplate\n\n\ndef _normexpr(expr):\n    \"\"\"Normalize an Expression object's parts, collapsing multiple\n    adjacent text blocks and removing empty text blocks. Generates a\n    sequence of parts.\n    \"\"\"\n    textbuf = []\n    for part in expr.parts:\n        if isinstance(part, str):\n            textbuf.append(part)\n        else:\n            if textbuf:\n                text = \"\".join(textbuf)\n                if text:\n                    yield text\n                    textbuf = []\n            yield part\n    if textbuf:\n        text = \"\".join(textbuf)\n        if text:\n            yield text\n\n\ndef _normparse(text):\n    \"\"\"Parse a template and then normalize the resulting Expression.\"\"\"\n    return _normexpr(functemplate._parse(text))\n\n\nclass ParseTest(unittest.TestCase):\n    def test_empty_string(self):\n        assert list(_normparse(\"\")) == []\n\n    def _assert_symbol(self, obj, ident):\n        \"\"\"Assert that an object is a Symbol with the given identifier.\"\"\"\n        assert isinstance(obj, functemplate.Symbol), f\"not a Symbol: {obj}\"\n        assert obj.ident == ident, f\"wrong identifier: {obj.ident} vs. {ident}\"\n\n    def _assert_call(self, obj, ident, numargs):\n        \"\"\"Assert that an object is a Call with the given identifier and\n        argument count.\n        \"\"\"\n        assert isinstance(obj, functemplate.Call), f\"not a Call: {obj}\"\n        assert obj.ident == ident, f\"wrong identifier: {obj.ident} vs. {ident}\"\n        assert len(obj.args) == numargs, (\n            f\"wrong argument count in {obj.ident}: {len(obj.args)} vs. {numargs}\"\n        )\n\n    def test_plain_text(self):\n        assert list(_normparse(\"hello world\")) == [\"hello world\"]\n\n    def test_escaped_character_only(self):\n        assert list(_normparse(\"$$\")) == [\"$\"]\n\n    def test_escaped_character_in_text(self):\n        assert list(_normparse(\"a $$ b\")) == [\"a $ b\"]\n\n    def test_escaped_character_at_start(self):\n        assert list(_normparse(\"$$ hello\")) == [\"$ hello\"]\n\n    def test_escaped_character_at_end(self):\n        assert list(_normparse(\"hello $$\")) == [\"hello $\"]\n\n    def test_escaped_function_delim(self):\n        assert list(_normparse(\"a $% b\")) == [\"a % b\"]\n\n    def test_escaped_sep(self):\n        assert list(_normparse(\"a $, b\")) == [\"a , b\"]\n\n    def test_escaped_close_brace(self):\n        assert list(_normparse(\"a $} b\")) == [\"a } b\"]\n\n    def test_bare_value_delim_kept_intact(self):\n        assert list(_normparse(\"a $ b\")) == [\"a $ b\"]\n\n    def test_bare_function_delim_kept_intact(self):\n        assert list(_normparse(\"a % b\")) == [\"a % b\"]\n\n    def test_bare_opener_kept_intact(self):\n        assert list(_normparse(\"a { b\")) == [\"a { b\"]\n\n    def test_bare_closer_kept_intact(self):\n        assert list(_normparse(\"a } b\")) == [\"a } b\"]\n\n    def test_bare_sep_kept_intact(self):\n        assert list(_normparse(\"a , b\")) == [\"a , b\"]\n\n    def test_symbol_alone(self):\n        parts = list(_normparse(\"$foo\"))\n        assert len(parts) == 1\n        self._assert_symbol(parts[0], \"foo\")\n\n    def test_symbol_in_text(self):\n        parts = list(_normparse(\"hello $foo world\"))\n        assert len(parts) == 3\n        assert parts[0] == \"hello \"\n        self._assert_symbol(parts[1], \"foo\")\n        assert parts[2] == \" world\"\n\n    def test_symbol_with_braces(self):\n        parts = list(_normparse(\"hello${foo}world\"))\n        assert len(parts) == 3\n        assert parts[0] == \"hello\"\n        self._assert_symbol(parts[1], \"foo\")\n        assert parts[2] == \"world\"\n\n    def test_unclosed_braces_symbol(self):\n        assert list(_normparse(\"a ${ b\")) == [\"a ${ b\"]\n\n    def test_empty_braces_symbol(self):\n        assert list(_normparse(\"a ${} b\")) == [\"a ${} b\"]\n\n    def test_call_without_args_at_end(self):\n        assert list(_normparse(\"foo %bar\")) == [\"foo %bar\"]\n\n    def test_call_without_args(self):\n        assert list(_normparse(\"foo %bar baz\")) == [\"foo %bar baz\"]\n\n    def test_call_with_unclosed_args(self):\n        assert list(_normparse(\"foo %bar{ baz\")) == [\"foo %bar{ baz\"]\n\n    def test_call_with_unclosed_multiple_args(self):\n        assert list(_normparse(\"foo %bar{bar,bar baz\")) == [\n            \"foo %bar{bar,bar baz\"\n        ]\n\n    def test_call_empty_arg(self):\n        parts = list(_normparse(\"%foo{}\"))\n        assert len(parts) == 1\n        self._assert_call(parts[0], \"foo\", 1)\n        assert list(_normexpr(parts[0].args[0])) == []\n\n    def test_call_single_arg(self):\n        parts = list(_normparse(\"%foo{bar}\"))\n        assert len(parts) == 1\n        self._assert_call(parts[0], \"foo\", 1)\n        assert list(_normexpr(parts[0].args[0])) == [\"bar\"]\n\n    def test_call_two_args(self):\n        parts = list(_normparse(\"%foo{bar,baz}\"))\n        assert len(parts) == 1\n        self._assert_call(parts[0], \"foo\", 2)\n        assert list(_normexpr(parts[0].args[0])) == [\"bar\"]\n        assert list(_normexpr(parts[0].args[1])) == [\"baz\"]\n\n    def test_call_with_escaped_sep(self):\n        parts = list(_normparse(\"%foo{bar$,baz}\"))\n        assert len(parts) == 1\n        self._assert_call(parts[0], \"foo\", 1)\n        assert list(_normexpr(parts[0].args[0])) == [\"bar,baz\"]\n\n    def test_call_with_escaped_close(self):\n        parts = list(_normparse(\"%foo{bar$}baz}\"))\n        assert len(parts) == 1\n        self._assert_call(parts[0], \"foo\", 1)\n        assert list(_normexpr(parts[0].args[0])) == [\"bar}baz\"]\n\n    def test_call_with_symbol_argument(self):\n        parts = list(_normparse(\"%foo{$bar,baz}\"))\n        assert len(parts) == 1\n        self._assert_call(parts[0], \"foo\", 2)\n        arg_parts = list(_normexpr(parts[0].args[0]))\n        assert len(arg_parts) == 1\n        self._assert_symbol(arg_parts[0], \"bar\")\n        assert list(_normexpr(parts[0].args[1])) == [\"baz\"]\n\n    def test_call_with_nested_call_argument(self):\n        parts = list(_normparse(\"%foo{%bar{},baz}\"))\n        assert len(parts) == 1\n        self._assert_call(parts[0], \"foo\", 2)\n        arg_parts = list(_normexpr(parts[0].args[0]))\n        assert len(arg_parts) == 1\n        self._assert_call(arg_parts[0], \"bar\", 1)\n        assert list(_normexpr(parts[0].args[1])) == [\"baz\"]\n\n    def test_nested_call_with_argument(self):\n        parts = list(_normparse(\"%foo{%bar{baz}}\"))\n        assert len(parts) == 1\n        self._assert_call(parts[0], \"foo\", 1)\n        arg_parts = list(_normexpr(parts[0].args[0]))\n        assert len(arg_parts) == 1\n        self._assert_call(arg_parts[0], \"bar\", 1)\n        assert list(_normexpr(arg_parts[0].args[0])) == [\"baz\"]\n\n    def test_sep_before_call_two_args(self):\n        parts = list(_normparse(\"hello, %foo{bar,baz}\"))\n        assert len(parts) == 2\n        assert parts[0] == \"hello, \"\n        self._assert_call(parts[1], \"foo\", 2)\n        assert list(_normexpr(parts[1].args[0])) == [\"bar\"]\n        assert list(_normexpr(parts[1].args[1])) == [\"baz\"]\n\n    def test_sep_with_symbols(self):\n        parts = list(_normparse(\"hello,$foo,$bar\"))\n        assert len(parts) == 4\n        assert parts[0] == \"hello,\"\n        self._assert_symbol(parts[1], \"foo\")\n        assert parts[2] == \",\"\n        self._assert_symbol(parts[3], \"bar\")\n\n    def test_newline_at_end(self):\n        parts = list(_normparse(\"foo\\n\"))\n        assert len(parts) == 1\n        assert parts[0] == \"foo\\n\"\n\n\nclass EvalTest(unittest.TestCase):\n    def _eval(self, template):\n        values = {\n            \"foo\": \"bar\",\n            \"baz\": \"BaR\",\n        }\n        functions = {\n            \"lower\": str.lower,\n            \"len\": len,\n        }\n        return functemplate.Template(template).substitute(values, functions)\n\n    def test_plain_text(self):\n        assert self._eval(\"foo\") == \"foo\"\n\n    def test_subtitute_value(self):\n        assert self._eval(\"$foo\") == \"bar\"\n\n    def test_subtitute_value_in_text(self):\n        assert self._eval(\"hello $foo world\") == \"hello bar world\"\n\n    def test_not_subtitute_undefined_value(self):\n        assert self._eval(\"$bar\") == \"$bar\"\n\n    def test_function_call(self):\n        assert self._eval(\"%lower{FOO}\") == \"foo\"\n\n    def test_function_call_with_text(self):\n        assert self._eval(\"A %lower{FOO} B\") == \"A foo B\"\n\n    def test_nested_function_call(self):\n        assert self._eval(\"%lower{%lower{FOO}}\") == \"foo\"\n\n    def test_symbol_in_argument(self):\n        assert self._eval(\"%lower{$baz}\") == \"bar\"\n\n    def test_function_call_exception(self):\n        res = self._eval(\"%lower{a,b,c,d,e}\")\n        assert isinstance(res, str)\n\n    def test_function_returning_integer(self):\n        assert self._eval(\"%len{foo}\") == \"3\"\n\n    def test_not_subtitute_undefined_func(self):\n        assert self._eval(\"%bar{}\") == \"%bar{}\"\n\n    def test_not_subtitute_func_with_no_args(self):\n        assert self._eval(\"%lower\") == \"%lower\"\n\n    def test_function_call_with_empty_arg(self):\n        assert self._eval(\"%len{}\") == \"0\"\n"
  },
  {
    "path": "test/test_types.py",
    "content": "import time\n\nimport beets\nfrom beets.dbcore import types\nfrom beets.util import normpath\n\n\ndef test_datetype():\n    t = types.DATE\n\n    # format\n    time_format = beets.config[\"time_format\"].as_str()\n    time_local = time.strftime(time_format, time.localtime(123456789))\n    assert time_local == t.format(123456789)\n    # parse\n    assert 123456789.0 == t.parse(time_local)\n    assert 123456789.0 == t.parse(\"123456789.0\")\n    assert t.null == t.parse(\"not123456789.0\")\n    assert t.null == t.parse(\"1973-11-29\")\n\n\ndef test_pathtype():\n    t = types.PathType()\n\n    # format\n    assert \"/tmp\" == t.format(\"/tmp\")\n    assert \"/tmp/\\xe4lbum\" == t.format(\"/tmp/\\u00e4lbum\")\n    # parse\n    assert normpath(b\"/tmp\") == t.parse(\"/tmp\")\n    assert normpath(b\"/tmp/\\xc3\\xa4lbum\") == t.parse(\"/tmp/\\u00e4lbum/\")\n\n\ndef test_musicalkey():\n    t = types.MusicalKey()\n\n    # parse\n    assert \"C#m\" == t.parse(\"c#m\")\n    assert \"Gm\" == t.parse(\"g   minor\")\n    assert \"Not c#m\" == t.parse(\"not C#m\")\n\n\ndef test_durationtype():\n    t = types.DurationType()\n\n    # format\n    assert \"1:01\" == t.format(61.23)\n    assert \"60:01\" == t.format(3601.23)\n    assert \"0:00\" == t.format(None)\n    # parse\n    assert 61.0 == t.parse(\"1:01\")\n    assert 61.23 == t.parse(\"61.23\")\n    assert 3601.0 == t.parse(\"60:01\")\n    assert t.null == t.parse(\"1:00:01\")\n    assert t.null == t.parse(\"not61.23\")\n    # config format_raw_length\n    beets.config[\"format_raw_length\"] = True\n    assert 61.23 == t.format(61.23)\n    assert 3601.23 == t.format(3601.23)\n"
  },
  {
    "path": "test/test_util.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\"\"\"Tests for base utils from the beets.util package.\"\"\"\n\nimport os\nimport platform\nimport re\nimport subprocess\nimport sys\nimport unittest\nfrom unittest.mock import Mock, patch\n\nimport pytest\n\nfrom beets import util\nfrom beets.library import Item\nfrom beets.test import _common\n\n\nclass UtilTest(unittest.TestCase):\n    def test_open_anything(self):\n        with _common.system_mock(\"Windows\"):\n            assert util.open_anything() == 'cmd /c start \"\"'\n\n        with _common.system_mock(\"Darwin\"):\n            assert util.open_anything() == \"open\"\n\n        with _common.system_mock(\"Tagada\"):\n            assert util.open_anything() == \"xdg-open\"\n\n    @patch(\"os.execlp\")\n    @patch(\"beets.util.open_anything\")\n    def test_interactive_open(self, mock_open, mock_execlp):\n        mock_open.return_value = \"tagada\"\n        util.interactive_open([\"foo\"], util.open_anything())\n        mock_execlp.assert_called_once_with(\"tagada\", \"tagada\", \"foo\")\n        mock_execlp.reset_mock()\n\n        util.interactive_open([\"foo\"], \"bar\")\n        mock_execlp.assert_called_once_with(\"bar\", \"bar\", \"foo\")\n\n    def test_sanitize_unix_replaces_leading_dot(self):\n        with _common.platform_posix():\n            p = util.sanitize_path(\"one/.two/three\")\n        assert \".\" not in p\n\n    def test_sanitize_windows_replaces_trailing_dot(self):\n        with _common.platform_windows():\n            p = util.sanitize_path(\"one/two./three\")\n        assert \".\" not in p\n\n    def test_sanitize_windows_replaces_illegal_chars(self):\n        with _common.platform_windows():\n            p = util.sanitize_path(':*?\"<>|')\n        assert \":\" not in p\n        assert \"*\" not in p\n        assert \"?\" not in p\n        assert '\"' not in p\n        assert \"<\" not in p\n        assert \">\" not in p\n        assert \"|\" not in p\n\n    def test_sanitize_windows_replaces_trailing_space(self):\n        with _common.platform_windows():\n            p = util.sanitize_path(\"one/two /three\")\n        assert \" \" not in p\n\n    def test_sanitize_path_works_on_empty_string(self):\n        with _common.platform_posix():\n            p = util.sanitize_path(\"\")\n        assert p == \"\"\n\n    def test_sanitize_with_custom_replace_overrides_built_in_sub(self):\n        with _common.platform_posix():\n            p = util.sanitize_path(\"a/.?/b\", [(re.compile(r\"foo\"), \"bar\")])\n        assert p == \"a/.?/b\"\n\n    def test_sanitize_with_custom_replace_adds_replacements(self):\n        with _common.platform_posix():\n            p = util.sanitize_path(\"foo/bar\", [(re.compile(r\"foo\"), \"bar\")])\n        assert p == \"bar/bar\"\n\n    @unittest.skip(\"unimplemented: #359\")\n    def test_sanitize_empty_component(self):\n        with _common.platform_posix():\n            p = util.sanitize_path(\"foo//bar\", [(re.compile(r\"^$\"), \"_\")])\n        assert p == \"foo/_/bar\"\n\n    @patch(\"beets.util.subprocess.Popen\")\n    def test_command_output(self, mock_popen):\n        def popen_fail(*args, **kwargs):\n            m = Mock(returncode=1)\n            m.communicate.return_value = \"foo\", \"bar\"\n            return m\n\n        mock_popen.side_effect = popen_fail\n        with pytest.raises(subprocess.CalledProcessError) as exc_info:\n            util.command_output([\"taga\", \"\\xc3\\xa9\"])\n        assert exc_info.value.returncode == 1\n        assert exc_info.value.cmd == \"taga \\xc3\\xa9\"\n\n    def test_case_sensitive_default(self):\n        path = util.bytestring_path(\n            util.normpath(\n                \"/this/path/does/not/exist\",\n            )\n        )\n\n        assert util.case_sensitive(path) == (platform.system() != \"Windows\")\n\n    @unittest.skipIf(sys.platform == \"win32\", \"fs is not case sensitive\")\n    def test_case_sensitive_detects_sensitive(self):\n        # FIXME: Add tests for more code paths of case_sensitive()\n        # when the filesystem on the test runner is not case sensitive\n        pass\n\n    @unittest.skipIf(sys.platform != \"win32\", \"fs is case sensitive\")\n    def test_case_sensitive_detects_insensitive(self):\n        # FIXME: Add tests for more code paths of case_sensitive()\n        # when the filesystem on the test runner is case sensitive\n        pass\n\n\nclass PathConversionTest(unittest.TestCase):\n    def test_syspath_windows_format(self):\n        with _common.platform_windows():\n            path = os.path.join(\"a\", \"b\", \"c\")\n            outpath = util.syspath(path)\n        assert isinstance(outpath, str)\n        assert outpath.startswith(\"\\\\\\\\?\\\\\")\n\n    def test_syspath_windows_format_unc_path(self):\n        # The \\\\?\\ prefix on Windows behaves differently with UNC\n        # (network share) paths.\n        path = \"\\\\\\\\server\\\\share\\\\file.mp3\"\n        with _common.platform_windows():\n            outpath = util.syspath(path)\n        assert isinstance(outpath, str)\n        assert outpath == \"\\\\\\\\?\\\\UNC\\\\server\\\\share\\\\file.mp3\"\n\n    def test_syspath_posix_unchanged(self):\n        with _common.platform_posix():\n            path = os.path.join(\"a\", \"b\", \"c\")\n            outpath = util.syspath(path)\n        assert path == outpath\n\n    def _windows_bytestring_path(self, path):\n        with _common.platform_windows():\n            return util.bytestring_path(path)\n\n    def test_bytestring_path_windows_encodes_utf8(self):\n        path = \"caf\\xe9\"\n        outpath = self._windows_bytestring_path(path)\n        assert path == outpath.decode(\"utf-8\")\n\n    def test_bytesting_path_windows_removes_magic_prefix(self):\n        path = \"\\\\\\\\?\\\\C:\\\\caf\\xe9\"\n        outpath = self._windows_bytestring_path(path)\n        assert outpath == \"C:\\\\caf\\xe9\".encode()\n\n\nclass TestPathLegalization:\n    _p = pytest.param\n\n    @pytest.fixture(autouse=True)\n    def _patch_max_filename_length(self, monkeypatch):\n        monkeypatch.setattr(\"beets.util.get_max_filename_length\", lambda: 5)\n\n    @pytest.mark.parametrize(\n        \"path, expected\",\n        [\n            _p(\"abcdeX/fgh\", \"abcde/fgh\", id=\"truncate-parent-dir\"),\n            _p(\"abcde/fXX.ext\", \"abcde/f.ext\", id=\"truncate-filename\"),\n            # note that 🎹 is 4 bytes long:\n            # >>> \"🎹\".encode(\"utf-8\")\n            # b'\\xf0\\x9f\\x8e\\xb9'\n            _p(\"a🎹/a.ext\", \"a🎹/a.ext\", id=\"unicode-fit\"),\n            _p(\"ab🎹/a.ext\", \"ab/a.ext\", id=\"unicode-truncate-fully-one-byte-over-limit\"),\n            _p(\"f.a.e\", \"f.a.e\", id=\"persist-dot-in-filename\"),  # see #5771\n        ],\n    )  # fmt: skip\n    def test_truncate(self, path, expected):\n        path = path.replace(\"/\", os.path.sep)\n        expected = expected.replace(\"/\", os.path.sep)\n\n        assert util.truncate_path(path) == expected\n\n    @pytest.mark.parametrize(\n        \"replacements, expected_path, expected_truncated\",\n        [  # [ repl before truncation, repl after truncation   ]\n            _p([                                                  ], \"_abcd\",  False, id=\"default\"),\n            _p([(r\"abcdX$\", \"1ST\"),                               ], \":1ST\",   False, id=\"1st_valid\"),\n            _p([(r\"abcdX$\", \"TOO_LONG\"),                          ], \":TOO_\",  False, id=\"1st_truncated\"),\n            _p([(r\"abcdX$\", \"1ST\"),       (r\"1ST$\",   \"2ND\")      ], \":2ND\",   False, id=\"both_valid\"),\n            _p([(r\"abcdX$\", \"TOO_LONG\"),  (r\"TOO_$\",  \"2ND\")      ], \":2ND\",   False, id=\"1st_truncated_2nd_valid\"),\n            _p([(r\"abcdX$\", \"1ST\"),       (r\"1ST$\",   \"TOO_LONG\") ], \":TOO_\",  False, id=\"1st_valid_2nd_truncated\"),\n            # if the logic truncates the path twice, it ends up applying the default replacements\n            _p([(r\"abcdX$\", \"TOO_LONG\"),  (r\"TOO_$\",  \"TOO_LONG\") ], \"_TOO_\",  True,  id=\"both_truncated_default_repl_applied\"),\n        ]\n    )  # fmt: skip\n    def test_replacements(\n        self, replacements, expected_path, expected_truncated\n    ):\n        replacements = [(re.compile(pat), repl) for pat, repl in replacements]\n\n        assert util.legalize_path(\":abcdX\", replacements, \"\") == (\n            expected_path,\n            expected_truncated,\n        )\n\n\nclass TestPlurality:\n    @pytest.mark.parametrize(\n        \"objs, expected_obj, expected_freq\",\n        [\n            pytest.param([1, 1, 1, 1], 1, 4, id=\"consensus\"),\n            pytest.param([1, 1, 2, 1], 1, 3, id=\"near consensus\"),\n            pytest.param([1, 1, 2, 2, 3], 1, 2, id=\"conflict-first-wins\"),\n        ],\n    )\n    def test_plurality(self, objs, expected_obj, expected_freq):\n        assert (expected_obj, expected_freq) == util.plurality(objs)\n\n    def test_empty_sequence_raises_error(self):\n        with pytest.raises(ValueError, match=\"must be non-empty\"):\n            util.plurality([])\n\n    def test_get_most_common_tags(self):\n        items = [\n            Item(albumartist=\"aartist\", label=\"label 1\", album=\"album\"),\n            Item(albumartist=\"aartist\", label=\"label 2\", album=\"album\"),\n            Item(albumartist=\"aartist\", label=\"label 3\", album=\"another album\"),\n        ]\n\n        likelies, consensus = util.get_most_common_tags(items)\n\n        assert likelies[\"albumartist\"] == \"aartist\"\n        assert likelies[\"album\"] == \"album\"\n        # albumartist consensus overrides artist\n        assert likelies[\"artist\"] == \"aartist\"\n        assert likelies[\"label\"] == \"label 1\"\n        assert likelies[\"year\"] == 0\n\n        assert consensus[\"year\"]\n        assert consensus[\"albumartist\"]\n        assert not consensus[\"album\"]\n        assert not consensus[\"label\"]\n"
  },
  {
    "path": "test/testall.py",
    "content": "#!/usr/bin/env python3\n\n# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\nimport os\nimport sys\n\npkgpath = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) or \"..\"\nsys.path.insert(0, pkgpath)\n"
  },
  {
    "path": "test/ui/__init__.py",
    "content": ""
  },
  {
    "path": "test/ui/commands/__init__.py",
    "content": ""
  },
  {
    "path": "test/ui/commands/test_completion.py",
    "content": "import os\nimport subprocess\nimport sys\n\nimport pytest\n\nfrom beets.test import _common\nfrom beets.test.helper import IOMixin, has_program\nfrom beets.ui.commands.completion import BASH_COMPLETION_PATHS\nfrom beets.util import syspath\n\nfrom ..test_ui import TestPluginTestCase\n\n\n@_common.slow_test()\n@pytest.mark.xfail(\n    os.environ.get(\"GITHUB_ACTIONS\") == \"true\" and sys.platform == \"linux\",\n    reason=\"Completion is for some reason unhappy on Ubuntu 24.04 in CI\",\n)\nclass CompletionTest(IOMixin, TestPluginTestCase):\n    def test_completion(self):\n        # Do not load any other bash completion scripts on the system.\n        env = dict(os.environ)\n        env[\"BASH_COMPLETION_DIR\"] = os.devnull\n        env[\"BASH_COMPLETION_COMPAT_DIR\"] = os.devnull\n\n        # Open a `bash` process to run the tests in. We'll pipe in bash\n        # commands via stdin.\n        cmd = os.environ.get(\"BEETS_TEST_SHELL\", \"/bin/bash --norc\").split()\n        if not has_program(cmd[0]):\n            self.skipTest(\"bash not available\")\n        tester = subprocess.Popen(\n            cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, env=env\n        )\n\n        # Load bash_completion library.\n        for path in BASH_COMPLETION_PATHS:\n            if os.path.exists(syspath(path)):\n                bash_completion = path\n                break\n        else:\n            self.skipTest(\"bash-completion script not found\")\n        try:\n            with open(syspath(bash_completion), \"rb\") as f:\n                tester.stdin.writelines(f)\n        except OSError:\n            self.skipTest(\"could not read bash-completion script\")\n\n        # Load completion script.\n        self.run_command(\"completion\", lib=None)\n        completion_script = self.io.getoutput().encode(\"utf-8\")\n        tester.stdin.writelines(completion_script.splitlines(True))\n\n        # Load test suite.\n        test_script_name = os.path.join(_common.RSRC, b\"test_completion.sh\")\n        with open(test_script_name, \"rb\") as test_script_file:\n            tester.stdin.writelines(test_script_file)\n        out, _ = tester.communicate()\n        assert tester.returncode == 0\n        assert out == b\"completion tests passed\\n\", (\n            \"test/test_completion.sh did not execute properly. \"\n            f\"Output:{out.decode('utf-8')}\"\n        )\n"
  },
  {
    "path": "test/ui/commands/test_config.py",
    "content": "import os\nfrom unittest.mock import patch\n\nimport pytest\nimport yaml\n\nfrom beets import config, ui\nfrom beets.test.helper import BeetsTestCase, IOMixin\n\n\nclass ConfigCommandTest(IOMixin, BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n        for k in (\"VISUAL\", \"EDITOR\"):\n            if k in os.environ:\n                del os.environ[k]\n\n        temp_dir = self.temp_dir.decode()\n\n        self.config_path = os.path.join(temp_dir, \"config.yaml\")\n        with open(self.config_path, \"w\") as file:\n            file.write(\"library: lib\\n\")\n            file.write(\"option: value\\n\")\n            file.write(\"password: password_value\")\n\n        self.cli_config_path = os.path.join(temp_dir, \"cli_config.yaml\")\n        with open(self.cli_config_path, \"w\") as file:\n            file.write(\"option: cli overwrite\")\n\n        config.clear()\n        config[\"password\"].redact = True\n        config._materialized = False\n\n    def _run_with_yaml_output(self, *args):\n        output = self.run_with_output(*args)\n        return yaml.safe_load(output)\n\n    def test_show_user_config(self):\n        output = self._run_with_yaml_output(\"config\", \"-c\")\n\n        assert output[\"option\"] == \"value\"\n        assert output[\"password\"] == \"password_value\"\n\n    def test_show_user_config_with_defaults(self):\n        output = self._run_with_yaml_output(\"config\", \"-dc\")\n\n        assert output[\"option\"] == \"value\"\n        assert output[\"password\"] == \"password_value\"\n        assert output[\"library\"] == \"lib\"\n        assert not output[\"import\"][\"timid\"]\n\n    def test_show_user_config_with_cli(self):\n        output = self._run_with_yaml_output(\n            \"--config\", self.cli_config_path, \"config\"\n        )\n\n        assert output[\"library\"] == \"lib\"\n        assert output[\"option\"] == \"cli overwrite\"\n\n    def test_show_redacted_user_config(self):\n        output = self._run_with_yaml_output(\"config\")\n\n        assert output[\"option\"] == \"value\"\n        assert output[\"password\"] == \"REDACTED\"\n\n    def test_show_redacted_user_config_with_defaults(self):\n        output = self._run_with_yaml_output(\"config\", \"-d\")\n\n        assert output[\"option\"] == \"value\"\n        assert output[\"password\"] == \"REDACTED\"\n        assert not output[\"import\"][\"timid\"]\n\n    def test_config_paths(self):\n        output = self.run_with_output(\"config\", \"-p\")\n\n        paths = output.split(\"\\n\")\n        assert len(paths) == 2\n        assert paths[0] == self.config_path\n\n    def test_config_paths_with_cli(self):\n        output = self.run_with_output(\n            \"--config\", self.cli_config_path, \"config\", \"-p\"\n        )\n        paths = output.split(\"\\n\")\n        assert len(paths) == 3\n        assert paths[0] == self.cli_config_path\n\n    def test_edit_config_with_visual_or_editor_env(self):\n        os.environ[\"EDITOR\"] = \"myeditor\"\n        with patch(\"os.execlp\") as execlp:\n            self.run_command(\"config\", \"-e\")\n        execlp.assert_called_once_with(\"myeditor\", \"myeditor\", self.config_path)\n\n        os.environ[\"VISUAL\"] = \"\"  # empty environment variables gets ignored\n        with patch(\"os.execlp\") as execlp:\n            self.run_command(\"config\", \"-e\")\n        execlp.assert_called_once_with(\"myeditor\", \"myeditor\", self.config_path)\n\n        os.environ[\"VISUAL\"] = \"myvisual\"\n        with patch(\"os.execlp\") as execlp:\n            self.run_command(\"config\", \"-e\")\n        execlp.assert_called_once_with(\"myvisual\", \"myvisual\", self.config_path)\n\n    def test_edit_config_with_automatic_open(self):\n        with patch(\"beets.util.open_anything\") as open:\n            open.return_value = \"please_open\"\n            with patch(\"os.execlp\") as execlp:\n                self.run_command(\"config\", \"-e\")\n        execlp.assert_called_once_with(\n            \"please_open\", \"please_open\", self.config_path\n        )\n\n    def test_config_editor_not_found(self):\n        msg_match = \"Could not edit configuration.*here is problem\"\n        with (\n            patch(\"os.execlp\", side_effect=OSError(\"here is problem\")),\n            pytest.raises(ui.UserError, match=msg_match),\n        ):\n            self.run_command(\"config\", \"-e\")\n\n    def test_edit_invalid_config_file(self):\n        with open(self.config_path, \"w\") as file:\n            file.write(\"invalid: [\")\n        config.clear()\n        config._materialized = False\n\n        os.environ[\"EDITOR\"] = \"myeditor\"\n        with patch(\"os.execlp\") as execlp:\n            self.run_command(\"config\", \"-e\")\n        execlp.assert_called_once_with(\"myeditor\", \"myeditor\", self.config_path)\n\n    def test_edit_config_with_custom_config_path(self):\n        os.environ[\"EDITOR\"] = \"myeditor\"\n        with patch(\"os.execlp\") as execlp:\n            self.run_command(\"--config\", self.cli_config_path, \"config\", \"-e\")\n        execlp.assert_called_once_with(\n            \"myeditor\", \"myeditor\", self.cli_config_path\n        )\n"
  },
  {
    "path": "test/ui/commands/test_fields.py",
    "content": "from beets import library\nfrom beets.test.helper import IOMixin, ItemInDBTestCase\nfrom beets.ui.commands.fields import fields_func\n\n\nclass FieldsTest(IOMixin, ItemInDBTestCase):\n    def remove_keys(self, keys, text):\n        for i in text:\n            try:\n                keys.remove(i)\n            except ValueError:\n                pass\n\n    def test_fields_func(self):\n        fields_func(self.lib, [], [])\n        items = library.Item.all_keys()\n        albums = library.Album.all_keys()\n\n        output = self.io.getoutput().split()\n        self.remove_keys(items, output)\n        self.remove_keys(albums, output)\n\n        assert len(items) == 0\n        assert len(albums) == 0\n"
  },
  {
    "path": "test/ui/commands/test_import.py",
    "content": "import os\nimport unittest\nfrom unittest.mock import Mock, patch\n\nimport pytest\n\nfrom beets import autotag, config, library, ui\nfrom beets.autotag.match import distance\nfrom beets.test import _common\nfrom beets.test.helper import BeetsTestCase, IOMixin\nfrom beets.ui.commands.import_ import import_files, paths_from_logfile\nfrom beets.ui.commands.import_.display import show_change\nfrom beets.ui.commands.import_.session import summarize_items\n\n\nclass ImportTest(BeetsTestCase):\n    def test_quiet_timid_disallowed(self):\n        config[\"import\"][\"quiet\"] = True\n        config[\"import\"][\"timid\"] = True\n        with pytest.raises(ui.UserError):\n            import_files(None, [], None)\n\n    def test_parse_paths_from_logfile(self):\n        if os.path.__name__ == \"ntpath\":\n            logfile_content = (\n                \"import started Wed Jun 15 23:08:26 2022\\n\"\n                \"asis C:\\\\music\\\\Beatles, The\\\\The Beatles; C:\\\\music\\\\Beatles, The\\\\The Beatles\\\\CD 01; C:\\\\music\\\\Beatles, The\\\\The Beatles\\\\CD 02\\n\"  # noqa: E501\n                \"duplicate-replace C:\\\\music\\\\Bill Evans\\\\Trio '65\\n\"\n                \"skip C:\\\\music\\\\Michael Jackson\\\\Bad\\n\"\n                \"skip C:\\\\music\\\\Soulwax\\\\Any Minute Now\\n\"\n            )\n            expected_paths = [\n                \"C:\\\\music\\\\Beatles, The\\\\The Beatles\",\n                \"C:\\\\music\\\\Michael Jackson\\\\Bad\",\n                \"C:\\\\music\\\\Soulwax\\\\Any Minute Now\",\n            ]\n        else:\n            logfile_content = (\n                \"import started Wed Jun 15 23:08:26 2022\\n\"\n                \"asis /music/Beatles, The/The Beatles; /music/Beatles, The/The Beatles/CD 01; /music/Beatles, The/The Beatles/CD 02\\n\"  # noqa: E501\n                \"duplicate-replace /music/Bill Evans/Trio '65\\n\"\n                \"skip /music/Michael Jackson/Bad\\n\"\n                \"skip /music/Soulwax/Any Minute Now\\n\"\n            )\n            expected_paths = [\n                \"/music/Beatles, The/The Beatles\",\n                \"/music/Michael Jackson/Bad\",\n                \"/music/Soulwax/Any Minute Now\",\n            ]\n\n        logfile = os.path.join(self.temp_dir, b\"logfile.log\")\n        with open(logfile, mode=\"w\") as fp:\n            fp.write(logfile_content)\n        actual_paths = list(paths_from_logfile(logfile))\n        assert actual_paths == expected_paths\n\n\n@patch(\"beets.ui.term_width\", Mock(return_value=54))\nclass ShowChangeTestCase(IOMixin, BeetsTestCase):\n    def _show_change(self):\n        \"\"\"Return an unicode string representing the changes\"\"\"\n        long_name = f\"a{' very' * 10} long name\"\n        items = [\n            _common.item(track=1, title=\"first title\"),\n            _common.item(track=2, title=\"\", path=b\"/path/to/file.mp3\"),\n            _common.item(track=3, title=\"caf\\xe9\"),\n            _common.item(track=4, title=f\"title with {long_name}\"),\n        ]\n        info = autotag.AlbumInfo(\n            album=\"caf\\xe9\",\n            album_id=\"album id\",\n            artist=\"the artist\",\n            artist_id=\"artist id\",\n            tracks=[\n                autotag.TrackInfo(title=\"first title\", index=1),\n                autotag.TrackInfo(title=\"second title\", index=2),\n                autotag.TrackInfo(title=\"third title\", index=3),\n                autotag.TrackInfo(title=\"fourth title\", index=4),\n            ],\n        )\n        item_info_pairs = list(zip(items, info.tracks))\n        self.config[\"ui\"][\"color\"] = False\n        self.config[\"import\"][\"detail\"] = True\n        change_dist = distance(items, info, item_info_pairs)\n        change_dist._penalties = {\"album\": [0.1], \"artist\": [0.1]}\n        show_change(\n            f\"another artist with {long_name}\",\n            \"another album\",\n            autotag.AlbumMatch(\n                change_dist, info, dict(item_info_pairs), set(), set()\n            ),\n        )\n        return self.io.getoutput()\n\n    def test_newline_layout(self):\n        self.config[\"ui\"][\"import\"][\"layout\"] = \"newline\"\n        msg = self._show_change()\n        assert (\n            msg\n            == \"\"\"\n  Match (90.0%):\n  the artist - café\n  ≠ album, artist\n  None, None, None, None, None, None, None\n  ≠ Artist: another artist with a very very very very\n    very very very very very very long name\n   -> the artist\n  ≠ Album: another album -> café\n     * (#1) first title (1:00)\n     ≠ (#2) file.mp3 (1:00)\n      -> (#2) second title (0:00)\n     ≠ (#3) café (1:00) -> (#3) third title (0:00)\n     ≠ (#4) title with a very very very very very very\n          very very very very long name (1:00)\n      -> (#4) fourth title (0:00)\n\"\"\"\n        )\n\n    def test_column_layout(self):\n        self.config[\"ui\"][\"import\"][\"layout\"] = \"column\"\n        msg = self._show_change()\n        assert (\n            msg\n            == \"\"\"\n  Match (90.0%):\n  the artist - café\n  ≠ album, artist\n  None, None, None, None, None, None, None\n  ≠ Artist: another artist -> the artist              \n            with a very                               \n            very very very                            \n            very very very                            \n            very very very                            \n            long name                                 \n  ≠ Album: another album -> café\n     * (#1) first title (1:00)\n     ≠ (#2) file.mp (1:00) -> (#2) second    (0:00)\n            3                      title           \n     ≠ (#3) café (1:00) -> (#3) third title (0:00)\n     ≠ (#4) title   (1:00) -> (#4) fourth    (0:00)\n            with a very            title           \n            very very very                         \n            very very very                         \n            very very very                         \n            long name                              \n\"\"\"  # noqa: W291\n        )\n\n\n@patch(\"beets.library.Item.try_filesize\", Mock(return_value=987))\nclass SummarizeItemsTest(unittest.TestCase):\n    def setUp(self):\n        super().setUp()\n        item = library.Item()\n        item.bitrate = 4321\n        item.length = 10 * 60 + 54\n        item.format = \"F\"\n        self.item = item\n\n    def test_summarize_item(self):\n        summary = summarize_items([], True)\n        assert summary == \"\"\n\n        summary = summarize_items([self.item], True)\n        assert summary == \"F, 4kbps, 10:54, 987.0 B\"\n\n    def test_summarize_items(self):\n        summary = summarize_items([], False)\n        assert summary == \"0 items\"\n\n        summary = summarize_items([self.item], False)\n        assert summary == \"1 items, F, 4kbps, 10:54, 987.0 B\"\n\n        # make a copy of self.item\n        i2 = self.item.copy()\n\n        summary = summarize_items([self.item, i2], False)\n        assert summary == \"2 items, F, 4kbps, 21:48, 1.9 KiB\"\n\n        i2.format = \"G\"\n        summary = summarize_items([self.item, i2], False)\n        assert summary == \"2 items, F 1, G 1, 4kbps, 21:48, 1.9 KiB\"\n\n        summary = summarize_items([self.item, i2, i2], False)\n        assert summary == \"3 items, G 2, F 1, 4kbps, 32:42, 2.9 KiB\"\n"
  },
  {
    "path": "test/ui/commands/test_list.py",
    "content": "from beets.test import _common\nfrom beets.test.helper import BeetsTestCase, IOMixin\nfrom beets.ui.commands.list import list_items\n\n\nclass ListTest(IOMixin, BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n        self.item = _common.item()\n        self.item.path = \"xxx/yyy\"\n        self.lib.add(self.item)\n        self.lib.add_album([self.item])\n\n    def _run_list(self, query=\"\", album=False, path=False, fmt=\"\"):\n        list_items(self.lib, query, album, fmt)\n        return self.io.getoutput()\n\n    def test_list_outputs_item(self):\n        stdout = self._run_list()\n        assert \"the title\" in stdout\n\n    def test_list_unicode_query(self):\n        self.item.title = \"na\\xefve\"\n        self.item.store()\n        self.lib._connection().commit()\n\n        stdout = self._run_list([\"na\\xefve\"])\n        out = stdout\n        assert \"na\\xefve\" in out\n\n    def test_list_item_path(self):\n        stdout = self._run_list(fmt=\"$path\")\n        assert stdout.strip() == \"xxx/yyy\"\n\n    def test_list_album_outputs_something(self):\n        stdout = self._run_list(album=True)\n        assert len(stdout) > 0\n\n    def test_list_album_path(self):\n        stdout = self._run_list(album=True, fmt=\"$path\")\n        assert stdout.strip() == \"xxx\"\n\n    def test_list_album_omits_title(self):\n        stdout = self._run_list(album=True)\n        assert \"the title\" not in stdout\n\n    def test_list_uses_track_artist(self):\n        stdout = self._run_list()\n        assert \"the artist\" in stdout\n        assert \"the album artist\" not in stdout\n\n    def test_list_album_uses_album_artist(self):\n        stdout = self._run_list(album=True)\n        assert \"the artist\" not in stdout\n        assert \"the album artist\" in stdout\n\n    def test_list_item_format_artist(self):\n        stdout = self._run_list(fmt=\"$artist\")\n        assert \"the artist\" in stdout\n\n    def test_list_item_format_multiple(self):\n        stdout = self._run_list(fmt=\"$artist - $album - $year\")\n        assert \"the artist - the album - 0001\" == stdout.strip()\n\n    def test_list_album_format(self):\n        stdout = self._run_list(album=True, fmt=\"$genres\")\n        assert \"the genre\" in stdout\n        assert \"the album\" not in stdout\n"
  },
  {
    "path": "test/ui/commands/test_modify.py",
    "content": "import unittest\n\nfrom mediafile import MediaFile\n\nfrom beets.test.helper import BeetsTestCase, IOMixin\nfrom beets.ui.commands.modify import modify_parse_args\nfrom beets.util import syspath\n\n\nclass ModifyTest(IOMixin, BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n        self.album = self.add_album_fixture()\n        [self.item] = self.album.items()\n\n    def modify_inp(self, inp: list[str], *args):\n        for chat in inp:\n            self.io.addinput(chat)\n        self.run_command(\"modify\", *args)\n\n    def modify(self, *args):\n        self.modify_inp([\"y\"], *args)\n\n    # Item tests\n\n    def test_modify_item(self):\n        self.modify(\"title=newTitle\")\n        item = self.lib.items().get()\n        assert item.title == \"newTitle\"\n\n    def test_modify_item_abort(self):\n        item = self.lib.items().get()\n        title = item.title\n        self.modify_inp([\"n\"], \"title=newTitle\")\n        item = self.lib.items().get()\n        assert item.title == title\n\n    def test_modify_item_no_change(self):\n        title = \"Tracktitle\"\n        item = self.add_item_fixture(title=title)\n        self.modify_inp([\"y\"], \"title\", f\"title={title}\")\n        item = self.lib.items(title).get()\n        assert item.title == title\n\n    def test_modify_write_tags(self):\n        self.modify(\"title=newTitle\")\n        item = self.lib.items().get()\n        item.read()\n        assert item.title == \"newTitle\"\n\n    def test_modify_dont_write_tags(self):\n        self.modify(\"--nowrite\", \"title=newTitle\")\n        item = self.lib.items().get()\n        item.read()\n        assert item.title != \"newTitle\"\n\n    def test_move(self):\n        self.modify(\"title=newTitle\")\n        item = self.lib.items().get()\n        assert b\"newTitle\" in item.path\n\n    def test_not_move(self):\n        self.modify(\"--nomove\", \"title=newTitle\")\n        item = self.lib.items().get()\n        assert b\"newTitle\" not in item.path\n\n    def test_no_write_no_move(self):\n        self.modify(\"--nomove\", \"--nowrite\", \"title=newTitle\")\n        item = self.lib.items().get()\n        item.read()\n        assert b\"newTitle\" not in item.path\n        assert item.title != \"newTitle\"\n\n    def test_update_mtime(self):\n        item = self.item\n        old_mtime = item.mtime\n\n        self.modify(\"title=newTitle\")\n        item.load()\n        assert old_mtime != item.mtime\n        assert item.current_mtime() == item.mtime\n\n    def test_reset_mtime_with_no_write(self):\n        item = self.item\n\n        self.modify(\"--nowrite\", \"title=newTitle\")\n        item.load()\n        assert 0 == item.mtime\n\n    def test_selective_modify(self):\n        title = \"Tracktitle\"\n        album = \"album\"\n        original_artist = \"composer\"\n        new_artist = \"coverArtist\"\n        for i in range(0, 10):\n            self.add_item_fixture(\n                title=f\"{title}{i}\", artist=original_artist, album=album\n            )\n        self.modify_inp(\n            [\"s\", \"y\", \"y\", \"y\", \"n\", \"n\", \"y\", \"y\", \"y\", \"y\", \"n\"],\n            title,\n            f\"artist={new_artist}\",\n        )\n        original_items = self.lib.items(f\"artist:{original_artist}\")\n        new_items = self.lib.items(f\"artist:{new_artist}\")\n        assert len(list(original_items)) == 3\n        assert len(list(new_items)) == 7\n\n    def test_modify_formatted(self):\n        for i in range(0, 3):\n            self.add_item_fixture(\n                title=f\"title{i}\", artist=\"artist\", album=\"album\"\n            )\n        items = list(self.lib.items())\n        self.modify(\"title=${title} - append\")\n        for item in items:\n            orig_title = item.title\n            item.load()\n            assert item.title == f\"{orig_title} - append\"\n\n    # Album Tests\n\n    def test_modify_album(self):\n        self.modify(\"--album\", \"album=newAlbum\")\n        album = self.lib.albums().get()\n        assert album.album == \"newAlbum\"\n\n    def test_modify_album_write_tags(self):\n        self.modify(\"--album\", \"album=newAlbum\")\n        item = self.lib.items().get()\n        item.read()\n        assert item.album == \"newAlbum\"\n\n    def test_modify_album_dont_write_tags(self):\n        self.modify(\"--album\", \"--nowrite\", \"album=newAlbum\")\n        item = self.lib.items().get()\n        item.read()\n        assert item.album == \"the album\"\n\n    def test_album_move(self):\n        self.modify(\"--album\", \"album=newAlbum\")\n        item = self.lib.items().get()\n        item.read()\n        assert b\"newAlbum\" in item.path\n\n    def test_album_not_move(self):\n        self.modify(\"--nomove\", \"--album\", \"album=newAlbum\")\n        item = self.lib.items().get()\n        item.read()\n        assert b\"newAlbum\" not in item.path\n\n    def test_modify_album_formatted(self):\n        item = self.lib.items().get()\n        orig_album = item.album\n        self.modify(\"--album\", \"album=${album} - append\")\n        item.load()\n        assert item.album == f\"{orig_album} - append\"\n\n    # Misc\n\n    def test_write_initial_key_tag(self):\n        self.modify(\"initial_key=C#m\")\n        item = self.lib.items().get()\n        mediafile = MediaFile(syspath(item.path))\n        assert mediafile.initial_key == \"C#m\"\n\n    def test_set_flexattr(self):\n        self.modify(\"flexattr=testAttr\")\n        item = self.lib.items().get()\n        assert item.flexattr == \"testAttr\"\n\n    def test_remove_flexattr(self):\n        item = self.lib.items().get()\n        item.flexattr = \"testAttr\"\n        item.store()\n\n        self.modify(\"flexattr!\")\n        item = self.lib.items().get()\n        assert \"flexattr\" not in item\n\n    @unittest.skip(\"not yet implemented\")\n    def test_delete_initial_key_tag(self):\n        item = self.lib.items().get()\n        item.initial_key = \"C#m\"\n        item.write()\n        item.store()\n\n        mediafile = MediaFile(syspath(item.path))\n        assert mediafile.initial_key == \"C#m\"\n\n        self.modify(\"initial_key!\")\n        mediafile = MediaFile(syspath(item.path))\n        assert mediafile.initial_key is None\n\n    def test_arg_parsing_colon_query(self):\n        query, mods, _ = modify_parse_args([\"title:oldTitle\", \"title=newTitle\"])\n        assert query == [\"title:oldTitle\"]\n        assert mods == {\"title\": \"newTitle\"}\n\n    def test_arg_parsing_delete(self):\n        query, _, dels = modify_parse_args([\"title:oldTitle\", \"title!\"])\n        assert query == [\"title:oldTitle\"]\n        assert dels == [\"title\"]\n\n    def test_arg_parsing_query_with_exclaimation(self):\n        query, mods, _ = modify_parse_args(\n            [\"title:oldTitle!\", \"title=newTitle!\"]\n        )\n        assert query == [\"title:oldTitle!\"]\n        assert mods == {\"title\": \"newTitle!\"}\n\n    def test_arg_parsing_equals_in_value(self):\n        query, mods, _ = modify_parse_args([\"title:foo=bar\", \"title=newTitle\"])\n        assert query == [\"title:foo=bar\"]\n        assert mods == {\"title\": \"newTitle\"}\n"
  },
  {
    "path": "test/ui/commands/test_move.py",
    "content": "import shutil\n\nfrom beets import library\nfrom beets.test.helper import BeetsTestCase\nfrom beets.ui.commands.move import move_items\n\n\nclass MoveTest(BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n\n        self.initial_item_path = self.lib_path / \"srcfile\"\n        shutil.copy(self.resource_path, self.initial_item_path)\n\n        # Add a file to the library but don't copy it in yet.\n        self.i = library.Item.from_path(self.initial_item_path)\n        self.lib.add(self.i)\n        self.album = self.lib.add_album([self.i])\n\n        # Alternate destination directory.\n        self.otherdir = self.temp_dir_path / \"testotherdir\"\n\n    def _move(\n        self,\n        query=(),\n        dest=None,\n        copy=False,\n        album=False,\n        pretend=False,\n        export=False,\n    ):\n        move_items(self.lib, dest, query, copy, album, pretend, export=export)\n\n    def test_move_item(self):\n        self._move()\n        self.i.load()\n        assert b\"libdir\" in self.i.path\n        assert self.i.filepath.exists()\n        assert not self.initial_item_path.exists()\n\n    def test_copy_item(self):\n        self._move(copy=True)\n        self.i.load()\n        assert b\"libdir\" in self.i.path\n        assert self.i.filepath.exists()\n        assert self.initial_item_path.exists()\n\n    def test_move_album(self):\n        self._move(album=True)\n        self.i.load()\n        assert b\"libdir\" in self.i.path\n        assert self.i.filepath.exists()\n        assert not self.initial_item_path.exists()\n\n    def test_copy_album(self):\n        self._move(copy=True, album=True)\n        self.i.load()\n        assert b\"libdir\" in self.i.path\n        assert self.i.filepath.exists()\n        assert self.initial_item_path.exists()\n\n    def test_move_item_custom_dir(self):\n        self._move(dest=self.otherdir)\n        self.i.load()\n        assert b\"testotherdir\" in self.i.path\n        assert self.i.filepath.exists()\n        assert not self.initial_item_path.exists()\n\n    def test_move_album_custom_dir(self):\n        self._move(dest=self.otherdir, album=True)\n        self.i.load()\n        assert b\"testotherdir\" in self.i.path\n        assert self.i.filepath.exists()\n        assert not self.initial_item_path.exists()\n\n    def test_pretend_move_item(self):\n        self._move(dest=self.otherdir, pretend=True)\n        self.i.load()\n        assert self.i.filepath == self.initial_item_path\n\n    def test_pretend_move_album(self):\n        self._move(album=True, pretend=True)\n        self.i.load()\n        assert self.i.filepath == self.initial_item_path\n\n    def test_export_item_custom_dir(self):\n        self._move(dest=self.otherdir, export=True)\n        self.i.load()\n        assert self.i.filepath == self.initial_item_path\n        assert self.otherdir.exists()\n\n    def test_export_album_custom_dir(self):\n        self._move(dest=self.otherdir, album=True, export=True)\n        self.i.load()\n        assert self.i.filepath == self.initial_item_path\n        assert self.otherdir.exists()\n\n    def test_pretend_export_item(self):\n        self._move(dest=self.otherdir, pretend=True, export=True)\n        self.i.load()\n        assert self.i.filepath == self.initial_item_path\n        assert not self.otherdir.exists()\n"
  },
  {
    "path": "test/ui/commands/test_remove.py",
    "content": "import os\n\nfrom beets import library\nfrom beets.test.helper import BeetsTestCase, IOMixin\nfrom beets.ui.commands.remove import remove_items\nfrom beets.util import MoveOperation, syspath\n\n\nclass RemoveTest(IOMixin, BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n\n        # Copy a file into the library.\n        self.i = library.Item.from_path(self.resource_path)\n        self.lib.add(self.i)\n        self.i.move(operation=MoveOperation.COPY)\n\n    def test_remove_items_no_delete(self):\n        self.io.addinput(\"y\")\n        remove_items(self.lib, \"\", False, False, False)\n        items = self.lib.items()\n        assert len(list(items)) == 0\n        assert self.i.filepath.exists()\n\n    def test_remove_items_with_delete(self):\n        self.io.addinput(\"y\")\n        remove_items(self.lib, \"\", False, True, False)\n        items = self.lib.items()\n        assert len(list(items)) == 0\n        assert not self.i.filepath.exists()\n\n    def test_remove_items_with_force_no_delete(self):\n        remove_items(self.lib, \"\", False, False, True)\n        items = self.lib.items()\n        assert len(list(items)) == 0\n        assert self.i.filepath.exists()\n\n    def test_remove_items_with_force_delete(self):\n        remove_items(self.lib, \"\", False, True, True)\n        items = self.lib.items()\n        assert len(list(items)) == 0\n        assert not self.i.filepath.exists()\n\n    def test_remove_items_select_with_delete(self):\n        i2 = library.Item.from_path(self.resource_path)\n        self.lib.add(i2)\n        i2.move(operation=MoveOperation.COPY)\n\n        for s in (\"s\", \"y\", \"n\"):\n            self.io.addinput(s)\n        remove_items(self.lib, \"\", False, True, False)\n        items = self.lib.items()\n        assert len(list(items)) == 1\n        # There is probably no guarantee that the items are queried in any\n        # spcecific order, thus just ensure that exactly one was removed.\n        # To improve upon this, self.io would need to have the capability to\n        # generate input that depends on previous output.\n        num_existing = 0\n        num_existing += 1 if os.path.exists(syspath(self.i.path)) else 0\n        num_existing += 1 if os.path.exists(syspath(i2.path)) else 0\n        assert num_existing == 1\n\n    def test_remove_albums_select_with_delete(self):\n        a1 = self.add_album_fixture()\n        a2 = self.add_album_fixture()\n        path1 = a1.items()[0].path\n        path2 = a2.items()[0].path\n        items = self.lib.items()\n        assert len(list(items)) == 3\n\n        for s in (\"s\", \"y\", \"n\"):\n            self.io.addinput(s)\n        remove_items(self.lib, \"\", True, True, False)\n        items = self.lib.items()\n        assert len(list(items)) == 2  # incl. the item from setUp()\n        # See test_remove_items_select_with_delete()\n        num_existing = 0\n        num_existing += 1 if os.path.exists(syspath(path1)) else 0\n        num_existing += 1 if os.path.exists(syspath(path2)) else 0\n        assert num_existing == 1\n"
  },
  {
    "path": "test/ui/commands/test_update.py",
    "content": "import os\n\nfrom mediafile import MediaFile\n\nfrom beets import library\nfrom beets.test import _common\nfrom beets.test.helper import BeetsTestCase, IOMixin\nfrom beets.ui.commands.update import update_items\nfrom beets.util import MoveOperation, remove, syspath\n\n\nclass UpdateTest(IOMixin, BeetsTestCase):\n    def setUp(self):\n        super().setUp()\n\n        # Copy a file into the library.\n        item_path = os.path.join(_common.RSRC, b\"full.mp3\")\n        item_path_two = os.path.join(_common.RSRC, b\"full.flac\")\n        self.i = library.Item.from_path(item_path)\n        self.i2 = library.Item.from_path(item_path_two)\n        self.lib.add(self.i)\n        self.lib.add(self.i2)\n        self.i.move(operation=MoveOperation.COPY)\n        self.i2.move(operation=MoveOperation.COPY)\n        self.album = self.lib.add_album([self.i, self.i2])\n\n        # Album art.\n        artfile = os.path.join(self.temp_dir, b\"testart.jpg\")\n        _common.touch(artfile)\n        self.album.set_art(artfile)\n        self.album.store()\n        remove(artfile)\n\n    def _update(\n        self,\n        query=(),\n        album=False,\n        move=False,\n        reset_mtime=True,\n        fields=None,\n        exclude_fields=None,\n    ):\n        self.io.addinput(\"y\")\n        if reset_mtime:\n            self.i.mtime = 0\n            self.i.store()\n        update_items(\n            self.lib,\n            query,\n            album,\n            move,\n            False,\n            fields=fields,\n            exclude_fields=exclude_fields,\n        )\n\n    def test_delete_removes_item(self):\n        assert list(self.lib.items())\n        remove(self.i.path)\n        remove(self.i2.path)\n        self._update()\n        assert not list(self.lib.items())\n\n    def test_delete_removes_album(self):\n        assert self.lib.albums()\n        remove(self.i.path)\n        remove(self.i2.path)\n        self._update()\n        assert not self.lib.albums()\n\n    def test_delete_removes_album_art(self):\n        art_filepath = self.album.art_filepath\n        assert art_filepath.exists()\n        remove(self.i.path)\n        remove(self.i2.path)\n        self._update()\n        assert not art_filepath.exists()\n\n    def test_modified_metadata_detected(self):\n        mf = MediaFile(syspath(self.i.path))\n        mf.title = \"differentTitle\"\n        mf.save()\n        self._update()\n        item = self.lib.items().get()\n        assert item.title == \"differentTitle\"\n\n    def test_modified_metadata_moved(self):\n        mf = MediaFile(syspath(self.i.path))\n        mf.title = \"differentTitle\"\n        mf.save()\n        self._update(move=True)\n        item = self.lib.items().get()\n        assert b\"differentTitle\" in item.path\n\n    def test_modified_metadata_not_moved(self):\n        mf = MediaFile(syspath(self.i.path))\n        mf.title = \"differentTitle\"\n        mf.save()\n        self._update(move=False)\n        item = self.lib.items().get()\n        assert b\"differentTitle\" not in item.path\n\n    def test_selective_modified_metadata_moved(self):\n        mf = MediaFile(syspath(self.i.path))\n        mf.title = \"differentTitle\"\n        mf.genres = [\"differentGenre\"]\n        mf.save()\n        self._update(move=True, fields=[\"title\"])\n        item = self.lib.items().get()\n        assert b\"differentTitle\" in item.path\n        assert item.genres != [\"differentGenre\"]\n\n    def test_selective_modified_metadata_not_moved(self):\n        mf = MediaFile(syspath(self.i.path))\n        mf.title = \"differentTitle\"\n        mf.genres = [\"differentGenre\"]\n        mf.save()\n        self._update(move=False, fields=[\"title\"])\n        item = self.lib.items().get()\n        assert b\"differentTitle\" not in item.path\n        assert item.genres != [\"differentGenre\"]\n\n    def test_modified_album_metadata_moved(self):\n        mf = MediaFile(syspath(self.i.path))\n        mf.album = \"differentAlbum\"\n        mf.save()\n        self._update(move=True)\n        item = self.lib.items().get()\n        assert b\"differentAlbum\" in item.path\n\n    def test_modified_album_metadata_art_moved(self):\n        artpath = self.album.artpath\n        mf = MediaFile(syspath(self.i.path))\n        mf.album = \"differentAlbum\"\n        mf.save()\n        self._update(move=True)\n        album = self.lib.albums()[0]\n        assert artpath != album.artpath\n        assert album.artpath is not None\n\n    def test_selective_modified_album_metadata_moved(self):\n        mf = MediaFile(syspath(self.i.path))\n        mf.album = \"differentAlbum\"\n        mf.genres = [\"differentGenre\"]\n        mf.save()\n        self._update(move=True, fields=[\"album\"])\n        item = self.lib.items().get()\n        assert b\"differentAlbum\" in item.path\n        assert item.genres != [\"differentGenre\"]\n\n    def test_selective_modified_album_metadata_not_moved(self):\n        mf = MediaFile(syspath(self.i.path))\n        mf.album = \"differentAlbum\"\n        mf.genres = [\"differentGenre\"]\n        mf.save()\n        self._update(move=True, fields=[\"genres\"])\n        item = self.lib.items().get()\n        assert b\"differentAlbum\" not in item.path\n        assert item.genres == [\"differentGenre\"]\n\n    def test_mtime_match_skips_update(self):\n        mf = MediaFile(syspath(self.i.path))\n        mf.title = \"differentTitle\"\n        mf.save()\n\n        # Make in-memory mtime match on-disk mtime.\n        self.i.mtime = os.path.getmtime(syspath(self.i.path))\n        self.i.store()\n\n        self._update(reset_mtime=False)\n        item = self.lib.items().get()\n        assert item.title == \"full\"\n\n    def test_multivalued_albumtype_roundtrip(self):\n        # https://github.com/beetbox/beets/issues/4528\n\n        # albumtypes is empty for our test fixtures, so populate it first\n        album = self.album\n        correct_albumtypes = [\"album\", \"live\"]\n\n        # Setting albumtypes does not set albumtype, currently.\n        # Using x[0] mirrors https://github.com/beetbox/mediafile/blob/057432ad53b3b84385e5582f69f44dc00d0a725d/mediafile.py#L1928  # noqa: E501\n        correct_albumtype = correct_albumtypes[0]\n\n        album.albumtype = correct_albumtype\n        album.albumtypes = correct_albumtypes\n        album.try_sync(write=True, move=False)\n\n        album.load()\n        assert album.albumtype == correct_albumtype\n        assert album.albumtypes == correct_albumtypes\n\n        self._update()\n\n        album.load()\n        assert album.albumtype == correct_albumtype\n        assert album.albumtypes == correct_albumtypes\n\n    def test_modified_metadata_excluded(self):\n        mf = MediaFile(syspath(self.i.path))\n        mf.lyrics = \"new lyrics\"\n        mf.save()\n        self._update(exclude_fields=[\"lyrics\"])\n        item = self.lib.items().get()\n        assert item.lyrics != \"new lyrics\"\n"
  },
  {
    "path": "test/ui/commands/test_utils.py",
    "content": "import os\nimport shutil\n\nimport pytest\n\nfrom beets import library, ui\nfrom beets.test import _common\nfrom beets.test.helper import BeetsTestCase\nfrom beets.ui.commands.utils import do_query\nfrom beets.util import syspath\n\n\nclass QueryTest(BeetsTestCase):\n    def add_item(self, filename=b\"srcfile\", templatefile=b\"full.mp3\"):\n        itempath = os.path.join(self.libdir, filename)\n        shutil.copy(\n            syspath(os.path.join(_common.RSRC, templatefile)),\n            syspath(itempath),\n        )\n        item = library.Item.from_path(itempath)\n        self.lib.add(item)\n        return item\n\n    def add_album(self, items):\n        album = self.lib.add_album(items)\n        return album\n\n    def check_do_query(\n        self, num_items, num_albums, q=(), album=False, also_items=True\n    ):\n        items, albums = do_query(self.lib, q, album, also_items)\n        assert len(items) == num_items\n        assert len(albums) == num_albums\n\n    def test_query_empty(self):\n        with pytest.raises(ui.UserError):\n            do_query(self.lib, (), False)\n\n    def test_query_empty_album(self):\n        with pytest.raises(ui.UserError):\n            do_query(self.lib, (), True)\n\n    def test_query_item(self):\n        self.add_item()\n        self.check_do_query(1, 0, album=False)\n        self.add_item()\n        self.check_do_query(2, 0, album=False)\n\n    def test_query_album(self):\n        item = self.add_item()\n        self.add_album([item])\n        self.check_do_query(1, 1, album=True)\n        self.check_do_query(0, 1, album=True, also_items=False)\n\n        item = self.add_item()\n        item2 = self.add_item()\n        self.add_album([item, item2])\n        self.check_do_query(3, 2, album=True)\n        self.check_do_query(0, 2, album=True, also_items=False)\n"
  },
  {
    "path": "test/ui/commands/test_write.py",
    "content": "from beets.test.helper import BeetsTestCase, IOMixin\n\n\nclass WriteTest(IOMixin, BeetsTestCase):\n    def write_cmd(self, *args):\n        return self.run_with_output(\"write\", *args)\n\n    def test_update_mtime(self):\n        item = self.add_item_fixture()\n        item[\"title\"] = \"a new title\"\n        item.store()\n\n        item = self.lib.items().get()\n        assert item.mtime == 0\n\n        self.write_cmd()\n        item = self.lib.items().get()\n        assert item.mtime == item.current_mtime()\n\n    def test_non_metadata_field_unchanged(self):\n        \"\"\"Changing a non-\"tag\" field like `bitrate` and writing should\n        have no effect.\n        \"\"\"\n        # An item that starts out \"clean\".\n        item = self.add_item_fixture()\n        item.read()\n\n        # ... but with a mismatched bitrate.\n        item.bitrate = 123\n        item.store()\n\n        output = self.write_cmd()\n\n        assert output == \"\"\n\n    def test_write_metadata_field(self):\n        item = self.add_item_fixture()\n        item.read()\n        old_title = item.title\n\n        item.title = \"new title\"\n        item.store()\n\n        output = self.write_cmd()\n\n        assert f\"{old_title} -> new title\" in output\n"
  },
  {
    "path": "test/ui/test_ui.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests for the command-line interface.\"\"\"\n\nimport os\nimport platform\nimport sys\nimport unittest\nfrom pathlib import Path\nfrom unittest.mock import patch\n\nimport pytest\nfrom confuse import ConfigError\n\nfrom beets import config, plugins, ui\nfrom beets.test import _common\nfrom beets.test.helper import BeetsTestCase, IOMixin, PluginTestCase\nfrom beets.ui import commands\nfrom beets.util import syspath\n\n\nclass PrintTest(IOMixin, unittest.TestCase):\n    def test_print_without_locale(self):\n        lang = os.environ.get(\"LANG\")\n        if lang:\n            del os.environ[\"LANG\"]\n\n        try:\n            ui.print_(\"something\")\n        except TypeError:\n            self.fail(\"TypeError during print\")\n        finally:\n            if lang:\n                os.environ[\"LANG\"] = lang\n\n    def test_print_with_invalid_locale(self):\n        old_lang = os.environ.get(\"LANG\")\n        os.environ[\"LANG\"] = \"\"\n        old_ctype = os.environ.get(\"LC_CTYPE\")\n        os.environ[\"LC_CTYPE\"] = \"UTF-8\"\n\n        try:\n            ui.print_(\"something\")\n        except ValueError:\n            self.fail(\"ValueError during print\")\n        finally:\n            if old_lang:\n                os.environ[\"LANG\"] = old_lang\n            else:\n                del os.environ[\"LANG\"]\n            if old_ctype:\n                os.environ[\"LC_CTYPE\"] = old_ctype\n            else:\n                del os.environ[\"LC_CTYPE\"]\n\n\nclass ShowModelChangesTest(IOMixin, BeetsTestCase):\n    def test_uses_database_state_when_old_not_provided(self):\n        item = self.add_item_fixture(title=\"old title\")\n        old_label = format(item.get_fresh_from_db())\n\n        item.title = \"new title\"\n\n        assert ui.show_model_changes(item) is True\n        assert self.io.getoutput().splitlines() == [\n            old_label,\n            \"  title: old title -> new title\",\n        ]\n\n\n@_common.slow_test()\nclass TestPluginTestCase(PluginTestCase):\n    plugin = \"test\"\n\n    def setUp(self):\n        self.config[\"pluginpath\"] = [_common.PLUGINPATH]\n        super().setUp()\n\n\nclass ConfigTest(IOMixin, TestPluginTestCase):\n    def setUp(self):\n        super().setUp()\n\n        # Don't use the BEETSDIR from `helper`. Instead, we point the home\n        # directory there. Some tests will set `BEETSDIR` themselves.\n        del os.environ[\"BEETSDIR\"]\n\n        # Also set APPDATA, the Windows equivalent of setting $HOME.\n        appdata_dir = self.temp_dir_path / \"AppData\" / \"Roaming\"\n\n        self._orig_cwd = os.getcwd()\n        self.test_cmd = self._make_test_cmd()\n        commands.default_commands.append(self.test_cmd)\n\n        # Default user configuration\n        if platform.system() == \"Windows\":\n            self.user_config_dir = appdata_dir / \"beets\"\n        else:\n            self.user_config_dir = self.temp_dir_path / \".config\" / \"beets\"\n        self.user_config_dir.mkdir(parents=True, exist_ok=True)\n        self.user_config_path = self.user_config_dir / \"config.yaml\"\n\n        # Custom BEETSDIR\n        self.beetsdir = self.temp_dir_path / \"beetsdir\"\n        self.beetsdir.mkdir(parents=True, exist_ok=True)\n\n        self.env_config_path = str(self.beetsdir / \"config.yaml\")\n        self.cli_config_path = str(self.temp_dir_path / \"config.yaml\")\n        self.env_patcher = patch(\n            \"os.environ\",\n            {\"HOME\": str(self.temp_dir_path), \"APPDATA\": str(appdata_dir)},\n        )\n        self.env_patcher.start()\n\n        self._reset_config()\n\n    def tearDown(self):\n        self.env_patcher.stop()\n        commands.default_commands.pop()\n        os.chdir(syspath(self._orig_cwd))\n        super().tearDown()\n\n    def _make_test_cmd(self):\n        test_cmd = ui.Subcommand(\"test\", help=\"test\")\n\n        def run(lib, options, args):\n            test_cmd.lib = lib\n            test_cmd.options = options\n            test_cmd.args = args\n\n        test_cmd.func = run\n        return test_cmd\n\n    def _reset_config(self):\n        # Config should read files again on demand\n        config.clear()\n        config._materialized = False\n\n    def write_config_file(self):\n        return open(self.user_config_path, \"w\")\n\n    def test_paths_section_respected(self):\n        with self.write_config_file() as config:\n            config.write(\"paths: {x: y}\")\n\n        self.run_command(\"test\", lib=None)\n        key, template = self.test_cmd.lib.path_formats[0]\n        assert key == \"x\"\n        assert template.original == \"y\"\n\n    def test_default_paths_preserved(self):\n        default_formats = ui.get_path_formats()\n\n        self._reset_config()\n        with self.write_config_file() as config:\n            config.write(\"paths: {x: y}\")\n        self.run_command(\"test\", lib=None)\n        key, template = self.test_cmd.lib.path_formats[0]\n        assert key == \"x\"\n        assert template.original == \"y\"\n        assert self.test_cmd.lib.path_formats[1:] == default_formats\n\n    def test_nonexistant_db(self):\n        with self.write_config_file() as config:\n            config.write(\"library: /xxx/yyy/not/a/real/path\")\n\n        self.io.addinput(\"n\")\n        with pytest.raises(ui.UserError):\n            self.run_command(\"test\", lib=None)\n\n    def test_user_config_file(self):\n        with self.write_config_file() as file:\n            file.write(\"anoption: value\")\n\n        self.run_command(\"test\", lib=None)\n        assert config[\"anoption\"].get() == \"value\"\n\n    def test_replacements_parsed(self):\n        with self.write_config_file() as config:\n            config.write(\"replace: {'[xy]': z}\")\n\n        self.run_command(\"test\", lib=None)\n        replacements = self.test_cmd.lib.replacements\n        repls = [(p.pattern, s) for p, s in replacements]  # Compare patterns.\n        assert repls == [(\"[xy]\", \"z\")]\n\n    def test_multiple_replacements_parsed(self):\n        with self.write_config_file() as config:\n            config.write(\"replace: {'[xy]': z, foo: bar}\")\n        self.run_command(\"test\", lib=None)\n        replacements = self.test_cmd.lib.replacements\n        repls = [(p.pattern, s) for p, s in replacements]\n        assert repls == [(\"[xy]\", \"z\"), (\"foo\", \"bar\")]\n\n    def test_cli_config_option(self):\n        with open(self.cli_config_path, \"w\") as file:\n            file.write(\"anoption: value\")\n        self.run_command(\"--config\", self.cli_config_path, \"test\", lib=None)\n        assert config[\"anoption\"].get() == \"value\"\n\n    def test_cli_config_file_overwrites_user_defaults(self):\n        with open(self.user_config_path, \"w\") as file:\n            file.write(\"anoption: value\")\n\n        with open(self.cli_config_path, \"w\") as file:\n            file.write(\"anoption: cli overwrite\")\n        self.run_command(\"--config\", self.cli_config_path, \"test\", lib=None)\n        assert config[\"anoption\"].get() == \"cli overwrite\"\n\n    def test_cli_config_file_overwrites_beetsdir_defaults(self):\n        os.environ[\"BEETSDIR\"] = str(self.beetsdir)\n        with open(self.env_config_path, \"w\") as file:\n            file.write(\"anoption: value\")\n\n        with open(self.cli_config_path, \"w\") as file:\n            file.write(\"anoption: cli overwrite\")\n        self.run_command(\"--config\", self.cli_config_path, \"test\", lib=None)\n        assert config[\"anoption\"].get() == \"cli overwrite\"\n\n    #    @unittest.skip('Difficult to implement with optparse')\n    #    def test_multiple_cli_config_files(self):\n    #        cli_config_path_1 = os.path.join(self.temp_dir, b'config.yaml')\n    #        cli_config_path_2 = os.path.join(self.temp_dir, b'config_2.yaml')\n    #\n    #        with open(cli_config_path_1, 'w') as file:\n    #            file.write('first: value')\n    #\n    #        with open(cli_config_path_2, 'w') as file:\n    #            file.write('second: value')\n    #\n    #        self.run_command('--config', cli_config_path_1,\n    #                      '--config', cli_config_path_2, 'test', lib=None)\n    #        assert config['first'].get() == 'value'\n    #        assert config['second'].get() == 'value'\n    #\n    #    @unittest.skip('Difficult to implement with optparse')\n    #    def test_multiple_cli_config_overwrite(self):\n    #        cli_overwrite_config_path = os.path.join(self.temp_dir,\n    #                                                 b'overwrite_config.yaml')\n    #\n    #        with open(self.cli_config_path, 'w') as file:\n    #            file.write('anoption: value')\n    #\n    #        with open(cli_overwrite_config_path, 'w') as file:\n    #            file.write('anoption: overwrite')\n    #\n    #        self.run_command('--config', self.cli_config_path,\n    #                      '--config', cli_overwrite_config_path, 'test')\n    #        assert config['anoption'].get() == 'cli overwrite'\n\n    # FIXME: fails on windows\n    @unittest.skipIf(sys.platform == \"win32\", \"win32\")\n    def test_cli_config_paths_resolve_relative_to_user_dir(self):\n        with open(self.cli_config_path, \"w\") as file:\n            file.write(\"library: beets.db\\n\")\n            file.write(\"statefile: state\")\n\n        self.run_command(\"--config\", self.cli_config_path, \"test\", lib=None)\n        assert config[\"library\"].as_path() == self.user_config_dir / \"beets.db\"\n        assert config[\"statefile\"].as_path() == self.user_config_dir / \"state\"\n\n    def test_cli_config_paths_resolve_relative_to_beetsdir(self):\n        os.environ[\"BEETSDIR\"] = str(self.beetsdir)\n\n        with open(self.cli_config_path, \"w\") as file:\n            file.write(\"library: beets.db\\n\")\n            file.write(\"statefile: state\")\n\n        self.run_command(\"--config\", self.cli_config_path, \"test\", lib=None)\n        assert config[\"library\"].as_path() == self.beetsdir / \"beets.db\"\n        assert config[\"statefile\"].as_path() == self.beetsdir / \"state\"\n\n    def test_command_line_option_relative_to_working_dir(self):\n        config.read()\n        os.chdir(syspath(self.temp_dir))\n        self.run_command(\"--library\", \"foo.db\", \"test\", lib=None)\n        assert config[\"library\"].as_path() == Path.cwd() / \"foo.db\"\n\n    def test_cli_config_file_loads_plugin_commands(self):\n        with open(self.cli_config_path, \"w\") as file:\n            file.write(f\"pluginpath: {_common.PLUGINPATH}\\n\")\n            file.write(\"plugins: test\")\n\n        self.run_command(\"--config\", self.cli_config_path, \"plugin\", lib=None)\n        plugs = plugins.find_plugins()\n        assert len(plugs) == 1\n        assert plugs[0].is_test_plugin\n        self.unload_plugins()\n\n    def test_beetsdir_config(self):\n        os.environ[\"BEETSDIR\"] = str(self.beetsdir)\n\n        with open(self.env_config_path, \"w\") as file:\n            file.write(\"anoption: overwrite\")\n\n        config.read()\n        assert config[\"anoption\"].get() == \"overwrite\"\n\n    def test_beetsdir_points_to_file_error(self):\n        beetsdir = str(self.temp_dir_path / \"beetsfile\")\n        open(beetsdir, \"a\").close()\n        os.environ[\"BEETSDIR\"] = beetsdir\n        with pytest.raises(ConfigError):\n            self.run_command(\"test\")\n\n    def test_beetsdir_config_does_not_load_default_user_config(self):\n        os.environ[\"BEETSDIR\"] = str(self.beetsdir)\n\n        with open(self.user_config_path, \"w\") as file:\n            file.write(\"anoption: value\")\n\n        config.read()\n        assert not config[\"anoption\"].exists()\n\n    def test_default_config_paths_resolve_relative_to_beetsdir(self):\n        os.environ[\"BEETSDIR\"] = str(self.beetsdir)\n\n        config.read()\n        assert config[\"library\"].as_path() == self.beetsdir / \"library.db\"\n        assert config[\"statefile\"].as_path() == self.beetsdir / \"state.pickle\"\n\n    def test_beetsdir_config_paths_resolve_relative_to_beetsdir(self):\n        os.environ[\"BEETSDIR\"] = str(self.beetsdir)\n\n        with open(self.env_config_path, \"w\") as file:\n            file.write(\"library: beets.db\\n\")\n            file.write(\"statefile: state\")\n\n        config.read()\n        assert config[\"library\"].as_path() == self.beetsdir / \"beets.db\"\n        assert config[\"statefile\"].as_path() == self.beetsdir / \"state\"\n\n\nclass PathFormatTest(unittest.TestCase):\n    def test_custom_paths_prepend(self):\n        default_formats = ui.get_path_formats()\n\n        config[\"paths\"] = {\"foo\": \"bar\"}\n        pf = ui.get_path_formats()\n        key, tmpl = pf[0]\n        assert key == \"foo\"\n        assert tmpl.original == \"bar\"\n        assert pf[1:] == default_formats\n\n\n@_common.slow_test()\nclass PluginTest(TestPluginTestCase):\n    def test_plugin_command_from_pluginpath(self):\n        self.run_command(\"test\", lib=None)\n\n\nclass CommonOptionsParserCliTest(IOMixin, BeetsTestCase):\n    \"\"\"Test CommonOptionsParser and formatting LibModel formatting on 'list'\n    command.\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.item = _common.item()\n        self.item.path = b\"xxx/yyy\"\n        self.lib.add(self.item)\n        self.lib.add_album([self.item])\n\n    def test_base(self):\n        output = self.run_with_output(\"ls\")\n        assert output == \"the artist - the album - the title\\n\"\n\n        output = self.run_with_output(\"ls\", \"-a\")\n        assert output == \"the album artist - the album\\n\"\n\n    def test_path_option(self):\n        output = self.run_with_output(\"ls\", \"-p\")\n        assert output == \"xxx/yyy\\n\"\n\n        output = self.run_with_output(\"ls\", \"-a\", \"-p\")\n        assert output == \"xxx\\n\"\n\n    def test_format_option(self):\n        output = self.run_with_output(\"ls\", \"-f\", \"$artist\")\n        assert output == \"the artist\\n\"\n\n        output = self.run_with_output(\"ls\", \"-a\", \"-f\", \"$albumartist\")\n        assert output == \"the album artist\\n\"\n\n    def test_format_option_unicode(self):\n        output = self.run_with_output(\"ls\", \"-f\", \"caf\\xe9\")\n        assert output == \"caf\\xe9\\n\"\n\n    def test_root_format_option(self):\n        output = self.run_with_output(\n            \"--format-item\", \"$artist\", \"--format-album\", \"foo\", \"ls\"\n        )\n        assert output == \"the artist\\n\"\n\n        output = self.run_with_output(\n            \"--format-item\", \"foo\", \"--format-album\", \"$albumartist\", \"ls\", \"-a\"\n        )\n        assert output == \"the album artist\\n\"\n\n    def test_help(self):\n        output = self.run_with_output(\"help\")\n        assert \"Usage:\" in output\n\n        output = self.run_with_output(\"help\", \"list\")\n        assert \"Usage:\" in output\n\n        with pytest.raises(ui.UserError):\n            self.run_command(\"help\", \"this.is.not.a.real.command\")\n\n    def test_stats(self):\n        output = self.run_with_output(\"stats\")\n        assert \"Approximate total size:\" in output\n\n        # # Need to have more realistic library setup for this to work\n        # output = self.run_with_output('stats', '-e')\n        # assert 'Total size:' in output\n\n    def test_version(self):\n        output = self.run_with_output(\"version\")\n        assert \"Python version\" in output\n        assert \"no plugins loaded\" in output\n\n        # # Need to have plugin loaded\n        # output = self.run_with_output('version')\n        # assert 'plugins: ' in output\n\n\nclass CommonOptionsParserTest(unittest.TestCase):\n    def test_album_option(self):\n        parser = ui.CommonOptionsParser()\n        assert not parser._album_flags\n        parser.add_album_option()\n        assert bool(parser._album_flags)\n\n        assert parser.parse_args([]) == ({\"album\": None}, [])\n        assert parser.parse_args([\"-a\"]) == ({\"album\": True}, [])\n        assert parser.parse_args([\"--album\"]) == ({\"album\": True}, [])\n\n    def test_path_option(self):\n        parser = ui.CommonOptionsParser()\n        parser.add_path_option()\n        assert not parser._album_flags\n\n        config[\"format_item\"].set(\"$foo\")\n        assert parser.parse_args([]) == ({\"path\": None}, [])\n        assert config[\"format_item\"].as_str() == \"$foo\"\n\n        assert parser.parse_args([\"-p\"]) == (\n            {\"path\": True, \"format\": \"$path\"},\n            [],\n        )\n        assert parser.parse_args([\"--path\"]) == (\n            {\"path\": True, \"format\": \"$path\"},\n            [],\n        )\n\n        assert config[\"format_item\"].as_str() == \"$path\"\n        assert config[\"format_album\"].as_str() == \"$path\"\n\n    def test_format_option(self):\n        parser = ui.CommonOptionsParser()\n        parser.add_format_option()\n        assert not parser._album_flags\n\n        config[\"format_item\"].set(\"$foo\")\n        assert parser.parse_args([]) == ({\"format\": None}, [])\n        assert config[\"format_item\"].as_str() == \"$foo\"\n\n        assert parser.parse_args([\"-f\", \"$bar\"]) == ({\"format\": \"$bar\"}, [])\n        assert parser.parse_args([\"--format\", \"$baz\"]) == (\n            {\"format\": \"$baz\"},\n            [],\n        )\n\n        assert config[\"format_item\"].as_str() == \"$baz\"\n        assert config[\"format_album\"].as_str() == \"$baz\"\n\n    def test_format_option_with_target(self):\n        with pytest.raises(KeyError):\n            ui.CommonOptionsParser().add_format_option(target=\"thingy\")\n\n        parser = ui.CommonOptionsParser()\n        parser.add_format_option(target=\"item\")\n\n        config[\"format_item\"].set(\"$item\")\n        config[\"format_album\"].set(\"$album\")\n\n        assert parser.parse_args([\"-f\", \"$bar\"]) == ({\"format\": \"$bar\"}, [])\n\n        assert config[\"format_item\"].as_str() == \"$bar\"\n        assert config[\"format_album\"].as_str() == \"$album\"\n\n    def test_format_option_with_album(self):\n        parser = ui.CommonOptionsParser()\n        parser.add_album_option()\n        parser.add_format_option()\n\n        config[\"format_item\"].set(\"$item\")\n        config[\"format_album\"].set(\"$album\")\n\n        parser.parse_args([\"-f\", \"$bar\"])\n        assert config[\"format_item\"].as_str() == \"$bar\"\n        assert config[\"format_album\"].as_str() == \"$album\"\n\n        parser.parse_args([\"-a\", \"-f\", \"$foo\"])\n        assert config[\"format_item\"].as_str() == \"$bar\"\n        assert config[\"format_album\"].as_str() == \"$foo\"\n\n        parser.parse_args([\"-f\", \"$foo2\", \"-a\"])\n        assert config[\"format_album\"].as_str() == \"$foo2\"\n\n    def test_add_all_common_options(self):\n        parser = ui.CommonOptionsParser()\n        parser.add_all_common_options()\n        assert parser.parse_args([]) == (\n            {\"album\": None, \"path\": None, \"format\": None},\n            [],\n        )\n\n\nclass EncodingTest(unittest.TestCase):\n    \"\"\"Tests for the `terminal_encoding` config option and our\n    `_in_encoding` and `_out_encoding` utility functions.\n    \"\"\"\n\n    def out_encoding_overridden(self):\n        config[\"terminal_encoding\"] = \"fake_encoding\"\n        assert ui._out_encoding() == \"fake_encoding\"\n\n    def in_encoding_overridden(self):\n        config[\"terminal_encoding\"] = \"fake_encoding\"\n        assert ui._in_encoding() == \"fake_encoding\"\n\n    def out_encoding_default_utf8(self):\n        with patch(\"sys.stdout\") as stdout:\n            stdout.encoding = None\n            assert ui._out_encoding() == \"utf-8\"\n\n    def in_encoding_default_utf8(self):\n        with patch(\"sys.stdin\") as stdin:\n            stdin.encoding = None\n            assert ui._in_encoding() == \"utf-8\"\n"
  },
  {
    "path": "test/ui/test_ui_importer.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Tests the TerminalImportSession. The tests are the same as in the\n\ntest_importer module. But here the test importer inherits from\n``TerminalImportSession``. So we test this class, too.\n\"\"\"\n\nfrom beets.test.helper import TerminalImportMixin\nfrom test import test_importer\n\n\nclass NonAutotaggedImportTest(\n    TerminalImportMixin, test_importer.NonAutotaggedImportTest\n):\n    pass\n\n\nclass ImportTest(TerminalImportMixin, test_importer.ImportTest):\n    pass\n\n\nclass ImportSingletonTest(\n    TerminalImportMixin, test_importer.ImportSingletonTest\n):\n    pass\n\n\nclass ImportTracksTest(TerminalImportMixin, test_importer.ImportTracksTest):\n    pass\n\n\nclass ImportCompilationTest(\n    TerminalImportMixin, test_importer.ImportCompilationTest\n):\n    pass\n\n\nclass ImportExistingTest(TerminalImportMixin, test_importer.ImportExistingTest):\n    pass\n\n\nclass ChooseCandidateTest(\n    TerminalImportMixin, test_importer.ChooseCandidateTest\n):\n    pass\n\n\nclass GroupAlbumsImportTest(\n    TerminalImportMixin, test_importer.GroupAlbumsImportTest\n):\n    pass\n\n\nclass GlobalGroupAlbumsImportTest(\n    TerminalImportMixin, test_importer.GlobalGroupAlbumsImportTest\n):\n    pass\n"
  },
  {
    "path": "test/ui/test_ui_init.py",
    "content": "# This file is part of beets.\n# Copyright 2016, Adrian Sampson.\n#\n# Permission is hereby granted, free of charge, to any person obtaining\n# a copy of this software and associated documentation files (the\n# \"Software\"), to deal in the Software without restriction, including\n# without limitation the rights to use, copy, modify, merge, publish,\n# distribute, sublicense, and/or sell copies of the Software, and to\n# permit persons to whom the Software is furnished to do so, subject to\n# the following conditions:\n#\n# The above copyright notice and this permission notice shall be\n# included in all copies or substantial portions of the Software.\n\n\"\"\"Test module for file ui/__init__.py\"\"\"\n\nimport os\nimport shutil\nimport unittest\nfrom copy import deepcopy\nfrom random import random\n\nfrom beets import config, ui\nfrom beets.test import _common\nfrom beets.test.helper import BeetsTestCase, IOMixin\n\n\nclass InputMethodsTest(IOMixin, unittest.TestCase):\n    def _print_helper(self, s):\n        print(s)\n\n    def _print_helper2(self, s, prefix):\n        print(prefix, s)\n\n    def test_input_select_objects(self):\n        full_items = [\"1\", \"2\", \"3\", \"4\", \"5\"]\n\n        # Test no\n        self.io.addinput(\"n\")\n        items = ui.input_select_objects(\n            \"Prompt\", full_items, self._print_helper\n        )\n        assert items == []\n\n        # Test yes\n        self.io.addinput(\"y\")\n        items = ui.input_select_objects(\n            \"Prompt\", full_items, self._print_helper\n        )\n        assert items == full_items\n\n        # Test selective 1\n        self.io.addinput(\"s\")\n        self.io.addinput(\"n\")\n        self.io.addinput(\"y\")\n        self.io.addinput(\"n\")\n        self.io.addinput(\"y\")\n        self.io.addinput(\"n\")\n        items = ui.input_select_objects(\n            \"Prompt\", full_items, self._print_helper\n        )\n        assert items == [\"2\", \"4\"]\n\n        # Test selective 2\n        self.io.addinput(\"s\")\n        self.io.addinput(\"y\")\n        self.io.addinput(\"y\")\n        self.io.addinput(\"n\")\n        self.io.addinput(\"y\")\n        self.io.addinput(\"n\")\n        items = ui.input_select_objects(\n            \"Prompt\", full_items, lambda s: self._print_helper2(s, \"Prefix\")\n        )\n        assert items == [\"1\", \"2\", \"4\"]\n\n        # Test selective 3\n        self.io.addinput(\"s\")\n        self.io.addinput(\"y\")\n        self.io.addinput(\"n\")\n        self.io.addinput(\"y\")\n        self.io.addinput(\"q\")\n        items = ui.input_select_objects(\n            \"Prompt\", full_items, self._print_helper\n        )\n        assert items == [\"1\", \"3\"]\n\n\nclass ParentalDirCreation(IOMixin, BeetsTestCase):\n    def test_create_yes(self):\n        non_exist_path = _common.os.fsdecode(\n            os.path.join(self.temp_dir, b\"nonexist\", str(random()).encode())\n        )\n        # Deepcopy instead of recovering because exceptions might\n        # occur; wish I can use a golang defer here.\n        test_config = deepcopy(config)\n        test_config[\"library\"] = non_exist_path\n        self.io.addinput(\"y\")\n        lib = ui._open_library(test_config)\n        lib._close()\n\n    def test_create_no(self):\n        non_exist_path_parent = _common.os.fsdecode(\n            os.path.join(self.temp_dir, b\"nonexist\")\n        )\n        non_exist_path = _common.os.fsdecode(\n            os.path.join(non_exist_path_parent.encode(), str(random()).encode())\n        )\n        test_config = deepcopy(config)\n        test_config[\"library\"] = non_exist_path\n\n        self.io.addinput(\"n\")\n        try:\n            lib = ui._open_library(test_config)\n        except ui.UserError:\n            if os.path.exists(non_exist_path_parent):\n                shutil.rmtree(non_exist_path_parent)\n                raise OSError(\"Parent directories should not be created.\")\n        else:\n            if lib:\n                lib._close()\n            raise OSError(\"Parent directories should not be created.\")\n"
  },
  {
    "path": "test/util/test_color.py",
    "content": "from unittest import TestCase\n\nfrom beets.util.color import color_split, uncolorize\n\n\nclass ColorTestCase(TestCase):\n    def test_uncolorize(self):\n        assert \"test\" == uncolorize(\"test\")\n        txt = uncolorize(\"\\x1b[31mtest\\x1b[39;49;00m\")\n        assert \"test\" == txt\n        txt = uncolorize(\"\\x1b[31mtest\\x1b[39;49;00m test\")\n        assert \"test test\" == txt\n        txt = uncolorize(\"\\x1b[31mtest\\x1b[39;49;00mtest\")\n        assert \"testtest\" == txt\n        txt = uncolorize(\"test \\x1b[31mtest\\x1b[39;49;00m test\")\n        assert \"test test test\" == txt\n\n    def test_color_split(self):\n        exp = (\"test\", \"\")\n        res = color_split(\"test\", 5)\n        assert exp == res\n        exp = (\"\\x1b[31mtes\\x1b[39;49;00m\", \"\\x1b[31mt\\x1b[39;49;00m\")\n        res = color_split(\"\\x1b[31mtest\\x1b[39;49;00m\", 3)\n        assert exp == res\n"
  },
  {
    "path": "test/util/test_config.py",
    "content": "import pytest\n\nfrom beets.util.config import sanitize_choices, sanitize_pairs\n\n\n@pytest.mark.parametrize(\n    \"input_choices, valid_choices, expected\",\n    [\n        ([\"A\", \"Z\"], (\"A\", \"B\"), [\"A\"]),\n        ([\"A\", \"A\"], (\"A\"), [\"A\"]),\n        ([\"D\", \"*\", \"A\"], (\"A\", \"B\", \"C\", \"D\"), [\"D\", \"B\", \"C\", \"A\"]),\n    ],\n)\ndef test_sanitize_choices(input_choices, valid_choices, expected):\n    assert sanitize_choices(input_choices, valid_choices) == expected\n\n\ndef test_sanitize_pairs():\n    assert sanitize_pairs(\n        [\n            (\"foo\", \"baz bar\"),\n            (\"foo\", \"baz bar\"),\n            (\"key\", \"*\"),\n            (\"*\", \"*\"),\n            (\"discard\", \"bye\"),\n        ],\n        [\n            (\"foo\", \"bar\"),\n            (\"foo\", \"baz\"),\n            (\"foo\", \"foobar\"),\n            (\"key\", \"value\"),\n        ],\n    ) == [\n        (\"foo\", \"baz\"),\n        (\"foo\", \"bar\"),\n        (\"key\", \"value\"),\n        (\"foo\", \"foobar\"),\n    ]\n"
  },
  {
    "path": "test/util/test_diff.py",
    "content": "from textwrap import dedent\n\nimport pytest\n\nfrom beets.library import Item\nfrom beets.util.diff import _field_diff\n\np = pytest.param\n\n\nclass TestFieldDiff:\n    @pytest.fixture(autouse=True)\n    def configure_color(self, config, color):\n        config[\"ui\"][\"color\"] = color\n\n    @pytest.fixture(autouse=True)\n    def patch_colorize(self, monkeypatch):\n        \"\"\"Patch to return a deterministic string format instead of ANSI codes.\"\"\"\n        monkeypatch.setattr(\n            \"beets.util.color._colorize\",\n            lambda color_name, text: f\"[{color_name}]{text}[/]\",\n        )\n\n    @staticmethod\n    def diff_fmt(old, new):\n        return f\"[text_diff_removed]{old}[/] -> [text_diff_added]{new}[/]\"\n\n    @pytest.mark.parametrize(\n        \"old_data, new_data, field, expected_diff\",\n        [\n            p({\"title\": \"foo\"}, {\"title\": \"foo\"}, \"title\", None, id=\"no_change\"),\n            p({\"bpm\": 120.0}, {\"bpm\": 120.005}, \"bpm\", None, id=\"float_close_enough\"),\n            p({\"bpm\": 120.0}, {\"bpm\": 121.0}, \"bpm\", f\"bpm: {diff_fmt('120', '121')}\", id=\"float_changed\"),\n            p({\"title\": \"foo\"}, {\"title\": \"bar\"}, \"title\", f\"title: {diff_fmt('foo', 'bar')}\", id=\"string_full_replace\"),\n            p({\"title\": \"prefix foo\"}, {\"title\": \"prefix bar\"}, \"title\", \"title: prefix [text_diff_removed]foo[/] -> prefix [text_diff_added]bar[/]\", id=\"string_partial_change\"),\n            p({\"year\": 2000}, {\"year\": 2001}, \"year\", f\"year: {diff_fmt('2000', '2001')}\", id=\"int_changed\"),\n            p({}, {\"artist\": \"Artist\"}, \"artist\", \"artist:  -> [text_diff_added]Artist[/]\", id=\"field_added\"),\n            p({\"artist\": \"Artist\"}, {}, \"artist\", \"artist: [text_diff_removed]Artist[/] -> \", id=\"field_removed\"),\n            p({\"track\": 1}, {\"track\": 2}, \"track\", f\"track: {diff_fmt('01', '02')}\", id=\"formatted_value_changed\"),\n            p({\"mb_trackid\": None}, {\"mb_trackid\": \"1234\"}, \"mb_trackid\", \"mb_trackid:  -> [text_diff_added]1234[/]\", id=\"none_to_value\"),\n            p({}, {\"new_flex\": \"foo\"}, \"new_flex\", \"[text_diff_added]new_flex: foo[/]\", id=\"flex_field_added\"),\n            p({\"old_flex\": \"foo\"}, {}, \"old_flex\", \"[text_diff_removed]old_flex: foo[/]\", id=\"flex_field_removed\"),\n            p({\"albumtypes\": [\"album\", \"ep\"]}, {\"albumtypes\": [\"ep\", \"album\"]}, \"albumtypes\", None, id=\"multi_value_unchanged\"),\n            p(\n                {\"albumtypes\": [\"ep\"]},\n                {\"albumtypes\": [\"album\", \"compilation\"]},\n                \"albumtypes\",\n                dedent(\"\"\"\n                    albumtypes:\n                    [text_diff_removed]  - ep[/]\n                    [text_diff_added]  + album[/]\n                    [text_diff_added]  + compilation[/]\n                \"\"\").strip(),\n                id=\"multi_value_changed\"\n            ),\n        ],\n    )  # fmt: skip\n    @pytest.mark.parametrize(\"color\", [True], ids=[\"color_enabled\"])\n    def test_field_diff_colors(self, old_data, new_data, field, expected_diff):\n        old_item = Item(**old_data)\n        new_item = Item(**new_data)\n\n        diff = _field_diff(field, old_item.formatted(), new_item.formatted())\n\n        assert diff == expected_diff\n\n    @pytest.mark.parametrize(\"color\", [False], ids=[\"color_disabled\"])\n    def test_field_diff_no_color(self):\n        old_item = Item(title=\"foo\")\n        new_item = Item(title=\"bar\")\n\n        diff = _field_diff(\"title\", old_item.formatted(), new_item.formatted())\n\n        assert diff == \"title: foo -> bar\"\n"
  },
  {
    "path": "test/util/test_id_extractors.py",
    "content": "from typing import NamedTuple\n\nimport pytest\n\nfrom beets.util.id_extractors import extract_release_id\n\n\n@pytest.mark.parametrize(\n    \"source, id_string, expected\",\n    [\n        (\"spotify\", \"39WqpoPgZxygo6YQjehLJJ\", \"39WqpoPgZxygo6YQjehLJJ\"),\n        (\"spotify\", \"blah blah\", None),\n        (\"spotify\", \"https://open.spotify.com/album/39WqpoPgZxygo6YQjehLJJ\", \"39WqpoPgZxygo6YQjehLJJ\"),\n        (\"deezer\", \"176356382\", \"176356382\"),\n        (\"deezer\", \"blah blah\", None),\n        (\"deezer\", \"https://www.deezer.com/album/176356382\", \"176356382\"),\n        (\"beatport\", \"3089651\", \"3089651\"),\n        (\"beatport\", \"blah blah\", None),\n        (\"beatport\", \"https://www.beatport.com/release/album-name/3089651\", \"3089651\"),\n        (\"discogs\", \"http://www.discogs.com/G%C3%BCnther-Lause-Meru-Ep/release/4354798\", \"4354798\"),\n        (\"discogs\", \"http://www.discogs.com/release/4354798-G%C3%BCnther-Lause-Meru-Ep\", \"4354798\"),\n        (\"discogs\", \"http://www.discogs.com/G%C3%BCnther-4354798Lause-Meru-Ep/release/4354798\", \"4354798\"),\n        (\"discogs\", \"http://www.discogs.com/release/4354798-G%C3%BCnther-4354798Lause-Meru-Ep/\", \"4354798\"),\n        (\"discogs\", \"[r4354798]\", \"4354798\"),\n        (\"discogs\", \"r4354798\", \"4354798\"),\n        (\"discogs\", \"4354798\", \"4354798\"),\n        (\"discogs\", \"yet-another-metadata-provider.org/foo/12345\", None),\n        (\"discogs\", \"005b84a0-ecd6-39f1-b2f6-6eb48756b268\", None),\n        (\"musicbrainz\", \"28e32c71-1450-463e-92bf-e0a46446fc11\", \"28e32c71-1450-463e-92bf-e0a46446fc11\"),\n        (\"musicbrainz\", \"blah blah\", None),\n        (\"musicbrainz\", \"https://musicbrainz.org/entity/28e32c71-1450-463e-92bf-e0a46446fc11\", \"28e32c71-1450-463e-92bf-e0a46446fc11\"),\n        (\"bandcamp\", \"https://nameofartist.bandcamp.com/album/nameofalbum\", \"https://nameofartist.bandcamp.com/album/nameofalbum\"),\n    ],\n)  # fmt: skip\ndef test_extract_release_id(source, id_string, expected):\n    assert extract_release_id(source, id_string) == expected\n\n\nclass SourceWithURL(NamedTuple):\n    source: str\n    url: str\n\n\nsource_with_urls = [\n    SourceWithURL(\"spotify\", \"https://open.spotify.com/album/39WqpoPgZxygo6YQjehLJJ\"),\n    SourceWithURL(\"deezer\", \"https://www.deezer.com/album/176356382\"),\n    SourceWithURL(\"beatport\", \"https://www.beatport.com/release/album-name/3089651\"),\n    SourceWithURL(\"discogs\", \"http://www.discogs.com/G%C3%BCnther-Lause-Meru-Ep/release/4354798\"),\n    SourceWithURL(\"musicbrainz\", \"https://musicbrainz.org/entity/28e32c71-1450-463e-92bf-e0a46446fc11\"),\n]  # fmt: skip\n\n\n@pytest.mark.parametrize(\"source\", [s.source for s in source_with_urls])\n@pytest.mark.parametrize(\"source_with_url\", source_with_urls)\ndef test_match_source_url(source, source_with_url):\n    if source == source_with_url.source:\n        assert extract_release_id(source, source_with_url.url)\n    else:\n        assert not extract_release_id(source, source_with_url.url), (\n            f\"Source {source} pattern should not match {source_with_url.source} URL\"\n        )\n"
  },
  {
    "path": "test/util/test_layout.py",
    "content": "from unittest import TestCase\n\nfrom beets.util.layout import split_into_lines\n\n\nclass LayoutTestCase(TestCase):\n    def test_split_into_lines(self):\n        # Test uncolored text\n        txt = split_into_lines(\"test test test\", 5, 5)\n        assert txt == [\"test\", \"test\", \"test\"]\n        # Test multiple colored texts\n        colored_text = \"\\x1b[31mtest \\x1b[39;49;00m\" * 3\n        split_txt = [\n            \"\\x1b[31mtest\\x1b[39;49;00m\",\n            \"\\x1b[31mtest\\x1b[39;49;00m\",\n            \"\\x1b[31mtest\\x1b[39;49;00m\",\n        ]\n        txt = split_into_lines(colored_text, 5, 5)\n        assert txt == split_txt\n        # Test single color, multi space text\n        colored_text = \"\\x1b[31m test test test \\x1b[39;49;00m\"\n        txt = split_into_lines(colored_text, 5, 5)\n        assert txt == split_txt\n        # Test single color, different spacing\n        colored_text = \"\\x1b[31mtest\\x1b[39;49;00mtest test test\"\n        # ToDo: fix color_len to handle mid-text color escapes, and thus\n        # split colored texts over newlines (potentially with dashes?)\n        split_txt = [\"\\x1b[31mtest\\x1b[39;49;00mt\", \"est\", \"test\", \"test\"]\n        txt = split_into_lines(colored_text, 5, 5)\n        assert txt == split_txt\n"
  },
  {
    "path": "test/util/test_lyrics.py",
    "content": "import textwrap\n\nfrom beets.util.lyrics import Lyrics\n\n\nclass TestLyrics:\n    def test_instrumental_lyrics(self):\n        lyrics = Lyrics(\n            \"[Instrumental]\", \"lrclib\", url=\"https://lrclib.net/api/1\"\n        )\n\n        assert lyrics.full_text == \"[Instrumental]\"\n        assert lyrics.backend == \"lrclib\"\n        assert lyrics.url == \"https://lrclib.net/api/1\"\n        assert lyrics.language is None\n        assert lyrics.translation_language is None\n\n    def test_from_legacy_text(self, is_importable):\n        text = textwrap.dedent(\"\"\"\n        [00:00.00] Some synced lyrics / Quelques paroles synchronisées\n        [00:00.50]\n        [00:01.00] Some more synced lyrics / Quelques paroles plus synchronisées\n\n        Source: https://lrclib.net/api/1/\"\"\")\n\n        lyrics = Lyrics.from_legacy_text(text)\n\n        assert lyrics.full_text == textwrap.dedent(\n            \"\"\"\n            [00:00.00] Some synced lyrics / Quelques paroles synchronisées\n            [00:00.50]\n            [00:01.00] Some more synced lyrics / Quelques paroles plus synchronisées\"\"\"\n        )\n        assert lyrics.backend == \"lrclib\"\n        assert lyrics.url == \"https://lrclib.net/api/1/\"\n        langdetect_available = is_importable(\"langdetect\")\n        assert lyrics.language == (\"EN\" if langdetect_available else None)\n        assert lyrics.translation_language == (\n            \"FR\" if langdetect_available else None\n        )\n"
  },
  {
    "path": "test/util/test_units.py",
    "content": "import pytest\n\nfrom beets.util.units import human_bytes, human_seconds\n\n\n@pytest.mark.parametrize(\n    \"input_bytes,expected\",\n    [\n        (0, \"0.0 B\"),\n        (30, \"30.0 B\"),\n        (pow(2, 10), \"1.0 KiB\"),\n        (pow(2, 20), \"1.0 MiB\"),\n        (pow(2, 30), \"1.0 GiB\"),\n        (pow(2, 40), \"1.0 TiB\"),\n        (pow(2, 50), \"1.0 PiB\"),\n        (pow(2, 60), \"1.0 EiB\"),\n        (pow(2, 70), \"1.0 ZiB\"),\n        (pow(2, 80), \"1.0 YiB\"),\n        (pow(2, 90), \"1.0 HiB\"),\n        (pow(2, 100), \"big\"),\n    ],\n)\ndef test_human_bytes(input_bytes, expected):\n    assert human_bytes(input_bytes) == expected\n\n\n@pytest.mark.parametrize(\n    \"input_seconds,expected\",\n    [\n        (0, \"0.0 seconds\"),\n        (30, \"30.0 seconds\"),\n        (60, \"1.0 minutes\"),\n        (90, \"1.5 minutes\"),\n        (125, \"2.1 minutes\"),\n        (3600, \"1.0 hours\"),\n        (86400, \"1.0 days\"),\n        (604800, \"1.0 weeks\"),\n        (31449600, \"1.0 years\"),\n        (314496000, \"1.0 decades\"),\n    ],\n)\ndef test_human_seconds(input_seconds, expected):\n    assert human_seconds(input_seconds) == expected\n"
  }
]