diff --git a/.cache b/.cache new file mode 100644 index 0000000..4d376ec --- /dev/null +++ b/.cache @@ -0,0 +1 @@ +{"access_token": "BQD30P4pxr2e-KGpYXD-EFn6t-X9nO0vFF2mh6HyqRgZUq5UkXEuka37D4o93Ino3Fz5rBGz9jlMo-pw5WE-NdJbajHBNHJsyEOK7tBrjV8DljS4a5FtpJqD5Xi-_J25pZBP-tstpLo", "token_type": "Bearer", "expires_in": 3600, "expires_at": 1775757187} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/anaconda_projects/db/project_filebrowser.db b/anaconda_projects/db/project_filebrowser.db new file mode 100644 index 0000000..3fa3a4a Binary files /dev/null and b/anaconda_projects/db/project_filebrowser.db differ diff --git a/lab-apis.ipynb b/lab-apis.ipynb index e923554..6ebb0e2 100644 --- a/lab-apis.ipynb +++ b/lab-apis.ipynb @@ -34,7 +34,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "id": "ea44c82a-5c07-4dbc-beb2-bba2601bb75e", "metadata": {}, "outputs": [], @@ -45,7 +45,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "id": "319cfd4e-f6fb-4419-80e0-e3983cd25e43", "metadata": {}, "outputs": [], @@ -56,10 +56,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 25, "id": "04e12954-fd70-4311-88a5-fb7e2c29799c", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requirement already satisfied: spotipy in C:\\Users\\pauli\\anaconda3\\Lib\\site-packages (2.25.2)\n", + "Requirement already satisfied: redis>=3.5.3 in C:\\Users\\pauli\\anaconda3\\Lib\\site-packages (from spotipy) (7.1.0)\n", + "Requirement already satisfied: requests>=2.25.0 in C:\\Users\\pauli\\anaconda3\\Lib\\site-packages (from spotipy) (2.32.3)\n", + "Requirement already satisfied: urllib3>=1.26.0 in C:\\Users\\pauli\\anaconda3\\Lib\\site-packages (from spotipy) (2.3.0)\n", + "Requirement already satisfied: charset-normalizer<4,>=2 in C:\\Users\\pauli\\anaconda3\\Lib\\site-packages (from requests>=2.25.0->spotipy) (3.3.2)\n", + "Requirement already satisfied: idna<4,>=2.5 in C:\\Users\\pauli\\anaconda3\\Lib\\site-packages (from requests>=2.25.0->spotipy) (3.7)\n", + "Requirement already satisfied: certifi>=2017.4.17 in C:\\Users\\pauli\\anaconda3\\Lib\\site-packages (from requests>=2.25.0->spotipy) (2025.10.5)\n" + ] + } + ], "source": [ "# If you havent done so, install the spotipy wrapper\n", "!pip install spotipy" @@ -75,7 +89,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 26, "id": "03034bc6-9858-412a-83b7-18abdc345d7e", "metadata": {}, "outputs": [], @@ -106,35 +120,35 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 27, "id": "7772a1e0-9692-4d04-a590-76111a272d8d", "metadata": { "tags": [] }, "outputs": [], "source": [ - "results = sp.search(q='Lady Gaga', limit=50)\n", - "results" + "# results = sp.search(q='Lady Gaga', limit=50)\n", + "# results" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 28, "id": "edc14c95-474b-4e2a-aea3-bdfd0a205546", "metadata": {}, "outputs": [], "source": [ - "results.keys() # We can see that we only have tracks" + "# results.keys() # We can see that we only have tracks" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 30, "id": "ad8ef934-1dbb-4008-ac8e-f5c29823fe6a", "metadata": {}, "outputs": [], "source": [ - "results[\"tracks\"].keys() # Let's check the values" + "# results[\"tracks\"].keys() # Let's check the values" ] }, { @@ -540,11 +554,2732 @@ "source": [ "# Your answer here" ] + }, + { + "cell_type": "markdown", + "id": "714b6f99", + "metadata": {}, + "source": [ + "# Lab | APIs-Spotify API (Adapted Version)" + ] + }, + { + "cell_type": "markdown", + "id": "f54f2f82", + "metadata": {}, + "source": [ + "## 1. Title & context" + ] + }, + { + "cell_type": "markdown", + "id": "4d90ae9e", + "metadata": {}, + "source": [ + "Why adapted?\n", + " - Client Credentials is stable for public catalog data.\n", + " - Some endpoints may return 403 Forbidden depending on app access and endpoint restrictions.\n", + " - This notebook includes defensive error handling." + ] + }, + { + "cell_type": "markdown", + "id": "55ec6ad3", + "metadata": {}, + "source": [ + "## 2. Install and imports" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47aade54", + "metadata": {}, + "outputs": [], + "source": [ + "!pip install -q spotipy pandas" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25b6e178", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import requests\n", + "import spotipy\n", + "\n", + "from spotipy.oauth2 import SpotifyClientCredentials\n", + "from spotipy.exceptions import SpotifyException" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ed913a5", + "metadata": {}, + "outputs": [], + "source": [ + "from dotenv import load_dotenv\n", + "import os\n", + "\n", + "load_dotenv(\"key.env\")\n", + "\n", + "CLIENT_ID = os.getenv(\"SPOTIPY_CLIENT_ID\")\n", + "CLIENT_SECRET = os.getenv(\"SPOTIPY_CLIENT_SECRET\")" + ] + }, + { + "cell_type": "markdown", + "id": "48f40958", + "metadata": {}, + "source": [ + "## 3. Credentials" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20fd4dc7", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "import os\n", + "\n", + "CLIENT_ID = os.getenv(\"SPOTIPY_CLIENT_ID\")\n", + "CLIENT_SECRET = os.getenv(\"SPOTIPY_CLIENT_SECRET\")" + ] + }, + { + "cell_type": "markdown", + "id": "e9bac6c3", + "metadata": {}, + "source": [ + "## 4. Authentication and initializing the API" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c1bf4419", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Spotify client created successfully.\n" + ] + } + ], + "source": [ + "def build_spotify_client(client_id: str, client_secret: str):\n", + " \"\"\"\n", + " Build and test a Spotify client using Client Credentials.\n", + " Returns a Spotipy client if successful, otherwise None.\n", + " \"\"\"\n", + " if not client_id or not client_secret:\n", + " print(\"Missing CLIENT_ID or CLIENT_SECRET.\")\n", + " return None\n", + "\n", + " try:\n", + " auth_manager = SpotifyClientCredentials(\n", + " client_id=client_id,\n", + " client_secret=client_secret\n", + " )\n", + "\n", + " sp = spotipy.Spotify(\n", + " auth_manager=auth_manager,\n", + " requests_timeout=10,\n", + " retries=3\n", + " )\n", + "\n", + " # Lightweight test call\n", + " _ = sp.search(q=\"Lady Gaga\", type=\"track\", limit=1)\n", + " print(\"Spotify client created successfully.\")\n", + " return sp\n", + "\n", + " except SpotifyException as e:\n", + " print(f\"SpotifyException | status={getattr(e, 'http_status', 'N/A')} | message={e}\")\n", + " except requests.exceptions.RequestException as e:\n", + " print(f\"Network error: {e}\")\n", + " except Exception as e:\n", + " print(f\"Unexpected error: {e}\")\n", + "\n", + " return None\n", + "\n", + "\n", + "sp = build_spotify_client(CLIENT_ID, CLIENT_SECRET)" + ] + }, + { + "cell_type": "markdown", + "id": "c88228fb", + "metadata": {}, + "source": [ + "## 5. Helper functions for safe API calls" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "974a3b69", + "metadata": {}, + "outputs": [], + "source": [ + "def safe_search(sp, q: str, search_type: str = \"track\", limit: int = 10):\n", + " \"\"\"\n", + " Safe wrapper for Spotify search.\n", + " Returns the API response dict or an empty dict if something fails.\n", + " \"\"\"\n", + " if sp is None:\n", + " print(\"Spotify client is not available.\")\n", + " return {}\n", + "\n", + " try:\n", + " return sp.search(q=q, type=search_type, limit=limit)\n", + " except SpotifyException as e:\n", + " status = getattr(e, \"http_status\", \"N/A\")\n", + " print(f\"SpotifyException during search | status={status} | query='{q}' | message={e}\")\n", + " return {}\n", + " except requests.exceptions.RequestException as e:\n", + " print(f\"Network error during search: {e}\")\n", + " return {}\n", + " except Exception as e:\n", + " print(f\"Unexpected error during search: {e}\")\n", + " return {}\n", + "\n", + "\n", + "def extract_track_summary(track: dict) -> dict:\n", + " \"\"\"\n", + " Extract a clean summary from a Spotify track object.\n", + " \"\"\"\n", + " if not track:\n", + " return {}\n", + "\n", + " return {\n", + " \"track_name\": track.get(\"name\"),\n", + " \"track_id\": track.get(\"id\"),\n", + " \"popularity\": track.get(\"popularity\"),\n", + " \"uri\": track.get(\"uri\"),\n", + " \"album_name\": track.get(\"album\", {}).get(\"name\"),\n", + " \"album_release_date\": track.get(\"album\", {}).get(\"release_date\"),\n", + " \"artists\": \", \".join([a.get(\"name\", \"\") for a in track.get(\"artists\", [])])\n", + " }" + ] + }, + { + "cell_type": "markdown", + "id": "23e74489", + "metadata": {}, + "source": [ + "## 6. Using the search method" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31a6e7ee", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "HTTP Error for GET to https://api.spotify.com/v1/search with Params: {'q': 'Lady Gaga', 'limit': 50, 'offset': 0, 'type': 'track', 'market': None} returned 400 due to Invalid limit\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SpotifyException during search | status=400 | query='Lady Gaga' | message=http status: 400, code: -1 - https://api.spotify.com/v1/search?q=Lady+Gaga&limit=50&offset=0&type=track:\n", + " Invalid limit, reason: None\n" + ] + }, + { + "data": { + "text/plain": [ + "dict_keys([])" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "results = safe_search(sp, q=\"Lady Gaga\", search_type=\"track\", limit=50)\n", + "results.keys()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4fb4d793", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys(['tracks'])" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "results = safe_search(sp, q=\"Lady Gaga\", search_type=\"track\", limit=10)\n", + "results.keys()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cbd04371", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys(['href', 'limit', 'next', 'offset', 'previous', 'total', 'items'])" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "results.get(\"tracks\", {}).keys()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "410d2948", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'https://api.spotify.com/v1/search?offset=0&limit=10&query=Lady%20Gaga&type=track'" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "results.get(\"tracks\", {}).get(\"href\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2d8e2920", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'album': {'album_type': 'album',\n", + " 'artists': [{'external_urls': {'spotify': 'https://open.spotify.com/artist/1HY2Jd0NmPuamShAr6KMms'},\n", + " 'href': 'https://api.spotify.com/v1/artists/1HY2Jd0NmPuamShAr6KMms',\n", + " 'id': '1HY2Jd0NmPuamShAr6KMms',\n", + " 'name': 'Lady Gaga',\n", + " 'type': 'artist',\n", + " 'uri': 'spotify:artist:1HY2Jd0NmPuamShAr6KMms'}],\n", + " 'external_urls': {'spotify': 'https://open.spotify.com/album/1qwlxZTNLe1jq3b0iidlue'},\n", + " 'href': 'https://api.spotify.com/v1/albums/1qwlxZTNLe1jq3b0iidlue',\n", + " 'id': '1qwlxZTNLe1jq3b0iidlue',\n", + " 'images': [{'height': 640,\n", + " 'width': 640,\n", + " 'url': 'https://i.scdn.co/image/ab67616d0000b273e691217483df8798445c82e2'},\n", + " {'height': 300,\n", + " 'width': 300,\n", + " 'url': 'https://i.scdn.co/image/ab67616d00001e02e691217483df8798445c82e2'},\n", + " {'height': 64,\n", + " 'width': 64,\n", + " 'url': 'https://i.scdn.co/image/ab67616d00004851e691217483df8798445c82e2'}],\n", + " 'is_playable': True,\n", + " 'name': 'The Fame',\n", + " 'release_date': '2008-01-01',\n", + " 'release_date_precision': 'day',\n", + " 'total_tracks': 15,\n", + " 'type': 'album',\n", + " 'uri': 'spotify:album:1qwlxZTNLe1jq3b0iidlue'},\n", + " 'artists': [{'external_urls': {'spotify': 'https://open.spotify.com/artist/1HY2Jd0NmPuamShAr6KMms'},\n", + " 'href': 'https://api.spotify.com/v1/artists/1HY2Jd0NmPuamShAr6KMms',\n", + " 'id': '1HY2Jd0NmPuamShAr6KMms',\n", + " 'name': 'Lady Gaga',\n", + " 'type': 'artist',\n", + " 'uri': 'spotify:artist:1HY2Jd0NmPuamShAr6KMms'}],\n", + " 'disc_number': 1,\n", + " 'duration_ms': 237200,\n", + " 'explicit': True,\n", + " 'external_ids': {'isrc': 'USUM70824409'},\n", + " 'external_urls': {'spotify': 'https://open.spotify.com/track/1QV6tiMFM6fSOKOGLMHYYg'},\n", + " 'href': 'https://api.spotify.com/v1/tracks/1QV6tiMFM6fSOKOGLMHYYg',\n", + " 'id': '1QV6tiMFM6fSOKOGLMHYYg',\n", + " 'is_local': False,\n", + " 'is_playable': True,\n", + " 'name': 'Poker Face',\n", + " 'track_number': 4,\n", + " 'type': 'track',\n", + " 'uri': 'spotify:track:1QV6tiMFM6fSOKOGLMHYYg'},\n", + " {'album': {'album_type': 'single',\n", + " 'artists': [{'external_urls': {'spotify': 'https://open.spotify.com/artist/1HY2Jd0NmPuamShAr6KMms'},\n", + " 'href': 'https://api.spotify.com/v1/artists/1HY2Jd0NmPuamShAr6KMms',\n", + " 'id': '1HY2Jd0NmPuamShAr6KMms',\n", + " 'name': 'Lady Gaga',\n", + " 'type': 'artist',\n", + " 'uri': 'spotify:artist:1HY2Jd0NmPuamShAr6KMms'},\n", + " {'external_urls': {'spotify': 'https://open.spotify.com/artist/0du5cEVh5yTK9QJze8zA0C'},\n", + " 'href': 'https://api.spotify.com/v1/artists/0du5cEVh5yTK9QJze8zA0C',\n", + " 'id': '0du5cEVh5yTK9QJze8zA0C',\n", + " 'name': 'Bruno Mars',\n", + " 'type': 'artist',\n", + " 'uri': 'spotify:artist:0du5cEVh5yTK9QJze8zA0C'}],\n", + " 'external_urls': {'spotify': 'https://open.spotify.com/album/10FLjwfpbxLmW8c25Xyc2N'},\n", + " 'href': 'https://api.spotify.com/v1/albums/10FLjwfpbxLmW8c25Xyc2N',\n", + " 'id': '10FLjwfpbxLmW8c25Xyc2N',\n", + " 'images': [{'height': 640,\n", + " 'width': 640,\n", + " 'url': 'https://i.scdn.co/image/ab67616d0000b27382ea2e9e1858aa012c57cd45'},\n", + " {'height': 300,\n", + " 'width': 300,\n", + " 'url': 'https://i.scdn.co/image/ab67616d00001e0282ea2e9e1858aa012c57cd45'},\n", + " {'height': 64,\n", + " 'width': 64,\n", + " 'url': 'https://i.scdn.co/image/ab67616d0000485182ea2e9e1858aa012c57cd45'}],\n", + " 'is_playable': True,\n", + " 'name': 'Die With A Smile',\n", + " 'release_date': '2024-08-16',\n", + " 'release_date_precision': 'day',\n", + " 'total_tracks': 1,\n", + " 'type': 'album',\n", + " 'uri': 'spotify:album:10FLjwfpbxLmW8c25Xyc2N'},\n", + " 'artists': [{'external_urls': {'spotify': 'https://open.spotify.com/artist/1HY2Jd0NmPuamShAr6KMms'},\n", + " 'href': 'https://api.spotify.com/v1/artists/1HY2Jd0NmPuamShAr6KMms',\n", + " 'id': '1HY2Jd0NmPuamShAr6KMms',\n", + " 'name': 'Lady Gaga',\n", + " 'type': 'artist',\n", + " 'uri': 'spotify:artist:1HY2Jd0NmPuamShAr6KMms'},\n", + " {'external_urls': {'spotify': 'https://open.spotify.com/artist/0du5cEVh5yTK9QJze8zA0C'},\n", + " 'href': 'https://api.spotify.com/v1/artists/0du5cEVh5yTK9QJze8zA0C',\n", + " 'id': '0du5cEVh5yTK9QJze8zA0C',\n", + " 'name': 'Bruno Mars',\n", + " 'type': 'artist',\n", + " 'uri': 'spotify:artist:0du5cEVh5yTK9QJze8zA0C'}],\n", + " 'disc_number': 1,\n", + " 'duration_ms': 251667,\n", + " 'explicit': False,\n", + " 'external_ids': {'isrc': 'USUM72409273'},\n", + " 'external_urls': {'spotify': 'https://open.spotify.com/track/2plbrEY59IikOBgBGLjaoe'},\n", + " 'href': 'https://api.spotify.com/v1/tracks/2plbrEY59IikOBgBGLjaoe',\n", + " 'id': '2plbrEY59IikOBgBGLjaoe',\n", + " 'is_local': False,\n", + " 'is_playable': True,\n", + " 'name': 'Die With A Smile',\n", + " 'track_number': 1,\n", + " 'type': 'track',\n", + " 'uri': 'spotify:track:2plbrEY59IikOBgBGLjaoe'},\n", + " {'album': {'album_type': 'album',\n", + " 'artists': [{'external_urls': {'spotify': 'https://open.spotify.com/artist/1HY2Jd0NmPuamShAr6KMms'},\n", + " 'href': 'https://api.spotify.com/v1/artists/1HY2Jd0NmPuamShAr6KMms',\n", + " 'id': '1HY2Jd0NmPuamShAr6KMms',\n", + " 'name': 'Lady Gaga',\n", + " 'type': 'artist',\n", + " 'uri': 'spotify:artist:1HY2Jd0NmPuamShAr6KMms'}],\n", + " 'external_urls': {'spotify': 'https://open.spotify.com/album/2MHUaRi9OCyTN02SoyRRBJ'},\n", + " 'href': 'https://api.spotify.com/v1/albums/2MHUaRi9OCyTN02SoyRRBJ',\n", + " 'id': '2MHUaRi9OCyTN02SoyRRBJ',\n", + " 'images': [{'height': 640,\n", + " 'width': 640,\n", + " 'url': 'https://i.scdn.co/image/ab67616d0000b273b0860cf0a98e09663c82290c'},\n", + " {'height': 300,\n", + " 'width': 300,\n", + " 'url': 'https://i.scdn.co/image/ab67616d00001e02b0860cf0a98e09663c82290c'},\n", + " {'height': 64,\n", + " 'width': 64,\n", + " 'url': 'https://i.scdn.co/image/ab67616d00004851b0860cf0a98e09663c82290c'}],\n", + " 'is_playable': True,\n", + " 'name': 'MAYHEM',\n", + " 'release_date': '2025-03-07',\n", + " 'release_date_precision': 'day',\n", + " 'total_tracks': 14,\n", + " 'type': 'album',\n", + " 'uri': 'spotify:album:2MHUaRi9OCyTN02SoyRRBJ'},\n", + " 'artists': [{'external_urls': {'spotify': 'https://open.spotify.com/artist/1HY2Jd0NmPuamShAr6KMms'},\n", + " 'href': 'https://api.spotify.com/v1/artists/1HY2Jd0NmPuamShAr6KMms',\n", + " 'id': '1HY2Jd0NmPuamShAr6KMms',\n", + " 'name': 'Lady Gaga',\n", + " 'type': 'artist',\n", + " 'uri': 'spotify:artist:1HY2Jd0NmPuamShAr6KMms'}],\n", + " 'disc_number': 1,\n", + " 'duration_ms': 223398,\n", + " 'explicit': False,\n", + " 'external_ids': {'isrc': 'USUM72412581'},\n", + " 'external_urls': {'spotify': 'https://open.spotify.com/track/2LHNTC9QZxsL3nWpt8iaSR'},\n", + " 'href': 'https://api.spotify.com/v1/tracks/2LHNTC9QZxsL3nWpt8iaSR',\n", + " 'id': '2LHNTC9QZxsL3nWpt8iaSR',\n", + " 'is_local': False,\n", + " 'is_playable': True,\n", + " 'name': 'Abracadabra',\n", + " 'track_number': 2,\n", + " 'type': 'track',\n", + " 'uri': 'spotify:track:2LHNTC9QZxsL3nWpt8iaSR'}]" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "results.get(\"tracks\", {}).get(\"items\", [])[:3]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6f82e71d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "10" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "results.get(\"tracks\", {}).get(\"limit\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e2dfca8d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'https://api.spotify.com/v1/search?offset=10&limit=10&query=Lady%20Gaga&type=track'" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "results.get(\"tracks\", {}).get(\"next\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7d97024f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "results.get(\"tracks\", {}).get(\"offset\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "953fb566", + "metadata": {}, + "outputs": [], + "source": [ + "results.get(\"tracks\", {}).get(\"previous\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ae2820d6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "16" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "results.get(\"tracks\", {}).get(\"total\")" + ] + }, + { + "cell_type": "markdown", + "id": "7e120476", + "metadata": {}, + "source": [ + "## 7. Exploring the tracks" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b8c27a4b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'album': {'album_type': 'album',\n", + " 'artists': [{'external_urls': {'spotify': 'https://open.spotify.com/artist/1HY2Jd0NmPuamShAr6KMms'},\n", + " 'href': 'https://api.spotify.com/v1/artists/1HY2Jd0NmPuamShAr6KMms',\n", + " 'id': '1HY2Jd0NmPuamShAr6KMms',\n", + " 'name': 'Lady Gaga',\n", + " 'type': 'artist',\n", + " 'uri': 'spotify:artist:1HY2Jd0NmPuamShAr6KMms'}],\n", + " 'external_urls': {'spotify': 'https://open.spotify.com/album/1qwlxZTNLe1jq3b0iidlue'},\n", + " 'href': 'https://api.spotify.com/v1/albums/1qwlxZTNLe1jq3b0iidlue',\n", + " 'id': '1qwlxZTNLe1jq3b0iidlue',\n", + " 'images': [{'height': 640,\n", + " 'width': 640,\n", + " 'url': 'https://i.scdn.co/image/ab67616d0000b273e691217483df8798445c82e2'},\n", + " {'height': 300,\n", + " 'width': 300,\n", + " 'url': 'https://i.scdn.co/image/ab67616d00001e02e691217483df8798445c82e2'},\n", + " {'height': 64,\n", + " 'width': 64,\n", + " 'url': 'https://i.scdn.co/image/ab67616d00004851e691217483df8798445c82e2'}],\n", + " 'is_playable': True,\n", + " 'name': 'The Fame',\n", + " 'release_date': '2008-01-01',\n", + " 'release_date_precision': 'day',\n", + " 'total_tracks': 15,\n", + " 'type': 'album',\n", + " 'uri': 'spotify:album:1qwlxZTNLe1jq3b0iidlue'},\n", + " 'artists': [{'external_urls': {'spotify': 'https://open.spotify.com/artist/1HY2Jd0NmPuamShAr6KMms'},\n", + " 'href': 'https://api.spotify.com/v1/artists/1HY2Jd0NmPuamShAr6KMms',\n", + " 'id': '1HY2Jd0NmPuamShAr6KMms',\n", + " 'name': 'Lady Gaga',\n", + " 'type': 'artist',\n", + " 'uri': 'spotify:artist:1HY2Jd0NmPuamShAr6KMms'}],\n", + " 'disc_number': 1,\n", + " 'duration_ms': 237200,\n", + " 'explicit': True,\n", + " 'external_ids': {'isrc': 'USUM70824409'},\n", + " 'external_urls': {'spotify': 'https://open.spotify.com/track/1QV6tiMFM6fSOKOGLMHYYg'},\n", + " 'href': 'https://api.spotify.com/v1/tracks/1QV6tiMFM6fSOKOGLMHYYg',\n", + " 'id': '1QV6tiMFM6fSOKOGLMHYYg',\n", + " 'is_local': False,\n", + " 'is_playable': True,\n", + " 'name': 'Poker Face',\n", + " 'track_number': 4,\n", + " 'type': 'track',\n", + " 'uri': 'spotify:track:1QV6tiMFM6fSOKOGLMHYYg'}" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "first_track = results.get(\"tracks\", {}).get(\"items\", [None])[0]\n", + "first_track" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d89632d6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "dict_keys(['album', 'artists', 'disc_number', 'duration_ms', 'explicit', 'external_ids', 'external_urls', 'href', 'id', 'is_local', 'is_playable', 'name', 'track_number', 'type', 'uri'])" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "first_track.keys() if first_track else \"No track found\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2df37558", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{'external_urls': {'spotify': 'https://open.spotify.com/artist/1HY2Jd0NmPuamShAr6KMms'},\n", + " 'href': 'https://api.spotify.com/v1/artists/1HY2Jd0NmPuamShAr6KMms',\n", + " 'id': '1HY2Jd0NmPuamShAr6KMms',\n", + " 'name': 'Lady Gaga',\n", + " 'type': 'artist',\n", + " 'uri': 'spotify:artist:1HY2Jd0NmPuamShAr6KMms'}]" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "first_track.get(\"artists\", []) if first_track else []" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1adc70b5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Lady Gaga\n" + ] + } + ], + "source": [ + "if first_track:\n", + " for artist in first_track.get(\"artists\", []):\n", + " print(artist.get(\"name\"))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9af65961", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'1QV6tiMFM6fSOKOGLMHYYg'" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "first_track.get(\"id\") if first_track else None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bdde879e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Poker Face'" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "first_track.get(\"name\") if first_track else None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f6b5d13", + "metadata": {}, + "outputs": [], + "source": [ + "first_track.get(\"popularity\") if first_track else None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25809a2c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'spotify:track:1QV6tiMFM6fSOKOGLMHYYg'" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "first_track.get(\"uri\") if first_track else None" + ] + }, + { + "cell_type": "markdown", + "id": "950b759f", + "metadata": {}, + "source": [ + "## 8. Clean DataFrame from search results" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6f39fe5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
track_nametrack_idpopularityurialbum_namealbum_release_dateartists
0Poker Face1QV6tiMFM6fSOKOGLMHYYgNonespotify:track:1QV6tiMFM6fSOKOGLMHYYgThe Fame2008-01-01Lady Gaga
1Die With A Smile2plbrEY59IikOBgBGLjaoeNonespotify:track:2plbrEY59IikOBgBGLjaoeDie With A Smile2024-08-16Lady Gaga, Bruno Mars
2Abracadabra2LHNTC9QZxsL3nWpt8iaSRNonespotify:track:2LHNTC9QZxsL3nWpt8iaSRMAYHEM2025-03-07Lady Gaga
3Bad Romance0SiywuOBRcynK0uKGWdCnnNonespotify:track:0SiywuOBRcynK0uKGWdCnnThe Fame Monster (Deluxe Edition)2009-11-05Lady Gaga
4Shallow2VxeLyX666F8uXCJ0dZF8BNonespotify:track:2VxeLyX666F8uXCJ0dZF8BA Star Is Born Soundtrack2018-10-05Lady Gaga, Bradley Cooper
5Just Dance1dzQoRqT5ucxXVaAhTcT0JNonespotify:track:1dzQoRqT5ucxXVaAhTcT0JThe Fame2008-01-01Lady Gaga, Colby O'Donis
6Always Remember Us This Way2rbDhOo9Fh61Bbu23T2qCkNonespotify:track:2rbDhOo9Fh61Bbu23T2qCkA Star Is Born Soundtrack (Without Dialogue)2018-10-05Lady Gaga
7Judas7F25roCtYi55JouckaayPCNonespotify:track:7F25roCtYi55JouckaayPCBorn This Way (International Special Edition V...2011-05-23Lady Gaga
8Abracadabra5ZLUm9eab8y3tqQ1OhQSHINonespotify:track:5ZLUm9eab8y3tqQ1OhQSHIAbracadabra2025-02-03Lady Gaga
9Telephone4TCL0qqKyqsMZml0G3M9IMNonespotify:track:4TCL0qqKyqsMZml0G3M9IMThe Fame Monster (Deluxe Edition)2009-11-05Lady Gaga, Beyoncé
\n", + "
" + ], + "text/plain": [ + " track_name track_id popularity \\\n", + "0 Poker Face 1QV6tiMFM6fSOKOGLMHYYg None \n", + "1 Die With A Smile 2plbrEY59IikOBgBGLjaoe None \n", + "2 Abracadabra 2LHNTC9QZxsL3nWpt8iaSR None \n", + "3 Bad Romance 0SiywuOBRcynK0uKGWdCnn None \n", + "4 Shallow 2VxeLyX666F8uXCJ0dZF8B None \n", + "5 Just Dance 1dzQoRqT5ucxXVaAhTcT0J None \n", + "6 Always Remember Us This Way 2rbDhOo9Fh61Bbu23T2qCk None \n", + "7 Judas 7F25roCtYi55JouckaayPC None \n", + "8 Abracadabra 5ZLUm9eab8y3tqQ1OhQSHI None \n", + "9 Telephone 4TCL0qqKyqsMZml0G3M9IM None \n", + "\n", + " uri \\\n", + "0 spotify:track:1QV6tiMFM6fSOKOGLMHYYg \n", + "1 spotify:track:2plbrEY59IikOBgBGLjaoe \n", + "2 spotify:track:2LHNTC9QZxsL3nWpt8iaSR \n", + "3 spotify:track:0SiywuOBRcynK0uKGWdCnn \n", + "4 spotify:track:2VxeLyX666F8uXCJ0dZF8B \n", + "5 spotify:track:1dzQoRqT5ucxXVaAhTcT0J \n", + "6 spotify:track:2rbDhOo9Fh61Bbu23T2qCk \n", + "7 spotify:track:7F25roCtYi55JouckaayPC \n", + "8 spotify:track:5ZLUm9eab8y3tqQ1OhQSHI \n", + "9 spotify:track:4TCL0qqKyqsMZml0G3M9IM \n", + "\n", + " album_name album_release_date \\\n", + "0 The Fame 2008-01-01 \n", + "1 Die With A Smile 2024-08-16 \n", + "2 MAYHEM 2025-03-07 \n", + "3 The Fame Monster (Deluxe Edition) 2009-11-05 \n", + "4 A Star Is Born Soundtrack 2018-10-05 \n", + "5 The Fame 2008-01-01 \n", + "6 A Star Is Born Soundtrack (Without Dialogue) 2018-10-05 \n", + "7 Born This Way (International Special Edition V... 2011-05-23 \n", + "8 Abracadabra 2025-02-03 \n", + "9 The Fame Monster (Deluxe Edition) 2009-11-05 \n", + "\n", + " artists \n", + "0 Lady Gaga \n", + "1 Lady Gaga, Bruno Mars \n", + "2 Lady Gaga \n", + "3 Lady Gaga \n", + "4 Lady Gaga, Bradley Cooper \n", + "5 Lady Gaga, Colby O'Donis \n", + "6 Lady Gaga \n", + "7 Lady Gaga \n", + "8 Lady Gaga \n", + "9 Lady Gaga, Beyoncé " + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "track_items = results.get(\"tracks\", {}).get(\"items\", [])\n", + "tracks_df = pd.DataFrame([extract_track_summary(track) for track in track_items])\n", + "tracks_df.head(10)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b3dda129", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(10, 7)" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tracks_df.shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1280878f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
track_nameartistsalbum_namepopularity
0Poker FaceLady GagaThe FameNone
1Die With A SmileLady Gaga, Bruno MarsDie With A SmileNone
2AbracadabraLady GagaMAYHEMNone
3Bad RomanceLady GagaThe Fame Monster (Deluxe Edition)None
4ShallowLady Gaga, Bradley CooperA Star Is Born SoundtrackNone
5Just DanceLady Gaga, Colby O'DonisThe FameNone
6Always Remember Us This WayLady GagaA Star Is Born Soundtrack (Without Dialogue)None
7JudasLady GagaBorn This Way (International Special Edition V...None
8AbracadabraLady GagaAbracadabraNone
9TelephoneLady Gaga, BeyoncéThe Fame Monster (Deluxe Edition)None
\n", + "
" + ], + "text/plain": [ + " track_name artists \\\n", + "0 Poker Face Lady Gaga \n", + "1 Die With A Smile Lady Gaga, Bruno Mars \n", + "2 Abracadabra Lady Gaga \n", + "3 Bad Romance Lady Gaga \n", + "4 Shallow Lady Gaga, Bradley Cooper \n", + "5 Just Dance Lady Gaga, Colby O'Donis \n", + "6 Always Remember Us This Way Lady Gaga \n", + "7 Judas Lady Gaga \n", + "8 Abracadabra Lady Gaga \n", + "9 Telephone Lady Gaga, Beyoncé \n", + "\n", + " album_name popularity \n", + "0 The Fame None \n", + "1 Die With A Smile None \n", + "2 MAYHEM None \n", + "3 The Fame Monster (Deluxe Edition) None \n", + "4 A Star Is Born Soundtrack None \n", + "5 The Fame None \n", + "6 A Star Is Born Soundtrack (Without Dialogue) None \n", + "7 Born This Way (International Special Edition V... None \n", + "8 Abracadabra None \n", + "9 The Fame Monster (Deluxe Edition) None " + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "tracks_df[[\"track_name\", \"artists\", \"album_name\", \"popularity\"]].head(10)" + ] + }, + { + "cell_type": "markdown", + "id": "89291bdb", + "metadata": {}, + "source": [ + "## Exercise 1: Discovering New Music through Your Favorite Artists" + ] + }, + { + "cell_type": "markdown", + "id": "a3e7509e", + "metadata": {}, + "source": [ + "### 9. Favorite artists list" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "91c1c6a8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['Los Fabulosos Cadillacs', 'Manu Chao', 'Muchachito Bombo Infierno']" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "artists = [\"Los Fabulosos Cadillacs\", \"Manu Chao\", \"Muchachito Bombo Infierno\"]\n", + "artists" + ] + }, + { + "cell_type": "markdown", + "id": "8d099578", + "metadata": {}, + "source": [ + "### 10. Helper: get artist ID safely" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "291fefd7", + "metadata": {}, + "outputs": [], + "source": [ + "def get_artist_id(artist_name: str):\n", + " \"\"\"\n", + " Search for an artist and return the first matching artist ID.\n", + " \"\"\"\n", + " result = safe_search(sp, q=artist_name, search_type=\"artist\", limit=1)\n", + " items = result.get(\"artists\", {}).get(\"items\", [])\n", + "\n", + " if not items:\n", + " print(f\"No artist found for: {artist_name}\")\n", + " return None\n", + "\n", + " return items[0].get(\"id\")" + ] + }, + { + "cell_type": "markdown", + "id": "0cd51048", + "metadata": {}, + "source": [ + "### 11. Function: get_top_tracks" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb73ab5c", + "metadata": {}, + "outputs": [], + "source": [ + "def get_top_tracks(artist_name: str, limit: int = 5, market: str = \"ES\") -> list:\n", + " \"\"\"\n", + " Return the first `limit` top tracks for a given artist.\n", + " Uses artist ID + artist_top_tracks when available.\n", + " Includes fallback and error handling.\n", + " \"\"\"\n", + " artist_id = get_artist_id(artist_name)\n", + "\n", + " if not artist_id:\n", + " return []\n", + "\n", + " try:\n", + " top_tracks = sp.artist_top_tracks(artist_id, country=market)\n", + " tracks = top_tracks.get(\"tracks\", [])\n", + "\n", + " track_names = []\n", + " for track in tracks[:limit]:\n", + " name = track.get(\"name\")\n", + " if name:\n", + " track_names.append(name)\n", + "\n", + " return track_names\n", + "\n", + " except SpotifyException as e:\n", + " status = getattr(e, \"http_status\", \"N/A\")\n", + " print(f\"SpotifyException | top tracks failed for '{artist_name}' | status={status} | message={e}\")\n", + "\n", + " except requests.exceptions.RequestException as e:\n", + " print(f\"Network error while fetching top tracks for '{artist_name}': {e}\")\n", + "\n", + " except Exception as e:\n", + " print(f\"Unexpected error while fetching top tracks for '{artist_name}': {e}\")\n", + "\n", + " # Fallback: use search if artist_top_tracks fails\n", + " try:\n", + " result = safe_search(sp, q=f\"artist:{artist_name}\", search_type=\"track\", limit=10)\n", + " items = result.get(\"tracks\", {}).get(\"items\", [])\n", + "\n", + " filtered = []\n", + " for t in items:\n", + " artists_in_track = [a.get(\"name\", \"\").lower() for a in t.get(\"artists\", [])]\n", + " if artist_name.lower() in artists_in_track:\n", + " filtered.append(t.get(\"name\"))\n", + "\n", + " # Remove duplicates while preserving order\n", + " seen = set()\n", + " cleaned = []\n", + " for t in filtered:\n", + " if t and t.lower() not in seen:\n", + " seen.add(t.lower())\n", + " cleaned.append(t)\n", + "\n", + " return cleaned[:limit]\n", + "\n", + " except Exception as e:\n", + " print(f\"Fallback search also failed for '{artist_name}': {e}\")\n", + " return []" + ] + }, + { + "cell_type": "markdown", + "id": "85ce7cfa", + "metadata": {}, + "source": [ + "### 12. Get top tracks for all favorite artists" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a3c79a54", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "HTTP Error for GET to https://api.spotify.com/v1/artists/2FS22haX3FYbyOsUAkuYqZ/top-tracks with Params: {'country': 'ES'} returned 403 due to Forbidden\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SpotifyException | top tracks failed for 'Los Fabulosos Cadillacs' | status=403 | message=http status: 403, code: -1 - https://api.spotify.com/v1/artists/2FS22haX3FYbyOsUAkuYqZ/top-tracks?country=ES:\n", + " Forbidden, reason: None\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "HTTP Error for GET to https://api.spotify.com/v1/artists/6wH6iStAh4KIaWfuhf0NYM/top-tracks with Params: {'country': 'ES'} returned 403 due to Forbidden\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SpotifyException | top tracks failed for 'Manu Chao' | status=403 | message=http status: 403, code: -1 - https://api.spotify.com/v1/artists/6wH6iStAh4KIaWfuhf0NYM/top-tracks?country=ES:\n", + " Forbidden, reason: None\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "HTTP Error for GET to https://api.spotify.com/v1/artists/6529KpSlnP8S0kXhx7ayBb/top-tracks with Params: {'country': 'ES'} returned 403 due to Forbidden\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SpotifyException | top tracks failed for 'Muchachito Bombo Infierno' | status=403 | message=http status: 403, code: -1 - https://api.spotify.com/v1/artists/6529KpSlnP8S0kXhx7ayBb/top-tracks?country=ES:\n", + " Forbidden, reason: None\n" + ] + }, + { + "data": { + "text/plain": [ + "[['Siguiendo la Luna - Remasterizado 2008',\n", + " 'Mal Bicho - Remasterizado 2008',\n", + " 'Manuel Santillán, el León (Versión Reggae) - Remasterizado 2008',\n", + " 'Matador - Remasterizado 2008',\n", + " 'Vasos Vacíos - Remasterizado 2008'],\n", + " ['Me Gustas Tu', 'Bongo Bong', \"Je ne t'aime plus\", 'A me mi piace'],\n", + " ['Siempre que quiera', 'Luna', 'Caraguapa', 'La quiero a morir', 'Tu nombre']]" + ] + }, + "execution_count": 34, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "top_tracks_list = [get_top_tracks(artist, limit=5) for artist in artists]\n", + "top_tracks_list" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65ef4500", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Artist: Los Fabulosos Cadillacs\n", + " - Siguiendo la Luna - Remasterizado 2008\n", + " - Mal Bicho - Remasterizado 2008\n", + " - Manuel Santillán, el León (Versión Reggae) - Remasterizado 2008\n", + " - Matador - Remasterizado 2008\n", + " - Vasos Vacíos - Remasterizado 2008\n", + "\n", + "Artist: Manu Chao\n", + " - Me Gustas Tu\n", + " - Bongo Bong\n", + " - Je ne t'aime plus\n", + " - A me mi piace\n", + "\n", + "Artist: Muchachito Bombo Infierno\n", + " - Siempre que quiera\n", + " - Luna\n", + " - Caraguapa\n", + " - La quiero a morir\n", + " - Tu nombre\n" + ] + } + ], + "source": [ + "for artist, tracks in zip(artists, top_tracks_list):\n", + " print(f\"\\nArtist: {artist}\")\n", + " if tracks:\n", + " for track in tracks:\n", + " print(f\" - {track}\")\n", + " else:\n", + " print(\" - No tracks found\")" + ] + }, + { + "cell_type": "markdown", + "id": "5b24e0cb", + "metadata": {}, + "source": [ + "### 13. Function: find_related_artists" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6fb16584", + "metadata": {}, + "outputs": [], + "source": [ + "def find_related_artists(artist_name: str, limit: int = 5) -> list:\n", + " \"\"\"\n", + " Return the first `limit` related artists for a given artist.\n", + " Uses artist_related_artists when available.\n", + " If it fails, falls back to recommendations seeded by the artist.\n", + " \"\"\"\n", + " artist_id = get_artist_id(artist_name)\n", + "\n", + " if not artist_id:\n", + " return []\n", + "\n", + " # First attempt: artist_related_artists\n", + " try:\n", + " related = sp.artist_related_artists(artist_id)\n", + " items = related.get(\"artists\", [])\n", + "\n", + " names = []\n", + " for artist in items[:limit]:\n", + " name = artist.get(\"name\")\n", + " if name:\n", + " names.append(name)\n", + "\n", + " if names:\n", + " return names\n", + "\n", + " except SpotifyException as e:\n", + " status = getattr(e, \"http_status\", \"N/A\")\n", + " print(f\"artist_related_artists failed for '{artist_name}' | status={status} | message={e}\")\n", + "\n", + " except requests.exceptions.RequestException as e:\n", + " print(f\"Network error while fetching related artists for '{artist_name}': {e}\")\n", + "\n", + " except Exception as e:\n", + " print(f\"Unexpected error in related artists for '{artist_name}': {e}\")\n", + "\n", + " # Fallback: recommendations\n", + " try:\n", + " recs = sp.recommendations(seed_artists=[artist_id], limit=50)\n", + " tracks = recs.get(\"tracks\", [])\n", + "\n", + " related_names = []\n", + " seen = set()\n", + " original_lower = artist_name.lower()\n", + "\n", + " for track in tracks:\n", + " for artist in track.get(\"artists\", []):\n", + " name = artist.get(\"name\", \"\")\n", + " lower_name = name.lower()\n", + "\n", + " if lower_name == original_lower:\n", + " continue\n", + " if lower_name in seen:\n", + " continue\n", + "\n", + " seen.add(lower_name)\n", + " related_names.append(name)\n", + "\n", + " if len(related_names) >= limit:\n", + " return related_names\n", + "\n", + " return related_names\n", + "\n", + " except SpotifyException as e:\n", + " status = getattr(e, \"http_status\", \"N/A\")\n", + " print(f\"recommendations fallback failed for '{artist_name}' | status={status} | message={e}\")\n", + " except requests.exceptions.RequestException as e:\n", + " print(f\"Network error in recommendations fallback for '{artist_name}': {e}\")\n", + " except Exception as e:\n", + " print(f\"Unexpected error in recommendations fallback for '{artist_name}': {e}\")\n", + "\n", + " return []" + ] + }, + { + "cell_type": "markdown", + "id": "f1506e2f", + "metadata": {}, + "source": [ + "### 14. Get related artists for all favorite artists" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8d448096", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "HTTP Error for GET to https://api.spotify.com/v1/artists/2FS22haX3FYbyOsUAkuYqZ/related-artists with Params: {} returned 403 due to Forbidden\n", + "HTTP Error for GET to https://api.spotify.com/v1/recommendations with Params: {'limit': 50, 'seed_artists': '2FS22haX3FYbyOsUAkuYqZ'} returned 404 due to None\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "artist_related_artists failed for 'Los Fabulosos Cadillacs' | status=403 | message=http status: 403, code: -1 - https://api.spotify.com/v1/artists/2FS22haX3FYbyOsUAkuYqZ/related-artists:\n", + " Forbidden, reason: None\n", + "recommendations fallback failed for 'Los Fabulosos Cadillacs' | status=404 | message=http status: 404, code: -1 - https://api.spotify.com/v1/recommendations?limit=50&seed_artists=2FS22haX3FYbyOsUAkuYqZ:\n", + " None, reason: None\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "HTTP Error for GET to https://api.spotify.com/v1/artists/6wH6iStAh4KIaWfuhf0NYM/related-artists with Params: {} returned 403 due to Forbidden\n", + "HTTP Error for GET to https://api.spotify.com/v1/recommendations with Params: {'limit': 50, 'seed_artists': '6wH6iStAh4KIaWfuhf0NYM'} returned 404 due to None\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "artist_related_artists failed for 'Manu Chao' | status=403 | message=http status: 403, code: -1 - https://api.spotify.com/v1/artists/6wH6iStAh4KIaWfuhf0NYM/related-artists:\n", + " Forbidden, reason: None\n", + "recommendations fallback failed for 'Manu Chao' | status=404 | message=http status: 404, code: -1 - https://api.spotify.com/v1/recommendations?limit=50&seed_artists=6wH6iStAh4KIaWfuhf0NYM:\n", + " None, reason: None\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "HTTP Error for GET to https://api.spotify.com/v1/artists/6529KpSlnP8S0kXhx7ayBb/related-artists with Params: {} returned 403 due to Forbidden\n", + "HTTP Error for GET to https://api.spotify.com/v1/recommendations with Params: {'limit': 50, 'seed_artists': '6529KpSlnP8S0kXhx7ayBb'} returned 404 due to None\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "artist_related_artists failed for 'Muchachito Bombo Infierno' | status=403 | message=http status: 403, code: -1 - https://api.spotify.com/v1/artists/6529KpSlnP8S0kXhx7ayBb/related-artists:\n", + " Forbidden, reason: None\n", + "recommendations fallback failed for 'Muchachito Bombo Infierno' | status=404 | message=http status: 404, code: -1 - https://api.spotify.com/v1/recommendations?limit=50&seed_artists=6529KpSlnP8S0kXhx7ayBb:\n", + " None, reason: None\n" + ] + }, + { + "data": { + "text/plain": [ + "[[], [], []]" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "related_artists_list = [find_related_artists(artist, limit=5) for artist in artists]\n", + "related_artists_list" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9a110941", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Artist: Los Fabulosos Cadillacs\n", + " - No related artists found\n", + "\n", + "Artist: Manu Chao\n", + " - No related artists found\n", + "\n", + "Artist: Muchachito Bombo Infierno\n", + " - No related artists found\n" + ] + } + ], + "source": [ + "for artist, related in zip(artists, related_artists_list):\n", + " print(f\"\\nArtist: {artist}\")\n", + " if related:\n", + " for r in related:\n", + " print(f\" - {r}\")\n", + " else:\n", + " print(\" - No related artists found\")" + ] + }, + { + "cell_type": "markdown", + "id": "ff40a4f5", + "metadata": {}, + "source": [ + "### 15. Challenge: create a discovery playlist structure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4df6cc95", + "metadata": {}, + "outputs": [], + "source": [ + "playlist_tracks = []\n", + "\n", + "# Add top tracks from favorite artists\n", + "for artist, track_list in zip(artists, top_tracks_list):\n", + " for track in track_list:\n", + " playlist_tracks.append({\n", + " \"artist\": artist,\n", + " \"track\": track,\n", + " \"source\": \"favorite_artist\"\n", + " })\n", + "\n", + "# Add top tracks from related artists\n", + "for related_group in related_artists_list:\n", + " for related_artist in related_group:\n", + " related_top_tracks = get_top_tracks(related_artist, limit=2)\n", + " for track in related_top_tracks:\n", + " playlist_tracks.append({\n", + " \"artist\": related_artist,\n", + " \"track\": track,\n", + " \"source\": \"related_artist\"\n", + " })" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d52d5f24", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
artisttracksource
0Los Fabulosos CadillacsSiguiendo la Luna - Remasterizado 2008favorite_artist
1Los Fabulosos CadillacsMal Bicho - Remasterizado 2008favorite_artist
2Los Fabulosos CadillacsManuel Santillán, el León (Versión Reggae) - R...favorite_artist
3Los Fabulosos CadillacsMatador - Remasterizado 2008favorite_artist
4Los Fabulosos CadillacsVasos Vacíos - Remasterizado 2008favorite_artist
5Manu ChaoMe Gustas Tufavorite_artist
6Manu ChaoBongo Bongfavorite_artist
7Manu ChaoJe ne t'aime plusfavorite_artist
8Manu ChaoA me mi piacefavorite_artist
9Muchachito Bombo InfiernoSiempre que quierafavorite_artist
10Muchachito Bombo InfiernoLunafavorite_artist
11Muchachito Bombo InfiernoCaraguapafavorite_artist
12Muchachito Bombo InfiernoLa quiero a morirfavorite_artist
13Muchachito Bombo InfiernoTu nombrefavorite_artist
\n", + "
" + ], + "text/plain": [ + " artist \\\n", + "0 Los Fabulosos Cadillacs \n", + "1 Los Fabulosos Cadillacs \n", + "2 Los Fabulosos Cadillacs \n", + "3 Los Fabulosos Cadillacs \n", + "4 Los Fabulosos Cadillacs \n", + "5 Manu Chao \n", + "6 Manu Chao \n", + "7 Manu Chao \n", + "8 Manu Chao \n", + "9 Muchachito Bombo Infierno \n", + "10 Muchachito Bombo Infierno \n", + "11 Muchachito Bombo Infierno \n", + "12 Muchachito Bombo Infierno \n", + "13 Muchachito Bombo Infierno \n", + "\n", + " track source \n", + "0 Siguiendo la Luna - Remasterizado 2008 favorite_artist \n", + "1 Mal Bicho - Remasterizado 2008 favorite_artist \n", + "2 Manuel Santillán, el León (Versión Reggae) - R... favorite_artist \n", + "3 Matador - Remasterizado 2008 favorite_artist \n", + "4 Vasos Vacíos - Remasterizado 2008 favorite_artist \n", + "5 Me Gustas Tu favorite_artist \n", + "6 Bongo Bong favorite_artist \n", + "7 Je ne t'aime plus favorite_artist \n", + "8 A me mi piace favorite_artist \n", + "9 Siempre que quiera favorite_artist \n", + "10 Luna favorite_artist \n", + "11 Caraguapa favorite_artist \n", + "12 La quiero a morir favorite_artist \n", + "13 Tu nombre favorite_artist " + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "playlist_df = pd.DataFrame(playlist_tracks)\n", + "playlist_df.head(20)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e5d7125b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(14, 3)" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "playlist_df.shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "222f7b8e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
artisttracksource
0Los Fabulosos CadillacsSiguiendo la Luna - Remasterizado 2008favorite_artist
1Los Fabulosos CadillacsMal Bicho - Remasterizado 2008favorite_artist
2Los Fabulosos CadillacsManuel Santillán, el León (Versión Reggae) - R...favorite_artist
3Los Fabulosos CadillacsMatador - Remasterizado 2008favorite_artist
4Los Fabulosos CadillacsVasos Vacíos - Remasterizado 2008favorite_artist
5Manu ChaoMe Gustas Tufavorite_artist
6Manu ChaoBongo Bongfavorite_artist
7Manu ChaoJe ne t'aime plusfavorite_artist
8Manu ChaoA me mi piacefavorite_artist
9Muchachito Bombo InfiernoSiempre que quierafavorite_artist
10Muchachito Bombo InfiernoLunafavorite_artist
11Muchachito Bombo InfiernoCaraguapafavorite_artist
12Muchachito Bombo InfiernoLa quiero a morirfavorite_artist
13Muchachito Bombo InfiernoTu nombrefavorite_artist
\n", + "
" + ], + "text/plain": [ + " artist \\\n", + "0 Los Fabulosos Cadillacs \n", + "1 Los Fabulosos Cadillacs \n", + "2 Los Fabulosos Cadillacs \n", + "3 Los Fabulosos Cadillacs \n", + "4 Los Fabulosos Cadillacs \n", + "5 Manu Chao \n", + "6 Manu Chao \n", + "7 Manu Chao \n", + "8 Manu Chao \n", + "9 Muchachito Bombo Infierno \n", + "10 Muchachito Bombo Infierno \n", + "11 Muchachito Bombo Infierno \n", + "12 Muchachito Bombo Infierno \n", + "13 Muchachito Bombo Infierno \n", + "\n", + " track source \n", + "0 Siguiendo la Luna - Remasterizado 2008 favorite_artist \n", + "1 Mal Bicho - Remasterizado 2008 favorite_artist \n", + "2 Manuel Santillán, el León (Versión Reggae) - R... favorite_artist \n", + "3 Matador - Remasterizado 2008 favorite_artist \n", + "4 Vasos Vacíos - Remasterizado 2008 favorite_artist \n", + "5 Me Gustas Tu favorite_artist \n", + "6 Bongo Bong favorite_artist \n", + "7 Je ne t'aime plus favorite_artist \n", + "8 A me mi piace favorite_artist \n", + "9 Siempre que quiera favorite_artist \n", + "10 Luna favorite_artist \n", + "11 Caraguapa favorite_artist \n", + "12 La quiero a morir favorite_artist \n", + "13 Tu nombre favorite_artist " + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "playlist_df.drop_duplicates(subset=[\"artist\", \"track\"]).reset_index(drop=True).head(30)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7bb99875", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Generated discovery playlist:\n", + "Los Fabulosos Cadillacs - Siguiendo la Luna - Remasterizado 2008 (favorite_artist)\n", + "Los Fabulosos Cadillacs - Mal Bicho - Remasterizado 2008 (favorite_artist)\n", + "Los Fabulosos Cadillacs - Manuel Santillán, el León (Versión Reggae) - Remasterizado 2008 (favorite_artist)\n", + "Los Fabulosos Cadillacs - Matador - Remasterizado 2008 (favorite_artist)\n", + "Los Fabulosos Cadillacs - Vasos Vacíos - Remasterizado 2008 (favorite_artist)\n", + "Manu Chao - Me Gustas Tu (favorite_artist)\n", + "Manu Chao - Bongo Bong (favorite_artist)\n", + "Manu Chao - Je ne t'aime plus (favorite_artist)\n", + "Manu Chao - A me mi piace (favorite_artist)\n", + "Muchachito Bombo Infierno - Siempre que quiera (favorite_artist)\n", + "Muchachito Bombo Infierno - Luna (favorite_artist)\n", + "Muchachito Bombo Infierno - Caraguapa (favorite_artist)\n", + "Muchachito Bombo Infierno - La quiero a morir (favorite_artist)\n", + "Muchachito Bombo Infierno - Tu nombre (favorite_artist)\n" + ] + } + ], + "source": [ + "print(\"Generated discovery playlist:\")\n", + "for _, row in playlist_df.drop_duplicates(subset=[\"artist\", \"track\"]).iterrows():\n", + " print(f\"{row['artist']} - {row['track']} ({row['source']})\")" + ] + }, + { + "cell_type": "markdown", + "id": "44acfa6c", + "metadata": {}, + "source": [ + "## Playlists" + ] + }, + { + "cell_type": "markdown", + "id": "ab713c3f", + "metadata": {}, + "source": [ + "### 16. Safe playlist helpers" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8e7c77ec", + "metadata": {}, + "outputs": [], + "source": [ + "def safe_featured_playlists(limit: int = 5):\n", + " \"\"\"\n", + " Try to fetch Spotify featured playlists.\n", + " If the endpoint is unavailable or forbidden, return an empty list.\n", + " \"\"\"\n", + " try:\n", + " results = sp.featured_playlists(limit=limit)\n", + " items = results.get(\"playlists\", {}).get(\"items\", [])\n", + "\n", + " cleaned = []\n", + " for pl in items:\n", + " if not pl:\n", + " continue\n", + " cleaned.append({\n", + " \"name\": pl.get(\"name\"),\n", + " \"id\": pl.get(\"id\")\n", + " })\n", + "\n", + " return cleaned\n", + "\n", + " except SpotifyException as e:\n", + " status = getattr(e, \"http_status\", \"N/A\")\n", + " print(f\"featured_playlists failed | status={status} | message={e}\")\n", + " return []\n", + "\n", + " except requests.exceptions.RequestException as e:\n", + " print(f\"Network error while fetching featured playlists: {e}\")\n", + " return []\n", + "\n", + " except Exception as e:\n", + " print(f\"Unexpected error while fetching featured playlists: {e}\")\n", + " return []" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "04926ae5", + "metadata": {}, + "outputs": [], + "source": [ + "def search_playlists_alternative(query: str = \"Top Hits\", limit: int = 5):\n", + " \"\"\"\n", + " Fallback method for playlists using search instead of featured_playlists.\n", + " This is useful if featured_playlists is unavailable or restricted.\n", + " \"\"\"\n", + " result = safe_search(sp, q=query, search_type=\"playlist\", limit=limit)\n", + " items = result.get(\"playlists\", {}).get(\"items\", [])\n", + "\n", + " cleaned = []\n", + " for pl in items:\n", + " if not pl:\n", + " continue\n", + " cleaned.append({\n", + " \"name\": pl.get(\"name\"),\n", + " \"id\": pl.get(\"id\")\n", + " })\n", + "\n", + " return cleaned" + ] + }, + { + "cell_type": "markdown", + "id": "4e274780", + "metadata": {}, + "source": [ + "### 17. Exercise 2-Featured Exploration" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc70574a", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "HTTP Error for GET to https://api.spotify.com/v1/browse/featured-playlists with Params: {'locale': None, 'country': None, 'timestamp': None, 'limit': 5, 'offset': 0} returned 403 due to Forbidden\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "featured_playlists failed | status=403 | message=http status: 403, code: -1 - https://api.spotify.com/v1/browse/featured-playlists?limit=5&offset=0:\n", + " Forbidden, reason: None\n", + "featured_playlists was unavailable. Using search-based playlist exploration instead.\n" + ] + }, + { + "data": { + "text/plain": [ + "[{'name': 'Hits 2026 🔥 🔥 Aktuelle Charts und die besten Songs 2026 🔝 🙌',\n", + " 'id': '1CnDCN10TJZjw6K2H3gNRv'},\n", + " {'name': 'Hits 2026 🌼 Top 100 Charts & Radio Songs · Mega Hits ',\n", + " 'id': '54kK2yufF2hgWtzI8hhBeH'}]" + ] + }, + "execution_count": 46, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "featured = safe_featured_playlists(limit=5)\n", + "\n", + "if not featured:\n", + " print(\"featured_playlists was unavailable. Using search-based playlist exploration instead.\")\n", + " featured = search_playlists_alternative(query=\"Top Hits\", limit=5)\n", + "\n", + "featured" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "80e3b472", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
nameid
0Hits 2026 🔥 🔥 Aktuelle Charts und die besten ...1CnDCN10TJZjw6K2H3gNRv
1Hits 2026 🌼 Top 100 Charts & Radio Songs · Meg...54kK2yufF2hgWtzI8hhBeH
\n", + "
" + ], + "text/plain": [ + " name id\n", + "0 Hits 2026 🔥 🔥 Aktuelle Charts und die besten ... 1CnDCN10TJZjw6K2H3gNRv\n", + "1 Hits 2026 🌼 Top 100 Charts & Radio Songs · Meg... 54kK2yufF2hgWtzI8hhBeH" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "featured_df = pd.DataFrame(featured)\n", + "featured_df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72db03b1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Top 5 playlists:\n", + "Name: Hits 2026 🔥 🔥 Aktuelle Charts und die besten Songs 2026 🔝 🙌 | ID: 1CnDCN10TJZjw6K2H3gNRv\n", + "Name: Hits 2026 🌼 Top 100 Charts & Radio Songs · Mega Hits | ID: 54kK2yufF2hgWtzI8hhBeH\n" + ] + } + ], + "source": [ + "print(\"Top 5 playlists:\")\n", + "for pl in featured:\n", + " print(f\"Name: {pl['name']} | ID: {pl['id']}\")" + ] + }, + { + "cell_type": "markdown", + "id": "70467456", + "metadata": {}, + "source": [ + "### 18. Choose one playlist" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43b1bbc1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'1CnDCN10TJZjw6K2H3gNRv'" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "playlist_id = featured[0][\"id\"] if featured else None\n", + "playlist_id" + ] + }, + { + "cell_type": "markdown", + "id": "11120ebd", + "metadata": {}, + "source": [ + "### 19. Safe playlist details" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb09e553", + "metadata": {}, + "outputs": [], + "source": [ + "def safe_get_playlist_details(playlist_id: str):\n", + " \"\"\"\n", + " Fetch playlist details safely.\n", + " Returns a dict or None.\n", + " \"\"\"\n", + " if not playlist_id:\n", + " print(\"No playlist_id provided.\")\n", + " return None\n", + "\n", + " try:\n", + " playlist = sp.playlist(playlist_id)\n", + " return playlist\n", + "\n", + " except SpotifyException as e:\n", + " status = getattr(e, \"http_status\", \"N/A\")\n", + " print(f\"Playlist details failed | status={status} | playlist_id={playlist_id} | message={e}\")\n", + " return None\n", + "\n", + " except requests.exceptions.RequestException as e:\n", + " print(f\"Network error while fetching playlist details: {e}\")\n", + " return None\n", + "\n", + " except Exception as e:\n", + " print(f\"Unexpected error while fetching playlist details: {e}\")\n", + " return None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f0c29779", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'collaborative': False,\n", + " 'description': 'Entdecke die ultimative Playlist mit den heißesten und neuesten Hits! Hier findest du alle Top-Songs aus dem Radio und den Charts, einschließlich Nina Chuba, Apache207, Miley Cyrus, Harry Styles und vielen anderen.',\n", + " 'external_urls': {'spotify': 'https://open.spotify.com/playlist/1CnDCN10TJZjw6K2H3gNRv'},\n", + " 'followers': {'href': None, 'total': 151874},\n", + " 'href': 'https://api.spotify.com/v1/playlists/1CnDCN10TJZjw6K2H3gNRv?additional_types=track',\n", + " 'id': '1CnDCN10TJZjw6K2H3gNRv',\n", + " 'images': [{'height': None,\n", + " 'url': 'https://image-cdn-ak.spotifycdn.com/image/ab67706c0000da842efcb7c0773121dd6140284b',\n", + " 'width': None}],\n", + " 'name': 'Hits 2026 🔥 🔥 Aktuelle Charts und die besten Songs 2026 🔝 🙌',\n", + " 'owner': {'display_name': 'Filtr Germany',\n", + " 'external_urls': {'spotify': 'https://open.spotify.com/user/playlists.germany'},\n", + " 'href': 'https://api.spotify.com/v1/users/playlists.germany',\n", + " 'id': 'playlists.germany',\n", + " 'type': 'user',\n", + " 'uri': 'spotify:user:playlists.germany'},\n", + " 'primary_color': None,\n", + " 'public': True,\n", + " 'snapshot_id': 'AAAGJ/c2pfu6PN0Q5RSBfyeGXMhuTebg',\n", + " 'type': 'playlist',\n", + " 'uri': 'spotify:playlist:1CnDCN10TJZjw6K2H3gNRv'}" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "playlist = safe_get_playlist_details(playlist_id)\n", + "playlist" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "62a2e35c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Playlist name: Hits 2026 🔥 🔥 Aktuelle Charts und die besten Songs 2026 🔝 🙌\n", + "Description: Entdecke die ultimative Playlist mit den heißesten und neuesten Hits! Hier findest du alle Top-Songs aus dem Radio und den Charts, einschließlich Nina Chuba, Apache207, Miley Cyrus, Harry Styles und vielen anderen.\n", + "Total tracks: None\n" + ] + } + ], + "source": [ + "if playlist:\n", + " print(\"Playlist name:\", playlist.get(\"name\"))\n", + " print(\"Description:\", playlist.get(\"description\"))\n", + " print(\"Total tracks:\", playlist.get(\"tracks\", {}).get(\"total\"))\n", + "else:\n", + " print(\"Could not retrieve playlist details.\")" + ] + }, + { + "cell_type": "markdown", + "id": "9b6af331", + "metadata": {}, + "source": [ + "### 20. Safe playlist tracks" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27c31068", + "metadata": {}, + "outputs": [], + "source": [ + "def safe_get_playlist_tracks(playlist_id: str, limit: int = 10):\n", + " \"\"\"\n", + " Fetch playlist tracks safely.\n", + " Returns a list of playlist items.\n", + " \"\"\"\n", + " if not playlist_id:\n", + " print(\"No playlist_id provided.\")\n", + " return []\n", + "\n", + " try:\n", + " results = sp.playlist_items(playlist_id, limit=limit)\n", + " return results.get(\"items\", [])\n", + "\n", + " except SpotifyException as e:\n", + " status = getattr(e, \"http_status\", \"N/A\")\n", + " print(f\"playlist_items failed | status={status} | playlist_id={playlist_id} | message={e}\")\n", + " return []\n", + "\n", + " except requests.exceptions.RequestException as e:\n", + " print(f\"Network error while fetching playlist items: {e}\")\n", + " return []\n", + "\n", + " except Exception as e:\n", + " print(f\"Unexpected error while fetching playlist items: {e}\")\n", + " return []" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9be692fc", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "HTTP Error for GET to https://api.spotify.com/v1/playlists/1CnDCN10TJZjw6K2H3gNRv/tracks with Params: {'limit': 10, 'offset': 0, 'fields': None, 'market': None, 'additional_types': 'track,episode'} returned 403 due to Forbidden\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "playlist_items failed | status=403 | playlist_id=1CnDCN10TJZjw6K2H3gNRv | message=http status: 403, code: -1 - https://api.spotify.com/v1/playlists/1CnDCN10TJZjw6K2H3gNRv/tracks?limit=10&offset=0&additional_types=track%2Cepisode:\n", + " Forbidden, reason: None\n" + ] + }, + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 54, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "playlist_items = safe_get_playlist_tracks(playlist_id, limit=10)\n", + "len(playlist_items)" + ] + }, + { + "cell_type": "markdown", + "id": "5f1bff67", + "metadata": {}, + "source": [ + "### 21. Extract and display first 10 tracks" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "68776fe0", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 55, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "first_10_track_names = []\n", + "\n", + "for item in playlist_items:\n", + " track = item.get(\"track\")\n", + " if not track:\n", + " continue\n", + "\n", + " track_name = track.get(\"name\")\n", + " if track_name:\n", + " first_10_track_names.append(track_name)\n", + "\n", + "first_10_track_names" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b4f562b6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "First 10 tracks in the chosen playlist:\n" + ] + } + ], + "source": [ + "print(\"First 10 tracks in the chosen playlist:\")\n", + "for name in first_10_track_names:\n", + " print(\"-\", name)" + ] + }, + { + "cell_type": "markdown", + "id": "87e255e0", + "metadata": {}, + "source": [ + "### 22. Dictionary: track -> artists" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a4cfe99e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{}" + ] + }, + "execution_count": 57, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "track_artists_dict = {}\n", + "\n", + "for item in playlist_items:\n", + " track = item.get(\"track\")\n", + " if not track:\n", + " continue\n", + "\n", + " track_name = track.get(\"name\")\n", + " artist_names = [artist.get(\"name\") for artist in track.get(\"artists\", []) if artist.get(\"name\")]\n", + "\n", + " if track_name:\n", + " track_artists_dict[track_name] = artist_names\n", + "\n", + "track_artists_dict" + ] + }, + { + "cell_type": "markdown", + "id": "419cb205", + "metadata": {}, + "source": [ + "### 23. Bonus: convert playlist tracks to DataFrame" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d18be38d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
" + ], + "text/plain": [ + "Empty DataFrame\n", + "Columns: []\n", + "Index: []" + ] + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "playlist_rows = []\n", + "\n", + "for item in playlist_items:\n", + " track = item.get(\"track\")\n", + " if not track:\n", + " continue\n", + "\n", + " playlist_rows.append({\n", + " \"track_name\": track.get(\"name\"),\n", + " \"artists\": \", \".join([a.get(\"name\", \"\") for a in track.get(\"artists\", [])]),\n", + " \"album_name\": track.get(\"album\", {}).get(\"name\"),\n", + " \"popularity\": track.get(\"popularity\")\n", + " })\n", + "\n", + "playlist_tracks_df = pd.DataFrame(playlist_rows)\n", + "playlist_tracks_df" + ] + }, + { + "cell_type": "markdown", + "id": "79592ca3", + "metadata": {}, + "source": [ + "### 24. Final conclusions" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "66d8e5c4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Conclusions:\n", + "1. The Spotify API can be used to search tracks, artists, and playlists.\n", + "2. The search endpoint is useful for exploring public music catalog data.\n", + "3. We successfully extracted top tracks for favorite artists and identified related artists.\n", + "4. We created a discovery playlist structure combining favorite and related artists.\n", + "5. Playlist exploration may depend on endpoint availability and app permissions, so defensive error handling is important.\n", + "6. This adapted lab is designed to work reliably with the Client Credentials flow.\n", + "\n" + ] + } + ], + "source": [ + "print(\"\"\"\n", + "Conclusions:\n", + "1. The Spotify API can be used to search tracks, artists, and playlists.\n", + "2. The search endpoint is useful for exploring public music catalog data.\n", + "3. We successfully extracted top tracks for favorite artists and identified related artists.\n", + "4. We created a discovery playlist structure combining favorite and related artists.\n", + "5. Playlist exploration may depend on endpoint availability and app permissions, so defensive error handling is important.\n", + "6. This adapted lab is designed to work reliably with the Client Credentials flow.\n", + "\"\"\")" + ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "base", "language": "python", "name": "python3" }, @@ -558,7 +3293,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13" + "version": "3.13.5" } }, "nbformat": 4,