Skip to content

Commit

Permalink
introduce exception classes (#620)
Browse files Browse the repository at this point in the history
  • Loading branch information
sigma67 authored Jul 13, 2024
1 parent 14ae785 commit 12cc15e
Show file tree
Hide file tree
Showing 11 changed files with 58 additions and 28 deletions.
5 changes: 3 additions & 2 deletions ytmusicapi/auth/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from requests.structures import CaseInsensitiveDict

from ytmusicapi.exceptions import YTMusicError, YTMusicUserError
from ytmusicapi.helpers import *

path = os.path.dirname(os.path.realpath(__file__)) + os.sep
Expand Down Expand Up @@ -46,11 +47,11 @@ def setup_browser(filepath: Optional[str] = None, headers_raw: Optional[str] = N
user_headers[header[0].lower()] = ": ".join(header[1:])

except Exception as e:
raise Exception(f"Error parsing your input, please try again. Full error: {e}") from e
raise YTMusicError(f"Error parsing your input, please try again. Full error: {e}") from e

missing_headers = {"cookie", "x-goog-authuser"} - set(k.lower() for k in user_headers.keys())
if missing_headers:
raise Exception(
raise YTMusicUserError(
"The following entries are missing in your headers: "
+ ", ".join(missing_headers)
+ ". Please try a different request (such as /browse) and make sure you are logged in."
Expand Down
3 changes: 2 additions & 1 deletion ytmusicapi/auth/oauth/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
OAUTH_USER_AGENT,
)

from ...exceptions import YTMusicServerError
from .exceptions import BadOAuthClient, UnauthorizedOAuthClient
from .models import AuthCodeDict, BaseTokenDict, RefreshableTokenDict

Expand Down Expand Up @@ -94,7 +95,7 @@ def _send_request(self, url, data):
"YouTubeData API is not enabled."
)
else:
raise Exception(
raise YTMusicServerError(
f"OAuth request error. status_code: {response.status_code}, url: {url}, content: {data}"
)
return response
Expand Down
16 changes: 16 additions & 0 deletions ytmusicapi/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""custom exception classes for ytmusicapi"""


class YTMusicError(Exception):
"""base error class
shall only be raised if none of the subclasses below are fitting
"""


class YTMusicUserError(YTMusicError):
"""error caused by invalid usage of ytmusicapi"""


class YTMusicServerError(YTMusicError):
"""error caused by the YouTube Music backend"""
4 changes: 3 additions & 1 deletion ytmusicapi/mixins/_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import re
from datetime import date

from ytmusicapi.exceptions import YTMusicUserError


def prepare_like_endpoint(rating):
if rating == "LIKE":
Expand All @@ -16,7 +18,7 @@ def prepare_like_endpoint(rating):
def validate_order_parameter(order):
orders = ["a_to_z", "z_to_a", "recently_added"]
if order and order not in orders:
raise Exception(
raise YTMusicUserError(
"Invalid order provided. Please use one of the following orders or leave out the parameter: "
+ ", ".join(orders)
)
Expand Down
13 changes: 7 additions & 6 deletions ytmusicapi/mixins/browsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from ytmusicapi.parsers.library import parse_albums
from ytmusicapi.parsers.playlists import parse_playlist_items

from ..exceptions import YTMusicError, YTMusicUserError
from ..navigation import *
from ._protocol import MixinProtocol
from ._utils import get_datestamp
Expand Down Expand Up @@ -510,7 +511,7 @@ def get_album(self, browseId: str) -> dict:
}
"""
if not browseId or not browseId.startswith("MPRE"):
raise Exception("Invalid album browseId provided, must start with MPRE.")
raise YTMusicUserError("Invalid album browseId provided, must start with MPRE.")

body = {"browseId": browseId}
endpoint = "browse"
Expand Down Expand Up @@ -793,7 +794,7 @@ def get_song_related(self, browseId: str):
]
"""
if not browseId:
raise Exception("Invalid browseId provided.")
raise YTMusicUserError("Invalid browseId provided.")

response = self._send_request("browse", {"browseId": browseId})
sections = nav(response, ["contents", *SECTION_LIST])
Expand All @@ -816,7 +817,7 @@ def get_lyrics(self, browseId: str) -> dict:
"""
lyrics = {}
if not browseId:
raise Exception("Invalid browseId provided. This song might not have lyrics.")
raise YTMusicUserError("Invalid browseId provided. This song might not have lyrics.")

response = self._send_request("browse", {"browseId": browseId})
lyrics["lyrics"] = nav(
Expand All @@ -837,7 +838,7 @@ def get_basejs_url(self):
response = self._send_get_request(url=YTM_DOMAIN)
match = re.search(r'jsUrl"\s*:\s*"([^"]+)"', response.text)
if match is None:
raise Exception("Could not identify the URL for base.js player.")
raise YTMusicError("Could not identify the URL for base.js player.")

return YTM_DOMAIN + match.group(1)

Expand All @@ -855,7 +856,7 @@ def get_signatureTimestamp(self, url: Optional[str] = None) -> int:
response = self._send_get_request(url=url)
match = re.search(r"signatureTimestamp[:=](\d+)", response.text)
if match is None:
raise Exception("Unable to identify the signatureTimestamp.")
raise YTMusicError("Unable to identify the signatureTimestamp.")

return int(match.group(1))

Expand Down Expand Up @@ -911,7 +912,7 @@ def set_tasteprofile(self, artists: list[str], taste_profile: Optional[dict] = N

for artist in artists:
if artist not in taste_profile:
raise Exception(f"The artist, {artist}, was not present in taste!")
raise YTMusicUserError(f"The artist {artist} was not present in taste!")
formData["selectedValues"].append(taste_profile[artist]["selectionValue"])

body = {"browseId": "FEmusic_home", "formData": formData}
Expand Down
5 changes: 3 additions & 2 deletions ytmusicapi/mixins/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from ytmusicapi.parsers.browsing import *
from ytmusicapi.parsers.library import *

from ..exceptions import YTMusicServerError
from ._protocol import MixinProtocol
from ._utils import *

Expand Down Expand Up @@ -69,7 +70,7 @@ def get_library_songs(
parse_func = lambda raw_response: parse_library_songs(raw_response)

if validate_responses and limit is None:
raise Exception("Validation is not supported without a limit parameter.")
raise YTMusicUserError("Validation is not supported without a limit parameter.")

if validate_responses:
validate_func = lambda parsed: validate_response(parsed, per_page, limit, 0)
Expand Down Expand Up @@ -297,7 +298,7 @@ def get_history(self) -> list[dict]:
data = nav(content, [*MUSIC_SHELF, "contents"], True)
if not data:
error = nav(content, ["musicNotifierShelfRenderer", *TITLE], True)
raise Exception(error)
raise YTMusicServerError(error)
menu_entries = [[-1, *MENU_SERVICE, *FEEDBACK_TOKEN]]
songlist = parse_playlist_items(data, menu_entries)
for song in songlist:
Expand Down
8 changes: 6 additions & 2 deletions ytmusicapi/mixins/playlists.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,9 @@ def add_playlist_items(
self._check_auth()
body: dict[str, Any] = {"playlistId": validate_playlist_id(playlistId), "actions": []}
if not videoIds and not source_playlist:
raise Exception("You must provide either videoIds or a source_playlist to add to the playlist")
raise YTMusicUserError(
"You must provide either videoIds or a source_playlist to add to the playlist"
)

if videoIds:
for videoId in videoIds:
Expand Down Expand Up @@ -388,7 +390,9 @@ def remove_playlist_items(self, playlistId: str, videos: list[dict]) -> Union[st
self._check_auth()
videos = list(filter(lambda x: "videoId" in x and "setVideoId" in x, videos))
if len(videos) == 0:
raise Exception("Cannot remove songs, because setVideoId is missing. Do you own this playlist?")
raise YTMusicUserError(
"Cannot remove songs, because setVideoId is missing. Do you own this playlist?"
)

body: dict[str, Any] = {"playlistId": validate_playlist_id(playlistId), "actions": []}
for video in videos:
Expand Down
9 changes: 5 additions & 4 deletions ytmusicapi/mixins/search.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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 *

Expand Down Expand Up @@ -150,26 +151,26 @@ def search(
"episodes",
]
if filter and filter not in filters:
raise Exception(
raise YTMusicUserError(
"Invalid filter provided. Please use one of the following filters or leave out the parameter: "
+ ", ".join(filters)
)

scopes = ["library", "uploads"]
if scope and scope not in scopes:
raise Exception(
raise YTMusicUserError(
"Invalid scope provided. Please use one of the following scopes or leave out the parameter: "
+ ", ".join(scopes)
)

if scope == scopes[1] and filter:
raise Exception(
raise YTMusicUserError(
"No filter can be set when searching uploads. Please unset the filter parameter when scope is set to "
"uploads. "
)

if scope == scopes[0] and filter in filters[3:5]:
raise Exception(
raise YTMusicUserError(
f"{filter} cannot be set when searching library. "
f"Please use one of the following filters or leave out the parameter: "
+ ", ".join(filters[0:3] + filters[5:])
Expand Down
7 changes: 4 additions & 3 deletions ytmusicapi/mixins/uploads.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from ..auth.types import AuthType
from ..enums import ResponseStatus
from ..exceptions import YTMusicUserError
from ._protocol import MixinProtocol
from ._utils import prepare_order_params, validate_order_parameter

Expand Down Expand Up @@ -210,13 +211,13 @@ def upload_song(self, filepath: str) -> Union[ResponseStatus, requests.Response]
"""
self._check_auth()
if not self.auth_type == AuthType.BROWSER:
raise Exception("Please provide browser authentication before using this function")
raise YTMusicUserError("Please provide browser authentication before using this function")
if not os.path.isfile(filepath):
raise Exception("The provided file does not exist.")
raise YTMusicUserError("The provided file does not exist.")

supported_filetypes = ["mp3", "m4a", "wma", "flac", "ogg"]
if os.path.splitext(filepath)[1][1:] not in supported_filetypes:
raise Exception(
raise YTMusicUserError(
"The provided file type is not supported by YouTube Music. Supported file types are "
+ ", ".join(supported_filetypes)
)
Expand Down
5 changes: 3 additions & 2 deletions ytmusicapi/mixins/watch.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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 *
Expand Down Expand Up @@ -109,7 +110,7 @@ def get_watch_playlist(
"tunerSettingValue": "AUTOMIX_SETTING_NORMAL",
}
if not videoId and not playlistId:
raise Exception("You must provide either a video id, a playlist id, or both")
raise YTMusicUserError("You must provide either a video id, a playlist id, or both")
if videoId:
body["videoId"] = videoId
if not playlistId:
Expand Down Expand Up @@ -153,7 +154,7 @@ def get_watch_playlist(
msg = "No content returned by the server."
if playlistId:
msg += f"\nEnsure you have access to {playlistId} - a private playlist may cause this."
raise Exception(msg)
raise YTMusicServerError(msg)

playlist = next(
filter(
Expand Down
11 changes: 6 additions & 5 deletions ytmusicapi/ytmusic.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from .auth.oauth import OAuthCredentials, OAuthToken, RefreshingToken
from .auth.oauth.token import Token
from .auth.types import AuthType
from .exceptions import YTMusicServerError, YTMusicUserError


class YTMusicBase:
Expand Down Expand Up @@ -145,11 +146,11 @@ def __init__(

if location:
if location not in SUPPORTED_LOCATIONS:
raise Exception("Location not supported. Check the FAQ for supported locations.")
raise YTMusicUserError("Location not supported. Check the FAQ for supported locations.")
self.context["context"]["client"]["gl"] = location

if language not in SUPPORTED_LANGUAGES:
raise Exception(
raise YTMusicUserError(
"Language not supported. Supported languages are " + (", ".join(SUPPORTED_LANGUAGES)) + "."
)
self.context["context"]["client"]["hl"] = language
Expand Down Expand Up @@ -183,7 +184,7 @@ def __init__(
self.sapisid = sapisid_from_cookie(cookie)
self.origin = self.base_headers.get("origin", self.base_headers.get("x-origin"))
except KeyError:
raise Exception("Your cookie is missing the required value __Secure-3PAPISID")
raise YTMusicUserError("Your cookie is missing the required value __Secure-3PAPISID")

@property
def base_headers(self):
Expand Down Expand Up @@ -238,7 +239,7 @@ def _send_request(self, endpoint: str, body: dict, additionalParams: str = "") -
if response.status_code >= 400:
message = "Server returned HTTP " + str(response.status_code) + ": " + response.reason + ".\n"
error = response_text.get("error", {}).get("message")
raise Exception(message + error)
raise YTMusicServerError(message + error)
return response_text

def _send_get_request(self, url: str, params: Optional[dict] = None) -> Response:
Expand All @@ -254,7 +255,7 @@ def _send_get_request(self, url: str, params: Optional[dict] = None) -> Response

def _check_auth(self):
if not self.auth:
raise Exception("Please provide authentication before using this function")
raise YTMusicUserError("Please provide authentication before using this function")

def __enter__(self):
return self
Expand Down

0 comments on commit 12cc15e

Please sign in to comment.