import sys, json
try:
from ytmusicapi import YTMusic
except ImportError:
print(json.dumps({"error": "ytmusicapi not installed"}))
sys.exit(1)
SCOPE_FILTER = {
"all": None,
"songs": "songs",
"videos": "videos",
"artists": "artists",
"albums": "albums",
"playlists": "playlists",
}
EXPECTED_TYPE = {
"songs": "song",
"videos": "video",
"artists": "artist",
"albums": "album",
"playlists": "playlist",
}
def _make_track(vid, title, artist, *, duration=0, thumbnail="",
album=None, artist_id=None, views=""):
return {
"kind": "track",
"resultType": "song",
"id": vid,
"title": title,
"subtitle": artist,
"url": f"https://youtube.com/watch?v={vid}",
"duration": duration,
"thumbnail": thumbnail,
"channel": artist,
"artist_id": artist_id,
"album": album,
"views": views,
}
def _yt():
return YTMusic()
def _charts_items(scope, limit=20):
yt = _yt()
try:
charts = yt.get_charts("ZZ")
except Exception:
return []
if scope in ("songs", "videos"):
vids = charts.get("videos") or []
if not vids:
return []
pid = vids[0].get("playlistId") or ""
if not pid:
return []
want = max(limit, 60)
try:
pl = yt.get_playlist(pid, limit=want)
except Exception:
return []
out = []
for e in pl.get("tracks", []):
t = _track_from_album_entry(e)
if t:
out.append(t)
if len(out) >= want:
break
return out
if scope == "artists":
out = []
for a in charts.get("artists") or []:
bid = a.get("browseId") or ""
if not bid:
continue
thumbs = a.get("thumbnails") or []
rank = a.get("rank") or ""
out.append({
"kind": "artist",
"resultType": "artist",
"id": bid,
"title": a.get("title") or "",
"subtitle": f"#{rank}" if rank else "",
"url": f"https://music.youtube.com/channel/{bid}",
"duration": 0,
"thumbnail": thumbs[-1].get("url", "") if thumbs else "",
"channel": a.get("title") or "",
})
if len(out) >= limit:
break
return out
return []
def search(query, scope="all", limit=20):
if not query.strip():
return _charts_items(scope, limit)
filt = SCOPE_FILTER.get(scope, None)
if filt is None:
results = _yt().search(query, limit=limit * 3)
else:
results = _yt().search(query, filter=filt, limit=limit)
out = []
for r in results:
rt = r.get("resultType")
if scope != "all" and rt != EXPECTED_TYPE.get(scope):
continue
item = result_to_item(r, rt)
if item is not None:
out.append(item)
if len(out) >= limit:
break
return out
def result_to_item(r, rt):
thumbs = r.get("thumbnails") or []
thumb = thumbs[-1].get("url", "") if thumbs else ""
if rt in ("song", "video"):
vid = r.get("videoId", "")
if not vid:
return None
artists = r.get("artists", [])
artist = artists[0].get("name", "") if artists else ""
artist_id = artists[0].get("id") if artists else None
duration = r.get("duration_seconds", 0) or 0
album = r.get("album") or {}
album_name = album.get("name", "") or ""
album_id = album.get("id", "") or ""
album_obj = (
{"name": album_name, "id": album_id}
if album_name and album_id
else None
)
return _make_track(
vid, r.get("title", ""), artist,
duration=duration, thumbnail=thumb, album=album_obj,
artist_id=artist_id, views=r.get("views") or "",
)
if rt == "artist":
bid = r.get("browseId", "")
if not bid:
return None
name = r.get("artist") or r.get("name") or r.get("title") or ""
return {
"kind": "artist",
"resultType": rt,
"id": bid,
"title": name,
"subtitle": "",
"url": f"https://music.youtube.com/channel/{bid}",
"duration": 0,
"thumbnail": thumb,
"channel": name,
}
if rt == "album":
bid = r.get("browseId", "")
if not bid:
return None
artists = r.get("artists", [])
artist = artists[0].get("name", "") if artists else ""
return {
"kind": "album",
"resultType": rt,
"id": bid,
"title": r.get("title", ""),
"subtitle": artist,
"url": f"https://music.youtube.com/browse/{bid}",
"duration": 0,
"thumbnail": thumb,
"channel": artist,
}
if rt == "playlist":
pid = r.get("browseId", "")
if not pid:
return None
author = r.get("author", "")
if isinstance(author, list):
author = ", ".join(a.get("name", "") for a in author if isinstance(a, dict))
return {
"kind": "playlist",
"resultType": rt,
"id": pid,
"title": r.get("title", ""),
"subtitle": author or "",
"url": f"https://music.youtube.com/playlist?list={pid}",
"duration": 0,
"thumbnail": thumb,
"channel": author or "",
}
return None
def _track_from_album_entry(e, album=None):
vid = e.get("videoId") or ""
if not vid:
return None
artist = e.get("artists", [{}])[0].get("name", "") if e.get("artists") \
else (e.get("artist", "") or "")
thumb = (e.get("thumbnails") or [{}])[-1].get("url", "")
views = e.get("views")
return _make_track(
vid, e.get("title", ""), artist,
duration=e.get("duration") or 0, thumbnail=thumb, album=album,
views=str(views) if views else "",
)
def browse(browse_id, limit=50, kind=None):
yt = _yt()
out = []
if kind == "album" or (kind is None and browse_id.startswith("MPRE")):
album = yt.get_album(browse_id)
album_name = album.get("title", "") or ""
album_id = album.get("albumId") or album.get("browseId", "") or browse_id
album_obj = {"name": album_name, "id": album_id}
for e in album.get("tracks", []):
t = _track_from_album_entry(e, album_obj)
if t:
out.append(t)
if len(out) >= limit:
break
meta = {
"badge": album.get("type", "") or "",
"date": str(album.get("year", "") or ""),
"thumbnail": (album.get("thumbnails") or [{}])[-1].get("url", ""),
}
return {"meta": meta, "tracks": out}
if kind == "playlist" or (kind is None and (browse_id.startswith("PL") or browse_id.startswith("OL"))):
pl = yt.get_playlist(browse_id, limit=limit)
for e in pl.get("tracks", []):
vid = e.get("videoId") or e.get("browseId") or ""
if not vid.startswith("PL") and not vid.startswith("OL"):
t = _track_from_album_entry(e)
if t:
out.append(t)
return out
try:
artist = yt.get_artist(browse_id)
for e in artist.get("songs", {}).get("results", []):
vid = e.get("videoId") or ""
if not vid:
continue
thumb = (e.get("thumbnails") or [{}])[-1].get("url", "")
out.append(
_make_track(
vid, e.get("title", ""), e.get("artist", ""),
duration=e.get("duration") or 0, thumbnail=thumb,
artist_id=browse_id,
)
)
if len(out) >= limit:
break
except Exception:
pass
return out
def _playlists_shelf_fallback(browse_id):
stack = [_yt()._send_request("browse", {"browseId": browse_id})]
out = []
while stack:
cur = stack.pop()
if isinstance(cur, dict):
if "musicCarouselShelfRenderer" in cur:
m = cur["musicCarouselShelfRenderer"]
header = m.get("header", {}).get("musicCarouselShelfBasicHeaderRenderer", {})
title = "".join(r.get("text", "") for r in header.get("title", {}).get("runs", []))
lowered = title.lower()
if "playlist" not in lowered and "featured on" not in lowered:
continue
for c in m.get("contents", []):
item = c.get("musicTwoRowItemRenderer")
if not item:
continue
pid = item.get("navigationEndpoint", {}).get("browseEndpoint", {}).get("browseId", "") or ""
if not pid.startswith(("VL", "PL", "OL")):
continue
if pid.startswith("VL"):
pid = pid[2:]
name = "".join(r.get("text", "") for r in item.get("title", {}).get("runs", []))
thumbs = (
(item.get("thumbnailRenderer") or {})
.get("musicThumbnailRenderer", {})
.get("thumbnail", {})
.get("thumbnails", [])
)
out.append({
"id": pid,
"title": name,
"subtitle": "",
"thumbnail": thumbs[-1].get("url", "") if thumbs else "",
})
else:
stack.extend(cur.values())
elif isinstance(cur, list):
stack.extend(cur)
return out
def artist_page(browse_id):
yt = _yt()
a = yt.get_artist(browse_id)
def thumb(e):
return (e.get("thumbnails") or [{}])[-1].get("url", "")
header = {
"image": thumb(a),
"stats": [],
"description": a.get("description", "") or "",
}
if a.get("monthlyListeners"):
header["stats"].append(["Monthly listeners", a["monthlyListeners"]])
if a.get("subscribers"):
header["stats"].append(["YouTube Subscribers", a["subscribers"]])
popular = []
for e in a.get("songs", {}).get("results", []):
vid = e.get("videoId") or ""
if not vid:
continue
artists = e.get("artists") or []
album = e.get("album") or {}
album_obj = (
{"name": album.get("name", ""), "id": album.get("id", "")}
if album.get("name") and album.get("id")
else None
)
t = _make_track(
vid, e.get("title", ""),
artists[0].get("name", "") if artists else "",
duration=e.get("duration") or 0, thumbnail=thumb(e),
album=album_obj,
artist_id=browse_id,
views=e.get("views") or "",
)
popular.append(t)
albums = []
for badge in (None, "Single"):
shelf = a.get("albums" if badge is None else "singles", {})
for e in shelf.get("results", []):
bid = e.get("browseId") or ""
if not bid:
continue
label = e.get("type", "") or badge or ("Single" if badge else "Album")
albums.append({
"id": bid,
"title": e.get("title", ""),
"date": e.get("year", "") or "",
"badge": label,
"thumbnail": thumb(e),
})
playlists = []
for e in a.get("playlists", {}).get("results", []):
pid = e.get("playlistId") or e.get("browseId") or ""
if not pid:
continue
playlists.append({
"id": pid,
"title": e.get("title", ""),
"subtitle": "",
"thumbnail": thumb(e),
})
if not playlists:
playlists = _playlists_shelf_fallback(browse_id)
related = []
for e in a.get("related", {}).get("results", []):
rid = e.get("browseId") or ""
if not rid:
continue
related.append({
"id": rid,
"name": e.get("title", ""),
"stat": e.get("subscribers", "") or "",
"thumbnail": thumb(e),
})
return {
"header": header,
"popular": popular,
"albums": albums,
"playlists": playlists,
"related": related,
}
def watch_playlist(video_id=None, playlist_id=None, radio=True, limit=50):
yt = _yt()
if playlist_id and playlist_id.startswith("UC"):
try:
artist = yt.get_artist(playlist_id)
radio_id = artist.get("radioId")
if radio_id:
playlist_id = radio_id
except Exception:
pass
try:
wp = yt.get_watch_playlist(
videoId=video_id, playlistId=playlist_id, radio=radio
)
except Exception as e:
print(json.dumps({"error": str(e)}))
sys.exit(1)
tracks = wp.get("tracks", [])
out = []
for t in tracks:
vid = t.get("videoId") or ""
if not vid:
continue
artists = t.get("artists", [])
artist = artists[0].get("name", "") if artists else ""
artist_id = artists[0].get("id") if artists else None
thumbs = t.get("thumbnails") or []
thumbnail = thumbs[-1].get("url", "") if thumbs else f"https://i.ytimg.com/vi/{vid}/mqdefault.jpg"
out.append(
_make_track(
vid, t.get("title", ""), artist,
duration=t.get("length") or 0, thumbnail=thumbnail,
artist_id=artist_id,
)
)
if len(out) >= limit:
break
return out
def main():
cmd = sys.argv[1] if len(sys.argv) > 1 else ""
if cmd == "browse":
bid = sys.argv[2] if len(sys.argv) > 2 else ""
limit = int(sys.argv[3]) if len(sys.argv) > 3 else 50
kind = sys.argv[4] if len(sys.argv) > 4 else None
print(json.dumps(browse(bid, limit, kind)))
return
if cmd == "artist_page":
bid = sys.argv[2] if len(sys.argv) > 2 else ""
print(json.dumps(artist_page(bid)))
return
if cmd == "watch":
video_id = sys.argv[2] if len(sys.argv) > 2 and sys.argv[2] else None
playlist_id = sys.argv[3] if len(sys.argv) > 3 and sys.argv[3] else None
limit = int(sys.argv[4]) if len(sys.argv) > 4 else 50
print(json.dumps(watch_playlist(video_id, playlist_id, True, limit)))
return
query = sys.argv[2] if len(sys.argv) > 2 else ""
scope = sys.argv[3] if len(sys.argv) > 3 else "all"
limit = int(sys.argv[4]) if len(sys.argv) > 4 else 20
print(json.dumps(search(query, scope, limit)))
if __name__ == "__main__":
main()