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 &params) 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 }