Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add YoutubeMusic MusicProvider #397

Merged
merged 46 commits into from
Jul 7, 2022
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
c5879a9
Update settings
Jun 30, 2022
2b1ff23
Can query albums and tracks
MarvinSchenkel Jul 1, 2022
9956711
Add url parsing for playback
MarvinSchenkel Jul 1, 2022
5da0716
Generate valid stream urls
MarvinSchenkel Jul 2, 2022
0924725
Parse artist, album and track
MarvinSchenkel Jul 2, 2022
d90a093
Add serach for artist and album
MarvinSchenkel Jul 2, 2022
5efd176
Add get_album_tracks and get_playlist_tracks
MarvinSchenkel Jul 3, 2022
81c4a32
precommit fixes
MarvinSchenkel Jul 5, 2022
61bafe9
Add library_artist and library_album
MarvinSchenkel Jul 5, 2022
2c0ccc1
Add authentication from config
MarvinSchenkel Jul 6, 2022
c2cd62f
Add library playlist, library songs and artist albums
MarvinSchenkel Jul 6, 2022
c82e1b4
Update settings
Jun 30, 2022
4c0eb68
Can query albums and tracks
MarvinSchenkel Jul 1, 2022
67d1b2c
Add url parsing for playback
MarvinSchenkel Jul 1, 2022
e0712ce
Generate valid stream urls
MarvinSchenkel Jul 2, 2022
297811d
Parse artist, album and track
MarvinSchenkel Jul 2, 2022
8e4eeaf
Add serach for artist and album
MarvinSchenkel Jul 2, 2022
b6a3448
Add get_album_tracks and get_playlist_tracks
MarvinSchenkel Jul 3, 2022
7b2b8c2
precommit fixes
MarvinSchenkel Jul 5, 2022
99573b7
Add library_artist and library_album
MarvinSchenkel Jul 5, 2022
e9e279d
Add authentication from config
MarvinSchenkel Jul 6, 2022
84fba16
Add library playlist, library songs and artist albums
MarvinSchenkel Jul 6, 2022
31f0a95
Add library playlist, library songs and artist albums
MarvinSchenkel Jul 6, 2022
ac4814a
Add library playlist, library songs and artist albums
MarvinSchenkel Jul 6, 2022
8152e95
Merge branch 'music-assistant:master' into master
MarvinSchenkel Jul 6, 2022
dc98eeb
update example
marcelveldt Jul 6, 2022
cab585d
Update artists.py
marcelveldt Jul 6, 2022
09d0cf9
Update artists.py
marcelveldt Jul 6, 2022
ec0ad07
adjust example
marcelveldt Jul 6, 2022
01c1210
Update full.py
marcelveldt Jul 6, 2022
5d01114
Add artist to track when possible
MarvinSchenkel Jul 6, 2022
9a04b98
Merge branch 'master' of https://github.com/MarvinSchenkel/music-assi…
MarvinSchenkel Jul 6, 2022
c4a732e
fix streaming
marcelveldt Jul 6, 2022
1dda6a5
Merge branch 'master' of https://github.com/MarvinSchenkel/music-assi…
marcelveldt Jul 6, 2022
9731666
parse contenttype
marcelveldt Jul 6, 2022
43ab57c
Fix parse tracks
MarvinSchenkel Jul 6, 2022
cd3b14d
Merge branch 'master' of https://github.com/MarvinSchenkel/music-assi…
MarvinSchenkel Jul 6, 2022
aafe581
guard missing album
marcelveldt Jul 6, 2022
5b5cf9c
lint
marcelveldt Jul 6, 2022
ade2488
Merge branch 'master' into pr/397
marcelveldt Jul 6, 2022
fe72c05
Various prasing fixes
MarvinSchenkel Jul 6, 2022
d3c41ec
run blocking call in executor
marcelveldt Jul 6, 2022
735938e
Fix track images
MarvinSchenkel Jul 6, 2022
b77af98
Merge branch 'master' of https://github.com/MarvinSchenkel/music-assi…
MarvinSchenkel Jul 6, 2022
6a9e782
Add artist top tracks
MarvinSchenkel Jul 7, 2022
a6e6cb7
Remove ytmusic example
MarvinSchenkel Jul 7, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@
"python.linting.flake8Enabled": true,
"python.linting.flake8Args": ["--config=${workspaceFolder}/setup.cfg"],
"python.linting.mypyEnabled": false,
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
}
159 changes: 159 additions & 0 deletions examples/ytmusic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
"""Example file to play music from YTM. Might omit later"""
import argparse
import asyncio
import logging
import os
from os.path import abspath, dirname
from sys import path

path.insert(1, dirname(dirname(abspath(__file__))))

# pylint: disable=wrong-import-position
from music_assistant.mass import MusicAssistant
from music_assistant.models.config import MassConfig, MusicProviderConfig
from music_assistant.models.enums import ProviderType
from music_assistant.models.player import Player, PlayerState

parser = argparse.ArgumentParser(description="MusicAssistant")
parser.add_argument(
"--youtubemusic_username",
required=False,
help="YoutubeMusic username",
)
parser.add_argument(
"--youtubemusic_cookie",
required=False,
help="YoutubeMusic cookie",
)
args = parser.parse_args()

# setup logger
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)-15s %(levelname)-5s %(name)s -- %(message)s",
)
# silence some loggers
logging.getLogger("aiorun").setLevel(logging.WARNING)
logging.getLogger("asyncio").setLevel(logging.INFO)
logging.getLogger("aiosqlite").setLevel(logging.WARNING)
logging.getLogger("databases").setLevel(logging.WARNING)


# default database based on sqlite
data_dir = os.getenv("APPDATA") if os.name == "nt" else os.path.expanduser("~")
data_dir = os.path.join(data_dir, ".musicassistant")
if not os.path.isdir(data_dir):
os.makedirs(data_dir)
db_file = os.path.join(data_dir, "music_assistant.db")


mass_conf = MassConfig(
database_url=f"sqlite:///{db_file}",
)

if args.youtubemusic_username and args.youtubemusic_cookie:
mass_conf.providers.append(
MusicProviderConfig(
ProviderType.YTMUSIC,
username=args.youtubemusic_username,
password=args.youtubemusic_cookie,
)
)


class TestPlayer(Player):
"""Demonstatration player implementation."""

def __init__(self, player_id: str):
"""Init."""
self.player_id = player_id
self._attr_name = player_id
self._attr_powered = True
self._attr_elapsed_time = 0
self._attr_current_url = ""
self._attr_state = PlayerState.IDLE
self._attr_available = True
self._attr_volume_level = 100

async def play_url(self, url: str) -> None:
"""Play the specified url on the player."""
print(f"stream url: {url}")
self._attr_current_url = url
self.update_state()

async def stop(self) -> None:
"""Send STOP command to player."""
print("stop called")
self._attr_state = PlayerState.IDLE
self._attr_current_url = None
self._attr_elapsed_time = 0
self.update_state()

async def play(self) -> None:
"""Send PLAY/UNPAUSE command to player."""
print("play called")
self._attr_state = PlayerState.PLAYING
self._attr_elapsed_time = 1
self.update_state()

async def pause(self) -> None:
"""Send PAUSE command to player."""
print("pause called")
self._attr_state = PlayerState.PAUSED
self.update_state()

async def power(self, powered: bool) -> None:
"""Send POWER command to player."""
print(f"POWER CALLED - new power: {powered}")
self._attr_powered = powered
self._attr_current_url = None
self.update_state()

async def volume_set(self, volume_level: int) -> None:
"""Send volume level (0..100) command to player."""
print(f"volume_set called - {volume_level}")
self._attr_volume_level = volume_level
self.update_state()


async def main():
"""Handle main execution."""

asyncio.get_event_loop().set_debug(True)

async with MusicAssistant(mass_conf) as mass:
# get some data
# ytm = mass.music.get_provider(ProviderType.YTMUSIC)
# track = await ytm.get_track("pE3ju1qS848")
# album = await ytm.get_album("MPREb_AYetWMZunqA")
# await ytm.get_artist_albums("UCFpQsnvFBNlXi7rwQo6IW7g")
# albums = await ytm.get_artist_albums("UCFpQsnvFBNlXi7rwQo6IW7g")*")
# start sync
await mass.music.start_sync(schedule=3)
artists = await mass.music.artists.count()
print(f"Got {artists} artists in library")
albums = await mass.music.albums.count()
print(f"Got {albums} albums in library")
tracks = await mass.music.tracks.count()
print(f"Got {tracks} tracks in library")
playlists = await mass.music.playlists.library()
print(f"Got {len(playlists)} playlists in library")
# artists = await mass.music.artists.count()
# print(f"Got {artists} artists in library")
# albums = await mass.music.albums.count()
# print(f"Got {albums} albums in library")
# sd = await yt.get_stream_details(track.item_id)
# print(sd.data)

# test_player1 = TestPlayer("test1")
# await mass.players.register_player(test_player1)
# await test_player1.active_queue.play_media(track.uri)

await asyncio.sleep(3600)


if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
pass
2 changes: 2 additions & 0 deletions music_assistant/controllers/music/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from music_assistant.music_providers.qobuz import QobuzProvider
from music_assistant.music_providers.spotify import SpotifyProvider
from music_assistant.music_providers.tunein import TuneInProvider
from music_assistant.music_providers.ytmusic import YTMusic
from music_assistant.music_providers.url import PROVIDER_CONFIG as URL_CONFIG
from music_assistant.music_providers.url import URLProvider

Expand All @@ -46,6 +47,7 @@
ProviderType.SPOTIFY: SpotifyProvider,
ProviderType.QOBUZ: QobuzProvider,
ProviderType.TUNEIN: TuneInProvider,
ProviderType.YTMUSIC: YTMusic,
}


Expand Down
49 changes: 26 additions & 23 deletions music_assistant/controllers/music/artists.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,29 +176,32 @@ async def add_db_item(
self, item: Artist, overwrite_existing: bool = False
) -> Artist:
"""Add a new item record to the database."""
assert item.provider_ids, "Album is missing provider id(s)"
# always try to grab existing item by musicbrainz_id
cur_item = None
if item.musicbrainz_id:
match = {"musicbrainz_id": item.musicbrainz_id}
cur_item = await self.mass.database.get_row(self.db_table, match)
if not cur_item:
# fallback to exact name match
# NOTE: we match an artist by name which could theoretically lead to collisions
# but the chance is so small it is not worth the additional overhead of grabbing
# the musicbrainz id upfront
match = {"sort_name": item.sort_name}
for row in await self.mass.database.get_rows(self.db_table, match):
row_artist = Artist.from_db_row(row)
if row_artist.sort_name == item.sort_name:
# just to be sure ?!
cur_item = row_artist
break
if cur_item:
# update existing
return await self.update_db_item(
cur_item.item_id, item, overwrite=overwrite_existing
)
assert item.provider_ids, "Artist is missing provider id(s)"
async with self.mass.database.get_db(db) as db:
# always try to grab existing item by musicbrainz_id
cur_item = None
if item.musicbrainz_id:
match = {"musicbrainz_id": item.musicbrainz_id}
cur_item = await self.mass.database.get_row(self.db_table, match, db=db)
if not cur_item:
# fallback to matching
# NOTE: we match an artist by name which could theoretically lead to collisions
# but the chance is so small it is not worth the additional overhead of grabbing
# the musicbrainz id upfront
match = {"sort_name": item.sort_name}
for row in await self.mass.database.get_rows(
self.db_table, match, db=db
):
row_artist = Artist.from_db_row(row)
if row_artist.sort_name == item.sort_name:
# just to be sure ?!
cur_item = row_artist
break
if cur_item:
# update existing
return await self.update_db_item(
cur_item.item_id, item, overwrite=overwrite_existing, db=db
)

# insert item
if item.in_library and not item.timestamp:
Expand Down
2 changes: 2 additions & 0 deletions music_assistant/models/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class AlbumType(Enum):
ALBUM = "album"
SINGLE = "single"
COMPILATION = "compilation"
EP = "ep"
UNKNOWN = "unknown"


Expand Down Expand Up @@ -209,6 +210,7 @@ class ProviderType(Enum):
SPOTIFY = "spotify"
QOBUZ = "qobuz"
TUNEIN = "tunein"
YTMUSIC = "ytmusic"
DATABASE = "database" # internal only
URL = "url" # internal only

Expand Down