Skip to content

Commit

Permalink
fixed remaining issues
Browse files Browse the repository at this point in the history
  • Loading branch information
heinrich26 committed Oct 16, 2024
1 parent 049b0b0 commit 3198b1f
Show file tree
Hide file tree
Showing 11 changed files with 127 additions and 173 deletions.
7 changes: 3 additions & 4 deletions tests/mixins/test_browsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pytest

from tests.test_helpers import is_ci
from ytmusicapi import LyricLine
from ytmusicapi.models.lyrics import LyricLine


class TestBrowsing:
Expand Down Expand Up @@ -174,15 +174,14 @@ def test_get_lyrics(self, config, yt, sample_video):
# test lyrics with timestamps
lyrics_song = yt.get_lyrics(playlist["lyrics"], timestamps = True)
assert lyrics_song is not None
assert isinstance(lyrics_song["lyrics"], list)
assert len(lyrics_song["lyrics"]) >= 1
assert lyrics_song["hasTimestamps"] is True

# check the LyricLine object
song = lyrics_song["lyrics"][0]
assert isinstance(song, LyricLine)
assert isinstance(song.text, str)
assert isinstance(song.start_time, int)
assert isinstance(song.end_time, int)
assert song.start_time <= song.end_time
assert isinstance(song.id, int)

playlist = yt.get_watch_playlist(config["uploads"]["private_upload_id"])
Expand Down
4 changes: 1 addition & 3 deletions ytmusicapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from ytmusicapi.setup import setup, setup_oauth
from ytmusicapi.ytmusic import YTMusic
from .mixins.browsing import Lyrics, TimedLyrics, LyricLine

try:
__version__ = version("ytmusicapi")
Expand All @@ -13,5 +12,4 @@
__copyright__ = "Copyright 2024 sigma67"
__license__ = "MIT"
__title__ = "ytmusicapi"
__all__ = ["YTMusic", "setup_oauth", "setup",
"Lyrics", "TimedLyrics", "LyricLine"]
__all__ = ["YTMusic", "setup_oauth", "setup"]
11 changes: 6 additions & 5 deletions ytmusicapi/mixins/_protocol.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""protocol that defines the functions available to mixins"""

from typing import Mapping, Optional, Protocol
from typing import Optional, Protocol
from contextlib import contextmanager

from requests import Response
from requests.structures import CaseInsensitiveDict
Expand All @@ -22,17 +23,17 @@ class MixinProtocol(Protocol):

def _check_auth(self) -> None:
"""checks if self has authentication"""
...

def _send_request(self, endpoint: str, body: dict, additionalParams: str = "") -> dict:
"""for sending post requests to YouTube Music"""
...

def _send_get_request(self, url: str, params: Optional[dict] = None) -> Response:
"""for sending get requests to YouTube Music"""
...

@contextmanager
def as_mobile(self):
"""context-manager, that allows requests as the YouTube Music Mobile-App"""

@property
def headers(self) -> CaseInsensitiveDict[str]:
"""property for getting request headers"""
...
4 changes: 2 additions & 2 deletions ytmusicapi/mixins/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from ytmusicapi.exceptions import YTMusicUserError


OrderType = Literal['a_to_z', 'z_to_a', 'recently_added']
LibraryOrderType = Literal['a_to_z', 'z_to_a', 'recently_added']


def prepare_like_endpoint(rating):
Expand All @@ -28,7 +28,7 @@ def validate_order_parameter(order):
)


def prepare_order_params(order: OrderType):
def prepare_order_params(order: LibraryOrderType):
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
Expand Down
160 changes: 16 additions & 144 deletions ytmusicapi/mixins/browsing.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from dataclasses import dataclass
import re
import warnings
from typing import Any, Optional, TypedDict, cast
from typing import Any, Optional, cast

from ytmusicapi.continuations import (
get_continuations,
Expand All @@ -18,56 +17,14 @@
)
from ytmusicapi.parsers.library import parse_albums
from ytmusicapi.parsers.playlists import parse_playlist_items
from ytmusicapi.models.lyrics import Lyrics, TimedLyrics, LyricLine

from ..exceptions import YTMusicError, YTMusicUserError
from ..navigation import *
from ._protocol import MixinProtocol
from ._utils import get_datestamp


@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.
"""
text: str
start_time: int
end_time: int
id: int

@classmethod
def from_raw(cls, raw_lyric: dict):
"""
Converts lyrics in the format from the api to a more reasonable format
:param raw_lyric: The raw lyric-data returned by the mobile api.
:return LyricLine: A `LyricLine`
"""
text = raw_lyric["lyricLine"]
cue_range = raw_lyric["cueRange"]
start_time = int(cue_range["startTimeMilliseconds"])
end_time = int(cue_range["endTimeMilliseconds"])
id = int(cue_range["metadata"]["id"])
return cls(text, start_time, end_time, id)


class Lyrics(TypedDict):
lyrics: str
source: Optional[str]
hasTimestamps: Literal[False]


class TimedLyrics(TypedDict):
lyrics: list[LyricLine]
source: Optional[str]
hasTimestamps: Literal[True]


class BrowsingMixin(MixinProtocol):
def get_home(self, limit=3) -> list[dict]:
"""
Expand Down Expand Up @@ -315,7 +272,7 @@ def get_artist(self, channelId: str) -> dict:
musicShelf = nav(results[0], MUSIC_SHELF)
if "navigationEndpoint" in nav(musicShelf, TITLE):
artist["songs"]["browseId"] = nav(musicShelf, TITLE + NAVIGATION_BROWSE_ID)
artist["songs"]["results"] = parse_playlist_items(musicShelf["contents"]) # type: ignore
artist["songs"]["results"] = parse_playlist_items(musicShelf["contents"])

artist.update(self.parser.parse_channel_contents(results))
return artist
Expand Down Expand Up @@ -885,102 +842,24 @@ def get_song_related(self, browseId: str):

@overload
def get_lyrics(self, browseId: str, timestamps: Literal[False] = False) -> Optional[Lyrics]:
"""
Returns lyrics of a song or video. When `timestamps` is set, lyrics are returned with
timestamps, if available.
:param browseId: Lyrics browse-id obtained from :py:func:`get_watch_playlist` (startswith `MPLYt`).
:param timestamps: Whether to return bare lyrics or lyrics with timestamps, if available.
:return: Dictionary with song lyrics or `None`, if no lyrics are found.
The `hasTimestamps`-key determines the format of the data.
Example when `timestamps` is set to `False`, or not timestamps are available::
{
"lyrics": "Today is gonna be the day\\nThat they're gonna throw it back to you\\n",
"source": "Source: LyricFind",
"hasTimestamps": False
}
Example when `timestamps` is set to `True` and timestamps are available::
{
"lyrics": [
LyricLine(
text="I was a liar",
start_time=9200,
end_time=10630,
id=1
),
LyricLine(
text="I gave in to the fire",
start_time=10680,
end_time=12540,
id=2
),
],
"source": "Source: LyricFind",
"hasTimestamps": True
}
"""
"""overload for mypy only"""

@overload
def get_lyrics(self, browseId: str, timestamps: Literal[True] = True) -> Optional[Lyrics|TimedLyrics]:
"""
Returns lyrics of a song or video. When `timestamps` is set, lyrics are returned with
timestamps, if available.
:param browseId: Lyrics browse-id obtained from :py:func:`get_watch_playlist` (startswith `MPLYt`).
:param timestamps: Whether to return bare lyrics or lyrics with timestamps, if available.
:return: Dictionary with song lyrics or `None`, if no lyrics are found.
The `hasTimestamps`-key determines the format of the data.
Example when `timestamps` is set to `False`, or not timestamps are available::
{
"lyrics": "Today is gonna be the day\\nThat they're gonna throw it back to you\\n",
"source": "Source: LyricFind",
"hasTimestamps": False
}
Example when `timestamps` is set to `True` and timestamps are available::
"""overload for mypy only"""

{
"lyrics": [
LyricLine(
text="I was a liar",
start_time=9200,
end_time=10630,
id=1
),
LyricLine(
text="I gave in to the fire",
start_time=10680,
end_time=12540,
id=2
),
],
"source": "Source: LyricFind",
"hasTimestamps": True
}
"""

def get_lyrics(self, browseId: str, timestamps: bool = False) -> Optional[Lyrics|TimedLyrics]:
def get_lyrics(self, browseId: str, timestamps: Optional[bool] = False) -> Optional[Lyrics|TimedLyrics]:
"""
Returns lyrics of a song or video. When `timestamps` is set, lyrics are returned with
timestamps, if available.
:param browseId: Lyrics browse-id obtained from :py:func:`get_watch_playlist` (startswith `MPLYt`).
:param timestamps: Whether to return bare lyrics or lyrics with timestamps, if available.
:param browseId: Lyrics browse-id obtained from :py:func:`get_watch_playlist` (startswith `MPLYt...`).
:param timestamps: Optional. Whether to return bare lyrics or lyrics with timestamps, if available. (Default: `False`)
:return: Dictionary with song lyrics or `None`, if no lyrics are found.
The `hasTimestamps`-key determines the format of the data.
Example when `timestamps` is set to `False`, or not timestamps are available::
Example when `timestamps=False`, or no timestamps are available::
{
"lyrics": "Today is gonna be the day\\nThat they're gonna throw it back to you\\n",
Expand Down Expand Up @@ -1017,18 +896,11 @@ def get_lyrics(self, browseId: str, timestamps: bool = False) -> Optional[Lyrics
"Invalid browseId provided. This song might not have lyrics.")

if timestamps:
# change the client to get lyrics with timestamps (mobile only)
copied_context_client = self.context["context"]["client"].copy()
self.context["context"]["client"].update({
"clientName": "ANDROID_MUSIC",
"clientVersion": "7.21.50"
})

response = self._send_request("browse", {"browseId": browseId})

if timestamps:
# restore the old context
self.context["context"]["client"] = copied_context_client # type: ignore
# changes and restores the client to get lyrics with timestamps (mobile only)
with self.as_mobile():
response = self._send_request("browse", {"browseId": browseId})
else:
response = self._send_request("browse", {"browseId": browseId})

# unpack the response

Expand Down Expand Up @@ -1057,7 +929,7 @@ def get_lyrics(self, browseId: str, timestamps: bool = False) -> Optional[Lyrics

return cast(Lyrics | TimedLyrics, lyrics)

def get_basejs_url(self):
def get_basejs_url(self) -> str:
"""
Extract the URL for the `base.js` script from YouTube Music.
Expand All @@ -1068,7 +940,7 @@ def get_basejs_url(self):
if match is None:
raise YTMusicError("Could not identify the URL for base.js player.")

return cast(str, YTM_DOMAIN + match.group(1))
return YTM_DOMAIN + match.group(1)

def get_signatureTimestamp(self, url: Optional[str] = None) -> int:
"""
Expand Down
12 changes: 6 additions & 6 deletions ytmusicapi/mixins/library.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def get_library_playlists(self, limit: Optional[int] = 25) -> list[dict]:
return playlists

def get_library_songs(
self, limit: int = 25, validate_responses: bool = False, order: Optional[OrderType] = None
self, limit: int = 25, validate_responses: bool = False, order: Optional[LibraryOrderType] = None
) -> list[dict]:
"""
Gets the songs in the user's library (liked videos are not included).
Expand Down Expand Up @@ -116,7 +116,7 @@ def get_library_songs(

return songs

def get_library_albums(self, limit: int = 25, order: Optional[OrderType] = None) -> list[dict]:
def get_library_albums(self, limit: int = 25, order: Optional[LibraryOrderType] = None) -> list[dict]:
"""
Gets the albums in the user's library.
Expand Down Expand Up @@ -151,7 +151,7 @@ def get_library_albums(self, limit: int = 25, order: Optional[OrderType] = None)
response, lambda additionalParams: self._send_request(endpoint, body, additionalParams), limit
)

def get_library_artists(self, limit: int = 25, order: Optional[OrderType] = None) -> list[dict]:
def get_library_artists(self, limit: int = 25, order: Optional[LibraryOrderType] = None) -> list[dict]:
"""
Gets the artists of the songs in the user's library.
Expand Down Expand Up @@ -179,7 +179,7 @@ def get_library_artists(self, limit: int = 25, order: Optional[OrderType] = None
response, lambda additionalParams: self._send_request(endpoint, body, additionalParams), limit
)

def get_library_subscriptions(self, limit: int = 25, order: Optional[OrderType] = None) -> list[dict]:
def get_library_subscriptions(self, limit: int = 25, order: Optional[LibraryOrderType] = None) -> list[dict]:
"""
Gets the artists the user has subscribed to.
Expand All @@ -198,7 +198,7 @@ def get_library_subscriptions(self, limit: int = 25, order: Optional[OrderType]
response, lambda additionalParams: self._send_request(endpoint, body, additionalParams), limit
)

def get_library_podcasts(self, limit: int = 25, order: Optional[OrderType] = None) -> list[dict]:
def get_library_podcasts(self, limit: int = 25, order: Optional[LibraryOrderType] = None) -> list[dict]:
"""
Get podcasts the user has added to the library
Expand Down Expand Up @@ -244,7 +244,7 @@ def get_library_podcasts(self, limit: int = 25, order: Optional[OrderType] = Non
response, lambda additionalParams: self._send_request(endpoint, body, additionalParams), limit
)

def get_library_channels(self, limit: int = 25, order: Optional[OrderType] = None) -> list[dict]:
def get_library_channels(self, limit: int = 25, order: Optional[LibraryOrderType] = None) -> list[dict]:
"""
Get channels the user has added to the library
Expand Down
8 changes: 3 additions & 5 deletions ytmusicapi/mixins/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@
from ytmusicapi.parsers.search import *


FilterType = Literal['songs', 'videos', 'albums', 'artists', 'playlists', 'community_playlists', 'featured_playlists', 'uploads']

class SearchMixin(MixinProtocol):
def search(
self,
query: str,
filter: Optional[FilterType] = None,
scope: Optional[Literal["library", "uploads"]] = None,
filter: Optional[str] = None,
scope: Optional[str] = None,
limit: int = 20,
ignore_spelling: bool = False,
) -> list[dict]:
Expand Down Expand Up @@ -206,7 +204,7 @@ def search(
if filter and "playlists" in filter:
filter = "playlists"
elif scope == scopes[1]:
filter = scopes[1] # type:ignore
filter = scopes[1]

for res in section_list:
result_type = category = None
Expand Down
Loading

0 comments on commit 3198b1f

Please sign in to comment.