From 69dd4a9f2f282b34069c93b945c3a13944a61437 Mon Sep 17 00:00:00 2001 From: sigma67 Date: Tue, 2 Jan 2024 21:58:33 +0100 Subject: [PATCH] test restructuring via pytest --- .github/workflows/coverage.yml | 10 +- .gitignore | 1 + pdm.lock | 136 +++++++- pyproject.toml | 13 +- tests/README.rst | 5 +- tests/__init__.py | 15 + tests/auth/__init__.py | 0 tests/auth/test_browser.py | 17 + tests/auth/test_oauth.py | 101 ++++++ tests/conftest.py | 72 ++++ tests/mixins/test_browsing.py | 126 +++++++ tests/mixins/test_explore.py | 17 + tests/mixins/test_library.py | 105 ++++++ tests/mixins/test_playlists.py | 75 ++++ tests/mixins/test_search.py | 90 +++++ tests/mixins/test_uploads.py | 62 ++++ tests/mixins/test_watch.py | 16 + tests/test.py | 610 --------------------------------- tests/test_ytmusic.py | 6 + 19 files changed, 858 insertions(+), 619 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/auth/__init__.py create mode 100644 tests/auth/test_browser.py create mode 100644 tests/auth/test_oauth.py create mode 100644 tests/conftest.py create mode 100644 tests/mixins/test_browsing.py create mode 100644 tests/mixins/test_explore.py create mode 100644 tests/mixins/test_library.py create mode 100644 tests/mixins/test_playlists.py create mode 100644 tests/mixins/test_search.py create mode 100644 tests/mixins/test_uploads.py create mode 100644 tests/mixins/test_watch.py delete mode 100644 tests/test.py create mode 100644 tests/test_ytmusic.py diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index a12c5208..b11da693 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -17,24 +17,26 @@ jobs: uses: actions/setup-python@master with: python-version: 3.x + - name: Setup PDM + uses: pdm-project/setup-pdm@v3 - name: create-json uses: jsdaniell/create-json@v1.2.2 with: name: "oauth.json" dir: "tests/" json: ${{ secrets.OAUTH_JSON }} + - name: Install dependencies + run: pdm install - name: Generate coverage report env: HEADERS_AUTH: ${{ secrets.HEADERS_AUTH }} TEST_CFG: ${{ secrets.TEST_CFG }} run: | - pip install -e . - pip install coverage curl -o tests/test.mp3 https://www.kozco.com/tech/piano2-CoolEdit.mp3 cat <<< "$HEADERS_AUTH" > tests/browser.json cat <<< "$TEST_CFG" > tests/test.cfg - coverage run - coverage xml + pdm run pytest + pdm run coverage xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: diff --git a/.gitignore b/.gitignore index c89cb371..318a0ce8 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ venv*/ build dist .pdm-python +.venv diff --git a/pdm.lock b/pdm.lock index 0e1fff8e..b84be4f4 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "dev"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.4.1" -content_hash = "sha256:b26172c266ad5d5a9ad9604af3348c8f2577fd45938709849735bda1cdbd9f2d" +content_hash = "sha256:ab467aa4e09402f8249d103fc719d3dd2f9bab661ea269227b9358a8c223fa6e" [[package]] name = "alabaster" @@ -202,6 +202,72 @@ files = [ {file = "coverage-7.4.0.tar.gz", hash = "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e"}, ] +[[package]] +name = "coverage" +version = "7.4.0" +extras = ["toml"] +requires_python = ">=3.8" +summary = "Code coverage measurement for Python" +groups = ["dev"] +dependencies = [ + "coverage==7.4.0", + "tomli; python_full_version <= \"3.11.0a6\"", +] +files = [ + {file = "coverage-7.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a"}, + {file = "coverage-7.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43"}, + {file = "coverage-7.4.0-cp310-cp310-win32.whl", hash = "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451"}, + {file = "coverage-7.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137"}, + {file = "coverage-7.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca"}, + {file = "coverage-7.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26"}, + {file = "coverage-7.4.0-cp311-cp311-win32.whl", hash = "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614"}, + {file = "coverage-7.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590"}, + {file = "coverage-7.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143"}, + {file = "coverage-7.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa"}, + {file = "coverage-7.4.0-cp312-cp312-win32.whl", hash = "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450"}, + {file = "coverage-7.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0"}, + {file = "coverage-7.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e"}, + {file = "coverage-7.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105"}, + {file = "coverage-7.4.0-cp38-cp38-win32.whl", hash = "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2"}, + {file = "coverage-7.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555"}, + {file = "coverage-7.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42"}, + {file = "coverage-7.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f"}, + {file = "coverage-7.4.0-cp39-cp39-win32.whl", hash = "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932"}, + {file = "coverage-7.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e"}, + {file = "coverage-7.4.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6"}, + {file = "coverage-7.4.0.tar.gz", hash = "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e"}, +] + [[package]] name = "docutils" version = "0.19" @@ -213,6 +279,18 @@ files = [ {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, ] +[[package]] +name = "exceptiongroup" +version = "1.2.0" +requires_python = ">=3.7" +summary = "Backport of PEP 654 (exception groups)" +groups = ["dev"] +marker = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + [[package]] name = "idna" version = "3.6" @@ -250,6 +328,17 @@ files = [ {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"}, ] +[[package]] +name = "iniconfig" +version = "2.0.0" +requires_python = ">=3.7" +summary = "brain-dead simple config-ini parsing" +groups = ["dev"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "jinja2" version = "3.1.2" @@ -387,6 +476,17 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "pluggy" +version = "1.3.0" +requires_python = ">=3.8" +summary = "plugin and hook calling mechanisms for python" +groups = ["dev"] +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + [[package]] name = "pygments" version = "2.17.2" @@ -398,6 +498,40 @@ files = [ {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, ] +[[package]] +name = "pytest" +version = "7.4.4" +requires_python = ">=3.7" +summary = "pytest: simple powerful testing with Python" +groups = ["dev"] +dependencies = [ + "colorama; sys_platform == \"win32\"", + "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", + "iniconfig", + "packaging", + "pluggy<2.0,>=0.12", + "tomli>=1.0.0; python_version < \"3.11\"", +] +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +requires_python = ">=3.7" +summary = "Pytest plugin for measuring coverage." +groups = ["dev"] +dependencies = [ + "coverage[toml]>=5.2.1", + "pytest>=4.6", +] +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + [[package]] name = "pytz" version = "2023.3.post1" diff --git a/pyproject.toml b/pyproject.toml index e6b5f706..db28899a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,8 +36,17 @@ include-package-data=false [tool.setuptools.package-data] "*" = ["**.rst", "**.py", "**.mo"] +############### +# DEVELOPMENT # +############### + +[tool.pytest.ini_options] +python_functions = "test_*" +testpaths = ["tests"] +addopts = "--verbose --cov" + [tool.coverage.run] -command_line = "-m unittest discover tests" +source = ["ytmusicapi"] [tool.ruff] line-length = 110 @@ -59,4 +68,6 @@ dev = [ 'sphinx-rtd-theme', "ruff>=0.1.9", "mypy>=1.8.0", + "pytest>=7.4.4", + "pytest-cov>=4.1.0", ] diff --git a/tests/README.rst b/tests/README.rst index ff54d83f..3cacf5ce 100644 --- a/tests/README.rst +++ b/tests/README.rst @@ -30,8 +30,7 @@ Make sure you installed the dev requirements as explained in `CONTRIBUTING.rst < .. code-block:: bash - cd tests - coverage run -m unittest test.py + pdm run pytest -to generate a coverage report. \ No newline at end of file +to generate a coverage report. diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..4f3ce510 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,15 @@ +from importlib.metadata import PackageNotFoundError, version + +from ytmusicapi.setup import setup, setup_oauth +from ytmusicapi.ytmusic import YTMusic + +try: + __version__ = version("ytmusicapi") +except PackageNotFoundError: + # package is not installed + pass + +__copyright__ = "Copyright 2023 sigma67" +__license__ = "MIT" +__title__ = "ytmusicapi" +__all__ = ["YTMusic", "setup_oauth", "setup"] diff --git a/tests/auth/__init__.py b/tests/auth/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/auth/test_browser.py b/tests/auth/test_browser.py new file mode 100644 index 00000000..c77c8715 --- /dev/null +++ b/tests/auth/test_browser.py @@ -0,0 +1,17 @@ +from unittest import mock + +import ytmusicapi.setup +from ytmusicapi.setup import main + + +class TestBrowser: + def test_setup_browser(self, config, browser_filepath: str): + headers = ytmusicapi.setup(browser_filepath, config["auth"]["headers_raw"]) + assert len(headers) >= 2 + headers_raw = config["auth"]["headers_raw"].split("\n") + with ( + mock.patch("sys.argv", ["ytmusicapi", "browser", "--file", browser_filepath]), + mock.patch("builtins.input", side_effect=(headers_raw + [EOFError()])), + ): + headers = main() + assert len(headers) >= 2 diff --git a/tests/auth/test_oauth.py b/tests/auth/test_oauth.py new file mode 100644 index 00000000..d2309283 --- /dev/null +++ b/tests/auth/test_oauth.py @@ -0,0 +1,101 @@ +import json +import time +from pathlib import Path +from typing import Any, Dict +from unittest import mock + +import pytest +from requests import Response + +from ytmusicapi.auth.types import AuthType +from ytmusicapi.constants import OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET +from ytmusicapi.setup import main +from ytmusicapi.ytmusic import OAuthCredentials, YTMusic + + +@pytest.fixture(name="blank_code") +def fixture_blank_code() -> Dict[str, Any]: + return { + "device_code": "", + "user_code": "", + "expires_in": 1800, + "interval": 5, + "verification_url": "https://www.google.com/device", + } + + +@pytest.fixture(name="alt_oauth_credentials") +def fixture_alt_oauth_credentials() -> OAuthCredentials: + return OAuthCredentials(OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET) + + +@pytest.fixture(name="yt_alt_oauth") +def fixture_yt_alt_oauth(browser_filepath: str, alt_oauth_credentials: OAuthCredentials) -> YTMusic: + return YTMusic(browser_filepath, oauth_credentials=alt_oauth_credentials) + + +class TestOAuth: + @mock.patch("requests.Response.json") + @mock.patch("requests.Session.post") + def test_setup_oauth(self, session_mock, json_mock, oauth_filepath, blank_code, yt_oauth): + session_mock.return_value = Response() + fresh_token = yt_oauth._token.as_dict() + json_mock.side_effect = [blank_code, fresh_token] + with mock.patch("builtins.input", return_value="y"), mock.patch( + "sys.argv", ["ytmusicapi", "oauth", "--file", oauth_filepath] + ): + main() + assert Path(oauth_filepath).exists() + + json_mock.side_effect = None + with open(oauth_filepath, mode="r", encoding="utf8") as oauth_file: + string_oauth_token = oauth_file.read() + + YTMusic(string_oauth_token) + + def test_oauth_tokens(self, oauth_filepath: str, yt_oauth: YTMusic): + # ensure instance initialized token + assert yt_oauth._token is not None + + # set reference file + with open(oauth_filepath, "r") as f: + first_json = json.load(f) + + # pull reference values from underlying token + first_token = yt_oauth._token.access_token + first_expire = yt_oauth._token.expires_at + # make token expire + yt_oauth._token.expires_at = int(time.time()) + # check + assert yt_oauth._token.is_expiring + # pull new values, assuming token will be refreshed on access + second_token = yt_oauth._token.access_token + second_expire = yt_oauth._token.expires_at + second_token_inner = yt_oauth._token.access_token + # check it was refreshed + assert first_token != second_token + # check expiration timestamps to confirm + assert second_expire != first_expire + assert second_expire > time.time() + 60 + # check token is propagating properly + assert second_token == second_token_inner + + with open(oauth_filepath, "r") as f2: + second_json = json.load(f2) + + # ensure token is updating local file + assert first_json != second_json + + def test_oauth_custom_client( + self, alt_oauth_credentials: OAuthCredentials, oauth_filepath: str, yt_alt_oauth: YTMusic + ): + # ensure client works/ignores alt if browser credentials passed as auth + assert yt_alt_oauth.auth_type != AuthType.OAUTH_CUSTOM_CLIENT + with open(oauth_filepath, "r") as f: + token_dict = json.load(f) + # oauth token dict entry and alt + yt_alt_oauth = YTMusic(token_dict, oauth_credentials=alt_oauth_credentials) + assert yt_alt_oauth.auth_type == AuthType.OAUTH_CUSTOM_CLIENT + + def test_alt_oauth_request(self, yt_alt_oauth: YTMusic, sample_video): + yt_alt_oauth.get_watch_playlist(sample_video) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..29b5bfb8 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,72 @@ +import configparser +from pathlib import Path + +import pytest + +from ytmusicapi import YTMusic + + +def get_resource(file: str) -> str: + data_dir = Path(__file__).parent + return data_dir.joinpath(file).as_posix() + + +@pytest.fixture(name="config") +def fixture_config() -> configparser.RawConfigParser: + config = configparser.RawConfigParser() + config.read(get_resource("test.cfg"), "utf-8") + return config + + +@pytest.fixture(name="sample_album") +def fixture_sample_album() -> str: + """Eminem - Revival""" + return "MPREb_4pL8gzRtw1p" + + +@pytest.fixture(name="sample_video") +def fixture_sample_video() -> str: + """Oasis - Wonderwall""" + return "hpSrLjc5SMs" + + +@pytest.fixture(name="sample_playlist") +def fixture_sample_playlist() -> str: + """very large playlist""" + return "PL6bPxvf5dW5clc3y9wAoslzqUrmkZ5c-u" + + +@pytest.fixture(name="browser_filepath") +def fixture_browser_filepath(config) -> str: + return get_resource(config["auth"]["browser_file"]) + + +@pytest.fixture(name="oauth_filepath") +def fixture_oauth_filepath(config) -> str: + return get_resource(config["auth"]["oauth_file"]) + + +@pytest.fixture(name="yt") +def fixture_yt() -> YTMusic: + return YTMusic() + + +@pytest.fixture(name="yt_auth") +def fixture_yt_auth(browser_filepath) -> YTMusic: + """a non-brand account that is able to create uploads""" + return YTMusic(browser_filepath, location="GB") + + +@pytest.fixture(name="yt_oauth") +def fixture_yt_oauth(oauth_filepath) -> YTMusic: + return YTMusic(oauth_filepath) + + +@pytest.fixture(name="yt_brand") +def fixture_yt_brand(config) -> YTMusic: + return YTMusic(config["auth"]["headers"], config["auth"]["brand_account"]) + + +@pytest.fixture(name="yt_empty") +def fixture_yt_empty(config) -> YTMusic: + return YTMusic(config["auth"]["headers_empty"], config["auth"]["brand_account_empty"]) diff --git a/tests/mixins/test_browsing.py b/tests/mixins/test_browsing.py new file mode 100644 index 00000000..fc8677f8 --- /dev/null +++ b/tests/mixins/test_browsing.py @@ -0,0 +1,126 @@ +import warnings + +import pytest + + +class TestBrowsing: + def test_get_home(self, yt, yt_auth): + result = yt.get_home(limit=6) + assert len(result) >= 6 + result = yt_auth.get_home(limit=15) + assert len(result) >= 15 + + def test_get_artist(self, yt): + results = yt.get_artist("MPLAUCmMUZbaYdNH0bEd1PAlAqsA") + assert len(results) == 14 + + # test correctness of related artists + related = results["related"]["results"] + assert len( + [x for x in related if set(x.keys()) == {"browseId", "subscribers", "title", "thumbnails"}] + ) == len(related) + + results = yt.get_artist("UCLZ7tlKC06ResyDmEStSrOw") # no album year + assert len(results) >= 11 + + def test_get_artist_albums(self, yt): + artist = yt.get_artist("UCj5ZiBBqpe0Tg4zfKGHEFuQ") + results = yt.get_artist_albums(artist["albums"]["browseId"], artist["albums"]["params"]) + assert len(results) > 0 + + def test_get_artist_singles(self, yt): + artist = yt.get_artist("UCAeLFBCQS7FvI8PvBrWvSBg") + results = yt.get_artist_albums(artist["singles"]["browseId"], artist["singles"]["params"]) + assert len(results) > 0 + + def test_get_user(self, yt): + results = yt.get_user("UC44hbeRoCZVVMVg5z0FfIww") + assert len(results) == 3 + + def test_get_user_playlists(self, yt_auth): + results = yt_auth.get_user("UCPVhZsC2od1xjGhgEc2NEPQ") # Vevo playlists + results = yt_auth.get_user_playlists("UCPVhZsC2od1xjGhgEc2NEPQ", results["playlists"]["params"]) + assert len(results) > 100 + + def test_get_album_browse_id(self, yt, sample_album): + warnings.filterwarnings(action="ignore", category=DeprecationWarning) + browse_id = yt.get_album_browse_id("OLAK5uy_nMr9h2VlS-2PULNz3M3XVXQj_P3C2bqaY") + assert browse_id == sample_album + + def test_get_album_browse_id_issue_470(self, yt): + escaped_browse_id = yt.get_album_browse_id("OLAK5uy_nbMYyrfeg5ZgknoOsOGBL268hGxtcbnDM") + assert escaped_browse_id == "MPREb_scJdtUCpPE2" + + def test_get_album(self, yt, yt_auth, sample_album): + results = yt_auth.get_album(sample_album) + assert len(results) >= 9 + assert results["tracks"][0]["isExplicit"] + assert all(item["views"] is not None for item in results["tracks"]) + assert all(item["album"] is not None for item in results["tracks"]) + assert "feedbackTokens" in results["tracks"][0] + assert len(results["other_versions"]) >= 1 # appears to be regional + results = yt.get_album("MPREb_BQZvl3BFGay") + assert len(results["tracks"]) == 7 + assert len(results["tracks"][0]["artists"]) == 1 + results = yt.get_album("MPREb_rqH94Zr3NN0") + assert len(results["tracks"][0]["artists"]) == 2 + + def test_get_song(self, config, yt, yt_oauth, sample_video): + song = yt_oauth.get_song(config["uploads"]["private_upload_id"]) # private upload + assert len(song) == 5 + song = yt.get_song(sample_video) + assert len(song["streamingData"]["adaptiveFormats"]) >= 10 + + def test_get_song_related_content(self, yt_oauth, sample_video): + song = yt_oauth.get_watch_playlist(sample_video) + song = yt_oauth.get_song_related(song["related"]) + assert len(song) >= 5 + + def test_get_lyrics(self, config, yt, sample_video): + playlist = yt.get_watch_playlist(sample_video) + lyrics_song = yt.get_lyrics(playlist["lyrics"]) + assert lyrics_song["lyrics"] is not None + assert lyrics_song["source"] is not None + + playlist = yt.get_watch_playlist(config["uploads"]["private_upload_id"]) + assert playlist["lyrics"] is None + with pytest.raises(Exception): + yt.get_lyrics(playlist["lyrics"]) + + def test_get_signatureTimestamp(self, yt): + signature_timestamp = yt.get_signatureTimestamp() + assert signature_timestamp is not None + + def test_set_tasteprofile(self, yt, yt_brand): + with pytest.raises(Exception): + yt.set_tasteprofile(["not an artist"]) + taste_profile = yt.get_tasteprofile() + assert yt.set_tasteprofile(list(taste_profile)[:5], taste_profile) is None + + with pytest.raises(Exception): + yt_brand.set_tasteprofile(["test", "test2"]) + taste_profile = yt_brand.get_tasteprofile() + assert yt_brand.set_tasteprofile(list(taste_profile)[:1], taste_profile) is None + + def test_get_tasteprofile(self, yt, yt_oauth): + result = yt.get_tasteprofile() + assert len(result) >= 0 + + result = yt_oauth.get_tasteprofile() + assert len(result) >= 0 + + def test_get_search_suggestions(self, yt, yt_brand, yt_auth): + result = yt.get_search_suggestions("fade") + assert len(result) >= 0 + + result = yt.get_search_suggestions("fade", detailed_runs=True) + assert len(result) >= 0 + + # add search term to history + first_pass = yt_brand.search("b") + assert len(first_pass) > 0 + + # get results + results = yt_auth.get_search_suggestions("b", detailed_runs=True) + assert len(results) > 0 + assert any(not item["fromHistory"] for item in results) diff --git a/tests/mixins/test_explore.py b/tests/mixins/test_explore.py new file mode 100644 index 00000000..1a7e8f57 --- /dev/null +++ b/tests/mixins/test_explore.py @@ -0,0 +1,17 @@ +class TestExplore: + def test_get_mood_playlists(self, yt): + categories = yt.get_mood_categories() + assert len(list(categories)) > 0 + cat = list(categories)[0] + assert len(categories[cat]) > 0 + playlists = yt.get_mood_playlists(categories[cat][0]["params"]) + assert len(playlists) > 0 + + def test_get_charts(self, yt, yt_oauth): + charts = yt_oauth.get_charts() + # songs section appears to be removed currently (US) + assert len(charts) >= 3 + charts = yt.get_charts(country="US") + assert len(charts) == 5 + charts = yt.get_charts(country="BE") + assert len(charts) == 4 diff --git a/tests/mixins/test_library.py b/tests/mixins/test_library.py new file mode 100644 index 00000000..3e236375 --- /dev/null +++ b/tests/mixins/test_library.py @@ -0,0 +1,105 @@ +import pytest + + +class TestLibrary: + def test_get_library_playlists(self, config, yt_oauth, yt_empty): + playlists = yt_oauth.get_library_playlists(50) + assert len(playlists) > 25 + + playlists = yt_oauth.get_library_playlists(None) + assert len(playlists) >= config.getint("limits", "library_playlists") + + playlists = yt_empty.get_library_playlists() + assert len(playlists) <= 1 # "Episodes saved for later" + + def test_get_library_songs(self, config, yt_oauth, yt_empty): + with pytest.raises(Exception): + yt_oauth.get_library_songs(None, True) + songs = yt_oauth.get_library_songs(100) + assert len(songs) >= 100 + songs = yt_oauth.get_library_songs(200, validate_responses=True) + assert len(songs) >= config.getint("limits", "library_songs") + songs = yt_oauth.get_library_songs(order="a_to_z") + assert len(songs) >= 25 + songs = yt_empty.get_library_songs() + assert len(songs) == 0 + + def test_get_library_albums(self, yt_oauth, yt_brand, yt_empty): + albums = yt_oauth.get_library_albums(100) + assert len(albums) > 50 + for album in albums: + assert "playlistId" in album + albums = yt_brand.get_library_albums(100, order="a_to_z") + assert len(albums) > 50 + albums = yt_brand.get_library_albums(100, order="z_to_a") + assert len(albums) > 50 + albums = yt_brand.get_library_albums(100, order="recently_added") + assert len(albums) > 50 + albums = yt_empty.get_library_albums() + assert len(albums) == 0 + + def test_get_library_artists(self, config, yt_auth, yt_oauth, yt_brand, yt_empty): + artists = yt_auth.get_library_artists(50) + assert len(artists) > 40 + artists = yt_oauth.get_library_artists(order="a_to_z", limit=50) + assert len(artists) > 40 + artists = yt_brand.get_library_artists(limit=None) + assert len(artists) > config.getint("limits", "library_artists") + artists = yt_empty.get_library_artists() + assert len(artists) == 0 + + def test_get_library_subscriptions(self, config, yt_brand, yt_empty): + artists = yt_brand.get_library_subscriptions(50) + assert len(artists) > 40 + artists = yt_brand.get_library_subscriptions(order="z_to_a") + assert len(artists) > 20 + artists = yt_brand.get_library_subscriptions(limit=None) + assert len(artists) > config.getint("limits", "library_subscriptions") + artists = yt_empty.get_library_subscriptions() + assert len(artists) == 0 + + def test_get_liked_songs(self, yt_brand, yt_empty): + songs = yt_brand.get_liked_songs(200) + assert len(songs["tracks"]) > 100 + songs = yt_empty.get_liked_songs() + assert songs["trackCount"] == 0 + + def test_get_history(self, yt_oauth): + songs = yt_oauth.get_history() + assert len(songs) > 0 + + def test_manipulate_history_items(self, yt_auth, sample_video): + song = yt_auth.get_song(sample_video) + response = yt_auth.add_history_item(song) + assert response.status_code == 204 + songs = yt_auth.get_history() + assert len(songs) > 0 + response = yt_auth.remove_history_items([songs[0]["feedbackToken"]]) + assert "feedbackResponses" in response + + def test_rate_song(self, yt_auth, sample_video): + response = yt_auth.rate_song(sample_video, "LIKE") + assert "actions" in response + response = yt_auth.rate_song(sample_video, "INDIFFERENT") + assert "actions" in response + + def test_edit_song_library_status(self, yt_brand, sample_album): + album = yt_brand.get_album(sample_album) + response = yt_brand.edit_song_library_status(album["tracks"][0]["feedbackTokens"]["add"]) + album = yt_brand.get_album(sample_album) + assert album["tracks"][0]["inLibrary"] + assert response["feedbackResponses"][0]["isProcessed"] + response = yt_brand.edit_song_library_status(album["tracks"][0]["feedbackTokens"]["remove"]) + album = yt_brand.get_album(sample_album) + assert not album["tracks"][0]["inLibrary"] + assert response["feedbackResponses"][0]["isProcessed"] + + def test_rate_playlist(self, yt_auth): + response = yt_auth.rate_playlist("OLAK5uy_l3g4WcHZsEx_QuEDZzWEiyFzZl6pL0xZ4", "LIKE") + assert "actions" in response + response = yt_auth.rate_playlist("OLAK5uy_l3g4WcHZsEx_QuEDZzWEiyFzZl6pL0xZ4", "INDIFFERENT") + assert "actions" in response + + def test_subscribe_artists(self, yt_auth): + yt_auth.subscribe_artists(["UCUDVBtnOQi4c7E8jebpjc9Q", "UCiMhD4jzUqG-IgPzUmmytRQ"]) + yt_auth.unsubscribe_artists(["UCUDVBtnOQi4c7E8jebpjc9Q", "UCiMhD4jzUqG-IgPzUmmytRQ"]) diff --git a/tests/mixins/test_playlists.py b/tests/mixins/test_playlists.py new file mode 100644 index 00000000..3ce433f9 --- /dev/null +++ b/tests/mixins/test_playlists.py @@ -0,0 +1,75 @@ +import time + +import pytest + + +class TestPlaylists: + def test_get_playlist_foreign(self, yt, yt_auth, yt_oauth): + with pytest.raises(Exception): + yt.get_playlist("PLABC") + playlist = yt_auth.get_playlist("PLk5BdzXBUiUe8Q5I13ZSCD8HbxMqJUUQA", limit=300, suggestions_limit=7) + assert len(playlist["duration"]) > 5 + assert len(playlist["tracks"]) > 200 + assert "suggestions" not in playlist + + yt.get_playlist("RDATgXd-") + assert len(playlist["tracks"]) >= 100 + + playlist = yt_oauth.get_playlist("PLj4BSJLnVpNyIjbCWXWNAmybc97FXLlTk", limit=None, related=True) + assert len(playlist["tracks"]) > 200 + assert len(playlist["related"]) == 0 + + def test_get_playlist_owned(self, config, yt_brand): + playlist = yt_brand.get_playlist(config["playlists"]["own"], related=True, suggestions_limit=21) + assert len(playlist["tracks"]) < 100 + assert len(playlist["suggestions"]) == 21 + assert len(playlist["related"]) == 10 + + def test_edit_playlist(self, config, yt_brand): + playlist = yt_brand.get_playlist(config["playlists"]["own"]) + response = yt_brand.edit_playlist( + playlist["id"], + title="", + description="", + privacyStatus="PRIVATE", + moveItem=( + playlist["tracks"][1]["setVideoId"], + playlist["tracks"][0]["setVideoId"], + ), + ) + assert response == "STATUS_SUCCEEDED", "Playlist edit failed" + yt_brand.edit_playlist( + playlist["id"], + title=playlist["title"], + description=playlist["description"], + privacyStatus=playlist["privacy"], + moveItem=( + playlist["tracks"][0]["setVideoId"], + playlist["tracks"][1]["setVideoId"], + ), + ) + assert response == "STATUS_SUCCEEDED", "Playlist edit failed" + + def test_end2end(self, config, yt_brand, sample_video): + playlist_id = yt_brand.create_playlist( + "test", + "test description", + source_playlist="OLAK5uy_lGQfnMNGvYCRdDq9ZLzJV2BJL2aHQsz9Y", + ) + assert len(playlist_id) == 34, "Playlist creation failed" + yt_brand.edit_playlist(playlist_id, addToTop=True) + response = yt_brand.add_playlist_items( + playlist_id, + [sample_video, sample_video], + source_playlist="OLAK5uy_nvjTE32aFYdFN7HCyMv3cGqD3wqBb4Jow", + duplicates=True, + ) + assert response["status"] == "STATUS_SUCCEEDED", "Adding playlist item failed" + assert len(response["playlistEditResults"]) > 0, "Adding playlist item failed" + time.sleep(3) + yt_brand.edit_playlist(playlist_id, addToTop=False) + playlist = yt_brand.get_playlist(playlist_id, related=True) + assert len(playlist["tracks"]) == 46, "Getting playlist items failed" + response = yt_brand.remove_playlist_items(playlist_id, playlist["tracks"]) + assert response == "STATUS_SUCCEEDED", "Playlist item removal failed" + yt_brand.delete_playlist(playlist_id) diff --git a/tests/mixins/test_search.py b/tests/mixins/test_search.py new file mode 100644 index 00000000..017af097 --- /dev/null +++ b/tests/mixins/test_search.py @@ -0,0 +1,90 @@ +import pytest + + +class TestSearch: + def test_search_exceptions(self): + query = "edm playlist" + with pytest.raises(Exception): + yt_auth.search(query, filter="song") + with pytest.raises(Exception): + yt_auth.search(query, scope="upload") + + @pytest.mark.parametrize("query", ["Monekes", "qllwlwl", "heun"]) + def test_search_queries(self, yt, yt_brand, query: str) -> None: + results = yt_brand.search(query) + assert ["resultType" in r for r in results] == [True] * len(results) + assert len(results) >= 10 + results = yt.search(query) + assert len(results) >= 10 + + def test_search_ignore_spelling(self, yt_auth): + results = yt_auth.search("Martin Stig Andersen - Deteriation", ignore_spelling=True) + assert len(results) > 0 + + def test_search_filters(self, yt_auth): + query = "hip hop playlist" + results = yt_auth.search(query, filter="songs") + assert len(results) > 10 + assert all(item["resultType"] == "song" for item in results) + results = yt_auth.search(query, filter="videos") + assert len(results) > 10 + assert all(item["resultType"] == "video" for item in results) + results = yt_auth.search(query, filter="albums", limit=40) + assert len(results) > 20 + assert all(item["resultType"] == "album" for item in results) + results = yt_auth.search("project-2", filter="artists", ignore_spelling=True) + assert len(results) > 10 + assert all(item["resultType"] == "artist" for item in results) + results = yt_auth.search("classical music", filter="playlists") + assert len(results) > 10 + assert all(item["resultType"] == "playlist" for item in results) + results = yt_auth.search("clasical music", filter="playlists", ignore_spelling=True) + assert len(results) > 10 + results = yt_auth.search("clasic rock", filter="community_playlists", ignore_spelling=True) + assert len(results) > 10 + assert all(item["resultType"] == "playlist" for item in results) + results = yt_auth.search("hip hop", filter="featured_playlists") + assert len(results) > 10 + assert all(item["resultType"] == "playlist" for item in results) + results = yt_auth.search("some user", filter="profiles") + assert len(results) > 10 + assert all(item["resultType"] == "profile" for item in results) + results = yt_auth.search(query, filter="podcasts") + assert len(results) > 10 + assert all(item["resultType"] == "podcast" for item in results) + results = yt_auth.search(query, filter="episodes") + assert len(results) > 10 + assert all(item["resultType"] == "episode" for item in results) + + def test_search_uploads(self, config, yt, yt_oauth): + with pytest.raises(Exception, match="No filter can be set when searching uploads"): + yt.search( + config["queries"]["uploads_songs"], + filter="songs", + scope="uploads", + limit=40, + ) + results = yt_oauth.search(config["queries"]["uploads_songs"], scope="uploads", limit=40) + assert len(results) > 20 + + def test_search_library(self, config, yt_oauth): + results = yt_oauth.search(config["queries"]["library_any"], scope="library") + assert len(results) > 5 + results = yt_oauth.search( + config["queries"]["library_songs"], filter="songs", scope="library", limit=40 + ) + assert len(results) > 10 + results = yt_oauth.search( + config["queries"]["library_albums"], filter="albums", scope="library", limit=40 + ) + assert len(results) >= 4 + results = yt_oauth.search( + config["queries"]["library_artists"], filter="artists", scope="library", limit=40 + ) + assert len(results) >= 1 + results = yt_oauth.search(config["queries"]["library_playlists"], filter="playlists", scope="library") + assert len(results) >= 1 + with pytest.raises(Exception): + yt_oauth.search("beatles", filter="community_playlists", scope="library", limit=40) + with pytest.raises(Exception): + yt_oauth.search("beatles", filter="featured_playlists", scope="library", limit=40) diff --git a/tests/mixins/test_uploads.py b/tests/mixins/test_uploads.py new file mode 100644 index 00000000..c37e6e02 --- /dev/null +++ b/tests/mixins/test_uploads.py @@ -0,0 +1,62 @@ +import tempfile + +import pytest + +from tests.conftest import get_resource + + +class TestUploads: + def test_get_library_upload_songs(self, yt_oauth, yt_empty): + results = yt_oauth.get_library_upload_songs(50, order="z_to_a") + assert len(results) > 25 + + results = yt_empty.get_library_upload_songs(100) + assert len(results) == 0 + + def test_get_library_upload_albums(self, config, yt_oauth, yt_empty): + results = yt_oauth.get_library_upload_albums(50, order="a_to_z") + assert len(results) > 40 + + albums = yt_oauth.get_library_upload_albums(None) + assert len(albums) >= config.getint("limits", "library_upload_albums") + + results = yt_empty.get_library_upload_albums(100) + assert len(results) == 0 + + def test_get_library_upload_artists(self, config, yt_oauth, yt_empty): + artists = yt_oauth.get_library_upload_artists(None) + assert len(artists) >= config.getint("limits", "library_upload_artists") + + results = yt_oauth.get_library_upload_artists(50, order="recently_added") + assert len(results) >= 25 + + results = yt_empty.get_library_upload_artists(100) + assert len(results) == 0 + + def test_upload_song_exceptions(self, config, yt_auth, yt_oauth): + with pytest.raises(Exception, match="The provided file does not exist."): + yt_auth.upload_song("song.wav") + with tempfile.NamedTemporaryFile(suffix="wav") as temp, pytest.raises( + Exception, match="The provided file type is not supported" + ): + yt_auth.upload_song(temp.name) + with pytest.raises(Exception, match="Please provide browser authentication"): + yt_oauth.upload_song(config["uploads"]["file"]) + + def test_upload_song(self, config, yt_auth): + response = yt_auth.upload_song(get_resource(config["uploads"]["file"])) + assert response.status_code == 409 + + @pytest.mark.skip(reason="Do not delete uploads") + def test_delete_upload_entity(self, yt_oauth): + results = yt_oauth.get_library_upload_songs() + response = yt_oauth.delete_upload_entity(results[0]["entityId"]) + assert response == "STATUS_SUCCEEDED" + + def test_get_library_upload_album(self, config, yt_oauth): + album = yt_oauth.get_library_upload_album(config["uploads"]["private_album_id"]) + assert len(album["tracks"]) > 0 + + def test_get_library_upload_artist(self, config, yt_oauth): + tracks = yt_oauth.get_library_upload_artist(config["uploads"]["private_artist_id"], 100) + assert len(tracks) > 0 diff --git a/tests/mixins/test_watch.py b/tests/mixins/test_watch.py new file mode 100644 index 00000000..0262274a --- /dev/null +++ b/tests/mixins/test_watch.py @@ -0,0 +1,16 @@ +class TestWatch: + def test_get_watch_playlist(self, config, yt, yt_brand, yt_oauth): + playlist = yt_oauth.get_watch_playlist( + playlistId="RDAMPLOLAK5uy_l_fKDQGOUsk8kbWsm9s86n4-nZNd2JR8Q", + radio=True, + limit=90, + ) + assert len(playlist["tracks"]) >= 90 + playlist = yt_oauth.get_watch_playlist("9mWr4c_ig54", limit=50) + assert len(playlist["tracks"]) > 45 + playlist = yt_oauth.get_watch_playlist("UoAf_y9Ok4k") # private track + assert len(playlist["tracks"]) >= 25 + playlist = yt.get_watch_playlist(playlistId=config["albums"]["album_browse_id"], shuffle=True) + assert len(playlist["tracks"]) == config.getint("albums", "album_track_length") + playlist = yt_brand.get_watch_playlist(playlistId=config["playlists"]["own"], shuffle=True) + assert len(playlist["tracks"]) == config.getint("playlists", "own_length") diff --git a/tests/test.py b/tests/test.py deleted file mode 100644 index f25836b4..00000000 --- a/tests/test.py +++ /dev/null @@ -1,610 +0,0 @@ -import configparser -import json -import time -import unittest -import warnings -from pathlib import Path -from unittest import mock - -from requests import Response - -from ytmusicapi.auth.types import AuthType -from ytmusicapi.constants import OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET -from ytmusicapi.setup import main, setup # noqa: E402 -from ytmusicapi.ytmusic import OAuthCredentials, YTMusic # noqa: E402 - - -def get_resource(file: str) -> str: - data_dir = Path(__file__).parent - return data_dir.joinpath(file).as_posix() - - -config = configparser.RawConfigParser() -config.read(get_resource("test.cfg"), "utf-8") - -sample_album = "MPREb_4pL8gzRtw1p" # Eminem - Revival -sample_video = "hpSrLjc5SMs" # Oasis - Wonderwall -sample_playlist = "PL6bPxvf5dW5clc3y9wAoslzqUrmkZ5c-u" # very large playlist -blank_code = { - "device_code": "", - "user_code": "", - "expires_in": 1800, - "interval": 5, - "verification_url": "https://www.google.com/device", -} - -oauth_filepath = get_resource(config["auth"]["oauth_file"]) -browser_filepath = get_resource(config["auth"]["browser_file"]) - -alt_oauth_creds = OAuthCredentials(OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET) - - -class TestYTMusic(unittest.TestCase): - @classmethod - def setUpClass(cls): - warnings.filterwarnings(action="ignore", message="unclosed", category=ResourceWarning) - with YTMusic(requests_session=False) as yt: - assert isinstance(yt, YTMusic) - cls.yt = YTMusic() - cls.yt_oauth = YTMusic(oauth_filepath) - cls.yt_alt_oauth = YTMusic(browser_filepath, oauth_credentials=alt_oauth_creds) - cls.yt_auth = YTMusic(browser_filepath, location="GB") - cls.yt_brand = YTMusic(config["auth"]["headers"], config["auth"]["brand_account"]) - cls.yt_empty = YTMusic(config["auth"]["headers_empty"], config["auth"]["brand_account_empty"]) - - @mock.patch("sys.argv", ["ytmusicapi", "browser", "--file", browser_filepath]) - def test_setup_browser(self): - headers = setup(browser_filepath, config["auth"]["headers_raw"]) - self.assertGreaterEqual(len(headers), 2) - headers_raw = config["auth"]["headers_raw"].split("\n") - with mock.patch("builtins.input", side_effect=(headers_raw + [EOFError()])): - headers = main() - self.assertGreaterEqual(len(headers), 2) - - @mock.patch("requests.Response.json") - @mock.patch("requests.Session.post") - @mock.patch("sys.argv", ["ytmusicapi", "oauth", "--file", oauth_filepath]) - def test_setup_oauth(self, session_mock, json_mock): - session_mock.return_value = Response() - fresh_token = self.yt_oauth._token.as_dict() - json_mock.side_effect = [blank_code, fresh_token] - with mock.patch("builtins.input", return_value="y"): - main() - self.assertTrue(Path(oauth_filepath).exists()) - - json_mock.side_effect = None - with open(oauth_filepath, mode="r", encoding="utf8") as oauth_file: - string_oauth_token = oauth_file.read() - self.yt_oauth = YTMusic(string_oauth_token) - - ############### - # OAUTH - ############### - def test_oauth_tokens(self): - # ensure instance initialized token - self.assertIsNotNone(self.yt_oauth._token) - - # set reference file - with open(oauth_filepath, "r") as f: - first_json = json.load(f) - - # pull reference values from underlying token - first_token = self.yt_oauth._token.access_token - first_expire = self.yt_oauth._token.expires_at - # make token expire - self.yt_oauth._token.expires_at = time.time() - # check - self.assertTrue(self.yt_oauth._token.is_expiring) - # pull new values, assuming token will be refreshed on access - second_token = self.yt_oauth._token.access_token - second_expire = self.yt_oauth._token.expires_at - second_token_inner = self.yt_oauth._token.access_token - # check it was refreshed - self.assertNotEqual(first_token, second_token) - # check expiration timestamps to confirm - self.assertNotEqual(second_expire, first_expire) - self.assertGreater(second_expire, time.time() + 60) - # check token is propagating properly - self.assertEqual(second_token, second_token_inner) - - with open(oauth_filepath, "r") as f2: - second_json = json.load(f2) - - # ensure token is updating local file - self.assertNotEqual(first_json, second_json) - - def test_oauth_custom_client(self): - # ensure client works/ignores alt if browser credentials passed as auth - self.assertNotEqual(self.yt_alt_oauth.auth_type, AuthType.OAUTH_CUSTOM_CLIENT) - with open(oauth_filepath, "r") as f: - token_dict = json.load(f) - # oauth token dict entry and alt - self.yt_alt_oauth = YTMusic(token_dict, oauth_credentials=alt_oauth_creds) - self.assertEqual(self.yt_alt_oauth.auth_type, AuthType.OAUTH_CUSTOM_CLIENT) - - ############### - # BROWSING - ############### - - def test_get_home(self): - result = self.yt.get_home(limit=6) - self.assertGreaterEqual(len(result), 6) - result = self.yt_auth.get_home(limit=15) - self.assertGreaterEqual(len(result), 15) - - def test_search(self): - query = "edm playlist" - self.assertRaises(Exception, self.yt_auth.search, query, filter="song") - self.assertRaises(Exception, self.yt_auth.search, query, scope="upload") - queries = ["Monekes", "qllwlwl", "heun"] - for q in queries: - with self.subTest(): - results = self.yt_brand.search(q) - self.assertListEqual(["resultType" in r for r in results], [True] * len(results)) - self.assertGreaterEqual(len(results), 10) - results = self.yt.search(q) - self.assertGreaterEqual(len(results), 10) - results = self.yt_auth.search("Martin Stig Andersen - Deteriation", ignore_spelling=True) - self.assertGreater(len(results), 0) - - def test_search_filters(self): - query = "hip hop playlist" - results = self.yt_auth.search(query, filter="songs") - self.assertGreater(len(results), 10) - self.assertTrue(all(item["resultType"] == "song" for item in results)) - results = self.yt_auth.search(query, filter="videos") - self.assertGreater(len(results), 10) - self.assertTrue(all(item["resultType"] == "video" for item in results)) - results = self.yt_auth.search(query, filter="albums", limit=40) - self.assertGreater(len(results), 20) - self.assertTrue(all(item["resultType"] == "album" for item in results)) - results = self.yt_auth.search("project-2", filter="artists", ignore_spelling=True) - self.assertGreater(len(results), 10) - self.assertTrue(all(item["resultType"] == "artist" for item in results)) - results = self.yt_auth.search("classical music", filter="playlists") - self.assertGreater(len(results), 10) - self.assertTrue(all(item["resultType"] == "playlist" for item in results)) - results = self.yt_auth.search("clasical music", filter="playlists", ignore_spelling=True) - self.assertGreater(len(results), 10) - results = self.yt_auth.search("clasic rock", filter="community_playlists", ignore_spelling=True) - self.assertGreater(len(results), 10) - self.assertTrue(all(item["resultType"] == "playlist" for item in results)) - results = self.yt_auth.search("hip hop", filter="featured_playlists") - self.assertGreater(len(results), 10) - self.assertTrue(all(item["resultType"] == "playlist" for item in results)) - results = self.yt_auth.search("some user", filter="profiles") - self.assertGreater(len(results), 10) - self.assertTrue(all(item["resultType"] == "profile" for item in results)) - results = self.yt_auth.search(query, filter="podcasts") - self.assertGreater(len(results), 10) - self.assertTrue(all(item["resultType"] == "podcast" for item in results)) - results = self.yt_auth.search(query, filter="episodes") - self.assertGreater(len(results), 10) - self.assertTrue(all(item["resultType"] == "episode" for item in results)) - - def test_search_uploads(self): - self.assertRaises( - Exception, - self.yt.search, - config["queries"]["uploads_songs"], - filter="songs", - scope="uploads", - limit=40, - ) - results = self.yt_auth.search(config["queries"]["uploads_songs"], scope="uploads", limit=40) - self.assertGreater(len(results), 20) - - def test_search_library(self): - results = self.yt_oauth.search(config["queries"]["library_any"], scope="library") - self.assertGreater(len(results), 5) - results = self.yt_alt_oauth.search( - config["queries"]["library_songs"], filter="songs", scope="library", limit=40 - ) - self.assertGreater(len(results), 10) - results = self.yt_auth.search( - config["queries"]["library_albums"], filter="albums", scope="library", limit=40 - ) - self.assertGreaterEqual(len(results), 4) - results = self.yt_auth.search( - config["queries"]["library_artists"], filter="artists", scope="library", limit=40 - ) - self.assertGreaterEqual(len(results), 1) - results = self.yt_auth.search( - config["queries"]["library_playlists"], filter="playlists", scope="library" - ) - self.assertGreaterEqual(len(results), 1) - self.assertRaises( - Exception, self.yt_auth.search, "beatles", filter="community_playlists", scope="library", limit=40 - ) - self.assertRaises( - Exception, self.yt_auth.search, "beatles", filter="featured_playlists", scope="library", limit=40 - ) - - def test_get_artist(self): - results = self.yt.get_artist("MPLAUCmMUZbaYdNH0bEd1PAlAqsA") - self.assertEqual(len(results), 14) - - # test correctness of related artists - related = results["related"]["results"] - self.assertEqual( - len([x for x in related if set(x.keys()) == {"browseId", "subscribers", "title", "thumbnails"}]), - len(related), - ) - - results = self.yt.get_artist("UCLZ7tlKC06ResyDmEStSrOw") # no album year - self.assertGreaterEqual(len(results), 11) - - def test_get_artist_albums(self): - artist = self.yt.get_artist("UCj5ZiBBqpe0Tg4zfKGHEFuQ") - results = self.yt.get_artist_albums(artist["albums"]["browseId"], artist["albums"]["params"]) - self.assertGreater(len(results), 0) - - def test_get_artist_singles(self): - artist = self.yt.get_artist("UCAeLFBCQS7FvI8PvBrWvSBg") - results = self.yt.get_artist_albums(artist["singles"]["browseId"], artist["singles"]["params"]) - self.assertGreater(len(results), 0) - - def test_get_user(self): - results = self.yt.get_user("UC44hbeRoCZVVMVg5z0FfIww") - self.assertEqual(len(results), 3) - - def test_get_user_playlists(self): - results = self.yt.get_user("UCPVhZsC2od1xjGhgEc2NEPQ") - results = self.yt.get_user_playlists("UCPVhZsC2od1xjGhgEc2NEPQ", results["playlists"]["params"]) - self.assertGreater(len(results), 100) - - def test_get_album_browse_id(self): - warnings.filterwarnings(action="ignore", category=DeprecationWarning) - browse_id = self.yt.get_album_browse_id("OLAK5uy_nMr9h2VlS-2PULNz3M3XVXQj_P3C2bqaY") - self.assertEqual(browse_id, sample_album) - with self.subTest(): - escaped_browse_id = self.yt.get_album_browse_id("OLAK5uy_nbMYyrfeg5ZgknoOsOGBL268hGxtcbnDM") - self.assertEqual(escaped_browse_id, "MPREb_scJdtUCpPE2") - - def test_get_album(self): - results = self.yt_auth.get_album(sample_album) - self.assertGreaterEqual(len(results), 9) - self.assertTrue(results["tracks"][0]["isExplicit"]) - self.assertTrue(all(item["views"] is not None for item in results["tracks"])) - self.assertTrue(all(item["album"] is not None for item in results["tracks"])) - self.assertIn("feedbackTokens", results["tracks"][0]) - self.assertGreaterEqual(len(results["other_versions"]), 1) # appears to be regional - results = self.yt.get_album("MPREb_BQZvl3BFGay") - self.assertEqual(len(results["tracks"]), 7) - self.assertEqual(len(results["tracks"][0]["artists"]), 1) - results = self.yt.get_album("MPREb_rqH94Zr3NN0") - self.assertEqual(len(results["tracks"][0]["artists"]), 2) - - def test_get_song(self): - song = self.yt_oauth.get_song(config["uploads"]["private_upload_id"]) # private upload - self.assertEqual(len(song), 5) - song = self.yt.get_song(sample_video) - self.assertGreaterEqual(len(song["streamingData"]["adaptiveFormats"]), 10) - - def test_get_song_related_content(self): - song = self.yt_oauth.get_watch_playlist(sample_video) - song = self.yt_alt_oauth.get_song_related(song["related"]) - self.assertGreaterEqual(len(song), 5) - - def test_get_lyrics(self): - playlist = self.yt.get_watch_playlist(sample_video) - lyrics_song = self.yt.get_lyrics(playlist["lyrics"]) - self.assertIsNotNone(lyrics_song["lyrics"]) - self.assertIsNotNone(lyrics_song["source"]) - - playlist = self.yt.get_watch_playlist(config["uploads"]["private_upload_id"]) - self.assertIsNone(playlist["lyrics"]) - self.assertRaises(Exception, self.yt.get_lyrics, playlist["lyrics"]) - - def test_get_signatureTimestamp(self): - signature_timestamp = self.yt.get_signatureTimestamp() - self.assertIsNotNone(signature_timestamp) - - def test_set_tasteprofile(self): - self.assertRaises(Exception, self.yt.set_tasteprofile, "not an artist") - taste_profile = self.yt.get_tasteprofile() - self.assertIsNone(self.yt.set_tasteprofile(list(taste_profile)[:5], taste_profile)) - - self.assertRaises(Exception, self.yt_brand.set_tasteprofile, ["test", "test2"]) - taste_profile = self.yt_brand.get_tasteprofile() - self.assertIsNone(self.yt_brand.set_tasteprofile(list(taste_profile)[:1], taste_profile)) - - def test_get_tasteprofile(self): - result = self.yt.get_tasteprofile() - self.assertGreaterEqual(len(result), 0) - - result = self.yt_oauth.get_tasteprofile() - self.assertGreaterEqual(len(result), 0) - - def test_get_search_suggestions(self): - result = self.yt.get_search_suggestions("fade") - self.assertGreaterEqual(len(result), 0) - - result = self.yt.get_search_suggestions("fade", detailed_runs=True) - self.assertGreaterEqual(len(result), 0) - - # add search term to history - first_pass = self.yt_brand.search("b") - self.assertGreater(len(first_pass), 0) - # get results - results = self.yt_auth.get_search_suggestions("b", detailed_runs=True) - self.assertGreater(len(results), 0) - self.assertTrue(any(not item["fromHistory"] for item in results)) - - ################ - # EXPLORE - ################ - - def test_get_mood_playlists(self): - categories = self.yt.get_mood_categories() - self.assertGreater(len(list(categories)), 0) - cat = list(categories)[0] - self.assertGreater(len(categories[cat]), 0) - playlists = self.yt.get_mood_playlists(categories[cat][0]["params"]) - self.assertGreater(len(playlists), 0) - - def test_get_charts(self): - charts = self.yt_oauth.get_charts() - # songs section appears to be removed currently (US) - self.assertGreaterEqual(len(charts), 3) - charts = self.yt.get_charts(country="US") - self.assertEqual(len(charts), 5) - charts = self.yt.get_charts(country="BE") - self.assertEqual(len(charts), 4) - - ############### - # WATCH - ############### - - def test_get_watch_playlist(self): - playlist = self.yt_oauth.get_watch_playlist( - playlistId="RDAMPLOLAK5uy_l_fKDQGOUsk8kbWsm9s86n4-nZNd2JR8Q", - radio=True, - limit=90, - ) - self.assertGreaterEqual(len(playlist["tracks"]), 90) - playlist = self.yt_oauth.get_watch_playlist("9mWr4c_ig54", limit=50) - self.assertGreater(len(playlist["tracks"]), 45) - playlist = self.yt_oauth.get_watch_playlist("UoAf_y9Ok4k") # private track - self.assertGreaterEqual(len(playlist["tracks"]), 25) - playlist = self.yt.get_watch_playlist(playlistId=config["albums"]["album_browse_id"], shuffle=True) - self.assertEqual(len(playlist["tracks"]), config.getint("albums", "album_track_length")) - playlist = self.yt_brand.get_watch_playlist(playlistId=config["playlists"]["own"], shuffle=True) - self.assertEqual(len(playlist["tracks"]), config.getint("playlists", "own_length")) - - ################ - # LIBRARY - ################ - - def test_get_library_playlists(self): - playlists = self.yt_oauth.get_library_playlists(50) - self.assertGreater(len(playlists), 25) - - playlists = self.yt_auth.get_library_playlists(None) - self.assertGreaterEqual(len(playlists), config.getint("limits", "library_playlists")) - - playlists = self.yt_empty.get_library_playlists() - self.assertLessEqual(len(playlists), 1) # "Episodes saved for later" - - def test_get_library_songs(self): - self.assertRaises(Exception, self.yt_auth.get_library_songs, None, True) - songs = self.yt_oauth.get_library_songs(100) - self.assertGreaterEqual(len(songs), 100) - songs = self.yt_auth.get_library_songs(200, validate_responses=True) - self.assertGreaterEqual(len(songs), config.getint("limits", "library_songs")) - songs = self.yt_auth.get_library_songs(order="a_to_z") - self.assertGreaterEqual(len(songs), 25) - songs = self.yt_empty.get_library_songs() - self.assertEqual(len(songs), 0) - - def test_get_library_albums(self): - albums = self.yt_oauth.get_library_albums(100) - self.assertGreater(len(albums), 50) - for album in albums: - self.assertIn("playlistId", album) - albums = self.yt_brand.get_library_albums(100, order="a_to_z") - self.assertGreater(len(albums), 50) - albums = self.yt_brand.get_library_albums(100, order="z_to_a") - self.assertGreater(len(albums), 50) - albums = self.yt_brand.get_library_albums(100, order="recently_added") - self.assertGreater(len(albums), 50) - albums = self.yt_empty.get_library_albums() - self.assertEqual(len(albums), 0) - - def test_get_library_artists(self): - artists = self.yt_auth.get_library_artists(50) - self.assertGreater(len(artists), 40) - artists = self.yt_oauth.get_library_artists(order="a_to_z", limit=50) - self.assertGreater(len(artists), 40) - artists = self.yt_brand.get_library_artists(limit=None) - self.assertGreater(len(artists), config.getint("limits", "library_artists")) - artists = self.yt_empty.get_library_artists() - self.assertEqual(len(artists), 0) - - def test_get_library_subscriptions(self): - artists = self.yt_brand.get_library_subscriptions(50) - self.assertGreater(len(artists), 40) - artists = self.yt_brand.get_library_subscriptions(order="z_to_a") - self.assertGreater(len(artists), 20) - artists = self.yt_brand.get_library_subscriptions(limit=None) - self.assertGreater(len(artists), config.getint("limits", "library_subscriptions")) - artists = self.yt_empty.get_library_subscriptions() - self.assertEqual(len(artists), 0) - - def test_get_liked_songs(self): - songs = self.yt_brand.get_liked_songs(200) - self.assertGreater(len(songs["tracks"]), 100) - songs = self.yt_empty.get_liked_songs() - self.assertEqual(songs["trackCount"], 0) - - def test_get_history(self): - songs = self.yt_oauth.get_history() - self.assertGreater(len(songs), 0) - - def test_manipulate_history_items(self): - song = self.yt_auth.get_song(sample_video) - response = self.yt_auth.add_history_item(song) - self.assertEqual(response.status_code, 204) - songs = self.yt_auth.get_history() - self.assertGreater(len(songs), 0) - response = self.yt_auth.remove_history_items([songs[0]["feedbackToken"]]) - self.assertIn("feedbackResponses", response) - - def test_rate_song(self): - response = self.yt_auth.rate_song(sample_video, "LIKE") - self.assertIn("actions", response) - response = self.yt_auth.rate_song(sample_video, "INDIFFERENT") - self.assertIn("actions", response) - - def test_edit_song_library_status(self): - album = self.yt_brand.get_album(sample_album) - response = self.yt_brand.edit_song_library_status(album["tracks"][0]["feedbackTokens"]["add"]) - album = self.yt_brand.get_album(sample_album) - self.assertTrue(album["tracks"][0]["inLibrary"]) - self.assertTrue(response["feedbackResponses"][0]["isProcessed"]) - response = self.yt_brand.edit_song_library_status(album["tracks"][0]["feedbackTokens"]["remove"]) - album = self.yt_brand.get_album(sample_album) - self.assertFalse(album["tracks"][0]["inLibrary"]) - self.assertTrue(response["feedbackResponses"][0]["isProcessed"]) - - def test_rate_playlist(self): - response = self.yt_auth.rate_playlist("OLAK5uy_l3g4WcHZsEx_QuEDZzWEiyFzZl6pL0xZ4", "LIKE") - self.assertIn("actions", response) - response = self.yt_auth.rate_playlist("OLAK5uy_l3g4WcHZsEx_QuEDZzWEiyFzZl6pL0xZ4", "INDIFFERENT") - self.assertIn("actions", response) - - def test_subscribe_artists(self): - self.yt_auth.subscribe_artists(["UCUDVBtnOQi4c7E8jebpjc9Q", "UCiMhD4jzUqG-IgPzUmmytRQ"]) - self.yt_auth.unsubscribe_artists(["UCUDVBtnOQi4c7E8jebpjc9Q", "UCiMhD4jzUqG-IgPzUmmytRQ"]) - - ############### - # PLAYLISTS - ############### - - def test_get_playlist_foreign(self): - self.assertRaises(Exception, self.yt.get_playlist, "PLABC") - playlist = self.yt_auth.get_playlist( - "PLk5BdzXBUiUe8Q5I13ZSCD8HbxMqJUUQA", limit=300, suggestions_limit=7 - ) - self.assertGreater(len(playlist["duration"]), 5) - self.assertGreater(len(playlist["tracks"]), 200) - self.assertNotIn("suggestions", playlist) - - self.yt.get_playlist("RDATgXd-") - self.assertGreaterEqual(len(playlist["tracks"]), 100) - - playlist = self.yt_oauth.get_playlist("PLj4BSJLnVpNyIjbCWXWNAmybc97FXLlTk", limit=None, related=True) - self.assertGreater(len(playlist["tracks"]), 200) - self.assertEqual(len(playlist["related"]), 0) - - def test_get_playlist_owned(self): - playlist = self.yt_brand.get_playlist(config["playlists"]["own"], related=True, suggestions_limit=21) - self.assertLess(len(playlist["tracks"]), 100) - self.assertEqual(len(playlist["suggestions"]), 21) - self.assertEqual(len(playlist["related"]), 10) - - def test_edit_playlist(self): - playlist = self.yt_brand.get_playlist(config["playlists"]["own"]) - response = self.yt_brand.edit_playlist( - playlist["id"], - title="", - description="", - privacyStatus="PRIVATE", - moveItem=( - playlist["tracks"][1]["setVideoId"], - playlist["tracks"][0]["setVideoId"], - ), - ) - self.assertEqual(response, "STATUS_SUCCEEDED", "Playlist edit failed") - self.yt_brand.edit_playlist( - playlist["id"], - title=playlist["title"], - description=playlist["description"], - privacyStatus=playlist["privacy"], - moveItem=( - playlist["tracks"][0]["setVideoId"], - playlist["tracks"][1]["setVideoId"], - ), - ) - self.assertEqual(response, "STATUS_SUCCEEDED", "Playlist edit failed") - - # end-to-end test adding playlist, adding item, deleting item, deleting playlist - # @unittest.skip('You are creating too many playlists. Please wait a bit...') - def test_end2end(self): - playlist_id = self.yt_brand.create_playlist( - "test", - "test description", - source_playlist="OLAK5uy_lGQfnMNGvYCRdDq9ZLzJV2BJL2aHQsz9Y", - ) - self.assertEqual(len(playlist_id), 34, "Playlist creation failed") - self.yt_brand.edit_playlist(playlist_id, addToTop=True) - response = self.yt_brand.add_playlist_items( - playlist_id, - [sample_video, sample_video], - source_playlist="OLAK5uy_nvjTE32aFYdFN7HCyMv3cGqD3wqBb4Jow", - duplicates=True, - ) - self.assertEqual(response["status"], "STATUS_SUCCEEDED", "Adding playlist item failed") - self.assertGreater(len(response["playlistEditResults"]), 0, "Adding playlist item failed") - time.sleep(2) - self.yt_brand.edit_playlist(playlist_id, addToTop=False) - playlist = self.yt_brand.get_playlist(playlist_id, related=True) - self.assertEqual(len(playlist["tracks"]), 46, "Getting playlist items failed") - response = self.yt_brand.remove_playlist_items(playlist_id, playlist["tracks"]) - self.assertEqual(response, "STATUS_SUCCEEDED", "Playlist item removal failed") - self.yt_brand.delete_playlist(playlist_id) - - ############### - # UPLOADS - ############### - - def test_get_library_upload_songs(self): - results = self.yt_oauth.get_library_upload_songs(50, order="z_to_a") - self.assertGreater(len(results), 25) - - results = self.yt_empty.get_library_upload_songs(100) - self.assertEqual(len(results), 0) - - def test_get_library_upload_albums(self): - results = self.yt_oauth.get_library_upload_albums(50, order="a_to_z") - self.assertGreater(len(results), 40) - - albums = self.yt_auth.get_library_upload_albums(None) - self.assertGreaterEqual(len(albums), config.getint("limits", "library_upload_albums")) - - results = self.yt_empty.get_library_upload_albums(100) - self.assertEqual(len(results), 0) - - def test_get_library_upload_artists(self): - artists = self.yt_oauth.get_library_upload_artists(None) - self.assertGreaterEqual(len(artists), config.getint("limits", "library_upload_artists")) - - results = self.yt_auth.get_library_upload_artists(50, order="recently_added") - self.assertGreaterEqual(len(results), 25) - - results = self.yt_empty.get_library_upload_artists(100) - self.assertEqual(len(results), 0) - - def test_upload_song(self): - self.assertRaises(Exception, self.yt_auth.upload_song, "song.wav") - self.assertRaises(Exception, self.yt_oauth.upload_song, config["uploads"]["file"]) - response = self.yt_auth.upload_song(get_resource(config["uploads"]["file"])) - self.assertEqual(response.status_code, 409) - - @unittest.skip("Do not delete uploads") - def test_delete_upload_entity(self): - results = self.yt_oauth.get_library_upload_songs() - response = self.yt_oauth.delete_upload_entity(results[0]["entityId"]) - self.assertEqual(response, "STATUS_SUCCEEDED") - - def test_get_library_upload_album(self): - album = self.yt_oauth.get_library_upload_album(config["uploads"]["private_album_id"]) - self.assertGreater(len(album["tracks"]), 0) - - def test_get_library_upload_artist(self): - tracks = self.yt_oauth.get_library_upload_artist(config["uploads"]["private_artist_id"], 100) - self.assertGreater(len(tracks), 0) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_ytmusic.py b/tests/test_ytmusic.py new file mode 100644 index 00000000..63ae9544 --- /dev/null +++ b/tests/test_ytmusic.py @@ -0,0 +1,6 @@ +from ytmusicapi import YTMusic + + +def test_ytmusic_context(): + with YTMusic(requests_session=False) as yt: + assert isinstance(yt, YTMusic)