File indexing completed on 2024-05-12 16:21:19
0001 // SPDX-FileCopyrightText: 2021 Jonah BrĂ¼chert <jbb@kaidan.im> 0002 // 0003 // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0004 0005 #include "ytmusic.h" 0006 0007 #include <algorithm> 0008 #include <iostream> 0009 0010 #include <pybind11/embed.h> 0011 #include <pybind11/stl.h> 0012 0013 namespace py = pybind11; 0014 0015 using namespace py::literals; 0016 0017 /// Useful for debugging 0018 void pyPrintPretty(py::handle obj) { 0019 auto json = py::module::import("json"); 0020 py::print(json.attr("dumps")(obj, "indent"_a=py::int_(4))); 0021 } 0022 0023 #ifdef WIN32 0024 #define UNEXPORT __attribute__ ((visibility("hidden"))) 0025 #else 0026 #define UNEXPORT 0027 #endif 0028 0029 struct UNEXPORT YTMusicPrivate { 0030 py::scoped_interpreter guard {}; 0031 0032 std::optional<std::string> auth; 0033 std::optional<std::string> user; 0034 std::optional<bool> requests_session; 0035 std::optional<std::map<std::string, std::string> > proxies; 0036 std::string language; 0037 0038 py::object get_ytmusic() { 0039 if (ytmusic.is_none()) { 0040 ytmusicapi_module = py::module::import("ytmusicapi"); 0041 ytmusic = ytmusicapi_module.attr("YTMusic")(auth, user, requests_session, proxies, language); 0042 0043 auto oldVersion = ytmusicapi_module.attr("__dict__").contains("_version"); 0044 if (oldVersion) { 0045 std::cerr << "Running with outdated and untested version of ytmusicapi." << std::endl; 0046 std::cerr << "The currently tested and supported version is " << TESTED_YTMUSICAPI_VERSION << std::endl; 0047 } else { 0048 const auto version = ytmusicapi_module.attr("__version__").cast<std::string>(); 0049 if (version != TESTED_YTMUSICAPI_VERSION) { 0050 std::cerr << "Running with untested version of ytmusicapi " << version << "." << std::endl; 0051 std::cerr << "The currently tested and supported version is " << TESTED_YTMUSICAPI_VERSION << std::endl; 0052 } 0053 } 0054 } 0055 0056 return ytmusic; 0057 } 0058 0059 py::object get_ytdl() { 0060 // lazy initialization 0061 if (ytdl.is_none()) { 0062 ytdl = py::module::import("yt_dlp").attr("YoutubeDL")(py::dict()); 0063 } 0064 0065 return ytdl; 0066 } 0067 0068 public: 0069 py::module ytmusicapi_module; 0070 0071 private: 0072 py::object ytmusic = py::none(); 0073 py::object ytdl = py::none(); 0074 }; 0075 0076 template <typename T> 0077 std::optional<T> optional_key(py::handle obj, const char *name) { 0078 if (!obj.cast<py::dict>().contains(name)) { 0079 return std::nullopt; 0080 } 0081 0082 return obj[name].cast<std::optional<T>>(); 0083 } 0084 0085 meta::Thumbnail extract_thumbnail(py::handle thumbnail) { 0086 return { 0087 thumbnail["url"].cast<std::string>(), 0088 thumbnail["width"].cast<int>(), 0089 thumbnail["height"].cast<int>() 0090 }; 0091 } 0092 0093 meta::Artist extract_meta_artist(py::handle artist) { 0094 return { 0095 artist["name"].cast<std::string>(), 0096 artist["id"].cast<std::optional<std::string>>() 0097 }; 0098 }; 0099 0100 playlist::Track extract_playlist_track(py::handle track); 0101 watch::Playlist::Track extract_watch_track(py::handle track); 0102 album::Track extract_album_track(py::handle track); 0103 video_info::Format extract_format(py::handle format); 0104 0105 template <typename T> 0106 inline auto extract_py_list(py::handle obj) { 0107 if (obj.is_none()) { 0108 return std::vector<T>(); 0109 } 0110 0111 const auto list = obj.cast<py::list>(); 0112 std::vector<T> output; 0113 0114 std::transform(list.begin(), list.end(), std::back_inserter(output), [](py::handle item) { 0115 if constexpr(std::is_same_v<T, meta::Thumbnail>) { 0116 return extract_thumbnail(item); 0117 } else if constexpr(std::is_same_v<T, meta::Artist>) { 0118 return extract_meta_artist(item); 0119 } else if constexpr(std::is_same_v<T, album::Track>) { 0120 return extract_album_track(item); 0121 } else if constexpr(std::is_same_v<T, playlist::Track>) { 0122 return extract_playlist_track(item); 0123 } else if constexpr(std::is_same_v<T, video_info::Format>) { 0124 return extract_format(item); 0125 } else if constexpr(std::is_same_v<T, watch::Playlist::Track>) { 0126 return extract_watch_track(item); 0127 } else { 0128 return item.cast<T>(); 0129 } 0130 }); 0131 0132 return output; 0133 } 0134 0135 album::Track extract_album_track(py::handle track) { 0136 return { 0137 optional_key<bool>(track, "isExplicit"), 0138 track["title"].cast<std::string>(), 0139 extract_py_list<meta::Artist>(track["artists"]), 0140 track["album"].cast<std::optional<std::string>>(), 0141 track["videoId"].cast<std::optional<std::string>>(), // E rated songs don't have a videoId 0142 track["duration"].cast<std::optional<std::string>>(), // 0143 track["likeStatus"].cast<std::optional<std::string>>() 0144 }; 0145 } 0146 0147 video_info::Format extract_format(py::handle format) { 0148 return { 0149 optional_key<float>(format, "quality"), 0150 format["url"].cast<std::string>(), 0151 format["vcodec"].cast<std::string>(), 0152 optional_key<std::string>(format, "acodec").value_or("none") // returned inconsistently by yt-dlp 0153 }; 0154 } 0155 0156 meta::Album extract_meta_album(py::handle album) { 0157 return meta::Album { 0158 album["name"].cast<std::string>(), 0159 album["id"].cast<std::optional<std::string>>() 0160 }; 0161 } 0162 0163 watch::Playlist::Track extract_watch_track(py::handle track) { 0164 return { 0165 track["title"].cast<std::string>(), 0166 track["length"].cast<std::optional<std::string>>(), 0167 track["videoId"].cast<std::string>(), 0168 optional_key<std::string>(track, "playlistId"), 0169 extract_py_list<meta::Thumbnail>(track["thumbnail"]), 0170 track["likeStatus"].cast<std::optional<std::string>>(), 0171 extract_py_list<meta::Artist>(track["artists"]), 0172 [&]() -> std::optional<meta::Album> { 0173 if (!track.cast<py::dict>().contains("album")) { 0174 return std::nullopt; 0175 } 0176 0177 if (track["album"].is_none()) { 0178 return std::nullopt; 0179 } 0180 0181 return extract_meta_album(track["album"]); 0182 }() 0183 }; 0184 } 0185 0186 0187 playlist::Track extract_playlist_track(py::handle track) { 0188 return { 0189 track["videoId"].cast<std::optional<std::string>>(), 0190 track["title"].cast<std::string>(), 0191 extract_py_list<meta::Artist>(track["artists"]), 0192 [&]() -> std::optional<meta::Album> { 0193 if (track["album"].is_none()) { 0194 return std::nullopt; 0195 } 0196 0197 return extract_meta_album(track["album"]); 0198 }(), 0199 optional_key<std::string>(track, "duration"), 0200 track["likeStatus"].cast<std::optional<std::string>>(), 0201 extract_py_list<meta::Thumbnail>(track["thumbnails"]), 0202 track["isAvailable"].cast<bool>(), 0203 optional_key<bool>(track, "isExplicit") 0204 }; 0205 } 0206 0207 artist::Artist::Song::Album extract_song_album(py::handle album) { 0208 if (album.is_none() || album["id"].is_none()) { 0209 return {}; 0210 } 0211 return { 0212 album["name"].cast<std::string>(), 0213 album["id"].cast<std::string>() 0214 }; 0215 }; 0216 0217 template <typename T> 0218 auto extract_artist_section_results(py::handle section) { 0219 if (!section.cast<py::dict>().contains("results")) { 0220 return std::vector<T>(); 0221 } 0222 0223 const py::list py_results = section["results"]; 0224 std::vector<T> results; 0225 std::transform(py_results.begin(), py_results.end(), std::back_inserter(results), [](py::handle result) { 0226 if constexpr(std::is_same_v<T, artist::Artist::Song>) { 0227 return artist::Artist::Song { 0228 result["videoId"].cast<std::string>(), 0229 result["title"].cast<std::string>(), 0230 extract_py_list<meta::Thumbnail>(result["thumbnails"]), 0231 extract_py_list<meta::Artist>(result["artists"]), 0232 extract_song_album(result["album"]) 0233 }; 0234 } else if constexpr(std::is_same_v<T, artist::Artist::Album>) { 0235 return artist::Artist::Album { 0236 result["title"].cast<std::string>(), 0237 extract_py_list<meta::Thumbnail>(result["thumbnails"]), 0238 result["year"].cast<std::optional<std::string>>(), 0239 result["browseId"].cast<std::string>(), 0240 std::nullopt 0241 }; 0242 } else if constexpr(std::is_same_v<T, artist::Artist::Single>) { 0243 return artist::Artist::Single { 0244 result["title"].cast<std::string>(), 0245 extract_py_list<meta::Thumbnail>(result["thumbnails"]), 0246 result["year"].cast<std::string>(), 0247 result["browseId"].cast<std::string>() 0248 }; 0249 } else if constexpr(std::is_same_v<T, artist::Artist::Video>) { 0250 return artist::Artist::Video { 0251 result["title"].cast<std::string>(), 0252 extract_py_list<meta::Thumbnail>(result["thumbnails"]), 0253 optional_key<std::string>(result, "views"), 0254 result["videoId"].cast<std::string>(), 0255 result["playlistId"].cast<std::string>() 0256 }; 0257 } else { 0258 Py_UNREACHABLE(); 0259 } 0260 }); 0261 0262 return results; 0263 } 0264 0265 template<typename T> 0266 std::optional<artist::Artist::Section<T>> extract_artist_section(py::handle artist, const char* name) { 0267 if (artist.cast<py::dict>().contains(name)) { 0268 const auto section = artist[name]; 0269 return artist::Artist::Section<T> { 0270 section["browseId"].cast<std::optional<std::string>>(), 0271 extract_artist_section_results<T>(section), 0272 optional_key<std::string>(section, "params") 0273 }; 0274 } else { 0275 return std::nullopt; 0276 } 0277 } 0278 0279 std::optional<search::SearchResultItem> extract_search_result(py::handle result) { 0280 const auto resultType = result["resultType"].cast<std::string>(); 0281 0282 if (result["category"].cast<std::optional<std::string>>() == "Top result") { 0283 return search::TopResult { 0284 result["category"].cast<std::string>(), 0285 result["resultType"].cast<std::string>(), 0286 optional_key<std::string>(result, "videoId"), 0287 optional_key<std::string>(result, "title"), 0288 extract_py_list<meta::Artist>(result["artists"]), 0289 extract_py_list<meta::Thumbnail>(result["thumbnails"]) 0290 }; 0291 } 0292 0293 if (resultType == "video") { 0294 return search::Video { 0295 { 0296 result["videoId"].cast<std::string>(), 0297 result["title"].cast<std::string>(), 0298 extract_py_list<meta::Artist>(result["artists"]), 0299 optional_key<std::string>(result, "duration"), 0300 extract_py_list<meta::Thumbnail>(result["thumbnails"]) 0301 }, 0302 optional_key<std::string>(result, "views") 0303 }; 0304 } else if (resultType == "song") { 0305 return search::Song { 0306 { 0307 result["videoId"].cast<std::string>(), 0308 result["title"].cast<std::string>(), 0309 extract_py_list<meta::Artist>(result["artists"]), 0310 optional_key<std::string>(result, "duration"), 0311 extract_py_list<meta::Thumbnail>(result["thumbnails"]) 0312 }, 0313 result["album"].is_none() ? std::optional<meta::Album> {} : extract_meta_album(result["album"]), 0314 optional_key<bool>(result, "isExplicit") 0315 }; 0316 } else if (resultType == "album") { 0317 return search::Album { 0318 result["browseId"].cast<std::optional<std::string>>(), 0319 result["title"].cast<std::string>(), 0320 result["type"].cast<std::string>(), 0321 extract_py_list<meta::Artist>(result["artists"]), 0322 result["year"].cast<std::optional<std::string>>(), 0323 result["isExplicit"].cast<bool>(), 0324 extract_py_list<meta::Thumbnail>(result["thumbnails"]) 0325 }; 0326 } else if (resultType == "playlist") { 0327 return search::Playlist { 0328 result["browseId"].cast<std::string>(), 0329 result["title"].cast<std::string>(), 0330 result["author"].cast<std::optional<std::string>>(), 0331 result["itemCount"].cast<std::string>(), 0332 extract_py_list<meta::Thumbnail>(result["thumbnails"]) 0333 }; 0334 } else if (resultType == "artist") { 0335 return search::Artist { 0336 result["browseId"].cast<std::string>(), 0337 result["artist"].cast<std::string>(), 0338 optional_key<std::string>(result, "shuffleId"), 0339 optional_key<std::string>(result, "radioId"), 0340 extract_py_list<meta::Thumbnail>(result["thumbnails"]) 0341 }; 0342 } else { 0343 std::cerr << "Warning: Unsupported search result type found" << std::endl; 0344 std::cerr << "It's called: " << resultType << std::endl; 0345 pyPrintPretty(result); 0346 return std::nullopt; 0347 } 0348 } 0349 0350 YTMusic::YTMusic( 0351 const std::optional<std::string> &auth, 0352 const std::optional<std::string> &user, 0353 const std::optional<bool> requests_session, 0354 const std::optional<std::map<std::string, std::string> > &proxies, 0355 const std::string &language) 0356 : d(std::make_unique<YTMusicPrivate>()) 0357 { 0358 d->auth = auth; 0359 d->user = user; 0360 d->requests_session = requests_session; 0361 d->proxies = proxies; 0362 d->language = language; 0363 } 0364 0365 YTMusic::~YTMusic() = default; 0366 0367 std::vector<search::SearchResultItem> YTMusic::search( 0368 const std::string &query, 0369 const std::optional<std::string> &filter, 0370 const std::optional<std::string> &scope, 0371 const int limit, 0372 const bool ignore_spelling) const 0373 { 0374 const auto results = d->get_ytmusic().attr("search")("query"_a=query, "filter"_a=filter, "scope"_a=scope, "limit"_a = limit, "ignore_spelling"_a = ignore_spelling).cast<py::list>(); 0375 0376 std::vector<search::SearchResultItem> output; 0377 for (const auto &result : results) { 0378 if (result.is_none()) { 0379 continue; 0380 } 0381 0382 try { 0383 if (const auto opt = extract_search_result(result); opt.has_value()) { 0384 output.push_back(opt.value()); 0385 } 0386 } catch (const std::exception &e) { 0387 std::cerr << "Failed to parse search result because:" << e.what(); 0388 } 0389 }; 0390 0391 return output; 0392 } 0393 0394 artist::Artist YTMusic::get_artist(const std::string &channel_id) const 0395 { 0396 const auto artist = d->get_ytmusic().attr("get_artist")(channel_id); 0397 return artist::Artist { 0398 optional_key<std::string>(artist, "description"), 0399 artist["views"].cast<std::optional<std::string>>(), 0400 artist["name"].cast<std::string>(), 0401 artist["channelId"].cast<std::string>(), 0402 artist["subscribers"].cast<std::optional<std::string>>(), 0403 artist["subscribed"].cast<bool>(), 0404 extract_py_list<meta::Thumbnail>(artist["thumbnails"]), 0405 extract_artist_section<artist::Artist::Song>(artist, "songs"), 0406 extract_artist_section<artist::Artist::Album>(artist, "albums"), 0407 extract_artist_section<artist::Artist::Single>(artist, "singles"), 0408 extract_artist_section<artist::Artist::Video>(artist, "videos"), 0409 }; 0410 } 0411 0412 album::Album YTMusic::get_album(const std::string &browseId) const 0413 { 0414 const auto album = d->get_ytmusic().attr("get_album")(browseId); 0415 return { 0416 album["title"].cast<std::string>(), 0417 album["trackCount"].cast<int>(), 0418 album["duration"].cast<std::string>(), 0419 album["audioPlaylistId"].cast<std::string>(), 0420 optional_key<std::string>(album, "year"), 0421 optional_key<std::string>(album, "description"), 0422 extract_py_list<meta::Thumbnail>(album["thumbnails"]), 0423 extract_py_list<album::Track>(album["tracks"]), 0424 extract_py_list<meta::Artist>(album["artists"]) 0425 }; 0426 } 0427 0428 std::optional<song::Song> YTMusic::get_song(const std::string &video_id) const 0429 { 0430 const auto song = d->get_ytmusic().attr("get_song")(video_id); 0431 auto videoDetails = song["videoDetails"].cast<py::dict>(); 0432 0433 if (!videoDetails.contains("videoId")) { 0434 return std::nullopt; 0435 } 0436 0437 return song::Song { 0438 videoDetails["videoId"].cast<std::string>(), 0439 videoDetails["title"].cast<std::string>(), 0440 videoDetails["lengthSeconds"].cast<std::string>(), 0441 videoDetails["channelId"].cast<std::string>(), 0442 videoDetails["isOwnerViewing"].cast<bool>(), 0443 videoDetails["isCrawlable"].cast<bool>(), 0444 song::Song::Thumbnail { 0445 extract_py_list<meta::Thumbnail>(videoDetails["thumbnail"]["thumbnails"]) 0446 }, 0447 videoDetails["viewCount"].cast<std::string>(), 0448 videoDetails["author"].cast<std::string>(), 0449 videoDetails["isPrivate"].cast<bool>(), 0450 videoDetails["isUnpluggedCorpus"].cast<bool>(), 0451 videoDetails["isLiveContent"].cast<bool>(), 0452 [&]() -> std::vector<std::string> { 0453 if (videoDetails.contains("artists")) { 0454 return extract_py_list<std::string>(videoDetails["artists"]); 0455 } else { 0456 return {}; 0457 } 0458 }(), 0459 }; 0460 } 0461 0462 playlist::Playlist YTMusic::get_playlist(const std::string &playlist_id, int limit) const 0463 { 0464 const auto playlist = d->get_ytmusic().attr("get_playlist")(playlist_id, limit); 0465 0466 return { 0467 playlist["id"].cast<std::string>(), 0468 playlist["privacy"].cast<std::string>(), 0469 playlist["title"].cast<std::string>(), 0470 extract_py_list<meta::Thumbnail>(playlist["thumbnails"]), 0471 extract_meta_artist(playlist["author"]), 0472 optional_key<std::string>(playlist, "year"), 0473 playlist["duration"].cast<std::string>(), 0474 playlist["trackCount"].cast<int>(), 0475 extract_py_list<playlist::Track>(playlist["tracks"]), 0476 }; 0477 } 0478 0479 std::vector<artist::Artist::Album> YTMusic::get_artist_albums(const std::string &channel_id, const std::string ¶ms) const 0480 { 0481 const auto py_albums = d->get_ytmusic().attr("get_artist_albums")(channel_id, params); 0482 std::vector<artist::Artist::Album> albums; 0483 0484 std::transform(py_albums.begin(), py_albums.end(), std::back_inserter(albums), [](py::handle album) { 0485 return artist::Artist::Album { 0486 album["title"].cast<std::string>(), 0487 extract_py_list<meta::Thumbnail>(album["thumbnails"]), 0488 album["year"].cast<std::string>(), 0489 album["browseId"].cast<std::string>(), 0490 album["type"].cast<std::string>() 0491 }; 0492 }); 0493 0494 return albums; 0495 } 0496 0497 video_info::VideoInfo YTMusic::extract_video_info(const std::string &video_id) const 0498 { 0499 using namespace pybind11::literals; 0500 0501 const auto info = d->get_ytdl().attr("extract_info")(video_id, "download"_a=py::bool_(false)); 0502 0503 return { 0504 info["id"].cast<std::string>(), 0505 info["title"].cast<std::string>(), 0506 info.contains("artist") ? info["artist"].cast<std::string>() : "", 0507 info.contains("channel") ? info["channel"].cast<std::string>() : "", 0508 extract_py_list<video_info::Format>(info["formats"]), 0509 info["thumbnail"].cast<std::string>() 0510 }; 0511 } 0512 0513 watch::Playlist YTMusic::get_watch_playlist(const std::optional<std::string> &videoId, 0514 const std::optional<std::string> &playlistId, 0515 int limit) const 0516 { 0517 const auto playlist = d->get_ytmusic().attr("get_watch_playlist")("videoId"_a = videoId, 0518 "playlistId"_a = playlistId, 0519 "limit"_a = py::int_(limit)); 0520 0521 return { 0522 extract_py_list<watch::Playlist::Track>(playlist["tracks"]), 0523 playlist["lyrics"].cast<std::optional<std::string>>() 0524 }; 0525 } 0526 0527 0528 Lyrics YTMusic::get_lyrics(const std::string &browse_id) const 0529 { 0530 auto lyrics = d->get_ytmusic().attr("get_lyrics")(browse_id); 0531 0532 return { 0533 lyrics["source"].cast<std::optional<std::string>>(), 0534 lyrics["lyrics"].cast<std::string>() 0535 }; 0536 } 0537 0538 std::string YTMusic::get_version() const 0539 { 0540 d->get_ytmusic(); 0541 return d->ytmusicapi_module.attr("__version__").cast<std::string>(); 0542 }