Repository: sigma67/ytmusicapi
Pull request #765 (2025-04-17 10:01:01)

>=3.10 strict typing


Patch:
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index 5795a25..060c0f1 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -22,7 +22,7 @@ jobs:
     - name: Setup Python
       uses: actions/setup-python@v5
       with:
-        python-version: 3.9
+        python-version: 3.10
     - name: Setup PDM
       uses: pdm-project/setup-pdm@v4
     - name: create-json
@@ -41,7 +41,7 @@ jobs:
         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
-        pdm run pytest --junitxml=test-results.xml
+        pdm run pytest
         pdm run coverage xml
     - name: Upload coverage to Codecov
       uses: codecov/codecov-action@v5
@@ -55,4 +55,3 @@ jobs:
       uses: codecov/test-results-action@v1
       with:
         token: ${{ secrets.CODECOV_TOKEN }}
-        files: test-results.xml
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index ff9d055..a4a7b3c 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -15,10 +15,10 @@ jobs:
       - uses: actions/checkout@v4
       - uses: chartboost/ruff-action@v1
         with:
-          version: 0.9.4
+          version: 0.11.5
       - uses: chartboost/ruff-action@v1
         with:
-          version: 0.9.4
+          version: 0.11.5
           args: format --check
   mypy:
     runs-on: ubuntu-latest
@@ -26,6 +26,6 @@ jobs:
       - uses: actions/checkout@v4
       - uses: actions/setup-python@v5
         with:
-          python-version: "3.9"
-      - run: pip install mypy==1.14.1
+          python-version: "3.10"
+      - run: pip install mypy==1.15.0
       - run:  mypy --install-types --non-interactive
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index df7f0d6..ffd6a45 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,7 +1,7 @@
 repos:
 - repo: https://github.com/astral-sh/ruff-pre-commit
   # Ruff version.
-  rev: v0.9.4
+  rev: v0.11.5
   hooks:
     # Run the linter.
     - id: ruff
@@ -10,8 +10,9 @@ repos:
     - id: ruff-format
 
 - repo: https://github.com/pre-commit/mirrors-mypy
-  rev: v1.14.1
+  rev: v1.15.0
   hooks:
   -   id: mypy
       verbose: true
       additional_dependencies: [ 'types-requests' ]
+      pass_filenames: false
diff --git a/pdm.lock b/pdm.lock
index ac0f08a..0a05a52 100644
--- a/pdm.lock
+++ b/pdm.lock
@@ -5,10 +5,10 @@
 groups = ["default", "dev"]
 strategy = ["inherit_metadata"]
 lock_version = "4.5.0"
-content_hash = "sha256:c789497f83027a6f145d2ddb8cd18747f395b42f89330109b847bec1a417449c"
+content_hash = "sha256:7b171453e1787fabeb4b9f70f8a2a757516a30b9150b879cd2f8bb662f8aa793"
 
 [[metadata.targets]]
-requires_python = ">=3.9"
+requires_python = ">=3.10"
 
 [[package]]
 name = "alabaster"
@@ -326,22 +326,6 @@ files = [
     {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"},
 ]
 
-[[package]]
-name = "importlib-metadata"
-version = "8.6.1"
-requires_python = ">=3.9"
-summary = "Read metadata from Python packages"
-groups = ["dev"]
-marker = "python_version < \"3.10\""
-dependencies = [
-    "typing-extensions>=3.6.4; python_version < \"3.8\"",
-    "zipp>=3.20",
-]
-files = [
-    {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"},
-    {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"},
-]
-
 [[package]]
 name = "iniconfig"
 version = "2.0.0"
@@ -439,8 +423,8 @@ files = [
 
 [[package]]
 name = "mypy"
-version = "1.14.1"
-requires_python = ">=3.8"
+version = "1.15.0"
+requires_python = ">=3.9"
 summary = "Optional static typing for Python"
 groups = ["dev"]
 dependencies = [
@@ -449,38 +433,32 @@ dependencies = [
     "typing-extensions>=4.6.0",
 ]
 files = [
-    {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"},
-    {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"},
-    {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"},
-    {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"},
-    {file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"},
-    {file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"},
-    {file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"},
-    {file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"},
-    {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"},
-    {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"},
-    {file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"},
-    {file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"},
-    {file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"},
-    {file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"},
-    {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"},
-    {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"},
-    {file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"},
-    {file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"},
-    {file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"},
-    {file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"},
-    {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"},
-    {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"},
-    {file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"},
-    {file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"},
-    {file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"},
-    {file = "mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc"},
-    {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9"},
-    {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb"},
-    {file = "mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60"},
-    {file = "mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c"},
-    {file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"},
-    {file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"},
+    {file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"},
+    {file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"},
+    {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"},
+    {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"},
+    {file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"},
+    {file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"},
+    {file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"},
+    {file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"},
+    {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"},
+    {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"},
+    {file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"},
+    {file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"},
+    {file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"},
+    {file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"},
+    {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"},
+    {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"},
+    {file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"},
+    {file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"},
+    {file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"},
+    {file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"},
+    {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"},
+    {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"},
+    {file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"},
+    {file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"},
+    {file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"},
+    {file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"},
 ]
 
 [[package]]
@@ -594,29 +572,29 @@ files = [
 
 [[package]]
 name = "ruff"
-version = "0.9.4"
+version = "0.11.5"
 requires_python = ">=3.7"
 summary = "An extremely fast Python linter and code formatter, written in Rust."
 groups = ["dev"]
 files = [
-    {file = "ruff-0.9.4-py3-none-linux_armv6l.whl", hash = "sha256:64e73d25b954f71ff100bb70f39f1ee09e880728efb4250c632ceed4e4cdf706"},
-    {file = "ruff-0.9.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6ce6743ed64d9afab4fafeaea70d3631b4d4b28b592db21a5c2d1f0ef52934bf"},
-    {file = "ruff-0.9.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:54499fb08408e32b57360f6f9de7157a5fec24ad79cb3f42ef2c3f3f728dfe2b"},
-    {file = "ruff-0.9.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37c892540108314a6f01f105040b5106aeb829fa5fb0561d2dcaf71485021137"},
-    {file = "ruff-0.9.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de9edf2ce4b9ddf43fd93e20ef635a900e25f622f87ed6e3047a664d0e8f810e"},
-    {file = "ruff-0.9.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87c90c32357c74f11deb7fbb065126d91771b207bf9bfaaee01277ca59b574ec"},
-    {file = "ruff-0.9.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56acd6c694da3695a7461cc55775f3a409c3815ac467279dfa126061d84b314b"},
-    {file = "ruff-0.9.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0c93e7d47ed951b9394cf352d6695b31498e68fd5782d6cbc282425655f687a"},
-    {file = "ruff-0.9.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d4c8772670aecf037d1bf7a07c39106574d143b26cfe5ed1787d2f31e800214"},
-    {file = "ruff-0.9.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfc5f1d7afeda8d5d37660eeca6d389b142d7f2b5a1ab659d9214ebd0e025231"},
-    {file = "ruff-0.9.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:faa935fc00ae854d8b638c16a5f1ce881bc3f67446957dd6f2af440a5fc8526b"},
-    {file = "ruff-0.9.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a6c634fc6f5a0ceae1ab3e13c58183978185d131a29c425e4eaa9f40afe1e6d6"},
-    {file = "ruff-0.9.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:433dedf6ddfdec7f1ac7575ec1eb9844fa60c4c8c2f8887a070672b8d353d34c"},
-    {file = "ruff-0.9.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d612dbd0f3a919a8cc1d12037168bfa536862066808960e0cc901404b77968f0"},
-    {file = "ruff-0.9.4-py3-none-win32.whl", hash = "sha256:db1192ddda2200671f9ef61d9597fcef89d934f5d1705e571a93a67fb13a4402"},
-    {file = "ruff-0.9.4-py3-none-win_amd64.whl", hash = "sha256:05bebf4cdbe3ef75430d26c375773978950bbf4ee3c95ccb5448940dc092408e"},
-    {file = "ruff-0.9.4-py3-none-win_arm64.whl", hash = "sha256:585792f1e81509e38ac5123492f8875fbc36f3ede8185af0a26df348e5154f41"},
-    {file = "ruff-0.9.4.tar.gz", hash = "sha256:6907ee3529244bb0ed066683e075f09285b38dd5b4039370df6ff06041ca19e7"},
+    {file = "ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b"},
+    {file = "ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077"},
+    {file = "ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779"},
+    {file = "ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794"},
+    {file = "ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038"},
+    {file = "ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f"},
+    {file = "ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82"},
+    {file = "ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304"},
+    {file = "ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470"},
+    {file = "ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a"},
+    {file = "ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b"},
+    {file = "ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a"},
+    {file = "ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159"},
+    {file = "ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783"},
+    {file = "ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe"},
+    {file = "ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800"},
+    {file = "ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e"},
+    {file = "ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef"},
 ]
 
 [[package]]
@@ -846,15 +824,3 @@ files = [
     {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"},
     {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"},
 ]
-
-[[package]]
-name = "zipp"
-version = "3.21.0"
-requires_python = ">=3.9"
-summary = "Backport of pathlib-compatible object wrapper for zip files"
-groups = ["dev"]
-marker = "python_version < \"3.10\""
-files = [
-    {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"},
-    {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"},
-]
diff --git a/pyproject.toml b/pyproject.toml
index c164ec0..70fcb75 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
 [project]
 name = "ytmusicapi"
 description = "Unofficial API for YouTube Music"
-requires-python = ">=3.9"
+requires-python = ">=3.10"
 authors=[{name = "sigma67", email= "ytmusicapi@gmail.com"}]
 license={file="LICENSE"}
 classifiers = [
@@ -70,6 +70,7 @@ files = [
     "ytmusicapi/"
 ]
 mypy_path = "ytmusicapi"
+strict = true
 
 [dependency-groups]
 dev = [
@@ -77,7 +78,7 @@ dev = [
     'sphinx<7',
     'sphinx-rtd-theme',
     "ruff>=0.9.0",
-    "mypy>=1.14.0",
+    "mypy>=1.15.0",
     "pytest>=7.4.4",
     "pytest-cov>=4.1.0",
     "types-requests>=2.31.0.20240218",
diff --git a/ytmusicapi/__init__.py b/ytmusicapi/__init__.py
index b654578..c741bff 100644
--- a/ytmusicapi/__init__.py
+++ b/ytmusicapi/__init__.py
@@ -1,6 +1,7 @@
 from importlib.metadata import PackageNotFoundError, version
 
 from ytmusicapi.auth.oauth.credentials import OAuthCredentials
+from ytmusicapi.models.content.enums import LikeStatus
 from ytmusicapi.setup import setup, setup_oauth
 from ytmusicapi.ytmusic import YTMusic
 
@@ -13,4 +14,4 @@ except PackageNotFoundError:
 __copyright__ = "Copyright 2024 sigma67"
 __license__ = "MIT"
 __title__ = "ytmusicapi"
-__all__ = ["OAuthCredentials", "YTMusic", "setup", "setup_oauth"]
+__all__ = ["LikeStatus", "OAuthCredentials", "YTMusic", "setup", "setup_oauth"]
diff --git a/ytmusicapi/auth/auth_parse.py b/ytmusicapi/auth/auth_parse.py
index 4d9abc1..a9abc82 100644
--- a/ytmusicapi/auth/auth_parse.py
+++ b/ytmusicapi/auth/auth_parse.py
@@ -1,21 +1,21 @@
 import json
 from pathlib import Path
-from typing import Optional, Union
 
 from requests.structures import CaseInsensitiveDict
 
 from ytmusicapi.auth.oauth import OAuthToken
 from ytmusicapi.auth.types import AuthType
 from ytmusicapi.exceptions import YTMusicUserError
+from ytmusicapi.type_alias import JsonDict
 
 
-def parse_auth_str(auth: Union[str, dict]) -> tuple[CaseInsensitiveDict, Optional[Path]]:
+def parse_auth_str(auth: str | JsonDict) -> tuple[CaseInsensitiveDict[str], Path | None]:
     """
 
     :param auth: user-provided auth string or dict
     :return: parsed header dict based on auth, optionally path to file if it auth was a path to a file
     """
-    auth_path: Optional[Path] = None
+    auth_path: Path | None = None
     if isinstance(auth, str):
         auth_str: str = auth
         if auth.startswith("{"):
@@ -31,7 +31,7 @@ def parse_auth_str(auth: Union[str, dict]) -> tuple[CaseInsensitiveDict, Optiona
         return CaseInsensitiveDict(auth), auth_path
 
 
-def determine_auth_type(auth_headers: CaseInsensitiveDict) -> AuthType:
+def determine_auth_type(auth_headers: CaseInsensitiveDict[str]) -> AuthType:
     """
     Determine the type of auth based on auth headers.
 
diff --git a/ytmusicapi/auth/browser.py b/ytmusicapi/auth/browser.py
index 6094945..7b017fe 100644
--- a/ytmusicapi/auth/browser.py
+++ b/ytmusicapi/auth/browser.py
@@ -1,5 +1,5 @@
+import json
 import platform
-from typing import Optional
 
 from requests.structures import CaseInsensitiveDict
 
@@ -7,12 +7,12 @@ from ytmusicapi.exceptions import YTMusicError, YTMusicUserError
 from ytmusicapi.helpers import *
 
 
-def is_browser(headers: CaseInsensitiveDict) -> bool:
+def is_browser(headers: CaseInsensitiveDict[str]) -> bool:
     browser_structure = {"authorization", "cookie"}
     return all(key in headers for key in browser_structure)
 
 
-def setup_browser(filepath: Optional[str] = None, headers_raw: Optional[str] = None) -> str:
+def setup_browser(filepath: str | None = None, headers_raw: str | None = None) -> str:
     contents = []
     if not headers_raw:
         eof = "Ctrl-D" if platform.system() != "Windows" else "'Enter, Ctrl-Z, Enter'"
diff --git a/ytmusicapi/auth/oauth/credentials.py b/ytmusicapi/auth/oauth/credentials.py
index 3d5c581..d042b51 100644
--- a/ytmusicapi/auth/oauth/credentials.py
+++ b/ytmusicapi/auth/oauth/credentials.py
@@ -1,9 +1,9 @@
+import typing
 from abc import ABC, abstractmethod
-from collections.abc import Mapping
 from dataclasses import dataclass
-from typing import Optional
 
 import requests
+from requests import Response
 
 from ytmusicapi.constants import (
     OAUTH_CODE_URL,
@@ -13,6 +13,7 @@ from ytmusicapi.constants import (
 )
 
 from ...exceptions import YTMusicServerError
+from ...type_alias import JsonDict
 from .exceptions import BadOAuthClient, UnauthorizedOAuthClient
 from .models import AuthCodeDict, BaseTokenDict, RefreshableTokenDict
 
@@ -25,7 +26,7 @@ class Credentials(ABC):
     client_secret: str
 
     @abstractmethod
-    def get_code(self) -> Mapping:
+    def get_code(self) -> AuthCodeDict:
         """Method for obtaining a new user auth code. First step of token creation."""
 
     @abstractmethod
@@ -50,8 +51,8 @@ class OAuthCredentials(Credentials):
         self,
         client_id: str,
         client_secret: str,
-        session: Optional[requests.Session] = None,
-        proxies: Optional[dict] = None,
+        session: requests.Session | None = None,
+        proxies: dict[str, str] | None = None,
     ):
         """
         :param client_id: Optional. Set the GoogleAPI ``client_id`` used for auth flows.
@@ -77,9 +78,9 @@ class OAuthCredentials(Credentials):
     def get_code(self) -> AuthCodeDict:
         """Method for obtaining a new user auth code. First step of token creation."""
         code_response = self._send_request(OAUTH_CODE_URL, data={"scope": OAUTH_SCOPE})
-        return code_response.json()
+        return typing.cast(AuthCodeDict, code_response.json())
 
-    def _send_request(self, url, data):
+    def _send_request(self, url: str, data: JsonDict) -> Response:
         """Method for sending post requests with required client_id and User-Agent modifications"""
 
         data.update({"client_id": self.client_id})
@@ -111,7 +112,7 @@ class OAuthCredentials(Credentials):
                 "code": device_code,
             },
         )
-        return response.json()
+        return typing.cast(RefreshableTokenDict, response.json())
 
     def refresh_token(self, refresh_token: str) -> BaseTokenDict:
         """
@@ -130,4 +131,4 @@ class OAuthCredentials(Credentials):
             },
         )
 
-        return response.json()
+        return typing.cast(BaseTokenDict, response.json())
diff --git a/ytmusicapi/auth/oauth/models.py b/ytmusicapi/auth/oauth/models.py
index b22e802..8a3ff88 100644
--- a/ytmusicapi/auth/oauth/models.py
+++ b/ytmusicapi/auth/oauth/models.py
@@ -1,12 +1,12 @@
 """models for oauth authentication"""
 
-from typing import Literal, TypedDict, Union
+from typing import Literal, TypedDict
 
-DefaultScope = Union[str, Literal["https://www.googleapis.com/auth/youtube"]]
-Bearer = Union[str, Literal["Bearer"]]
+DefaultScope = str | Literal["https://www.googleapis.com/auth/youtube"]
+Bearer = str | Literal["Bearer"]
 
 
-class BaseTokenDict(TypedDict):
+class BaseTokenDict(TypedDict, total=False):
     """Limited token. Does not provide a refresh token. Commonly obtained via a token refresh."""
 
     access_token: str  #: str to be used in Authorization header
@@ -15,14 +15,14 @@ class BaseTokenDict(TypedDict):
     token_type: Bearer  #: should be 'Bearer'
 
 
-class RefreshableTokenDict(BaseTokenDict):
+class RefreshableTokenDict(BaseTokenDict, total=False):
     """Entire token. Including refresh. Obtained through token setup."""
 
     expires_at: int  #: UNIX epoch timestamp in seconds
     refresh_token: str  #: str used to obtain new access token upon expiration
 
 
-class AuthCodeDict(TypedDict):
+class AuthCodeDict(TypedDict, total=False):
     """Keys for the json object obtained via code response during auth flow."""
 
     device_code: str  #: code obtained via user confirmation and oauth consent
diff --git a/ytmusicapi/auth/oauth/token.py b/ytmusicapi/auth/oauth/token.py
index e176129..1e6bd2f 100644
--- a/ytmusicapi/auth/oauth/token.py
+++ b/ytmusicapi/auth/oauth/token.py
@@ -1,17 +1,18 @@
 import json
 import time
 import webbrowser
+from collections.abc import KeysView
 from dataclasses import dataclass
 from pathlib import Path
-from typing import Optional
+from typing import Any
 
 from requests.structures import CaseInsensitiveDict
 
-from ytmusicapi.auth.oauth.credentials import Credentials
+from ytmusicapi.auth.oauth.credentials import Credentials, OAuthCredentials
 from ytmusicapi.auth.oauth.models import BaseTokenDict, Bearer, DefaultScope, RefreshableTokenDict
 
 
-@dataclass
+@dataclass(kw_only=True)
 class Token:
     """Base class representation of the YouTubeMusicAPI OAuth token."""
 
@@ -24,7 +25,7 @@ class Token:
     expires_in: int = 0
 
     @staticmethod
-    def members():
+    def members() -> KeysView[str]:
         return Token.__annotations__.keys()
 
     def __repr__(self) -> str:
@@ -51,10 +52,10 @@ class OAuthToken(Token):
     """Wrapper for an OAuth token implementing expiration methods."""
 
     @staticmethod
-    def is_oauth(headers: CaseInsensitiveDict) -> bool:
+    def is_oauth(headers: CaseInsensitiveDict[str]) -> bool:
         return all(key in headers for key in Token.members())
 
-    def update(self, fresh_access: BaseTokenDict):
+    def update(self, fresh_access: BaseTokenDict) -> None:
         """
         Update access_token and expiration attributes with a BaseTokenDict inplace.
         expires_at attribute set using current epoch, avoid expiration desync
@@ -85,12 +86,12 @@ class RefreshingToken(OAuthToken):
     """
 
     #: credentials used for access_token refreshing
-    credentials: Optional[Credentials] = None
+    credentials: Credentials
 
     #: protected/property attribute enables auto writing token values to new file location via setter
-    _local_cache: Optional[Path] = None
+    _local_cache: Path | None = None
 
-    def __getattribute__(self, item):
+    def __getattribute__(self, item: str) -> Any:
         """access token setter to auto-refresh if it is expiring"""
         if item == "access_token" and self.is_expiring:
             fresh = self.credentials.refresh_token(self.refresh_token)
@@ -100,18 +101,18 @@ class RefreshingToken(OAuthToken):
         return super().__getattribute__(item)
 
     @property
-    def local_cache(self) -> Optional[Path]:
+    def local_cache(self) -> Path | None:
         return self._local_cache
 
     @local_cache.setter
-    def local_cache(self, path: Path):
+    def local_cache(self, path: Path) -> None:
         """Update attribute and dump token to new path."""
         self._local_cache = path
         self.store_token()
 
     @classmethod
     def prompt_for_token(
-        cls, credentials: Credentials, open_browser: bool = False, to_file: Optional[str] = None
+        cls, credentials: OAuthCredentials, open_browser: bool = False, to_file: str | None = None
     ) -> "RefreshingToken":
         """
         Method for CLI token creation via user inputs.
@@ -133,7 +134,7 @@ class RefreshingToken(OAuthToken):
             ref_token.local_cache = Path(to_file)
         return ref_token
 
-    def store_token(self, path: Optional[str] = None) -> None:
+    def store_token(self, path: str | None = None) -> None:
         """
         Write token values to json file at specified path, defaulting to self.local_cache.
         Operation does not update instance local_cache attribute.
diff --git a/ytmusicapi/continuations.py b/ytmusicapi/continuations.py
index 0279c4b..2fab2c0 100644
--- a/ytmusicapi/continuations.py
+++ b/ytmusicapi/continuations.py
@@ -1,17 +1,31 @@
-from typing import Any, Optional
+from collections.abc import Callable
+from typing import Any
 
 from ytmusicapi.navigation import nav
+from ytmusicapi.type_alias import (
+    JsonDict,
+    JsonList,
+    ParseFuncDictType,
+    ParseFuncType,
+    RequestFuncBodyType,
+    RequestFuncType,
+)
 
 CONTINUATION_TOKEN = ["continuationItemRenderer", "continuationEndpoint", "continuationCommand", "token"]
 CONTINUATION_ITEMS = ["onResponseReceivedActions", 0, "appendContinuationItemsAction", "continuationItems"]
 
 
-def get_continuation_token(results: list[dict[str, Any]]) -> Optional[str]:
+def get_continuation_token(results: JsonList) -> str | None:
     return nav(results[-1], CONTINUATION_TOKEN, True)
 
 
-def get_continuations_2025(results, limit, request_func, parse_func):
-    items = []
+def get_continuations_2025(
+    results: JsonDict,
+    limit: int | None,
+    request_func: RequestFuncBodyType,
+    parse_func: ParseFuncType,
+) -> JsonList:
+    items: JsonList = []
     continuation_token = get_continuation_token(results["contents"])
     while continuation_token and (limit is None or len(items) < limit):
         response = request_func({"continuation": continuation_token})
@@ -28,17 +42,47 @@ def get_continuations_2025(results, limit, request_func, parse_func):
     return items
 
 
+def get_reloadable_continuations(
+    results: JsonDict,
+    continuation_type: str,
+    limit: int | None,
+    request_func: RequestFuncType,
+    parse_func: ParseFuncType,
+) -> JsonList:
+    """Reloadable continuations are a special case that only exists on the playlists page (suggestions)."""
+    additionalParams = get_reloadable_continuation_params(results)
+    return get_continuations(
+        results, continuation_type, limit, request_func, parse_func, additionalParams=additionalParams
+    )
+
+
 def get_continuations(
-    results, continuation_type, limit, request_func, parse_func, ctoken_path="", reloadable=False
-):
-    items = []
+    results: JsonDict,
+    continuation_type: str,
+    limit: int | None,
+    request_func: RequestFuncType,
+    parse_func: ParseFuncType,
+    ctoken_path: str = "",
+    additionalParams: str | None = None,
+) -> JsonList:
+    """
+
+    :param results: result list from request data
+    :param continuation_type: type of continuation,
+            determines which subkey will be used to navigate the continuation return data
+    :param limit: determines minimum of how many items to retrieve in total.
+            None to retrieve all items until no more continuations are returned
+    :param request_func: the request func to use to get the continuations
+    :param parse_func: the parse func to apply on the returned continuations
+    :param ctoken_path: rarely used specifier applied to retrieve the ctoken ("next<ctoken_path>ContinuationData").
+            Default empty string
+    :param additionalParams: Optional additional params to pass to the request func. Default: use get_continuation_params
+    :return: list of parsed continuation results
+    """
+    items: JsonList = []
     while "continuations" in results and (limit is None or len(items) < limit):
-        additionalParams = (
-            get_reloadable_continuation_params(results)
-            if reloadable
-            else get_continuation_params(results, ctoken_path)
-        )
-        response = request_func(additionalParams)
+        additional_params = additionalParams or get_continuation_params(results, ctoken_path)
+        response = request_func(additional_params)
         if "continuationContents" in response:
             results = response["continuationContents"][continuation_type]
         else:
@@ -52,9 +96,15 @@ def get_continuations(
 
 
 def get_validated_continuations(
-    results, continuation_type, limit, per_page, request_func, parse_func, ctoken_path=""
-):
-    items = []
+    results: JsonDict,
+    continuation_type: str,
+    limit: int,
+    per_page: int,
+    request_func: RequestFuncType,
+    parse_func: ParseFuncType,
+    ctoken_path: str = "",
+) -> JsonList:
+    items: JsonList = []
     while "continuations" in results and len(items) < limit:
         additionalParams = get_continuation_params(results, ctoken_path)
         wrapped_parse_func = lambda raw_response: get_parsed_continuation_items(
@@ -71,26 +121,33 @@ def get_validated_continuations(
     return items
 
 
-def get_parsed_continuation_items(response, parse_func, continuation_type):
+def get_parsed_continuation_items(
+    response: JsonDict, parse_func: ParseFuncType, continuation_type: str
+) -> JsonDict:
     results = response["continuationContents"][continuation_type]
     return {"results": results, "parsed": get_continuation_contents(results, parse_func)}
 
 
-def get_continuation_params(results, ctoken_path=""):
+def get_continuation_params(results: JsonDict, ctoken_path: str = "") -> str:
     ctoken = nav(results, ["continuations", 0, "next" + ctoken_path + "ContinuationData", "continuation"])
     return get_continuation_string(ctoken)
 
 
-def get_reloadable_continuation_params(results):
+def get_reloadable_continuation_params(results: JsonDict) -> str:
     ctoken = nav(results, ["continuations", 0, "reloadContinuationData", "continuation"])
     return get_continuation_string(ctoken)
 
 
-def get_continuation_string(ctoken):
+def get_continuation_string(ctoken: str) -> str:
+    """
+    Returns the continuation string used in the continuation request
+
+    :param ctoken: the unique continuation token
+    """
     return "&ctoken=" + ctoken + "&continuation=" + ctoken
 
 
-def get_continuation_contents(continuation, parse_func):
+def get_continuation_contents(continuation: JsonDict, parse_func: ParseFuncType) -> JsonList:
     for term in ["contents", "items"]:
         if term in continuation:
             return parse_func(continuation[term])
@@ -99,8 +156,12 @@ def get_continuation_contents(continuation, parse_func):
 
 
 def resend_request_until_parsed_response_is_valid(
-    request_func, request_additional_params, parse_func, validate_func, max_retries
-):
+    request_func: RequestFuncType,
+    request_additional_params: str,
+    parse_func: ParseFuncDictType,
+    validate_func: Callable[[dict[str, Any]], bool],
+    max_retries: int,
+) -> JsonDict:
     response = request_func(request_additional_params)
     parsed_object = parse_func(response)
     retry_counter = 0
@@ -114,7 +175,7 @@ def resend_request_until_parsed_response_is_valid(
     return parsed_object
 
 
-def validate_response(response, per_page, limit, current_count):
+def validate_response(response: JsonDict, per_page: int, limit: int, current_count: int) -> bool:
     remaining_items_count = limit - current_count
     expected_items_count = min(per_page, remaining_items_count)
 
diff --git a/ytmusicapi/helpers.py b/ytmusicapi/helpers.py
index 65d39fa..bbd9561 100644
--- a/ytmusicapi/helpers.py
+++ b/ytmusicapi/helpers.py
@@ -3,24 +3,31 @@ import locale
 import re
 import time
 import unicodedata
+from collections.abc import Callable
 from hashlib import sha1
 from http.cookies import SimpleCookie
 
+from requests import Response
+from requests.structures import CaseInsensitiveDict
+
 from ytmusicapi.constants import *
+from ytmusicapi.type_alias import JsonDict
 
 
-def initialize_headers():
-    return {
-        "user-agent": USER_AGENT,
-        "accept": "*/*",
-        "accept-encoding": "gzip, deflate",
-        "content-type": "application/json",
-        "content-encoding": "gzip",
-        "origin": YTM_DOMAIN,
-    }
+def initialize_headers() -> CaseInsensitiveDict[str]:
+    return CaseInsensitiveDict(
+        {
+            "user-agent": USER_AGENT,
+            "accept": "*/*",
+            "accept-encoding": "gzip, deflate",
+            "content-type": "application/json",
+            "content-encoding": "gzip",
+            "origin": YTM_DOMAIN,
+        }
+    )
 
 
-def initialize_context():
+def initialize_context() -> JsonDict:
     return {
         "context": {
             "client": {
@@ -32,7 +39,7 @@ def initialize_context():
     }
 
 
-def get_visitor_id(request_func):
+def get_visitor_id(request_func: Callable[[str], Response]) -> dict[str, str]:
     response = request_func(YTM_DOMAIN)
     matches = re.findall(r"ytcfg\.set\s*\(\s*({.+?})\s*\)\s*;", response.text)
     visitor_id = ""
@@ -42,7 +49,7 @@ def get_visitor_id(request_func):
     return {"X-Goog-Visitor-Id": visitor_id}
 
 
-def sapisid_from_cookie(raw_cookie):
+def sapisid_from_cookie(raw_cookie: str) -> str:
     cookie = SimpleCookie()
     cookie.load(raw_cookie.replace('"', ""))
     return cookie["__Secure-3PAPISID"].value
@@ -50,14 +57,26 @@ def sapisid_from_cookie(raw_cookie):
 
 # SAPISID Hash reverse engineered by
 # https://stackoverflow.com/a/32065323/5726546
-def get_authorization(auth):
+def get_authorization(auth: str) -> str:
+    """Returns SAPISIDHASH value based on headers and current time
+
+    :param auth: SAPISID and Origin value from headers concatenated with space
+    """
     sha_1 = sha1()
     unix_timestamp = str(int(time.time()))
     sha_1.update((unix_timestamp + " " + auth).encode("utf-8"))
     return "SAPISIDHASH " + unix_timestamp + "_" + sha_1.hexdigest()
 
 
-def to_int(string):
+def to_int(string: str) -> int:
+    """Attempts to cast a string to an integer using locale or Python int cast
+
+    :param string: string that can be cast to an integer
+
+    :return Integer if string is a valid integer
+
+    :raise ValueError if string is not a valid integer
+    """
     string = unicodedata.normalize("NFKD", string)
     number_string = re.sub(r"\D", "", string)
     try:
@@ -68,7 +87,7 @@ def to_int(string):
     return int_value
 
 
-def sum_total_duration(item):
+def sum_total_duration(item: JsonDict) -> int:
     if "tracks" not in item:
         return 0
     return sum(
diff --git a/ytmusicapi/mixins/_protocol.py b/ytmusicapi/mixins/_protocol.py
index 0ae2b95..727ad8f 100644
--- a/ytmusicapi/mixins/_protocol.py
+++ b/ytmusicapi/mixins/_protocol.py
@@ -2,13 +2,14 @@
 
 from collections.abc import Iterator
 from contextlib import contextmanager
-from typing import Optional, Protocol
+from typing import Protocol
 
 from requests import Response
 from requests.structures import CaseInsensitiveDict
 
 from ytmusicapi.auth.types import AuthType
 from ytmusicapi.parsers.i18n import Parser
+from ytmusicapi.type_alias import JsonDict
 
 
 class MixinProtocol(Protocol):
@@ -18,15 +19,15 @@ class MixinProtocol(Protocol):
 
     parser: Parser
 
-    proxies: Optional[dict[str, str]]
+    proxies: dict[str, str] | None
 
     def _check_auth(self) -> None:
         """checks if self has authentication"""
 
-    def _send_request(self, endpoint: str, body: dict, additionalParams: str = "") -> dict:
+    def _send_request(self, endpoint: str, body: JsonDict, additionalParams: str = "") -> JsonDict:
         """for sending post requests to YouTube Music"""
 
-    def _send_get_request(self, url: str, params: Optional[dict] = None) -> Response:
+    def _send_get_request(self, url: str, params: JsonDict | None = None) -> Response:
         """for sending get requests to YouTube Music"""
 
     @contextmanager
diff --git a/ytmusicapi/mixins/_utils.py b/ytmusicapi/mixins/_utils.py
index 9a262db..b1fc712 100644
--- a/ytmusicapi/mixins/_utils.py
+++ b/ytmusicapi/mixins/_utils.py
@@ -3,22 +3,30 @@ from datetime import date
 from typing import Literal
 
 from ytmusicapi.exceptions import YTMusicUserError
+from ytmusicapi.models.content.enums import LikeStatus
 
 LibraryOrderType = Literal["a_to_z", "z_to_a", "recently_added"]
 
 
-def prepare_like_endpoint(rating):
-    if rating == "LIKE":
-        return "like/like"
-    elif rating == "DISLIKE":
-        return "like/dislike"
-    elif rating == "INDIFFERENT":
-        return "like/removelike"
-    else:
-        return None
+def prepare_like_endpoint(rating: str | LikeStatus) -> str:
+    match rating:
+        case LikeStatus.LIKE:
+            return "like/like"
+        case LikeStatus.DISLIKE:
+            return "like/dislike"
+        case LikeStatus.INDIFFERENT:
+            return "like/removelike"
+        case _:
+            raise YTMusicUserError(
+                f"Invalid rating provided. Use one of {[e.value for e in LikeStatus.__members__.values()]}."
+            )
 
 
-def validate_order_parameter(order):
+def validate_order_parameter(order: LibraryOrderType | None) -> None:
+    """Validate the provided order, if any
+
+    :raises YTMusicUserError: if the provided order is invalid
+    """
     orders = ["a_to_z", "z_to_a", "recently_added"]
     if order and order not in orders:
         raise YTMusicUserError(
@@ -27,20 +35,28 @@ def validate_order_parameter(order):
         )
 
 
-def prepare_order_params(order: LibraryOrderType):
+def prepare_order_params(order: LibraryOrderType) -> str:
+    """Returns request params belonging to a specific sorting order."""
     orders = ["a_to_z", "z_to_a", "recently_added"]
-    if order is not None:
-        # determine order_params via `.contents.singleColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents[1].itemSectionRenderer.header.itemSectionTabbedHeaderRenderer.endItems[1].dropdownRenderer.entries[].dropdownItemRenderer.onSelectCommand.browseEndpoint.params` of `/youtubei/v1/browse` response
-        order_params = ["ggMGKgQIARAA", "ggMGKgQIARAB", "ggMGKgQIABAB"]
-        return order_params[orders.index(order)]
+    # determine order_params via `.contents.singleColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents[1].itemSectionRenderer.header.itemSectionTabbedHeaderRenderer.endItems[1].dropdownRenderer.entries[].dropdownItemRenderer.onSelectCommand.browseEndpoint.params` of `/youtubei/v1/browse` response
+    order_params = ["ggMGKgQIARAA", "ggMGKgQIARAB", "ggMGKgQIABAB"]
+    return order_params[orders.index(order)]
+
 
+def html_to_txt(html_text: str) -> str:
+    """
+    Sanitize tags from html
 
-def html_to_txt(html_text):
+    :param html_text: String containing html tags.
+    :return: String without < > characters
+    """
     tags = re.findall("<[^>]+>", html_text)
     for tag in tags:
         html_text = html_text.replace(tag, "")
     return html_text
 
 
-def get_datestamp():
+def get_datestamp() -> int:
+    """Returns the number of days since January 1, 1970.
+    Currently only used for the signature timestamp in :py:func:`get_song`."""
     return (date.today() - date.fromtimestamp(0)).days
diff --git a/ytmusicapi/mixins/browsing.py b/ytmusicapi/mixins/browsing.py
index 31eb6b2..d962c28 100644
--- a/ytmusicapi/mixins/browsing.py
+++ b/ytmusicapi/mixins/browsing.py
@@ -1,6 +1,7 @@
 import re
 import warnings
-from typing import Any, Optional, Union, cast, overload
+from collections.abc import Callable
+from typing import Literal, cast, overload
 
 from ytmusicapi.continuations import (
     get_continuations,
@@ -18,6 +19,7 @@ from ytmusicapi.parsers.browsing import (
 )
 from ytmusicapi.parsers.library import parse_albums
 from ytmusicapi.parsers.playlists import parse_playlist_items
+from ytmusicapi.type_alias import JsonDict, JsonList, ParseFuncType, RequestFuncType
 
 from ..exceptions import YTMusicError, YTMusicUserError
 from ..navigation import *
@@ -26,13 +28,13 @@ from ._utils import get_datestamp
 
 
 class BrowsingMixin(MixinProtocol):
-    def get_home(self, limit=3) -> list[dict]:
+    def get_home(self, limit: int = 3) -> JsonList:
         """
         Get the home page.
         The home page is structured as titled rows, returning 3 rows of music suggestions at a time.
         Content varies and may contain artist, album, song or playlist suggestions, sometimes mixed within the same row
 
-        :param limit: Number of rows to return
+        :param limit: Number of rows on the home page to return
         :return: List of dictionaries keyed with 'title' text and 'contents' list
 
         Example list::
@@ -120,9 +122,11 @@ class BrowsingMixin(MixinProtocol):
 
         section_list = nav(response, [*SINGLE_COLUMN_TAB, "sectionListRenderer"])
         if "continuations" in section_list:
-            request_func = lambda additionalParams: self._send_request(endpoint, body, additionalParams)
+            request_func: RequestFuncType = lambda additionalParams: self._send_request(
+                endpoint, body, additionalParams
+            )
 
-            parse_func = lambda contents: parse_mixed_content(contents)
+            parse_func: Callable[[JsonList], JsonList] = lambda contents: parse_mixed_content(contents)
 
             home.extend(
                 get_continuations(
@@ -132,7 +136,7 @@ class BrowsingMixin(MixinProtocol):
 
         return home
 
-    def get_artist(self, channelId: str) -> dict:
+    def get_artist(self, channelId: str) -> JsonDict:
         """
         Get information about an artist and their top releases (songs,
         albums, singles, videos, and related artists). The top lists
@@ -247,7 +251,7 @@ class BrowsingMixin(MixinProtocol):
         response = self._send_request(endpoint, body)
         results = nav(response, SINGLE_COLUMN_TAB + SECTION_LIST)
 
-        artist: dict[str, Any] = {"description": None, "views": None}
+        artist: JsonDict = {"description": None, "views": None}
         header = response["header"]["musicImmersiveHeaderRenderer"]
         artist["name"] = nav(header, TITLE_TEXT)
         descriptionShelf = find_object_by_key(results, DESCRIPTION_SHELF[0], is_key=True)
@@ -278,8 +282,8 @@ class BrowsingMixin(MixinProtocol):
     ArtistOrderType = Literal["Recency", "Popularity", "Alphabetical order"]
 
     def get_artist_albums(
-        self, channelId: str, params: str, limit: Optional[int] = 100, order: Optional[ArtistOrderType] = None
-    ) -> list[dict]:
+        self, channelId: str, params: str, limit: int | None = 100, order: ArtistOrderType | None = None
+    ) -> JsonList:
         """
         Get the full list of an artist's albums, singles or shows
 
@@ -295,8 +299,10 @@ class BrowsingMixin(MixinProtocol):
         endpoint = "browse"
         response = self._send_request(endpoint, body)
 
-        request_func = lambda additionalParams: self._send_request(endpoint, body, additionalParams)
-        parse_func = lambda contents: parse_albums(contents)
+        request_func: RequestFuncType = lambda additionalParams: self._send_request(
+            endpoint, body, additionalParams
+        )
+        parse_func: ParseFuncType = lambda contents: parse_albums(contents)
 
         if order:
             # pick the correct continuation from response depending on the order chosen
@@ -358,7 +364,7 @@ class BrowsingMixin(MixinProtocol):
 
         return albums
 
-    def get_user(self, channelId: str) -> dict:
+    def get_user(self, channelId: str) -> JsonDict:
         """
         Retrieve a user's page. A user may own videos or playlists.
 
@@ -423,7 +429,7 @@ class BrowsingMixin(MixinProtocol):
         user.update(self.parser.parse_channel_contents(results))
         return user
 
-    def get_user_playlists(self, channelId: str, params: str) -> list[dict]:
+    def get_user_playlists(self, channelId: str, params: str) -> JsonList:
         """
         Retrieve a list of playlists for a given user.
         Call this function again with the returned ``params`` to get the full list.
@@ -444,7 +450,7 @@ class BrowsingMixin(MixinProtocol):
 
         return user_playlists
 
-    def get_user_videos(self, channelId: str, params: str) -> list[dict]:
+    def get_user_videos(self, channelId: str, params: str) -> JsonList:
         """
         Retrieve a list of videos for a given user.
         Call this function again with the returned ``params`` to get the full list.
@@ -465,7 +471,7 @@ class BrowsingMixin(MixinProtocol):
 
         return user_videos
 
-    def get_album_browse_id(self, audioPlaylistId: str) -> Optional[str]:
+    def get_album_browse_id(self, audioPlaylistId: str) -> str | None:
         """
         Get an album's browseId based on its audioPlaylistId
 
@@ -486,7 +492,7 @@ class BrowsingMixin(MixinProtocol):
             browse_id = matches.group().strip('"')
         return browse_id
 
-    def get_album(self, browseId: str) -> dict:
+    def get_album(self, browseId: str) -> JsonDict:
         """
         Get information and tracks of an album
 
@@ -553,7 +559,7 @@ class BrowsingMixin(MixinProtocol):
         body = {"browseId": browseId}
         endpoint = "browse"
         response = self._send_request(endpoint, body)
-        album = parse_album_header_2024(response)
+        album: JsonDict = parse_album_header_2024(response)
 
         results = nav(response, [*TWO_COLUMN_RENDERER, "secondaryContents", *SECTION_LIST_ITEM, *MUSIC_SHELF])
         album["tracks"] = parse_playlist_items(results["contents"], is_album=True)
@@ -570,7 +576,7 @@ class BrowsingMixin(MixinProtocol):
 
         return album
 
-    def get_song(self, videoId: str, signatureTimestamp: Optional[int] = None) -> dict:
+    def get_song(self, videoId: str, signatureTimestamp: int | None = None) -> JsonDict:
         """
         Returns metadata and streaming information about a song or video.
 
@@ -756,7 +762,7 @@ class BrowsingMixin(MixinProtocol):
                 del response[k]
         return response
 
-    def get_song_related(self, browseId: str):
+    def get_song_related(self, browseId: str) -> JsonList:
         """
         Gets related content for a song. Equivalent to the content
         shown in the "Related" tab of the watch panel.
@@ -838,18 +844,14 @@ class BrowsingMixin(MixinProtocol):
         return parse_mixed_content(sections)
 
     @overload
-    def get_lyrics(self, browseId: str, timestamps: Literal[False] = False) -> Optional[Lyrics]:
+    def get_lyrics(self, browseId: str, timestamps: Literal[False] = False) -> Lyrics | None:
         """overload for mypy only"""
 
     @overload
-    def get_lyrics(
-        self, browseId: str, timestamps: Literal[True] = True
-    ) -> Optional[Union[Lyrics, TimedLyrics]]:
+    def get_lyrics(self, browseId: str, timestamps: Literal[True] = True) -> Lyrics | TimedLyrics | None:
         """overload for mypy only"""
 
-    def get_lyrics(
-        self, browseId: str, timestamps: Optional[bool] = False
-    ) -> Optional[Union[Lyrics, TimedLyrics]]:
+    def get_lyrics(self, browseId: str, timestamps: bool | None = False) -> Lyrics | TimedLyrics | None:
         """
         Returns lyrics of a song or video. When `timestamps` is set, lyrics are returned with
         timestamps, if available.
@@ -901,7 +903,7 @@ class BrowsingMixin(MixinProtocol):
             response = self._send_request("browse", {"browseId": browseId})
 
         # unpack the response
-        lyrics: Union[Lyrics, TimedLyrics]
+        lyrics: Lyrics | TimedLyrics
         if timestamps and (data := nav(response, TIMESTAMPED_LYRICS, True)) is not None:
             # we got lyrics with timestamps
             assert isinstance(data, dict)
@@ -928,7 +930,7 @@ class BrowsingMixin(MixinProtocol):
                 hasTimestamps=False,
             )
 
-        return cast(Union[Lyrics, TimedLyrics], lyrics)
+        return cast(Lyrics | TimedLyrics, lyrics)
 
     def get_basejs_url(self) -> str:
         """
@@ -943,7 +945,7 @@ class BrowsingMixin(MixinProtocol):
 
         return YTM_DOMAIN + match.group(1)
 
-    def get_signatureTimestamp(self, url: Optional[str] = None) -> int:
+    def get_signatureTimestamp(self, url: str | None = None) -> int:
         """
         Fetch the `base.js` script from YouTube Music and parse out the
         ``signatureTimestamp`` for use with :py:func:`get_song`.
@@ -961,9 +963,9 @@ class BrowsingMixin(MixinProtocol):
 
         return int(match.group(1))
 
-    def get_tasteprofile(self) -> dict:
+    def get_tasteprofile(self) -> JsonDict:
         """
-        Fetches suggested artists from taste profile (music.youtube.com/tasteprofile).
+        Fetches suggested artists from taste profile (music.youtube.com/tasteprofile). Must be authenticated.
         Tasteprofile allows users to pick artists to update their recommendations.
         Only returns a list of suggested artists, not the actual list of selected entries
 
@@ -979,7 +981,7 @@ class BrowsingMixin(MixinProtocol):
             }
 
         """
-
+        self._check_auth()
         response = self._send_request("browse", {"browseId": "FEmusic_tastebuilder"})
         profiles = nav(response, TASTE_PROFILE_ITEMS)
 
@@ -993,7 +995,7 @@ class BrowsingMixin(MixinProtocol):
                 }
         return taste_profiles
 
-    def set_tasteprofile(self, artists: list[str], taste_profile: Optional[dict] = None) -> None:
+    def set_tasteprofile(self, artists: list[str], taste_profile: JsonDict | None = None) -> None:
         """
         Favorites artists to see more recommendations from the artist.
         Use :py:func:`get_tasteprofile` to see which artists are available to be recommended
diff --git a/ytmusicapi/mixins/explore.py b/ytmusicapi/mixins/explore.py
index 5b33717..0444832 100644
--- a/ytmusicapi/mixins/explore.py
+++ b/ytmusicapi/mixins/explore.py
@@ -1,11 +1,13 @@
+from collections.abc import Callable
 from typing import Any
 
 from ytmusicapi.mixins._protocol import MixinProtocol
 from ytmusicapi.parsers.explore import *
+from ytmusicapi.type_alias import JsonDict, JsonList, ParseFuncDictType
 
 
 class ExploreMixin(MixinProtocol):
-    def get_mood_categories(self) -> dict:
+    def get_mood_categories(self) -> JsonDict:
         """
         Fetch "Moods & Genres" categories from YouTube Music.
 
@@ -50,7 +52,7 @@ class ExploreMixin(MixinProtocol):
             }
 
         """
-        sections: dict[str, Any] = {}
+        sections: JsonDict = {}
         response = self._send_request("browse", {"browseId": "FEmusic_moods_and_genres"})
         for section in nav(response, SINGLE_COLUMN_TAB + SECTION_LIST):
             title = nav(section, [*GRID, "header", "gridHeaderRenderer", *TITLE_TEXT])
@@ -62,7 +64,7 @@ class ExploreMixin(MixinProtocol):
 
         return sections
 
-    def get_mood_playlists(self, params: str) -> list[dict]:
+    def get_mood_playlists(self, params: str) -> JsonList:
         """
         Retrieve a list of playlists for a given "Moods & Genres" category.
 
@@ -88,7 +90,7 @@ class ExploreMixin(MixinProtocol):
 
         return playlists
 
-    def get_charts(self, country: str = "ZZ") -> dict:
+    def get_charts(self, country: str = "ZZ") -> JsonDict:
         """
         Get latest charts data from YouTube Music: Top songs, top videos, top artists and top trending videos.
         Global charts have no Trending section, US charts have an extra Genres section with some Genre charts.
@@ -188,13 +190,13 @@ class ExploreMixin(MixinProtocol):
             }
 
         """
-        body: dict[str, Any] = {"browseId": "FEmusic_charts"}
+        body: JsonDict = {"browseId": "FEmusic_charts"}
         if country:
             body["formData"] = {"selectedValues": [country]}
 
         response = self._send_request("browse", body)
         results = nav(response, SINGLE_COLUMN_TAB + SECTION_LIST)
-        charts: dict[str, Any] = {"countries": {}}
+        charts: JsonDict = {"countries": {}}
         menu = nav(
             results[0],
             [
@@ -233,8 +235,10 @@ class ExploreMixin(MixinProtocol):
         if has_trending:
             charts_categories.append("trending")
 
-        parse_chart = lambda i, parse_func, key: parse_content_list(
-            nav(results[i + has_songs], CAROUSEL_CONTENTS), parse_func, key
+        parse_chart: Callable[[int, ParseFuncDictType, str], list[dict[str, Any]]] = (
+            lambda index, parse_func, key: parse_content_list(
+                nav(results[index + has_songs], CAROUSEL_CONTENTS), parse_func, key
+            )
         )
         for i, c in enumerate(charts_categories):
             charts[c] = {
diff --git a/ytmusicapi/mixins/library.py b/ytmusicapi/mixins/library.py
index dc67eca..15f77f2 100644
--- a/ytmusicapi/mixins/library.py
+++ b/ytmusicapi/mixins/library.py
@@ -1,9 +1,15 @@
+from collections.abc import Callable
 from random import randint
-from typing import Optional
+
+from requests import Response
 
 from ytmusicapi.continuations import *
+from ytmusicapi.exceptions import YTMusicUserError
+from ytmusicapi.models.content.enums import LikeStatus
 from ytmusicapi.parsers.browsing import *
 from ytmusicapi.parsers.library import *
+from ytmusicapi.parsers.playlists import parse_playlist_items
+from ytmusicapi.type_alias import JsonDict, JsonList, ParseFuncDictType, ParseFuncType, RequestFuncType
 
 from ..exceptions import YTMusicServerError
 from ._protocol import MixinProtocol
@@ -11,7 +17,7 @@ from ._utils import *
 
 
 class LibraryMixin(MixinProtocol):
-    def get_library_playlists(self, limit: Optional[int] = 25) -> list[dict]:
+    def get_library_playlists(self, limit: int | None = 25) -> JsonList:
         """
         Retrieves the playlists in the user's library.
 
@@ -33,11 +39,15 @@ class LibraryMixin(MixinProtocol):
         response = self._send_request(endpoint, body)
 
         results = get_library_contents(response, GRID)
+        if results is None:
+            return []
         playlists = parse_content_list(results["items"][1:], parse_playlist)
 
         if "continuations" in results:
-            request_func = lambda additionalParams: self._send_request(endpoint, body, additionalParams)
-            parse_func = lambda contents: parse_content_list(contents, parse_playlist)
+            request_func: RequestFuncType = lambda additionalParams: self._send_request(
+                endpoint, body, additionalParams
+            )
+            parse_func: ParseFuncType = lambda contents: parse_content_list(contents, parse_playlist)
             remaining_limit = None if limit is None else (limit - len(playlists))
             playlists.extend(
                 get_continuations(results, "gridContinuation", remaining_limit, request_func, parse_func)
@@ -46,8 +56,8 @@ class LibraryMixin(MixinProtocol):
         return playlists
 
     def get_library_songs(
-        self, limit: int = 25, validate_responses: bool = False, order: Optional[LibraryOrderType] = None
-    ) -> list[dict]:
+        self, limit: int = 25, validate_responses: bool = False, order: LibraryOrderType | None = None
+    ) -> JsonList:
         """
         Gets the songs in the user's library (liked videos are not included).
         To get liked songs and videos, use :py:func:`get_liked_songs`
@@ -66,22 +76,24 @@ class LibraryMixin(MixinProtocol):
         endpoint = "browse"
         per_page = 25
 
-        request_func = lambda additionalParams: self._send_request(endpoint, body)
-        parse_func = lambda raw_response: parse_library_songs(raw_response)
+        request_func: RequestFuncType = lambda additionalParams: self._send_request(endpoint, body)
+        parse_func: ParseFuncDictType = lambda raw_response: parse_library_songs(raw_response)
 
         if validate_responses and limit is None:
             raise YTMusicUserError("Validation is not supported without a limit parameter.")
 
         if validate_responses:
-            validate_func = lambda parsed: validate_response(parsed, per_page, limit, 0)
+            validate_func: Callable[[JsonDict], bool] = lambda parsed: validate_response(
+                parsed, per_page, limit, 0
+            )
             response = resend_request_until_parsed_response_is_valid(
-                request_func, None, parse_func, validate_func, 3
+                request_func, "", parse_func, validate_func, 3
             )
         else:
-            response = parse_func(request_func(None))
+            response = parse_func(request_func(""))
 
         results = response["results"]
-        songs = response["parsed"]
+        songs: JsonList | None = response["parsed"]
         if songs is None:
             return []
 
@@ -116,7 +128,7 @@ class LibraryMixin(MixinProtocol):
 
         return songs
 
-    def get_library_albums(self, limit: int = 25, order: Optional[LibraryOrderType] = None) -> list[dict]:
+    def get_library_albums(self, limit: int = 25, order: LibraryOrderType | None = None) -> JsonList:
         """
         Gets the albums in the user's library.
 
@@ -151,7 +163,7 @@ class LibraryMixin(MixinProtocol):
             response, lambda additionalParams: self._send_request(endpoint, body, additionalParams), limit
         )
 
-    def get_library_artists(self, limit: int = 25, order: Optional[LibraryOrderType] = None) -> list[dict]:
+    def get_library_artists(self, limit: int = 25, order: LibraryOrderType | None = None) -> JsonList:
         """
         Gets the artists of the songs in the user's library.
 
@@ -179,9 +191,7 @@ class LibraryMixin(MixinProtocol):
             response, lambda additionalParams: self._send_request(endpoint, body, additionalParams), limit
         )
 
-    def get_library_subscriptions(
-        self, limit: int = 25, order: Optional[LibraryOrderType] = None
-    ) -> list[dict]:
+    def get_library_subscriptions(self, limit: int = 25, order: LibraryOrderType | None = None) -> JsonList:
         """
         Gets the artists the user has subscribed to.
 
@@ -200,7 +210,7 @@ class LibraryMixin(MixinProtocol):
             response, lambda additionalParams: self._send_request(endpoint, body, additionalParams), limit
         )
 
-    def get_library_podcasts(self, limit: int = 25, order: Optional[LibraryOrderType] = None) -> list[dict]:
+    def get_library_podcasts(self, limit: int = 25, order: LibraryOrderType | None = None) -> JsonList:
         """
         Get podcasts the user has added to the library
 
@@ -246,7 +256,7 @@ class LibraryMixin(MixinProtocol):
             response, lambda additionalParams: self._send_request(endpoint, body, additionalParams), limit
         )
 
-    def get_library_channels(self, limit: int = 25, order: Optional[LibraryOrderType] = None) -> list[dict]:
+    def get_library_channels(self, limit: int = 25, order: LibraryOrderType | None = None) -> JsonList:
         """
         Get channels the user has added to the library
 
@@ -282,7 +292,7 @@ class LibraryMixin(MixinProtocol):
             response, lambda additionalParams: self._send_request(endpoint, body, additionalParams), limit
         )
 
-    def get_history(self) -> list[dict]:
+    def get_history(self) -> JsonList:
         """
         Gets your play history in reverse chronological order
 
@@ -309,7 +319,7 @@ class LibraryMixin(MixinProtocol):
 
         return songs
 
-    def add_history_item(self, song):
+    def add_history_item(self, song: JsonDict) -> Response:
         """
         Add an item to the account's history using the playbackTracking URI
         obtained from :py:func:`get_song`. A ``204`` return code indicates success.
@@ -333,7 +343,7 @@ class LibraryMixin(MixinProtocol):
         params = {"ver": 2, "c": "WEB_REMIX", "cpn": cpn}
         return self._send_get_request(url, params)
 
-    def remove_history_items(self, feedbackTokens: list[str]) -> dict:  # pragma: no cover
+    def remove_history_items(self, feedbackTokens: list[str]) -> JsonDict:  # pragma: no cover
         """
         Remove an item from the account's history. This method does currently not work with brand accounts
 
@@ -347,7 +357,7 @@ class LibraryMixin(MixinProtocol):
 
         return response
 
-    def rate_song(self, videoId: str, rating: str = "INDIFFERENT") -> Optional[dict]:
+    def rate_song(self, videoId: str, rating: LikeStatus = LikeStatus.INDIFFERENT) -> JsonDict | None:
         """
         Rates a song ("thumbs up"/"thumbs down" interactions on YouTube Music)
 
@@ -357,16 +367,14 @@ class LibraryMixin(MixinProtocol):
           | ``INDIFFERENT`` removes the previous rating and assigns no rating
 
         :return: Full response
+        :raises: YTMusicUserError if an invalid rating ir povided
         """
         self._check_auth()
         body = {"target": {"videoId": videoId}}
         endpoint = prepare_like_endpoint(rating)
-        if endpoint is None:
-            return None
-
         return self._send_request(endpoint, body)
 
-    def edit_song_library_status(self, feedbackTokens: Optional[list[str]] = None) -> dict:
+    def edit_song_library_status(self, feedbackTokens: list[str] | None = None) -> JsonDict:
         """
         Adds or removes a song from your library depending on the token provided.
 
@@ -379,7 +387,7 @@ class LibraryMixin(MixinProtocol):
         endpoint = "feedback"
         return self._send_request(endpoint, body)
 
-    def rate_playlist(self, playlistId: str, rating: str = "INDIFFERENT") -> dict:
+    def rate_playlist(self, playlistId: str, rating: LikeStatus = LikeStatus.INDIFFERENT) -> JsonDict:
         """
         Rates a playlist/album ("Add to library"/"Remove from library" interactions on YouTube Music)
         You can also dislike a playlist/album, which has an effect on your recommendations
@@ -390,13 +398,14 @@ class LibraryMixin(MixinProtocol):
           | ``INDIFFERENT`` removes the playlist/album from the library
 
         :return: Full response
+        :raises: YTMusicUserError if an invalid rating is provided
         """
         self._check_auth()
         body = {"target": {"playlistId": playlistId}}
         endpoint = prepare_like_endpoint(rating)
-        return endpoint if not endpoint else self._send_request(endpoint, body)
+        return self._send_request(endpoint, body)
 
-    def subscribe_artists(self, channelIds: list[str]) -> dict:
+    def subscribe_artists(self, channelIds: list[str]) -> JsonDict:
         """
         Subscribe to artists. Adds the artists to your library
 
@@ -408,7 +417,7 @@ class LibraryMixin(MixinProtocol):
         endpoint = "subscription/subscribe"
         return self._send_request(endpoint, body)
 
-    def unsubscribe_artists(self, channelIds: list[str]) -> dict:
+    def unsubscribe_artists(self, channelIds: list[str]) -> JsonDict:
         """
         Unsubscribe from artists. Removes the artists from your library
 
@@ -420,7 +429,7 @@ class LibraryMixin(MixinProtocol):
         endpoint = "subscription/unsubscribe"
         return self._send_request(endpoint, body)
 
-    def get_account_info(self) -> dict:
+    def get_account_info(self) -> JsonDict:
         """
         Gets information about the currently authenticated user's account.
 
diff --git a/ytmusicapi/mixins/playlists.py b/ytmusicapi/mixins/playlists.py
index 1eccfec..f2b5ee5 100644
--- a/ytmusicapi/mixins/playlists.py
+++ b/ytmusicapi/mixins/playlists.py
@@ -1,10 +1,10 @@
-from typing import Any, Optional, Union
-
 from ytmusicapi.continuations import *
+from ytmusicapi.exceptions import YTMusicUserError
 from ytmusicapi.helpers import sum_total_duration
 from ytmusicapi.navigation import *
 from ytmusicapi.parsers.browsing import parse_content_list, parse_playlist
 from ytmusicapi.parsers.playlists import *
+from ytmusicapi.type_alias import JsonDict, JsonList, ParseFuncType, RequestFuncBodyType, RequestFuncType
 
 from ._protocol import MixinProtocol
 from ._utils import *
@@ -12,8 +12,8 @@ from ._utils import *
 
 class PlaylistsMixin(MixinProtocol):
     def get_playlist(
-        self, playlistId: str, limit: Optional[int] = 100, related: bool = False, suggestions_limit: int = 0
-    ) -> dict:
+        self, playlistId: str, limit: int | None = 100, related: bool = False, suggestions_limit: int = 0
+    ) -> JsonDict:
         """
         Returns a list of playlist items
 
@@ -107,16 +107,18 @@ class PlaylistsMixin(MixinProtocol):
         browseId = "VL" + playlistId if not playlistId.startswith("VL") else playlistId
         body = {"browseId": browseId}
         endpoint = "browse"
-        request_func = lambda additionalParams: self._send_request(endpoint, body, additionalParams)
+        request_func: RequestFuncType = lambda additionalParams: self._send_request(
+            endpoint, body, additionalParams
+        )
         response = request_func("")
 
-        request_func_continuations = lambda body: self._send_request(endpoint, body)
+        request_func_continuations: RequestFuncBodyType = lambda body: self._send_request(endpoint, body)
         if playlistId.startswith("OLA") or playlistId.startswith("VLOLA"):
             return parse_audio_playlist(response, limit, request_func_continuations)
 
         header_data = nav(response, [*TWO_COLUMN_RENDERER, *TAB_CONTENT, *SECTION_LIST_ITEM])
         section_list = nav(response, [*TWO_COLUMN_RENDERER, "secondaryContents", *SECTION])
-        playlist: dict = {}
+        playlist: JsonDict = {}
         playlist["owned"] = EDITABLE_PLAYLIST_DETAIL_HEADER[0] in header_data
         if not playlist["owned"]:
             header = nav(header_data, RESPONSIVE_HEADER)
@@ -149,7 +151,7 @@ class PlaylistsMixin(MixinProtocol):
         if "continuations" in section_list:
             additionalParams = get_continuation_params(section_list)
             if playlist["owned"] and (suggestions_limit > 0 or related):
-                parse_func = lambda results: parse_playlist_items(results)
+                parse_func: ParseFuncType = lambda results: parse_playlist_items(results)
                 suggested = request_func(additionalParams)
                 continuation = nav(suggested, SECTION_LIST_CONTINUATION)
                 additionalParams = get_continuation_params(continuation)
@@ -158,13 +160,12 @@ class PlaylistsMixin(MixinProtocol):
 
                 parse_func = lambda results: parse_playlist_items(results)
                 playlist["suggestions"].extend(
-                    get_continuations(
+                    get_reloadable_continuations(
                         suggestions_shelf,
                         "musicShelfContinuation",
                         suggestions_limit - len(playlist["suggestions"]),
                         request_func,
                         parse_func,
-                        reloadable=True,
                     )
                 )
 
@@ -190,7 +191,7 @@ class PlaylistsMixin(MixinProtocol):
         playlist["duration_seconds"] = sum_total_duration(playlist)
         return playlist
 
-    def get_liked_songs(self, limit: int = 100) -> dict:
+    def get_liked_songs(self, limit: int = 100) -> JsonDict:
         """
         Gets playlist items for the 'Liked Songs' playlist
 
@@ -199,7 +200,7 @@ class PlaylistsMixin(MixinProtocol):
         """
         return self.get_playlist("LM", limit)
 
-    def get_saved_episodes(self, limit: int = 100) -> dict:
+    def get_saved_episodes(self, limit: int = 100) -> JsonDict:
         """
         Gets playlist items for the 'Liked Songs' playlist
 
@@ -213,9 +214,9 @@ class PlaylistsMixin(MixinProtocol):
         title: str,
         description: str,
         privacy_status: str = "PRIVATE",
-        video_ids: Optional[list] = None,
-        source_playlist: Optional[str] = None,
-    ) -> Union[str, dict]:
+        video_ids: list[str] | None = None,
+        source_playlist: str | None = None,
+    ) -> str | JsonDict:
         """
         Creates a new empty playlist and returns its id.
 
@@ -234,7 +235,7 @@ class PlaylistsMixin(MixinProtocol):
             msg = f"{title} contains invalid characters: {', '.join(invalid_characters_found)}"
             raise YTMusicUserError(msg)
 
-        body = {
+        body: JsonDict = {
             "title": title,
             "description": html_to_txt(description),  # YT does not allow HTML tags
             "privacyStatus": privacy_status,
@@ -252,13 +253,13 @@ class PlaylistsMixin(MixinProtocol):
     def edit_playlist(
         self,
         playlistId: str,
-        title: Optional[str] = None,
-        description: Optional[str] = None,
-        privacyStatus: Optional[str] = None,
-        moveItem: Optional[Union[str, tuple[str, str]]] = None,
-        addPlaylistId: Optional[str] = None,
-        addToTop: Optional[bool] = None,
-    ) -> Union[str, dict]:
+        title: str | None = None,
+        description: str | None = None,
+        privacyStatus: str | None = None,
+        moveItem: str | tuple[str, str] | None = None,
+        addPlaylistId: str | None = None,
+        addToTop: bool | None = None,
+    ) -> str | JsonDict:
         """
         Edit title, description or privacyStatus of a playlist.
         You may also move an item within a playlist or append another playlist to this playlist.
@@ -275,7 +276,7 @@ class PlaylistsMixin(MixinProtocol):
         :return: Status String or full response
         """
         self._check_auth()
-        body: dict[str, Any] = {"playlistId": validate_playlist_id(playlistId)}
+        body: JsonDict = {"playlistId": validate_playlist_id(playlistId)}
         actions = []
         if title:
             actions.append({"action": "ACTION_SET_PLAYLIST_NAME", "playlistName": title})
@@ -309,7 +310,7 @@ class PlaylistsMixin(MixinProtocol):
         response = self._send_request(endpoint, body)
         return response["status"] if "status" in response else response
 
-    def delete_playlist(self, playlistId: str) -> Union[str, dict]:
+    def delete_playlist(self, playlistId: str) -> str | JsonDict:
         """
         Delete a playlist.
 
@@ -325,10 +326,10 @@ class PlaylistsMixin(MixinProtocol):
     def add_playlist_items(
         self,
         playlistId: str,
-        videoIds: Optional[list[str]] = None,
-        source_playlist: Optional[str] = None,
+        videoIds: list[str] | None = None,
+        source_playlist: str | None = None,
         duplicates: bool = False,
-    ) -> Union[str, dict]:
+    ) -> str | JsonDict:
         """
         Add songs to an existing playlist
 
@@ -339,7 +340,7 @@ class PlaylistsMixin(MixinProtocol):
         :return: Status String and a dict containing the new setVideoId for each videoId or full response
         """
         self._check_auth()
-        body: dict[str, Any] = {"playlistId": validate_playlist_id(playlistId), "actions": []}
+        body: JsonDict = {"playlistId": validate_playlist_id(playlistId), "actions": []}
         if not videoIds and not source_playlist:
             raise YTMusicUserError(
                 "You must provide either videoIds or a source_playlist to add to the playlist"
@@ -371,7 +372,7 @@ class PlaylistsMixin(MixinProtocol):
         else:
             return response
 
-    def remove_playlist_items(self, playlistId: str, videos: list[dict]) -> Union[str, dict]:
+    def remove_playlist_items(self, playlistId: str, videos: JsonList) -> str | JsonDict:
         """
         Remove songs from an existing playlist
 
@@ -387,7 +388,7 @@ class PlaylistsMixin(MixinProtocol):
                 "Cannot remove songs, because setVideoId is missing. Do you own this playlist?"
             )
 
-        body: dict[str, Any] = {"playlistId": validate_playlist_id(playlistId), "actions": []}
+        body: JsonDict = {"playlistId": validate_playlist_id(playlistId), "actions": []}
         for video in videos:
             body["actions"].append(
                 {
diff --git a/ytmusicapi/mixins/podcasts.py b/ytmusicapi/mixins/podcasts.py
index ceac29a..7d4ad79 100644
--- a/ytmusicapi/mixins/podcasts.py
+++ b/ytmusicapi/mixins/podcasts.py
@@ -4,6 +4,7 @@ from ytmusicapi.navigation import *
 from ytmusicapi.parsers.browsing import parse_content_list
 from ytmusicapi.parsers.playlists import parse_playlist_header
 from ytmusicapi.parsers.podcasts import *
+from ytmusicapi.type_alias import JsonDict, JsonList, ParseFuncType, RequestFuncType
 
 from ._utils import *
 
@@ -11,7 +12,7 @@ from ._utils import *
 class PodcastsMixin(MixinProtocol):
     """Podcasts Mixin"""
 
-    def get_channel(self, channelId: str) -> dict:
+    def get_channel(self, channelId: str) -> JsonDict:
         """
         Get information about a podcast channel (episodes, podcasts). For episodes, a
         maximum of 10 episodes are returned, the full list of episodes can be retrieved
@@ -78,7 +79,7 @@ class PodcastsMixin(MixinProtocol):
 
         return channel
 
-    def get_channel_episodes(self, channelId: str, params: str) -> list[dict]:
+    def get_channel_episodes(self, channelId: str, params: str) -> JsonList:
         """
         Get all channel episodes. This endpoint is currently unlimited
 
@@ -93,7 +94,7 @@ class PodcastsMixin(MixinProtocol):
         results = nav(response, SINGLE_COLUMN_TAB + SECTION_LIST_ITEM + GRID_ITEMS)
         return parse_content_list(results, parse_episode, MMRIR)
 
-    def get_podcast(self, playlistId: str, limit: Optional[int] = 100) -> dict:
+    def get_podcast(self, playlistId: str, limit: int | None = 100) -> JsonDict:
         """
         Returns podcast metadata and episodes
 
@@ -138,14 +139,16 @@ class PodcastsMixin(MixinProtocol):
         response = self._send_request(endpoint, body)
         two_columns = nav(response, TWO_COLUMN_RENDERER)
         header = nav(two_columns, [*TAB_CONTENT, *SECTION_LIST_ITEM, *RESPONSIVE_HEADER])
-        podcast = parse_podcast_header(header)
+        podcast: JsonDict = parse_podcast_header(header)
 
         results = nav(two_columns, ["secondaryContents", *SECTION_LIST_ITEM, *MUSIC_SHELF])
-        parse_func = lambda contents: parse_content_list(contents, parse_episode, MMRIR)
+        parse_func: ParseFuncType = lambda contents: parse_content_list(contents, parse_episode, MMRIR)
         episodes = parse_func(results["contents"])
 
         if "continuations" in results:
-            request_func = lambda additionalParams: self._send_request(endpoint, body, additionalParams)
+            request_func: RequestFuncType = lambda additionalParams: self._send_request(
+                endpoint, body, additionalParams
+            )
             remaining_limit = None if limit is None else (limit - len(episodes))
             episodes.extend(
                 get_continuations(
@@ -157,7 +160,7 @@ class PodcastsMixin(MixinProtocol):
 
         return podcast
 
-    def get_episode(self, videoId: str) -> dict:
+    def get_episode(self, videoId: str) -> JsonDict:
         """
         Retrieve episode data for a single episode
 
@@ -228,7 +231,7 @@ class PodcastsMixin(MixinProtocol):
 
         return episode
 
-    def get_episodes_playlist(self, playlist_id: str = "RDPN") -> dict:
+    def get_episodes_playlist(self, playlist_id: str = "RDPN") -> JsonDict:
         """
         Get all episodes in an episodes playlist. Currently the only known playlist is the
         "New Episodes" auto-generated playlist
@@ -243,7 +246,7 @@ class PodcastsMixin(MixinProtocol):
         playlist = parse_playlist_header(response)
 
         results = nav(response, [*TWO_COLUMN_RENDERER, "secondaryContents", *SECTION_LIST_ITEM, *MUSIC_SHELF])
-        parse_func = lambda contents: parse_content_list(contents, parse_episode, MMRIR)
+        parse_func: ParseFuncType = lambda contents: parse_content_list(contents, parse_episode, MMRIR)
         playlist["episodes"] = parse_func(results["contents"])
 
         return playlist
diff --git a/ytmusicapi/mixins/search.py b/ytmusicapi/mixins/search.py
index 70f6e91..dc07352 100644
--- a/ytmusicapi/mixins/search.py
+++ b/ytmusicapi/mixins/search.py
@@ -1,20 +1,19 @@
-from typing import Any, Optional, Union
-
 from ytmusicapi.continuations import get_continuations
 from ytmusicapi.exceptions import YTMusicUserError
 from ytmusicapi.mixins._protocol import MixinProtocol
 from ytmusicapi.parsers.search import *
+from ytmusicapi.type_alias import JsonList, ParseFuncType, RequestFuncType
 
 
 class SearchMixin(MixinProtocol):
     def search(
         self,
         query: str,
-        filter: Optional[str] = None,
-        scope: Optional[str] = None,
+        filter: str | None = None,
+        scope: str | None = None,
         limit: int = 20,
         ignore_spelling: bool = False,
-    ) -> list[dict]:
+    ) -> JsonList:
         """
         Search YouTube music
         Returns results within the provided category.
@@ -137,7 +136,7 @@ class SearchMixin(MixinProtocol):
         """
         body = {"query": query}
         endpoint = "search"
-        search_results: list[dict[str, Any]] = []
+        search_results: JsonList = []
         filters = [
             "albums",
             "artists",
@@ -201,13 +200,15 @@ class SearchMixin(MixinProtocol):
             return search_results
 
         # set filter for parser
+        result_type = None
         if filter and "playlists" in filter:
             filter = "playlists"
-        elif scope == scopes[1]:
+        elif scope == scopes[1]:  # uploads
             filter = scopes[1]
+            result_type = scopes[1][:-1]
 
         for res in section_list:
-            result_type = category = None
+            category = None
 
             if "musicCardShelfRenderer" in res:
                 top_result = parse_top_result(
@@ -240,12 +241,12 @@ class SearchMixin(MixinProtocol):
             )
 
             if filter:  # if filter is set, there are continuations
-
-                def request_func(additionalParams):
-                    return self._send_request(endpoint, body, additionalParams)
-
-                def parse_func(contents):
-                    return parse_search_results(contents, api_search_result_types, result_type, category)
+                request_func: RequestFuncType = lambda additionalParams: self._send_request(
+                    endpoint, body, additionalParams
+                )
+                parse_func: ParseFuncType = lambda contents: parse_search_results(
+                    contents, api_search_result_types, result_type, category
+                )
 
                 search_results.extend(
                     get_continuations(
@@ -259,7 +260,7 @@ class SearchMixin(MixinProtocol):
 
         return search_results
 
-    def get_search_suggestions(self, query: str, detailed_runs=False) -> Union[list[str], list[dict]]:
+    def get_search_suggestions(self, query: str, detailed_runs: bool = False) -> list[str] | JsonList:
         """
         Get Search Suggestions
 
@@ -339,9 +340,7 @@ class SearchMixin(MixinProtocol):
 
         return parse_search_suggestions(response, detailed_runs)
 
-    def remove_search_suggestions(
-        self, suggestions: list[dict[str, Any]], indices: Optional[list[int]] = None
-    ) -> bool:
+    def remove_search_suggestions(self, suggestions: JsonList, indices: list[int] | None = None) -> bool:
         """
         Remove search suggestion from the user search history.
 
diff --git a/ytmusicapi/mixins/uploads.py b/ytmusicapi/mixins/uploads.py
index 84afe6d..6351a52 100644
--- a/ytmusicapi/mixins/uploads.py
+++ b/ytmusicapi/mixins/uploads.py
@@ -1,5 +1,5 @@
+import typing
 from pathlib import Path
-from typing import Optional, Union
 
 import requests
 
@@ -14,6 +14,7 @@ from ytmusicapi.parsers.library import (
     pop_songs_random_mix,
 )
 from ytmusicapi.parsers.uploads import parse_uploaded_items
+from ytmusicapi.type_alias import JsonDict, JsonList, ParseFuncType, RequestFuncType
 
 from ..auth.types import AuthType
 from ..enums import ResponseStatus
@@ -24,8 +25,8 @@ from ._utils import LibraryOrderType, prepare_order_params, validate_order_param
 
 class UploadsMixin(MixinProtocol):
     def get_library_upload_songs(
-        self, limit: Optional[int] = 25, order: Optional[LibraryOrderType] = None
-    ) -> list[dict]:
+        self, limit: int | None = 25, order: LibraryOrderType | None = None
+    ) -> JsonList:
         """
         Returns a list of uploaded songs
 
@@ -59,10 +60,12 @@ class UploadsMixin(MixinProtocol):
         if results is None:
             return []
         pop_songs_random_mix(results)
-        songs = parse_uploaded_items(results["contents"])
+        songs: JsonList = parse_uploaded_items(results["contents"])
 
         if "continuations" in results:
-            request_func = lambda additionalParams: self._send_request(endpoint, body, additionalParams)
+            request_func: RequestFuncType = lambda additionalParams: self._send_request(
+                endpoint, body, additionalParams
+            )
             remaining_limit = None if limit is None else (limit - len(songs))
             songs.extend(
                 get_continuations(
@@ -73,8 +76,8 @@ class UploadsMixin(MixinProtocol):
         return songs
 
     def get_library_upload_albums(
-        self, limit: Optional[int] = 25, order: Optional[LibraryOrderType] = None
-    ) -> list[dict]:
+        self, limit: int | None = 25, order: LibraryOrderType | None = None
+    ) -> JsonList:
         """
         Gets the albums of uploaded songs in the user's library.
 
@@ -94,8 +97,8 @@ class UploadsMixin(MixinProtocol):
         )
 
     def get_library_upload_artists(
-        self, limit: Optional[int] = 25, order: Optional[LibraryOrderType] = None
-    ) -> list[dict]:
+        self, limit: int | None = 25, order: LibraryOrderType | None = None
+    ) -> JsonList:
         """
         Gets the artists of uploaded songs in the user's library.
 
@@ -114,7 +117,7 @@ class UploadsMixin(MixinProtocol):
             response, lambda additionalParams: self._send_request(endpoint, body, additionalParams), limit
         )
 
-    def get_library_upload_artist(self, browseId: str, limit: int = 25) -> list[dict]:
+    def get_library_upload_artist(self, browseId: str, limit: int = 25) -> JsonList:
         """
         Returns a list of uploaded tracks for the artist.
 
@@ -152,8 +155,10 @@ class UploadsMixin(MixinProtocol):
         items = parse_uploaded_items(results["contents"])
 
         if "continuations" in results:
-            request_func = lambda additionalParams: self._send_request(endpoint, body, additionalParams)
-            parse_func = lambda contents: parse_uploaded_items(contents)
+            request_func: RequestFuncType = lambda additionalParams: self._send_request(
+                endpoint, body, additionalParams
+            )
+            parse_func: ParseFuncType = lambda contents: parse_uploaded_items(contents)
             remaining_limit = None if limit is None else (limit - len(items))
             items.extend(
                 get_continuations(
@@ -163,7 +168,7 @@ class UploadsMixin(MixinProtocol):
 
         return items
 
-    def get_library_upload_album(self, browseId: str) -> dict:
+    def get_library_upload_album(self, browseId: str) -> JsonDict:
         """
         Get information and tracks of an album associated with uploaded tracks
 
@@ -205,7 +210,7 @@ class UploadsMixin(MixinProtocol):
         album["duration_seconds"] = sum_total_duration(album)
         return album
 
-    def upload_song(self, filepath: str) -> Union[ResponseStatus, requests.Response]:
+    def upload_song(self, filepath: str) -> ResponseStatus | requests.Response:
         """
         Uploads a song to YouTube Music
 
@@ -251,7 +256,7 @@ class UploadsMixin(MixinProtocol):
         else:
             return response
 
-    def delete_upload_entity(self, entityId: str) -> Union[str, dict]:  # pragma: no cover
+    def delete_upload_entity(self, entityId: str) -> str | JsonDict:  # pragma: no cover
         """
         Deletes a previously uploaded song or album
 
@@ -270,4 +275,4 @@ class UploadsMixin(MixinProtocol):
         if "error" not in response:
             return ResponseStatus.SUCCEEDED
         else:
-            return response["error"]
+            return typing.cast(str, response["error"])
diff --git a/ytmusicapi/mixins/watch.py b/ytmusicapi/mixins/watch.py
index 6b7770d..00458e1 100644
--- a/ytmusicapi/mixins/watch.py
+++ b/ytmusicapi/mixins/watch.py
@@ -1,21 +1,20 @@
-from typing import Optional, Union
-
 from ytmusicapi.continuations import get_continuations
 from ytmusicapi.exceptions import YTMusicServerError, YTMusicUserError
 from ytmusicapi.mixins._protocol import MixinProtocol
 from ytmusicapi.parsers.playlists import validate_playlist_id
 from ytmusicapi.parsers.watch import *
+from ytmusicapi.type_alias import JsonList, ParseFuncType, RequestFuncType
 
 
 class WatchMixin(MixinProtocol):
     def get_watch_playlist(
         self,
-        videoId: Optional[str] = None,
-        playlistId: Optional[str] = None,
-        limit=25,
+        videoId: str | None = None,
+        playlistId: str | None = None,
+        limit: int = 25,
         radio: bool = False,
         shuffle: bool = False,
-    ) -> dict[str, Union[list[dict], str, None]]:
+    ) -> dict[str, JsonList | str | None]:
         """
         Get a watch list of tracks. This watch playlist appears when you press
         play on a track in YouTube Music.
@@ -169,8 +168,10 @@ class WatchMixin(MixinProtocol):
         tracks = parse_watch_playlist(results["contents"])
 
         if "continuations" in results:
-            request_func = lambda additionalParams: self._send_request(endpoint, body, additionalParams)
-            parse_func = lambda contents: parse_watch_playlist(contents)
+            request_func: RequestFuncType = lambda additionalParams: self._send_request(
+                endpoint, body, additionalParams
+            )
+            parse_func: ParseFuncType = lambda contents: parse_watch_playlist(contents)
             tracks.extend(
                 get_continuations(
                     results,
diff --git a/ytmusicapi/models/content/__init__.py b/ytmusicapi/models/content/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/ytmusicapi/models/content/enums.py b/ytmusicapi/models/content/enums.py
new file mode 100644
index 0000000..2a66767
--- /dev/null
+++ b/ytmusicapi/models/content/enums.py
@@ -0,0 +1,25 @@
+from enum import Enum
+from typing import Any
+
+
+class PrivacyStatus(str, Enum):
+    PUBLIC = "PUBLIC"
+    PRIVATE = "PRIVATE"
+    UNLISTED = "UNLISTED"
+
+
+class LikeStatus(str, Enum):
+    LIKE = "LIKE"
+    DISLIKE = "DISLIKE"
+    INDIFFERENT = "INDIFFERENT"
+
+    @classmethod
+    def _missing_(cls, value: Any) -> "LikeStatus":
+        return cls.INDIFFERENT
+
+
+class VideoType(str, Enum):
+    OMV = "MUSIC_VIDEO_TYPE_OMV"
+    UGC = "MUSIC_VIDEO_TYPE_UGC"
+    ATV = "MUSIC_VIDEO_TYPE_ATV"
+    OFFICIAL_SOURCE_MUSIC = "MUSIC_VIDEO_TYPE_OFFICIAL_SOURCE_MUSIC"
diff --git a/ytmusicapi/models/lyrics.py b/ytmusicapi/models/lyrics.py
index 534c691..85f389f 100644
--- a/ytmusicapi/models/lyrics.py
+++ b/ytmusicapi/models/lyrics.py
@@ -1,16 +1,17 @@
 from dataclasses import dataclass
-from typing import Literal, Optional, TypedDict
+from typing import Literal, TypedDict
+
+from ytmusicapi.type_alias import JsonDict
 
 
 @dataclass
 class LyricLine:
     """Represents a line of lyrics with timestamps (in milliseconds).
 
-    Args:
-        text (str): The Songtext.
-        start_time (int): Begin of the lyric in milliseconds.
-        end_time (int): End of the lyric in milliseconds.
-        id (int): A Metadata-Id that probably uniquely identifies each lyric line.
+    :param text (str): The Songtext.
+    :param start_time (int): Begin of the lyric in milliseconds.
+    :param end_time (int): End of the lyric in milliseconds.
+    :param id (int): A Metadata-Id that probably uniquely identifies each lyric line.
     """
 
     text: str
@@ -19,7 +20,7 @@ class LyricLine:
     id: int
 
     @classmethod
-    def from_raw(cls, raw_lyric: dict):
+    def from_raw(cls, raw_lyric: JsonDict) -> "LyricLine":
         """
         Converts lyrics in the format from the api to a more reasonable format
 
@@ -36,11 +37,11 @@ class LyricLine:
 
 class Lyrics(TypedDict):
     lyrics: str
-    source: Optional[str]
+    source: str | None
     hasTimestamps: Literal[False]
 
 
 class TimedLyrics(TypedDict):
     lyrics: list[LyricLine]
-    source: Optional[str]
+    source: str | None
     hasTimestamps: Literal[True]
diff --git a/ytmusicapi/navigation.py b/ytmusicapi/navigation.py
index 5ab6d7c..4c05da9 100644
--- a/ytmusicapi/navigation.py
+++ b/ytmusicapi/navigation.py
@@ -1,6 +1,8 @@
 """commonly used navigation paths"""
 
-from typing import Any, Literal, Optional, overload
+from typing import Any, Literal, overload
+
+from ytmusicapi.type_alias import JsonDict, JsonList
 
 CONTENT = ["contents", 0]
 RUN_TEXT = ["runs", 0, "text"]
@@ -101,37 +103,42 @@ TIMESTAMPED_LYRICS = [
 
 
 @overload
-def nav(root: dict[str, Any], items: list[Any], none_if_absent: Literal[False] = False) -> Any:
+def nav(root: JsonDict | None, items: list[Any], none_if_absent: Literal[False] = False) -> Any:
     """overload for mypy only"""
 
 
 @overload
-def nav(root: dict, items: list[Any], none_if_absent: Literal[True] = True) -> Optional[Any]:
+def nav(root: JsonDict | None, items: list[Any], none_if_absent: Literal[True] = True) -> Any | None:
     """overload for mypy only"""
 
 
-def nav(root: dict, items: list[Any], none_if_absent: bool = False) -> Optional[Any]:
+def nav(root: JsonDict | None, items: list[Any], none_if_absent: bool = False) -> Any | None:
     """Access a nested object in root by item sequence."""
+    if root is None:
+        return None
     try:
         for k in items:
-            root = root[k]
-    except (KeyError, IndexError, TypeError) as e:
+            root = root[k]  # type: ignore[index]
+    except (KeyError, IndexError) as e:
         if none_if_absent:
             return None
         raise type(e)(f"Unable to find '{k}' using path {items!r} on {root!r}, exception: {e}")
     return root
 
 
-def find_object_by_key(object_list, key, nested=None, is_key=False):
+def find_object_by_key(
+    object_list: JsonList, key: str, nested: str | None = None, is_key: bool = False
+) -> JsonDict | None:
     for item in object_list:
         if nested:
             item = item[nested]
         if key in item:
             return item[key] if is_key else item
+
     return None
 
 
-def find_objects_by_key(object_list, key, nested=None):
+def find_objects_by_key(object_list: JsonList, key: str, nested: str | None = None) -> JsonList:
     objects = []
     for item in object_list:
         if nested:
diff --git a/ytmusicapi/parsers/_utils.py b/ytmusicapi/parsers/_utils.py
index 4df215b..a4a2b4b 100644
--- a/ytmusicapi/parsers/_utils.py
+++ b/ytmusicapi/parsers/_utils.py
@@ -1,9 +1,21 @@
+import typing
+from collections.abc import Callable
 from functools import wraps
+from gettext import GNUTranslations
+from typing import ParamSpec, TypeVar
 
 from ytmusicapi.navigation import *
+from ytmusicapi.type_alias import JsonDict, JsonList
 
+P = ParamSpec("P")
+R = TypeVar("R")
 
-def parse_menu_playlists(data, result):
+if typing.TYPE_CHECKING:
+    pass
+
+
+def parse_menu_playlists(data: JsonDict, result: JsonDict) -> None:
+    """performs in-place replacement based on :param:data: in :param:result"""
     menu_items = nav(data, MENU_ITEMS, True)
     if menu_items is None:
         return
@@ -24,16 +36,17 @@ def parse_menu_playlists(data, result):
             result[watch_key] = watch_id
 
 
-def get_item_text(item, index, run_index=0, none_if_absent=False):
+def get_item_text(item: JsonDict, index: int, run_index: int = 0, none_if_absent: bool = False) -> str | None:
     column = get_flex_column_item(item, index)
     if not column:
         return None
     if none_if_absent and len(column["text"]["runs"]) < run_index + 1:
         return None
-    return column["text"]["runs"][run_index]["text"]
 
+    return typing.cast(str, column["text"]["runs"][run_index]["text"])
 
-def get_flex_column_item(item, index):
+
+def get_flex_column_item(item: JsonDict, index: int) -> JsonDict | None:
     if (
         len(item["flexColumns"]) <= index
         or "text" not in item["flexColumns"][index]["musicResponsiveListItemFlexColumnRenderer"]
@@ -41,20 +54,20 @@ def get_flex_column_item(item, index):
     ):
         return None
 
-    return item["flexColumns"][index]["musicResponsiveListItemFlexColumnRenderer"]
+    return typing.cast(JsonDict, item["flexColumns"][index]["musicResponsiveListItemFlexColumnRenderer"])
 
 
-def get_fixed_column_item(item, index):
+def get_fixed_column_item(item: JsonDict, index: int) -> JsonDict | None:
     if (
         "text" not in item["fixedColumns"][index]["musicResponsiveListItemFixedColumnRenderer"]
         or "runs" not in item["fixedColumns"][index]["musicResponsiveListItemFixedColumnRenderer"]["text"]
     ):
         return None
 
-    return item["fixedColumns"][index]["musicResponsiveListItemFixedColumnRenderer"]
+    return typing.cast(JsonDict, item["fixedColumns"][index]["musicResponsiveListItemFixedColumnRenderer"])
 
 
-def get_dot_separator_index(runs):
+def get_dot_separator_index(runs: JsonList) -> int:
     try:
         index = runs.index({"text": " • "})
     except ValueError:
@@ -63,10 +76,16 @@ def get_dot_separator_index(runs):
     return index
 
 
-def parse_duration(duration):
+def parse_duration(duration: str | None) -> int | None:
+    """
+    Parse duration to a value in seconds.
+
+    :param duration: Duration string
+    :return: Duration in seconds
+    """
     # duration may be falsy or a single space: ' '
     if not duration or not duration.strip():
-        return duration
+        return None
     duration_split = duration.strip().split(":")
     for d in duration_split:
         if not d.isdigit():  # For e.g: "2,343"
@@ -76,16 +95,21 @@ def parse_duration(duration):
     return seconds
 
 
-def i18n(method):
+class SupportsLang(typing.Protocol):
+    lang: GNUTranslations
+
+
+def i18n(method: Callable[P, R]) -> Callable[P, R]:
     @wraps(method)
-    def _impl(self, *method_args, **method_kwargs):
-        method.__globals__["_"] = self.lang.gettext
-        return method(self, *method_args, **method_kwargs)
+    def _impl(*args: P.args, **kwargs: P.kwargs) -> R:
+        self = args[0]
+        method.__globals__["_"] = self.lang.gettext  # type: ignore
+        return method(*args, **kwargs)
 
     return _impl
 
 
-def parse_id_name(sub_run):
+def parse_id_name(sub_run: JsonDict | None) -> JsonDict:
     return {
         "id": nav(sub_run, NAVIGATION_BROWSE_ID, True),
         "name": nav(sub_run, ["text"], True),
diff --git a/ytmusicapi/parsers/albums.py b/ytmusicapi/parsers/albums.py
index 182a8c4..5679ab2 100644
--- a/ytmusicapi/parsers/albums.py
+++ b/ytmusicapi/parsers/albums.py
@@ -1,11 +1,12 @@
 from ytmusicapi.helpers import to_int
+from ytmusicapi.type_alias import JsonDict
 
 from ._utils import *
 from .podcasts import parse_base_header
 from .songs import parse_like_status, parse_song_runs
 
 
-def parse_album_header(response):
+def parse_album_header(response: JsonDict) -> JsonDict:
     header = nav(response, HEADER_DETAIL)
     album = {
         "title": nav(header, TITLE_TEXT),
@@ -39,7 +40,7 @@ def parse_album_header(response):
     return album
 
 
-def parse_album_header_2024(response):
+def parse_album_header_2024(response: JsonDict) -> JsonDict:
     header = nav(response, [*TWO_COLUMN_RENDERER, *TAB_CONTENT, *SECTION_LIST_ITEM, *RESPONSIVE_HEADER])
     album = {
         "title": nav(header, TITLE_TEXT),
diff --git a/ytmusicapi/parsers/browsing.py b/ytmusicapi/parsers/browsing.py
index 16568c6..3356718 100644
--- a/ytmusicapi/parsers/browsing.py
+++ b/ytmusicapi/parsers/browsing.py
@@ -1,8 +1,12 @@
+import re
+
+from ytmusicapi.type_alias import JsonDict, JsonList, ParseFuncDictType
+
 from .podcasts import parse_episode, parse_podcast
 from .songs import *
 
 
-def parse_mixed_content(rows):
+def parse_mixed_content(rows: JsonList) -> JsonList:
     items = []
     for row in rows:
         if DESCRIPTION_SHELF[0] in row:
@@ -46,7 +50,7 @@ def parse_mixed_content(rows):
     return items
 
 
-def parse_content_list(results, parse_func, key=MTRIR):
+def parse_content_list(results: JsonList, parse_func: ParseFuncDictType, key: str = MTRIR) -> JsonList:
     contents = []
     for result in results:
         contents.append(parse_func(result[key]))
@@ -54,7 +58,7 @@ def parse_content_list(results, parse_func, key=MTRIR):
     return contents
 
 
-def parse_album(result):
+def parse_album(result: JsonDict) -> JsonDict:
     album = {
         "title": nav(result, TITLE_TEXT),
         "type": nav(result, SUBTITLE),
@@ -71,7 +75,7 @@ def parse_album(result):
     return album
 
 
-def parse_single(result):
+def parse_single(result: JsonDict) -> JsonDict:
     return {
         "title": nav(result, TITLE_TEXT),
         "year": nav(result, SUBTITLE, True),
@@ -80,7 +84,7 @@ def parse_single(result):
     }
 
 
-def parse_song(result):
+def parse_song(result: JsonDict) -> JsonDict:
     song = {
         "title": nav(result, TITLE_TEXT),
         "videoId": nav(result, NAVIGATION_VIDEO_ID),
@@ -91,7 +95,7 @@ def parse_song(result):
     return song
 
 
-def parse_song_flat(data):
+def parse_song_flat(data: JsonDict) -> JsonDict:
     columns = [get_flex_column_item(data, i) for i in range(0, len(data["flexColumns"]))]
     song = {
         "title": nav(columns[0], TEXT_RUN_TEXT),
@@ -111,7 +115,7 @@ def parse_song_flat(data):
     return song
 
 
-def parse_video(result):
+def parse_video(result: JsonDict) -> JsonDict:
     runs = nav(result, SUBTITLE_RUNS)
     artists_len = get_dot_separator_index(runs)
     videoId = nav(result, NAVIGATION_VIDEO_ID, True)
@@ -131,7 +135,7 @@ def parse_video(result):
     }
 
 
-def parse_playlist(data):
+def parse_playlist(data: JsonDict) -> JsonDict:
     playlist = {
         "title": nav(data, TITLE_TEXT),
         "playlistId": nav(data, TITLE + NAVIGATION_BROWSE_ID)[2:],
@@ -147,7 +151,7 @@ def parse_playlist(data):
     return playlist
 
 
-def parse_related_artist(data):
+def parse_related_artist(data: JsonDict) -> JsonDict:
     subscribers = nav(data, SUBTITLE, True)
     if subscribers:
         subscribers = subscribers.split(" ")[0]
@@ -159,7 +163,7 @@ def parse_related_artist(data):
     }
 
 
-def parse_watch_playlist(data):
+def parse_watch_playlist(data: JsonDict) -> JsonDict:
     return {
         "title": nav(data, TITLE_TEXT),
         "playlistId": nav(data, NAVIGATION_WATCH_PLAYLIST_ID),
diff --git a/ytmusicapi/parsers/explore.py b/ytmusicapi/parsers/explore.py
index 4b366a3..bf81895 100644
--- a/ytmusicapi/parsers/explore.py
+++ b/ytmusicapi/parsers/explore.py
@@ -1,15 +1,16 @@
 from ytmusicapi.parsers.browsing import *
+from ytmusicapi.type_alias import JsonDict
 
 TRENDS = {"ARROW_DROP_UP": "up", "ARROW_DROP_DOWN": "down", "ARROW_CHART_NEUTRAL": "neutral"}
 
 
-def parse_chart_song(data):
+def parse_chart_song(data: JsonDict) -> JsonDict:
     parsed = parse_song_flat(data)
     parsed.update(parse_ranking(data))
     return parsed
 
 
-def parse_chart_artist(data):
+def parse_chart_artist(data: JsonDict) -> JsonDict:
     subscribers = get_flex_column_item(data, 1)
     if subscribers:
         subscribers = nav(subscribers, TEXT_RUN_TEXT).split(" ")[0]
@@ -24,7 +25,7 @@ def parse_chart_artist(data):
     return parsed
 
 
-def parse_chart_trending(data):
+def parse_chart_trending(data: JsonDict) -> JsonDict:
     flex_0 = get_flex_column_item(data, 0)
     artists = parse_song_artists(data, 1)
     index = get_dot_separator_index(artists)
@@ -40,7 +41,7 @@ def parse_chart_trending(data):
     }
 
 
-def parse_ranking(data):
+def parse_ranking(data: JsonDict) -> JsonDict:
     return {
         "rank": nav(data, ["customIndexColumn", "musicCustomIndexColumnRenderer", *TEXT_RUN_TEXT]),
         "trend": TRENDS[nav(data, ["customIndexColumn", "musicCustomIndexColumnRenderer", *ICON_TYPE])],
diff --git a/ytmusicapi/parsers/i18n.py b/ytmusicapi/parsers/i18n.py
index 5a592db..aeb78ff 100644
--- a/ytmusicapi/parsers/i18n.py
+++ b/ytmusicapi/parsers/i18n.py
@@ -1,5 +1,5 @@
+from gettext import GNUTranslations
 from gettext import gettext as _
-from typing import Any
 
 from ytmusicapi.navigation import (
     CAROUSEL,
@@ -20,14 +20,15 @@ from ytmusicapi.parsers.browsing import (
     parse_video,
 )
 from ytmusicapi.parsers.podcasts import parse_episode, parse_podcast
+from ytmusicapi.type_alias import JsonDict, JsonList
 
 
 class Parser:
-    def __init__(self, language):
+    def __init__(self, language: GNUTranslations) -> None:
         self.lang = language
 
     @i18n
-    def get_search_result_types(self):
+    def get_search_result_types(self) -> list[str]:
         return [
             _("album"),
             _("artist"),
@@ -41,7 +42,7 @@ class Parser:
         ]
 
     @i18n
-    def get_api_result_types(self):
+    def get_api_result_types(self) -> list[str]:
         return [
             _("single"),
             _("ep"),
@@ -49,8 +50,7 @@ class Parser:
         ]
 
     @i18n
-    def parse_channel_contents(self, results: list) -> dict:
-        # type: ignore[name-defined]
+    def parse_channel_contents(self, results: JsonList) -> JsonDict:
         categories = [
             ("albums", _("albums"), parse_album, MTRIR),
             ("singles", _("singles & eps"), parse_single, MTRIR),
@@ -61,7 +61,7 @@ class Parser:
             ("episodes", _("episodes"), parse_episode, MMRIR),
             ("podcasts", _("podcasts"), parse_podcast, MTRIR),
         ]
-        artist: dict[str, Any] = {}
+        artist: JsonDict = {}
         for category, category_local, category_parser, category_key in categories:
             data = [
                 r["musicCarouselShelfRenderer"]
diff --git a/ytmusicapi/parsers/library.py b/ytmusicapi/parsers/library.py
index 5474aa1..d05ab1d 100644
--- a/ytmusicapi/parsers/library.py
+++ b/ytmusicapi/parsers/library.py
@@ -1,4 +1,5 @@
 from ytmusicapi.continuations import get_continuations
+from ytmusicapi.type_alias import JsonDict, JsonList, ParseFuncType, RequestFuncType
 
 from ._utils import *
 from .browsing import parse_content_list
@@ -7,7 +8,7 @@ from .podcasts import parse_podcast
 from .songs import parse_song_runs
 
 
-def parse_artists(results, uploaded=False):
+def parse_artists(results: JsonList, uploaded: bool = False) -> JsonList:
     artists = []
     for result in results:
         data = result[MRLIR]
@@ -21,7 +22,7 @@ def parse_artists(results, uploaded=False):
             artist["type"] = "artist"
         parse_menu_playlists(data, artist)
         if uploaded:
-            artist["songs"] = get_item_text(data, 1).split(" ")[0]
+            artist["songs"] = (get_item_text(data, 1) or "").split(" ")[0]
         else:
             subtitle = get_item_text(data, 1)
             if subtitle:
@@ -32,14 +33,14 @@ def parse_artists(results, uploaded=False):
     return artists
 
 
-def parse_library_albums(response, request_func, limit):
+def parse_library_albums(response: JsonDict, request_func: RequestFuncType, limit: int | None) -> JsonList:
     results = get_library_contents(response, GRID)
     if results is None:
         return []
     albums = parse_albums(results["items"])
 
     if "continuations" in results:
-        parse_func = lambda contents: parse_albums(contents)
+        parse_func: ParseFuncType = lambda contents: parse_albums(contents)
         remaining_limit = None if limit is None else (limit - len(albums))
         albums.extend(
             get_continuations(results, "gridContinuation", remaining_limit, request_func, parse_func)
@@ -48,7 +49,7 @@ def parse_library_albums(response, request_func, limit):
     return albums
 
 
-def parse_albums(results):
+def parse_albums(results: JsonList) -> JsonList:
     albums = []
     for result in results:
         data = result[MTRIR]
@@ -67,9 +68,11 @@ def parse_albums(results):
     return albums
 
 
-def parse_library_podcasts(response, request_func, limit):
+def parse_library_podcasts(response: JsonDict, request_func: RequestFuncType, limit: int | None) -> JsonList:
     results = get_library_contents(response, GRID)
-    parse_func = lambda contents: parse_content_list(contents, parse_podcast)
+    if results is None:
+        return []
+    parse_func: ParseFuncType = lambda contents: parse_content_list(contents, parse_podcast)
     podcasts = parse_func(results["items"][1:])  # skip first entry "Add podcast"
 
     if "continuations" in results:
@@ -81,14 +84,14 @@ def parse_library_podcasts(response, request_func, limit):
     return podcasts
 
 
-def parse_library_artists(response, request_func, limit):
+def parse_library_artists(response: JsonDict, request_func: RequestFuncType, limit: int | None) -> JsonList:
     results = get_library_contents(response, MUSIC_SHELF)
     if results is None:
         return []
     artists = parse_artists(results["contents"])
 
     if "continuations" in results:
-        parse_func = lambda contents: parse_artists(contents)
+        parse_func: ParseFuncType = lambda contents: parse_artists(contents)
         remaining_limit = None if limit is None else (limit - len(artists))
         artists.extend(
             get_continuations(results, "musicShelfContinuation", remaining_limit, request_func, parse_func)
@@ -97,20 +100,20 @@ def parse_library_artists(response, request_func, limit):
     return artists
 
 
-def pop_songs_random_mix(results) -> None:
+def pop_songs_random_mix(results: JsonDict | None) -> None:
     """remove the random mix that conditionally appears at the start of library songs"""
     if results:
         if len(results["contents"]) >= 2:
             results["contents"].pop(0)
 
 
-def parse_library_songs(response):
+def parse_library_songs(response: JsonDict) -> JsonDict:
     results = get_library_contents(response, MUSIC_SHELF)
     pop_songs_random_mix(results)
     return {"results": results, "parsed": parse_playlist_items(results["contents"]) if results else results}
 
 
-def get_library_contents(response, renderer):
+def get_library_contents(response: JsonDict, renderer: list[str]) -> JsonDict | None:
     """
     Find library contents. This function is a bit messy now
     as it is supporting two different response types. Can be
@@ -120,7 +123,6 @@ def get_library_contents(response, renderer):
     :return: library contents or None
     """
     section = nav(response, SINGLE_COLUMN_TAB + SECTION_LIST, True)
-    contents = None
     if section is None:  # empty library or uploads
         # covers the case of non-premium subscribers - no downloads tab
         num_tabs = len(nav(response, [*SINGLE_COLUMN, "tabs"]))
diff --git a/ytmusicapi/parsers/playlists.py b/ytmusicapi/parsers/playlists.py
index b06bba7..2c385c7 100644
--- a/ytmusicapi/parsers/playlists.py
+++ b/ytmusicapi/parsers/playlists.py
@@ -1,14 +1,15 @@
-from typing import Optional
+import re
 
 from ytmusicapi.continuations import *
 from ytmusicapi.helpers import sum_total_duration
+from ytmusicapi.type_alias import JsonDict, JsonList, ParseFuncType, RequestFuncBodyType
 
 from ..helpers import to_int
 from .songs import *
 
 
-def parse_playlist_header(response: dict) -> dict[str, Any]:
-    playlist: dict[str, Any] = {}
+def parse_playlist_header(response: JsonDict) -> JsonDict:
+    playlist: JsonDict = {}
     editable_header = nav(response, [*HEADER, *EDITABLE_PLAYLIST_DETAIL_HEADER], True)
     playlist["owned"] = editable_header is not None
     playlist["privacy"] = "PUBLIC"
@@ -38,7 +39,7 @@ def parse_playlist_header(response: dict) -> dict[str, Any]:
     return playlist
 
 
-def parse_playlist_header_meta(header: dict[str, Any]) -> dict[str, Any]:
+def parse_playlist_header_meta(header: JsonDict) -> JsonDict:
     playlist_meta = {
         "views": None,
         "duration": None,
@@ -64,8 +65,10 @@ def parse_playlist_header_meta(header: dict[str, Any]) -> dict[str, Any]:
     return playlist_meta
 
 
-def parse_audio_playlist(response: dict, limit: Optional[int], request_func) -> dict[str, Any]:
-    playlist: dict = {
+def parse_audio_playlist(
+    response: JsonDict, limit: int | None, request_func: RequestFuncBodyType
+) -> JsonDict:
+    playlist: JsonDict = {
         "owned": False,
         "privacy": "PUBLIC",
         "description": None,
@@ -88,7 +91,7 @@ def parse_audio_playlist(response: dict, limit: Optional[int], request_func) ->
     if "contents" in content_data:
         playlist["tracks"] = parse_playlist_items(content_data["contents"])
 
-        parse_func = lambda contents: parse_playlist_items(contents)
+        parse_func: ParseFuncType = lambda contents: parse_playlist_items(contents)
         playlist["tracks"].extend(get_continuations_2025(content_data, limit, request_func, parse_func))
 
     playlist["title"] = playlist["tracks"][0]["album"]["name"]
@@ -97,7 +100,9 @@ def parse_audio_playlist(response: dict, limit: Optional[int], request_func) ->
     return playlist
 
 
-def parse_playlist_items(results, menu_entries: Optional[list[list]] = None, is_album=False):
+def parse_playlist_items(
+    results: JsonList, menu_entries: list[list[str]] | None = None, is_album: bool = False
+) -> JsonList:
     songs = []
     for result in results:
         if MRLIR not in result:
@@ -111,8 +116,8 @@ def parse_playlist_items(results, menu_entries: Optional[list[list]] = None, is_
 
 
 def parse_playlist_item(
-    data: dict, menu_entries: Optional[list[list]] = None, is_album=False
-) -> Optional[dict]:
+    data: JsonDict, menu_entries: list[list[str]] | None = None, is_album: bool = False
+) -> JsonDict | None:
     videoId = setVideoId = None
     like = None
     feedback_tokens = None
@@ -208,10 +213,10 @@ def parse_playlist_item(
 
     duration = None
     if "fixedColumns" in data:
-        if "simpleText" in get_fixed_column_item(data, 0)["text"]:
-            duration = get_fixed_column_item(data, 0)["text"]["simpleText"]
+        if "simpleText" in nav(get_fixed_column_item(data, 0), ["text"]):
+            duration = nav(get_fixed_column_item(data, 0), ["text", "simpleText"])
         else:
-            duration = get_fixed_column_item(data, 0)["text"]["runs"][0]["text"]
+            duration = nav(get_fixed_column_item(data, 0), TEXT_RUN_TEXT)
 
     thumbnails = nav(data, THUMBNAILS, True)
 
diff --git a/ytmusicapi/parsers/podcasts.py b/ytmusicapi/parsers/podcasts.py
index c7d27f1..5858b09 100644
--- a/ytmusicapi/parsers/podcasts.py
+++ b/ytmusicapi/parsers/podcasts.py
@@ -1,4 +1,7 @@
 from dataclasses import dataclass
+from typing import Any
+
+from ytmusicapi.type_alias import JsonDict, JsonList
 
 from .songs import *
 
@@ -26,7 +29,7 @@ class Timestamp(DescriptionElement):
 
 @dataclass
 class Description(list[DescriptionElement]):
-    def __init__(self, *args, **kwargs):
+    def __init__(self, *args: Any, **kwargs: Any):
         super().__init__(args[0])
 
     @property
@@ -34,7 +37,7 @@ class Description(list[DescriptionElement]):
         return "".join(str(element) for element in self)
 
     @classmethod
-    def from_runs(cls, description_runs: list[dict]) -> "Description":
+    def from_runs(cls, description_runs: JsonList) -> "Description":
         """parse the description runs into a usable format
 
         :param description_runs: the original description runs
@@ -61,7 +64,7 @@ class Description(list[DescriptionElement]):
         return cls(elements)
 
 
-def parse_base_header(header: dict) -> dict:
+def parse_base_header(header: JsonDict) -> JsonDict:
     """parse common left hand side (header) items of an episode or podcast page"""
     strapline = nav(header, ["straplineTextOne"])
     return {
@@ -74,7 +77,7 @@ def parse_base_header(header: dict) -> dict:
     }
 
 
-def parse_podcast_header(header: dict) -> dict:
+def parse_podcast_header(header: JsonDict) -> JsonDict:
     metadata = parse_base_header(header)
     metadata["description"] = nav(header, ["description", *DESCRIPTION_SHELF, *DESCRIPTION], True)
     metadata["saved"] = nav(header, ["buttons", 1, *TOGGLED_BUTTON])
@@ -82,7 +85,7 @@ def parse_podcast_header(header: dict) -> dict:
     return metadata
 
 
-def parse_episode_header(header: dict) -> dict:
+def parse_episode_header(header: JsonDict) -> JsonDict:
     metadata = parse_base_header(header)
     metadata["date"] = nav(header, [*SUBTITLE])
     progress_renderer = nav(header, ["progress", *PROGRESS_RENDERER])
@@ -99,7 +102,7 @@ def parse_episode_header(header: dict) -> dict:
     return metadata
 
 
-def parse_episode(data):
+def parse_episode(data: JsonDict) -> JsonDict:
     """Parses a single episode under "Episodes" on a channel page or on a podcast page"""
     thumbnails = nav(data, THUMBNAILS)
     date = nav(data, SUBTITLE, True)
@@ -123,7 +126,7 @@ def parse_episode(data):
     }
 
 
-def parse_podcast(data):
+def parse_podcast(data: JsonDict) -> JsonDict:
     """Parses a single podcast under "Podcasts" on a channel page"""
     return {
         "title": nav(data, TITLE_TEXT),
diff --git a/ytmusicapi/parsers/search.py b/ytmusicapi/parsers/search.py
index 2369494..79c6c68 100644
--- a/ytmusicapi/parsers/search.py
+++ b/ytmusicapi/parsers/search.py
@@ -1,4 +1,4 @@
-from typing import Union
+from ytmusicapi.type_alias import JsonDict, JsonList
 
 from ..helpers import to_int
 from ._utils import *
@@ -18,7 +18,7 @@ ALL_RESULT_TYPES = [
 API_RESULT_TYPES = ["single", "ep", *ALL_RESULT_TYPES]
 
 
-def get_search_result_type(result_type_local, result_types_local):
+def get_search_result_type(result_type_local: str, result_types_local: list[str]) -> str | None:
     if not result_type_local:
         return None
     result_type_local = result_type_local.lower()
@@ -31,7 +31,7 @@ def get_search_result_type(result_type_local, result_types_local):
     return result_type
 
 
-def parse_top_result(data, search_result_types):
+def parse_top_result(data: JsonDict, search_result_types: list[str]) -> JsonDict:
     result_type = get_search_result_type(nav(data, SUBTITLE), search_result_types)
     search_result = {"category": nav(data, CARD_SHELF_TITLE), "resultType": result_type}
     if result_type == "artist":
@@ -71,9 +71,11 @@ def parse_top_result(data, search_result_types):
     return search_result
 
 
-def parse_search_result(data, api_search_result_types, result_type, category):
+def parse_search_result(
+    data: JsonDict, api_search_result_types: list[str], result_type: str | None, category: str | None
+) -> JsonDict:
     default_offset = (not result_type or result_type == "album") * 2
-    search_result = {"category": category}
+    search_result: JsonDict = {"category": category}
     video_type = nav(data, [*PLAY_BUTTON, "playNavigationEndpoint", *NAVIGATION_VIDEO_TYPE], True)
 
     # determine result type based on browseId
@@ -111,9 +113,11 @@ def parse_search_result(data, api_search_result_types, result_type, category):
         search_result["playlistId"] = parse_album_playlistid_if_exists(play_navigation)
 
     elif result_type == "playlist":
-        flex_item = get_flex_column_item(data, 1)["text"]["runs"]
+        flex_item = nav(get_flex_column_item(data, 1), TEXT_RUNS)
         has_author = len(flex_item) == default_offset + 3
-        search_result["itemCount"] = get_item_text(data, 1, default_offset + has_author * 2).split(" ")[0]
+        search_result["itemCount"] = (get_item_text(data, 1, default_offset + has_author * 2) or "").split(
+            " "
+        )[0]
         if search_result["itemCount"] and search_result["itemCount"].isnumeric():
             search_result["itemCount"] = to_int(search_result["itemCount"])
         search_result["author"] = None if not has_author else get_item_text(data, 1, default_offset)
@@ -150,7 +154,11 @@ def parse_search_result(data, api_search_result_types, result_type, category):
                 search_result["resultType"] = "artist"
             else:
                 flex_item2 = get_flex_column_item(data, 1)
-                runs = [run["text"] for i, run in enumerate(flex_item2["text"]["runs"]) if i % 2 == 0]
+                runs = (
+                    [run["text"] for i, run in enumerate(flex_item2["text"]["runs"]) if i % 2 == 0]
+                    if flex_item2
+                    else []
+                )
                 if len(runs) > 1:
                     search_result["artist"] = runs[1]
                 if len(runs) > 2:  # date may be missing
@@ -168,8 +176,6 @@ def parse_search_result(data, api_search_result_types, result_type, category):
         search_result["year"] = None
         flex_item = get_flex_column_item(data, 1)
         runs = flex_item["text"]["runs"]
-        if flex_item2 := get_flex_column_item(data, 2):
-            runs.extend([{"text": ""}, *flex_item2["text"]["runs"]])  # first item is a dummy separator
         # ignore the first run if it is a type specifier (like "Single" or "Album")
         runs_offset = (len(runs[0]) == 1 and runs[0]["text"].lower() in api_search_result_types) * 2
         song_info = parse_song_runs(runs[runs_offset:])
@@ -195,19 +201,32 @@ def parse_search_result(data, api_search_result_types, result_type, category):
     return search_result
 
 
-def parse_album_playlistid_if_exists(data: dict[str, Any]) -> Optional[str]:
+def parse_album_playlistid_if_exists(data: JsonDict | None) -> str | None:
     """the content of the data changes based on whether the user is authenticated or not"""
     return nav(data, WATCH_PID, True) or nav(data, WATCH_PLAYLIST_ID, True) if data else None
 
 
-def parse_search_results(results, api_search_result_types, resultType=None, category=None):
+def parse_search_results(
+    results: JsonList,
+    api_search_result_types: list[str],
+    resultType: str | None = None,
+    category: str | None = None,
+) -> JsonList:
     return [
         parse_search_result(result[MRLIR], api_search_result_types, resultType, category)
         for result in results
     ]
 
 
-def get_search_params(filter, scope, ignore_spelling):
+def get_search_params(filter: str | None, scope: str | None, ignore_spelling: bool) -> str | None:
+    """
+    Get search params for search query string based on user input
+
+    :param filter: The search filter
+    :param scope: The search scope
+    :param ignore_spelling: If spelling shall be ignored
+    :return: search param string
+    """
     filtered_param1 = "EgWKAQ"
     params = None
     if filter is None and scope is None and not ignore_spelling:
@@ -258,7 +277,7 @@ def get_search_params(filter, scope, ignore_spelling):
     return params if params else param1 + param2 + param3
 
 
-def _get_param2(filter):
+def _get_param2(filter: str) -> str:
     filter_params = {
         "songs": "II",
         "videos": "IQ",
@@ -272,9 +291,7 @@ def _get_param2(filter):
     return filter_params[filter]
 
 
-def parse_search_suggestions(
-    results: dict[str, Any], detailed_runs: bool
-) -> Union[list[str], list[dict[str, Any]]]:
+def parse_search_suggestions(results: JsonDict, detailed_runs: bool) -> list[str] | JsonList:
     if not results.get("contents", [{}])[0].get("searchSuggestionsSectionRenderer", {}).get("contents", []):
         return []
 
diff --git a/ytmusicapi/parsers/songs.py b/ytmusicapi/parsers/songs.py
index 1347ee4..1a91a59 100644
--- a/ytmusicapi/parsers/songs.py
+++ b/ytmusicapi/parsers/songs.py
@@ -1,26 +1,28 @@
 import re
 
+from ytmusicapi.type_alias import JsonDict, JsonList
+
 from ._utils import *
 
 
-def parse_song_artists(data, index):
+def parse_song_artists(data: JsonDict, index: int) -> JsonList:
     flex_item = get_flex_column_item(data, index)
     if not flex_item:
-        return None
+        return []
     else:
         runs = flex_item["text"]["runs"]
         return parse_song_artists_runs(runs)
 
 
-def parse_song_artists_runs(runs):
+def parse_song_artists_runs(runs: JsonList) -> JsonList:
     artists = []
     for j in range(int(len(runs) / 2) + 1):
         artists.append({"name": runs[j * 2]["text"], "id": nav(runs[j * 2], NAVIGATION_BROWSE_ID, True)})
     return artists
 
 
-def parse_song_runs(runs):
-    parsed = {"artists": []}
+def parse_song_runs(runs: JsonList) -> JsonDict:
+    parsed: JsonDict = {"artists": []}
     for i, run in enumerate(runs):
         if i % 2:  # uneven items are always separators
             continue
@@ -51,20 +53,20 @@ def parse_song_runs(runs):
     return parsed
 
 
-def parse_song_album(data, index):
+def parse_song_album(data: JsonDict, index: int) -> JsonDict | None:
     flex_item = get_flex_column_item(data, index)
     browse_id = nav(flex_item, TEXT_RUN + NAVIGATION_BROWSE_ID, True)
     return None if not flex_item else {"name": get_item_text(data, index), "id": browse_id}
 
 
-def parse_song_library_status(item) -> bool:
+def parse_song_library_status(item: JsonDict) -> bool:
     """Returns True if song is in the library"""
     library_status = nav(item, [TOGGLE_MENU, "defaultIcon", "iconType"], True)
 
     return library_status == "LIBRARY_SAVED"
 
 
-def parse_song_menu_tokens(item):
+def parse_song_menu_tokens(item: JsonDict) -> dict[str, str | None]:
     toggle_menu = item[TOGGLE_MENU]
 
     library_add_token = nav(toggle_menu, ["defaultServiceEndpoint", *FEEDBACK_TOKEN], True)
@@ -77,6 +79,6 @@ def parse_song_menu_tokens(item):
     return {"add": library_add_token, "remove": library_remove_token}
 
 
-def parse_like_status(service):
+def parse_like_status(service: JsonDict) -> str:
     status = ["LIKE", "INDIFFERENT"]
     return status[status.index(service["likeEndpoint"]["status"]) - 1]
diff --git a/ytmusicapi/parsers/uploads.py b/ytmusicapi/parsers/uploads.py
index 8caf8ec..11f2290 100644
--- a/ytmusicapi/parsers/uploads.py
+++ b/ytmusicapi/parsers/uploads.py
@@ -1,8 +1,10 @@
+from ytmusicapi.type_alias import JsonList
+
 from ._utils import *
 from .songs import parse_song_album, parse_song_artists
 
 
-def parse_uploaded_items(results):
+def parse_uploaded_items(results: JsonList) -> JsonList:
     songs = []
     for result in results:
         data = result[MRLIR]
@@ -32,7 +34,7 @@ def parse_uploaded_items(results):
         thumbnails = nav(data, THUMBNAILS) if "thumbnail" in data else None
         duration = None
         if "fixedColumns" in data:
-            duration = get_fixed_column_item(data, 0)["text"]["runs"][0]["text"]
+            duration = nav(get_fixed_column_item(data, 0), TEXT_RUN_TEXT)
         song = {
             "entityId": entityId,
             "videoId": videoId,
diff --git a/ytmusicapi/parsers/watch.py b/ytmusicapi/parsers/watch.py
index b27b7ae..00e9771 100644
--- a/ytmusicapi/parsers/watch.py
+++ b/ytmusicapi/parsers/watch.py
@@ -1,9 +1,11 @@
-from typing import Any
+import typing
+
+from ytmusicapi.type_alias import JsonDict, JsonList
 
 from .songs import *
 
 
-def parse_watch_playlist(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
+def parse_watch_playlist(results: JsonList) -> JsonList:
     tracks = []
     PPVWR = "playlistPanelVideoWrapperRenderer"
     PPVR = "playlistPanelVideoRenderer"
@@ -26,7 +28,7 @@ def parse_watch_playlist(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
     return tracks
 
 
-def parse_watch_track(data):
+def parse_watch_track(data: JsonDict) -> JsonDict:
     feedback_tokens = like_status = library_status = None
     for item in nav(data, MENU_ITEMS):
         if TOGGLE_MENU in item:
@@ -54,8 +56,10 @@ def parse_watch_track(data):
     return track
 
 
-def get_tab_browse_id(watchNextRenderer, tab_id):
+def get_tab_browse_id(watchNextRenderer: JsonDict, tab_id: int) -> str | None:
     if "unselectable" not in watchNextRenderer["tabs"][tab_id]["tabRenderer"]:
-        return watchNextRenderer["tabs"][tab_id]["tabRenderer"]["endpoint"]["browseEndpoint"]["browseId"]
+        return typing.cast(
+            str, watchNextRenderer["tabs"][tab_id]["tabRenderer"]["endpoint"]["browseEndpoint"]["browseId"]
+        )
     else:
         return None
diff --git a/ytmusicapi/py.typed b/ytmusicapi/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/ytmusicapi/setup.py b/ytmusicapi/setup.py
index d20cafc..a309039 100644
--- a/ytmusicapi/setup.py
+++ b/ytmusicapi/setup.py
@@ -2,7 +2,6 @@ import argparse
 import importlib.metadata
 import sys
 from pathlib import Path
-from typing import Optional, Union
 
 import requests
 
@@ -10,7 +9,7 @@ from ytmusicapi.auth.browser import setup_browser
 from ytmusicapi.auth.oauth import OAuthCredentials, RefreshingToken
 
 
-def setup(filepath: Optional[str] = None, headers_raw: Optional[str] = None) -> str:
+def setup(filepath: str | None = None, headers_raw: str | None = None) -> str:
     """
     Requests browser headers from the user via command line
     and returns a string that can be passed to YTMusic()
@@ -26,9 +25,9 @@ def setup(filepath: Optional[str] = None, headers_raw: Optional[str] = None) ->
 def setup_oauth(
     client_id: str,
     client_secret: str,
-    filepath: Optional[str] = None,
-    session: Optional[requests.Session] = None,
-    proxies: Optional[dict] = None,
+    filepath: str | None = None,
+    session: requests.Session | None = None,
+    proxies: dict[str, str] | None = None,
     open_browser: bool = False,
 ) -> RefreshingToken:
     """
@@ -53,7 +52,7 @@ def setup_oauth(
     return RefreshingToken.prompt_for_token(oauth_credentials, open_browser, filepath)
 
 
-def parse_args(args):
+def parse_args(args: list[str]) -> argparse.Namespace:
     parser = argparse.ArgumentParser(description="Setup ytmusicapi.")
     parser.add_argument(
         "-v",
@@ -80,7 +79,7 @@ def parse_args(args):
     return parser.parse_args(args)
 
 
-def main() -> Union[RefreshingToken, str]:
+def main() -> RefreshingToken | str:
     args = parse_args(sys.argv[1:])
     filename = args.file.as_posix() if args.file else f"{args.setup_type}.json"
     print(f"Creating {Path(filename).resolve().as_uri()} with your authentication credentials...")
diff --git a/ytmusicapi/type_alias.py b/ytmusicapi/type_alias.py
new file mode 100644
index 0000000..b438396
--- /dev/null
+++ b/ytmusicapi/type_alias.py
@@ -0,0 +1,10 @@
+from collections.abc import Callable
+from typing import Any
+
+JsonDict = dict[str, Any]
+JsonList = list[JsonDict]
+
+RequestFuncType = Callable[[str], JsonDict]
+RequestFuncBodyType = Callable[[JsonDict], JsonDict]
+ParseFuncType = Callable[[JsonList], JsonList]
+ParseFuncDictType = Callable[[JsonDict], JsonDict]
diff --git a/ytmusicapi/ytmusic.py b/ytmusicapi/ytmusic.py
index c1d019f..ba0cd38 100644
--- a/ytmusicapi/ytmusic.py
+++ b/ytmusicapi/ytmusic.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import gettext
 import json
 import locale
@@ -6,7 +8,7 @@ from collections.abc import Iterator
 from contextlib import contextmanager, suppress
 from functools import cached_property, partial
 from pathlib import Path
-from typing import Optional, Union
+from typing import Any
 
 import requests
 from requests import Response
@@ -39,18 +41,19 @@ from .auth.oauth import OAuthCredentials, RefreshingToken
 from .auth.oauth.token import Token
 from .auth.types import AuthType
 from .exceptions import YTMusicServerError, YTMusicUserError
+from .type_alias import JsonDict
 
 
 class YTMusicBase:
     def __init__(
         self,
-        auth: Optional[Union[str, dict]] = None,
-        user: Optional[str] = None,
-        requests_session: Optional[requests.Session] = None,
-        proxies: Optional[dict[str, str]] = None,
+        auth: str | JsonDict | None = None,
+        user: str | None = None,
+        requests_session: requests.Session | None = None,
+        proxies: dict[str, str] | None = None,
         language: str = "en",
         location: str = "",
-        oauth_credentials: Optional[OAuthCredentials] = None,
+        oauth_credentials: OAuthCredentials | None = None,
     ):
         """
         Create a new instance to interact with YouTube Music.
@@ -88,12 +91,12 @@ class YTMusicBase:
         """
         #: request session for connection pooling
         self._session = self._prepare_session(requests_session)
-        self.proxies: Optional[dict[str, str]] = proxies  #: params for session modification
+        self.proxies: dict[str, str] | None = proxies  #: params for session modification
         # see google cookie docs: https://policies.google.com/technologies/cookies
         # value from https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/extractor/youtube.py#L502
         self.cookies = {"SOCS": "CAI"}
 
-        self._auth_headers: CaseInsensitiveDict = CaseInsensitiveDict()
+        self._auth_headers: CaseInsensitiveDict[str] = CaseInsensitiveDict[str]()
         self.auth_type = AuthType.UNAUTHORIZED
         if auth is not None:
             self._auth_headers, auth_path = parse_auth_str(auth)
@@ -143,14 +146,14 @@ class YTMusicBase:
         if self.auth_type == AuthType.BROWSER:
             self.params += YTM_PARAMS_KEY
             try:
-                cookie = self.base_headers.get("cookie")
+                cookie = self.base_headers["cookie"]
                 self.sapisid = sapisid_from_cookie(cookie)
-                self.origin = self.base_headers.get("origin", self.base_headers.get("x-origin"))
+                self.origin: str = self.base_headers.get("origin", str(self.base_headers.get("x-origin")))
             except KeyError:
                 raise YTMusicUserError("Your cookie is missing the required value __Secure-3PAPISID")
 
     @cached_property
-    def base_headers(self) -> CaseInsensitiveDict:
+    def base_headers(self) -> CaseInsensitiveDict[str]:
         headers = (
             self._auth_headers
             if self.auth_type == AuthType.BROWSER or self.auth_type == AuthType.OAUTH_CUSTOM_FULL
@@ -163,7 +166,7 @@ class YTMusicBase:
         return headers
 
     @property
-    def headers(self) -> CaseInsensitiveDict:
+    def headers(self) -> CaseInsensitiveDict[str]:
         headers = self.base_headers
 
         # keys updated each use, custom oauth implementations left untouched
@@ -212,7 +215,7 @@ class YTMusicBase:
             # safely restore the old context
             self.context["context"]["client"] = copied_context_client
 
-    def _prepare_session(self, requests_session: Optional[requests.Session]) -> requests.Session:
+    def _prepare_session(self, requests_session: requests.Session | None) -> requests.Session:
         """Prepare requests session or use user-provided requests_session"""
         if isinstance(requests_session, requests.Session):
             return requests_session
@@ -220,7 +223,7 @@ class YTMusicBase:
         self._session.request = partial(self._session.request, timeout=30)  # type: ignore[method-assign]
         return self._session
 
-    def _send_request(self, endpoint: str, body: dict, additionalParams: str = "") -> dict:
+    def _send_request(self, endpoint: str, body: JsonDict, additionalParams: str = "") -> JsonDict:
         body.update(self.context)
 
         response = self._session.post(
@@ -230,7 +233,7 @@ class YTMusicBase:
             proxies=self.proxies,
             cookies=self.cookies,
         )
-        response_text = json.loads(response.text)
+        response_text: JsonDict = json.loads(response.text)
         if response.status_code >= 400:
             message = "Server returned HTTP " + str(response.status_code) + ": " + response.reason + ".\n"
             error = response_text.get("error", {}).get("message")
@@ -238,7 +241,7 @@ class YTMusicBase:
         return response_text
 
     def _send_get_request(
-        self, url: str, params: Optional[dict] = None, use_base_headers: bool = False
+        self, url: str, params: JsonDict | None = None, use_base_headers: bool = False
     ) -> Response:
         response = self._session.get(
             url,
@@ -250,14 +253,25 @@ class YTMusicBase:
         )
         return response
 
-    def _check_auth(self):
+    def _check_auth(self) -> None:
+        """
+        Checks if the user has provided authorization credentials
+
+        Raises:
+            YTMusicUserError: if the user is not authorized
+        """
         if self.auth_type == AuthType.UNAUTHORIZED:
             raise YTMusicUserError("Please provide authentication before using this function")
 
-    def __enter__(self):
+    def __enter__(self) -> YTMusicBase:
         return self
 
-    def __exit__(self, execType=None, execValue=None, trackback=None):
+    def __exit__(
+        self,
+        exc_type: type[BaseException] | None,
+        exc_value: BaseException | None,
+        traceback: Any | None,
+    ) -> bool | None:
         pass
 
 


Test patch:
diff --git a/tests/auth/test_oauth.py b/tests/auth/test_oauth.py
index bea97c6..b454acb 100644
--- a/tests/auth/test_oauth.py
+++ b/tests/auth/test_oauth.py
@@ -2,7 +2,6 @@ import json
 import tempfile
 import time
 from pathlib import Path
-from typing import Any
 from unittest import mock
 
 import pytest
@@ -12,11 +11,12 @@ from ytmusicapi.auth.oauth import OAuthToken
 from ytmusicapi.auth.types import AuthType
 from ytmusicapi.exceptions import YTMusicUserError
 from ytmusicapi.setup import main
+from ytmusicapi.type_alias import JsonDict
 from ytmusicapi.ytmusic import OAuthCredentials, YTMusic
 
 
 @pytest.fixture(name="blank_code")
-def fixture_blank_code() -> dict[str, Any]:
+def fixture_blank_code() -> JsonDict:
     return {
         "device_code": "",
         "user_code": "",
diff --git a/tests/mixins/test_browsing.py b/tests/mixins/test_browsing.py
index b6798a5..9a212e3 100644
--- a/tests/mixins/test_browsing.py
+++ b/tests/mixins/test_browsing.py
@@ -89,7 +89,7 @@ class TestBrowsing:
 
     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"
+        assert escaped_browse_id == "MPREb_pZhPA6GfQmN"
 
     def test_get_album_2024(self, yt):
         with open(Path(__file__).parent.parent / "data" / "2024_03_get_album.json", encoding="utf8") as f:
@@ -204,21 +204,13 @@ class TestBrowsing:
         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
-
+    def test_set_tasteprofile(self, yt_brand):
         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
 
diff --git a/tests/mixins/test_library.py b/tests/mixins/test_library.py
index bb980b1..a1aae04 100644
--- a/tests/mixins/test_library.py
+++ b/tests/mixins/test_library.py
@@ -2,6 +2,8 @@ from urllib.parse import urlparse
 
 import pytest
 
+from ytmusicapi.exceptions import YTMusicUserError
+
 
 class TestLibrary:
     def test_get_library_playlists(self, config, yt_oauth, yt_empty):
@@ -111,8 +113,8 @@ class TestLibrary:
         assert "actions" in response
         response = yt_auth.rate_song(sample_video, "INDIFFERENT")
         assert response
-        response = yt_auth.rate_song(sample_video, "notexist")
-        assert not response
+        with pytest.raises(YTMusicUserError):
+            yt_auth.rate_song(sample_video, "notexist")
 
     @pytest.mark.skip(reason="edit_song_library_status is currently broken due to server-side update")
     def test_edit_song_library_status(self, yt_brand, sample_album):
diff --git a/tests/mixins/test_search.py b/tests/mixins/test_search.py
index 0b38171..15e63b3 100644
--- a/tests/mixins/test_search.py
+++ b/tests/mixins/test_search.py
@@ -1,3 +1,5 @@
+from typing import Any
+
 import pytest
 
 from ytmusicapi import YTMusic
@@ -51,7 +53,7 @@ class TestSearch:
             ),
         ],
     )
-    def test_search_album_artists(self, yt, case: tuple[str, dict]):
+    def test_search_album_artists(self, yt, case: tuple[str, dict[str, Any]]):
         (query, expected) = case
         results = yt.search(query, filter="albums")
 
@@ -74,12 +76,10 @@ class TestSearch:
         query = "hip hop playlist"
         results = yt_auth.search(query, filter="songs")
         assert len(results) > 10
-        assert all(item["views"] != "" for item in results)
         assert all(len(item["artists"]) > 0 for item in results)
         assert all(item["resultType"] == "song" for item in results)
         results = yt_auth.search(query, filter="videos")
         assert len(results) > 10
-        assert all(item["views"] != "" for item in results)
         assert all(item["resultType"] == "video" for item in results)
         results = yt_auth.search(query, filter="albums", limit=40)
         assert len(results) > 20
@@ -125,6 +125,9 @@ class TestSearch:
             )
         results = yt_oauth.search(config["queries"]["uploads_songs"], scope="uploads", limit=40)
         assert len(results) > 20
+        assert all(isinstance(item["title"], str) for item in results)
+        assert all(item.get("browseId", None) or item.get("videoId", None) for item in results)
+        assert all(len(item["thumbnails"]) >= 2 for item in results)
 
     def test_search_library(self, config, yt_oauth):
         results = yt_oauth.search(config["queries"]["library_any"], scope="library")