Skip to content

Commit

Permalink
add brand account setup script (#520)
Browse files Browse the repository at this point in the history
  • Loading branch information
sigma67 committed Feb 4, 2024
1 parent eef807f commit 04abd8e
Show file tree
Hide file tree
Showing 15 changed files with 309 additions and 262 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/docsbuild.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ name: Build Documentation

on:
push:
branches:
- main
paths:
- ytmusicapi/**
- docs/**
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.9
rev: v0.2.0
hooks:
# Run the linter.
- id: ruff
Expand Down
4 changes: 2 additions & 2 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

sys.path.insert(0, os.path.abspath("."))
sys.path.insert(0, "../..")
from ytmusicapi import __version__ # noqa: E402
from ytmusicapi import __version__

on_rtd = os.environ.get("READTHEDOCS", None) == "True"

Expand Down Expand Up @@ -49,6 +49,6 @@
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# exclude_patterns = []

html_theme = "sphinx_rtd_theme"
18 changes: 11 additions & 7 deletions docs/source/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,17 @@ For more details see the :doc:`reference`.
Usage
-----------------------

How do I add a song, album, artist or playlist to my library?
How do I add content to my library?
***********************************************************************
- **songs**: `edit_song_library_status <Reference.html#ytmusicapi.YTMusic.edit_song_library_status>`__ .
Liking a song using `rate_song <Reference.html#ytmusicapi.YTMusic.rate_song>`__
- **songs**: `edit_song_library_status <reference.html#ytmusicapi.YTMusic.edit_song_library_status>`__ .
Liking a song using `rate_song <reference.html#ytmusicapi.YTMusic.rate_song>`__
does *not* add it to your library, only to your liked songs playlist.
- **albums, playlists**: `rate_playlist <Reference.html#ytmusicapi.YTMusic.rate_playlist>`__
- **artists**: `subscribe_artists <Reference.html#ytmusicapi.YTMusic.subscribe_artists>`__ .
- **albums, playlists**: `rate_playlist <reference.html#ytmusicapi.YTMusic.rate_playlist>`__
- **artists**: `subscribe_artists <reference.html#ytmusicapi.YTMusic.subscribe_artists>`__ .
This will add the artist to your Subscriptions tab. The Artists tab is determined by the songs/albums you have
added to your library.
- **podcasts**: `rate_playlist <reference.html#ytmusicapi.YTMusic.rate_playlist>`__
- **episodes**: `add_playlist_items("SE", episode_id) <reference.html#ytmusicapi.YTMusic.add_playlist_items>`__



Expand All @@ -34,15 +36,17 @@ How can I get the radio playlist for a song, video, playlist or album?
- **songs, videos**: ``RDAMVM`` + ``videoId``
- **playlists, albums**: ``RDAMPL`` + ``playlistId``

See also `What is a browseId <faq.html#what-is-a-browseid>`__ below.


How can I get the shuffle playlist for a playlist or album?
***********************************************************************
Use `get_watch_playlist_shuffle <Reference.html#ytmusicapi.YTMusic.get_watch_playlist_shuffle>`__
Use `get_watch_playlist_shuffle <reference.html#ytmusicapi.YTMusic.get_watch_playlist_shuffle>`__
with the ``playlistId`` or ``audioPlaylistId`` (albums).

How can I get all my public playlists in a single request?
***********************************************************************
Call `get_user_playlists <Reference.html#ytmusicapi.YTMusic.get_user_playlists>`__
Call `get_user_playlists <reference.html#ytmusicapi.YTMusic.get_user_playlists>`__
with your own ``channelId``.

Can I download songs?
Expand Down
394 changes: 197 additions & 197 deletions pdm.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ precision = 2

[tool.ruff]
line-length = 110

[tool.ruff.lint]
ignore = [ "F403", "F405", "F821", "E731" ]
extend-select = [
"I", # isort
Expand Down
11 changes: 10 additions & 1 deletion tests/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,25 @@ These two files can be easily obtained as the default outputs of running the fol
Copy ``tests/test.cfg.example`` to ``tests/test.cfg`` to run the tests. The entry descriptions should be self-explanatory.
First, run

.. code-block:: bash
cp tests/test.example.cfg tests/test.cfg
The entry descriptions should be self-explanatory.
For the headers_raw, you need to indent the overflowing lines with a tab character. For the upload test you need a suitable music file in the test directory.
Adjust the file to contain appropriate information for your YouTube account and local setup.

Brand accounts can be created by first signing into the google account you wish to have as the parent/controlling
account then navigating `here. <https://www.youtube.com/create_channel?action_create_new_channel_redirect=true>`_

Once the brand account/channel has been created, you can obtain the account ID needed for your test.cfg by
navigating to your `google account page <https://myaccount.google.com>`_ and selecting the brand account via the
profile drop down in the top right, the brand ID should then be present in the URL.

You can populate the brand account with content using the script provided in ``tests/setup/setup_account.py``.

Coverage badge
--------------
Make sure you installed the dev requirements as explained in `CONTRIBUTING.rst <https://github.com/sigma67/ytmusicapi/blob/master/CONTRIBUTING.rst>`_. Run
Expand Down
8 changes: 6 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ def get_resource(file: str) -> str:
return data_dir.joinpath(file).as_posix()


@pytest.fixture(name="config")
def fixture_config() -> configparser.RawConfigParser:
def get_config() -> configparser.RawConfigParser:
config = configparser.RawConfigParser()
config.read(get_resource("test.cfg"), "utf-8")
return config


@pytest.fixture(name="config")
def fixture_config() -> configparser.RawConfigParser:
return get_config()


@pytest.fixture(name="sample_album")
def fixture_sample_album() -> str:
"""Eminem - Revival"""
Expand Down
9 changes: 6 additions & 3 deletions tests/mixins/test_browsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,10 @@ def test_get_album(self, yt, yt_auth, sample_album):
def test_get_album_other_versions(self, yt):
# Eminem - Curtain Call: The Hits (Explicit Variant)
album = yt.get_album("MPREb_LQCAymzbaKJ")
assert len(variants := album["other_versions"]) >= 1 # appears to be regional
assert (variant := variants[0])["type"] == "Album"
variants = album["other_versions"]
assert len(variants) >= 1 # appears to be regional
variant = variants[0]
assert variant["type"] == "Album"
assert len(variant["artists"]) == 1
assert variant["artists"][0] == {"name": "Eminem", "id": "UCedvOgsKFzcK3hA5taf3KoQ"}
assert variant["audioPlaylistId"] is not None
Expand All @@ -99,7 +101,8 @@ def test_get_album_other_versions(self, yt):
# Cassö & RAYE - Prada
album = yt.get_album("MPREb_of3qfisa0yU")
assert not album["isExplicit"]
assert (variant := album["other_versions"][0])["type"] == "Single"
variant = album["other_versions"][0]
assert variant["type"] == "Single"
assert variant["isExplicit"]
assert len(variant["artists"]) == 3
assert variant["artists"][0]["id"] == "UCGWMNnI1Ky5bMcRlr73Cj2Q"
Expand Down
13 changes: 6 additions & 7 deletions tests/mixins/test_podcasts.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
class TestPodcasts:
def test_get_podcast(self, yt, yt_brand):
podcast_id = "PLxq_lXOUlvQDgCVFj9L79kqJybW0k6OaB" # Think Fast, Talk Smart: The Podcast

def test_get_podcast(self, config, yt, yt_brand):
podcast_id = config["podcasts"]["podcast_id"]
results = yt.get_podcast(podcast_id)
assert len(results["episodes"]) == 100
assert not results["saved"]
Expand All @@ -16,14 +15,14 @@ def test_many_podcasts(self, yt):
results = yt.get_podcast(result["browseId"])
assert len(results) > 0

def test_get_episode(self, yt, yt_brand):
podcast_id = "KNkyHCLOr1o" # 124. Making Meetings Meaningful, Pt. 1: How to Structure
result = yt.get_episode(podcast_id)
def test_get_episode(self, config, yt, yt_brand):
episode_id = config["podcasts"]["episode_id"]
result = yt.get_episode(episode_id)
assert len(result["description"]) == 50
assert not result["saved"]
assert result["playlistId"] is not None

result = yt_brand.get_episode(podcast_id)
result = yt_brand.get_episode(episode_id)
assert result["saved"]

def test_many_episodes(self, yt):
Expand Down
56 changes: 56 additions & 0 deletions tests/setup/setup_account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import itertools
from contextlib import suppress
from pathlib import Path

from conftest import get_config

from ytmusicapi import YTMusic

config = get_config()

# To get started, go to https://www.youtube.com/account and create a new channel (=brand account)
brand_account = "114511851949139689139"

# run ytmusicapi browser in tests/setup, paste headers from your test account
yt_brand = YTMusic(Path(__file__).parent.joinpath("oauth.json").as_posix(), brand_account)


def populate_account():
"""idempotent requests to populate an account"""
# library
playlist_id = "RDCLAK5uy_l9ex2d91-Qb1i-W7d0MLCEl_ZjRXss0Dk" # fixed playlist with many artists
yt_playlist = yt_brand.get_playlist(playlist_id)
artists = [track["artists"] for track in yt_playlist["tracks"]]
unique_artists = list(set(artist["id"] for artist in itertools.chain.from_iterable(artists)))
with suppress(Exception):
for artist in unique_artists:
print(f"Adding artist {artist}")
yt_brand.subscribe_artists([artist]) # add one by one to avoid "requested entity not found"

# add some albums, which also populates songs and artists as a side effect
unique_albums = set(track["album"]["id"] for track in yt_playlist["tracks"])
playlist_ids = [yt_brand.get_album(album)["audioPlaylistId"] for album in unique_albums if album]
for playlist_id in playlist_ids:
print(f"Adding album {playlist_id}")
yt_brand.rate_playlist(playlist_id, "LIKE")

# like some songs
for track in yt_playlist["tracks"]:
print(f"liking track {track['videoId']}")
yt_brand.rate_song(track["videoId"], "LIKE")

# create own playlist
playlistId = yt_brand.create_playlist(
title="ytmusicapi test playlist don't delete",
description="description",
source_playlist="PLJZsotfVeN2D3pHlgWT_FFSYsJe_thVmh",
)
print(f"Created playlist {playlistId}, don't forget to set this in test.cfg playlists/own")

# podcasts
yt_brand.rate_playlist(config["podcasts"]["podcast_id"], rating="LIKE")
yt_brand.add_playlist_items("SE", [config["podcasts"]["episode_id"]])


if __name__ == "__main__":
populate_account()
3 changes: 0 additions & 3 deletions tests/setup_account.py

This file was deleted.

38 changes: 0 additions & 38 deletions tests/test.cfg.example

This file was deleted.

3 changes: 3 additions & 0 deletions ytmusicapi/mixins/browsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,9 @@ def get_album(self, browseId: str) -> Dict:
"duration_seconds": 4657
}
"""
if not browseId or not browseId.startswith("MPRE"):
raise Exception("Invalid album browseId provided, must start with MPRE.")

body = {"browseId": browseId}
endpoint = "browse"
response = self._send_request(endpoint, body)
Expand Down
8 changes: 7 additions & 1 deletion ytmusicapi/mixins/watch.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,14 @@ def get_watch_playlist(
related_browse_id = get_tab_browse_id(watchNextRenderer, 2)

results = nav(
watchNextRenderer, [*TAB_CONTENT, "musicQueueRenderer", "content", "playlistPanelRenderer"]
watchNextRenderer, [*TAB_CONTENT, "musicQueueRenderer", "content", "playlistPanelRenderer"], True
)
if not results:
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)

playlist = next(
filter(
bool,
Expand Down

0 comments on commit 04abd8e

Please sign in to comment.